diff --git a/.github/workflows/docs-antora.yml b/.github/workflows/docs-antora.yml
new file mode 100644
index 0000000..96036b2
--- /dev/null
+++ b/.github/workflows/docs-antora.yml
@@ -0,0 +1,34 @@
+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: '20'
+
+ - 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
diff --git a/.gitignore b/.gitignore
index aa5cc25..6986eee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,4 +19,6 @@ OliveTin
integration-tests/configs/authRequireGuestsToLogin/sessions.yaml
webui
webui.dev
-sessions.yaml
\ No newline at end of file
+sessions.yaml
+docs/build/
+build/
\ No newline at end of file
diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc
index e0f0672..40ac7da 100644
--- a/CONTRIBUTING.adoc
+++ b/CONTRIBUTING.adoc
@@ -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.
diff --git a/README.md b/README.md
index 792d035..a1c8a9d 100644
--- a/README.md
+++ b/README.md
@@ -20,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;
diff --git a/docs/antora.yml b/docs/antora.yml
new file mode 100644
index 0000000..ec27f39
--- /dev/null
+++ b/docs/antora.yml
@@ -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
+
+
diff --git a/docs/modules/ROOT/check_chevron_links.py b/docs/modules/ROOT/check_chevron_links.py
new file mode 100755
index 0000000..b7683f9
--- /dev/null
+++ b/docs/modules/ROOT/check_chevron_links.py
@@ -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)
diff --git a/docs/modules/ROOT/check_no_h1.py b/docs/modules/ROOT/check_no_h1.py
new file mode 100755
index 0000000..04319b1
--- /dev/null
+++ b/docs/modules/ROOT/check_no_h1.py
@@ -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)
+
diff --git a/docs/modules/ROOT/check_unnavigable.py b/docs/modules/ROOT/check_unnavigable.py
new file mode 100755
index 0000000..826f550
--- /dev/null
+++ b/docs/modules/ROOT/check_unnavigable.py
@@ -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)
diff --git a/docs/modules/ROOT/examples/action_customization/icons/config.yaml b/docs/modules/ROOT/examples/action_customization/icons/config.yaml
new file mode 100644
index 0000000..13028dc
--- /dev/null
+++ b/docs/modules/ROOT/examples/action_customization/icons/config.yaml
@@ -0,0 +1,15 @@
+actions:
+ - title: Unicode (emoji) alias icon
+ shell: echo "Hello!"
+ icon: smile
+
+ - title: Unicode (emoji) icon
+ shell: echo "Hello!"
+ icon: "😎"
+
+ - title: Iconify Icon
+ icon:
+
+ - title: HTML Image (jpg/png/gif/etc) icon
+ shell: echo "Hello!"
+ icon: '
'
diff --git a/docs/modules/ROOT/examples/k8s_configmap.yml b/docs/modules/ROOT/examples/k8s_configmap.yml
new file mode 100644
index 0000000..15131cf
--- /dev/null
+++ b/docs/modules/ROOT/examples/k8s_configmap.yml
@@ -0,0 +1,9 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: olivetin-config
+data:
+ config.yaml: |
+ actions:
+ - title: "Hello world!"
+ shell: echo 'Hello World!'
diff --git a/docs/modules/ROOT/examples/k8s_deployment.yml b/docs/modules/ROOT/examples/k8s_deployment.yml
new file mode 100644
index 0000000..0e6756d
--- /dev/null
+++ b/docs/modules/ROOT/examples/k8s_deployment.yml
@@ -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
+
diff --git a/docs/modules/ROOT/examples/k8s_ingress.yml b/docs/modules/ROOT/examples/k8s_ingress.yml
new file mode 100644
index 0000000..af03a40
--- /dev/null
+++ b/docs/modules/ROOT/examples/k8s_ingress.yml
@@ -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
diff --git a/docs/modules/ROOT/examples/reverse-proxies/etc/npm-docker-compose.yml b/docs/modules/ROOT/examples/reverse-proxies/etc/npm-docker-compose.yml
new file mode 100644
index 0000000..c49c4e7
--- /dev/null
+++ b/docs/modules/ROOT/examples/reverse-proxies/etc/npm-docker-compose.yml
@@ -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
diff --git a/docs/modules/ROOT/examples/reverse-proxies/etc/reverse_proxy_nginx_dns.conf b/docs/modules/ROOT/examples/reverse-proxies/etc/reverse_proxy_nginx_dns.conf
new file mode 100644
index 0000000..3023d7a
--- /dev/null
+++ b/docs/modules/ROOT/examples/reverse-proxies/etc/reverse_proxy_nginx_dns.conf
@@ -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;
+ }
+}
+
diff --git a/docs/modules/ROOT/examples/solutions/container-control-panel/config/config.yaml b/docs/modules/ROOT/examples/solutions/container-control-panel/config/config.yaml
new file mode 100644
index 0000000..5e8ff4d
--- /dev/null
+++ b/docs/modules/ROOT/examples/solutions/container-control-panel/config/config.yaml
@@ -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 }}
{{ container.State }}
+ - title: 'Start {{ container.Names }}'
+ - title: 'Stop {{ container.Names }}'
diff --git a/docs/modules/ROOT/examples/solutions/container-control-panel/config/containers.json b/docs/modules/ROOT/examples/solutions/container-control-panel/config/containers.json
new file mode 100644
index 0000000..fc6f592
--- /dev/null
+++ b/docs/modules/ROOT/examples/solutions/container-control-panel/config/containers.json
@@ -0,0 +1,2 @@
+{"Command":"\"/bin/bash\"","CreatedAt":"2024-02-28 22:33:35 +0000 GMT","ID":"fcf468e18a0e","Image":"fedora","Labels":"maintainer=Clement Verna \u003ccverna@fedoraproject.org\u003e","LocalVolumes":"0","Mounts":"","Names":"minecraft","Networks":"bridge","Ports":"","RunningFor":"3 minutes ago","Size":"0B","State":"created","Status":"Created"}
+{"Command":"\"/bin/bash\"","CreatedAt":"2024-02-23 23:18:57 +0000 GMT","ID":"442dd6fe316a","Image":"fedora","Labels":"maintainer=Clement Verna \u003ccverna@fedoraproject.org\u003e","LocalVolumes":"0","Mounts":"","Names":"brave_shirley","Networks":"bridge","Ports":"","RunningFor":"4 days ago","Size":"0B","State":"created","Status":"Created"}
diff --git a/docs/modules/ROOT/examples/solutions/directory-actions/config.yaml b/docs/modules/ROOT/examples/solutions/directory-actions/config.yaml
new file mode 100644
index 0000000..436d265
--- /dev/null
+++ b/docs/modules/ROOT/examples/solutions/directory-actions/config.yaml
@@ -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)
diff --git a/docs/modules/ROOT/examples/solutions/heating-control-panel/configs/config.yaml b/docs/modules/ROOT/examples/solutions/heating-control-panel/configs/config.yaml
new file mode 100644
index 0000000..a84130b
--- /dev/null
+++ b/docs/modules/ROOT/examples/solutions/heating-control-panel/configs/config.yaml
@@ -0,0 +1,28 @@
+logLevel: "INFO"
+
+actions:
+ - title: Turn heating up
+ icon: '🔼'
+ shell: /opt/heating.sh up
+
+ - title: Turn heating down
+ icon: '🔽'
+ 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: |
+ 🌡
{{ heating.temperature }}
+
+ - title: Turn heating up
+ - title: Turn heating down
diff --git a/docs/modules/ROOT/examples/solutions/heating-control-panel/configs/heating.yaml b/docs/modules/ROOT/examples/solutions/heating-control-panel/configs/heating.yaml
new file mode 100644
index 0000000..5d8aebb
--- /dev/null
+++ b/docs/modules/ROOT/examples/solutions/heating-control-panel/configs/heating.yaml
@@ -0,0 +1,2 @@
+- title: Main heater
+ temperature: 20 degrees
diff --git a/docs/modules/ROOT/examples/solutions/primitive-password/password.js b/docs/modules/ROOT/examples/solutions/primitive-password/password.js
new file mode 100644
index 0000000..64cef31
--- /dev/null
+++ b/docs/modules/ROOT/examples/solutions/primitive-password/password.js
@@ -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)
diff --git a/docs/modules/ROOT/examples/solutions/systemd-control-panel/config/config.yaml b/docs/modules/ROOT/examples/solutions/systemd-control-panel/config/config.yaml
new file mode 100644
index 0000000..6fe5749
--- /dev/null
+++ b/docs/modules/ROOT/examples/solutions/systemd-control-panel/config/config.yaml
@@ -0,0 +1,36 @@
+actions:
+ - title: Stop {{ systemd_unit.unit }}
+ shell: systemctl stop {{ systemd_unit.unit }}
+ icon:
+ entity: systemd_unit
+ triggers:
+ - Update services file
+
+ - title: Start {{ systemd_unit.unit }}
+ shell: systemctl start {{ systemd_unit.unit }}
+ 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 }}
diff --git a/docs/modules/ROOT/examples/solutions/systemd-control-panel/config/systemd_units.json b/docs/modules/ROOT/examples/solutions/systemd-control-panel/config/systemd_units.json
new file mode 100644
index 0000000..00a0022
--- /dev/null
+++ b/docs/modules/ROOT/examples/solutions/systemd-control-panel/config/systemd_units.json
@@ -0,0 +1,4 @@
+{"unit":"boot.mount","load":"loaded","active":"active","sub":"mounted","description":"/boot"}
+{"unit":"podman.service","load":"loaded","active":"inactive","sub":"dead","description":"Podman API Service"}
+{"unit":"upsilon-drone.service","load":"loaded","active":"active","sub":"running","description":"upsilon-drone"}
+{"unit":"podman.socket","load":"loaded","active":"active","sub":"listening","description":"Podman API Socket"}
diff --git a/docs/modules/ROOT/examples/solutions/wol/config.yaml b/docs/modules/ROOT/examples/solutions/wol/config.yaml
new file mode 100644
index 0000000..ab38c02
--- /dev/null
+++ b/docs/modules/ROOT/examples/solutions/wol/config.yaml
@@ -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
diff --git a/docs/modules/ROOT/examples/solutions/wol/config_docker.yaml b/docs/modules/ROOT/examples/solutions/wol/config_docker.yaml
new file mode 100644
index 0000000..fc37b89
--- /dev/null
+++ b/docs/modules/ROOT/examples/solutions/wol/config_docker.yaml
@@ -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
diff --git a/docs/modules/ROOT/images/action-button-iconify.png b/docs/modules/ROOT/images/action-button-iconify.png
new file mode 100644
index 0000000..94239d8
Binary files /dev/null and b/docs/modules/ROOT/images/action-button-iconify.png differ
diff --git a/docs/modules/ROOT/images/action-confirmation.png b/docs/modules/ROOT/images/action-confirmation.png
new file mode 100644
index 0000000..d681c03
Binary files /dev/null and b/docs/modules/ROOT/images/action-confirmation.png differ
diff --git a/docs/modules/ROOT/images/additionalNavigationLinks.png b/docs/modules/ROOT/images/additionalNavigationLinks.png
new file mode 100644
index 0000000..012a928
Binary files /dev/null and b/docs/modules/ROOT/images/additionalNavigationLinks.png differ
diff --git a/docs/modules/ROOT/images/arg-datetime.png b/docs/modules/ROOT/images/arg-datetime.png
new file mode 100644
index 0000000..f25158a
Binary files /dev/null and b/docs/modules/ROOT/images/arg-datetime.png differ
diff --git a/docs/modules/ROOT/images/arg-suggestions-chrome.png b/docs/modules/ROOT/images/arg-suggestions-chrome.png
new file mode 100644
index 0000000..adb369b
Binary files /dev/null and b/docs/modules/ROOT/images/arg-suggestions-chrome.png differ
diff --git a/docs/modules/ROOT/images/arg-suggestions-firefox.png b/docs/modules/ROOT/images/arg-suggestions-firefox.png
new file mode 100644
index 0000000..f044d47
Binary files /dev/null and b/docs/modules/ROOT/images/arg-suggestions-firefox.png differ
diff --git a/docs/modules/ROOT/images/args-choices-entities.png b/docs/modules/ROOT/images/args-choices-entities.png
new file mode 100644
index 0000000..77a7ea4
Binary files /dev/null and b/docs/modules/ROOT/images/args-choices-entities.png differ
diff --git a/docs/modules/ROOT/images/args-choices-exec.png b/docs/modules/ROOT/images/args-choices-exec.png
new file mode 100644
index 0000000..aeca1d5
Binary files /dev/null and b/docs/modules/ROOT/images/args-choices-exec.png differ
diff --git a/docs/modules/ROOT/images/args-multiline-text.png b/docs/modules/ROOT/images/args-multiline-text.png
new file mode 100644
index 0000000..02ac610
Binary files /dev/null and b/docs/modules/ROOT/images/args-multiline-text.png differ
diff --git a/docs/modules/ROOT/images/args1.png b/docs/modules/ROOT/images/args1.png
new file mode 100644
index 0000000..035595f
Binary files /dev/null and b/docs/modules/ROOT/images/args1.png differ
diff --git a/docs/modules/ROOT/images/args2.png b/docs/modules/ROOT/images/args2.png
new file mode 100644
index 0000000..0a5b8a4
Binary files /dev/null and b/docs/modules/ROOT/images/args2.png differ
diff --git a/docs/modules/ROOT/images/args3.png b/docs/modules/ROOT/images/args3.png
new file mode 100644
index 0000000..5425026
Binary files /dev/null and b/docs/modules/ROOT/images/args3.png differ
diff --git a/docs/modules/ROOT/images/args4.png b/docs/modules/ROOT/images/args4.png
new file mode 100644
index 0000000..33b893f
Binary files /dev/null and b/docs/modules/ROOT/images/args4.png differ
diff --git a/docs/modules/ROOT/images/authentik_login.png b/docs/modules/ROOT/images/authentik_login.png
new file mode 100644
index 0000000..28fafd0
Binary files /dev/null and b/docs/modules/ROOT/images/authentik_login.png differ
diff --git a/docs/modules/ROOT/images/authentik_login2.png b/docs/modules/ROOT/images/authentik_login2.png
new file mode 100644
index 0000000..df868e0
Binary files /dev/null and b/docs/modules/ROOT/images/authentik_login2.png differ
diff --git a/docs/modules/ROOT/images/authentik_login3.png b/docs/modules/ROOT/images/authentik_login3.png
new file mode 100644
index 0000000..9a9f411
Binary files /dev/null and b/docs/modules/ROOT/images/authentik_login3.png differ
diff --git a/docs/modules/ROOT/images/authentik_new_app.png b/docs/modules/ROOT/images/authentik_new_app.png
new file mode 100644
index 0000000..d6d0995
Binary files /dev/null and b/docs/modules/ROOT/images/authentik_new_app.png differ
diff --git a/docs/modules/ROOT/images/authentik_provider_config.png b/docs/modules/ROOT/images/authentik_provider_config.png
new file mode 100644
index 0000000..625cfb2
Binary files /dev/null and b/docs/modules/ROOT/images/authentik_provider_config.png differ
diff --git a/docs/modules/ROOT/images/authentik_provider_secrets.png b/docs/modules/ROOT/images/authentik_provider_secrets.png
new file mode 100644
index 0000000..a0901ab
Binary files /dev/null and b/docs/modules/ROOT/images/authentik_provider_secrets.png differ
diff --git a/docs/modules/ROOT/images/authentik_select_oauth2.png b/docs/modules/ROOT/images/authentik_select_oauth2.png
new file mode 100644
index 0000000..92a6605
Binary files /dev/null and b/docs/modules/ROOT/images/authentik_select_oauth2.png differ
diff --git a/docs/modules/ROOT/images/blocked.png b/docs/modules/ROOT/images/blocked.png
new file mode 100644
index 0000000..ea0b3b2
Binary files /dev/null and b/docs/modules/ROOT/images/blocked.png differ
diff --git a/docs/modules/ROOT/images/dashboard-display.png b/docs/modules/ROOT/images/dashboard-display.png
new file mode 100644
index 0000000..2554d7a
Binary files /dev/null and b/docs/modules/ROOT/images/dashboard-display.png differ
diff --git a/docs/modules/ROOT/images/dashboard-heating-control-panel.png b/docs/modules/ROOT/images/dashboard-heating-control-panel.png
new file mode 100644
index 0000000..2d09885
Binary files /dev/null and b/docs/modules/ROOT/images/dashboard-heating-control-panel.png differ
diff --git a/docs/modules/ROOT/images/dashboard.png b/docs/modules/ROOT/images/dashboard.png
new file mode 100644
index 0000000..1fe44d8
Binary files /dev/null and b/docs/modules/ROOT/images/dashboard.png differ
diff --git a/docs/modules/ROOT/images/defaultUiHideNav.png b/docs/modules/ROOT/images/defaultUiHideNav.png
new file mode 100644
index 0000000..07eaf35
Binary files /dev/null and b/docs/modules/ROOT/images/defaultUiHideNav.png differ
diff --git a/docs/modules/ROOT/images/defaultUiWithNav.png b/docs/modules/ROOT/images/defaultUiWithNav.png
new file mode 100644
index 0000000..16d1a49
Binary files /dev/null and b/docs/modules/ROOT/images/defaultUiWithNav.png differ
diff --git a/docs/modules/ROOT/images/diagnostics.png b/docs/modules/ROOT/images/diagnostics.png
new file mode 100644
index 0000000..4b216e7
Binary files /dev/null and b/docs/modules/ROOT/images/diagnostics.png differ
diff --git a/docs/modules/ROOT/images/directory-actions-screenshot.png b/docs/modules/ROOT/images/directory-actions-screenshot.png
new file mode 100644
index 0000000..3b80c49
Binary files /dev/null and b/docs/modules/ROOT/images/directory-actions-screenshot.png differ
diff --git a/docs/modules/ROOT/images/exampleIcons.png b/docs/modules/ROOT/images/exampleIcons.png
new file mode 100644
index 0000000..92ba1c3
Binary files /dev/null and b/docs/modules/ROOT/images/exampleIcons.png differ
diff --git a/docs/modules/ROOT/images/executionButtons.png b/docs/modules/ROOT/images/executionButtons.png
new file mode 100644
index 0000000..7a6f1d7
Binary files /dev/null and b/docs/modules/ROOT/images/executionButtons.png differ
diff --git a/docs/modules/ROOT/images/executionDialog.png b/docs/modules/ROOT/images/executionDialog.png
new file mode 100644
index 0000000..af7dfc3
Binary files /dev/null and b/docs/modules/ROOT/images/executionDialog.png differ
diff --git a/docs/modules/ROOT/images/fieldset.png b/docs/modules/ROOT/images/fieldset.png
new file mode 100644
index 0000000..cd9c0a5
Binary files /dev/null and b/docs/modules/ROOT/images/fieldset.png differ
diff --git a/docs/modules/ROOT/images/flashyButton.png b/docs/modules/ROOT/images/flashyButton.png
new file mode 100644
index 0000000..fb03e28
Binary files /dev/null and b/docs/modules/ROOT/images/flashyButton.png differ
diff --git a/docs/modules/ROOT/images/folders.png b/docs/modules/ROOT/images/folders.png
new file mode 100644
index 0000000..7a36ff9
Binary files /dev/null and b/docs/modules/ROOT/images/folders.png differ
diff --git a/docs/modules/ROOT/images/gitops.png b/docs/modules/ROOT/images/gitops.png
new file mode 100644
index 0000000..4288de6
Binary files /dev/null and b/docs/modules/ROOT/images/gitops.png differ
diff --git a/docs/modules/ROOT/images/hacs-custom-repo.png b/docs/modules/ROOT/images/hacs-custom-repo.png
new file mode 100644
index 0000000..a5e3dde
Binary files /dev/null and b/docs/modules/ROOT/images/hacs-custom-repo.png differ
diff --git a/docs/modules/ROOT/images/hacs-download.png b/docs/modules/ROOT/images/hacs-download.png
new file mode 100644
index 0000000..159515a
Binary files /dev/null and b/docs/modules/ROOT/images/hacs-download.png differ
diff --git a/docs/modules/ROOT/images/hacs-dropdown.png b/docs/modules/ROOT/images/hacs-dropdown.png
new file mode 100644
index 0000000..ddec470
Binary files /dev/null and b/docs/modules/ROOT/images/hacs-dropdown.png differ
diff --git a/docs/modules/ROOT/images/hacs-search.png b/docs/modules/ROOT/images/hacs-search.png
new file mode 100644
index 0000000..3fcafd8
Binary files /dev/null and b/docs/modules/ROOT/images/hacs-search.png differ
diff --git a/docs/modules/ROOT/images/hass-add-integration.png b/docs/modules/ROOT/images/hass-add-integration.png
new file mode 100644
index 0000000..46d5913
Binary files /dev/null and b/docs/modules/ROOT/images/hass-add-integration.png differ
diff --git a/docs/modules/ROOT/images/hass-buttons.png b/docs/modules/ROOT/images/hass-buttons.png
new file mode 100644
index 0000000..dad896b
Binary files /dev/null and b/docs/modules/ROOT/images/hass-buttons.png differ
diff --git a/docs/modules/ROOT/images/hass-configure-integration.png b/docs/modules/ROOT/images/hass-configure-integration.png
new file mode 100644
index 0000000..5876874
Binary files /dev/null and b/docs/modules/ROOT/images/hass-configure-integration.png differ
diff --git a/docs/modules/ROOT/images/hass-devices-and-services.png b/docs/modules/ROOT/images/hass-devices-and-services.png
new file mode 100644
index 0000000..0b45924
Binary files /dev/null and b/docs/modules/ROOT/images/hass-devices-and-services.png differ
diff --git a/docs/modules/ROOT/images/hassButtonSetup.png b/docs/modules/ROOT/images/hassButtonSetup.png
new file mode 100644
index 0000000..b590c84
Binary files /dev/null and b/docs/modules/ROOT/images/hassButtonSetup.png differ
diff --git a/docs/modules/ROOT/images/hassConfigYaml.png b/docs/modules/ROOT/images/hassConfigYaml.png
new file mode 100644
index 0000000..b7bcd89
Binary files /dev/null and b/docs/modules/ROOT/images/hassConfigYaml.png differ
diff --git a/docs/modules/ROOT/images/hassFileEditor.png b/docs/modules/ROOT/images/hassFileEditor.png
new file mode 100644
index 0000000..9608753
Binary files /dev/null and b/docs/modules/ROOT/images/hassFileEditor.png differ
diff --git a/docs/modules/ROOT/images/hassFileEditorConfig.png b/docs/modules/ROOT/images/hassFileEditorConfig.png
new file mode 100644
index 0000000..eeb3ecc
Binary files /dev/null and b/docs/modules/ROOT/images/hassFileEditorConfig.png differ
diff --git a/docs/modules/ROOT/images/hello-world.png b/docs/modules/ROOT/images/hello-world.png
new file mode 100644
index 0000000..ab8268a
Binary files /dev/null and b/docs/modules/ROOT/images/hello-world.png differ
diff --git a/docs/modules/ROOT/images/iconify.png b/docs/modules/ROOT/images/iconify.png
new file mode 100644
index 0000000..a1c73d9
Binary files /dev/null and b/docs/modules/ROOT/images/iconify.png differ
diff --git a/docs/modules/ROOT/images/icons/Discord.png b/docs/modules/ROOT/images/icons/Discord.png
new file mode 100644
index 0000000..db47845
Binary files /dev/null and b/docs/modules/ROOT/images/icons/Discord.png differ
diff --git a/docs/modules/ROOT/images/icons/GitHub.png b/docs/modules/ROOT/images/icons/GitHub.png
new file mode 100644
index 0000000..329d361
Binary files /dev/null and b/docs/modules/ROOT/images/icons/GitHub.png differ
diff --git a/docs/modules/ROOT/images/icons/OliveTinLogo.png b/docs/modules/ROOT/images/icons/OliveTinLogo.png
new file mode 100644
index 0000000..cfe1027
Binary files /dev/null and b/docs/modules/ROOT/images/icons/OliveTinLogo.png differ
diff --git a/docs/modules/ROOT/images/maxRate.png b/docs/modules/ROOT/images/maxRate.png
new file mode 100644
index 0000000..7721e2c
Binary files /dev/null and b/docs/modules/ROOT/images/maxRate.png differ
diff --git a/docs/modules/ROOT/images/mrGreenAction.png b/docs/modules/ROOT/images/mrGreenAction.png
new file mode 100644
index 0000000..cd211a6
Binary files /dev/null and b/docs/modules/ROOT/images/mrGreenAction.png differ
diff --git a/docs/modules/ROOT/images/mre.png b/docs/modules/ROOT/images/mre.png
new file mode 100644
index 0000000..21eb2a6
Binary files /dev/null and b/docs/modules/ROOT/images/mre.png differ
diff --git a/docs/modules/ROOT/images/mrgreen.gif b/docs/modules/ROOT/images/mrgreen.gif
new file mode 100644
index 0000000..636a01f
Binary files /dev/null and b/docs/modules/ROOT/images/mrgreen.gif differ
diff --git a/docs/modules/ROOT/images/npm.png b/docs/modules/ROOT/images/npm.png
new file mode 100644
index 0000000..5cde079
Binary files /dev/null and b/docs/modules/ROOT/images/npm.png differ
diff --git a/docs/modules/ROOT/images/page-title.png b/docs/modules/ROOT/images/page-title.png
new file mode 100644
index 0000000..12af3be
Binary files /dev/null and b/docs/modules/ROOT/images/page-title.png differ
diff --git a/docs/modules/ROOT/images/pocketid.png b/docs/modules/ROOT/images/pocketid.png
new file mode 100644
index 0000000..e846e06
Binary files /dev/null and b/docs/modules/ROOT/images/pocketid.png differ
diff --git a/docs/modules/ROOT/images/popupOutputOnly.png b/docs/modules/ROOT/images/popupOutputOnly.png
new file mode 100644
index 0000000..69feb03
Binary files /dev/null and b/docs/modules/ROOT/images/popupOutputOnly.png differ
diff --git a/docs/modules/ROOT/images/portDiagram.png b/docs/modules/ROOT/images/portDiagram.png
new file mode 100644
index 0000000..ae1b527
Binary files /dev/null and b/docs/modules/ROOT/images/portDiagram.png differ
diff --git a/docs/modules/ROOT/images/sidebar.png b/docs/modules/ROOT/images/sidebar.png
new file mode 100644
index 0000000..bcae4ac
Binary files /dev/null and b/docs/modules/ROOT/images/sidebar.png differ
diff --git a/docs/modules/ROOT/images/snapshot-archive.png b/docs/modules/ROOT/images/snapshot-archive.png
new file mode 100644
index 0000000..7716e71
Binary files /dev/null and b/docs/modules/ROOT/images/snapshot-archive.png differ
diff --git a/docs/modules/ROOT/images/snapshot-download.png b/docs/modules/ROOT/images/snapshot-download.png
new file mode 100644
index 0000000..44fb102
Binary files /dev/null and b/docs/modules/ROOT/images/snapshot-download.png differ
diff --git a/docs/modules/ROOT/images/snapshots.png b/docs/modules/ROOT/images/snapshots.png
new file mode 100644
index 0000000..6180126
Binary files /dev/null and b/docs/modules/ROOT/images/snapshots.png differ
diff --git a/docs/modules/ROOT/images/solution-k8s-hosted.png b/docs/modules/ROOT/images/solution-k8s-hosted.png
new file mode 100644
index 0000000..cbbb92b
Binary files /dev/null and b/docs/modules/ROOT/images/solution-k8s-hosted.png differ
diff --git a/docs/modules/ROOT/images/solution-systemd-control-panel.png b/docs/modules/ROOT/images/solution-systemd-control-panel.png
new file mode 100644
index 0000000..24b7b90
Binary files /dev/null and b/docs/modules/ROOT/images/solution-systemd-control-panel.png differ
diff --git a/docs/modules/ROOT/images/solutions/container-control-panel/preview.png b/docs/modules/ROOT/images/solutions/container-control-panel/preview.png
new file mode 100644
index 0000000..08c69d8
Binary files /dev/null and b/docs/modules/ROOT/images/solutions/container-control-panel/preview.png differ
diff --git a/docs/modules/ROOT/images/solutions/wol/preview.png b/docs/modules/ROOT/images/solutions/wol/preview.png
new file mode 100644
index 0000000..27fbaf3
Binary files /dev/null and b/docs/modules/ROOT/images/solutions/wol/preview.png differ
diff --git a/docs/modules/ROOT/images/ssh-diagram.png b/docs/modules/ROOT/images/ssh-diagram.png
new file mode 100644
index 0000000..ff11170
Binary files /dev/null and b/docs/modules/ROOT/images/ssh-diagram.png differ
diff --git a/docs/modules/ROOT/images/stream-deck/config.png b/docs/modules/ROOT/images/stream-deck/config.png
new file mode 100644
index 0000000..0091379
Binary files /dev/null and b/docs/modules/ROOT/images/stream-deck/config.png differ
diff --git a/docs/modules/ROOT/images/stream-deck/inputs.png b/docs/modules/ROOT/images/stream-deck/inputs.png
new file mode 100644
index 0000000..63b60c2
Binary files /dev/null and b/docs/modules/ROOT/images/stream-deck/inputs.png differ
diff --git a/docs/modules/ROOT/images/stream-deck/marketplace.png b/docs/modules/ROOT/images/stream-deck/marketplace.png
new file mode 100644
index 0000000..aaa686a
Binary files /dev/null and b/docs/modules/ROOT/images/stream-deck/marketplace.png differ
diff --git a/docs/modules/ROOT/images/stream-deck/panel.png b/docs/modules/ROOT/images/stream-deck/panel.png
new file mode 100644
index 0000000..bda18a8
Binary files /dev/null and b/docs/modules/ROOT/images/stream-deck/panel.png differ
diff --git a/docs/modules/ROOT/images/timeoutLogs.png b/docs/modules/ROOT/images/timeoutLogs.png
new file mode 100644
index 0000000..7a74fbe
Binary files /dev/null and b/docs/modules/ROOT/images/timeoutLogs.png differ
diff --git a/docs/modules/ROOT/images/topbar.png b/docs/modules/ROOT/images/topbar.png
new file mode 100644
index 0000000..347b465
Binary files /dev/null and b/docs/modules/ROOT/images/topbar.png differ
diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc
new file mode 100644
index 0000000..f907e50
--- /dev/null
+++ b/docs/modules/ROOT/nav.adoc
@@ -0,0 +1,174 @@
+* xref:install/intro.adoc[Installation Guide]
+*** Linux
+**** xref:install/container_vs_service.adoc[Containers or Service?]
+**** Linux Service
+***** xref:install/linux_fedora.adoc[Fedora Linux]
+***** xref:install/linux_alpine.adoc[Alpine Linux]
+***** xref:install/linux_manjaro.adoc[Manjaro Linux]
+***** xref:install/linux_arch.adoc[Arch Linux]
+***** xref:install/linux_rpm.adoc[Generic .rpm based Linux]
+***** xref:install/linux_deb.adoc[Generic .deb based Linux]
+***** xref:install/targz.adoc[.tar.gz Install (manual)]
+**** xref:install/container.adoc[Linux Container]
+***** xref:install/podmandocker.adoc[Docker or Podman]
+***** xref:install/docker_compose.adoc[Docker Compose]
+***** xref:install/helm.adoc[Kubernetes with Helm]
+***** xref:install/k8s.adoc[Kubernetes with Manifests]
+*** xref:install/bsd.adoc[BSD]
+*** xref:install/windows.adoc[Windows]
+**** xref:install/windows_service.adoc[Windows Service]
+*** xref:install/macos.adoc[MacOS]
+*** xref:install/choose_package.adoc[All download options]
+* Upgrade Guide
+** xref:upgrade/2k3k.adoc[Understanding 2k vs 3k]
+** xref:upgrade/upgrade_notes.adoc[Updates Notes]
+** xref:upgrade/github_latest.adoc[Warning: GitHub Latest]
+** xref:reference/updateChecks.adoc[Update Checks]
+** xref:reference/updateTracking.adoc[Update Tracking (legacy)]
+* xref:config.adoc[Configuration]
+* xref:reverse-proxies/intro.adoc[Reverse Proxies]
+** xref:reverse-proxies/apache.adoc[Apache]
+** xref:reverse-proxies/caddy.adoc[Caddy]
+** xref:reverse-proxies/haproxy.adoc[HAProxy]
+** xref:reverse-proxies/nginx.adoc[Nginx]
+** xref:reverse-proxies/nginx_proxy_manager.adoc[Nginx Proxy Manager]
+** xref:reverse-proxies/traefik.adoc[Traefik]
+* Action Execution
+** xref:action_execution/create_your_first.adoc[Create your first action]
+** xref:action_execution/shellvsexec.adoc[Shell vs Exec]
+** xref:action_execution/ondemand.adoc[Execute on demand]
+** xref:action_execution/oncron.adoc[Execute on schedule (cron)]
+** xref:action_execution/onstartup.adoc[Execute on startup]
+** xref:action_execution/onwebhook.adoc[Execute on webhook]
+*** xref:action_execution/onwebhook_github.adoc[GitHub Webhooks]
+** xref:action_execution/onfilecreated.adoc[Execute on file created]
+** xref:action_execution/onfilechanged.adoc[Execute on file changed]
+** xref:action_execution/oncalendar.adoc[Execute on calendar file]
+** xref:action_execution/aftercompletion.adoc[Execute after completion]
+** xref:action_execution/triggers.adoc[Triggers]
+* xref:action_customization/intro.adoc[Action Customization]
+** xref:action_customization/popuponstart.adoc[Popup on start]
+** xref:action_customization/icons.adoc[Icons]
+** xref:action_customization/timeouts.adoc[Timeouts]
+** xref:action_customization/users.adoc[Users]
+** xref:action_customization/concurrency.adoc[Concurrency]
+** xref:action_customization/ratelimiting.adoc[Rate Limiting]
+** xref:action_customization/enabledExpression.adoc[Enabled Expression]
+** xref:action_customization/ids.adoc[IDs]
+** xref:action_customization/savelogs.adoc[Save Logs]
+* xref:action_examples/intro.adoc[Action Examples]
+** xref:action_examples/containers.adoc[Containers - start/stop]
+*** xref:action_examples/docker-proxy.adoc[Docker control, via proxy]
+** xref:action_examples/systemd_service.adoc[Systemd Service]
+** xref:action_examples/ping.adoc[Ping]
+** xref:action_examples/ssh-easy.adoc[SSH (easy)]
+** xref:action_examples/ssh-manual.adoc[SSH (manual)]
+** xref:action_examples/powershell.adoc[Powershell]
+** xref:action_examples/ansible.adoc[Ansible]
+* xref:args/intro.adoc[Arguments]
+** xref:args/safety.adoc[Safety]
+** xref:args/types.adoc[Types]
+** xref:args/input.adoc[Input]
+** xref:args/regex.adoc[Input: Regex]
+** xref:args/password.adoc[Input: Password]
+** xref:args/input_checkbox.adoc[Input: Checkbox/Boolean]
+** xref:args/input_dropdown.adoc[Input: Dropdown]
+** xref:args/input_datetime.adoc[Input: Date & Time]
+** xref:args/input_confirmation.adoc[Input: Confirmation]
+** xref:args/input_textarea.adoc[Input: Textarea]
+** xref:args/suggestions.adoc[Suggestions]
+** xref:args/env.adoc[Environment Variables]
+* xref:dashboards/intro.adoc[Dashboards]
+** xref:dashboards/examples.adoc[Examples]
+** xref:dashboards/actions.adoc[Actions (Linked)]
+** xref:dashboards/inline-actions.adoc[Actions (Inline)]
+** xref:dashboards/css.adoc[Change component style]
+** xref:dashboards/2-fieldsets.adoc[Fieldsets]
+** xref:dashboards/3-folders.adoc[Folders]
+** xref:dashboards/4-displays.adoc[Displays]
+** xref:dashboards/5-output-views.adoc[Output Views]
+** xref:dashboards/entity-directories.adoc[Entity Directories]
+* xref:entities/intro.adoc[Entities]
+** xref:entities/examples.adoc[Examples]
+** xref:entities/yaml.adoc[YAML Entity Files]
+** xref:entities/json.adoc[JSON Entity Files]
+* xref:security/concepts.adoc[Security]
+** xref:security/acl.adoc[Access Control Lists]
+** xref:security/local.adoc[Local Users Authorization]
+** xref:security/trusted_header.adoc[Trusted Header Authorization]
+** xref:security/jwt.adoc[JWT Authorization]
+*** xref:security/jwt_keys.adoc[JWT with Keys]
+*** xref:security/jwt_hmac.adoc[JWT with HMAC]
+** xref:security/oauth2.adoc[OAuth2]
+*** xref:security/oauth2_authentik.adoc[OAuth2 with Authentik]
+*** xref:security/oauth2_authelia.adoc[OAuth2 with Authelia]
+*** xref:security/oauth2_pocketid.adoc[OAuth2 with Pocket ID]
+** xref:security/examples.adoc[Security examples]
+*** xref:security/example_login_required.adoc[Example: Login Required]
+*** xref:security/example_some_admin_actions.adoc[Example: Some actions require admin]
+** xref:security/design_choices.adoc[Security Design & Hardening Recommendations]
+** xref:security/content_security_policy.adoc[Content Security Policy headers]
+* Integrations
+** xref:integrations/homeassistant-integration.adoc[Home Assistant (HACS Integration)]
+** xref:integrations/homeassistant.adoc[Home Assistant (REST)]
+** xref:integrations/stream-deck.adoc[Stream-Deck]
+** xref:integrations/n8n.adoc[n8n]
+** xref:security/oauth2_authentik.adoc[Authentik]
+** xref:security/oauth2_pocketid.adoc[Pocket ID]
+** xref:reverse-proxies/intro.adoc[Reverse Proxies]
+* xref:solutions/intro.adoc[Solutions]
+** xref:security/examples.adoc[Security examples]
+** xref:solutions/on-git-push/index.adoc[Self hosted GitOps]
+** xref:solutions/container-control-panel/index.adoc[Container Control Panel]
+** xref:solutions/systemd-control-panel/index.adoc[Systemd Control Panel]
+** xref:solutions/heating-control-panel/index.adoc[Heating Control Panel]
+** xref:solutions/k8s-control-panel-hosted/index.adoc[Kubernetes Control Panel (hosted)]
+** xref:solutions/primitive-password/index.adoc[Primitive Password Protection]
+** xref:solutions/wol/index.adoc[Wake on LAN]
+** xref:solutions/cloudflare_access_tunnel/index.adoc[Cloudflare Access & Tunnels]
+** xref:solutions/directory-actions/index.adoc[Directory Actions]
+* xref:advanced_configuration/intro.adoc[Advanced Configuration]
+** xref:advanced_configuration/logs.adoc[Logging - Application]
+** xref:advanced_configuration/logs-actions.adoc[Logging - Actions]
+** xref:advanced_configuration/logs-calendar.adoc[Logs Calendar View]
+** xref:advanced_configuration/diagnostics.adoc[Diagnostics]
+** xref:advanced_configuration/config_envs.adoc[Config Envs]
+** xref:advanced_configuration/ports.adoc[Ports]
+** xref:advanced_configuration/stylemods.adoc[Style Mods]
+** xref:advanced_configuration/prometheus.adoc[Prometheus]
+** xref:advanced_configuration/timezones.adoc[Timezones]
+** xref:advanced_configuration/webui.adoc[Customize the WebUI]
+* Reference
+** xref:reference/network-ports.adoc[Network Ports]
+** xref:reference/exitCodes.adoc[Exit Codes]
+** xref:reference/containerInstallPackages.adoc[Install packages in containers]
+** xref:reference/reference_snapshots.adoc[Snapshots]
+** xref:reference/reference_themes_for_users.adoc[Themes (for users)]
+** xref:reference/reference_themes_for_developers.adoc[Themes (for developers)]
+** xref:reference/contribute.adoc[Contribute]
+** xref:reference/donations_and_sponsorship.adoc[Donations & Sponsorship]
+** xref:reference/multiple_instances.adoc[Multiple Instances]
+** xref:reference/release_policy.adoc[Release Policy]
+** xref:reference/includes.adoc[Includes]
+* Troubleshooting
+** xref:troubleshooting/wheretofindhelp.adoc[Where to find help]
+** xref:troubleshooting/browser-console-logs.adoc[Browser console logs (WebUI)]
+** xref:troubleshooting/service-logs.adoc[Service logs (OliveTin process)]
+** xref:troubleshooting/server-diagnostics.adoc[Server diagnostics]
+** xref:troubleshooting/puid-pgid.adoc[No PUID/PGID support]
+** xref:troubleshooting/log-debug-options.adoc[Log Debug Options]
+** xref:troubleshooting/exit127.adoc[Exit Code 127]
+** xref:troubleshooting/err-fetch-webui-settings.adoc[Error: WebUI Settings]
+** xref:troubleshooting/err-fetch-buttons.adoc[Error: Fetch Buttons]
+** xref:troubleshooting/err-js-modules-not-supported.adoc[Error: JS Modules not supported]
+** xref:troubleshooting/err-websocket-connection.adoc[Error: Websocket Connection]
+** xref:troubleshooting/err-webui-mismatch.adoc[Error: WebUI Version Mismatch]
+** xref:troubleshooting/advanced.adoc[Advanced Troubleshooting]
+* xref:api/intro.adoc[API]
+** xref:api/start_action.adoc[Start Actions from the API]
+*** xref:api/method_StartAction.adoc[StartAction]
+*** xref:api/method_StartActionByGet.adoc[StartActionByGet]
+*** xref:api/method_StartActionAndWait.adoc[StartActionAndWait]
+*** xref:api/method_StartActionByGetAndWait.adoc[StartActionByGetAndWait]
+** xref:api/misc.adoc[Misc API calls]
+** xref:api/login.adoc[Local user login via the API]
diff --git a/docs/modules/ROOT/pages/action_customization/concurrency.adoc b/docs/modules/ROOT/pages/action_customization/concurrency.adoc
new file mode 100644
index 0000000..2d63bbb
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_customization/concurrency.adoc
@@ -0,0 +1,32 @@
+[#concurrency]
+= Concurrency
+
+By default, OliveTin will allow you to run several instances of an action at the same time. For example, an action might take 20 seconds, and if you click the button 3 times, for a time there will be 3 actions running at the same time.
+
+Sometimes you don't want to allow this - an example case where it would not make sense is in the case of a backup script. To stop this, we can set `maxConcurrent` to `1`.
+
+[source,yaml]
+----
+actions:
+ - title: Run Backup Script
+ icon: backup
+ shell: /opt/backupScript.sh
+ maxConcurrent: 1
+----
+
+If you try and run a 2nd instance of this action while the first is currently running, you'll get a "blocked" message that looks like this;
+
+image::../blocked.png[]
+
+Additionally, OliveTin will log a message that looks like this;
+
+[source,log]
+.OliveTin log showing an action being blocked rom running.
+----
+INFO Action requested actionTitle="Run backup script"
+WARN Blocked from executing. This would mean this action is running 2 times concurrently, but this action has maxExecutions set to 1. actionTitle="Run backup script"
+----
+
+Naturally, you can set `maxConcurrent` to `3` or some other number, to limit the amount of times the action executes at once.
+
+
diff --git a/docs/modules/ROOT/pages/action_customization/enabledExpression.adoc b/docs/modules/ROOT/pages/action_customization/enabledExpression.adoc
new file mode 100644
index 0000000..9f76fb5
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_customization/enabledExpression.adoc
@@ -0,0 +1,189 @@
+[#enabled-expression]
+= Enabled Expression
+
+The `enabledExpression` property allows you to dynamically enable or disable action buttons based on entity properties. This is useful when you want to show context-appropriate actions - for example, only showing a "Turn Off" button when a device is already on, or only allowing a "Start" action when a service is stopped.
+
+== Basic Usage
+
+The `enabledExpression` is a Go template that must evaluate to a boolean value. When the expression evaluates to `true`, the action button will be enabled (clickable). When it evaluates to `false`, the button will be disabled (greyed out and not clickable).
+
+[source,yaml]
+----
+actions:
+ - title: Turn On Light
+ shell: echo "Turning on {{ .CurrentEntity.name }}"
+ icon: đź’ˇ
+ entity: light
+ enabledExpression: "{{ eq .CurrentEntity.powered_on false }}"
+
+ - title: Turn Off Light
+ shell: echo "Turning off {{ .CurrentEntity.name }}"
+ icon: đź’ˇ
+ entity: light
+ enabledExpression: "{{ eq .CurrentEntity.powered_on true }}"
+----
+
+In this example:
+
+* The "Turn On Light" button is only enabled when `powered_on` is `false`
+* The "Turn Off Light" button is only enabled when `powered_on` is `true`
+
+== How It Works
+
+The `enabledExpression` uses the same Go template syntax used elsewhere in OliveTin. It has access to the `.CurrentEntity` variable which contains all properties of the entity the action is bound to.
+
+=== Result Evaluation
+
+The template result is evaluated as follows:
+
+[cols="1,1", options="header"]
+|===
+| Result | Enabled?
+| `true` (case insensitive) | âś“ Yes
+| Non-zero integer (e.g., `1`, `42`) | âś“ Yes
+| `false` (case insensitive) | âś— No
+| `0` | âś— No
+| Empty string | âś— No
+| Template error | âś— No
+|===
+
+=== Default Behavior
+
+If `enabledExpression` is not specified, the action is always enabled (assuming the user has permission to execute it via ACLs).
+
+== Examples
+
+=== Simple Boolean Check
+
+[source,yaml]
+----
+actions:
+ - title: Start Service
+ shell: systemctl start {{ .CurrentEntity.service_name }}
+ entity: service
+ enabledExpression: "{{ eq .CurrentEntity.running false }}"
+
+ - title: Stop Service
+ shell: systemctl stop {{ .CurrentEntity.service_name }}
+ entity: service
+ enabledExpression: "{{ eq .CurrentEntity.running true }}"
+----
+
+=== Checking Status Values
+
+[source,yaml]
+----
+actions:
+ - title: Resume Download
+ shell: resume-download {{ .CurrentEntity.id }}
+ entity: download
+ enabledExpression: "{{ eq .CurrentEntity.status \"paused\" }}"
+----
+
+=== Using Integer Status Codes
+
+If your entity has integer status values, you can use them directly:
+
+[source,yaml]
+----
+actions:
+ - title: Process Item
+ shell: process {{ .CurrentEntity.id }}
+ entity: item
+ # Status 1 means "ready" - action is enabled when status is 1
+ enabledExpression: "{{ .CurrentEntity.status }}"
+----
+
+=== Combining with Other Template Functions
+
+You can use Go template functions for more complex logic:
+
+[source,yaml]
+----
+actions:
+ - title: Deploy to Production
+ shell: deploy {{ .CurrentEntity.name }}
+ entity: service
+ # Only enable if status is "ready" AND environment is "staging"
+ enabledExpression: "{{ and (eq .CurrentEntity.status \"ready\") (eq .CurrentEntity.environment \"staging\") }}"
+----
+
+== Complete Example
+
+Here's a complete configuration showing `enabledExpression` with entities and dashboards:
+
+[source,yaml]
+----
+entities:
+ - file: /etc/OliveTin/lights.yaml
+ name: light
+
+actions:
+ - title: Turn On Light
+ shell: /opt/smart-home/light-control.sh on {{ .CurrentEntity.id }}
+ icon: đź’ˇ
+ entity: light
+ enabledExpression: "{{ eq .CurrentEntity.powered_on false }}"
+
+ - title: Turn Off Light
+ shell: /opt/smart-home/light-control.sh off {{ .CurrentEntity.id }}
+ icon: 🔌
+ entity: light
+ enabledExpression: "{{ eq .CurrentEntity.powered_on true }}"
+
+dashboards:
+ - title: Light Controls
+ contents:
+ - title: Lights
+ type: fieldset
+ entity: light
+ contents:
+ - type: display
+ title: |
+ {{ .CurrentEntity.name }}
+ - title: Turn On Light
+ - title: Turn Off Light
+----
+
+With an entity file like:
+
+[source,yaml]
+./etc/OliveTin/lights.yaml
+----
+- id: kitchen
+ name: Kitchen Light
+ powered_on: false
+
+- id: living_room
+ name: Living Room Light
+ powered_on: true
+----
+
+In this setup:
+
+* The Kitchen Light will have "Turn On" enabled and "Turn Off" disabled
+* The Living Room Light will have "Turn Off" enabled and "Turn On" disabled
+
+== Error Handling
+
+If the `enabledExpression` template fails to parse or execute (e.g., due to syntax errors or missing entity properties), OliveTin will:
+
+1. Log a warning message with details about the failure
+2. Treat the action as **disabled** for safety
+
+This ensures that misconfigured expressions don't accidentally allow unintended actions.
+
+== Relationship with ACLs
+
+The `enabledExpression` works in combination with xref:security/acl.adoc[Access Control Lists (ACLs)]. An action button is only enabled when **both** conditions are met:
+
+1. The user has `exec` permission via ACLs
+2. The `enabledExpression` evaluates to `true`
+
+If either condition is not met, the action button will be disabled.
+
+== See Also
+
+* xref:entities/intro.adoc[Entities] - Learn about defining entities
+* xref:dashboards/intro.adoc[Dashboards] - Display entity-bound actions
+* xref:security/acl.adoc[Access Control Lists] - Control who can execute actions
diff --git a/docs/modules/ROOT/pages/action_customization/icons.adoc b/docs/modules/ROOT/pages/action_customization/icons.adoc
new file mode 100644
index 0000000..78ffff9
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_customization/icons.adoc
@@ -0,0 +1,144 @@
+[#icons]
+= Icons
+
+You can specify any HTML for an icon. It's a popular choice to use Unicode
+icons because they are extremely fast to load and there are a lot of them,
+but OliveTin also support Iconify, and simple PNG, JPG, WEBP and similar images.
+
+.Examples of icons in OliveTin
+image::../exampleIcons.png[]
+
+For a quick reference, here are some examples of how to use different types of icons in OliveTin;
+
+.`config.yaml`
+```yaml
+include::example$action_customization/icons/config.yaml[]
+```
+
+== Iconify Icons
+
+Browse over 200,000 icons that can be used with OliveTin here; https://icon-sets.iconify.design/
+
+Note, the icons are loaded from the internet, but should be cached by your browser afer the first load.
+
+On the Iconfiy website, you should select **Iconify Icon**
+
+image::../iconify.png[]
+
+Then copy this icon code, and place it in your config;
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: Iconify Icon
+ icon:
+----
+
+And you should get something that looks like this;
+
+image::../action-button-iconify.png[]
+
+== Unicode icons ("emoji")
+
+Using simple emoji (unicode) icons from your browser's font is extremely fast, and can look good on some platforms. However, the icons are platform specific, which mean's they'll look different between browsers and between operating systems.
+
+There are great sites like link:https://symbl.cc/en/emoji/[symbl.cc - a list of
+"Emoji" in unicode].
+
+For example, if you find "link:https://symbl.cc/en/1F60E/[Smiling face with sunglasses]" you can click
+on it to see it's "HTML-code". In OliveTin, you'd setup the icon like this;
+
+----
+actions:
+ - title: Unicode (emoji) icon
+ icon: "😎"
+ shell: echo "You are awesome"
+----
+
+=== Unicode alises
+
+OliveTin has hard-coded aliases for a few commonly used icons, so you don't have to type out the full unicode codes. A list of those hard coded icons is;
+
+.Alias'd unicode reference table
+[%header]
+|===
+| Alias | Rendered as
+
+| `poop` | 💩
+| `smile` | 😀
+| `ping` | 📡
+| `backup` | 💾
+| `reboot` | 🔄
+| `restart` | 🔄
+| `box` | 📦
+| `ashtonished` | 😲
+| `clock` | 🕒
+| `disk` | 💽
+| `logs` | 🔍
+| `light` | 💡
+| `robot` | 🤖
+| `ssh` | 🔐
+| `theme` | 🎨
+|===
+
+A full reference can be found in: https://github.com/OliveTin/OliveTin/blob/main/service/internal/config/emoji.go
+
+== Full HTML icons (`
'
+ shell: docker ps
+----
+
+=== Saving and serving icons for "offline" use
+
+Sometimes you might want to store images to use as icons, with your installation of OliveTin. This can be useful when your installation is meant to be offline, or disconnected from the internet. This is easily done.
+
+OliveTin will try to create a directory called `custom-webui` in the same directory as the `config.yaml` file. If this directory exists, OliveTin will serve files from this directory as if they were in the standard webui directory, in the same path as your OliveTin web UI.
+
+Ideally, put your icons in a directory like `/custom-webui/icons/`. If this directory contained a file called "mrgreen.gif", then it would be served at `http://myserver:1337/custom-webui/icons/mrgreen.gif`. Below is a picture of Mr Green. Feel free to save his likeness and awesomeness for yourself, for future awesome offline usage.
+
+.Mr Green, the original awesome smily.
+image::../mrgreen.gif[Mr Green]
+
+In your OliveTin config, customize your command again using HTML, like this;
+
+----
+actions:
+ - title: Mr Green
+ icon: '
'
+ shell: echo "I don't like the word 'emoji' "
+----
+
+This will result in a locally hosted icon that will work offline, that looks like this;
+
+image::../mrGreenAction.png[]
+
+////
+= CSS styles
+
+OliveTin allows you to write any CSS style rules directly on a single action.
+This is both pretty powerful if you want an action to have a particular style,
+but it does require understanding that you are writing your code - and can
+break things! Be careful!
+
+A tutorial on how to use CSS can easily be found online, but here are some
+examples;
+
+== Example: Bold & Purple action
+
+----
+- actions:
+ - title: My special action
+ css:
+ background-color: purple
+ font-weight: bold
+ shell: echo "I like purple"
+----
+////
+
+
diff --git a/docs/modules/ROOT/pages/action_customization/ids.adoc b/docs/modules/ROOT/pages/action_customization/ids.adoc
new file mode 100644
index 0000000..949a94f
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_customization/ids.adoc
@@ -0,0 +1,16 @@
+[#action-ids]
+= Action IDs
+
+OliveTin actions do not require IDs to be specified in the `config.yaml`, as most users of OliveTin start off with the Web Interface. However, if you want to use OliveTin actions via the xref:api/intro.adoc[API], then you will need to set your action IDs manually.
+
+NOTE: OliveTin will automatically generate a new ID for actions every time it starts up, for actions that don't have an `id:` property set.
+
+[source,yaml]
+----
+actions:
+ - title: Start the reactor
+ id: start_reactor
+ shell: /bin/startReactor.sh
+----
+
+
diff --git a/docs/modules/ROOT/pages/action_customization/intro.adoc b/docs/modules/ROOT/pages/action_customization/intro.adoc
new file mode 100644
index 0000000..bbaed3f
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_customization/intro.adoc
@@ -0,0 +1,33 @@
+[#action-customisation]
+= Action customisation
+
+Actions in OliveTin can be customized in many ways to fit your specific needs. This section covers various customization options that allow you to control how actions behave, appear, and execute.
+
+You can customize actions by:
+
+* Setting icons to make actions visually distinct
+* Configuring timeouts to control how long actions can run
+* Assigning actions to specific users or groups
+* Controlling concurrency to limit how many instances of an action can run simultaneously
+* Setting rate limits to prevent actions from being executed too frequently
+* Using enabled expressions to dynamically enable/disable actions based on entity state
+* Assigning unique IDs for API access
+* Configuring log saving for audit trails
+* Setting popup behaviors when actions start
+
+See the links in this section for detailed information on each customization option.
+
+== What's Next?
+
+Explore specific customization options:
+
+* xref:action_customization/icons.adoc[Customize icons] - Set visual icons for your actions
+* xref:action_customization/timeouts.adoc[Configure timeouts] - Control how long actions can run
+* xref:action_customization/users.adoc[Assign to users] - Restrict actions to specific users
+* xref:action_customization/concurrency.adoc[Control concurrency] - Limit simultaneous executions
+* xref:action_customization/ratelimiting.adoc[Set rate limits] - Prevent actions from running too frequently
+* xref:action_customization/enabledExpression.adoc[Enabled expressions] - Dynamically enable/disable actions based on entity state
+* xref:action_customization/popuponstart.adoc[Configure popups] - Control popup behavior when actions start
+* xref:action_customization/savelogs.adoc[Save action logs] - Configure log retention for actions
+* xref:action_customization/ids.adoc[Set action IDs] - Assign IDs for API access
+
diff --git a/docs/modules/ROOT/pages/action_customization/popuponstart.adoc b/docs/modules/ROOT/pages/action_customization/popuponstart.adoc
new file mode 100644
index 0000000..0e2623a
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_customization/popuponstart.adoc
@@ -0,0 +1,71 @@
+[#popup-on-start]
+= Popup on Start (Execution Feedback)
+
+OliveTin now has several options to control "execution feedback" when actions are started. This can be controlled on
+a per-action basis, using the `popupOnStart` configuration option.
+
+You can also set the default for OliveTin using the `defaultPopupOnStart` configuration option.
+
+== Big Flashy Buttons (default)
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: Ping the Internet
+ popupOnStart: default
+----
+
+This will also be the option that is used if no other values match.
+
+image::../flashyButton.png[]
+
+== Execution Dialog (Output Only)
+
+This can be useful for just displaying the output of a command, without too many additional details like the start time, end time, etc.
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: Check disk space
+ popupOnStart: execution-dialog-stdout-only
+----
+
+[NOTE]
+OliveTin used to separate out the Standard Output (`stdout`) and Standard Error (`stderr`) into two separate output streams. This made no sense, as lines would effectively be separated. This behavior has change to now display `stdout` and `stderr` in the same output stream. However, the configuration option `execution-dialog-stdout-only` was not renamed - and now it includes `stderr` as well.
+
+image::../popupOutputOnly.png[]
+
+== Execution Dialog
+
+The `execution-dialog` option for `popupOnStart` is simialr to the above `execution-dialog-stdout-only`, but it includes the start time, end time, exit code and the duration of time it took for the command to execute.
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: Check dmesg logs
+ popupOnStart: execution-dialog
+----
+
+.Example of `popupOnStart: execution-dialog`
+image::../executionDialog.png[]
+
+== Execution Buttons
+
+This mode of `popupOnStart` will create a new button for each individual execution. This can be useful for actions that are executed again and again.
+
+The text of the button (eg, "0s" in the screenshot below), is the time it took to execute the action in seconds.
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: date
+ popupOnStart: execution-button
+----
+
+image::../executionButtons.png[]
+
+
diff --git a/docs/modules/ROOT/pages/action_customization/ratelimiting.adoc b/docs/modules/ROOT/pages/action_customization/ratelimiting.adoc
new file mode 100644
index 0000000..a229d0d
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_customization/ratelimiting.adoc
@@ -0,0 +1,30 @@
+[#ratelimits]
+= Rate limiting
+
+By default, OliveTin allows you to execute actions as fast as you can click the button. This is fine if you are running OliveTin with trusted users in a trusted environment, but otherwise you may want to rate limit actions.
+
+Rate limiting is implemented like this;
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: date
+ shell: date
+ icon: clock
+ maxRate:
+ - limit: 3
+ duration: 5m
+----
+
+If you try to execute `date` more than 3 times in 5 minutes, you will get a log message in the UI that looks like this;
+
+image::maxRate.png[]
+
+Additionally, OliveTin will also output this to it's process log;
+
+----
+INFO Blocked from executing. This action has run 3 out of 3 allowed times in the last 5m. actionTitle="date"
+----
+
+
diff --git a/docs/modules/ROOT/pages/action_customization/savelogs.adoc b/docs/modules/ROOT/pages/action_customization/savelogs.adoc
new file mode 100644
index 0000000..c3e660b
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_customization/savelogs.adoc
@@ -0,0 +1,64 @@
+[#save-logs]
+= Saving logs
+
+By default, OliveTin only keeps logs in memory, meaning that if you restart OliveTin your logs will be lost. For some use cases this is acceptable, but you can configure OliveTin to save logs for you.
+
+You can configure the global setting for saving logs, or override it on a per-action basis;
+
+[source,yaml]
+.`config.yaml`
+----
+saveLogs:
+ resultsDirectory: /var/log/OliveTin/results/
+ outputDirectory: /var/log/OliveTin/output/
+
+
+actions:
+ # This will use the default `saveLogs` setting.
+ - title: date
+ shell: date
+
+ # This will override the default `saveLogs` setting.
+ - title: date2
+ shell: date
+ saveLogs:
+ resultsDirectory: /logs/
+ outputDirectory: /logs/
+
+----
+
+From the above example, you can see there There are two types of logs - **results (.yaml)** and **output (.log)**
+
+* **Results (.yaml)** - this captures almost everything that OliveTin knows about the action and looks like this.
++
+[source,yaml]
+.Example results - date.1714333384.5e2dc9e5-b6b3-445b-bff9-c2082b0bbbb2.yaml
+----
+datetimestarted: 2024-04-28T20:43:04.426754136+01:00
+datetimefinished: 2024-04-28T20:43:04.436596926+01:00
+stdout: |
+ Sun 28 Apr 20:43:04 BST 2024
+stderr: ""
+timedout: false
+blocked: false
+exitcode: 0
+tags: []
+executionstarted: true
+executionfinished: true
+executiontrackingid: 5e2dc9e5-b6b3-445b-bff9-c2082b0bbbb2
+process:
+ pid: 4168638
+actiontitle: date
+actionicon: '😀'
+actionid: d3cf6e25-8bab-432d-b4f9-e6f531b2b67b
+----
+
+* **output (.log)** - this just captures the output - stdout, stderr from an execution,
++
+[source]
+.Example output - date.1714333384.5e2dc9e5-b6b3-445b-bff9-c2082b0bbbb2.log
+----
+Sun 28 Apr 20:43:04 BST 2024
+----
+
+
diff --git a/docs/modules/ROOT/pages/action_customization/timeouts.adoc b/docs/modules/ROOT/pages/action_customization/timeouts.adoc
new file mode 100644
index 0000000..98f14e9
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_customization/timeouts.adoc
@@ -0,0 +1,30 @@
+[#timeout]
+= Timeouts
+
+By default, actions in OliveTin have a **3 second timeout** for all actions.
+This means that OliveTin will kill the action if it is running for longer than
+the timeout, which can be useful to stop commands running for a long time.
+
+You can set your own timeouts like this;
+
+[source,yaml]
+----
+actions:
+ - title: My special action
+ shell: sleep 5
+ timeout: 10
+----
+
+NOTE: Allowing commands to run for infinity just doesn't seem to make sense, or
+at least is probably a bad case for OliveTin. Therefore, if you set a timeout
+*less than 3 seconds*, OliveTin will overwrite your Timeout and default to 3
+seconds. If you think you have a use case where a shorter (or infinite) timeout
+makes sense, please open an issue and let's discuss.
+
+== Check the logs
+
+If a action really does "time out", it will show in the logs with "(timed out)" next to the exist code;
+
+image:timeoutLogs.png[]
+
+
diff --git a/docs/modules/ROOT/pages/action_customization/users.adoc b/docs/modules/ROOT/pages/action_customization/users.adoc
new file mode 100644
index 0000000..2546412
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_customization/users.adoc
@@ -0,0 +1,26 @@
+= Run as different users
+
+OliveTin does not *need* to run as root. It does not request any special
+permissions from the operating system that require root (as long as you run on
+ports above 1024, and it can read/write it's configuration). So, you can run as
+any non-root user if you wish.
+
+However, it is very convenient to run as root, as many users will need to run
+actions and jobs that do require root permissions.
+
+There are no ways in OliveTin to specify which user runs an action, because the
+Linux OS has several great ways to do this already, and adding support for it
+in OliveTin just adds bloat when there are perfectly good ways that already
+exist.
+
+== EG: Using sudo;
+
+----
+actions:
+ - title: Run echo as a different user
+ shell: sudo -u bob echo "I am Bob."
+----
+
+If you are worried about security, you could run OliveTin as a non-privileged
+user, and use sudo rules to control what it can and cannot do.
+
diff --git a/docs/modules/ROOT/pages/action_examples/ansible.adoc b/docs/modules/ROOT/pages/action_examples/ansible.adoc
new file mode 100644
index 0000000..d1c1805
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_examples/ansible.adoc
@@ -0,0 +1,21 @@
+[#ansible-playbook]
+= Ansible Playbooks
+
+:systemd: Easy
+:container: Install the `ansible` package in your xref:reference/containerInstallPackages.adoc[OliveTin container].
+include::partial$action_examples/actionHeader.adoc[]
+
+Many users use OliveTin to easily execute Ansible playbooks, somtimes as a simple alternative to AWX.
+
+.Run an Ansible Playbook
+[source,yaml]
+----
+actions:
+ - title: Run Ansible Playbook
+ icon: "🇦"
+ shell: ansible-playbook -i /etc/hosts /root/myRepo/myPlaybook.yaml
+ timeout: 120
+----
+
+You probably want to set the xref:action_customization/timeouts.adoc[timeout] to more than the default 3 seconds.
+
diff --git a/docs/modules/ROOT/pages/action_examples/containers.adoc b/docs/modules/ROOT/pages/action_examples/containers.adoc
new file mode 100644
index 0000000..6335998
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_examples/containers.adoc
@@ -0,0 +1,21 @@
+[#action-container-control]
+= Containers - start/stop
+
+NOTE: There is a complete example of how to setup a xref:solutions/container-control-panel/index.adoc[container control panel] in the solutions section.
+
+:systemd: Easy
+:container: Setup needed - see below
+include::partial$action_examples/actionHeader.adoc[]
+
+[#example-control-containers]
+....
+actions:
+ - title: Stop Plex
+ shell: docker stop plex
+
+ - title: Start plex
+ shell: docker start plex
+....
+
+include::partial$container_socket.adoc[]
+
diff --git a/docs/modules/ROOT/pages/action_examples/docker-proxy.adoc b/docs/modules/ROOT/pages/action_examples/docker-proxy.adoc
new file mode 100644
index 0000000..adfd710
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_examples/docker-proxy.adoc
@@ -0,0 +1,40 @@
+[#action-container-proxy]
+= Using the Docker socket proxy
+
+The OliveTin container comes with the official docker CLI pre-installed, as well as the compose plugin. This is because OliveTin is very often used to start and stop containers.
+
+You can choose to directly bind-mount the docker control socket into OliveTin, or optionally use a docker socket proxy host if you feel you need more security. You can use a docker socket proxy as an additional security measure and as an alternative to mounting the docker socket directly.
+
+Most people will want to add the docker socket proxy into the same compose file that they are running OliveTin from;
+
+[source,yaml]
+.docker-compose.yaml
+....
+services:
+ olivetin:
+ container_name: olivetin
+ image: jamesread/olivetin
+ ...
+ socket-proxy:
+ image: lscr.io/linuxserver/socket-proxy:latest
+ container_name: socket-proxy
+ environment:
+ - ALLOW_START=1 #optional
+ - ALLOW_STOP=1 #optional
+ ...
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+....
+
+You can find all the documentation for all the socket-proxy options here on the link:https://github.com/linuxserver/docker-socket-proxy[LinuxServer.io socket-proxy page].
+
+Assuming your docker socket proxy is running as `socket-proxy` running on port 1028;
+
+[source,yaml]
+.OliveTin config.yaml
+----
+actions:
+ - title: Stop container
+ shell: DOCKER_HOST=socket-proxy:1028 docker stop mycontainer
+----
+
diff --git a/docs/modules/ROOT/pages/action_examples/intro.adoc b/docs/modules/ROOT/pages/action_examples/intro.adoc
new file mode 100644
index 0000000..10faae5
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_examples/intro.adoc
@@ -0,0 +1,24 @@
+= Action Examples
+
+This section provides practical examples of how to configure actions in OliveTin for common use cases. These examples demonstrate real-world scenarios and can serve as starting points for your own configurations.
+
+The examples cover:
+
+* Container management (starting, stopping, and managing containers)
+* Systemd service control
+* Network utilities like ping
+* Remote execution via SSH
+* PowerShell commands for Windows environments
+* Integration with automation tools like Ansible
+
+Each example includes a complete configuration snippet that you can adapt for your environment. Browse through the examples to find scenarios that match your needs, or use them as inspiration for creating your own custom actions.
+
+== What's Next?
+
+After reviewing the examples, you can:
+
+* xref:action_execution/create_your_first.adoc[Create your own action] - Build a custom action for your needs
+* xref:action_customization/intro.adoc[Customize your actions] - Learn how to configure action properties
+* xref:args/intro.adoc[Add arguments] - Make your actions interactive with user input
+* xref:solutions/intro.adoc[Explore complete solutions] - See full configurations for common use cases
+* xref:action_execution/shellvsexec.adoc[Understand shell vs exec] - Learn about execution methods for security
diff --git a/docs/modules/ROOT/pages/action_examples/ping.adoc b/docs/modules/ROOT/pages/action_examples/ping.adoc
new file mode 100644
index 0000000..d227fb5
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_examples/ping.adoc
@@ -0,0 +1,16 @@
+[#action-ping]
+= Ping an address
+
+:systemd: Easy
+:container: Easy
+include::partial$action_examples/actionHeader.adoc[]
+
+[source,yaml]
+----
+actions:
+ # This sends 1 ping to google.com.
+ - title: ping google.com
+ shell: ping google.com -c 1
+ icon: ping
+ timeout: 3
+----
diff --git a/docs/modules/ROOT/pages/action_examples/powershell.adoc b/docs/modules/ROOT/pages/action_examples/powershell.adoc
new file mode 100644
index 0000000..262f265
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_examples/powershell.adoc
@@ -0,0 +1,17 @@
+[#powershell]
+= Powershell
+
+:systemd: Easy
+:container: Not possible
+include::partial$action_examples/actionHeader.adoc[]
+
+Powershell requires `pwsh` to execute commands.
+
+[source,yaml]
+.`config.yaml`
+....
+actions:
+ - title: Run Powershell Script:
+ shell: pwsh C:/Scripts/MyScript.ps1
+....
+
diff --git a/docs/modules/ROOT/pages/action_examples/ssh-easy.adoc b/docs/modules/ROOT/pages/action_examples/ssh-easy.adoc
new file mode 100644
index 0000000..183c7fe
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_examples/ssh-easy.adoc
@@ -0,0 +1,43 @@
+[#action-ssh-easy]
+= SSH (easy setup)
+
+include::partial$action_examples/ssh_intro.adoc[]
+
+NOTE: This is the easy method of setting up SSH with OliveTin - this generates a new SSH key for you, and a configuration file that disables SSH host key checking, to make it faster to do useful things with OliveTin. This is fine for most homelab setups, but if you are using OliveTin in a production environment, you should use the more secure method of setting up SSH and set it up manually, see xref:action_examples/ssh-easy.adoc[SSH (manual setup)].
+
+== SSH from inside a Container - setup instructions
+
+* [red]#<># Use the olivetin-setup-easy-ssh script to generate a new SSH key and configuration file
+** Add this key fingerprint to servers and hosts that you want to SSH to.
+* [red]#<># Setup actions that use SSH with this configuration file (which points to the key)
+
+Visually, this is what it looks like - OliveTin is running in the (orange) container, and then can either connect back to _server-with-olivetin_ or _server2_.
+
+image::../ssh-diagram.png[]
+
+[#ssh-easy-step-1]
+== [red]#Step 1#: Run the olivetin-setup-easy-ssh script
+
+Setup an action as follows, to use the builtin olivetin-setup-easy-ssh script that comes with OliveTin containers. This script does **not** work on Windows, MacOS, or outside of a container.
+
+.config.yaml
+[source,yaml]
+----
+actions:
+ - title: Setup SSH
+ shell: olivetin-setup-easy-ssh
+ popupOnStart: execution-dialog
+----
+
+[#ssh-easy-step-2]
+== [red]#Step 2#: Use the configuration file in your actions
+
+To use the configuration file generated by the script, you can use the following in your other actions:
+
+.config.yaml
+[source,yaml]
+----
+actions:
+ - title: SSH into a server
+ shell: ssh -F /config/ssh/config root@myserver '/opt/script-on-my-server.sh'
+----
diff --git a/docs/modules/ROOT/pages/action_examples/ssh-manual.adoc b/docs/modules/ROOT/pages/action_examples/ssh-manual.adoc
new file mode 100644
index 0000000..c5e67e5
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_examples/ssh-manual.adoc
@@ -0,0 +1,159 @@
+[#action-ssh]
+= SSH (manual setup)
+
+include::partial$action_examples/ssh_intro.adoc[]
+
+NOTE: There is an easy method of setting up SSH with OliveTin, which is described in the <> section. This section is for those who want to set up SSH manually.
+
+:systemd: Easy
+:container: Needs some setting up - see the <>
+include::partial$action_examples/actionHeader.adoc[]
+
+.OliveTin `config.yaml`
+[source,yaml]
+....
+actions:
+ # This will SSH into a server an run the command 'service httpd restart'
+ - title: Restart httpd on Server 1
+ shell: ssh root@server-with-olivetin 'service httpd restart'
+ icon: ping
+ timeout: 5
+....
+
+*Note about SSH keys*: You should make sure that the user that OliveTin is running as has access to a SSH key. This applies to container images as well. The setup instructions below briefly explain how to generate a SSH key and make it accessible to OliveTin which is running inside a container.
+
+[#ssh-container]
+ SSH from inside a Container - setup instructions
+
+This is a two step process;
+
+* [red]#<># Give OliveTin a SSH key
+* [red]#<># Setup actions that use SSH with this key
+
+Visually, this is what it looks like - OliveTin is running in the (orange) container, and then can either connect back to _server-with-olivetin_ or _server2_.
+
+image::../ssh-diagram.png[]
+
+The steps in detail are below;
+
+[#ssh-step-1]
+ [red]#Step 1#: Give OliveTin a SSH key
+
+Open a terminal window on _server-with-olivetin_.
+
+[loweralpha]
+. Create the `/opt/OliveTinSshKeys` directory, to create a shared directory for your SSH key file.
++
+[source,bash]
+----
+root@server-with-olivetin: mkdir /opt/OliveTinSshKeys
+----
++
+This will later be used as a "volume mount" when you create a docker container.
+
+. Run `ssh-keygen` to generate a SSH key just for OliveTin.
++
+[source,bash]
+----
+root@server-with-olivetin: ssh-keygen
+----
+[lowerroman]
+.. Enter the file in which to save the key: `/opt/OliveTinSshKeys/id_rsa`
+.. Enter passphrase (empty for no passphrase): ``
++
+This will create a passwordless SSH key that OliveTin can use. It is safe as long as nobody steals your SSH key file! OliveTin cannot enter passwords into SSH keys, so you have to leave the password blank.
+
+. You should get something that looks like this. If you get a "permission denied" error when creating files, try running `chmod 0777 /opt/OliveTinSshKeys` and try again.
++
+[source]
+----
+root@server-with-olivetin: ssh-keygen
+Generating public/private rsa key pair.
+Enter file in which to save the key (/root/.ssh/id_rsa): /opt/OliveTinSshKeys/id_rsa
+Enter passphrase (empty for no passphrase):
+Enter same passphrase again:
+Your identification has been saved in /opt/OliveTinSshKeys/id_rsa
+Your public key has been saved in /opt/OliveTinSshKeys/id_rsa.pub
+The key fingerprint is:
+SHA256:t+vGUn+MTeOtRDpxKanO3Cg63+gvAHslZCe3YVNnfWU root@server-with-olivetin
+The key's randomart image is:
++---[RSA 3072]----+
+| .. o. E|
+| + * o ...|
+| o = + . |
+| . . o . . |
+| o oS . + + |
+| . o ..o *o |
+| . . oo.o*.o |
+| . +*o+oo= .|
+| .=+BX .... |
++----[SHA256------+
+----
++
+This will create two files, `/opt/OliveTinSshKeys/id_rsa` (your private key) and `/opt/OliveTinSshKeys/id_rsa.pub` (your public key).
+
+. Copy your public key to every server you want to connect to.
++
+Using the `ssh-copy-id` command is a really quick and safe way to do this.
++
+----
+root@server-with-olivetin: ssh-copy-id -i /opt/OliveTinSshKeys/id_rsa.pub root@localhost
+(enter your SSH password)
+
+root@server2: ssh-copy-id ssh-copy-id -i /opt/OliveTinSshKeys/id_rsa.pub root@server2
+(enter your SSH password)
+----
++
+You will be asked to login with a password for each server.
++
+After you have done that, you will then be able to login with the ssh key instead. Here is a quick way that you can test your SSH key manually;
++
+----
+root@server-with-olivetin: ssh -i /opt/OliveTinSshKeys/id_rsa root@server2
+(you should login without a password)
+----
+
+. Give the SSH key to the OliveTin container.
++
+The way to do this is via a "volume mount". When you create the container, you use "-v" to specify a volume.
++
+You should mount your SSH keys directory into the OliveTin user's home directory by creating the container like this;
++
+.If you want to create the container from the command line
+----
+docker run -v /opt/OliveTinSshKeys/:/home/olivetin/.ssh/ -v /etc/OliveTin/:/config --name OliveTin jamesread/olivetin
+----
++
+.If you are using docker-compose
+[source,yaml]
+----
+services:
+ olivetin:
+ container_name: olivetin
+ image: jamesread/olivetin
+ volumes:
+ - "/etc/OliveTin/:/config"
+ - "/opt/OliveTinSshKeys:/home/olivetin/.ssh"
+ ports:
+ - "1337:1337"
+ restart: unless-stopped
+----
+
+This also works for things like SSH configuration files, if you want to use them. This is step 1 complete from the diagram above.
+
+[#ssh-step-2]
+ [red]#Step 2#: Setup actions that use SSH with this key
+
+Thankfully, step 2 is very simple! `ssh` commands in your OliveTin `config.yaml` should work without a password!, and allow OliveTin to access services, files, and other stuff outside of the OliveTin container.
+
+.OliveTin `config.yaml`
+[source,shell]
+....
+actions:
+ # This will SSH into a server an run the command 'service httpd restart'
+ - title: Restart httpd on Server 1
+ shell: ssh root@server-with-olivetin 'service httpd restart'
+ icon: ping
+ timeout: 5
+....
+
diff --git a/docs/modules/ROOT/pages/action_examples/systemd_service.adoc b/docs/modules/ROOT/pages/action_examples/systemd_service.adoc
new file mode 100644
index 0000000..4e3e34d
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_examples/systemd_service.adoc
@@ -0,0 +1,24 @@
+[#action-service]
+= Restart a systemd service
+
+:systemd: Easy
+:container: Not really possible to do.
+include::partial$action_examples/actionHeader.adoc[]
+
+[source,yaml]
+....
+actions:
+ - title: Start httpd
+ shell: systemctl start httpd
+
+ - title: Stop httpd
+ shell: systemctl stop httpd
+
+ - title: Restart httpd
+ shell: systemctl restart httpd
+
+ # https://docs.olivetin.app/action-ssh.html
+ - title: Restart httpd on server 1
+ shell: ssh root@server1 'service httpd restart'
+....
+
diff --git a/docs/modules/ROOT/pages/action_execution/aftercompletion.adoc b/docs/modules/ROOT/pages/action_execution/aftercompletion.adoc
new file mode 100644
index 0000000..0de2767
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_execution/aftercompletion.adoc
@@ -0,0 +1,39 @@
+[#after-completion]
+= Execute after completion
+
+Sometimes you want to execute another command after the main command executes, this is often the case when you want to check the status of the main command, or if you want to send a notification.
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: Check date and send notification via apprise
+ icon: date
+ shell: date
+ shellAfterCompleted: "apprise -c /config/apprise.yml -t 'Notification: Backup script completed' -b 'The backup script completed with code {{ exitCode}}. The log is: \n {{ output }} '"
+----
+
+When running shellAfterCompleted, you *cannot* use argument values - they are not passed to the command. However the following special arguments are defined;
+
+* `{{ exitCode }}` - The exit code of the previous shell command
+* `{{ output }}` - The standard output of the previous shell command
+* `{{ .Arguments.ot_executionTrackingId }}` - The unique execution tracking id for this execution (version 3k; in 2k use `{{ ot_executionTrackingId }}`)
+* `{{ .Arguments.ot_username }}` - The username of the user who started the execution (version 3k; in 2k use `{{ ot_username }}`). May be `guest` or `cron` for unauthenticated or automated runs.
+
+You can only use a single `shellAfterCompleted`, so use it for notifications, or similar. It would be an antipattern to use this do run 2 commands making up a mini script.
+
+The official OliveTin container images from version 2024.03.24 onwards include the fantastic apprise tool, which makes chat notifications on many protocols very easy.
+
+* https://github.com/caronc/apprise
+* https://github.com/caronc/apprise/wiki/config
+
+[source,yaml]
+.`/config/apprise.yaml`
+----
+urls:
+ - tgram://bottoken/ChatID
+----
+
+== See Also
+
+* xref:./triggers.adoc[Triggers] - Executing full actions after this one (with separate arguments, etc).
diff --git a/docs/modules/ROOT/pages/action_execution/create_your_first.adoc b/docs/modules/ROOT/pages/action_execution/create_your_first.adoc
new file mode 100644
index 0000000..4394937
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_execution/create_your_first.adoc
@@ -0,0 +1,35 @@
+[#create-your-first-action]
+= Create your first action
+
+This is an example of your very first action. First of all, edit your config.yaml, and enter this YAML markup;
+
+.config.yaml
+[source,yaml]
+----
+actions:
+ - title: Say hello
+ shell: echo "Hello!"
+ icon: smile
+----
+
+If OliveTin is running, it should popup on your dashboard like this;
+
+image::hello-world.png[]
+
+Simply click on the button to execute the shell command. You can expand the executions to view the logs.
+
+== Important considerations
+
+* The action title must be unique. If you have multiple actions with the same title, only one will be shown.
+
+== What's Next?
+
+Now that you've created your first action, here are some logical next steps:
+
+* xref:action_customization/intro.adoc[Customize your actions] - Learn how to set icons, timeouts, and other action properties
+* xref:args/intro.adoc[Add arguments to actions] - Make your actions interactive with user input
+* xref:action_examples/intro.adoc[Browse action examples] - See real-world examples for common use cases
+* xref:action_execution/oncron.adoc[Schedule actions] - Set up actions to run automatically on a schedule
+* xref:action_execution/onwebhook.adoc[Trigger actions via webhooks] - Integrate OliveTin with external systems
+* xref:dashboards/intro.adoc[Organize actions with dashboards] - Create custom views to organize your actions
+
diff --git a/docs/modules/ROOT/pages/action_execution/oncalendar.adoc b/docs/modules/ROOT/pages/action_execution/oncalendar.adoc
new file mode 100644
index 0000000..416382b
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_execution/oncalendar.adoc
@@ -0,0 +1,48 @@
+[#exec-on-calendar]
+= Execute on calendar file
+
+NOTE: The feature is currently experimental.
+
+Sometimes you want to schedule an action to run at a specific date and time, like at 2024-02-07 at 15:30. This is technically called "an instant", and OliveTin can watch a file that contains a list of instants for new additions.
+
+[source,yaml]
+.`start-server-calendar.yaml`
+----
+- 2024-03-08T20:11:45+00:00
+- 2024-03-08T20:12:30+00:00
+----
+
+OliveTin will watch this file, and also load it on startup. If an instant is seen that is in the past, it is just ignored. If it is in the future then it is scheduled.
+
+This is how you setup an action to use the calendar file:
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: start server
+ shell: echo "Starting Server!"
+ execOnCalendarFile: start-server-calendar.yaml
+----
+
+You will often want an easy way to schedule actions from the web interface as well, you can do this by creating a separate schedule action that adds an instant to the calendar file.
+
+You can use an argument with `type: datetime` to create a date selector in the web interface, to easily select dates to be added to the calendar file. You will have to add hardcoded timezone to suit your needs, you can see below that "+00:00" is being added to "{{ when }}" to create an instant in the UTC timezone.
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: Schedule server
+ shell: echo '- {{ when }}+00:00' >> start-server-calendar.yaml
+ arguments:
+ - name: when
+ title: When?
+ type: datetime
+
+ - title: start server
+ shell: echo "Starting Server!"
+ execOnCalendarFile: start-server-calendar.yaml
+----
+
+
diff --git a/docs/modules/ROOT/pages/action_execution/oncron.adoc b/docs/modules/ROOT/pages/action_execution/oncron.adoc
new file mode 100644
index 0000000..e602680
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_execution/oncron.adoc
@@ -0,0 +1,62 @@
+[#exec-cron]
+= Execute on schedule (cron)
+
+OliveTin can execute actions on a schedule, and uses a cron format for configuration.
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: Say hello
+ shell: echo "Hello!"
+ execOnCron:
+ - "@hourly"
+
+ - title: Say goodbye
+ shell: echo "Say Goodbye"
+ execOnCron:
+ - "*/5 * * * *" # Every 5 minutes
+----
+
+This is a fantastic website: https://cron.help/
+
+== Support for seconds in cron
+
+The default cron format for OliveTin supports the Unix/Linux format - 5 fields, with no support for seconds. This is by far the most popular format that most people are used to.
+
+If you need per-second resolution for your actions, this can be enabled in your config - meaning that your cronlines will support 6 columns. The first "new" column is seconds. For example, to execute `date` every 5 seconds;
+
+[source,yaml]
+.`config.yaml`
+----
+cronSupportForSeconds: true
+
+actions:
+ title: Execute every 5 seconds
+ shell: date
+ execOnCron:
+ - "*/5 * * * * *"
+----
+
+
+== Cron and ACLs
+
+If you have enabled ACL, cron tasks are run as the user `cron`, which means that your ACL needs to allow the cron user to execute the action. This is one possibilty:
+
+[source,yaml]
+.`config.yaml`
+----
+accessControlLists:
+ - name: "cron"
+ matchUsernames:
+ - cron
+ permissions:
+ exec: true
+actions:
+ - title: Say hello
+ shell: echo "Hello!"
+ execOnCron:
+ - "@hourly"
+ acls:
+ - "cron"
+----
diff --git a/docs/modules/ROOT/pages/action_execution/ondemand.adoc b/docs/modules/ROOT/pages/action_execution/ondemand.adoc
new file mode 100644
index 0000000..4e38941
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_execution/ondemand.adoc
@@ -0,0 +1,5 @@
+[#exec-on-demand]
+= Execute on demand
+
+This is the default, meaning that the action shows up on a dashboard, and at the moment cannot be changed.
+
diff --git a/docs/modules/ROOT/pages/action_execution/onfilechanged.adoc b/docs/modules/ROOT/pages/action_execution/onfilechanged.adoc
new file mode 100644
index 0000000..943d5b2
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_execution/onfilechanged.adoc
@@ -0,0 +1,27 @@
+[#exec-file-changed]
+= Execute on file changed
+
+You can execute an action when a file is changed in a directory. The argument `filename` is pre-populated for you.
+
+[source,yaml]
+----
+actions:
+ - title: Print names of new files
+ shell: "echo Filename: {{ filename }} Filedir: {{ filedir }} Filext: {{ fileext }}"
+ arguments:
+ - name: filename
+ type: unicode_identifier
+
+ - name: filedir
+ type: unicode_identifier
+
+ - name: fileext
+ type: unicode_identifier
+
+ execOnFileChangedInDir:
+ - /home/user/Downloads/
+----
+
+include::partial$action_execution/onfileindir_arguments.adoc[]
+
+
diff --git a/docs/modules/ROOT/pages/action_execution/onfilecreated.adoc b/docs/modules/ROOT/pages/action_execution/onfilecreated.adoc
new file mode 100644
index 0000000..8b23dd3
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_execution/onfilecreated.adoc
@@ -0,0 +1,26 @@
+[#exec-file-created]
+= Execute on file created
+
+You can execute an action when a file is created in a directory. The argument `filename` is pre-populated for you.
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: Print names of new files
+ shell: echo {{ filename }}
+ arguments:
+ - name: filename
+ type: unicode_identifier
+ - name: filedir
+ type: unicode_identifier
+ - name: fileext
+ type: unicode_identifier
+
+ execOnFileCreatedInDir:
+ - /home/user/Downloads/
+----
+
+include::partial$action_execution/onfileindir_arguments.adoc[]
+
+
diff --git a/docs/modules/ROOT/pages/action_execution/onstartup.adoc b/docs/modules/ROOT/pages/action_execution/onstartup.adoc
new file mode 100644
index 0000000..3a58e38
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_execution/onstartup.adoc
@@ -0,0 +1,32 @@
+[#exec-startup]
+= Execute on startup
+
+OliveTin can execute actions on a startup.
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: Say hello
+ shell: echo "Hello!"
+ execOnStartup: true
+----
+
+[#dnf-startup]
+== Example: Install additional commands into OliveTin
+
+This functionality to execute actions on startup is a very easy way to install additional commands in OliveTin, however it requires running OliveTin as a root user to be able to use `microdnf`;
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: Install dnsmasq
+ shell: microdnf install bind-utils
+ execOnStartup: true
+----
+
+A more secure method than running DNF as root, is a manual command the temporarily runs as root. To learn more about how to install additional packages into the container in this more secure way, see xref:reference/containerInstallPackages.adoc[Installing extra container packages].
+
+
+
diff --git a/docs/modules/ROOT/pages/action_execution/onwebhook.adoc b/docs/modules/ROOT/pages/action_execution/onwebhook.adoc
new file mode 100644
index 0000000..2fb1505
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_execution/onwebhook.adoc
@@ -0,0 +1,320 @@
+[#exec-webhook]
+= Execute on webhook
+
+Webhooks allow external services to trigger OliveTin actions by sending HTTP POST requests. This is useful for integrating OliveTin with CI/CD pipelines, monitoring systems, IoT devices, or any service that can send HTTP requests.
+
+OliveTin provides a dedicated webhook endpoint at `/webhooks` that can receive webhook payloads and match them to configured actions.
+
+== Basic Configuration
+
+To configure an action to run on a webhook, add the `execOnWebhook` property to your action:
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: Deploy Application
+ id: deploy
+ shell: /opt/scripts/deploy.sh
+ execOnWebhook:
+ - matchHeaders:
+ X-Event-Type: deploy
+----
+
+This action will be triggered when a POST request is sent to `/webhooks` with the header `X-Event-Type: deploy`.
+
+== Webhook Endpoint
+
+All webhooks are received at:
+
+----
+http://your-olivetin-server:1337/webhooks
+----
+
+or
+
+----
+http://your-olivetin-server:1337/webhooks/
+----
+
+Both paths work identically. All webhook requests must use the HTTP POST method.
+
+== Matching Webhooks
+
+OliveTin can match incoming webhooks based on several criteria:
+
+=== Match by Headers
+
+Match webhooks based on HTTP header values:
+
+[source,yaml]
+----
+actions:
+ - title: Process Event
+ shell: echo "Processing event"
+ execOnWebhook:
+ - matchHeaders:
+ X-Event-Type: my-event
+ X-Source: my-service
+----
+
+All specified headers must match for the webhook to trigger the action.
+
+=== Match by Query Parameters
+
+Match webhooks based on URL query parameters:
+
+[source,yaml]
+----
+actions:
+ - title: Process Request
+ shell: echo "Processing request for {{ service }}"
+ arguments:
+ - name: service
+ type: ascii
+ execOnWebhook:
+ - matchQuery:
+ action: deploy
+ env: production
+----
+
+A request to `/webhooks?action=deploy&env=production` would match this action.
+
+=== Match by JSON Body Path
+
+Match webhooks based on values in the JSON request body using JSONPath expressions:
+
+[source,yaml]
+----
+actions:
+ - title: Handle Push Event
+ shell: echo "Push to {{ branch }}"
+ arguments:
+ - name: branch
+ type: ascii
+ execOnWebhook:
+ - matchPath: "$.event_type=push"
+----
+
+The `matchPath` format is `jsonpath=value`. You can also just specify a JSONPath without a value to match if the path exists:
+
+[source,yaml]
+----
+execOnWebhook:
+ - matchPath: "$.repository.name" # Matches if this path exists in the JSON
+----
+
+=== Using Regex for Matching
+
+Header and query parameter values can use regex patterns by prefixing with `regex:`:
+
+[source,yaml]
+----
+actions:
+ - title: Handle Multiple Events
+ shell: echo "Handling event"
+ execOnWebhook:
+ - matchHeaders:
+ X-Event-Type: "regex:^(push|pull_request|release)$"
+----
+
+=== Combining Match Criteria
+
+You can combine multiple match criteria. All criteria must match for the webhook to trigger:
+
+[source,yaml]
+----
+actions:
+ - title: Production Deploy
+ shell: /opt/scripts/deploy.sh production
+ execOnWebhook:
+ - matchHeaders:
+ X-Event-Type: deploy
+ matchQuery:
+ environment: production
+ matchPath: "$.status=approved"
+----
+
+== Extracting Arguments from Webhooks
+
+You can extract values from the webhook payload and pass them as arguments to your action using JSONPath expressions:
+
+[source,yaml]
+----
+actions:
+ - title: Deploy Version
+ shell: |
+ echo "Deploying version {{ version }} to {{ environment }}"
+ /opt/scripts/deploy.sh "{{ version }}" "{{ environment }}"
+ arguments:
+ - name: version
+ type: ascii
+ - name: environment
+ type: ascii
+ execOnWebhook:
+ - matchHeaders:
+ X-Event-Type: deploy
+ extract:
+ version: "$.release.tag_name"
+ environment: "$.target.environment"
+----
+
+The `extract` map defines which action arguments to populate from the webhook payload. The key is the argument name, and the value is the JSONPath expression to extract the value.
+
+=== Automatic Webhook Metadata
+
+OliveTin automatically adds several metadata arguments from each webhook request:
+
+* `webhook_method` - The HTTP method (always POST for webhooks)
+* `webhook_path` - The request URL path
+* `webhook_query` - The raw query string
+* `webhook_header_` - Each HTTP header (lowercase name)
+
+For example, to access the `X-Request-Id` header in your action:
+
+[source,yaml]
+----
+actions:
+ - title: Log Request
+ shell: echo "Request ID: {{ webhook_header_x-request-id }}"
+ arguments:
+ - name: webhook_header_x-request-id
+ type: ascii
+ execOnWebhook:
+ - matchHeaders:
+ X-Event-Type: log
+----
+
+== Webhook Authentication
+
+OliveTin supports several authentication methods to verify webhook requests:
+
+=== No Authentication
+
+By default, webhooks have no authentication. Any request matching the criteria will trigger the action:
+
+[source,yaml]
+----
+execOnWebhook:
+ - authType: none
+ matchHeaders:
+ X-Event-Type: my-event
+----
+
+=== HMAC-SHA256 Signature
+
+Verify webhooks using HMAC-SHA256 signatures (commonly used by GitHub, GitLab, etc.):
+
+[source,yaml]
+----
+execOnWebhook:
+ - authType: hmac-sha256
+ authHeader: X-Hub-Signature-256
+ secret: your-webhook-secret
+ matchHeaders:
+ X-Event-Type: push
+----
+
+The `authHeader` specifies which header contains the signature. The signature should be in the format `sha256=`.
+
+=== HMAC-SHA1 Signature
+
+For services using HMAC-SHA1 (legacy GitHub webhooks):
+
+[source,yaml]
+----
+execOnWebhook:
+ - authType: hmac-sha1
+ authHeader: X-Hub-Signature
+ secret: your-webhook-secret
+ matchHeaders:
+ X-Event-Type: push
+----
+
+=== Bearer Token
+
+Verify webhooks using a Bearer token in the Authorization header:
+
+[source,yaml]
+----
+execOnWebhook:
+ - authType: bearer
+ secret: your-bearer-token
+ matchHeaders:
+ X-Event-Type: deploy
+----
+
+The webhook sender must include `Authorization: Bearer your-bearer-token` in the request.
+
+=== Basic Authentication
+
+Verify webhooks using HTTP Basic authentication:
+
+[source,yaml]
+----
+execOnWebhook:
+ - authType: basic
+ secret: "username:password"
+ matchHeaders:
+ X-Event-Type: deploy
+----
+
+Or with password only:
+
+[source,yaml]
+----
+execOnWebhook:
+ - authType: basic
+ secret: "mypassword"
+ matchHeaders:
+ X-Event-Type: deploy
+----
+
+== Multiple Webhook Triggers
+
+An action can have multiple webhook configurations. The action will be triggered if any of them match:
+
+[source,yaml]
+----
+actions:
+ - title: Deploy
+ shell: /opt/scripts/deploy.sh
+ execOnWebhook:
+ - matchHeaders:
+ X-Event-Type: deploy-manual
+ - matchHeaders:
+ X-Event-Type: deploy-auto
+ matchPath: "$.status=success"
+----
+
+== Testing Webhooks
+
+You can test your webhook configuration using `curl`:
+
+[source,bash]
+----
+# Simple webhook with headers
+curl -X POST \
+ -H "Content-Type: application/json" \
+ -H "X-Event-Type: deploy" \
+ -d '{"version": "1.2.3"}' \
+ http://localhost:1337/webhooks
+
+# Webhook with HMAC-SHA256 authentication
+SECRET="your-secret"
+PAYLOAD='{"event": "push", "branch": "main"}'
+SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
+
+curl -X POST \
+ -H "Content-Type: application/json" \
+ -H "X-Event-Type: push" \
+ -H "X-Hub-Signature-256: sha256=$SIGNATURE" \
+ -d "$PAYLOAD" \
+ http://localhost:1337/webhooks
+----
+
+== See Also
+
+* xref:action_execution/onwebhook_github.adoc[GitHub Webhooks] - Specific configuration for GitHub webhook events
+* xref:solutions/on-git-push/index.adoc[GitOps Solution] - Running actions on Git push using hooks
+* xref:api/start_action.adoc[Start Action API] - Alternative method for triggering actions via API
diff --git a/docs/modules/ROOT/pages/action_execution/onwebhook_github.adoc b/docs/modules/ROOT/pages/action_execution/onwebhook_github.adoc
new file mode 100644
index 0000000..e4e31c9
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_execution/onwebhook_github.adoc
@@ -0,0 +1,385 @@
+[#exec-webhook-github]
+= GitHub Webhooks
+
+OliveTin includes built-in templates for GitHub webhooks that simplify configuration. Instead of manually configuring header matching and argument extraction, you can use a template that handles all the common settings for you.
+
+== Supported GitHub Templates
+
+OliveTin supports the following GitHub webhook templates:
+
+* `github-push` - Triggered on push events
+* `github-pr` or `github-pull-request` - Triggered on pull request events
+* `github-release` - Triggered on release events
+* `github-workflow` - Triggered on workflow run events
+
+== Setting Up GitHub Webhooks
+
+=== 1. Configure the Webhook in GitHub
+
+1. Go to your GitHub repository → **Settings** → **Webhooks** → **Add webhook**
+2. Set the **Payload URL** to `http://your-olivetin-server:1337/webhooks`
+3. Set **Content type** to `application/json`
+4. Enter a **Secret** (you'll use this in your OliveTin config)
+5. Choose which events to trigger the webhook:
+ - Select **Just the push event** for push triggers
+ - Or select **Let me select individual events** for more control
+6. Click **Add webhook**
+
+=== 2. Configure OliveTin
+
+Use the `template` property to apply GitHub-specific settings:
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: Deploy on Push
+ shell: |
+ echo "Deploying commit {{ git_commit }} to {{ git_branch }}"
+ /opt/scripts/deploy.sh "{{ git_branch }}"
+ arguments:
+ - name: git_commit
+ type: ascii
+ - name: git_branch
+ type: ascii
+ execOnWebhook:
+ - template: github-push
+ secret: your-github-webhook-secret
+----
+
+== GitHub Push Template
+
+The `github-push` template is designed for push events. It automatically:
+
+* Sets authentication to HMAC-SHA256 with the `X-Hub-Signature-256` header
+* Matches the `X-GitHub-Event: push` header
+* Extracts common push event data
+
+=== Extracted Arguments
+
+The following arguments are automatically extracted and available in your action:
+
+[cols="1,2,2"]
+|===
+|Argument Name |Description |JSONPath
+
+|`git_repository`
+|Full repository name (owner/repo)
+|`$.repository.full_name`
+
+|`git_ref`
+|Full git reference (e.g., refs/heads/main)
+|`$.ref`
+
+|`git_commit`
+|The HEAD commit SHA
+|`$.head_commit.id`
+
+|`git_branch`
+|The branch reference
+|`$.ref`
+
+|`git_message`
+|The commit message
+|`$.head_commit.message`
+
+|`git_author`
+|The commit author's name
+|`$.head_commit.author.name`
+|===
+
+=== Example: Deploy on Push to Main
+
+[source,yaml]
+----
+actions:
+ - title: Deploy to Production
+ shell: |
+ if [ "{{ git_ref }}" = "refs/heads/main" ]; then
+ echo "Deploying {{ git_commit }} by {{ git_author }}"
+ /opt/scripts/deploy.sh production
+ else
+ echo "Ignoring push to non-main branch"
+ fi
+ arguments:
+ - name: git_ref
+ type: ascii
+ - name: git_commit
+ type: ascii
+ - name: git_author
+ type: ascii
+ execOnWebhook:
+ - template: github-push
+ secret: your-secret
+----
+
+=== Example: Filter by Branch
+
+To only trigger on specific branches, add a `matchPath` condition:
+
+[source,yaml]
+----
+actions:
+ - title: Deploy Staging
+ shell: /opt/scripts/deploy.sh staging
+ execOnWebhook:
+ - template: github-push
+ secret: your-secret
+ matchPath: '$.ref="refs/heads/develop"'
+----
+
+== GitHub Pull Request Template
+
+The `github-pr` (or `github-pull-request`) template handles pull request events.
+
+=== Extracted Arguments
+
+[cols="1,2,2"]
+|===
+|Argument Name |Description |JSONPath
+
+|`pr_number`
+|Pull request number
+|`$.number`
+
+|`pr_title`
+|Pull request title
+|`$.pull_request.title`
+
+|`pr_author`
+|PR author's username
+|`$.pull_request.user.login`
+
+|`pr_action`
+|Event action (opened, closed, synchronize, etc.)
+|`$.action`
+
+|`git_repository`
+|Full repository name
+|`$.repository.full_name`
+
+|`pr_state`
+|PR state (open, closed)
+|`$.pull_request.state`
+
+|`pr_head_sha`
+|HEAD commit SHA of the PR branch
+|`$.pull_request.head.sha`
+|===
+
+=== Example: Run Tests on PR
+
+[source,yaml]
+----
+actions:
+ - title: Run PR Tests
+ shell: |
+ echo "Running tests for PR #{{ pr_number }}: {{ pr_title }}"
+ echo "Action: {{ pr_action }}, Author: {{ pr_author }}"
+ /opt/scripts/run-tests.sh "{{ pr_head_sha }}"
+ arguments:
+ - name: pr_number
+ type: ascii
+ - name: pr_title
+ type: ascii
+ - name: pr_action
+ type: ascii
+ - name: pr_author
+ type: ascii
+ - name: pr_head_sha
+ type: ascii
+ execOnWebhook:
+ - template: github-pr
+ secret: your-secret
+----
+
+=== Example: Only on PR Open or Synchronize
+
+[source,yaml]
+----
+actions:
+ - title: CI Build
+ shell: /opt/scripts/ci-build.sh "{{ pr_head_sha }}"
+ arguments:
+ - name: pr_head_sha
+ type: ascii
+ execOnWebhook:
+ - template: github-pr
+ secret: your-secret
+ matchPath: '$.action="opened"'
+ - template: github-pr
+ secret: your-secret
+ matchPath: '$.action="synchronize"'
+----
+
+== GitHub Release Template
+
+The `github-release` template handles release events.
+
+=== Extracted Arguments
+
+[cols="1,2,2"]
+|===
+|Argument Name |Description |JSONPath
+
+|`release_action`
+|Release action (published, created, etc.)
+|`$.action`
+
+|`release_tag`
+|Release tag name
+|`$.release.tag_name`
+
+|`release_name`
+|Release name/title
+|`$.release.name`
+
+|`git_repository`
+|Full repository name
+|`$.repository.full_name`
+
+|`release_author`
+|Release author's username
+|`$.release.author.login`
+|===
+
+=== Example: Deploy on Release
+
+[source,yaml]
+----
+actions:
+ - title: Deploy Release
+ shell: |
+ echo "Deploying release {{ release_tag }}: {{ release_name }}"
+ /opt/scripts/deploy-release.sh "{{ release_tag }}"
+ arguments:
+ - name: release_tag
+ type: ascii
+ - name: release_name
+ type: ascii
+ execOnWebhook:
+ - template: github-release
+ secret: your-secret
+ matchPath: '$.action="published"'
+----
+
+== GitHub Workflow Template
+
+The `github-workflow` template handles workflow run events, useful for triggering actions when GitHub Actions workflows complete.
+
+=== Extracted Arguments
+
+[cols="1,2,2"]
+|===
+|Argument Name |Description |JSONPath
+
+|`workflow_name`
+|Name of the workflow
+|`$.workflow_run.name`
+
+|`workflow_status`
+|Workflow status
+|`$.workflow_run.status`
+
+|`workflow_conclusion`
+|Workflow conclusion (success, failure, etc.)
+|`$.workflow_run.conclusion`
+
+|`git_repository`
+|Full repository name
+|`$.repository.full_name`
+
+|`git_commit`
+|HEAD commit SHA
+|`$.workflow_run.head_sha`
+
+|`git_branch`
+|Branch that triggered the workflow
+|`$.workflow_run.head_branch`
+|===
+
+=== Example: Deploy After CI Success
+
+[source,yaml]
+----
+actions:
+ - title: Deploy After CI
+ shell: |
+ echo "CI workflow '{{ workflow_name }}' completed with {{ workflow_conclusion }}"
+ if [ "{{ workflow_conclusion }}" = "success" ]; then
+ /opt/scripts/deploy.sh "{{ git_branch }}" "{{ git_commit }}"
+ fi
+ arguments:
+ - name: workflow_name
+ type: ascii
+ - name: workflow_conclusion
+ type: ascii
+ - name: git_branch
+ type: ascii
+ - name: git_commit
+ type: ascii
+ execOnWebhook:
+ - template: github-workflow
+ secret: your-secret
+ matchPath: '$.action="completed"'
+----
+
+== Customizing Templates
+
+Templates provide default values, but you can override or extend them:
+
+[source,yaml]
+----
+actions:
+ - title: Custom Push Handler
+ shell: echo "Push from {{ custom_field }}"
+ arguments:
+ - name: custom_field
+ type: ascii
+ - name: git_commit
+ type: ascii
+ execOnWebhook:
+ - template: github-push
+ secret: your-secret
+ # Add additional extractions
+ extract:
+ custom_field: "$.sender.login"
+ # Add additional match criteria
+ matchPath: '$.repository.private=false'
+----
+
+Custom `extract` values are merged with template defaults, so you can add extra fields without losing the standard ones.
+
+== Security Considerations
+
+1. **Always use a secret** - Without a secret, anyone can trigger your webhooks
+2. **Use HTTPS** - When exposing OliveTin to the internet, use a reverse proxy with TLS
+3. **Limit webhook events** - Only subscribe to the events you actually need in GitHub
+4. **Validate in your scripts** - Add additional validation in your shell scripts for sensitive operations
+
+== Troubleshooting
+
+=== Webhook Not Triggering
+
+1. Check the OliveTin logs with `logLevel: DEBUG` for webhook processing details
+2. Verify the webhook is being received (check GitHub webhook delivery history)
+3. Ensure the secret matches exactly between GitHub and OliveTin config
+4. Verify the `template` name is spelled correctly
+
+=== Signature Verification Failed
+
+1. Ensure the secret in OliveTin matches the one configured in GitHub
+2. Check that you're using the correct template (GitHub uses HMAC-SHA256 by default)
+3. Make sure the webhook Content-Type is set to `application/json`
+
+=== Arguments Not Extracted
+
+1. Verify the argument names match exactly (case-sensitive)
+2. Check that arguments are defined in the action's `arguments` list
+3. Use `logLevel: DEBUG` to see extracted values in the logs
+
+== See Also
+
+* xref:action_execution/onwebhook.adoc[Webhooks Overview] - General webhook configuration
+* xref:solutions/on-git-push/index.adoc[GitOps Solution] - Alternative approach using Git hooks
+* https://docs.github.com/en/webhooks[GitHub Webhooks Documentation^] - Official GitHub webhook documentation
diff --git a/docs/modules/ROOT/pages/action_execution/shellvsexec.adoc b/docs/modules/ROOT/pages/action_execution/shellvsexec.adoc
new file mode 100644
index 0000000..5c40367
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_execution/shellvsexec.adoc
@@ -0,0 +1,44 @@
+= Shell vs Exec
+
+OliveTin supports two different methods to run commands: `shell` and `exec`. The difference between these two is that "shell" accepts strings, and will wrap that whole command in a shell with "bash -c". Exec uses a syscall directly to execute commands.
+
+* **Shell** is more flexible, because it allows you to chain commands (eg, using &&) and redirect or pipe output (eg: ">" or "|").
+* **Exec** is more secure, because it does not invoke a shell, and thus avoids shell injection attacks.
+
+Shell can be safe and secure with simple argument types (like ascii_identifier), but some argument types like URL can contain basically any character - /, :, ?, &, etc - which can lead to shell injection vulnerabilities while still being a valid URL.
+
+OliveTin will try and prevent you from using dangerous characters in shell commands (eg, URL is no longer permitted with Shell).
+
+The way that you specify these two types of execution is different - `shell` expects a single string, while `exec` expects a list of strings (the first being the command, the rest being the arguments).
+
+[source,yaml]
+.Using Shell
+----
+actions:
+ - title: List files
+ shell: ls -l /some/directory
+----
+
+[source,yaml]
+.Using Exec
+----
+actions:
+ - title: List files
+ exec:
+ - ls
+ - -l
+ - /some/directory
+----
+
+When in doubt, prefer `exec` over `shell` for better security. Shell was added in both OliveTin 3k and OliveTin 2k in October 2025.
+
+== What's Next?
+
+Now that you understand execution methods, continue building your actions:
+
+* xref:action_execution/create_your_first.adoc[Create your first action] - Build a simple action to get started
+* xref:args/intro.adoc[Add arguments to actions] - Make actions interactive with user input
+* xref:action_execution/oncron.adoc[Schedule actions] - Set up automated execution
+* xref:action_execution/onwebhook.adoc[Trigger via webhooks] - Integrate with external systems
+* xref:security/concepts.adoc[Configure security] - Secure your actions with authentication and authorization
+* xref:action_examples/intro.adoc[Browse examples] - See real-world action configurations
diff --git a/docs/modules/ROOT/pages/action_execution/triggers.adoc b/docs/modules/ROOT/pages/action_execution/triggers.adoc
new file mode 100644
index 0000000..cf1b144
--- /dev/null
+++ b/docs/modules/ROOT/pages/action_execution/triggers.adoc
@@ -0,0 +1,25 @@
+[#triggers]
+= Triggers
+
+Sometimes you want to trigger another action after the first one completes. This is mostly useful for updating hidden actions that update entity files, without having to run those updates on a cron job every 10 seconds!
+
+NOTE: OliveTin used to support a single action trigger, but now supports multiple triggers. The field `trigger` was renamed to `triggers` and is now an array of triggers.
+
+[source,yaml]
+----
+entities:
+ - file: /etc/OliveTin/entities/containers.json
+ name: container
+
+actions:
+ - title: stop {{ container.Names }}
+ shell: docker stop {{ container.Names}}
+ entity: container
+ triggers:
+ - update containers
+
+ - title: update containers
+ shell: docker ps -a --format=json > /etc/OliveTin/entities/containers.json
+ hidden: true
+----
+
diff --git a/docs/modules/ROOT/pages/advanced_configuration/config_envs.adoc b/docs/modules/ROOT/pages/advanced_configuration/config_envs.adoc
new file mode 100644
index 0000000..296a2b7
--- /dev/null
+++ b/docs/modules/ROOT/pages/advanced_configuration/config_envs.adoc
@@ -0,0 +1,54 @@
+[#config-envs]
+= Environment Variables in the Config File
+
+You can pull configuration values from environment variables like this:
+
+.`config.yaml`
+[source,yaml]
+----
+logLevel: ${{ LOG_LEVEL }}
+
+pageTitle: Olivetin - ${{ DEPLOY_ENV }}
+
+actions:
+ ...
+----
+
+While loading the config file, Olivetin will substitute the value of the named environment variable for the token.
+If the variable is unset, Olivetin will use an empty string as the value and log a warning. This syntax works even for
+configuration values that aren't strings, as long as the final string value can be converted to the proper type.
+
+== Notes
+
+. These variables are read while loading the config file. Changes in the environment won't be reflected until the config
+file is reloaded. If you want to read environment variables at execution time in your action's `shell` line, make sure
+to use regular shell syntax, i.e., `$FOO` rather than `${{ FOO }}`. See xref:args/env.adoc[environment variables]
+for info on using environment variables in your actions.
+
+[#using-env-in-template-replacements]
+== Using .Env in template replacements
+
+In addition to config-file substitution, OliveTin supports using the process environment inside *action templates* (e.g. `shell`, `shellAfterCompleted`, entity directory titles, and other fields that use Go template syntax). Use `{{ .Env.VAR_NAME }}` to substitute an environment variable at the time the action is executed.
+
+This is useful when you want a command to depend on the runtime environment (e.g. container or system env) rather than config-load time, or when you need env values in template fields that are not the raw `shell` command (where you could use `$VAR`).
+
+.Example: use `.Env` in a shell command
+[source,yaml]
+----
+actions:
+ - title: Run with deploy env
+ shell: /opt/deploy.sh --env {{ .Env.DEPLOY_ENV }} --host {{ .Env.HOSTNAME }}
+----
+
+.Example: use `.Env` in a completion notification
+[source,yaml]
+----
+actions:
+ - title: Backup
+ shell: /opt/backup.sh
+ shellAfterCompleted: "apprise -t 'Backup on {{ .Env.HOSTNAME }}' -b '{{ output }}'"
+----
+
+`.Env` uses the same Go template context as other action variables (e.g. `.Arguments`, `.CurrentEntity`, `.OliveTin`). The map is built from the process environment when OliveTin starts; values are read at template execution time. If a variable is missing, the template engine will report a missing-key error (with `missingkey=error`), so use defaulting when a variable might be unset, e.g. `{{ or .Env.OPTIONAL_VAR "default" }}`.
+
+This feature addresses the need to use environment variables in templates without changing the config loader (see link:https://github.com/OliveTin/OliveTin/issues/840[GitHub issue #840]).
diff --git a/docs/modules/ROOT/pages/advanced_configuration/diagnostics.adoc b/docs/modules/ROOT/pages/advanced_configuration/diagnostics.adoc
new file mode 100644
index 0000000..c12d09e
--- /dev/null
+++ b/docs/modules/ROOT/pages/advanced_configuration/diagnostics.adoc
@@ -0,0 +1,38 @@
+= Diagnostics
+
+OliveTin provides a built-in diagnostics page that can be used to help check how OliveTin is running and help to troubleshoot issues. It's mainly designed for checking SSH configuration at the moment.
+
+This is a screenshot of the diagnostics page, which can be accessed by clicking the "Diagnostics" link in the navigation bar:
+
+image::diagnostics.png[]
+
+== Disabling Diagnostics
+
+The diagnostics page is enabled by default, but you can disable it by using the OliveTin xref::security/acl.adoc#_acls_and_policies_global[security policy configuration], using the defaults, or via an ACL. Examples are shown below for each of these methods.
+
+=== Disable Diagnostics for all users;
+
+[source, yaml]
+----
+logLevel: info
+
+defaultPolicy:
+ showDiagnostics: false
+----
+
+=== Disable Diagnostics expect for admin users
+
+[source, yaml]
+----
+logLevel: info
+defaultPolicy:
+ showDiagnostics: false
+
+accessControlLists:
+ - name: admin
+ matchUsernames:
+ - alice
+ - bob
+ policy:
+ showDiagnostics: true
+----
diff --git a/docs/modules/ROOT/pages/advanced_configuration/intro.adoc b/docs/modules/ROOT/pages/advanced_configuration/intro.adoc
new file mode 100644
index 0000000..1d0f6c8
--- /dev/null
+++ b/docs/modules/ROOT/pages/advanced_configuration/intro.adoc
@@ -0,0 +1,32 @@
+= Advanced Configuration
+
+This section covers advanced configuration options that provide additional control and customization for OliveTin. These options are typically used by users who need fine-grained control over logging, diagnostics, networking, UI customization, and other advanced features.
+
+Topics covered in this section include:
+
+* Application and action logging configuration
+* Diagnostics and troubleshooting tools
+* Environment variable configuration
+* Network port configuration
+* Style modifications for UI customization
+* Prometheus metrics integration
+* Timezone configuration
+* Web UI customization options
+* Version display in the footer (show or hide the application version)
+
+Most users will not need to modify these settings for basic OliveTin usage, but they become important when you need to integrate OliveTin into complex environments or customize its behavior to match specific requirements.
+
+== What's Next?
+
+Explore the advanced configuration options:
+
+* xref:advanced_configuration/logs.adoc[Configure logging] - Set up application and action logging
+* xref:advanced_configuration/logs-calendar.adoc[Use the logs calendar] - Browse execution history visually
+* xref:advanced_configuration/diagnostics.adoc[Use diagnostics] - Access troubleshooting and diagnostic tools
+* xref:advanced_configuration/webui.adoc[Customize the web UI] - Modify the appearance and behavior of the interface
+* xref:advanced_configuration/stylemods.adoc[Apply style mods] - Use style modifications for UI customization
+* xref:advanced_configuration/prometheus.adoc[Set up Prometheus] - Configure metrics collection
+* xref:advanced_configuration/timezones.adoc[Configure timezones] - Set timezone handling for actions
+* xref:advanced_configuration/config_envs.adoc[Use environment variables] - Configure environment variable handling
+* xref:reference/network-ports.adoc[Understand network ports] - Learn about OliveTin's port configuration
+* xref:reference/version_display.adoc[Version display] - Show or hide the application version in the footer
diff --git a/docs/modules/ROOT/pages/advanced_configuration/logs-actions.adoc b/docs/modules/ROOT/pages/advanced_configuration/logs-actions.adoc
new file mode 100644
index 0000000..baead5d
--- /dev/null
+++ b/docs/modules/ROOT/pages/advanced_configuration/logs-actions.adoc
@@ -0,0 +1,79 @@
+[#log-levels]
+= Logging - Actions
+
+[NOTE]
+There are two different types of logs in OliveTin - xref:advanced_configuration/logs.adoc[application logs] and xref:advanced_configuration/logs-actions.adoc[action logs]. This page is about the __action logs__, which are the logs that are generated by the actions that you run in OliveTin.
+
+== Controlling access to see action logs
+
+You can control access to action logs using **permissions**, which are assigned to actions.
+
+=== Disabling logs for all users
+
+[source, yaml]
+----
+logLevel: info
+
+defaultPermissions:
+ logs: false
+----
+
+=== Enabling logs for a specific ACL
+
+[source, yaml]
+----
+logLevel: info
+
+defaultPermissions:
+ logs: false
+
+accessControlLists:
+ - name: admin
+ matchUsernames:
+ - alice
+ - bob
+ permissions:
+ logs: true
+
+ addToEveryAction: true
+
+actions:
+ - title: My Action
+ shell: echo "Hello World"
+----
+
+== Disabling the log viewer
+
+You can disable the log viewer in the interface with xref::security/acl.adoc#_acls_and_policies_global[security policy configuration], using the defaults, or via an ACL. Examples are shown below for each of these methods.
+
+=== Disable log viewer for all users;
+
+[source, yaml]
+----
+logLevel: info
+defaultPolicy:
+ showLogList: false
+----
+
+=== Disable log viewer expect for admin users
+
+[source, yaml]
+----
+logLevel: info
+
+defaultPolicy:
+ showLogList: false
+accessControlLists:
+ - name: admin
+ matchUsernames:
+ - alice
+ - bob
+ policy:
+ showLogList: true
+----
+
+== See Also
+
+* xref:action_customization/savelogs.adoc[Saving Action logs]
+* xref:advanced_configuration/logs.adoc[Application Logs]
+* xref:advanced_configuration/logs-calendar.adoc[Logs Calendar View]
diff --git a/docs/modules/ROOT/pages/advanced_configuration/logs-calendar.adoc b/docs/modules/ROOT/pages/advanced_configuration/logs-calendar.adoc
new file mode 100644
index 0000000..49e588c
--- /dev/null
+++ b/docs/modules/ROOT/pages/advanced_configuration/logs-calendar.adoc
@@ -0,0 +1,55 @@
+[#logs-calendar]
+= Logs Calendar View
+
+The logs calendar view provides a visual way to browse your action execution history. Instead of scrolling through a list of logs, you can see executions displayed on a calendar, making it easy to identify patterns, busy periods, or specific dates when actions were run.
+
+== Accessing the Calendar View
+
+To access the calendar view, navigate to the **Logs** page and click the **Calendar** button in the toolbar. This will switch from the list view to the calendar view.
+
+You can return to the list view at any time by clicking the **Back to list** button.
+
+== Features
+
+=== View Executions on a Calendar
+
+Each action execution is displayed as an event on the calendar. Events show:
+
+* The action title
+* The action icon (if configured)
+
+This gives you a quick visual overview of when actions were executed throughout the month.
+
+=== Navigate Between Months
+
+Use the navigation controls at the top of the calendar to move between months. The calendar will display all logged executions for the visible month.
+
+=== Click on an Event
+
+Click on any event (execution) on the calendar to view the full execution details, including:
+
+* The complete output of the action
+* Execution timing information
+* Exit code and status
+* User who triggered the action
+
+=== Click on a Date
+
+Click on any date on the calendar to filter the logs list view by that specific day. This is useful when you want to see all executions from a particular date in the traditional list format.
+
+After clicking a date, you'll be redirected to the logs list view with a date filter applied. You can clear this filter using the clear button next to the timestamp header.
+
+== Use Cases
+
+The calendar view is particularly useful for:
+
+* **Auditing** - Quickly identify when specific actions were run
+* **Troubleshooting** - Find executions around a specific date when issues occurred
+* **Pattern recognition** - Visualize how often scheduled actions run
+* **Historical review** - Get an overview of system activity over time
+
+== See Also
+
+* xref:advanced_configuration/logs-actions.adoc[Logging - Actions]
+* xref:advanced_configuration/logs.adoc[Logging - Application]
+* xref:action_customization/savelogs.adoc[Saving Action Logs]
diff --git a/docs/modules/ROOT/pages/advanced_configuration/logs.adoc b/docs/modules/ROOT/pages/advanced_configuration/logs.adoc
new file mode 100644
index 0000000..3b0a829
--- /dev/null
+++ b/docs/modules/ROOT/pages/advanced_configuration/logs.adoc
@@ -0,0 +1,51 @@
+[#log-levels]
+= Logging - Application
+
+[NOTE]
+There are two different types of logs in OliveTin - xref:advanced_configuration/logs.adoc[application logs] and xref:advanced_configuration/logs-actions.adoc[action logs]. This page is about the __application logs__, which are the logs that OliveTin itself generates. The action logs are the logs that are generated by the actions that you run in OliveTin.
+
+OliveTin supports a few different log levels. The default logLevel is `INFO`.
+
+You can set a `logLevel` in config.yaml like this;
+
+.`config.yaml`
+[source,yaml]
+----
+logLevel: "INFO"
+
+actions:
+ ....
+----
+
+The supported log levels are;
+
+* `DEBUG` - Every possible log message will be shown. This will use a lot of disk space and is not recommended unless you are a developer / like reading code.
+* `ERROR` - OliveTin rarely uses the `ERROR` log level.
+* `WARN` - Very few messages, only warnings are shown.
+* `INFO` - The defualt log level.
+
+You can change the `logLevel` while OliveTin is running, and it should update as soon as you save your config.yaml. You will always get a log message like this;
+
+[source,bash]
+----
+INFO Setting log level to warning
+----
+
+== JSON Log Format
+
+* OliveTin 2k supports JSON log format from version **2025.10.30**.
+* OliveTin 3k supports JSON log format from version **3000.2.2**.
+
+You can enable JSON log format by setting the `OLIVETIN_LOG_FORMAT` environment variable to `json`.
+
+[source,bash]
+.Example of setting `OLIVETIN_LOG_FORMAT` to `json`.
+----
+root@server: ./OliveTin
+{"commit":"nocommit","date":"nodate","level":"info","msg":"OliveTin initializing","time":"2025-10-30T10:09:55Z","version":"dev"}
+{"level":"debug","msg":"Value of -configdir flag","time":"2025-10-30T10:09:55Z","value":"."}
+{"level":"debug","msg":"servicehost nonwin","time":"2025-10-30T10:09:55Z"}
+{"level":"info","msg":"Setting log level to info","time":"2025-10-30T10:09:55Z"}
+{"level":"info","msg":"OliveTin initialization complete","time":"2025-10-30T10:09:55Z"}
+{"configDir":"/home/xconspirisist/sandbox/Development/OliveTin/OliveTin","level":"info","msg":"OliveTin started","time":"2025-10-30T10:09:55Z"}
+----
diff --git a/docs/modules/ROOT/pages/advanced_configuration/ports.adoc b/docs/modules/ROOT/pages/advanced_configuration/ports.adoc
new file mode 100644
index 0000000..b1d07cd
--- /dev/null
+++ b/docs/modules/ROOT/pages/advanced_configuration/ports.adoc
@@ -0,0 +1,6 @@
+[#ports]
+= Ports
+
+See xref:reference/network-ports.adoc[the network ports] documentation in the reference section.
+
+
diff --git a/docs/modules/ROOT/pages/advanced_configuration/prometheus.adoc b/docs/modules/ROOT/pages/advanced_configuration/prometheus.adoc
new file mode 100644
index 0000000..7c811e2
--- /dev/null
+++ b/docs/modules/ROOT/pages/advanced_configuration/prometheus.adoc
@@ -0,0 +1,40 @@
+[#prometheus]
+= Prometheus
+
+OliveTin supports basic Prometheus metrics, and the project is interested to hear about what more metrics people would find useful, as well!
+
+To enable Prometheus support;
+
+.`config.yaml`
+[source,yaml]
+----
+logLevel: INFO
+
+prometheus:
+ enabled: true
+ defaultGoMetrics: false
+----
+
+It is required to restart OliveTin after changing the `prometheus` configuration.
+
+The `defaultGoMetrics` option will enable the default Go metrics, which expose metrics like go_memstats_alloc_bytes, or go_memstats_heap_alloc_bytes,
+and generally most people don't need these.
+
+This will give you metrics available at http://yourserver:1337/metrics. The page should look something like this;
+
+[source]
+----
+# HELP olivetin_actions_requested_count The actions requested count
+# TYPE olivetin_actions_requested_count gauge
+olivetin_actions_requested_count 0
+# HELP olivetin_config_action_count Then number of actions in the config file
+# TYPE olivetin_config_action_count gauge
+olivetin_config_action_count 18
+# HELP olivetin_config_reloaded_count The number of times the config has been reloaded
+# TYPE olivetin_config_reloaded_count counter
+olivetin_config_reloaded_count 1
+# HELP olivetin_sv_count The number entries in the sv map
+# TYPE olivetin_sv_count gauge
+olivetin_sv_count 49
+----
+
diff --git a/docs/modules/ROOT/pages/advanced_configuration/stylemods.adoc b/docs/modules/ROOT/pages/advanced_configuration/stylemods.adoc
new file mode 100644
index 0000000..6ac66e6
--- /dev/null
+++ b/docs/modules/ROOT/pages/advanced_configuration/stylemods.adoc
@@ -0,0 +1,26 @@
+= Stylemods
+
+There are several style modifications that some people like to use, which can easily be added to OliveTin. The configuration syntax in your `config.yaml` is very simple, and looks like this with a single style mod;
+
+[source,yaml]
+----
+stylemods:
+ - sm-side-icons
+----
+
+You can add as many style mods as you like, but note that some of them may conflict with each other. The style mods are applied in the order they are listed, so if you have a conflict, the last one in the list will take precedence.
+
+== Available Style Mods
+
+* `sm-side-icons`: Display action buttons with the icons on the left hand side rather than above the text.
+* `sm-imageicons-fullwidth`: Display image icons in full width, rather than the default size.
+* `sm-transparent-header`: Make the header background transparent.
+* `sm-transparent-footer`: Make the footer background transparent.
+
+NOTE: The `sm-transparent-header` and `sm-transparent-footer` style mods often fix themes that were designed for OliveTin 2k.
+
+The list of style mods on this page is maintained as new style modifications are added to OliveTin.
+
+== Feature history
+
+* OliveTin `2025.7.29` introduced the ability to use style modifications, or "style mods".
diff --git a/docs/modules/ROOT/pages/advanced_configuration/timezones.adoc b/docs/modules/ROOT/pages/advanced_configuration/timezones.adoc
new file mode 100644
index 0000000..20aa52f
--- /dev/null
+++ b/docs/modules/ROOT/pages/advanced_configuration/timezones.adoc
@@ -0,0 +1,18 @@
+[#timezone]
+= Timezones
+
+OliveTin will obviously use the system time just like all other programs, but when running in a container, time works in a slightly unusual way.
+
+You may be used to using a TZ or TIMEZONE environment variable in your Linux container inages, but this is not a standard that works for all Linux distributions - it's mostly supported by Debain based containers. OliveTin's base container image is fedora-minimal, which deliberately does not include timezone data, to reduce storage space.
+
+To change the time in the OliveTin container, simply bind-mount the correct zone file;
+
+.Same as the container host
+----
+docker create -v /etc/localtime:/etc/localtime -v /etc/OliveTin:/config --name OliveTin docker.io/jamesread/olivetin
+----
+
+.Different timezone to the container host
+----
+docker create -v /usr/share/zoneinfo/Japan:/etc/localtime -v /etc/OliveTin:/config --name OliveTin docker.io/jamesread/olivetin
+----
diff --git a/docs/modules/ROOT/pages/advanced_configuration/webui.adoc b/docs/modules/ROOT/pages/advanced_configuration/webui.adoc
new file mode 100644
index 0000000..6d3405b
--- /dev/null
+++ b/docs/modules/ROOT/pages/advanced_configuration/webui.adoc
@@ -0,0 +1,184 @@
+[#customize-webui]
+= Customize the web UI
+
+The OliveTin web UI is reasonably customizable - parts of the page that you don't need can be hidden when they're not needed.
+
+== Page Title
+
+You can customize the page title;
+
+image::page-title.png[]
+
+.`config.yaml`
+[source,yaml]
+----
+pageTitle: My OliveTin Instance
+----
+
+[#show-nav]
+== Navigation - show / hide
+
+You can choose to hide the navigation elements in OliveTin, to present a simplified user interface.
+
+.The default user interface with the sidebar shown
+image::defaultUiWithNav.png[]
+
+To have OliveTin hide these buttons, add `showNavigation: false` to your config.yaml;
+
+.`config.yaml`
+[source,yaml]
+----
+logLevel: "INFO"
+showNavigation: false
+
+actions:
+ ....
+----
+
+.The same user interface, but with the sidebar hidden (`showNavigation: false`)
+image::defaultUiHideNav.png[]
+
+[#show-navigate-on-start-icons]
+== Navigate-on-start icons on action buttons
+
+When enabled (the default), each action button can show a small icon indicating what happens when the action is started:
+
+* **Popup dialog** — the action opens a popup (e.g. `popupOnStart: execution-dialog`)
+* **Argument form** — the action opens an argument form on start
+* **Run in background** — the action runs without opening a dialog
+
+Set `showNavigateOnStartIcons: false` in your `config.yaml` to hide these indicator icons for a cleaner look.
+
+.`config.yaml`
+[source,yaml]
+----
+showNavigateOnStartIcons: false
+----
+
+[#section-navgiation-style]
+== Section Navigation Style
+
+`sectionNavigationStyle` - You can choose to have the section navigation buttons displayed as a Sidebar (`sidebar` - default), or along the top (`topbar`).
+
+=== Sidebar navigation style (default)
+
+`sectionNavigationStyle: sidebar` looks like this;
+
+image::sidebar.png[]
+
+=== Topbar navigation style
+
+`sectionNavigationStyle: topbar` looks like this;
+
+image::topbar.png[]
+
+[#show-version-number]
+== Version number in the footer
+
+You can control whether the installed OliveTin version is shown in the web interface. When enabled (the default), the footer displays text like **OliveTin 2024.06.02**. When disabled, the footer shows only **OliveTin** with no version number.
+
+This is controlled by the **showVersionNumber** policy (in `defaultPolicy` or per user/group in ACLs). Hiding the version also hides any "new version available" link in the footer and redacts the version in xref:troubleshooting/server-diagnostics.adoc[server diagnostics] output, which can be useful for privacy when sharing reports.
+
+* xref:reference/version_display.adoc[Version display] — full configuration and policy examples
+
+[#show-new-versions]
+== New version available - show/hide
+
+You can disable the "new version" information in the footer - the default for `showNewVersions` is `true`;
+
+.`config.yaml`
+[source,yaml]
+----
+logLevel: "INFO"
+showNewVersions: false
+----
+
+OliveTin does not check for updates by default. To enable it, see xref:reference/updateChecks.adoc[enable update checking].
+
+
+[#show-footer]
+== Footer visibility - show / hide
+
+You can disable the entire footer, if you would like a really minimal interface. The default for `showFooter` is `true`.
+
+.`config.yaml`
+[source,yaml]
+----
+logLevel: "INFO"
+showFooter: false
+----
+
+This means the <> configuration option will automatically be `false` as well.
+
+== Additional section navigation links
+
+You can add custom links to the OliveTin navigation bar. This is useful if you want to link to other OliveTin instances, or other web applications.
+
+[source,yaml]
+----
+additionalNavigationLinks:
+ - title: Duck Duck Go
+ url: https://duckduckgo.com
+ target: _blank
+----
+
+This will render like this;
+
+image::additionalNavigationLinks.png[]
+
+[#custom-js]
+== Custom JavaScript
+
+This is considered an advanced feature, and is not recommended unless you like writing your own code.
+
+You can add custom JavaScript to OliveTin, which will be executed on every page load. This can be useful for adding custom functionality to the web UI.
+
+1. The custom javascript should be in a file called `custom.js` and saved in `custom-webui/`, which should be in the same directory as your `config.yaml`.
+2. You can put whatever code you like really in your `custom.js` file.
+3. Set `enableCustomJs: true` in your `config.yaml` to enable this feature.
+4. Restart OliveTin. Note that the custom JavaScript will only be loaded once on startup, so if you are changing the custom JavaScript while OliveTin is running, you will need to restart OliveTin to see the changes.
+
+If the browser blocks your script or network calls with Content Security Policy errors, see xref:security/content_security_policy.adoc[Content Security Policy headers] for how to adjust or disable the CSP sent by OliveTin.
+
+== Custom CSS (with a custom theme)
+
+You can customize OliveTin with themes, but it's also possible to write your on very simple theme that contains just a few CSS rules to change the look and feel of OliveTin. This is very useful if you just want to change the colours of OliveTin, or hide a few elements.
+
+=== Writing a simple theme with a CSS change
+
+You'll need to create a new theme, and let's assume our theme name is going to be called `uihack`. OliveTin themes are simply a directory of CSS and other assets. OliveTin looks for a directory called `custom-webui/themes/` in the same directory as your `config.yaml` file.
+
+Start by creating a directory called `custom-webui/themes/uihack` relative to the same directory as your `config.yaml` file. In this directory, create a file called `theme.css`.
+
+[source,yaml]
+----
+├── config.yaml
+└── custom-webui
+ └── themes
+ └── uihack
+ └── theme.css
+----
+
+Here's an example of what your `theme.css` should contain;
+
+```css
+body {
+ background-color: red;
+}
+```
+
+=== Setup OliveTin config to use your theme
+
+Now you need to tell OliveTin to use your new theme. To do this, set `themeName: uihack` in your OliveTin config.yaml and restart OliveTin.
+
+```yaml
+logLevel: "INFO"
+themeName: uihack
+```
+
+[WARNING]
+OliveTin will by default only read theme.css once on startup. If you are intending to change theme.css while OliveTin is running, set `themeCacheDisabled: true` in your config.yaml. This will make OliveTin read theme.css on every request, and is useful for development.
+
+Restart OliveTin for the theme change to take effect. Beware of the theme cache mentioned above, if you are making changes to the CCS and refreshing the page a few times.
+
+* xref:reference/reference_themes_for_developers.adoc[More information on theme development]
diff --git a/docs/modules/ROOT/pages/api/intro.adoc b/docs/modules/ROOT/pages/api/intro.adoc
new file mode 100644
index 0000000..4415e33
--- /dev/null
+++ b/docs/modules/ROOT/pages/api/intro.adoc
@@ -0,0 +1,27 @@
+[#api]
+
+= API Overview
+This section of the documentation is intended for developers, and those who want to hack around with OliveTin and extend it. This page provides a few pointers to get started.
+
+**Short version**:
+
+* http://olivetinServer:1337/api on your OliveTin server to get the REST API.
+* link:http://docs.olivetin.app/api/swagger/[Swagger] documents the API.
+
+**Longer version**: The OliveTin API is formally defined using the Protobuf IDL, which generates gRPC stubs, as well as a REST Gateway.
+
+The REST API gateway is used by the WebUI, and you can use it too by default - it is exposed at "/api" by default.
+
+The gRPC API only listens on localhost default, but it can be set to listen publicly. See xref:reference/network-ports.adoc[the network ports documentation] for a better description of how the APIs are exposed. Most people do not need to use the gRPC API.
+
+== Links
+
+* link:http://docs.olivetin.app/api/swagger/[OliveTin 2k API: Swagger UI]
+* link:http://docs.olivetin.app/api/swagger/OliveTin.openapi.json[OliveTin 2k API: OpenAPI JSON Definition]
+* link:http://docs.olivetin.app/api/swagger/3k/[OliveTin 3k API: Swagger UI]
+* link:http://docs.olivetin.app/api/swagger/3k/OliveTin.openapi.json[OliveTin 3k API: OpenAPI JSON Definition]
+* link:https://github.com/OliveTin/OliveTin/blob/main/proto/olivetin/api/v1/olivetin.proto[The OliveTin Protobuf file].
+
+Please do talk to the developers on Discord if you'd like help using the API, or you're thinking about building something interesting using the API!
+
+
diff --git a/docs/modules/ROOT/pages/api/login.adoc b/docs/modules/ROOT/pages/api/login.adoc
new file mode 100644
index 0000000..1fe09bb
--- /dev/null
+++ b/docs/modules/ROOT/pages/api/login.adoc
@@ -0,0 +1,28 @@
+= Local User Login via API
+
+OliveTin supports serveral different ways to login, and most installations will probably login via reverse proxy, or via local user login.
+
+To login via local user login, you can use the following API call: `/LocalUserLogin`. This is documented in the API documentation which can be found here; https://docs.olivetin.app/api/
+
+The API call is a POST request, and you need to provide a `username` and `password` as a JSON object in the body of the request. Here is an example of how you can login via a cURL request;
+
+```bash
+user@host: curl -X POST "http://olivetin-server:1337/api/LocalUserLogin" -H "accept: application/json" -H "Content-Type: application/json" -d '{"username":"admin","password":"toomanysecrets"}' -v
+...
+< Set-Cookie: olivetin-sid-local=c6c5b2b3-a58b-4dbc-b070-ed7bdd3f1956; Path=/; Max-Age=31556952; HttpOnly
+...
+{"success":true}
+```
+
+You can see from the above example that the header response sets a cookie called "olivetin-sid-local" to a UUID which is your session ID. You must include this cookie with future requests to authenticate yourself.
+
+You can also that the body of the response is a JSON object with a simple `success` key, which will be `true` if the login was successful, and `false` if it was not.
+
+== Check login with `WhoAmI`
+
+You can check your current login status by using the `/WhoAmI` API call. This is a GET request, and you need to provide the `olivetin-sid-local` cookie in the request. Here is an example of how you can check your login status via a cURL request;
+
+```bash
+user@host: curl -X GET http://localhost:1337/api/WhoAmI -H "accept: application/json" -H 'Content-Type: application/json' -b "olivetin-sid-local=cd33aa9c-c613-473e-8581-2b742716ab8e"
+{"authenticatedUser":"admin", "usergroup":"", "provider":"local", "acls":[], "sid":""}
+```
diff --git a/docs/modules/ROOT/pages/api/method_StartAction.adoc b/docs/modules/ROOT/pages/api/method_StartAction.adoc
new file mode 100644
index 0000000..31b7dd7
--- /dev/null
+++ b/docs/modules/ROOT/pages/api/method_StartAction.adoc
@@ -0,0 +1,35 @@
+= API Method: StartAction
+
+This is the method the OliveTin web UI uses to start actions, and is probably the best method to use if you are writing scripts.
+
+* **HTTP Method**: `POST`
+* **Request Type**: OliveTin request object
+* **Response Type**: Execution Tracking ID
+
+There are several variants of this API call available which might be easier for scripts (or humans) to work with:
+
+include::partial$api/start_action_methods.adoc[]
+
+== Example API call: Start an action using `StartAction`
+
+[source,bash]
+.curl
+----
+user@host: curl "http://olivetin.webapps.teratan.lan/api/StartAction" --json '{"bindingId": "nuclear_reactor_shutdown"}'
+----
+
+[source,powershell]
+.Powershell
+----
+PS C:\Users\xcons> $json = '{"bindingId": "deploy_attack_gnomes"}'
+PS C:\Users\xcons> Invoke-RestMethod -Method "Post" -Uri "http://olivetinServer:1337/api/StartAction" -Body $json
+----
+
+== Example API call: Start an action using `StartAction` with arguments
+
+[source,bash]
+.curl
+----
+user:host: curl 'http://olivetin.example.com/api/StartAction' --json '{"bindingId": "Ping_host", "arguments": [{"name": "host", "value": "example.com"},{"name": "count", "value": "1"}]}'
+----
+
diff --git a/docs/modules/ROOT/pages/api/method_StartActionAndWait.adoc b/docs/modules/ROOT/pages/api/method_StartActionAndWait.adoc
new file mode 100644
index 0000000..0dd5ec0
--- /dev/null
+++ b/docs/modules/ROOT/pages/api/method_StartActionAndWait.adoc
@@ -0,0 +1,12 @@
+= API Method: StartActionAndWait
+
+This method is useful is you are writing a script, and want to wait for the action to finish so that you can check the result, or get the output.
+
+* **HTTP Method**: `POST`
+* **Request Type**: OliveTin request object
+* **Response Type**: Log Entry (waits for the action to finish)
+
+There are several variants of this API call available which might be easier for scripts (or humans) to work with:
+
+
+include::partial$api/start_action_methods.adoc[]
diff --git a/docs/modules/ROOT/pages/api/method_StartActionByGet.adoc b/docs/modules/ROOT/pages/api/method_StartActionByGet.adoc
new file mode 100644
index 0000000..f0b0320
--- /dev/null
+++ b/docs/modules/ROOT/pages/api/method_StartActionByGet.adoc
@@ -0,0 +1,29 @@
+= API Method: StartActionByGet
+
+This is the method that allows you to specify the action ID in the URL, and is probably the best to do quick integrations - QR Codes, streamdeck, etc. You cannot pass arguments using this method.
+
+* **HTTP Method**: `GET`
+* **Request Type**: Action ID in the URL
+* **Response Type**: Execution Tracking ID
+
+include::partial$api/start_action_methods.adoc[]
+
+== Example API call; Start an action by ID in the URL
+
+.curl
+----
+user@host: curl http://olivetin.example.com/api/StartActionByGet/pingGithub
+----
+
+The corresponding config.yaml would look like this;
+
+[source,yaml]
+----
+actions:
+ - title: Ping GitHub.com
+ id: pingGithub
+ shell: ping github.com -c 1
+----
+
+IDs are used by these API calls, as you probably want the interface to display a human-readable title, whereas the API call doesn't want to have spaces or punctuation.
+
diff --git a/docs/modules/ROOT/pages/api/method_StartActionByGetAndWait.adoc b/docs/modules/ROOT/pages/api/method_StartActionByGetAndWait.adoc
new file mode 100644
index 0000000..baf1fe6
--- /dev/null
+++ b/docs/modules/ROOT/pages/api/method_StartActionByGetAndWait.adoc
@@ -0,0 +1,11 @@
+= API Method: StartActionByGetAndWait
+
+This method is also a very easy way to quickly start an action, but it waits for the action to finish before returning the result. This is useful if you want to get the output of the action or check its result without having to poll for it.
+
+* **HTTP Method**: `GET`
+* **Request Type**: Action ID in the URL
+* **Response Type**: Log Entry (waits for the action to finish)
+
+There are several variants of this API call available which might be easier for scripts (or humans) to work with:
+
+include::partial$api/start_action_methods.adoc[]
diff --git a/docs/modules/ROOT/pages/api/misc.adoc b/docs/modules/ROOT/pages/api/misc.adoc
new file mode 100644
index 0000000..feb1542
--- /dev/null
+++ b/docs/modules/ROOT/pages/api/misc.adoc
@@ -0,0 +1,20 @@
+= Misc API calls
+
+== Example API call: Get the dashboard buttons ("components")
+
+.curl
+----
+user@host: curl http://olivetinServer:1337/api/GetDashboardComponents
+----
+
+== Example API call: readyz Healthcheck
+
+This is useful for configuring healthchecks in docker containers, or on Kubernetes.
+
+.curl
+----
+user@host: curl http://olivetinServer:1337/api/readyz
+{"status": "ok"}
+----
+
+The response will always be "status: ok" to indicate that the API is up, or it will timeout.
diff --git a/docs/modules/ROOT/pages/api/start_action.adoc b/docs/modules/ROOT/pages/api/start_action.adoc
new file mode 100644
index 0000000..3e4624e
--- /dev/null
+++ b/docs/modules/ROOT/pages/api/start_action.adoc
@@ -0,0 +1,105 @@
+[#api-start-action]
+= Starting Actions from the API
+
+There are several variants of this API call available which might be easier for scripts (or humans) to work with!
+
+include::partial$api/start_action_methods.adoc[]
+
+[#api-request-idurl]
+== Request type: Action ID in the URL
+
+Used by:
+
+*** xref:api/method_StartActionByGet.adoc[StartActionByGet]
+*** xref:api/method_StartActionByGetAndWait.adoc[StartActionByGetAndWait]
+
+If you are trying to integrate OliveTin with your own scripts or processes, it's probably easiest to start actions by using an ID directly in the URL, <>.
+
+[#api-request-obj]
+== Request type: OliveTin request object
+
+Used by:
+
+*** xref:api/method_StartAction.adoc[StartAction]
+*** xref:api/method_StartActionAndWait.adoc[StartActionAndWait]
+
+[source,json]
+.OliveTin request object structure
+----
+{
+ "actionId": "string",
+ "arguments": [
+ {
+ "name": "string",
+ "value": "string"
+ }
+ ],
+ "uniqueTrackingId": "string"
+}
+----
+
+To find your Action ID, and understand how Action IDs work, see the xref:action_customization/ids.adoc[Action ID] documentation
+
+If you need more control over the execution, then the only other option is to submit a `OliveTin reqjest object`, which is just a very simple JSON structure like this;
+
+[source,json]
+----
+{
+ "actionId": "Generate cryptocurrency",
+ "arguments": [],
+ "uniqueTrackingId": "my-tracking-id",
+}
+----
+
+More detailed examples can be seen below.
+
+[#api-response-trackingid]
+== Response type: Execution Tracking ID
+
+Used by:
+
+*** xref:api/method_StartAction.adoc[StartAction]
+*** xref:api/method_StartActionByGet.adoc[StartActionByGet]
+
+.Example Execution Tracking ID response
+[source,json]
+----
+{"executionTrackingId":"5bb4860c-bbd0-4bc9-a7d6-42240551500c"}
+----
+
+[#api-response-logentry]
+== Response type: LogEntry
+
+Used by:
+
+*** xref:api/method_StartActionAndWait.adoc[StartActionAndWait]
+*** xref:api/method_StartActionByGetAndWait.adoc[StartActionByGetAndWait]
+
+.Example log entry
+[source,json]
+----
+{
+ "logEntry": {
+ "datetimeStarted": "2024-02-27 14:14:49",
+ "actionTitle": "Restart httpd on server1",
+ "stdout": "",
+ "stderr": "",
+ "timedOut": true,
+ "exitCode": -1,
+ "user": "",
+ "userClass": "",
+ "actionIcon": "🔄",
+ "tags": [
+
+ ],
+ "executionTrackingId": "b04b1e90-d457-4158-b7dc-da9e81f21568",
+ "datetimeFinished": "2024-02-27 14:14:52",
+ "actionId": "restart_httpd",
+ "executionStarted": true,
+ "executionFinished": true,
+ "blocked": false
+ }
+}
+----
+
+
diff --git a/docs/modules/ROOT/pages/args/env.adoc b/docs/modules/ROOT/pages/args/env.adoc
new file mode 100644
index 0000000..e36d27c
--- /dev/null
+++ b/docs/modules/ROOT/pages/args/env.adoc
@@ -0,0 +1,95 @@
+[#env-vars]
+= Environment variables
+
+All argument names and values are also passed as environment variables as well, which can be very useful when passing several arguments to a script, for example.
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: Print names of new files
+ shell: /opt/newfile.py
+ arguments:
+ - name: filename
+ type: unicode_identifier
+
+ - name: filesizebytes
+ type: unicode_identifier
+
+ - name: fileisdir
+ type: unicode_identifier
+
+ execOnFileCreatedInDir:
+ - /home/user/Downloads/
+----
+
+This is an example of a python script using the environment variables;
+
+[source,python]
+.`/opt/newfile.py`
+----
+#!/usr/bin/env python
+
+import os
+
+print(os.environ['OLIVETIN'])
+print(os.environ['FILENAME'])
+print(os.environ['FILESIZEBYTES'])
+print(os.environ['FILEISDIR'])
+----
+
+[#execution-request-variables]
+== Execution Request Variables
+
+OliveTin injects two execution request variables into every action execution. They are available as template variables (e.g. in `shell`, `shellAfterCompleted`, or other template fields) and as environment variables passed to the process.
+
+* `ot_username` — The username of the user who started the execution. In templates (version 3k) use `{{ .Arguments.ot_username }}`; in the process environment it is `OT_USERNAME`. For unauthenticated or automated runs this may be `guest`, `cron`, or similar, depending on how the action was triggered.
+* `ot_executionTrackingId` — A unique identifier for this execution. In templates (version 3k) use `{{ .Arguments.ot_executionTrackingId }}`; in the process environment it is `OT_EXECUTIONTRACKINGID`. Useful for logging, correlating with the API or execution log, or idempotency in scripts.
+
+In version 2k, the template syntax was `{{ ot_username }}` and `{{ ot_executionTrackingId }}` (without the `.Arguments.` prefix). Version 3k uses the `.Arguments.` form.
+
+Example in a shell command (version 3k):
+
+[source,yaml]
+----
+shell: echo "Run by {{ .Arguments.ot_username }} (execution {{ .Arguments.ot_executionTrackingId }})"
+----
+
+Example in a script using environment variables:
+
+[source,shell]
+----
+#!/bin/sh
+echo "Started by $OT_USERNAME with tracking id $OT_EXECUTIONTRACKINGID"
+----
+
+In xref:action_execution/aftercompletion.adoc[Execute after completion] (`shellAfterCompleted`), the same variables are available as template variables (e.g. `{{ .Arguments.ot_username }}`, `{{ .Arguments.ot_executionTrackingId }}` in version 3k); user-defined argument values are not passed there.
+
+[#olivetin-env-var]
+== The OLIVETIN environment variable
+
+OliveTin sets the environment variable `OLIVETIN` to `1` for every action it runs. Scripts can check this variable to detect whether they are running inside OliveTin (for example, to adjust logging, skip interactive prompts, or enable OliveTin-specific behavior).
+
+[source,shell]
+.Example: detect OliveTin in a shell script
+----
+#!/bin/sh
+if [ "$OLIVETIN" = "1" ]; then
+ echo "Running under OliveTin"
+else
+ echo "Running outside OliveTin"
+fi
+----
+
+== Using process environment in templates
+
+To use the *process* environment (the environment OliveTin was started with) inside action template fields such as `shell` or `shellAfterCompleted`, use the `.Env` template variable: `{{ .Env.VAR_NAME }}`. This substitutes the value at execution time. See xref:advanced_configuration/config_envs.adoc#using-env-in-template-replacements[Using .Env in template replacements] for details and examples.
+
+== Notes
+
+. Argument names are converted to uppercase for environment variables, `name: filename` becomes `FILENAME`.
+. OliveTin sets `OLIVETIN=1` in the process environment for every action; see <> above.
+. The execution request variables are exposed as `OT_USERNAME` and `OT_EXECUTIONTRACKINGID` in the process environment; see <> above.
+. The environment variables are passed into the execution context which uses a shell (/bin/sh on Linux), so it is also possible to use them with the $ notation in the `shell` line, like this; `shell: echo $FILENAME` for example.
+
+
diff --git a/docs/modules/ROOT/pages/args/input.adoc b/docs/modules/ROOT/pages/args/input.adoc
new file mode 100644
index 0000000..3cf3bf1
--- /dev/null
+++ b/docs/modules/ROOT/pages/args/input.adoc
@@ -0,0 +1,50 @@
+[#arg-textbox]
+= Input: Textbox
+
+Many times you need to customize how an action/shell command is run, with arguments. For example;
+
+----
+echo "Hello world"
+----
+
+In the example above, `Hello world` is an argument passed to the `echo` command. OliveTin allows you to add pre-defined, and free-text arguments to commands in this way. Below is the OliveTin version of the `echo` command shown above;
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: echo a message
+ icon: smile
+ shell: echo {{ message }}
+ arguments:
+ - name: message
+ default: Hello World
+ type: ascii_sentence
+
+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
+----
+
+This will give you a normal button, like this;
+
+image::args1.png[]
+
+However, when you click on it, you'll get a prompt to enter arguments, like this;
+
+image::args2.png[]
+
+You'll see that the type is set to `ascii_sentence`. This applies fairly safe
+input validation to arguments, so that only a-z, 0-9, spaces and .'s are allowed.
+
+When you start the action, and it's finished, go to the "logs" view to view the output of the command we've just run.
+
+image::args3.png[]
+
+
diff --git a/docs/modules/ROOT/pages/args/input_checkbox.adoc b/docs/modules/ROOT/pages/args/input_checkbox.adoc
new file mode 100644
index 0000000..09196aa
--- /dev/null
+++ b/docs/modules/ROOT/pages/args/input_checkbox.adoc
@@ -0,0 +1,21 @@
+[#confirmation]
+= Input: Checkbox/Boolean
+
+The `checkbox` type argument is a simple checkbox that can be used to toggle a boolean value. It can be especially useful to pass flags to your actions.
+
+[source,yaml]
+----
+actions:
+ - title: remove files
+ shell: rm {{ useTheForce }} /tmp/Downloads/
+ arguments:
+ - title: Use rm -rf?
+ name: useTheForce
+ type: checkbox
+ choices:
+ - title: 1
+ value: "-rf"
+
+ - title: 0
+ value: ""
+----
diff --git a/docs/modules/ROOT/pages/args/input_confirmation.adoc b/docs/modules/ROOT/pages/args/input_confirmation.adoc
new file mode 100644
index 0000000..6eb0f93
--- /dev/null
+++ b/docs/modules/ROOT/pages/args/input_confirmation.adoc
@@ -0,0 +1,21 @@
+[#confirmation]
+= Input: Confirmation
+
+The `confirmation` type argument is a special argument type, which simply disables the "Start" button until a checkbox is ticked. This can be useful if you have an action with no other arguments, but you want to prevent accidental button-clicks starting the action.
+
+[source,yaml]
+----
+actions:
+ - title: Delete old backups
+ icon: ashtonished
+ shell: rm -rf /opt/oldBackups/
+ arguments:
+ - type: confirmation
+ title: Are you sure?!
+----
+
+image::action-confirmation.png[]
+
+Notice in the webui the "start" button is disabled.
+
+
diff --git a/docs/modules/ROOT/pages/args/input_datetime.adoc b/docs/modules/ROOT/pages/args/input_datetime.adoc
new file mode 100644
index 0000000..3d9b385
--- /dev/null
+++ b/docs/modules/ROOT/pages/args/input_datetime.adoc
@@ -0,0 +1,30 @@
+[#arg-datetime]
+= Input: DateTime
+
+OliveTin supports datetime pickers - note that these do NOT add your timezone, so it up to your scripts / commands to interpret which timezone is being used.
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: Print your favourite datetime!
+ shell: echo {{ my_favourite_time }}
+ arguments:
+ - type: datetime
+ title: My Favourite DateTime
+----
+
+image::arg-datetime.png[]
+
+== Format & Validation
+
+[NOTE]
+====
+The OliveTin server does try to parse and validate the date on the server side to prevent dangerous input, but there is no validation in the browser, beyond what your browser might do to prevent you from picking an invalid date. +
+
+**This is safe**, as what really matters is what the server allows to be passed to be executed - and that is checked.
+====
+
+At the time of writing, it is not yet possible to specify only a date, or only a time, or change the date / time format.
+
+include::partial$args/reject-null.adoc[]
diff --git a/docs/modules/ROOT/pages/args/input_dropdown.adoc b/docs/modules/ROOT/pages/args/input_dropdown.adoc
new file mode 100644
index 0000000..fe0db77
--- /dev/null
+++ b/docs/modules/ROOT/pages/args/input_dropdown.adoc
@@ -0,0 +1,83 @@
+[#arg-dropdowns]
+= Input: Dropdowns
+
+Predefined choices are normally the safest type of arguments, because users are limited to only enter values that you specify.
+
+[source,yaml]
+----
+actions:
+ - title: Print a message
+ icon: smile
+ shell: echo "{{ message }}"
+ arguments:
+ - name: message
+ description: The message you want to print out.
+ choices:
+ - title: Hello
+ value: Hello there!
+
+ - title: Goodbye
+ value: Aww, goodbye. :-(
+----
+
+Note that when predefined choices are used, the argument type is ignored.
+
+This is what it looks like in the web interface;
+
+image::args4.png[]
+
+Then finally, when you execute this command, it would look something like this (remember that this is just a basic "echo" command).
+
+image::args-choices-exec.png[]
+
+In the logs, you can then click on the log entry link to open the results;
+
+image::args3.png[]
+
+[#args-dropdown-entities]
+== Using Entities in Dropdowns
+
+Dropdowns can also be populated with a list of entities, like this;
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: restart container
+ shell: 'docker restart {{ containerToRestart }}'
+ arguments:
+ - name: containerToRestart
+ entity: container
+ title: 'Select Container'
+ choices:
+ - value: '{{ container.Names }}'
+ title: '{{ container.Names }}'
+
+entities:
+ - file: entities/containers.json
+ name: container
+----
+
+This is what it looks like in the web interface;
+
+image::args-choices-entities.png[]
+
+include::partial$args/reject-null.adoc[]
+
+== Default values
+
+Dropdown arguments can also have default values. This is done by adding a `default` key to the argument definition.
+
+include::partial$config-start.adoc[]
+----
+actions:
+ - title: "Print your favorite movie"
+ shell: echo '{{ movie }} is amazing'
+ arguments:
+ - name: movie
+ choices:
+ - value: "Star Wars"
+ - value: "Star Trek"
+ - value: "The Matrix"
+ default: "Star Trek"
+----
diff --git a/docs/modules/ROOT/pages/args/input_textarea.adoc b/docs/modules/ROOT/pages/args/input_textarea.adoc
new file mode 100644
index 0000000..2e06980
--- /dev/null
+++ b/docs/modules/ROOT/pages/args/input_textarea.adoc
@@ -0,0 +1,21 @@
+[#arg-textarea]
+= Input: Textarea
+
+OliveTin supports multi-line text inputs, which can be useful for longer messages or scripts. You should set your argument `type` to `raw_string_multiline` to use these.
+
+As the name implies, textareas are raw, and are NOT validated by any regex.
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: Save text to file
+ shell: echo "$CONTENT" > file
+ arguments:
+ - type: raw_string_multiline
+ name: content
+----
+
+This renders like this;
+
+image::args-multiline-text.png[]
diff --git a/docs/modules/ROOT/pages/args/intro.adoc b/docs/modules/ROOT/pages/args/intro.adoc
new file mode 100644
index 0000000..5945296
--- /dev/null
+++ b/docs/modules/ROOT/pages/args/intro.adoc
@@ -0,0 +1,23 @@
+[#args]
+= Introduction to Arguments
+
+Actions and commands that OliveTin runs, without arguments, are generally quite safe - only that command can be run, without modifications. However, many users need the flexibility to set options on that command - normally called command line arguments. In OliveTin, arguments are defined in a shell commands like `echo {{ message }}`, with a bit of extra configuration.
+
+Examples of valid argument names are `{{ personName }}`, `{{ customer_number }}` and `{{ ISBN11_code }}`.
+
+* a-z (case insensitive)
+* _ is allowed
+* numbers are allowed (argument names can also start with numbers)
+* all other characters are invalid for argument names.
+
+== What's Next?
+
+Now that you understand how arguments work, explore the different argument types and features:
+
+* xref:args/types.adoc[Argument types] - Learn about different input types (text, dropdown, checkbox, etc.)
+* xref:args/safety.adoc[Argument safety] - Understand how OliveTin keeps arguments safe
+* xref:args/suggestions.adoc[Argument suggestions] - Add dynamic suggestions to help users
+* xref:args/regex.adoc[Input validation with regex] - Validate user input with regular expressions
+* xref:args/env.adoc[Environment variables] - Use arguments to set environment variables
+* xref:action_examples/intro.adoc[See examples] - View real-world examples using arguments
+
diff --git a/docs/modules/ROOT/pages/args/password.adoc b/docs/modules/ROOT/pages/args/password.adoc
new file mode 100644
index 0000000..7016531
--- /dev/null
+++ b/docs/modules/ROOT/pages/args/password.adoc
@@ -0,0 +1,19 @@
+= Password
+
+Sometimes you want to mask the input you pass, and a password field is useful for this.
+
+[WARNING]
+Passwords are passed to the OliveTin server in cleartext (unless you're using HTTPS), and are just treated as a string on the server side.
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: echo a message
+ icon: smile
+ shell: echo {{ my_password }}
+ arguments:
+ - name: my_password
+ type: password
+----
+
diff --git a/docs/modules/ROOT/pages/args/regex.adoc b/docs/modules/ROOT/pages/args/regex.adoc
new file mode 100644
index 0000000..b173795
--- /dev/null
+++ b/docs/modules/ROOT/pages/args/regex.adoc
@@ -0,0 +1,25 @@
+[#args-custom-regex]
+= Custom regex
+
+OliveTin version 2024.02.081 and above support custom regex patterns for argument types. Here is an example to validate against any 3 letter word;
+
+NOTE: The regex pattern should be enclosed in single quotes, otherwise you will probably get a YAML error when starting OliveTin.
+
+[source,yaml]
+.`config.yaml`
+----
+actions:
+ - title: echo a message
+ icon: smile
+ shell: echo "{{ message }}"
+ arguments:
+ - name: message
+ type: 'regex:^\w\w\w$'
+----
+
+The site http://regex101.com is a good place to test your regex patterns. OliveTin checks your regex 2 times;
+
+. **Regex in the browser** (which probably uses PCRE or Perl Compatible Regular Expressions) - this is so that the browser can give you a nice validation message. This is ignored when it reaches the server though, or if you are using the API directly. Select "PCRE" on the regex101 site when testing.
+. **Regex on the server** (which uses Golang's regex engine) - this is the one that actually validates the input. Select "Golang" on the regex101 site when testing.
+
+You cannot specify different regex patterns for the browser and server. The regex pattern you create will need to be compatible with both types of regex engine.
diff --git a/docs/modules/ROOT/pages/args/safety.adoc b/docs/modules/ROOT/pages/args/safety.adoc
new file mode 100644
index 0000000..1260527
--- /dev/null
+++ b/docs/modules/ROOT/pages/args/safety.adoc
@@ -0,0 +1,12 @@
+= Important Safety Warning
+
+Before you continue, it's important to read through this safety warning.
+
+OliveTin supports customization of command line arguments, but there is a element of risk. For example, if your command is `echo {{ message }}`, and you allow your users to set `{{ message }}` to the value `"" && rm -rf /` , then you've got real problems. For this reason, OliveTin tries to give you useful ways to restrict what users are allowed to enter - with **argument types**.
+
+However, here are some important rules to try and follow with argument types;
+
+* Use the most restrictive argument types when possible - `ascii` and `int`. This will stop users entering argument values that might be used dangerously, but it's not foolproof. For example, if you have a command like `createSnapshot.sh --count {{ snapshotCount }}`, and set `snapshotCount` to `int`, then at least users will only be able to enter integer numbers. However, nothing stops them entering crazy values like 9999.
+* Don't give access to actions with arguments to people you don't trust. Please don't ever put your OliveTin install on the public internet!
+
+
diff --git a/docs/modules/ROOT/pages/args/suggestions.adoc b/docs/modules/ROOT/pages/args/suggestions.adoc
new file mode 100644
index 0000000..71e47c5
--- /dev/null
+++ b/docs/modules/ROOT/pages/args/suggestions.adoc
@@ -0,0 +1,129 @@
+[#arg-suggestions]
+= Suggestions
+
+Argument inputs can also have "suggested" values, which can make it quicker to type commonly used options. The way that these are displayed will vary depending on your browser, as they are implemented as a modern HTML5 browser feature called "datalist".
+
+Suggestions are configured like this;
+
+[source,yaml]
+.Configuration example of input suggestions
+----
+actions:
+ - title: Restart Docker Container
+ icon: restart
+ shell: docker restart {{ container }}
+ arguments:
+ - name: container
+ title: Container name
+ suggestions:
+ - plex:
+ - graefik:
+ - grafana:
+ - wifi-controller: WiFi Controller
+ - firewall-controller: Firewall Controller
+----
+
+In the examples above, there are 5 suggestions. The first 3 suggestions contain a suggestion with a blank title. The last 2 suggestions contain a human readable title (eg: `wifi-controller` is the suggestion, and `WiFi Controller` is the title).
+
+NOTE: `suggestions:` is a yaml **map**, not a **list**. If you leave the title empty you must still end the suggestion with a ":".
+
+== Examples
+
+.Screenshot of input suggestions with Firefox on Linux.
+image::arg-suggestions-firefox.png[]
+
+.Screenshot of input suggestions with Chrome on Linux.
+image::arg-suggestions-chrome.png[]
+
+[#suggestions-browser-key]
+== Browser-Stored Suggestions
+
+In addition to static suggestions defined in your configuration, OliveTin can remember values that users have previously entered and offer them as suggestions for future use. This is enabled using the `suggestionsBrowserKey` property.
+
+When a user submits an action with a `suggestionsBrowserKey` configured, the entered value is saved in the browser's local storage. The next time the user opens the same form (or any form with the same key), those previously-used values appear as suggestions alongside any static suggestions.
+
+=== Basic Usage
+
+[source,yaml]
+----
+actions:
+ - title: SSH to Server
+ shell: ssh {{ hostname }}
+ arguments:
+ - name: hostname
+ title: Hostname
+ description: Server to connect to
+ suggestionsBrowserKey: ssh-hosts
+----
+
+With this configuration:
+
+1. The first time a user runs this action and enters `server1.example.com`, that value is saved
+2. The next time they open the action, `server1.example.com` appears as a suggestion
+3. Each new unique value they enter is added to the suggestions list
+
+=== Sharing Suggestions Across Arguments
+
+Multiple arguments can share the same `suggestionsBrowserKey`, allowing suggestions to be reused across different actions or arguments. This is useful when the same type of value is used in multiple places.
+
+[source,yaml]
+----
+actions:
+ - title: Ping Host
+ shell: ping -c 4 {{ host }}
+ arguments:
+ - name: host
+ title: Hostname
+ suggestionsBrowserKey: network-hosts
+
+ - title: SSH to Host
+ shell: ssh {{ server }}
+ arguments:
+ - name: server
+ title: Server
+ suggestionsBrowserKey: network-hosts
+
+ - title: Traceroute
+ shell: traceroute {{ destination }}
+ arguments:
+ - name: destination
+ title: Destination
+ suggestionsBrowserKey: network-hosts
+----
+
+In this example, all three actions share the `network-hosts` key. If a user enters `192.168.1.100` in the Ping action, that value will also appear as a suggestion in the SSH and Traceroute actions.
+
+=== Combining Static and Browser Suggestions
+
+You can use both static `suggestions` and `suggestionsBrowserKey` together. Both sets of suggestions will be displayed to the user:
+
+[source,yaml]
+----
+actions:
+ - title: Deploy to Environment
+ shell: /opt/scripts/deploy.sh {{ environment }}
+ arguments:
+ - name: environment
+ title: Environment
+ suggestions:
+ production: Production Server
+ staging: Staging Server
+ development: Development Server
+ suggestionsBrowserKey: deploy-environments
+----
+
+This gives users quick access to the predefined environments while also remembering any custom environments they've deployed to.
+
+=== Behavior Notes
+
+* **Password and sensitive fields**: Values from `password`, `checkbox`, and `confirmation` type arguments are never saved to browser storage
+* **Empty values**: Empty or blank values are not saved as suggestions
+* **Local storage**: Suggestions are stored in the browser's `localStorage` under keys prefixed with `olivetin-suggestions-`
+* **Per-browser**: Since suggestions use browser local storage, they are specific to each browser and device - they don't sync across devices or browsers
+* **Clearing suggestions**: Users can clear their saved suggestions by clearing their browser's local storage for the OliveTin site
+
+== Browser Support
+
+`datalist` is widely supported now-a-days, but Firefox on Android notably lacks support; https://caniuse.com/datalist . See the upstream bug here; https://bugzilla.mozilla.org/show_bug.cgi?id=1535985 .
+
+
diff --git a/docs/modules/ROOT/pages/args/types.adoc b/docs/modules/ROOT/pages/args/types.adoc
new file mode 100644
index 0000000..fa10b99
--- /dev/null
+++ b/docs/modules/ROOT/pages/args/types.adoc
@@ -0,0 +1,33 @@
+[#arg-types]
+= Argument types
+
+A full list of argument types are below;
+
+.Argument types reference table
+[%header,cols="1,0,2"]
+|===
+| Type | Rendered as | Allowed values
+| (default) | xref:args/input.adoc[Textbox] | If a `type:` is not set, and `choices:` is empty, then ascii will be used, and a warning will be logged. It is recommended that you set the type explicitly, rather than relying on defaults.
+| ascii | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, but no spaces or punctuation
+| ascii_identifier | xref:args/input.adoc[Textbox] | Like a DNS name, a-Z (case insensitive), 0-9, `-`, `.`, and `_`.
+| ascii_sentence | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, with spaces, `.` and `,`.
+| unicode_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but allows unicode characters. This is useful for languages that use non-ascii characters, such as Chinese, Japanese, etc.
+| email | xref:args/input.adoc[Textbox] | An email address.
+| password | xref:args/password.adoc[Password] | A password, which is hidden when typed.
+| very_dangerous_raw_string | xref:args/input.adoc[Textbox] | Anything. This is **incredibly dangerous**, as effectively people can type anything they like, including executing additional commands beyond what you specify. Absolutely should not be used unless your OliveTin instance can only be used by people you trust entirely.
+| regex:... | xref:args/input.adoc[Textbox] | Version 2024.03.081 and above support custom regex patterns. See xref:args/regex.adoc[Custom regex arguments].
+| int | xref:args/input.adoc[Textbox] | Any number, made up of the characters 0 to 9. Negative numbers are not supported.
+| url | xref:args/input.adoc[Textbox] | A URL (e.g. https://example.com). Accepts any scheme, including `file://` and `ftp://`. See warning below.
+| confirmation | xref:args/input_confirmation.adoc[Confirmation] | A "hidden" argument that makes the action require a confirmation before launching.
+| n/a, but `choices` used | xref:args/input_dropdown.adoc[Dropdown] | A "hidden" argument that makes the action require a confirmation before launching.
+| raw_string_multiline | xref:args/input_textarea.adoc[Textarea] | Anything. This is **dangerous**, as effectively people can type anything they like
+|===
+
+[WARNING]
+.Security risk: URL argument type
+====
+The `url` argument type does not restrict the URL scheme. Users can enter `file://` (local filesystem) URLs, `ftp://`, or other schemes. If the argument value is passed directly to curl, wget, or similar tools, a malicious or mistaken input could read local files, access internal services, or trigger unwanted network requests.
+
+If your action might be used by untrusted users, validate or filter the URL in your script (e.g. allow only `https://`) before using the value.
+====
+
diff --git a/docs/modules/ROOT/pages/config.adoc b/docs/modules/ROOT/pages/config.adoc
new file mode 100644
index 0000000..cd5de64
--- /dev/null
+++ b/docs/modules/ROOT/pages/config.adoc
@@ -0,0 +1,133 @@
+[#config]
+= Configuration
+
+OliveTin is controlled by a `config.yaml` file. On startup, it looks for this
+file in the following locations;
+
+1. The value specified by the `--configdir` argument, which defaults to the current working directory (`./`)
+2. `/config/` - Mostly used for containers
+3. `/etc/OliveTin/` - this is the recommended directory on Linux for your `config.yaml`.
+
+The most simple `config.yaml` would be something like this;
+
+.The most simple `config.yaml` file.
+[source,yaml]
+----
+actions:
+ - title: "Hello world!"
+ shell: echo 'Hello World!'
+----
+
+The configuration does not really get more complicated than that. You can of course add more actions, and customize more, but the syntax otherwise extremely simple.
+
+For building up from here, look at the following resources;
+
+* See the xref:action_examples/intro.adoc[action examples] section for extra examples of what OliveTin could be configured to do.
+
+* See the xref:action_customization/intro.adoc[action customization] documentation to customize how those actions work.
+
+* See the xref:solutions/intro.adoc[Solutions] documentation for just the essential configuration to achieve popular use cases.
+
+All configuration options are covered in the solution sections
+
+[#config-list]
+== Core functionality
+
+|===
+| Option | Description | Default | Live Reloadable | Documentation
+
+| `actions` | The list of available actions. | `-` | Live Reloadable, but refreshing the web browser is recommended. | xref:action_examples/intro.adoc[Action examples]
+| `entities` | A list of "things" you can attach actions to. | `-` | Live Reloadable, but restart is recommended. | xref:entities/intro.adoc[Entities]
+| `dashboards` | A grouping of actions, with optional displays, or actions generated from entities. | `-` | Live Reloadable | xref:dashboards/intro.adoc[Dashboards]
+|===
+
+== UI Customization
+
+|===
+| Option | Description | Default | Live Reloadable | Documentation
+
+| `pageTitle` | A custom title for the OliveTin page. | `OliveTin` | Live reloadable | xref:advanced_configuration/webui.adoc[Customize the web UI].
+| `showFooter` | Show (or hide) the footer. | `true` | Live reloadable | xref:advanced_configuration/webui.adoc[Customize the web UI].
+| `showNewVersions` | Show (or hide) new versions in the footer. | `true` | Live reloadable | xref:advanced_configuration/webui.adoc[Customize the web UI].
+| `defaultPolicy.showVersionNumber` | Show (or hide) the application version in the footer. Can be overridden per user/group in ACLs. | `true` | Requires restart | xref:reference/version_display.adoc[Version display]
+| `showNavigation` | Show (or hide) the sidebar/topbar section navigation. | `true` | Live reloadable | xref:advanced_configuration/webui.adoc[Customize the web UI].
+| `showNavigateOnStartIcons` | Show (or hide) the small icons on action buttons that indicate popup/argument/background behavior on start. | `true` | Live reloadable | xref:advanced_configuration/webui.adoc[Customize the web UI].
+| `sectionNavigationStyle` | The style of the section navigation. `sidebar`, `topbar` | `sidebar` | Live reloadable | xref:advanced_configuration/webui.adoc[Customize the web UI].
+| `defaultPopupOnStart` | The default popup to show on start. | `none` | Live reloadable | xref:action_customization/popuponstart.adoc[Popup On Start].
+| `defaultIconForActions` | The default icon to use for actions. | `smile` | Requires Restart | -
+| `defaultIconForDirectories` | The default icon to use for directories. | `directory` | Requires Restart | -
+| `defaultIconForBack` | The default icon to use for back (from directories). | `«` | Requires Restart | -
+| `enableCustomJs` | Enable custom JavaScript. | `false` | Live Reloadable, but refreshing the web browser is required. | xref:advanced_configuration/webui.adoc[Custom JS].
+| `themeName` | The theme to use. | `` | Restart recommended | xref:reference/reference_themes_for_users.adoc[Themes].
+|===
+
+== Security Configuration
+
+|===
+| Option | Description | Default | Live Reloadable | Documentation
+
+| `AuthJwtCookieName` | The name of the cookie to use for JWT authentication. | `` | Requires restart | xref:security/jwt_hmac.adoc[JWT with HMAC], xref:security/jwt_keys.adoc[JWT with Keys]
+| `AuthJwtAud` | The audience to use for JWT authentication. | `` | Requires restart | xref:security/jwt_keys.adoc[JWT with Keys]
+| `AuthJwtDomain` | The domain to use for JWT authentication. | `` | Requires restart | xref:security/jwt_hmac.adoc[JWT with HMAC], xref:security/jwt_keys.adoc[JWT with Keys]
+| `AuthJwtCertsURL` | The URL to fetch the public keys from with JWKS | `` | Requires restart | xref:security/jwt_keys.adoc[JWT with Keys]
+| `AuthJwtClaimUsername` | The claim to use for the username. | `sub` | Requires restart | xref:security/jwt_hmac.adoc[JWT with HMAC], xref:security/jwt_keys.adoc[JWT with Keys]
+| `AuthJwtClaimUserGroup` | The claim to use for the usergroup. | `sub` | Requires restart | xref:security/jwt_hmac.adoc[JWT with HMAC], xref:security/jwt_keys.adoc[JWT with Keys]
+| `AuthJwtHeader` | The HTTP header to use for JWT authentication. | `` | Requires restart | xref:security/jwt_keys.adoc[JWT with Keys]
+| `AuthJwtPubKeyPath` | The path to the public key to use for JWT authentication. | `` | Requires restart | xref:security/jwt_keys.adoc[JWT with Keys]
+| `AuthHttpHeaderUsername` | The HTTP header to use for the username. | `` | Requires restart | xref:security/trusted_header.adoc[Trusted Headers]
+| `AuthHttpHeaderUserGroup` | The HTTP header to use for the usergroup. | `` | Requires restart | xref:security/trusted_header.adoc[Trusted Headers]
+| `AuthLocalUsers` | The list of local users. | `[]` | Requires restart | xref:security/local.adoc[Local Users]
+| `AuthLoginUrl` | The URL to redirect to for login. | `` | Requires restart | xref:security/local.adoc[Login URL]
+| `AuthRequireGuestsToLogin` | Basically disables all functionality for guests. It sets all default permissions to false. | `false` | Requires restart | xref:security/acl.adoc[Access Control Lists]
+| `DefaultPermissions` | The default permissions to use. | `[]` | Requires restart | xref:security/acl.adoc[Access Control Lists]
+| `AccessControlLists` | The list of access control lists. | `[]` | Requires restart | xref:security/acl.adoc[Access Control Lists]
+| `security.headerContentSecurityPolicy` | Whether to send a `Content-Security-Policy` header from the single HTTP frontend. | `true` | Live reloadable | xref:security/content_security_policy.adoc[Content Security Policy headers]
+| `security.contentSecurityPolicy` | CSP header value when `security.headerContentSecurityPolicy` is enabled. If empty, a built-in default is used. | (built-in default) | Live reloadable | xref:security/content_security_policy.adoc[Content Security Policy headers]
+|===
+
+== Networking Configuration
+
+|===
+| Option | Description | Default | Live Reloadable | Documentation
+
+| `UseSingleHttpFrontend` | Whether or not to start the internal "microproxy" frontend. Disabling this is highly unusual and is only really useful for power users. | true | Requires Restart | xref:reference/network-ports.adoc[Network Ports]
+| `ListenAddressSingleHTTPFrontend` | The address to listen on for the internal "microproxy" frontend. | `0.0.0.0:1337` | Requires Restart | xref:reference/network-ports.adoc[Network Ports]
+| `ListenAddressWebUI` | The address to listen on for the web UI. | `localhost:1340` | Requires Restart | xref:reference/network-ports.adoc[Network Ports]
+| `ListenAddressRestActions` | The address for the API | `localhost:1338` | Requires Restart | xref:reference/network-ports.adoc[Network Ports]
+| `ListenAddressGrpcActions` | The address for the gRPC API | `localhost:1339` | Requires Restart | xref:reference/network-ports.adoc[Network Ports]
+| `ListenAddressPrometheus` | The address for the Prometheus metrics | `localhost:1341` | Requires Restart | xref:reference/network-ports.adoc[Network Ports], xref:advanced_configuration/prometheus.adoc[Prometheus]
+| `ExternalRestAddress` | The address the web browser should use to connect to the API. | `.` | Requires Restart | xref:reference/network-ports.adoc[Network Ports]
+|===
+
+== Debugging Configuration
+
+|===
+| Option | Description | Default | Live Reloadable | Documentation
+
+| `LogLevel` | The log level to use. `INFO`, `DEBUG`, `WARN` | `INFO` | Requires Restart | -
+| `LogDebugOptions` | Enable various debug logs. | `-` | Requires Restart | xref:troubleshooting/advanced.adoc[Advanced Troubleshooting]
+| `Insecure*` | Various options to disable security features. | `false` | Restart recommended | xref:troubleshooting/advanced.adoc[Advanced Troubleshooting]
+|===
+
+== Miscellaneous Configuration
+
+|===
+| Option | Description | Default | Live Reloadable | Documentation
+
+| `WebUIDir` | The directory to serve the web UI from. | Calculated at runtime. | Requires Restart | -
+| `CronSupportForSeconds` | Whether or not to support seconds in cron expressions. | `false` | Requires Restart | xref:action_execution/oncron.adoc[Cron]
+| `SaveLogs` | Whether or not to save logs to disk. | `[]` | Requires Restart | xref:action_customization/savelogs.adoc[Save Logs]
+| `Prometheus` | Prometheus configuration. | `-` | Requires Restart | xref:advanced_configuration/prometheus.adoc[Prometheus]
+|===
+
+== What's Next?
+
+Now that you understand the configuration structure, here are the next steps:
+
+* xref:action_execution/create_your_first.adoc[Create your first action] - Start building actions for your use case
+* xref:action_examples/intro.adoc[Browse action examples] - Get inspiration from real-world configurations
+* xref:args/intro.adoc[Add arguments to actions] - Make actions interactive with user input
+* xref:dashboards/intro.adoc[Organize with dashboards] - Create custom views to organize your actions
+* xref:entities/intro.adoc[Use entities] - Dynamically generate actions from entity files
+* xref:security/concepts.adoc[Configure security] - Set up authentication and authorization
+* xref:solutions/intro.adoc[Explore solutions] - Find complete configurations for common scenarios
diff --git a/docs/modules/ROOT/pages/dashboards/2-fieldsets.adoc b/docs/modules/ROOT/pages/dashboards/2-fieldsets.adoc
new file mode 100644
index 0000000..270aa78
--- /dev/null
+++ b/docs/modules/ROOT/pages/dashboards/2-fieldsets.adoc
@@ -0,0 +1,42 @@
+[#fieldsets]
+= Fieldsets
+
+It is possible to group actions together in a "group", which is not a directory, but is called
+a "fieldset". This is an example of a fieldset that contains two xref:dashboards/3-folders.adoc[Folders].
+
+image::fieldset.png[]
+
+Fieldsets are defined under a xref:dashboards/intro.adoc[Dashboards] in your config.yaml.
+
+.`config.yaml`
+[source,yaml]
+----
+dashboards:
+ - title: My First Dashboard
+ contents:
+ - title: Fieldset 1
+ type: fieldset
+ contents: []
+
+ - title: Fieldset 2
+ type: fieldset
+ contents: []
+----
+
+Fieldsets are also generated for you when you use xref:entities/intro.adoc[Entities].
+
+.`config.yaml`
+[source,yaml]
+----
+dashboards:
+ - title: My First Dashboard
+ contents:
+ - title: Fieldset 1
+ type: fieldset
+ entity: server
+ contents:
+ - title: Start {{ server.Name }}
+ - title: Shutdown {{ server.Name }}
+----
+
+
diff --git a/docs/modules/ROOT/pages/dashboards/3-folders.adoc b/docs/modules/ROOT/pages/dashboards/3-folders.adoc
new file mode 100644
index 0000000..b4e964e
--- /dev/null
+++ b/docs/modules/ROOT/pages/dashboards/3-folders.adoc
@@ -0,0 +1,52 @@
+[#folders]
+= Folders (Directories)
+
+Folders (Directories) are a good way to group up actions in the same way that you would
+organize files on your computer into directories.
+
+image::folders.png[]
+
+You must first create a dashboard to use a directory, and then you "reference" actions that you
+want in that folder based on the action name. Anything without a "contents" property is treated
+as an action.
+
+Let's look at the example below with 4 actions, 2 top level folders and 1 subfolder.
+
+.`config.yaml`
+[source,yaml]
+----
+actions:
+ - title: Action 1
+ shell: echo "action1"
+
+ - title: Action 2
+ shell: echo "action2"
+
+ - title: Action 3
+ shell: echo "action3"
+
+ - title: Action 4
+ shell: echo "action4"
+
+dashboards:
+ - title: My First Dashboard
+ contents:
+ - title: Fieldset 1
+ type: fieldset
+ contents:
+ - title: Folder 1
+ contents:
+ - title: Action 1
+ - title: Action 2
+
+ - title: Subfolder 2
+ contents:
+ - title: Action 3
+
+ - title: Folder 2
+ contents:
+ - title: Action 4
+
+----
+
+
diff --git a/docs/modules/ROOT/pages/dashboards/4-displays.adoc b/docs/modules/ROOT/pages/dashboards/4-displays.adoc
new file mode 100644
index 0000000..41ce6bc
--- /dev/null
+++ b/docs/modules/ROOT/pages/dashboards/4-displays.adoc
@@ -0,0 +1,63 @@
+[#displays]
+= Displays
+
+Displays are a way of displaying text, values, variables and similar on a dashboard.
+
+They are rendered as just a simple box, that shown alongside actions. You can add arbitary HTML to a display, which makes it useful for showing links, etc.
+
+image::dashboard-display.png[]
+
+[source,yaml]
+.`config.yaml`
+----
+dashboards:
+ # This the second dashboard.
+ - title: My Containers
+ contents:
+ # This is a fieldset, which is a way of dashboard items together actions together.
+ - title: Container {{ container.Names }}
+ entity: container
+ type: fieldset
+ contents:
+ # This is a display
+ - type: display
+ title: |
+ {{ container.Names }}
{{ container.State }}
+
+ # These are the actions that we want on the dashboard.
+ - title: 'Start {{ container.Names }}'
+ - title: 'Stop {{ container.Names }}'
+----
+
+== CSS Classes
+
+You can also add CSS classes to the display, which can be useful for styling.
+
+[source,yaml]
+----
+dashboards:
+ - title: My Containers
+ contents:
+ - title: 'Container {{ container.Names.0 }} ({{ container.Image }})'
+ entity: container
+ type: fieldset
+ contents:
+ - type: display
+ cssClass: '{{ container.State }}'
+ title: |
+ {{ container.Status }}
{{ container.State }}
+ - title: 'Start {{ container.Names.0 }}'
+ - title: 'Stop {{ container.Names.0 }}'
+ - title: 'Remove {{ container.Names.0 }}'
+----
+
+You can then use the following CSS to style the display;
+
+[source,css]
+----
+div.display.running {
+ color: green;
+}
+----
+
+
diff --git a/docs/modules/ROOT/pages/dashboards/5-output-views.adoc b/docs/modules/ROOT/pages/dashboards/5-output-views.adoc
new file mode 100644
index 0000000..08b8c5f
--- /dev/null
+++ b/docs/modules/ROOT/pages/dashboards/5-output-views.adoc
@@ -0,0 +1,34 @@
+[#output-views]
+= Most recent action output
+
+This is considered an advanced and experimental feature at the moment.
+
+The `stdout-most-recent-execution` view is a way to display the most recent output of an action on a dashboard. This is useful for actions that are run on a schedule, or actions that are run on startup. This is a picture of what it looks like:
+
+image::mre.png[]
+
+To set this up, here is the configuration you need to add to your `config.yaml` file;
+
+[source,yaml]
+----
+actions:
+ - title: Get status
+ id: status_command
+ shell: date
+ execOnStartup: true
+ execOnCron:
+ - "*/1 * * * *"
+
+dashboards:
+ - title: Control Panel
+ contents:
+ - title: Status
+ type: fieldset
+ contents:
+ - type: stdout-most-recent-execution
+ title: status_command
+----
+
+Note that the output only refreshes with the browser, not when the button is clicked.
+
+As this is an experimental feature, please look at options for xref:troubleshooting/wheretofindhelp.adoc[support] if you need help getting it to work.
diff --git a/docs/modules/ROOT/pages/dashboards/actions.adoc b/docs/modules/ROOT/pages/dashboards/actions.adoc
new file mode 100644
index 0000000..24bee86
--- /dev/null
+++ b/docs/modules/ROOT/pages/dashboards/actions.adoc
@@ -0,0 +1,9 @@
+= The "actions" section
+
+To make Olivetin easy to use, it generates a default "Actions" section for you, as many
+people don't want the hassle of having to configure dashboards. This is fine, you absolutely
+do not need a `dashboards:` section in your `config.yaml` at all if you don't want it.
+
+However, some people prefer to put every action onto a Dashboard. If you so this, OliveTin
+will hide the `Actions` view for you on the sidebar.
+
diff --git a/docs/modules/ROOT/pages/dashboards/css.adoc b/docs/modules/ROOT/pages/dashboards/css.adoc
new file mode 100644
index 0000000..69cfe35
--- /dev/null
+++ b/docs/modules/ROOT/pages/dashboards/css.adoc
@@ -0,0 +1,38 @@
+[#dashboard-css]
+= Change component style
+
+You can change the style of any dashboard component by adding a `cssClass` property to the component. This is useful for styling actions, displays, fieldsets and folders. Here is an example of how to add a class to an action;
+
+[source,yaml]
+----
+themeName: my-theme
+
+actions:
+ - title: My Action
+ shell: echo "Hello"
+
+dashboards:
+ - title: My Dashboard
+ contents:
+ - title: My Fieldset
+ type: fieldset
+ contents:
+ - title: My Action
+ cssClass: big-button
+----
+
+You can then create a theme, and add the following CSS to style the action;
+
+[source,css]
+.`custom-webui/themes/my-theme/theme.css`
+----
+.big-button {
+ background-color: red;
+ color: white;
+ font-size: 20px;
+ grid-column: span 2;
+ grid-row: span 2;
+}
+----
+
+
diff --git a/docs/modules/ROOT/pages/dashboards/entity-directories.adoc b/docs/modules/ROOT/pages/dashboards/entity-directories.adoc
new file mode 100644
index 0000000..04955df
--- /dev/null
+++ b/docs/modules/ROOT/pages/dashboards/entity-directories.adoc
@@ -0,0 +1,35 @@
+= Entity Directories
+
+NOTE: Entity directories were added in 3000.6.0.
+
+Entity directories are a way of grouping actions together based on a single entity instance.
+
+For example, if you have a `server` entity, you can create a directory called `servers` and then add all the actions that you want to apply to all servers to that directory.
+
+In the default config, **More Options** is an **entity directory** because it's parent is a fieldset that is defined with an `entity` property.
+
+[source,yaml]
+----
+ - type: fieldset
+ entity: server
+ title: 'Server: {{ .CurrentEntity.hostname }}'
+ contents:
+ # By default OliveTin will look for an action with a matching title
+ # and put it on the dashboard.
+ #
+ # Fieldsets also support `type: display`, which can display arbitary
+ # text. This is useful for displaying things like a container's state.
+ - type: display
+ title: |
+ Hostname: {{ server.name }}
+ IP Address: {{ server.ip }}
+
+ # These are the actions (defined above) that we want on the dashboard.
+ - title: '{{ server.name }} Wake on Lan'
+ - title: '{{ server.name }} Power Off'
+
+ - title: More Options
+ type: directory
+ contents:
+ - title: '{{ server.name }} Print server name'
+----
\ No newline at end of file
diff --git a/docs/modules/ROOT/pages/dashboards/examples.adoc b/docs/modules/ROOT/pages/dashboards/examples.adoc
new file mode 100644
index 0000000..76ea7ea
--- /dev/null
+++ b/docs/modules/ROOT/pages/dashboards/examples.adoc
@@ -0,0 +1,7 @@
+= Example Dashboard usage
+
+Check out the following examples of dashboards in several complete OliveTin solutions;
+
+* xref:solutions/container-control-panel/index.adoc[Container Control Panel]
+* xref:solutions/systemd-control-panel/index.adoc[Systemd Control Panel]
+
diff --git a/docs/modules/ROOT/pages/dashboards/inline-actions.adoc b/docs/modules/ROOT/pages/dashboards/inline-actions.adoc
new file mode 100644
index 0000000..33f8c38
--- /dev/null
+++ b/docs/modules/ROOT/pages/dashboards/inline-actions.adoc
@@ -0,0 +1,18 @@
+= Actions Inline in Dashboards
+
+NOTE: This feature is available in OliveTin version 3000.7.0 and later. If you are using OliveTin 2k, you can only used xref:dashboards/actions.adoc[Linked Actions in Dashboards].
+
+[source,yaml]
+----
+dashboards:
+ - name: Main Dashboard
+ contents:
+ - inlineAction:
+ title: Date
+ shell: date
+ icon: date
+----
+
+== See Also
+
+* xref:dashboards/actions.adoc[Actions (Linked)]]
diff --git a/docs/modules/ROOT/pages/dashboards/intro.adoc b/docs/modules/ROOT/pages/dashboards/intro.adoc
new file mode 100644
index 0000000..b2f991f
--- /dev/null
+++ b/docs/modules/ROOT/pages/dashboards/intro.adoc
@@ -0,0 +1,96 @@
+[#dashboards]
+= Dashboards
+
+OliveTin generates a default view of actions which is useful for simple OliveTin use cases - this is always called "Actions" and cannot be renamed. The Actions view also does not support entities, fieldsets or folders.
+
+If you want to start organizing OliveTin actions more effectively, then **Dashboards** are for you!
+
+One of the biggest reasons to use Dashboards as opposed to just the "Actions" section, is that dashboards allow you to use xref:dashboards/3-folders.adoc[Folders], and dashboards really start to get exciting when you start using xref:entities/intro.adoc[entities] and displays.
+
+image::dashboard.png[]
+
+== Dashboards pull actions from the default view
+
+Dashboards are a way of pulling actions from the default "actions" view, and organizing them into groups - either into folders, or fieldsets. Because dashboards literally do "pull" actions from the default view, you cannot use an action in multiple dashboards - and every action must have a unique title.
+
+== Example configuration
+
+[source,yaml]
+.`config.yaml`
+----
+# Actions MUST be defined in the actions section, not in the dashboards
+# section. The dashboards only "link" to actions by their title.
+actions:
+ - title: Ping All Servers
+ shell: echo "ping all..."
+
+ - title: '{{ server.name }} Wake on Lan'
+ shell: 'wol {{ server.name }}'
+ timeout: 10
+ entity: server
+
+ - title: '{{ server.name }} Power Off'
+ shell: 'ssh root@{{ server.name }} "poweroff"'
+ timeout: 10
+ entity: server
+
+
+# Dashboards are a way of taking actions from the default "actions" view, and
+# organizing them into groups - either into folders, or fieldsets.
+#
+# The only way to properly use entities, are to use them with a `fieldset` on
+# a dashboard.
+dashboards:
+ # Top level items are dashboards.
+ - title: My Servers
+ contents:
+ # On dashboards, all items need to be in a "fieldset". If you don't
+ # specify a fieldset, actions will be assigned to a fieldset with a title
+ # called "default".
+ - title: All Servers
+ type: fieldset
+ contents:
+ # The contents of a dashboard will try to look for an action with a
+ # matching title IF the `contents: ` property is empty.
+ - title: Ping All Servers
+
+ # If you create an item with some "contents:", OliveTin will show that as
+ # directory.
+ - title: Hypervisors
+ contents:
+ - title: Ping hypervisor1
+ - title: Ping hypervisor2
+
+ # If you specify `type: fieldset` and some `contents`, it will show your
+ # actions grouped together without a folder.
+ - type: fieldset
+ entity: server
+ title: 'Server: {{ server.hostname }}'
+ contents:
+ # By default OliveTin will look for an action with a matching title
+ # and put it on the dashboard.
+ #
+ # Fieldsets also support `type: display`, which can display arbitary
+ # text. This is useful for displaying things like a container's state.
+ - type: display
+ title: |
+ Hostname: {{ server.name }}
+ IP Address: {{ server.ip }}
+
+ # These are the actions (defined above) that we want on the dashboard.
+ - title: '{{ server.name }} Wake on Lan'
+ - title: '{{ server.name }} Power Off'
+----
+
+== What's Next?
+
+Now that you understand dashboards, explore these related features:
+
+* xref:dashboards/2-fieldsets.adoc[Learn about fieldsets] - Group actions visually on dashboards
+* xref:dashboards/3-folders.adoc[Organize with folders] - Create folder structures for better organization
+* xref:dashboards/4-displays.adoc[Add displays] - Show information alongside actions
+* xref:dashboards/5-output-views.adoc[Configure output views] - Customize how action output is displayed
+* xref:entities/intro.adoc[Use entities with dashboards] - Dynamically generate actions from entity files
+* xref:dashboards/examples.adoc[View dashboard examples] - See complete dashboard configurations
+* xref:dashboards/css.adoc[Customize dashboard styling] - Change the appearance of dashboard components
+
diff --git a/docs/modules/ROOT/pages/entities/examples.adoc b/docs/modules/ROOT/pages/entities/examples.adoc
new file mode 100644
index 0000000..093b757
--- /dev/null
+++ b/docs/modules/ROOT/pages/entities/examples.adoc
@@ -0,0 +1,9 @@
+[#entity-examples]
+= Example Entity Usage
+
+Check out the following xref:solutions/intro.adoc[Solutions] which make good use of entities.
+
+* xref:solutions/container-control-panel/index.adoc[Container Control Panel]
+* xref:solutions/systemd-control-panel/index.adoc[Systemd Control Panel]
+
+
diff --git a/docs/modules/ROOT/pages/entities/intro.adoc b/docs/modules/ROOT/pages/entities/intro.adoc
new file mode 100644
index 0000000..401a2a2
--- /dev/null
+++ b/docs/modules/ROOT/pages/entities/intro.adoc
@@ -0,0 +1,35 @@
+[#entities]
+= Entities
+
+An entity is something that exists - a "thing", like a VM, or a Container is an entity. OliveTin allows you to then dynamically generate actions based around these entities.
+
+This is really useful if you want to generate wake on lan or poweroff actions for `server` entities, for example.
+
+A very popular use case that entities were designed for was for `container` entities - in a similar way you could generate `start`, `stop`, and `restart` container actions.
+
+Entities are just loaded from files on disk, OliveTin will also watch these files for updates while OliveTin is running, and update entities.
+
+Entities can have properties defined in those files, and those can be used in your configuration as variables. For example; `container.status`, or `vm.hostname`.
+
+[source,yaml]
+----
+entities:
+ - file: /etc/OliveTin/containers.json
+ name: container
+
+ - file: /etc/OliveTin/servers.yaml
+ name: server
+----
+
+Entity Actions can only be used on xref:dashboards/intro.adoc[Dashboards].
+
+== What's Next?
+
+Now that you understand entities, here's how to use them effectively:
+
+* xref:entities/yaml.adoc[Create YAML entity files] - Learn the YAML format for entity files
+* xref:entities/json.adoc[Create JSON entity files] - Learn the JSON format for entity files
+* xref:entities/examples.adoc[View entity examples] - See complete examples of entity configurations
+* xref:dashboards/intro.adoc[Use entities in dashboards] - Combine entities with dashboards for dynamic action generation
+* xref:solutions/container-control-panel/index.adoc[Container control panel solution] - See a complete example using container entities
+* xref:solutions/systemd-control-panel/index.adoc[Systemd control panel solution] - See a complete example using systemd entities
diff --git a/docs/modules/ROOT/pages/entities/json.adoc b/docs/modules/ROOT/pages/entities/json.adoc
new file mode 100644
index 0000000..490967f
--- /dev/null
+++ b/docs/modules/ROOT/pages/entities/json.adoc
@@ -0,0 +1,12 @@
+[#entities-json]
+= JSON entity files
+
+JSON files are parsed as if each line is a single JSON object. This can be super helpful for getting a list of containers, for example; `docker ps -a --format=json > /etc/OliveTin/containers.json`.
+
+[source,json]
+.`/etc/OliveTin/containers.json`
+----
+{"Command":"\"/opt/entrypoint.sh\"","CreatedAt":"2024-02-08 15:27:42 +0000 GMT","ID":"4bafe6f9f956","Image":"fedora","Labels":"?","LocalVolumes":"0","Mounts":"","Names":"media-indexing-container","Networks":"bridge","Ports":"","RunningFor":"13 days ago","Size":"0B","State":"exited","Status":"Exited (128) 13 days ago"}
+{"Command":"\"/opt/entrypoint.sh\"","CreatedAt":"2023-12-17 20:58:03 +0000 GMT","ID":"d25f37c49c35","Image":"fedora","Labels":"?","LocalVolumes":"0","Mounts":"","Names":"media-playback-container","Networks":"bridge","Ports":"","RunningFor":"27 days ago","Size":"0B","State":"exited","Status":"Exited (137) 27 days ago"}
+----
+
diff --git a/docs/modules/ROOT/pages/entities/yaml.adoc b/docs/modules/ROOT/pages/entities/yaml.adoc
new file mode 100644
index 0000000..897b762
--- /dev/null
+++ b/docs/modules/ROOT/pages/entities/yaml.adoc
@@ -0,0 +1,22 @@
+[#entities-yaml]
+= YAML entity files
+
+YAML files are the default expected format, so you can use .yml, .yaml, or even .txt - as long as the file contains a valid yaml LIST, then it will be loaded.
+
+.`/etc/OliveTin/servers.yaml`
+[source,yaml]
+----
+- name: server1
+ state: started
+ hostname: server1.example.com
+ ip: 192.168.0.1
+- name: server2
+ state: started
+ hostname: server2.example.com
+ ip: 192.168.0.2
+- name: server3
+ state: stopped
+ hostname: server3.example.com
+ ip: 192.168.0.3
+----
+
diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc
new file mode 100644
index 0000000..6c8432b
--- /dev/null
+++ b/docs/modules/ROOT/pages/index.adoc
@@ -0,0 +1,42 @@
+= OliveTin Introduction
+
+**link:https://www.olivetin.app[OliveTin]** gives **safe** and **simple** access to predefined shell commands from a web interface.
+
+image:icons/GitHub.png[inline] link:https://github.com/jamesread/OliveTin[OliveTin on GitHub]
+
+image:icons/Discord.png[inline] link:https://discord.gg/jhYWWpNJ3v[Chat on Discord]
+
+The link:https://www.olivetin.app[OliveTin Homepage is here]. This site that you are viewing is the documentation for OliveTin.
+
+'''
+
+== Use cases
+
+**Safely** give access to commands, for less technical people;
+
+* eg: Give your family a button to `podman restart plex`
+* eg: Give junior admins a simple web form with dropdowns, to start your custom script. `backupScript.sh --folder {{ customerName }}`
+* eg: Enable SSH access to the server for the next 20 mins `firewall-cmd --add-service ssh --timeout 20m`
+
+**Simplify** complex commands, make them accessible and repeatable;
+
+* eg: Expose complex commands on touchscreen tablets stuck on walls around your house. `wake-on-lan aa:bb:cc:11:22:33`
+* eg: Run long running on your servers from your cell phone. `dnf update -y`
+* eg: Define complex commands with lots of preset arguments, and turn a few arguments into dropdown select boxes. `docker rm {{ container }} && docker create {{ container }} && docker start {{ container }}`
+
+== Demo
+
+video::Ej6NM9rmZtk[youtube,width=100%]
+
+== Features
+
+* **Responsive, touch-friendly UI** - great for tablets and mobile
+* **Super simple config in YAML** - because if it's not YAML now-a-days, it's not "cloud native" :-)
+* **Dark mode** - for those of you that roll that way.
+* **Accessible** - passes all the accessibility checks in Firefox, and issues with accessibility are taken seriously.
+* **Container** - available for quickly testing and getting it up and running, great for the selfhosted community.
+* **Integrate with anything** - OliveTin just runs Linux shell commands, so theoretially you could integrate with a bunch of stuff just by using curl, ping, etc. However, writing your own shell scripts is a great way to extend OliveTin.
+* **Lightweight on resources** - uses only a few MB of RAM and barely any CPU. Written in Go, with a web interface written as a modern, responsive, Single Page App that uses the REST/gRPC API.
+* **Good amount of unit tests and style checks** - helps potential contributors be consistent, and helps with maintainability.
+
+You can learn more about OliveTin in the link:https://www.olivetin.app[OliveTin Homepage].
diff --git a/docs/modules/ROOT/pages/install/bsd.adoc b/docs/modules/ROOT/pages/install/bsd.adoc
new file mode 100644
index 0000000..d517bdb
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/bsd.adoc
@@ -0,0 +1,7 @@
+= BSD
+
+Using BSD? Good for you! :-)
+
+binary tar.gz: https://github.com/OliveTin/OliveTin/releases/latest/OliveTin-freebsd-amd64.tar.gz
+
+include::partial$install/post_generic.adoc[]
diff --git a/docs/modules/ROOT/pages/install/choose_package.adoc b/docs/modules/ROOT/pages/install/choose_package.adoc
new file mode 100644
index 0000000..e64e77f
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/choose_package.adoc
@@ -0,0 +1,46 @@
+[#choose-package]
+= Which download do I need?
+
+OliveTin can be run as a <> or a xref:install/container.adoc[container]. If you are not sure which is best for you, read xref:install/container_vs_service.adoc[containers vs services].
+
+[#package]
+== Packages (run OliveTin as a service)
+
+This is a table that explains which package/download is best for each environment;
+
+|===
+ | Processor Type | Operating System | Distribution | File on link:https://github.com/OliveTin/OliveTin/releases/latest[latest release page]
+
+.5+| AMD / Intel -> `amd64` .3+| Linux | Other -> `.tar.gz` | link:https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin-linux-amd64.tar.gz[`OliveTin-linux-amd64.tar.gz`] xref:install/targz.adoc[installation instructions]
+ | Red Hat, Fedora, etc -> `.rpm` | link:https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin_linux_amd64.rpm[`OliveTin_linux_amd64.rpm`] xref:install/linux_rpm.adoc[installation instructions]
+ | Debian, Ubuntu -> `.deb` | link:https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin_linux_amd64.deb[`OliveTin_linux_amd64.deb`] xref:install/linux_deb.adoc[installation instructions]
+ 2+| Windows | link:https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin-windows-amd64.zip[`OliveTin-windows-amd64.zip`]
+ 2+| macOS,macOS | link:https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin-macOS-amd64.tar.gz[`OliveTin-macOS-amd64.tar.gz`]
+.3+| 32bit ARM (Raspberry Pi 1, 2, or similar) -> `arm` .3+| Linux | Other -> `.tar.gz` | link:https://github.com/OliveTin/OliveTin/releases/latest[One of the `OliveTin-linux-arm....tar.gz` files] xref:install/targz.adoc[installation instructions]
+ | Red Hat, Fedora, etc -> `.rpm` | link:https://github.com/OliveTin/OliveTin/releases/latest[One of the `OliveTin_linux_arm.....rpm` files] xref:install/linux_rpm.adoc[installation instructions]
+ | Debian, Ubuntu -> `.deb` | link:https://github.com/OliveTin/OliveTin/releases/latest[One of the `OliveTin_linux_arm....deb` files] xref:install/linux_deb.adoc[installation instructions]
+.4+| 64bit ARM (Apple M1, Raspberry Pi 3, 4, or similar) -> `arm64` +
+
+**Note**: If you are running 32bit Raspberry Pi OS, choose the 32bit ARM download option instead.
+ .3+| Linux | Other -> `.tar.gz` | link:https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin-linux-arm64.tar.gz[`OliveTin-linux-arm64.tar.gz`] xref:install/targz.adoc[installation instructions]
+ | Red Hat, Fedora, etc -> `.rpm` | link:https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin_linux_arm64.rpm[`OliveTin_linux_arm64.rpm`] xref:install/linux_rpm.adoc[installation instructions]
+ | Debian, Ubuntu -> `.deb` ` | link:https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin_linux_arm64.deb[`OliveTin_linux_arm64.deb`] xref:install/linux_deb.adoc[installation instructions]
+ 2+| macOS | link:https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin-macOS-amd64.tar.gz[`OliveTin-macOS-arm64.tar.gz`] xref:install/targz.adoc[installation instructions]
+|===
+
+
+A full list of **packages** can be downloaded from the link:https://github.com/jamesread/OliveTin/releases[GitHub project releases] page.
+
+[#container-images]
+== Container images
+
+include::partial$install/container.adoc[]
+
+include::partial$install/container_registries.adoc[]
+
+The following methods can be used to install the container;
+
+* xref:install/container.adoc[Installation as a standalone container (podman/docker)]
+* xref:install/docker_compose.adoc[Installation with Docker Compose]
+* xref:install/helm.adoc[Installation on Kubernetes with Helm]
+* xref:install/k8s.adoc[Installation on Kubernetes (manually)]
diff --git a/docs/modules/ROOT/pages/install/container.adoc b/docs/modules/ROOT/pages/install/container.adoc
new file mode 100644
index 0000000..a61be82
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/container.adoc
@@ -0,0 +1,28 @@
+= Linux Container
+
+== Repositories
+
+The OliveTin container images are hosted on both Docker Hub and the GitHub Container Registry.
+
+The main OliveTin image is available at;
+
+* **Docker Hub**: `docker.io/jamesread/olivetin`
+ link:https://hub.docker.com/r/jamesread/olivetin/tags?page=1&ordering=last_updated[View on Docker Hub]
+* **GitHub**: `ghcr.io/olivetin/olivetin`
+ link:https://github.com/OliveTin/OliveTin/pkgs/container/olivetin[View on GitHub]
+
+== Tags
+
+* `latest-2k` - This tag will always point to the latest OliveTin 2k version (eg 2025.11.11)
+* `latest-3k` - This tag will always point to the latest OliveTin 3k version (eg 3000.2.0)
+* `latest` - This tag will always point to the latest OliveTin version (currently 3k)
+
+Read more about 2k vs 3k here: xref:../upgrade/2k3k.adoc[OliveTin 2k vs OliveTin 3k]
+
+== Container installation options
+
+* xref:install/podmandocker.adoc[Docker or Podman]
+* xref:install/docker_compose.adoc[Docker Compose]
+* xref:install/helm.adoc[Kubernetes with Helm]
+* xref:install/k8s.adoc[Kubernetes with Manifests]
+
diff --git a/docs/modules/ROOT/pages/install/container_vs_service.adoc b/docs/modules/ROOT/pages/install/container_vs_service.adoc
new file mode 100644
index 0000000..b251638
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/container_vs_service.adoc
@@ -0,0 +1,21 @@
+[#install-container-vs-service]
+= Containers vs Services
+
+Linux Containers have become an incredibly popular method for running software. OliveTin supports every way of running Linux containers - spanning from homelabs up to enterprise environments. As a container, it can be used xref:install/container.adoc[standalone using Podman/Docker], through to xref:install/docker_compose.adoc[using docker-compose] or even on xref:install/k8s.adoc[Kubernetes].
+
+**However**, a lot of OliveTin use cases, such as providing an interface to run scripts, rely on files on their local Linux filesystem - containers, by definition, have their own separate filesystem. It is of course possible to "bind mount" parts of your local filesystem into OliveTin, but sometimes this requires more effort, or introduces subtle problems that are more complicated to solve. If you have a good amount of experience with Linux containers, then this approach is absolutely fine - but if you are new to these types of problems - not seeing files you expect, file permissions errors, etc, then it might just be easier to xref:install/choose_package.adoc[install using a Linux package] instead.
+
+== Don't overcomplicate your containers - use SSH!
+
+Sometimes you just want to (or have to) use containers, but also want to use these local resources as well. Instead of bind-mounting lots of stuff into the Linux container and creating a really complicated container definition (which is hard to scale), consider a simple alternative: xref:action_examples/ssh-easy.adoc[SSH with OliveTin]. This allows you to easily SSH back into the container host and keep your container definition really simple. This approach works great and is popular for a lot of users.
+
+== Which should I choose?
+
+|===
+| Use Case | Recommended Option
+
+| You're experienced with Linux containers, and know how to bind mount volumes | xref:install/container.adoc[Linux Container]
+| You're new to Linux containers, or not very comfortable with them | xref:install/choose_package.adoc[Linux service], or xref:install/container.adoc[Linux container] with xref:action_examples/ssh-easy.adoc[SSH action]
+| Needs to use lots of different file paths on the host filesystem | Run as a systemd service, or bind-mount the filesystem into the container.
+| Needs to control processes, like systemd services | Run as a systemd service.
+|===
diff --git a/docs/modules/ROOT/pages/install/docker_compose.adoc b/docs/modules/ROOT/pages/install/docker_compose.adoc
new file mode 100644
index 0000000..6ed4bf1
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/docker_compose.adoc
@@ -0,0 +1,77 @@
+[#install-compose]
+= Docker Compose install
+
+Docker compose is a popular way to define multi-container applications using a
+infrastructure as code approach.
+
+If you personally prefer to use `docker compose`, then here is a sample to get
+you started;
+
+[source,yaml]
+.`docker-compose.yml`
+----
+services:
+ olivetin:
+ container_name: olivetin
+ image: jamesread/olivetin
+ volumes:
+ - OliveTin-config:/config # replace host path or volume as needed
+ ports:
+ - "1337:1337"
+ restart: unless-stopped
+
+
+volumes:
+ OliveTin-config:
+ external: false
+----
+
+include::partial$install/post_container.adoc[]
+
+== Controlling other docker containers from a Docker Compose install of OliveTin
+
+If you want to use OliveTin running in a container to control other Docker containers, you will need to pass through the Docker sock in your compose file.
+
+You will need to adjust your docker-compose file to include the docker socket, like this;
+
+ `docker-compose.yml` including docker socket
+[source,yaml]
+.docker-compose.yml
+----
+services:
+ olivetin:
+ container_name: olivetin
+ image: jamesread/olivetin
+ volumes:
+ - /docker/OliveTin:/config # replace host path or volume as needed
+ - /var/run/docker.sock:/var/run/docker.sock
+ ...
+----
+
+You will probably need to tell this container to run as root as well, to control docker (see below).
+
+== Controlling the docker user with Docker Compose
+
+This is the correct way to tell the OliveTin container to run as root (or any other user);
+
+----
+services:
+ olivetin:
+ container_name: olivetin
+ image: jamesread/olivetin
+ user: root
+ ...
+----
+
+See xref:action_examples/containers.adoc[containers] for alternatives to running as root.
+
+
+NOTE: xref:troubleshooting/puid-pgid.adoc[PUID and PGID are not used] by the official OliveTin container image.
+
+[#docker-compose-traefik]
+== Using Traefik with Docker Compose
+
+Traefik is a popular reverse proxy that seems to be used a lot in people's
+Docker compose setups. See the xref:reverse-proxies/traefik.adoc[Traefik + Docker Compose] page for more details.
+
+
diff --git a/docs/modules/ROOT/pages/install/helm.adoc b/docs/modules/ROOT/pages/install/helm.adoc
new file mode 100644
index 0000000..8c887ac
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/helm.adoc
@@ -0,0 +1,87 @@
+[#install-helm]
+= Installation on Kubernetes with Helm
+
+Helm makes installing OliveTin on Kubernetes very easy, the official chart is hosted on Artifact Hub.
+
+https://artifacthub.io/packages/search?repo=olivetin[image:https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/olivetin[Artifact Hub]]
+
+== Requirements
+
+=== Prerequisites
+
+* [x] A Kubernetes cluster setup and running
+* [x] Kubernetes client installed and authenticated
+* [x] An ingress controller configure for web traffic
+* [x] Helm installed and authenticated to the cluster
+
+== Installation
+
+[source,shell]
+----
+user@host: helm repo add olivetin https://olivetin.github.io/OliveTin-HelmChart/
+user@host: helm install olivetin olivetin/olivetin
+NAME: olivetin
+LAST DEPLOYED: Tue Apr 1 23:19:09 2025
+NAMESPACE: default
+STATUS: deployed
+REVISION: 1
+TEST SUITE: None
+----
+
+== Configure
+
+After a minute or two, check the pod status;
+
+[source,shell]
+----
+user@host: kubectl get pods
+NAME READY STATUS RESTARTS AGE
+olivetin-578b79766-4b8lg 1/1 Running 0 87s
+----
+
+Hopefully the pod is not crashlooping or anything like that. If it is, check the logs.
+
+The helm chart should have created a basic ConfigMap for you;
+
+[source,shell]
+----
+user@host: kubectl describe cm/olivetin-config
+Name: olivetin-config
+Namespace: default
+Labels: app.kubernetes.io/managed-by=Helm
+Annotations: meta.helm.sh/release-name: olivetin
+ meta.helm.sh/release-namespace: default
+
+Data
+====
+config.yaml:
+--
+actions:
+ - title: "Hello world!"
+ shell: echo 'Hello World!'
+
+----
+
+You should edit this ConfigMap to match your needs for OliveTin. Remember to restart the OliveTin deployment if you want the config changes to be picked up more quickly;
+
+[source,shell]
+----
+user@host: kubectl rollout restart deploy/olivetin
+deployment.apps/olivetin restarted
+----
+
+
+include::partial$install/to_config.adoc[]
+
+== Included templates
+
+You can view the raw templates here: https://github.com/OliveTin/OliveTin-HelmChart/tree/main/charts/olivetin/templates
+
+* Deployment
+* ConfigMap
+* Service
+* Ingress (optional)
+
+== See Also
+
+* xref:solutions/k8s-control-panel-hosted/index.adoc[Solution: Kubernetes Control Panel (Hosted)]
diff --git a/docs/modules/ROOT/pages/install/intro.adoc b/docs/modules/ROOT/pages/install/intro.adoc
new file mode 100644
index 0000000..862df3b
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/intro.adoc
@@ -0,0 +1,42 @@
+= Installation guide
+
+== Preparation
+
+You will need approximately **10 minutes** to install OliveTin on almost every platform, with root/system administrator access in most cases.
+
+You probably will need about **10-20 minutes** to understand how OliveTin configuration works, and to be able to write your first action to make it do something useful.
+
+== Where to find help
+
+include::partial$support.adoc[]
+
+== First step: where are you installing?
+
+* Linux
+** xref:install/container_vs_service.adoc[Containers or Service?]
+** Linux Service
+*** xref:install/linux_fedora.adoc[Fedora Linux]
+*** xref:install/linux_alpine.adoc[Alpine Linux]
+*** xref:install/linux_manjaro.adoc[Manjaro Linux]
+*** xref:install/linux_arch.adoc[Arch Linux]
+*** xref:install/linux_rpm.adoc[Generic .rpm based Linux]
+*** xref:install/linux_deb.adoc[Generic .deb based Linux]
+*** xref:install/targz.adoc[.tar.gz Install (manual)]
+** xref:install/container.adoc[Linux Container]
+*** xref:install/podmandocker.adoc[Docker or Podman]
+*** xref:install/docker_compose.adoc[Docker Compose]
+*** xref:install/helm.adoc[Kubernetes with Helm]
+*** xref:install/k8s.adoc[Kubernetes with Manifests]
+* xref:install/bsd.adoc[FreeBSD]
+* xref:install/windows.adoc[Windows]
+* xref:install/macos.adoc[MacOS]
+
+== What's Next?
+
+After installing OliveTin, follow these steps to get started:
+
+* xref:config.adoc[Learn about configuration] - Understand how OliveTin's configuration works
+* xref:action_execution/create_your_first.adoc[Create your first action] - Build a simple action to test your installation
+* xref:action_examples/intro.adoc[Explore action examples] - See what OliveTin can do with real-world examples
+* xref:solutions/intro.adoc[Check out solutions] - Find complete configurations for common use cases
+* xref:reverse-proxies/intro.adoc[Set up a reverse proxy] - Configure secure access through a reverse proxy
diff --git a/docs/modules/ROOT/pages/install/k8s.adoc b/docs/modules/ROOT/pages/install/k8s.adoc
new file mode 100644
index 0000000..f356997
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/k8s.adoc
@@ -0,0 +1,44 @@
+[#install-k8s]
+= Kubernetes with Manifest files
+
+OliveTin works just fine on Kubernetes. The easiest way to deploy it is with a
+Kubernetes `ConfigMap`, `Deployment`, `Service` and finally `Ingress`. Like so;
+
+== ConfigMap
+[source,yaml]
+----
+include::example$k8s_configmap.yml[]
+----
+
+The main application config.yml for OliveTin is specified in the `ConfigMap`
+above. You will want to edit this later - see the "Configuration" section.
+
+Next, we need a deployment;
+
+== Deployment
+[source,yaml]
+----
+include::example$k8s_deployment.yml[]
+----
+
+That should deploy OliveTin.
+
+== Service
+
+Now that OliveTin is deployed, expose it's port as a service;
+
+[source]
+----
+user@host: kubectl expose deployment/olivetin
+----
+
+Lastly, create a Ingress rule for for that service;
+
+== Ingress
+[source,yaml]
+----
+include::example$k8s_ingress.yml[]
+----
+
+include::partial$install/to_config.adoc[]
+
diff --git a/docs/modules/ROOT/pages/install/linux_alpine.adoc b/docs/modules/ROOT/pages/install/linux_alpine.adoc
new file mode 100644
index 0000000..39c5057
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/linux_alpine.adoc
@@ -0,0 +1,18 @@
+[#install-alpine]
+= Alpine Linux
+
+Alpine Linux is supported by an upstream .apk package, or a manual .tar.gz install, or by xref:install/container.adoc[a container].
+
+These instructions on this page are quite basic, because not many people have tried to use OliveTin on Alpine, or other OpenRC distributions. If you can help suggest improvements to these docs, or Alpine Linux support, it would be great to xref:troubleshooting/wheretofindhelp.adoc[hear from you]!
+
+== Installing the upstream `.apk`
+
+----
+user@host: wget https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin_linux_amd64.apk
+user@host: apk add --allow-untrusted OliveTin_linux_amd64.apk
+----
+
+== Installing the `.tar.gz`
+
+The standard xref:install/targz.adoc[.tar.gz instructions] should work just fine, replacing systemd for the OpenRC file.
+
diff --git a/docs/modules/ROOT/pages/install/linux_arch.adoc b/docs/modules/ROOT/pages/install/linux_arch.adoc
new file mode 100644
index 0000000..d7b5f71
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/linux_arch.adoc
@@ -0,0 +1,23 @@
+[#install-archbtw]
+= Arch Linux (AUR)
+
+There are 3 packages available for Arch Linux;
+
+. link:https://aur.archlinux.org/packages/olivetin[`olivetin` in AUR] - This builds from source, using a release Git tag.This is officially maintained by the authors of the OliveTin project.
+. link:https://aur.archlinux.org/packages/olivetin-bin[`olivetin-bin` in AUR] - This re-packages the binaries built by the official binaries. This is not officially maintained by the authors of the OliveTin project, and might be a bit older - but it should work just fine.
+. link:https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin_linux_amd64.apk[`olivetin` .apk built by the project] - This may be useful to have a package outside of AUR.
+
+== Installation (AUR)
+
+Install using `yay`;
+
+----
+user@host: yay -Syu olivetin
+----
+
+[WARNING]
+One does not simply just do something with Arch without telling someone about it. Therefore, after you successfully install OliveTin on Arch, that you tell at least two people "I'm using OliveTin on Arch, btw". :-)
+
+include::partial$install/post_systemd.adoc[]
+
+
diff --git a/docs/modules/ROOT/pages/install/linux_deb.adoc b/docs/modules/ROOT/pages/install/linux_deb.adoc
new file mode 100644
index 0000000..68aa88d
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/linux_deb.adoc
@@ -0,0 +1,20 @@
+[#install-linuxpackage]
+= Generic .deb based Linux
+
+Running OliveTin as a systemd service on a Linux machine means it can use any program installed on your machine (you don't have to add programs to a container). This is generally easier to use than a container, but containers can work just fine too with a bit more effort.
+
+There are .deb packages published for OliveTin on each release page. If you distribution is not linked in this installation guide, and you use a .deb based Linux distribution, this package should work.
+
+* link:https://github.com/jamesread/OliveTin/releases[downloads page].
+
+You can install these packages for .deb like this;
+
+[source,bash]
+....
+user@host: wget https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin_linux_amd64.deb
+user@host: dpkg -i OliveTin_linux_amd64.deb
+....
+
+include::partial$install/post_systemd.adoc[]
+
+
diff --git a/docs/modules/ROOT/pages/install/linux_fedora.adoc b/docs/modules/ROOT/pages/install/linux_fedora.adoc
new file mode 100644
index 0000000..1f9684a
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/linux_fedora.adoc
@@ -0,0 +1,27 @@
+[#install-fedora]
+= Fedora Linux (dnf)
+
+Fedora is included in the Fedora Project official repositories. For an overview of versions, see the Package Sources page;
+
+https://src.fedoraproject.org/rpms/OliveTin
+
+== Installation (project package)
+
+[bash]
+----
+root@host: rpm -U https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin_linux_amd64.rpm
+----
+
+== Installation (distribution package)
+
+WARN:
+The package included in the Fedora repositories is currently very old, and is not recommended. Please use the project package instead.
+
+Install using `dnf` (or yum, on older versions of Fedora);
+
+----
+user@host: dnf install -y OliveTin
+----
+
+include::partial$install/post_systemd.adoc[]
+
diff --git a/docs/modules/ROOT/pages/install/linux_manjaro.adoc b/docs/modules/ROOT/pages/install/linux_manjaro.adoc
new file mode 100644
index 0000000..c3e8481
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/linux_manjaro.adoc
@@ -0,0 +1,18 @@
+[#install-manjaro]
+= Manjaro (pamac/AUR)
+
+Please see teh xref:install-archbtw[Arch Linux] page for a description of the difference between `olivetin-bin` and `olivetin` in AUR. This page assumes you want to compile from source (`olivetin` package).
+
+[source,bash]
+----
+PATH=$PATH:~/go/bin/
+pamac install buf
+pamac install olivetin
+sudo mkdir -p /etc/OliveTin/custom-webui/themes/
+sudo mkdir -p /etc/OliveTin/entities/
+----
+
+
+include::partial$install/post_systemd.adoc[]
+
+
diff --git a/docs/modules/ROOT/pages/install/linux_rpm.adoc b/docs/modules/ROOT/pages/install/linux_rpm.adoc
new file mode 100644
index 0000000..4726f34
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/linux_rpm.adoc
@@ -0,0 +1,18 @@
+[#install-linuxpackage]
+= Generic .rpm based Linux
+
+Running OliveTin as a systemd service on a Linux machine means it can use any program installed on your machine (you don't have to add programs to a container). This is generally easier to use than a container, but containers can work just fine too with a bit more effort.
+
+There are .rpm packages published for OliveTin on each release page. If you distribution is not linked in this installation guide, and you use a .rpm based Linux distribution, this package should work.
+
+* link:https://github.com/jamesread/OliveTin/releases[downloads page].
+
+You can install these packages for .rpm like this;
+
+[source,bash]
+....
+user@host: rpm -U https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin_linux_amd64.rpm
+....
+
+include::partial$install/post_systemd.adoc[]
+
diff --git a/docs/modules/ROOT/pages/install/macos.adoc b/docs/modules/ROOT/pages/install/macos.adoc
new file mode 100644
index 0000000..1241181
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/macos.adoc
@@ -0,0 +1,8 @@
+= MacOS
+
+Sorry that these instructions are so primitive, the main developer of OliveTin's MacOS machine died :-( If you can help with instructions and screenshots that would be great.
+
+. There is a .tar.gz that you can download, extract. Link: link:https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin-darwin-amd64.tar.gz[`OliveTin-darwin-amd64.tar.gz`]
+. Start a terminal and CD into the OliveTin directory.
+
+include::partial$install/post_generic.adoc[]
diff --git a/docs/modules/ROOT/pages/install/podmandocker.adoc b/docs/modules/ROOT/pages/install/podmandocker.adoc
new file mode 100644
index 0000000..010fc1d
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/podmandocker.adoc
@@ -0,0 +1,32 @@
+= Docker or Podman
+
+include::partial$install/container.adoc[]
+
+include::partial$install/container_registries.adoc[]
+
+If you prefer to use docker-compose, then follow the xref:install/docker_compose.adoc[docker-compose] installation instructions>>.
+
+The standard container setup just needs **port 1337** forwarded for web traffic, and a volume **to store the configuration file**. Note that OliveTin containers expect the config to be in `/config/` inside the container, but it doesn't really matter where this directory is mounted from on the host. This documention uses the convention of `/etc/OliveTin` on the host, but `/dockerStuff/OliveTin/` or similar would be fine.
+
+[source, shell]
+.Create the container (but don't start it yet)
+....
+user@host: mkdir /etc/OliveTin/
+user@host: # ie: Your config file is /etc/OliveTin/config.yaml on the host machine. We'll create this in the post-installation step.
+user@host: docker pull jamesread/olivetin
+user@host: docker create --name olivetin -p 1337:1337 -v /etc/OliveTin/:/config:ro docker.io/jamesread/olivetin
+....
+
+NOTE: The OliveTin container is built using fedora-minimal, which doesn't use customizations that some people may be familiar with from popular projects like LSIO, or debian-based containers. The two top misunderstandings are xref:troubleshooting/puid-pgid.adoc[PUID and PGID are ignored]. Please see the instructions below if you're not familiar with changing users or timezones.
+
+[#container-user]
+== Container user
+
+OliveTin does not need to be run as a root, and does not need any special capabilities. If you want to change the user that OliveTin runs as, use `--user` when creating the container. OliveTin ignores xref:troubleshooting/puid-pgid.adoc[PUID and PGID].
+
+[#container-timezone]
+== Container timezone
+
+To change the xref:advanced_configuration/timezones.adoc[changing the timezone requires a bound-mount] from the host. Olivetin ignores the TZ variable as it is non-standard.
+
+include::partial$install/post_container.adoc[]
diff --git a/docs/modules/ROOT/pages/install/targz.adoc b/docs/modules/ROOT/pages/install/targz.adoc
new file mode 100644
index 0000000..28cb2d7
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/targz.adoc
@@ -0,0 +1,25 @@
+[#install-targz]
+= .tar.gz Install (manual)
+
+Installing OliveTin from a .tar.gz file is considered advanced setup, and is provided for users who cannot use the .deb or .rpm packages, or who don't want to use the Linux container.
+
+== Manual setup (.tar.gz)
+
+. Copy the `OliveTin` binary to `/usr/local/bin/OliveTin`
+.. Make sure it is executable: `chmod +x /usr/local/bin/OliveTin`
+. Make a directory for the configuration files: `mkdir -p /etc/OliveTin`
+.. Copy the `config.yaml` file to `/etc/OliveTin/`
+. Copy the `webui` directory contents to `/var/www/olivetin/` (eg, `/var/www/olivetin/index.html`)
+. Copy the `OliveTin.service` file to `/etc/systemd/system/`
+. Files in the `var` directory are all considered optional.
+.. `var/entities/` contains some example entity files used by the default config.yaml. You can copy these to `/etc/OliveTin/entities/` if you want to use them.
+.. `var/helper-actions/` contains some helpers that are mostly useful for containers. These should be copied to somewhere on your path if you want to use them, such as `/usr/local/bin/`.
+.. `var/initscript/OliveTin` is provided for init-based systems. You can copy this to `/etc/init.d/OliveTin` and make it executable if you want to use it.
+.. `var/manpage/OliveTin.1.gz` contains the manpage for OliveTin. You can copy this to `/usr/share/man/man1/` if you want to use it.
+.. `var/marketing` contains some marketing materials used by the repository. You can probably ignore these unless you're writing a blog article or something.
+.. `var/openrc/OliveTin` is provided for OpenRC-based systems. You can copy this to `/etc/init.d/OliveTin` and make it executable if you want to use it.
+.. `var/tekton` is a directory that contains an experimental Tekton base image builder. You don't need this.
+
+include::partial$install/post_systemd.adoc[]
+
+
diff --git a/docs/modules/ROOT/pages/install/windows.adoc b/docs/modules/ROOT/pages/install/windows.adoc
new file mode 100644
index 0000000..3b81c13
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/windows.adoc
@@ -0,0 +1,12 @@
+= Windows install
+
+Yes, OliveTin is supported on Windows, too!
+
+You can instal install OliveTin as a Windows service, follow the instructions to xref:install/windows_service.adoc[install OliveTin as a Windows service].
+
+== Download and run
+
+1. You can download the latest version of OliveTin here: link:https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin-windows-amd64.zip[`OliveTin-windows-amd64.zip`]
+2. Unzip and run "OliveTin.exe"
+
+include::partial$install/post_generic.adoc[]
diff --git a/docs/modules/ROOT/pages/install/windows_service.adoc b/docs/modules/ROOT/pages/install/windows_service.adoc
new file mode 100644
index 0000000..d4cf4cc
--- /dev/null
+++ b/docs/modules/ROOT/pages/install/windows_service.adoc
@@ -0,0 +1,65 @@
+= Windows Service install
+
+This option is to install OliveTin as a Windows service, which allows it to run in the background and start automatically when the system boots up. This is useful for servers or systems that need to run OliveTin without user intervention. If you want to run OliveTin as a regular application, you can follow the xref:install/windows.adoc[Windows install] instructions instead.
+
+== Download and extract;
+
+[NOTE]
+There is no .msi installer for OliveTin yet, so you will need to download the .zip file and extract it in the desired location.
+
+You can download the latest version of OliveTin here: link:https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin-windows-amd64.zip[`OliveTin-windows-amd64.zip`]
+
+* Create c:/Program Files/OliveTin/
+** Copy **OliveTin.exe** into this directory.
+** Copy the **webui** directory into this directory.
+
+* Create c:/ProgramData/OliveTin/
+** Copy the **config.yaml** file into this directory.
+
+== Test OliveTin startup
+
+Open a command prompt and make suer you are in the c:/Program Files/OliveTin/ directory, then run:
+
+[shell]
+----
+./OliveTin.exe
+----
+
+If everything is set up correctly, you should see the OliveTin service starting up and listening on port 1337.
+
+== Switch startup mode to a Windows service
+
+Windows services require executables to run a "service host" thread, which is not started by default for OliveTin on windows. To run OliveTin as a service, you will need to set this in your configuration file;
+
+include::partial$config-start.adoc[]
+----
+serviceHostMode: "winsvc-standard"
+
+logLevel: info
+
+actions:
+ ...
+----
+
+== Register the service
+
+Open a command prompt as Administrator and run the following command;
+
+[WARN]
+Make sure to run `sc.exe` and not just `sc`, as the latter is a PowerShell alias for `Set-Content` and does not display any output, which can be very confusing.
+
+[shell]
+----
+sc.exe create OliveTin binPath= "C:\Program Files\OliveTin\OliveTin.exe" start= auto
+----
+
+== Start the service
+
+Start the service from the Microsoft Management Console (MMC) or by running the following command;
+
+[shell]
+----
+sc.exe start OliveTin
+----
+
+include::partial$install/post_generic.adoc[]
diff --git a/docs/modules/ROOT/pages/integrations/homeassistant-integration.adoc b/docs/modules/ROOT/pages/integrations/homeassistant-integration.adoc
new file mode 100644
index 0000000..816c924
--- /dev/null
+++ b/docs/modules/ROOT/pages/integrations/homeassistant-integration.adoc
@@ -0,0 +1,39 @@
+= Home Assistant (HACS Integration)
+
+Integrating OliveTin with Home Assistant allows you to control OliveTin from your Home Assistant dashboard. Using the HACS integration is the recommended way to integrate OliveTin with Home Assistant. It is easy to set up and provides a seamless experience.
+
+If you are not familiar with HACs, it is a custom component for Home Assistant that allows you to install and manage custom integrations easily. It is similar to the Home Assistant Add-ons store but for integrations.
+
+== Setup guide
+
+. link:https://hacs.xyz/docs/use/[Install HACS]
+. Go to HACS in your home assistant control panel. Click the "..." dropdown in the top right corner, and select "**Custom Repositories**".
++
+image::hacs-dropdown.png[]
+. In the dialog that pops up, add the custom repo as follows;
++
+image::hacs-custom-repo.png[]
++
+.. **Repository link**: https://github.com/OliveTin/OliveTin-HomeAssistant
+.. **Type**: Integration
+. Search for "OliveTin" in the HACS store, and download it. You will probably need to restart Home Assistant for it be registered correctly.
++
+image::hacs-search.png[]
++
+image::hacs-download.png[]
+. Go to "Settings" and open "Devices & Services"
++
+image::hass-devices-and-services.png[]
+. Click "Add integration" and search for "OliveTin". Note that if it does not show up, you may need to restart Home Assistant.
++
+image::hass-add-integration.png[]
+.. Select "OliveTin"
+.. Host: http://your-server:1337/api
+.. Username:
+.. Password:
+. Under "Configured", you should see OliveTin. Open it by clicking on the arrow.
++
+image:hass-configure-integration.png[]
+. After configuring it, you should see buttons appear in Home Assistant;
++
+image:hass-buttons.png[]
diff --git a/docs/modules/ROOT/pages/integrations/homeassistant.adoc b/docs/modules/ROOT/pages/integrations/homeassistant.adoc
new file mode 100644
index 0000000..ab2295e
--- /dev/null
+++ b/docs/modules/ROOT/pages/integrations/homeassistant.adoc
@@ -0,0 +1,62 @@
+[#hass]
+= Home Assistant (REST)
+
+[NOTE]
+The recommended way to integrate HomeAssistant and OliveTin is via the xref:integrations/homeassistant-integration.adoc[HACS integration].
+
+Home Assistant is able to call REST API endpoints, making integration with OliveTin possible without any custom plugins or integrations in Home Assistant. This does require modifying your Home Assistant configuration.yml file though.
+
+== Give you actions an ID
+
+First, you need to give your actions an ID. This is done by adding an `id` field to your action. This ID will be used by Home Assistant to call the correct action. Here is an example of an action with an ID:
+
+[source, yaml]
+----
+actions:
+ - id: "server_sleep"
+ title: "Server Sleep
+ icon: ping
+ shell: ssh user@server "sudo systemctl suspend"
+----
+
+You then need to know the URL to call to trigger this action. This URL is the OliveTin API URL, with the action ID appended to it. For example, if your OliveTin is running at `http://yourserver:1337`, the URL to call to trigger the action above would be `http://yourserver:1337/api/StartActionAndWait/server_sleep`.
+
+You can learn more about starting actions via the OliveTin API by reading the link xref:api/start_action.adoc[Starting Actions via the API] page, but the method "StartActionAndWait" is the one you will want to use for Home Assistant.
+
+== Add the REST API call to Home Assistant
+
+Now that you have the URL to call to trigger your action, you can add this to your Home Assistant configuration. This is done by adding a `rest_command` to your configuration.yml file.
+
+* link:https://www.home-assistant.io/docs/configuration/[Home Assistant Configuration]
+
+That page assumes you will use the Home Assistant File Editor addon to edit your configuration.yaml. Install it from the Home Assistant addon store if you have not done so already;
+
+image::hassFileEditor.png[]
+
+The addon is started and added to the sidebar;
+
+image::hassFileEditorConfig.png[]
+
+Here is an example of a `rest_command` that calls the action above:
+
+From the file editor now in your sidebar, browse the filesystem to the configuration.yaml file and add the following to the file:
+
+[source, yaml]
+----
+rest_command:
+ olivetin_sleep_mindstorm:
+ url: http://olivetin.webapps.teratan.lan/api/StartActionByGetAndWait/server_sleep
+ method: get
+----
+
+You save the file, and restart Home Assistant to pick up the changes.
+
+== Add a button to your HASS Dashboard
+
+Now that you have a `rest_command` set up to call your action, you can add a button to your Home Assistant dashboard to trigger the action. This is done by adding a `button` to your dashboard configuration.
+
+image::hassButtonSetup.png[]
+
+Set the "Tap Action" to "Call Service" and select the `rest_command` you created earlier. You can also set the icon and name of the button to whatever you like.
+
+Good luck!
diff --git a/docs/modules/ROOT/pages/integrations/n8n.adoc b/docs/modules/ROOT/pages/integrations/n8n.adoc
new file mode 100644
index 0000000..4d594d5
--- /dev/null
+++ b/docs/modules/ROOT/pages/integrations/n8n.adoc
@@ -0,0 +1,15 @@
+= OliveTin n8n Integration
+
+The OliveTin n8n node is a community node for link:https://n8n.io[n8n] that lets you trigger OliveTin actions from your n8n workflows.
+
+* **npm package**: `n8n-nodes-olivetin`
+* **Repository**: link:https://github.com/OliveTin/OliveTin-n8n-node[OliveTin-n8n-node]
+
+== Installation
+
+Install the node in n8n via the Community nodes panel, or install the package in your n8n environment.
+
+[NOTE]
+====
+Detailed installation steps and usage documentation will be added here in a future update.
+====
diff --git a/docs/modules/ROOT/pages/integrations/stream-deck.adoc b/docs/modules/ROOT/pages/integrations/stream-deck.adoc
new file mode 100644
index 0000000..57b727b
--- /dev/null
+++ b/docs/modules/ROOT/pages/integrations/stream-deck.adoc
@@ -0,0 +1,33 @@
+= OliveTin Stream Deck Plugin
+
+[NOTE]
+Plugin has been developed, and is waiting for approval on the Marketplace.
+
+== Get the plugin on the Marketplace
+
+Head over to the link:https://marketplace.elgato.com/stream-deck/plugins[Elegato Marketplace], and search for "OliveTin".
+
+image::stream-deck/marketplace.png[]
+
+Click "Get" on the plugin to install it onto your Stream-Deck.
+
+== Configure a button
+
+Here are some screenshots, and later documentation will follow when the plugin gets approved.
+
+Add the OliveTin button;
+
+image::stream-deck/panel.png[]
+
+Set the OliveTin API URL to;
+
+http://yourserver:1337/api/
+
+image::stream-deck/config.png[]
+
+Switch to the **Input** tab, and the enter an action ID:
+
+image::stream-deck/inputs.png[]
+
+If you don't have IDs set on your action, then read xref:action_customization/ids.adoc[how to set action IDs].
+
diff --git a/docs/modules/ROOT/pages/reference/containerInstallPackages.adoc b/docs/modules/ROOT/pages/reference/containerInstallPackages.adoc
new file mode 100644
index 0000000..4d1eaa2
--- /dev/null
+++ b/docs/modules/ROOT/pages/reference/containerInstallPackages.adoc
@@ -0,0 +1,37 @@
+[#container-dnf]
+= Installing extra container packages
+
+The official OliveTin container image is based on Fedora Linux. Fedora has shown to offer a great mix of stability and support over two decades. The base container image for OliveTin is relatively lightweight, with not many tools installed by default. This keeps the download size small, but you may want to add additional packages.
+
+== Quickstart - using DNF to install additional packages
+
+You can of course create your own container image, but this is probably a lot of work for new users, or people who just want a few extra packages/commands. Instead of creating a whole new container image, you can simply run `microdnf` (the Fodora package manager) to install more commands.
+
+. Start the OliveTin container using one of the methods shown in the xref:install/container.adoc[container installation instructions].
+
+. Then, on the same host that is running the container, spawn a root shell inside the OliveTin container, like this;
++
+----
+user@host: docker exec -it olivetin -u root /bin/bash
+[root@019d08ef95bd /]#
+----
++
+The important thing here is passing `-u root`. By default, OliveTin does not run as root.
+
+. Once you have a root shell in OliveTin, you can use the Fedora package manager - `microdnf` to install things that you might need. If you are used to Debian's `apt-get` tool, it works in a very similar way;
++
+----
+[root@019d08ef95bd /]# microdnf install -y nc
+----
++
+Note that if you upgrade the OliveTin container image, you will need to reinstall these packages.
++
+Once you have finished installing these packages, just exit the root shell using `exit`. You don't need to restart the container - and OliveTin does not need to run as root to use most commands.
+
+== See also
+
+* link:https://hub.docker.com/r/jamesread/olivetin[OliveTin container on Docker Hub]
+* xref:install/container.adoc[Installing using a container]
+* xref:install/docker_compose.adoc[Installing using docker compose]
+* xref:install/helm.adoc[Installing on Kubernetes with Helm]
+* xref:install/k8s.adoc[Installing on Kubernetes (manually)]
diff --git a/docs/modules/ROOT/pages/reference/contribute.adoc b/docs/modules/ROOT/pages/reference/contribute.adoc
new file mode 100644
index 0000000..8d0e4f1
--- /dev/null
+++ b/docs/modules/ROOT/pages/reference/contribute.adoc
@@ -0,0 +1,14 @@
+[#contribute]
+= Contribute
+
+First of all, a huge, huge thanks for reading this page, and considering some form of contribution. Here are some suggestions below.
+
+NOTE: OliveTin does not accept xref:reference/donations_and_sponsorship.adoc[Donations and Sponsorship].
+
+. If you have 2 Minutes to contribute: **Share how you are using OliveTin**, on Reddit, Twitter/X, LinkedIn, Mastodon, or whatever - use the hashtag #OliveTin. Show a screenshot or blog about it. Tell people how OliveTin helped you. There is also #screenshot-showcase in the OliveTin Discord community. If you want to ping me directly, I really like getting email or PMs; jump on Discord and just say it, or contact me via one of the methods found at http://jread.com .
+. If you have 10 minutes to contribute: **Answer a call for support**: look for support issues in #support on discord, or tagged om GitHub issues, and help someone out! You don't have to solve the problem, just point someone in the right direction.
+. If you have 15 minutes to contribute:
+.. **Write up a feature request** on GitHub issues
+.. **Improve the docs** - I make a LOT of typos! https://github.com/OliveTin/docs.olivetin.app
+. **If you have lots of time:** contribute code! https://github.com/OliveTin/OliveTin/blob/main/CONTRIBUTING.adoc
+. **If you have spare compute** - spare server capacity where I can have a virtual machine with root access, then having more VMs to test OliveTin on is always very welcome indeed. I have a lot of sever capacity already though personally, so I'm probably just being greedy for CPU and RAM :-)
diff --git a/docs/modules/ROOT/pages/reference/donations_and_sponsorship.adoc b/docs/modules/ROOT/pages/reference/donations_and_sponsorship.adoc
new file mode 100644
index 0000000..ba51e6d
--- /dev/null
+++ b/docs/modules/ROOT/pages/reference/donations_and_sponsorship.adoc
@@ -0,0 +1,11 @@
+[#donations-and-sponsorship]
+= Donations & Sponsorship
+
+Sometimes I (James Read) get asked if people can donate money, or sponsor me for OliveTin. If you are reading this page, maybe you are thinking the same.
+
+**I do not accept donations or sponsorship** for OliveTin, but I want to **thank you very much indeed** for thinking about the potential.
+
+I have a job that pays me, where I don't write code as part of my day job (very often) - I enjoy coding as a hobby in my spare time. If I get money for that, it somehow takes the pleasure or fun out of it, or makes it feel like a job. There are concerns that I might "prefer" to work on one user's feature request if I am sponsored, or even feel compelled that I have to work on something because someone gave me money. I don't want OliveTin development to go that way. There are also other little considerations, like being paid on the side out of my job then affects my job, and tax, and so on and so on. This is why I don't take any form of money, donations or sponsorship.
+
+Another way that you can show your appreciation for OliveTin, that actually means a lot more than money, is to xref:reference/contribute.adoc[contribute to OliveTin]. There are little ways and big ways to contribute - depends on if you have 2 minutes or 2 weeks to give!
+
diff --git a/docs/modules/ROOT/pages/reference/exitCodes.adoc b/docs/modules/ROOT/pages/reference/exitCodes.adoc
new file mode 100644
index 0000000..532bc31
--- /dev/null
+++ b/docs/modules/ROOT/pages/reference/exitCodes.adoc
@@ -0,0 +1,20 @@
+= Understanding exit codes
+
+OliveTin just runs commands. If the command exits with an unusual exit code
+(something other than 0), OliveTin will tell you. Many Linux commands will exit
+with code 1, 2, 3, etc to indicate different types of errors.
+
+It's important to understand that OliveTin is just reporting back what the
+command exited with, it's very unusual for OliveTin to cause new types of
+errors!
+
+For example, if `ping` exits with code 1 or 2, the documentation for ping says
+that this indicates either a name not found, timeout, or other similar error.
+The best thing you can do is `man ping` to read the ping manual page to find
+out more.
+
+== Common error codes
+
+* **Exit code 127** is used by the Linux shell to indicate "Command not found". Most often this means you need to install the command (often in the linux container image).
+
+
diff --git a/docs/modules/ROOT/pages/reference/includes.adoc b/docs/modules/ROOT/pages/reference/includes.adoc
new file mode 100644
index 0000000..c961632
--- /dev/null
+++ b/docs/modules/ROOT/pages/reference/includes.adoc
@@ -0,0 +1,26 @@
+= Includes
+
+OliveTin 3k supports including configuration files from other files. This is useful for organizing large configurations or reusing common settings across multiple actions.
+
+== Include Syntax
+
+To include another configuration file, use the following syntax in your main configuration file:
+
+[source,yaml]
+----
+include: config.d
+----
+
+This will include all config files in the /config.d/ directory.
+
+== Include Logic
+
+Files are included in alphabetical order based on their filenames. This allows you to control the order of inclusion by naming your files accordingly. For example;
+
+* `01-setup.yaml` contains `logLevel: debug`
+* `02-actions.yaml` contains `logLevel: info`
+* Final `logLevel` will be `info` since `02-actions.yaml` is included after `01-setup.yaml`.
+
+Everything under `actions` is merged into a single `actions` list after all files are included. This means you can define actions in multiple files and they will be combined into one list.
+
+WARNING: All other "lists" are overwritten by later files. For example, if you define `dashboards`, `entities`, `accessControlLists` or similar in multiple files, only the last definition will be used.
diff --git a/docs/modules/ROOT/pages/reference/multiple_instances.adoc b/docs/modules/ROOT/pages/reference/multiple_instances.adoc
new file mode 100644
index 0000000..7f46cf2
--- /dev/null
+++ b/docs/modules/ROOT/pages/reference/multiple_instances.adoc
@@ -0,0 +1,59 @@
+[#multi-inst]
+= Multiple instances on a server
+
+Several users will find themselves wanting to run multiple instances of OliveTin. Depending on how you've setup OliveTin depends on how easy it is to configure that. This page includes instructions for OliveTin installed as a container, and as a package (.tar.gz).
+
+== With Containers
+
+This is the easiest way to run multiple OliveTin instances. Follow the xref:install/container.adoc[Container Installation instructions], or similar for xref:install/docker_compose.adoc[Docker Compose], xref:install/helm.adoc[Helm] or similar to get started.
+
+1. Create a `config.yaml` file for each instance of OliveTin (instances cannot share the same config).
+2. Choose a new external port for OliveTin and set it in the config file (by default that is `1337` is used). For example, set `listenAddressSingleHTTPFrontend: 0.0.0.0:2337` for your 2nd container's config.
+3. When creating the container, pass in the 2nd instance's config, eg; `-v /opt/OliveTin_two/:/config/`
+4. When creating the container, set the external port, eg: `2337:1337` - 2337 is the external port)
+
+You do not need to change the listenAddresses / ports for the other 3 ports that OliveTin uses, when you are running inside a container.
+
+== Without containers - using a package (.tar.gz)
+
+If you are not using containers, then it is probably best not to use a `.deb/.rpm` installation, as those packages can only be installed for one instance.
+
+Instead, follow the instructions for xref:install/targz.adoc[installing from a .tar.gz] archive.
+
+When you come to create the config.yaml file, OliveTin will look for this in it's own startup directory. Therefore it is probably best to extract the .tar.gz file like this and change the paths;
+
+* `/opt/OliveTin_one/`
+* `/opt/OliveTin_two/`
+* `/opt/OliveTin_three/`
+
+Because you are running outside of a container, you will also need to change the "internal" ports used by OliveTin so they are separate for all instances. OliveTin listens on 4 addresses (1 external, 3 internal) and needs 4 ports. You can read about these in the xref:reference/network-ports.adoc[network ports documentation].
+
+NOTE: OliveTin also supports reading the PORT environment variable, and will use this as a base port for the simgle frontend, will add 1 to start extra servers. For example of PORT is 2000, then the simgle frontend will start on port 2000, the REST API on 2001, and so on.
+
+You could end up with a setup that looks like this;
+
+[%header]
+|===
+| Instance Name | Install path | Config file path | Single frontend point (`listenAddressSingleHTTPFrontend`) | REST Actions port (`listenAddressRestActions`) | gRPC Actions port (`listenAddressGrpcActions`) | WebUI Port (`listenAddressWebUI`)
+| OliveTin_one | `/opt/OliveTin_one` | `/opt/OliveTin_one/config.yaml` | `0.0.0.0:1337` | `localhost:1338` | `localhost:1339` | `localhost:1340`
+| OliveTin_two | `/opt/OliveTin_two` | `/opt/OliveTin_two/config.yaml` | `0.0.0.0:2337` | `localhost:2338` | `localhost:2339` | `localhost:2340`
+| OliveTin_three | `/opt/OliveTin_three` | `/opt/OliveTin_three/config.yaml` | `0.0.0.0:3337` | `localhost:3338` | `localhost:3339` | `localhost:3340`
+|===
+
+Note that you will also need to adjust the default systemd service file to point to your install directory, if using that. Here is an example for `OliveTin_two`;
+
+.A modified systemd service file for a 2nd instance
+----
+[Unit]
+Description=OliveTin2
+
+[Service]
+WorkingDirectory=/opt/OliveTin_two/
+ExecStart=/opt/OliveTin_two/OliveTin
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
+----
+
+
diff --git a/docs/modules/ROOT/pages/reference/network-ports.adoc b/docs/modules/ROOT/pages/reference/network-ports.adoc
new file mode 100644
index 0000000..5453241
--- /dev/null
+++ b/docs/modules/ROOT/pages/reference/network-ports.adoc
@@ -0,0 +1,56 @@
+[#network-ports]
+= Network ports
+
+OliveTin might surprise some people when they see it is listening on several
+ports when it starts up. Most of these ports are internal and localhost-only by
+default. It keeps the architecture of OliveTin clean and simple, and allows for
+a lot of flexibility if needed.
+
+== Network flow diagram
+
+Here is the default flow of traffic in OliveTin without any config changes.
+
+[mermaid,png]
+.Flow of an inbound network request
+....
+%%{init: {'theme': 'neutral'}}%%
+graph LR
+ A[Your Browser] -->|HTTPS 443/tcp| C
+ C["Single HTTP frontend"]
+ H["Prometheus"]
+ B["gRPC API"]
+
+ subgraph "OliveTin service"
+ C -->|/api/| D[REST API] --> B
+ C -->|/| E[webui]
+ C -->|/metrics/| H
+ end
+....
+
+1. Traffic comes into OliveTin over your network and hits the only port
+listening - 1337, which listens on all interfaces. This is a micro HTTP reverse
+proxy.
+2. Traffic for `/` gets proxied to `localhost:1340` for the static web
+server.
+3. Traffic for `/api/` gets proxied to `localhost:1338` for REST actions.
+4. The REST API actually makes gRPC API calls internally, to port
+`localhost:1339`.
+
+Below is a detailed reference table.
+
+== Port Reference Table
+
+.Port reference table
+[%header,cols="1,2"]
+|===
+| Config file reference (and Default Address:Port) | Purpose
+| `listenAddressSingleHTTPFrontend: 0.0.0.0:1337` (listen on all available addresses) | This is a "micro reverse proxy" built into OliveTin. It's only purpose is to serve /ui and / (the web interface) from a single endpoint. This means that problems like CORSs and setting "external addresses" is not necessary. It does not do any caching or anything else. It can be disabled, but it makes life a lot easier for you. It's common to put your own reverse proxy like haproxy, traefik, etc in front of this single micro reverse proxy.
+| `listenAddressRestActions: localhost:1338` | REST - the protocol used by web pages to talk to web APIs. In the case of OliveTin, the API is used to get actions, and start actions.
+| `listenAddressGrpcActions:localhost:1339` | gRPC - a very popular method of service-to-service API communication. This provides the "real" API for OliveTin.
+| `listenAddressWebUI: localhost:1340` | Hosts a simple static web server with some HTML, stylesheets, Javascript etc for the web interface.
+| `listenAddressPrometheus: localhost:1341` | Hosts a prometheus endpoint, which is disabled by default. See xref:advanced_configuration/prometheus.adoc[Prometheus] to learn more.
+|===
+
+== See also
+
+* xref:reference/multiple_instances[Running Multiple instances of OliveTin on the same server]
diff --git a/docs/modules/ROOT/pages/reference/reference_snapshots.adoc b/docs/modules/ROOT/pages/reference/reference_snapshots.adoc
new file mode 100644
index 0000000..9d2edeb
--- /dev/null
+++ b/docs/modules/ROOT/pages/reference/reference_snapshots.adoc
@@ -0,0 +1,25 @@
+[#snapsnots]
+= Snapshot builds
+
+It's sometimes useful to test code changes in OliveTin that are still in development - and have not yet made it into an official version, yet. Thankfully, all code changes are automatically compiled into a "snapshot" builds and are saved in GitHub actions.
+
+If you browse to GitHub actions page for OliveTin, you'll find the "Build Snapshot" job, with a list of recent builds.
+
+* https://github.com/OliveTin/OliveTin/actions/workflows/build-snapshot.yml[OliveTin's Build Snapshot page]
+
+image::snapshots.png[]
+
+Most of the time you will want to select the top build, unless you've specifically been given a build link to use.
+
+== Download the snapshot archive
+
+On the job page, you will have a single "snapshot" file listed. In this screenshot, it is 109 MB.
+
+image::snapshot-download.png[]
+
+Once downloaded, you can open the archive using any tool that you use to open .zip files. The contents should read something like this;
+
+image::snapshot-archive.png[]
+
+Extract the file you need, and off you go!
+
diff --git a/docs/modules/ROOT/pages/reference/reference_themes_for_developers.adoc b/docs/modules/ROOT/pages/reference/reference_themes_for_developers.adoc
new file mode 100644
index 0000000..7f73158
--- /dev/null
+++ b/docs/modules/ROOT/pages/reference/reference_themes_for_developers.adoc
@@ -0,0 +1,53 @@
+[#themes-dev]
+= Themes (for theme developers)
+
+== Step by step theme guide
+
+OliveTin themes are simply a directory of CSS and other assets. OliveTin looks for a directory called `custom-webui/themes/` in the same directory as your `config.yaml` file.
+
+Start by creating a directory called `custom-webui/themes/` in the same directory as your `config.yaml` file. This is where you will put your theme files. A theme must also have a theme.css file, which is the main CSS file for your theme. This file must be called `theme.css` and must be in the same directory as your theme folder.
+
+* OliveTin will by default only read theme.css once on startup. If you are intending to change theme.css while OliveTin is running, set `themeCacheDisabled: true` in your config.yaml. This will make OliveTin read theme.css on every request, and is useful for development.
+* Go to https://github.com/OliveTin/theme-template and use this template repository to create your new theme repository on GitHub.
+* Install OliveTin somewhere, and clone your new repository using `git clone` into your themes directory.
+* Set `themeName: ` in your OliveTin config.yaml and restart OliveTin.
+
+Write beautiful CSS to create your theme as you like it, commit your changes to git.
+
+Note that OliveTin will load `/theme.css` depending on `themeName:` in your config file. Images and any other assets will be served at `/custom-webui/themes/mytheme/`.
+
+== Understanding theme URLs
+
+When you create a theme, OliveTin will serve your theme's CSS at `/theme.css` and any other assets at `/custom-webui/themes/mytheme/`. This might be a little strange at first, as your theme.css file wil be in the `/custom-webui/themes/mytheme/` directory, but OliveTin will still serve it at `/theme.css`. Let's explain why this happens;
+
+OliveTin wants to make it easy for your reverse proxy, cache server, or browser, to cache as much content as possible. This means that if OliveTin had to inject a new CSS file into the HTML every time you changed your theme, then your reverse proxy, cache server, or browser would have to re-download the HTML every time you changed your theme. This is not ideal.
+
+It is possible that OliveTin's initial webUiSettings.json (that is loaded to setup the page), could include the theme name, and then the JavaScript could then add an extra stylesheet to load, but this is slow, and creates a horrible "page flash" effect as the theme is requested.
+
+To make things fast, OliveTin will copy the content of your `/custom-webui/themes/mytheme/theme.css` file into memory when it starts, and then requests for `/theme.css` will load this file.
+
+What this means for you, is that to get to files like `backgrond.png` from your CSS, you must write your CSS to point to the file in the `/custom-webui/themes/mytheme/` directory;
+
+.Correct example
+```
+body {
+ background-image: url('/custom-webui/themes/mytheme/background.png');
+}
+```
+
+.Incorrect example
+```
+body {
+ background-image: url('/background.png');
+}
+```
+
+== How to list your theme on the OliveTin themes page
+
+The OliveTin themes page is here; https://olivetin.app/themes
+
+When you are done with your theme, fork https://github.com/OliveTin/themes on GitHub and create a new page under the "content" directory for your new theme. Commit that to GitHub and then raise a pull request.
+
+If you meed more help, please jump on our discord server!
+
+
diff --git a/docs/modules/ROOT/pages/reference/reference_themes_for_users.adoc b/docs/modules/ROOT/pages/reference/reference_themes_for_users.adoc
new file mode 100644
index 0000000..f983447
--- /dev/null
+++ b/docs/modules/ROOT/pages/reference/reference_themes_for_users.adoc
@@ -0,0 +1,96 @@
+[#themes]
+= Themes (for users)
+
+You can look for themes on the link:http://www.olivetin.app/themes/[OliveTin Theme Site].
+
+== Installing a theme
+
+There are 3 ways to install a theme;
+
+
+If running inside a Docker container:
+
+1. Use the `olivetin-get-theme` command to easily Git clone the theme into your `custom-webui/themes/` directory.
+
+If running without using containers:
+
+1. Download the theme .zip and copy it across to your `custom-webui/themes/` directory.
+2. Git Clone the theme into your `custom-webui/themes/` directory.
+
+[#get-theme]
+== How to use the `olivetin-get-theme` command
+
+The default OliveTin configuration comes with an action to get new OliveTin themes. If you deleted it from your configuration, you can add it back in by adding the following to your `config.yaml` file;
+
+[source,bash]
+----
+actions:
+ - title: Get OliveTin Theme
+ shell: olivetin-get-theme {{ themeGitRepo }} {{ themeFolderName }}
+ icon: theme
+ arguments:
+ - name: themeGitRepo
+ title: Theme's Git Repository
+ description: Find new themes at https://olivetin.app/themes
+ type: url
+
+ - name: themeFolderName
+ title: Theme's Folder Name
+ type: ascii_identifier
+----
+
+When you are browsing the OliveTin Theme Site, you can click on a theme and see it's Git Repository URL. You can then copy this URL and paste it into the `olivetin-get-theme` command.
+
+== Where do I find my themes directory?
+
+When OliveTin starts up, it will try to create a directory called `custom-webui/themes/` in your config directory. This directory is where you can put your own custom themes.
+
+OliveTin will then serve this theme directory at `http://yourserver:1337/custom-webui/themes/`, this means that all theme content should go into `/custom-webui/themes/mytheme/`.
+
+Install Themes into your `custom-webui/themes/` directory, which should be in your config directory. If this directory does not exist, you can create it.
+
+[source,yaml]
+----
+├── config.yaml
+├── custom-webui
+│ └── themes
+│ └── custom-icons
+│ ├── icon.png
+│ └── theme.css
+├── entities
+│ ├── containers.json
+│ ├── heating.yaml
+│ ├── servers2.yml
+│ ├── servers.yaml
+│ └── systemd_units.json
+└── installation-id.txt
+----
+
+== Create your own theme (without any intention of publishing it)
+
+Create a sub-directory under your theme directory (eg `custom-webui/themes/mytheme`);
+
+Set your theme in your config
+
+[source,yaml]
+.`config.yaml`
+----
+themeName: mytheme
+----
+
+Add your css into `custom-webui/themes/mytheme/theme.css`.
+
+Your theme css will be loaded "on top" of the existing stylesheet.
+
+To test it is working, set your theme CSS to something ridiculous like;
+
+----
+body {
+ background-color: red !important;
+}
+----
+
+Profit.
+
+Check out xref:reference/reference_themes_for_developers.adoc[Themes for Developers] for more information on how to develop themes.
+
diff --git a/docs/modules/ROOT/pages/reference/release_policy.adoc b/docs/modules/ROOT/pages/reference/release_policy.adoc
new file mode 100644
index 0000000..6fb8da4
--- /dev/null
+++ b/docs/modules/ROOT/pages/reference/release_policy.adoc
@@ -0,0 +1,29 @@
+= Release Policy
+
+This page is a collection of notes around the policy for releases.
+
+== Package and tag deletions
+
+The OliveTin project *WILL* delete packages and tags in the following situations;
+
+* A release went out accidently, or the package is broken in a way that prevents installation or use (something critical).
+* A release went out with the wrong tag, or wrong tag format (eg 2025-11-06 instead of 2025.11.06) for OliveTin 2k.
+ * This is because registries that contain bag tag formats that do not match smever will cause issues for users trying to install or upgrade in the future.
+
+It is understood that this can break some users workflows, but deleting packages occours rarely, and is only done to prevent further issues.
+
+If this is really a concern that you want to guard against, it is recommended to run your own private registry/mirror of OliveTin packages.
+
+Deleted packages will not be supported in any way;
+
+== History of deleted packages;
+
+* **OliveTin 2025-11-06** - deleted due to wrong tag format (2025-11-06 instead of 2025.11.06)
+** GitHub releases page
+** GHCR
+** Docker Hub
+
+* **OliveTin 2025-10-30** - deleted due to wrong tag format (2025-10-30 instead of 2025.10.30)
+** GitHub releases page
+** GHCR
+** Docker Hub
diff --git a/docs/modules/ROOT/pages/reference/updateChecks.adoc b/docs/modules/ROOT/pages/reference/updateChecks.adoc
new file mode 100644
index 0000000..9f65bb9
--- /dev/null
+++ b/docs/modules/ROOT/pages/reference/updateChecks.adoc
@@ -0,0 +1,20 @@
+[#update-checks]
+
+= Update Checks
+
+NOTE: This page is for OliveTin versions **2024.06.02** and afterwards. Previous versions of OliveTin used to have a form of tracking. To learn about how that worked, see xref:reference/updateTracking.adoc[update tracking].
+
+OliveTin has the ability to check for updates, which is now turned OFF if nothing is specified in your configuration file. To enable this feature, set the following in your `config.yaml` file;
+
+[source,yaml]
+.`config.yaml`
+----
+checkForUpdates: true
+----
+
+By enabling this feature, OliveTin will;
+
+* Do a HTTP GET in plaintext (not HTTPS) to http://update-check.olivetin.app/versions.json and download the contents of that file. While the server will see your IP address in it's webserver logs, this information isn't actively used, and the OliveTin project has no intention of actively parsing the logs to use that.
+* Once OliveTin has that json file, it will compare the "latestVersion" attribute against the version it is running.
+* If there is a later version, it is displayed in the page OliveTin footer.
+
diff --git a/docs/modules/ROOT/pages/reference/updateTracking.adoc b/docs/modules/ROOT/pages/reference/updateTracking.adoc
new file mode 100644
index 0000000..c887548
--- /dev/null
+++ b/docs/modules/ROOT/pages/reference/updateTracking.adoc
@@ -0,0 +1,121 @@
+[#update-tracking]
+= Update Checks & Tracking (legacy)
+
+NOTE: This page is for OliveTin versions **2022-01-06** to **2024.06.02**. To see the current behavior of update checking, go to xref:reference/updateChecks.adoc[update checks]
+
+The OliveTin server will now check for updates on startup, and every 7 days after that. It will report those updates as a log message in the console. It does not apply any updates, because this is the choice and responsibility of whoever is running OliveTin to decide if, when, and how to apply any updates.
+
+The information OliveTin sends to the update server is stored/saved., and this could be considered a form of tracking - that is tracking installations, not tracking people. This page hopefully helps explain what, how and why that information is used so you can be informed (and make changes if you wish).
+
+== Design considerations
+
+* The source code for the update check (client), and the update service are open and on GitHub, freely auditable.
+* A generated installation ID (which is just a UUID), that is used to differentiate between installations <>.
+* The update request is sent and stored in plain text - easy to check/audit.
+* The update request goes to an obvious domain name - update-check.olivetin.app
+* The checkins can be freely viewed by anyone in the public log.
+* The update check can be disabled.
+
+[#update-sent]
+== What is sent (and tracked)
+
+When OliveTin checks for updates, it will send the following;
+
+* CurrentVersion - eg: 1.0.0
+* CurrentCommit - the Git commit used to build this version
+* OS - The update check wants to know your OS, because it’s possible in the future that some versions and updates might not be available for all OSes at the same time.
+* Architecture (x86_64, ARM, etc)
+* InstallationID - See below
+
+Here is an example of a log entry that is sent/stored;
+
+----
+{"CurrentVersion":"dev","CurrentCommit":"nocommit","OS":"linux","Arch":"amd64","InstallationID":"f232d115-255c-4728-ba7f-a8f8b2b10a1f"}
+----
+
+If you would like to audit the update code, look at the following directory; https://github.com/OliveTin/OliveTin/tree/main/internal/updatecheck
+
+[#installation-id]
+== What is the OliveTin installation ID?
+
+This is a randomly generated link:https://en.wikipedia.org/wiki/Universally_unique_identifier[UUID] - that is not based on your operating system, not on any of your data. OliveTin tries to create a random installation-id.txt in your config directory when it starts up.
+
+The reason for creating a installation ID is to tell the differences between installations - without this identifier, we would not know if 10 instances, or 1 instances of OliveTin are running a specific version.
+
+In older versions of OliveTin, the MachineID was used instead of InstallationID (see below).
+
+[#machine-id]
+== Why do you need my machine ID?
+
+This was changed in OliveTin - the MachineID is no longer collected, and the project moved to InstallationID instead. The answer is kept here for old versions.
+
+First of all, OliveTin only sends a hashed version of a unique identifier for your machine. OliveTin does not use your actual machine ID, because this is private, and potentially sensitive information in it’s original form. This hashed version of this ID is used, which should be considered safe - because it’s a hash, it’s not possible to get back to the original sensitive machine ID.
+
+From a technical perspective, OliveTin uses the golang package `machineiid` to
+get as hashed version of your MachineId. OliveTin follows the security
+recommendations of that project by using the hashed MachineId; https://github.com/denisbrodbeck/machineid#security-considerations
+
+The reason for getting the machine ID is to tell the differences between installations -
+without this identifier, we would not know if 10 instances, or 1 instances of
+OliveTin are running a specific version.
+
+== Why do you need to store any information at all?
+
+This is incredibly useful information for project developers to know, because;
+
+1. It helps the project developers know how long it takes for updates to be applied by users of OliveTin (ie, should updates be released more often / less often) - the installation ID helps track this.
+2. This helps the project better understand what are the most popular operating systems and architectures (ie, so more testing can be done).
+3. How many old versions of the project to support?
+
+== What is stored?
+
+The update service only stores the information that is sent - <>.
+
+You can audit the update service code here; https://github.com/OliveTin/update-check.olivetin.app
+
+== What is not sent/stored
+
+* No information about you, users of your system, no files, nothing else apart from what is mentioned above.
+* Not your public IP address, or any information about your network
+* Not any information about how you have configured OliveTin, or any actions.
+
+== Where is the information sent
+
+To http://update-check.olivetin.app . This is a virtual machine which stores the logs on the machine filesystem. The data is accessible to all who which to view it, in the interest of transparency.
+
+== Why isn’t this opt-in?
+
+It seems the majority of software does perform update checks by default like this - Chrome, Firefox, most modern Operating Systems, etc. Because no information about people, or your data is being used, apart from your installation ID, this seems like a safe default.
+
+Also, if this were to default off, many people probably would not think about turning it on.
+
+[#disable-update-checks]
+== How do I disable update checking?
+
+If you are worried about privacy, or similar, please do make your concerns known. This is best if this is an open discussion.
+
+But, simply, to disable this feature, add to your config file;
+
+ checkForUpdates: false
+
+[#hide-news-versions]
+== How can I hide version upgrades in the OliveTin web interface?
+
+Set the following in your configuration file;
+
+ showNewVersions: false
+
+OliveTin will need to be restarted for this change to have affect.
+
+[#hsts]
+== Why can't I visit update-check.olivetin.app in my browser?
+
+The root domain for OliveTin (OliveTin.app) has HSTS turned on - this forces your browser to use SSL (HTTPS - the little encryption padlock) for all subdomains - including www.olivetin.app and docs.olivetin.app. Although both of those websites don't transmit anything that really needs encrypion, the web is certainly moving to having SSL turned on everywhere. It even has a positive impact on search engine rankings!
+
+The update-check service - which is accessible from update-check.olivetin.app - is designed to be only accessed via the OliveTin app. Non-web browsers, like this OliveTin app, generally ignore HSTS (and therefore don't try and access the update-check site via SSL/HTTPS.
+
+If you use a non-web browser to try to access the site over HTTP, (eg, curl), you should find it works like normal.
+
+As mentioned previously, the update-check site deliberately uses does not use SSL/HTTPS, to make it easy for people to audit what is actually being sent to the update site. Tools like tcpdump, wireshark, or others can verify that OliveTin is not sending more information than is described on this page.
+
+
diff --git a/docs/modules/ROOT/pages/reference/version_display.adoc b/docs/modules/ROOT/pages/reference/version_display.adoc
new file mode 100644
index 0000000..cdcf886
--- /dev/null
+++ b/docs/modules/ROOT/pages/reference/version_display.adoc
@@ -0,0 +1,70 @@
+[#version-display]
+= Version display
+
+NOTE: This feature was added in OliveTin version 3000.11.0.
+
+OliveTin can show or hide the application version in the web interface. This is controlled by the **showVersionNumber** policy, which can be set globally or per user/group via xref:security/acl.adoc#_acls_and_policies_global[ACL policies].
+
+== What you see
+
+When **showVersionNumber** is enabled (the default):
+
+* The page footer shows the application name and version, for example: **OliveTin 2024.06.02**.
+* If xref:reference/updateChecks.adoc[update checks] are enabled and a newer version exists, a link to the new version may appear in the footer.
+* xref:troubleshooting/server-diagnostics.adoc[Server diagnostics] includes the installed version.
+
+When **showVersionNumber** is disabled:
+
+* The footer shows only **OliveTin** (no version number).
+* No update-version link is shown, even if update checks are on.
+* In server diagnostics output, the version information is redacted, which can be useful for privacy when sharing the report.
+
+== Configuration
+
+The policy defaults to `true` for all users. You can change it in the **defaultPolicy** or override it per user or group in an ACL.
+
+=== Hide version for everyone
+
+[source,yaml]
+.`config.yaml`
+----
+defaultPolicy:
+ showVersionNumber: false
+----
+
+=== Show version only for some users
+
+To hide the version by default but show it for certain users (for example, admins):
+
+[source,yaml]
+.`config.yaml`
+----
+defaultPolicy:
+ showVersionNumber: false
+
+accessControlLists:
+ - name: admins
+ matchUsergroups:
+ - admins
+ policy:
+ showVersionNumber: true
+----
+
+=== Show version for everyone (default)
+
+If you do not set **showVersionNumber**, it is treated as `true`. To set it explicitly:
+
+[source,yaml]
+.`config.yaml`
+----
+defaultPolicy:
+ showVersionNumber: true
+ showDiagnostics: true
+ showLogList: true
+----
+
+== See also
+
+* xref:security/acl.adoc[Access Control Lists] — how policies and ACLs work
+* xref:reference/updateChecks.adoc[Update Checks] — how OliveTin checks for new versions
+* xref:troubleshooting/server-diagnostics.adoc[Server diagnostics] — version is included or redacted based on this policy
diff --git a/docs/modules/ROOT/pages/reverse-proxies/apache.adoc b/docs/modules/ROOT/pages/reverse-proxies/apache.adoc
new file mode 100644
index 0000000..09fdd8f
--- /dev/null
+++ b/docs/modules/ROOT/pages/reverse-proxies/apache.adoc
@@ -0,0 +1,42 @@
+[#apache-path]
+[#apache-dns]
+= Apache HTTPD
+
+include::partial$reverse-proxies/diagram.adoc[]
+
+This is an example of how to setup a DNS name based Apache HTTPD proxy for OliveTin. It assumes OliveTin is running on localhost, port 1337.
+
+./etc/httpd/conf.d/OliveTin.conf
+[source,apache]
+----
+
+ServerName olivetin.example.com
+ProxyPreserveHost On
+ProxyPass / http://localhost:1337/
+ProxyPassReverse / http://localhost:1337/
+
+# Optional: increase timeout for long-lived websocket connections.
+# The websocket endpoint is api/olivetin.api.v1.OliveTinApiService/EventStream (default may drop it).
+ProxyTimeout 600
+
+----
+
+[NOTE]
+====
+Apache's default proxy timeouts are short for long-lived connections. Without increasing them, the websocket to `api/olivetin.api.v1.OliveTinApiService/EventStream` is likely to disconnect regularly. OliveTin will attempt to reconnect automatically, but setting `ProxyTimeout` (or a per-route `timeout=` on the websocket `ProxyPass`) avoids unnecessary disconnects.
+====
+
+Note, you virtual host should *not* include a DocumentRoot directive - httpd is just proxying OliveTin, not serving it's actual pages.
+
+If you proxy the websocket path explicitly, set a per-route timeout (place these *before* the general `ProxyPass /`):
+
+[source,apache]
+----
+ProxyPreserveHost On
+ProxyPass /api/olivetin.api.v1.OliveTinApiService/EventStream ws://127.0.0.1:1337/api/olivetin.api.v1.OliveTinApiService/EventStream timeout=600
+ProxyPassReverse /api/olivetin.api.v1.OliveTinApiService/EventStream ws://127.0.0.1:1337/api/olivetin.api.v1.OliveTinApiService/EventStream
+
+# Optional global default for proxied backends
+ProxyTimeout 600
+----
+
diff --git a/docs/modules/ROOT/pages/reverse-proxies/caddy.adoc b/docs/modules/ROOT/pages/reverse-proxies/caddy.adoc
new file mode 100644
index 0000000..6514663
--- /dev/null
+++ b/docs/modules/ROOT/pages/reverse-proxies/caddy.adoc
@@ -0,0 +1,35 @@
+[#caddy-dns]
+= Caddy
+
+include::partial$reverse-proxies/diagram.adoc[]
+
+Caddy seems to work without any special configuration for websockets. If you see websocket disconnects, you may need to increase timeouts; see xref:troubleshooting/err-websocket-connection.adoc#reverse-proxies-will-close-websockets[Reverse proxies will close websockets]. A simple `Caddyfile` works like this;
+
+.Caddyfile
+[source,nginx]
+----
+http://olivetin.example.com {
+ reverse_proxy * http://localhost:1337
+}
+----
+
+
+[#caddy-path]
+== Custom paths
+
+.Caddyfile
+----
+....
+ handle {$GLOBAL_PORTAL_PATH}/olivetin* {
+ redir {$GLOBAL_PORTAL_PATH}/olivetin {$GLOBAL_PORTAL_PATH}/olivetin/
+ uri strip_prefix {$GLOBAL_PORTAL_PATH}/olivetin
+ basicauth {
+ {$GLOBAL_USER} HASH
+ }
+ reverse_proxy * localhost:1337
+ }
+....
+----
+
+include::partial$reverse-proxies/external-rest.adoc[]
+
diff --git a/docs/modules/ROOT/pages/reverse-proxies/haproxy.adoc b/docs/modules/ROOT/pages/reverse-proxies/haproxy.adoc
new file mode 100644
index 0000000..2b0b062
--- /dev/null
+++ b/docs/modules/ROOT/pages/reverse-proxies/haproxy.adoc
@@ -0,0 +1,30 @@
+[#haproxy-dns]
+= HAProxy
+
+:proxy: HAProxy
+include::partial$reverse-proxies/diagram.adoc[]
+
+This is a straightforward example of how to setup a DNS name based HAProxy setup for OliveTin.
+
+./etc/haproxy/haproxy.conf
+[source,python]
+----
+frontend cleartext_frontend
+ bind 0.0.0.0:80
+
+ option httplog
+
+ use_backend be_olivetin_webs if { hdr(Host) -i olivetin.example.com && path_beg /websocket }
+ use_backend be_olivetin_http if { hdr(Host) -i olivetin.example.com }
+
+backend be_olivetin_http
+ server olivetinServer 127.0.0.1:1337 check
+
+backend be_olivetin_webs
+ timeout tunnel 1h
+ option http-server-close
+ server olivetinServer 127.0.0.1:1337
+----
+
+The `timeout tunnel 1h` is important: without it, HAProxy's default timeouts will close long-lived websocket connections. See xref:troubleshooting/err-websocket-connection.adoc#reverse-proxies-will-close-websockets[Reverse proxies will close websockets] if you see disconnects.
+
diff --git a/docs/modules/ROOT/pages/reverse-proxies/intro.adoc b/docs/modules/ROOT/pages/reverse-proxies/intro.adoc
new file mode 100644
index 0000000..ac71db4
--- /dev/null
+++ b/docs/modules/ROOT/pages/reverse-proxies/intro.adoc
@@ -0,0 +1,50 @@
+[#reverse-proxies]
+= Reverse Proxies
+
+This section of the documentation has a few examples of reverse proxy configurations for popular reverse proxy servers.
+
+Configuring a reverse proxy server for OliveTin is entirely optional. If you don't want to use a reverse proxy, you can skip this section.
+
+[#proxy-guide]
+== Reverse Proxy general guide
+
+It's common to put OliveTin behind a reverse proxy, for authentication, customizing the OliveTin address/path, or for a variety of other reasons.
+
+=== DNS name vs Path based proxies
+
+DNS Name based virtual hosts (eg: olivetin.example.com ) are easier to setup and configure than path based virtual hosts (eg: www.example.com/utils/OliveTin), because path based virtual hosts need to take care mangle OliveTin paths without breaking things.
+
+* If using a path based reverse proxies, you may need to set `externalRestAddress` manually to something like; `http://example.com/utils/OliveTin` in the OliveTin config.yaml.
+* If using DNS Name based reverse proxies, then you should not need to change anything in config.yaml
+
+==== Which port?
+
+If you look at OliveTin startup logs, you will see OliveTin starting services on several ports. For most users, even under reverse proxy configurations, just proxying port 1337 should be all that is needed. To better understand why OliveTin uses several internal ports by default, see xref:reference/network-ports.adoc[network-ports].
+
+=== Handling websockets
+
+OliveTin versions after 2023-08 use websockets instead of polling for updates. Ensure your proxy re-passes the `Connection: Upgrade` and `Upgrade: websocket` headers for the websocket path. In OliveTin 3k the websocket endpoint is `api/olivetin.api.v1.OliveTinApiService/EventStream`; when proxying the whole site (e.g. `ProxyPass /` or `location /`), that path is covered automatically.
+
+Many reverse proxies use short default timeouts and will close long-lived websocket connections, causing disconnects (OliveTin will reconnect automatically). To avoid this, increase the websocket or proxy timeout in your reverse proxy—see the individual proxy pages (e.g. xref:reverse-proxies/apache.adoc[Apache]) and xref:troubleshooting/err-websocket-connection.adoc[Error Connecting to WebSocket] for details.
+
+==== General checklist
+
+* `olivetin.example.com/*` is all just HTTP traffic (port 1337)
+** `olivetin.example.com/` should show the standard webui (port 1337)
+** `olivetin.example.com/webUiSettings.json` should return a JSON file generated by OliveTin that sets up the web interface. (port 1337)
+** `olivetin.example.com/api` should show the REST based API. (port 1337)
+* The websocket (e.g. `api/olivetin.api.v1.OliveTinApiService/EventStream` in OliveTin 3k) should be a websocket connection upgrade.
+
+== What's Next?
+
+Choose your reverse proxy and follow the configuration guide:
+
+* xref:reverse-proxies/nginx.adoc[Nginx] - Configure Nginx as a reverse proxy
+* xref:reverse-proxies/apache.adoc[Apache] - Configure Apache as a reverse proxy
+* xref:reverse-proxies/caddy.adoc[Caddy] - Configure Caddy as a reverse proxy
+* xref:reverse-proxies/traefik.adoc[Traefik] - Configure Traefik as a reverse proxy
+* xref:reverse-proxies/haproxy.adoc[HAProxy] - Configure HAProxy as a reverse proxy
+* xref:reverse-proxies/nginx_proxy_manager.adoc[Nginx Proxy Manager] - Configure NPM as a reverse proxy
+* xref:security/trusted_header.adoc[Use trusted headers] - Authenticate users via reverse proxy headers
+* xref:reference/network-ports.adoc[Understand network ports] - Learn about OliveTin's port configuration
+
diff --git a/docs/modules/ROOT/pages/reverse-proxies/nginx.adoc b/docs/modules/ROOT/pages/reverse-proxies/nginx.adoc
new file mode 100644
index 0000000..6f09160
--- /dev/null
+++ b/docs/modules/ROOT/pages/reverse-proxies/nginx.adoc
@@ -0,0 +1,42 @@
+[#nginx-dns]
+= Nginx
+
+include::partial$reverse-proxies/diagram.adoc[]
+
+This is an example of DNS based proxying with Nginx.
+
+./etc/nginx/cond.d/OliveTin.conf
+[source,nginx]
+----
+include::example$reverse-proxies/etc/reverse_proxy_nginx_dns.conf[]
+----
+
+Increase the proxy timeout for the websocket location so the reverse proxy does not close long-lived connections; see xref:troubleshooting/err-websocket-connection.adoc#reverse-proxies-will-close-websockets[Reverse proxies will close websockets] if you see disconnects.
+
+
+[#nginx-path]
+== Custom paths
+
+These "custom path" instructions are for when you want to use OliveTin with a custom path like "apps.example.com/olivetin" instead of the root path + DNS name - eg: "olivetin.example.com". Generally it is **not recommended** to use a custom path for OliveTin. Instructions are provided below though, and it mostly-works.
+
+.nginx.conf
+[source,nginx]
+----
+....
+ location /OliveTin/ {
+ proxy_pass http://localhost:1337/;
+ proxy_redirect http://localhost:1337/ http://localhost/OliveTin/;
+ }
+
+ location /OliveTin/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;
+ }
+....
+----
+
+include::partial$reverse-proxies/external-rest.adoc[]
+
diff --git a/docs/modules/ROOT/pages/reverse-proxies/nginx_proxy_manager.adoc b/docs/modules/ROOT/pages/reverse-proxies/nginx_proxy_manager.adoc
new file mode 100644
index 0000000..1c2c9e3
--- /dev/null
+++ b/docs/modules/ROOT/pages/reverse-proxies/nginx_proxy_manager.adoc
@@ -0,0 +1,29 @@
+[#nginx-proxy-manager]
+= Nginx Proxy Manager
+
+include::partial$reverse-proxies/diagram.adoc[]
+
+This is an example of DNS based proxying with Nginx Proxy Manager.
+
+This example assumes that you are trying to access OliveTin at **olivetin.npm.teratan.lan** and already have a DNS record pointing to the IP address of the Nginx Proxy Manager. This also assumes you are running Nginx Proxy Manager and OliveTin using Docker Compose.
+
+.docker-compose.yml
+[source,yaml]
+----
+include::example$reverse-proxies/etc/npm-docker-compose.yml[]
+----
+
+Note that OliveTin needs a configuration file to run, see the xref:install/docker_compose.adoc[docker compose install instructions] for a bit more detail.
+
+Assuming you have Nginx Proxy Manager running, start by adding a new proxy host.
+
+* **Domain Names**: olivetin.npm.teratan.lan (again, assume this is the domain you have set up in your DNS)
+* **Scheme**: http (OliveTin does support HTTPS if you create your own certificates, but it is more normal to speak HTTP between Nginx and OliveTin, and just use HTTPS to the proxy).
+* **Forward Hostname/IP**: 192.168.66.168 (change this to be the IP address of your docker host)
+* **Forward Port**: 1337 (this is the default port for OliveTin)
+* **Websockets Support**: Yes (OliveTin uses websockets for the Web UI). If you see websocket disconnects, try increasing the proxy timeout in Nginx Proxy Manager's advanced config; see xref:troubleshooting/err-websocket-connection.adoc#reverse-proxies-will-close-websockets[Reverse proxies will close websockets].
+
+That really should be all that you need to get OliveTin working with Nginx Proxy Manager. If you have any issues, please check the logs of both OliveTin and Nginx Proxy Manager for any errors, and look for ways of getting xref:troubleshooting/wheretofindhelp.adoc[support].
+
+image::npm.png[]
+
diff --git a/docs/modules/ROOT/pages/reverse-proxies/traefik.adoc b/docs/modules/ROOT/pages/reverse-proxies/traefik.adoc
new file mode 100644
index 0000000..d5a62fb
--- /dev/null
+++ b/docs/modules/ROOT/pages/reverse-proxies/traefik.adoc
@@ -0,0 +1,40 @@
+[#traefik-docker-compose]
+= Traefik + Docker Compose
+
+include::partial$reverse-proxies/diagram.adoc[]
+
+The following example is known to work well with Traefik and docker-compose. If you see websocket disconnects, increase the proxy timeout in Traefik; see xref:troubleshooting/err-websocket-connection.adoc#reverse-proxies-will-close-websockets[Reverse proxies will close websockets].
+
+[source,yaml]
+----
+services:
+ olivetin:
+ container_name: olivetin
+ image: jamesread/olivetin
+ volumes:
+ - /docker/olivetin:/config # replace host path or volume as needed
+ ports:
+ - "1337:1337"
+ restart: unless-stopped
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.olivetin.entrypoints=web"
+ - "traefik.http.routers.olivetin.rule=Host(`olivetin.example.com`)"
+
+ traefik:
+ image: "traefik:v2.9"
+ container_name: "traefik"
+ command:
+ #- "--log.level=DEBUG"
+ - "--api.insecure=true"
+ - "--api.dashboard=true"
+ - "--providers.docker=true"
+ - "--providers.docker.exposedbydefault=false"
+ - "--entrypoints.web.address=:80"
+ ports:
+ - "80:80"
+ - "8080:8080"
+ volumes:
+ - "/var/run/docker.sock:/var/run/docker.sock:ro"
+----
+
diff --git a/docs/modules/ROOT/pages/security/acl.adoc b/docs/modules/ROOT/pages/security/acl.adoc
new file mode 100644
index 0000000..0ae984e
--- /dev/null
+++ b/docs/modules/ROOT/pages/security/acl.adoc
@@ -0,0 +1,152 @@
+[#acls]
+= Access Control Lists
+
+OliveTin uses Access Control Lists (ACLs) to implement it's security model, which allows you to have fine-grained control over indivividual actions or groups of actions. This can be used to implement role based access control (RBAC), or other security models that you may need.
+
+ACLs are built up of the following set of rules;
+
+* `name` - The name of the ACL. This is used to identify the ACL in the configuration file.
+* `matchUsergroups` - A list of usergroups that this ACL applies to. This is used to match users that are in the specified usergroup.
+* `matchUserNames` - A list of usernames that this ACL applies to. This is used to match users that are in the specified usergroup.
+* `permissions` - A set of permissions which are used with **actions**. eg: `view`, `exec`, `logs`, etc.
+** `addToEveryAction` - A boolean value that indicates if this ACL should be added to every action. This is useful if you want to apply the same ACL to all actions, without having to manually add it to each action.
+* `policy` - A policy is a set of rules that affect the **whole of OliveTin**.
+
+== ACLs and Policies (global)
+
+[mermaid, "sample", png]
+....
+graph TD
+ A[ACL] --> B[Policy]
+ A -->|User/UserGroup| C[User/UserGroup]
+....
+
+**Policies** are a set of rules that apply to the whole of OliveTin ("global"), and not just to individual actions (like permissions are).
+
+The **defaultPolicy** is special, in that all values are set to true by default. This means that if you do not set a `defaultPolicy`, then all policies will be set to `true` by default. This is effectively what the `defaultPolicy` is set to;
+
+[source,yaml]
+----
+defaultPolicy:
+ showDiagnostics: true
+ showLogList: true
+----
+
+You can override defaults using an ACL, like this;
+
+[source,yaml]
+----
+accessControlLists:
+ - name: admins
+ matchUsergroups:
+ - admins
+ policy:
+ showDiagnostics: true
+ showLogList: true
+
+defaultPolicy:
+ showDiagnostics: false
+ showLogList: false
+----
+
+== ACLs and Permissions (for Actions)
+
+[mermaid, "sample", png]
+....
+graph TD
+ A[Action] -->|ACL| B[ACL]
+ B -->|User/UserGroup| C[User/UserGroup]
+ B -->|Permissions| D[Permissions]
+....
+
+An action always starts with `defaultPermissions` (see below), and then then have one or more ACLs applied to it. This means that you can for example have an action that is only available to a certain group of users, or only to a single user.
+
+Let's say you have a user `james` and a usergroup `admins`. You can then create an ACL that only allows `james` and users in the `admins` group to view and execute an action.
+
+You can specify default permissions for all actions by changing the `defaultPermissions` like this;
+
+[source,yaml]
+.`config.yaml`
+----
+defaultPermissions:
+ view: false
+ exec: false
+ logs: true
+----
+
+In the example above, all users will start off with the permissions to only see action logs - but will not be able to view or execute actions.
+
+It is then possible to add an "admins" ACL on top of every action. In the example below, we define one extra ACL called "admins", which matches any users with the usergroup also called "admins". This ACL will then be applied to all actions, and will allow users in the "admins" usergroup to view and execute the action.
+
+[source,yaml]
+.`config.yaml`
+----
+defaultPermissions:
+ view: false
+ exec: false
+
+accessControlLists:
+ - name: admins
+ matchUsergroups:
+ - admins
+ permissions:
+ view: true
+ exec: true
+
+actions:
+ - title: Shutdown Reactor
+ acls:
+ - admins
+----
+
+=== Add an ACL to every action
+
+Sometimes you want to define an ACL that applies to all actions. It can be tedious and error prone to manually add the ACL under the "acls" list for every action, if you have several actions. Instead, there is a shortcut to add an ACL to all actions - `addToEveryAction: true`.
+
+[source,yaml]
+.`config.yaml`
+```yaml
+accessControlLists:
+ - name: admins
+ matchUsergroups:
+ - admins
+ permissions:
+ view: true
+ exec: true
+ addToEveryAction: true
+```
+
+== ACL Matching - usernames and usergroups.
+
+You can match users based on their usergroup which is the most common, but it is also possible to match based on the user's username.
+
+[source,yaml]
+.`config.yaml`
+```yaml
+accessControlLists:
+ - name: admins
+ matchUsergroups:
+ - admins
+ permissions:
+ view: true
+ exec: true
+
+ - name: james
+ matchUserNames:
+ - james
+ permissions:
+ view: true
+ exec: true
+```
+
+== What's Next?
+
+Now that you understand ACLs, here's how to implement them:
+
+* xref:security/examples.adoc[View security examples] - See complete ACL configurations
+* xref:security/example_login_required.adoc[Example: Login required] - Configure login requirements
+* xref:security/example_some_admin_actions.adoc[Example: Admin-only actions] - Restrict actions to admins
+* xref:security/local.adoc[Set up local users] - Create users for ACL matching
+* xref:security/oauth2.adoc[Configure OAuth2] - Set up OAuth2 for user groups
+* xref:security/design_choices.adoc[Security design recommendations] - Learn best practices for ACL design
+
diff --git a/docs/modules/ROOT/pages/security/concepts.adoc b/docs/modules/ROOT/pages/security/concepts.adoc
new file mode 100644
index 0000000..d66dc58
--- /dev/null
+++ b/docs/modules/ROOT/pages/security/concepts.adoc
@@ -0,0 +1,34 @@
+[#auth-concepts]
+= Security Concepts
+
+OliveTin implements a security model that covers **Authentication**, **Authorization** (via xref:security/acl.adoc[ACLs]) and **Accounting**.
+
+== Authentication
+
+To allow users to be Authenticated to OliveTin, there are several options to choose from;
+
+- xref:security/local.adoc[Local Users] (ie: Login with Username and Password)
+- xref:security/oauth2.adoc[OAuth2] (eg: Google, GitHub, etc)
+- xref:security/trusted_header.adoc[Trusted Header] (eg: Nginx, Apache, etc)
+- xref:security/jwt.adoc[JWT] (eg: Traefik, Organizr, etc)
+
+== Authorization
+
+OliveTin's authorization system, or permissions, is built on xref:security/acl.adoc[Access Control Lists]. This is a powerful mechanism that allows you to implement very fine grained access control, or your own role based access control (RBAC).
+
+== Accounting
+
+OliveTin's accounting is via it's logs. This aspect of OliveTin's security model is poorly documented at the moment.
+
+== What's Next?
+
+Now that you understand OliveTin's security model, implement it for your use case:
+
+* xref:security/local.adoc[Set up local users] - Configure username/password authentication
+* xref:security/oauth2.adoc[Configure OAuth2] - Integrate with OAuth2 providers (Google, GitHub, etc.)
+* xref:security/trusted_header.adoc[Use trusted headers] - Authenticate via reverse proxy headers
+* xref:security/jwt.adoc[Configure JWT] - Use JWT tokens for authentication
+* xref:security/acl.adoc[Set up Access Control Lists] - Implement fine-grained permissions
+* xref:security/examples.adoc[View security examples] - See complete security configurations
+* xref:security/design_choices.adoc[Security design recommendations] - Learn best practices for securing OliveTin
+
diff --git a/docs/modules/ROOT/pages/security/content_security_policy.adoc b/docs/modules/ROOT/pages/security/content_security_policy.adoc
new file mode 100644
index 0000000..fe9a393
--- /dev/null
+++ b/docs/modules/ROOT/pages/security/content_security_policy.adoc
@@ -0,0 +1,54 @@
+[#content-security-policy]
+= Content Security Policy (CSP)
+
+When xref:reference/network-ports.adoc[the single HTTP frontend] is enabled (the default), OliveTin adds several browser security headers to every response, including `Content-Security-Policy`. This page explains how to turn that header off or replace it with a less strict policy when you need to (for example, custom scripts, different API hosts, or embedding in an iframe).
+
+[WARNING]
+Relaxing or removing CSP weakens protection against cross-site scripting and related attacks. Prefer the smallest change that fixes your issue, and keep the rest of the policy as tight as you can.
+
+== Default behavior
+
+With default settings, OliveTin sends a `Content-Security-Policy` header similar to the following (single line in the actual response):
+
+[source,text]
+----
+default-src 'self'; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'
+----
+
+If `security.headerContentSecurityPolicy` is `true` but `security.contentSecurityPolicy` is left empty, OliveTin fills in this default on startup.
+
+== Disable the CSP header entirely
+
+Set `security.headerContentSecurityPolicy` to `false`. OliveTin will not send `Content-Security-Policy` on responses from the single HTTP frontend.
+
+include::partial$config-start.adoc[]
+----
+security:
+ headerContentSecurityPolicy: false
+----
+
+== Use a custom (relaxed) policy
+
+Keep `security.headerContentSecurityPolicy` `true` and set `security.contentSecurityPolicy` to the full header value you want. For example, to allow WebSocket connections to the same host when you serve the UI over plain HTTP in a lab (not recommended for production), you might widen `connect-src`:
+
+include::partial$config-start.adoc[]
+----
+security:
+ headerContentSecurityPolicy: true
+ contentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' http: https: ws: wss:; frame-ancestors 'none'; base-uri 'self'"
+----
+
+Build your policy from the https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP[MDN documentation on CSP] and test in the browser developer tools (Console will report CSP violations).
+
+Typical reasons people adjust this setting:
+
+* **Custom JavaScript or third-party scripts** — You may need to extend `script-src` (and sometimes `connect-src` for XHR/fetch). See xref:advanced_configuration/webui.adoc#custom-js[Custom JavaScript].
+* **Embedding OliveTin in another site** — The default includes `frame-ancestors 'none'`, which blocks iframes. You must change that directive (and may need to relax `X-Frame-Options` using `security.headerXFrameOptions` and `security.xFrameOptions`) if embedding is required.
+* **API or auth on another origin** — Extend `connect-src` (and possibly `form-action` or others) to include those URLs.
+* **Reverse proxy also sets CSP** — Your proxy might add a second policy; browsers combine them. Align OliveTin and the proxy so you do not get conflicting or unexpectedly strict effective policies.
+
+== Related settings
+
+Other keys under `security` in `config.yaml` control additional headers (for example `headerXContentTypeOptions`, `headerXFrameOptions`, and `xFrameOptions`). This page focuses on CSP; see the OliveTin `SecurityConfig` in the application source if you need the full list of fields.
+
+Configuration reload: if you use OliveTin's live config reload, changes to `security` are picked up without restarting the process in typical setups.
diff --git a/docs/modules/ROOT/pages/security/design_choices.adoc b/docs/modules/ROOT/pages/security/design_choices.adoc
new file mode 100644
index 0000000..e411074
--- /dev/null
+++ b/docs/modules/ROOT/pages/security/design_choices.adoc
@@ -0,0 +1,21 @@
+= Security Design & Hardening Recommendations
+
+[WARNING]
+OliveTin has not *yet* had a remote code execution vulnerability found in it. However, given what OliveTin does, it is possible, and likely that a security vulnerability will be found in the future. This document explains how OliveTin is designed so that you can make an informed decision about how to use OliveTin in your environment. It also provides hardening recommendations to help you secure OliveTin.
+
+== Security Design
+
+OliveTin has a few design choices that should help it's general security posture.
+
+* OliveTin does deliberate not have any web based control panel where commands can be typed in. This is try to avoid arbitary command execution vulnerabilities caused by authentication bypass attacks.
+* Control over what commands are run is determined via the `config.yaml` alone. OliveTin does NOT write to the config.yaml in any way. This is to avoid any of arbitary command execution vulnerabilities caused by writing to the config.yaml.
+* OliveTin listens on just 1 open public port by default (1337). The rest of the ports only listen on `localhost` so you don't have to worry about them in your firewall.
+* Standard Linux controls can be used to run OliveTin as non-root, with `sudo` permissions if needed. See the action customization section of these docs for more details.
+* Robust code-scanning, code review, and dependency analysis at build-time. OliveTin uses many linters and code checkers, especially on new pull requests. Out dated dependencies are addressed quickly.
+
+== Hardening Recommendations
+
+* Implement authentication on the OliveTin API using one of the many methods provided.
+* Run OliveTin as a non-root user, or even better, run OliveTin in a container (as non-root).
+* Use normal `sudo` permissions to elevate OliveTin to run privileged commands, and restrict the commands that OliveTin can run with `sudo` to only the ones you need.
+* Place a reverse proxy in front of OliveTin, or better, a web application firewall.
diff --git a/docs/modules/ROOT/pages/security/example_login_required.adoc b/docs/modules/ROOT/pages/security/example_login_required.adoc
new file mode 100644
index 0000000..4036493
--- /dev/null
+++ b/docs/modules/ROOT/pages/security/example_login_required.adoc
@@ -0,0 +1,61 @@
+[#example-login-required]
+= Example: Force Login
+
+A common use case for OliveTin with security is to expose some dashboards that require login to be able to use. This page brings together the configuration options that are needed to achieve this. The most important configuration option is setting `authRequireGuestsToLogin` to `true`.
+
+== Full example configuration
+
+```yaml
+logLevel: "INFO"
+
+authRequireGuestsToLogin: true
+
+accessControlLists:
+ - name: "admins"
+ permissions:
+ view: true
+ exec: true
+ logs: true
+ matchUsergroups:
+ - "admins"
+ addToEveryAction: true
+
+
+authLocalUsers:
+ enabled: true
+ users:
+ - username: "admin"
+ usergroup: admins
+ password: -- your password hash here --
+
+actions:
+ - title: "Restart"
+ shell: echo "Restart"
+
+dashboards:
+ - title: "Admin Dashboard"
+ contents:
+ - title: "Restart"
+```
+
+Note, to use this configuration, you will need to replace `-- your password hash here --` with a password hash. You can generate a password hash by looking at the options in the xref:security/local.adoc[local-users] configuration section.
+
+== Important configuration option: `AuthRequireGuestsToLogin`
+
+The `AuthRequireGuestsToLogin` option is a helpful shortcut that sets all `defaultPermissions` to false, and makes it so that all guests are prompted to login before they can do anything with OliveTin.
+
+Technically, you could achieve the same effect by setting `defaultPermissions` to `false` and setting up an ACL that allows access to the login page, but `AuthRequireGuestsToLogin` is a more convenient way to achieve the same effect.
+
+== Per-action ACLs, vs `addToEveryAction`
+
+It is possible to specify one or more ACL per action, like so;
+
+```yaml
+actions:
+ - title: "Restart"
+ shell: echo "Restart"
+ acl:
+ - name: "admins"
+```
+
+However, this configuration is also a bit more verbose, and if you just have one main ACL, can save yourself some typing by using the `addToEveryAction` option in the ACL configuration.
diff --git a/docs/modules/ROOT/pages/security/example_some_admin_actions.adoc b/docs/modules/ROOT/pages/security/example_some_admin_actions.adoc
new file mode 100644
index 0000000..5ebcaf7
--- /dev/null
+++ b/docs/modules/ROOT/pages/security/example_some_admin_actions.adoc
@@ -0,0 +1,52 @@
+= Example: Some actions require admin
+
+A common use case for OliveTin with security is to expose some actions to guests, and have some actions that require login to be able to use. This page brings together the configuration options that are needed to achieve this. The most important configuration option is setting `authRequireGuestsToLogin` to `true`.
+
+== Full example configuration
+
+```yaml
+logLevel: "INFO"
+
+accessControlLists:
+ - name: "noguests"
+ permissions:
+ view: false
+ exec: false
+ logs: false
+ matchUsernames: [ "guest" ]
+
+ - name: "admins"
+ permissions:
+ view: true
+ exec: true
+ logs: true
+ matchUsergroups: [ "admins" ]
+
+authLocalUsers:
+ enabled: true
+ users:
+ - username: "admin"
+ usergroup: admins
+ password: -- your password hash here --
+
+actions:
+ - title: "Date"
+ shell: date
+
+ - title: "Reboot"
+ shell: reboot" # Note that this won't work inside a container
+ acls:
+ - "noguests"
+ - "admins"
+
+dashboards:
+ - title: "Guest Dashboard"
+ contents:
+ - title: "Date"
+
+ - title: "Admin Dashboard"
+ contents:
+ - title: "Reboot"
+```
+
+Note, to use this configuration, you will need to replace `-- your password hash here --` with a password hash. You can generate a password hash by looking at the options in the xref:security/local.adoc[local-users] configuration section.
diff --git a/docs/modules/ROOT/pages/security/examples.adoc b/docs/modules/ROOT/pages/security/examples.adoc
new file mode 100644
index 0000000..0863297
--- /dev/null
+++ b/docs/modules/ROOT/pages/security/examples.adoc
@@ -0,0 +1,10 @@
+= Security Examples
+
+The following examples show you how to combine several security configuration options to setup common scenarios that people often ask for.
+
+* xref:security/example_login_required.adoc[Example: Login Required]
+* xref:security/example_some_admin_actions.adoc[Example: Some actions required admin]
+
+== Security Solutions
+
+* xref:solutions/cloudflare_access_tunnel/index.adoc[]
diff --git a/docs/modules/ROOT/pages/security/jwt.adoc b/docs/modules/ROOT/pages/security/jwt.adoc
new file mode 100644
index 0000000..9afcec8
--- /dev/null
+++ b/docs/modules/ROOT/pages/security/jwt.adoc
@@ -0,0 +1,22 @@
+[#jwt]
+= JWT Authorization
+
+One of the best ways to do authorization with OliveTin is to pass it a **JSON Web token (JWT)**, after first authenticating with a popular single sign on system, like Keycloak, CloudFlare Tunnels, Authentik or Organizr.
+
+Two types of JWT mechanisms are supported;
+
+* xref:security/jwt_keys.adoc[JWT with Keys] (eg: CloudFlare Tunnels, Authentik)
+** X509 Certs/Keys on disk are supported
+** **JWKS** is also supported
+* xref:security/jwt_hmac.adoc[JWT with HMAC] (eg: Organizr)
+
+== JWT Flow
+
+The flow generally goes like this;
+
+1. User browses to a website like Organizr and logs in, which sets a JWT Cookie for apps.example.com.
+2. User browses to OliveTin.apps.example.com, and the cookie is sent to OliveTin.
+3. OliveTin verifies the JWT token given the signing secret, and picks up on the `name` and `group` fields from the JWT claim.
+4. OliveTin matches any relevant ACLs based on the claims.
+5. If any ACLs are not matched, then the defaultPermissions are used.
+
diff --git a/docs/modules/ROOT/pages/security/jwt_hmac.adoc b/docs/modules/ROOT/pages/security/jwt_hmac.adoc
new file mode 100644
index 0000000..ca3a693
--- /dev/null
+++ b/docs/modules/ROOT/pages/security/jwt_hmac.adoc
@@ -0,0 +1,100 @@
+[#jwt-hmac]
+= JWT with HMAC
+
+You need to know your JWT **Cookie Name** and **Hash Secret**. Whatever tool you are using to authenticate users will probably have instructions on how to find this.
+
+* link:https://docs.organizr.app/features/server-authentication#validating-the-token[Organizr - Under "Validating the token"]
+
+ Adding JWT details to OliveTin config.yaml
+
+Setup your config file so it has something like this;
+
+[source,yaml]
+.`config.yaml`
+----
+# It's often useful to turn logging to DEBUG when trying to work out authentication problems
+logLevel: "INFO"
+
+authJwtCookieName: "Organizr_token_1234..."
+authJwtHmacSecret: "3l4jh23v_123!"
+authJwtClaimUsername: "username"
+authJwtClaimUsergroup: "usergroup"
+----
+
+Note that your `authJwtCookieName` and `authJwtSecret` will need to be set exactly as they appear in your Authentication software.
+
+== Usable claims
+
+OliveTin currently can match Access Control Lists based on a **username** or **user group(s)**. You can see if these are being used properly turning on `DEBUG` logging and looking at the jwt claims.
+
+If `authJwtClaimUsergroup` is any array, ACL groups will match any of the user groups in the array.
+
+== Setup default permissions
+
+OliveTin will assume that guests are able to View and Execute every action by default. When you are setting up authorization you probably want to limit this. You can do that by setting `defaultPermissions` like this;
+
+[source,yaml]
+.`config.yaml`
+----
+logLevel: "INFO"
+
+defaultPermissions:
+ view: false
+ exec: false
+----
+
+== Setup OliveTin Access Control Lists
+
+Access Control Lists are a way to override the default permissions.
+
+[source,yaml]
+.`config.yaml`
+----
+logLevel: "INFO"
+
+defaultPermissions:
+ view: false
+ exec: false
+ logs: true
+
+accessControlLists:
+ - name: Admins
+ addToEveryAction: true
+ matchUsergroups:
+ - Admins
+ permissions:
+ view: true
+ exec: true
+ logs: true
+
+ - name: "Developers"
+ matchUsergroups:
+ - "developer"
+ permissions:
+ view: true
+ exec: false
+ logs: false
+
+actions:
+ - name: Only visible to admins
+ shell: echo "I am a secret command only visible to admins"
+
+ - name: Restart database
+ shell: systemctl restart mariadb
+ acls:
+ - "developer"
+----
+
+In the example above, the `admins` ACL is automatically added to every action, because `addToEveryAction` is true.
+
+ Customizing field names
+
+You may need to customize the field names for your JWT authentication.
+
+[source,yaml]
+.`config.yaml`
+----
+authJwtClaimUsername: "username"
+authJwtClaimUsergroup: "usergroup"
+----
+
diff --git a/docs/modules/ROOT/pages/security/jwt_keys.adoc b/docs/modules/ROOT/pages/security/jwt_keys.adoc
new file mode 100644
index 0000000..f1b4b46
--- /dev/null
+++ b/docs/modules/ROOT/pages/security/jwt_keys.adoc
@@ -0,0 +1,51 @@
+[#jwt-keys]
+= JWT with Keys
+
+include::partial$earlydoc.adoc[]
+
+== Using Public Keys via JWKS
+
+OliveTin Supports **JSON Web Key Sets (JWKS)**. This approach is often used with services like CloudFlare.
+
+[source,yaml]
+.`config.yaml`
+----
+authJwtAud: "asdf1234"
+authJwtCertsURL: "https://mydomain.cloudflareaccess.com/cdn-cgi/access/certs"
+authJwtClaimUsername: email
+authJwtCookieName: "CF_Authorization"
+----
+
+You may well want to set `logLevel: DEBUG` and `insecureAllowDumpJwtClaims: true` in your config when testing JWT for the first time.
+
+== Using with Teleport/Headers
+
+If you are using Teleport, you can use the `authJwtCertsURL` to point to the Teleport JWKS.
+
+Teleport can only https://goteleport.com/docs/enroll-resources/application-access/jwt/introduction/#inject-jwt[inject the JWT into a header], so you will need to set `authJwtHeader` to the header name that you have configured Teleport to use, e.g., `Authorization`.
+
+
+[source,yaml]
+.`config.yaml`
+----
+authJwtCertsURL: "https://teleport.mydomain/.well-known/jwks.json"
+authJwtHeader: Authorization
+----
+
+Replace teleport.mydomain with the domain of your Teleport instance.
+
+== Using Public Keys on Disk
+
+This approach can be useful if your Authentication service does not support JWKS, or if you don't want to use it. Public Keys should be available on disk in a file - which can have any filename or extension you like. The files need to be RSA keys in PEM format to be used by OliveTin, though. P12 is not supported.
+[source,yaml]
+.`config.yaml`
+----
+authJwtAud: "asdf1234"
+authJwtPubKeyPath: "/opt/mykey.crt"
+authJwtClaimUsername: email
+authJwtCookieName: "CF_Authorization"
+----
+
+== See Also
+
+* xref:solutions/cloudflare_access_tunnel/index.adoc[Cloudflare Access & Tunnels Solution]
diff --git a/docs/modules/ROOT/pages/security/local.adoc b/docs/modules/ROOT/pages/security/local.adoc
new file mode 100644
index 0000000..6014eda
--- /dev/null
+++ b/docs/modules/ROOT/pages/security/local.adoc
@@ -0,0 +1,94 @@
+[#local-users]
+= Local Users Login
+
+OliveTin supports just basic users defined with a username and password in the config.yaml file. This can be used when you do not want to use a full authentication system like LDAP, OAuth2 or a Reverse Proxy.
+
+== Define a user
+
+include::partial$config-start.adoc[]
+----
+authLocalUsers:
+ enabled: true
+ users:
+ - username: james
+ password: $argon2id$v=19$m=65536,t=4,p=6$LnNW4sw+jZfa5Ex3YjfuHQ$vl8pjUJhxNmBxScV4lI3cgAZPkNB1rSrnX6ibgoAP8k
+----
+
+== Define users with a user group
+
+OliveTin local users do not need to be part of a user group, and unless any user groups are added, they will not be in any user group. However, if you want to add a user to a user group, you can do so like this:
+
+include::partial$config-start.adoc[]
+----
+authLocalUsers:
+ enabled: true
+ users:
+ - username: alice
+ usergroup: admins
+ password: $argon2id$v=19$m=65536,t=4,p=6$LnNW4sw+jZfa5Ex3YjfuHQ$vl8pjUJhxNmBxScV4lI3cgAZPkNB1rSrnX6ibgoAP8k
+
+ - username: bob
+ password: ...
+ usergroup: admins
+
+ - username: charlie
+ password: ...
+ usergroup: webmasters
+----
+
+== Get a Argon2id hashed password
+
+You will notice from the configuration examples above that the password is hashed using Argon2id. You can use any of the following methods to generate a Argon2id hashed password;
+
+=== Option A - Using OliveTin API
+
+You can see from the example above that the config contains a single user called *james*, and the password is hashed using Argon2id. OliveTin provides a utility API to hash passwords using Argon2id which can be useful when you want to create new users. Simply run the following curl command to hash a password:
+
+```bash
+curl -sS --json '{"password": "myPassword"}' http://olivetin.example.com:1337/api/PasswordHash
+```
+
+NOTE: Curl 7.82 added support for the `--json` option, if you are using an older version of curl, see link:https://github.com/OliveTin/OliveTin/issues/462[this issue].
+
+This will return a output like this, you can then copy and paste this hash into your config.yaml file;
+
+```
+Your password hash is: $argon2id$v=19$m=65536,t=4,p=6$dlWTV1RL04/Nuvxzl94NAg$KsYXvCFE2Eu/jkXi/dbbZM3I/2b2VByTAwRIenUwdJk
+```
+
+=== Option B - Using the `argon2` command line tool
+
+You can also easily hash the password using the `argon2` package:
+
+```bash
+echo -n "myPassword" | argon2 "$(openssl rand -base64 16)" -id -t 4 -m 16 -p 6 -l 32 -e
+```
+
+=== Opption C - Using the `hash` docker image
+Or using the link:https://hub.docker.com/r/leplusorg/hash[hash] docker image:
+
+```bash
+docker run --rm -i --net=none leplusorg/hash sh -c 'echo -n "myPassword" | argon2 "$(openssl rand -base64 16)" -id -t 4 -m 16 -p 6 -l 32 -e'
+```
+
+Then simply visit the OliveTin web interface and browse to the login page, eg: http://olivetin.example.com:1337/login
+
+=== Why does OliveTin use Argon2id?
+Argon2id is the password hashing algorithm that is link:https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html[recommended by OWASP] as of October 2024. There doesn't seem to be a good reason yet to provide configuration options for changing the password hashing algorithm, but if you have a good reason, please open an issue on the GitHub repository.
+
+
+== Force login page
+
+If you don't want to allow guests to do anything in OliveTin, you can use the `authRequireGuestsToLogin` option to force all users to login before they do anything. This will redirect all users to the login page if they are not logged in, and it will also set `defaultPermissions` to `false`, meaning that permissions must be explicitly set for each user or user group.
+
+include::partial$config-start.adoc[]
+----
+authRequireGuestsToLogin: true
+
+authLocalUsers:
+ enabled: true
+ users:
+ - username: james
+ password: $argon2id$v=19$m=65536,t=4,p=6$LnNW4sw+jZfa5Ex3YjfuHQ$vl8pjUJhxNmBxScV4lI3cgAZPkNB1rSrnX6ibgoAP8k
+----
+
diff --git a/docs/modules/ROOT/pages/security/oauth2.adoc b/docs/modules/ROOT/pages/security/oauth2.adoc
new file mode 100644
index 0000000..b07c338
--- /dev/null
+++ b/docs/modules/ROOT/pages/security/oauth2.adoc
@@ -0,0 +1,106 @@
+[#oauth2]
+= OAuth2
+
+include::partial$earlydoc.adoc[]
+
+OliveTin supports OAuth2 for login with any OAuth2 compliant provider.
+
+At the moment, username fetching is only supported on GitHub. More will be added soon, probably with the addition of OpenID Connect support.
+
+```yaml
+authOAuth2RedirectUrl: http://localhost:1337/oauth/callback
+authOAuth2Providers:
+ github:
+ clientId: 1234567890
+ clientSecret: 1234567890
+```
+
+== Provider configuration
+
+* `name` - a "simple name" for the provider, used in the login redirect and internally in OliveTin, e.g. `github`
+* `title` - the human-readable name of the provider, e.g. `GitHub`
+* `clientId` - the client ID provided by the OAuth2 provider
+* `clientSecret` - the client secret provided by the OAuth2 provider
+* `icon` - the icon to use for the provider. Accepts any HTML, e.g. ``
+* `scopes` - a list of scopes to request.
+* `authUrl` - the URL to redirect to for authentication
+* `tokenUrl` - the URL to exchange the code for a token
+* `whoamiUrl` - the URL to fetch user information from
+* `usernameField` - the field in the user information response to use as the username
+* `userGroupField` - the field in the user information response to use as the group. This is a string containing one group name, e.g. `olivetin_group`
+* `addToUsergroup` - a group name to add to every user who logs in via this provider. If the user already has a usergroup (e.g. from `userGroupField`), this value is appended to it; otherwise it becomes the user's usergroup. Useful for giving all users from this provider a common group for ACLs, e.g. `addToUsergroup: github`
+* `certBundlePath` - the path to a certificate to add to the truststore for authentication requests, e.g. `/certs/internal.crt`
+* `insecureSkipVerify` - a boolean to disable certificate verfication
+* `connectTimeout` - an integer for seconds until the request will timeout, e.g. `10`
+
+== Built-in providers (`name`)
+
+OliveTin comes with a few built-in providers for convenience. If you are using one of these with a `name`, then you don't need to specify the various URLs, scopes, icon, usernameField, etc. It will be automatically configured for you. You will still need to provide the client ID and client secret.
+
+* `github` - GitHub
+* `google` - Google
+
+== AddToUsergroup examples
+
+The `addToUsergroup` option assigns a group to every user who signs in through that OAuth2 provider. You can use it alone, or together with `userGroupField`, so that all users from the provider share a common group for ACLs (e.g. "everyone from GitHub") while still keeping provider-specific groups when available.
+
+=== When the provider does not return a group
+
+Some providers (e.g. Google with standard Gmail accounts) do not return a group claim. Users logging in through them get no group, so they do not match any ACLs and end up with no permissions or visible actions. Use `addToUsergroup` to give every user from that provider a default group such as `guest`:
+
+```yaml
+authOAuth2Providers:
+ google:
+ clientId: your-client-id
+ clientSecret: your-client-secret
+ addToUsergroup: guest
+```
+
+Then ensure an ACL matches that group, for example `matchUsergroups: ["guest"]`, so those users get the intended access.
+
+=== All users from a provider in one group
+
+Give every user who logs in via GitHub the group `github` so you can target them in ACLs:
+
+```yaml
+authOAuth2Providers:
+ github:
+ clientId: your-client-id
+ clientSecret: your-client-secret
+ addToUsergroup: github
+```
+
+Then in your actions you can restrict access with `allowedUserGroups: ["github"]`.
+
+=== Combining with userGroupField
+
+If your provider returns a group (e.g. from GitHub org/team or an IdP), use both `userGroupField` and `addToUsergroup`. The provider group is used as the user's group, and `addToUsergroup` is appended so the user belongs to both:
+
+```yaml
+authOAuth2Providers:
+ github:
+ clientId: your-client-id
+ clientSecret: your-client-secret
+ userGroupField: olivetin_group
+ addToUsergroup: github
+```
+
+A user with `olivetin_group: admins` will end up in groups `admins` and `github`; a user with no group will get only `github`.
+
+=== Multiple providers, shared and per-provider groups
+
+Use different `addToUsergroup` values per provider so you can allow "all OAuth users" or "only GitHub" / "only Google":
+
+```yaml
+authOAuth2Providers:
+ github:
+ clientId: github-client-id
+ clientSecret: github-client-secret
+ addToUsergroup: github
+ google:
+ clientId: google-client-id
+ clientSecret: google-client-secret
+ addToUsergroup: google
+```
+
+Then use ACLs such as `allowedUserGroups: ["github"]` for GitHub-only actions or `allowedUserGroups: ["github", "google"]` for any OAuth user.
diff --git a/docs/modules/ROOT/pages/security/oauth2_authelia.adoc b/docs/modules/ROOT/pages/security/oauth2_authelia.adoc
new file mode 100644
index 0000000..2e39adb
--- /dev/null
+++ b/docs/modules/ROOT/pages/security/oauth2_authelia.adoc
@@ -0,0 +1,79 @@
+= OAuth2 - Authelia
+
+Notes contributed by a member of the OliveTin community - many thanks Phampyk!
+
+[source,yaml]
+.Authelia code
+----
+identity_providers:
+ oidc:
+ hmac_secret: "xxxxxx"
+
+ jwks:
+ - key_id: "primary"
+ algorithm: "RS256"
+ use: "sig"
+ key: |
+ -----BEGIN PRIVATE KEY-----
+ xxxxxxxxxxxxxxxxxxxxxxxxxx
+ -----END PRIVATE KEY-----
+
+ clients:
+ - client_id: "olivetin"
+ client_name: "OliveTin"
+ client_secret: "xxxxxxxxxxxxxxxxx"
+ redirect_uris:
+ - "https://olivetin.hostname.com/oauth/callback"
+ scopes:
+ - openid
+ - profile
+ consent_mode: implicit
+----
+
+* hmac_secret generated with `openssl rand -hex 64 or can be authelia crypto rand --length 64 --charset alphanumeric` link:https://www.authelia.com/reference/guides/generating-secure-values/#generating-a-random-alphanumeric-string[Source]
+* Private key generated with `openssl genrsa -out oidc.key 2048 and openssl rsa -in oidc.key -pubout -out oidc.pub` but only used the oidc.key here
+* client_id olivetin is for the example, as per authelia docs the recomendation is a random string generated with `authelia authelia crypto rand --length 72 --charset rfc3986` link:https://www.authelia.com/integration/openid-connect/frequently-asked-questions/#client-id--identifier[Source]
+* consent_mode I had to set this one up as implicit or every time I loged in it was an extra step where you had to authorize OliveTin to access profile and openid. link:https://www.authelia.com/configuration/identity-providers/openid-connect/clients/#consent_mode[Source]
+* client_secret is recommended in the docs to be generated with `authelia crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 --random.charset rfc3986` and it gives you the password (Random password on the example) and the hash (Digest on the example). Olivetin needs the password and Authelia the hash link:https://www.authelia.com/integration/openid-connect/frequently-asked-questions/#client-secret[Source]
+
+----
+Random Password: JxMbHrQgmykaVm2n0p_5q6P_YoZG_YdRWvHxHbVJ5Alv.Ni3OJPVPHEJ6Tfw_AklrwayFl39
+Digest: $pbkdf2-sha512$310000$yQogpMZvkHoAmOBGiIHVJQ$hxKuvar6Q6pOlkdzQBMWq1i5WjXcBA3rvuXxeylvLeTuKI/hLVeZsM43R5TWejZ6gBp/OH8yy1hWytiohLQh5w
+----
+
+== OliveTin config
+
+[source,yaml]
+----
+authRequireGuestsToLogin: true
+authOAuth2RedirectURL: https://olivetin.hostname.com/oauth/callback
+authOAuth2Providers:
+ authelia:
+ name: authelia
+ title: Authelia
+ clientID: olivetin #same as authelia
+ clientSecret: xxxxxxx #same as authelia but not hashed
+ authURL: https://authelia.hostname.com/api/oidc/authorization
+ tokenURL: https://authelia.hostname.com/api/oidc/token
+ whoamiUrl: https://authelia.hostname.com/api/oidc/userinfo
+ scopes:
+ - openid
+ - profile
+ usernameField: preferred_username
+ icon:
+
+accessControlLists:
+ - name: john #same as authelia
+ matchUserNames:
+ - john
+ permissions:
+ view: true
+ exec: true
+ logs: true
+ addToEveryAction: true
+----
+
+== Next steps
+
+Once you have OAuth2 working, you will probably want to configure access control lists in OliveTin. This is described in the xref:security/acl.adoc[Access Control Lists] documentation page.
+
diff --git a/docs/modules/ROOT/pages/security/oauth2_authentik.adoc b/docs/modules/ROOT/pages/security/oauth2_authentik.adoc
new file mode 100644
index 0000000..03459d5
--- /dev/null
+++ b/docs/modules/ROOT/pages/security/oauth2_authentik.adoc
@@ -0,0 +1,203 @@
+[#oauth2-authentik]
+= OAuth2 - Authentik
+
+OliveTin has been tested with Authentik. This documentation page describes how to configure Authentik for use with OliveTin and assumes you already have Authentik installed and running.
+
+Login as an Authentik administrator and start by creating a new app as follows;
+
+image::authentik_new_app.png[]
+
+Click Next, and on the **Provider Type** page select **OAuth2**.
+
+image::authentik_select_oauth2.png[]
+
+Click Next, and on the **Provider Configuration** page, fill in the following fields;
+
+* **Authorization flow:** `default-authorization-eplicit-consent (Authorize Application)` (or similar)
+* **Client Type**: `confidential` - OliveTin requires a confidential client (it keeps it's secrets on the server side).
+
+image::authentik_provider_config.png[]
+
+Scroll down, and on the same page, copy the **Client ID** and **Client Secret** fields into a text file, a secret manager, or somewhere else safe. You will need these values later. These are used in the OliveTin configuration file later.
+
+For the **Redirect URIs**, OliveTin requires that the URI ends with `/oauth/callback`. Therefore if your OliveTin instance is running on `http://example.com:1337`, you should add the following redirect URI: `http://example.com:1337/oauth/callback`. Note that the URL must match the URL that you use to access OliveTin - so it is whatever you type in your browser address bar, with `/oauth/callback` appended to it.
+
+[NOTE]
+That URL says `oauth`, not `oauth2`. OliveTin only supports OAuth2, not "OAuth[1]", but the path is `oauth` nevertheless.
+
+image::authentik_provider_secrets.png[]
+
+If your Authentik instance has a "Configure Bindings" page, you can bind users to be able to access OliveTin like you would with any other application that you add to Authentik.
+
+Submit this wizard to save the configuration.
+
+== Group Mapping
+
+OliveTin `2024.11.24` added support for OAuth2 group mapping for a single group. OliveTin `2025.7.29` added support for OAuth2 group mapping for multiple groups when passed as a commaa-separated list.
+
+The examples below show various ways to map groups from Authentik to OliveTin.
+
+=== Multiple group mapping: Comma-separated list
+
+The below will match all groups the user is a member of and return them as a comma-separated list. If no groups are found then an empty string is returned (no groups)".
+
+In Authentik: `Admin Interface > Customization > Property Mappings > Create > Scope Mapping`
+
+* **Name**: `olivetin-group-mapping-multiple` (or similar)
+* **Scope Name**: `olivetin-group-mapping-multiple` (or similar)
+* **Description**: `map all groups to a comma-separated list for olivetin`
+* **Expression**:
+
+[source,python]
+.Multiple group mapping: Comma-separated list
+----
+groups = [group.name for group in user.ak_groups.all()]
+
+return {
+ "olivetin_group_list": ",".join(groups)
+}
+----
+
+
+=== Single group mapping: First Prefix Match
+
+The below will match the first group the user is a member of that matches the prefix defined in `group_prefix`, which is set to `olivetin`. If no match is found, the group `guest` is returned by default. Both `group_prefix` and `returned_group` can be changed to your needs.
+
+In Authentik: `Admin Interface > Customization > Property Mappings > Create > Scope Mapping`
+
+* **Name**: `olivetin-group-mapping`
+* **Scope Name**: `olivetin-group-mapping`
+* **Description**: `map first group that starts with "olivetin"`
+* **Expression**:
+
+[source,python]
+.Single group mapping: First Prefix Match
+----
+group_prefix = "olivetin"
+returned_group = "guest"
+
+groups = [group.name for group in user.ak_groups.all()]
+
+for group in groups:
+ if group.startswith(group_prefix):
+ returned_group = group
+ break
+
+return {
+ "olivetin_group_first": returned_group
+}
+----
+
+[IMPORTANT]
+If you use this multiple group mapping, you will need to set the `AuthHttpHeaderUserGroupSep` field to `,`. This may sound like a strangely named field, but it is the correct one to use for this mapping. It was originally created for the HTTP Trusted Header authentication method, but it is also used for OAuth2 group mapping.
+
+=== Single group mapping: Specific Group Match
+
+The below will match the specified group name to one of the groups the user is a member of. If no match is found, the group `guest` is returned by default. Both `olivetin_group` and `returned_group` can be changed to your needs.
+
+In Authentik: `Admin Interface > Customization > Property Mappings > Create > Scope Mapping`
+
+* **Name**: `olivetin-group-mapping-specific`
+* **Scope Name**: `olivetin-group-mapping-specific`
+* **Description**: `search and map specified group for olivetin`
+* **Expression**:
+
+[source,python]
+.Single group mapping: Specific Group Match
+----
+olivetin_group = "olivetin-users"
+returned_group = "guest"
+
+groups = [group.name for group in user.ak_groups.all()]
+
+if olivetin_group in groups:
+ returned_group = olivetin_group
+
+return {
+ "olivetin_group_specific": returned_group
+}
+----
+
+=== Enable Group Mapping
+
+After creating the scope mapping in Authentik, you will need to add it to your provider. For the your OliveTin config, use the `userGroupField` mentioned in the following section.
+
+In Authentik: `Admin Interface > Applications > Providers > {Your Provider} > Edit`
+
+* Open `Advanced protocol settings`
+* Under `Scopes`, add `your_scope_map` to `Selected Scopes`
+* Click `Update`
+
+== OliveTin configuration
+
+This section assumes that your authentik server is accessible in the browser at `http://localhost:9000` and that OliveTin is running on `http://localhost:1337`. Adjust the URLs as necessary to match your setup. The "path" part of the URL is important and should be common in all Authentik installations.
+
+The necessary OliveTin configuration is as follows:
+
+[source,yaml]
+----
+authRequireGuestsToLogin: true # Optional - depends if you want to "disable" guests.
+
+authOAuth2RedirectURL: "http://localhost:1337/oauth/callback"
+authOAuth2Providers:
+ authentik:
+ name: authentik
+ title: Authentik
+ clientID: "1234567890"
+ clientSecret: "123456789012345"
+ authURL: "http://localhost:9000/application/o/authorize/"
+ tokenURL: "http://localhost:9000/application/o/token/"
+ whoamiURL: "http://localhost:9000/application/o/userinfo/"
+ usernameField: "preferred_username"
+ icon:
+----
+
+Optional configuration values to consider are:
+[source,yaml]
+----
+authHttpHeaderUserGroupSep: "," # Optional - only needed if you use the multiple group mapping
+
+authOAuth2Providers:
+ authentik:
+ userGroupField: "olivetin_group_list" # or "olivetin_group_first" or "olivetin_group_specific" depending on which mapping you used
+ certBundlePath: "/path/to/mounted/certificate.pem"
+ insecureSkipVerify: true
+ connectTimeout: 15
+----
+
+You will need to restart OliveTin for the changes to take effect.
+
+== Testing
+
+You should now be able to login to OliveTin using Authentik, on the OliveTin page, a "Login" link should be available in the top right corner. This will take you to the login form, where you can select the Authentik provider.
+
+image::authentik_login.png[]
+
+When clicking on the Authentik login button, you will be redirected to the Authentik login page which should look something like this;
+
+image::authentik_login2.png[]
+
+Assuming that you have given permission to OliveTin to access your account, you should be redirected back to OliveTin and logged in. You can verify that you are logged in by checking the top right corner of the OliveTin page, where your username should be displayed.
+
+image::authentik_login3.png[]
+
+== Debugging
+
+OliveTin logs OAuth2 flows quite extensively. If you are having trouble with OAuth2, you should check your OliveTin logs.
+
+You may see errors such as "OAuth2: Error getting user data" or "Failed to get field from user data".
+
+Sometimes it can be infuriating to debug the user data mapping (username and usergroup), as you cannot easily capture the data that is being sent back from Authentik. To help with this, you can temporarily enable a debug log flag that is INSECURE (do not leave this enabled) to log the user data that is being sent back from Authentik. To do this, add the following to your OliveTin configuration file:
+
+[source,yaml]
+----
+logLevel: debug
+insecureAllowDumpOAuth2UserData: true
+----
+
+Once you have this working, you can disable the `insecureAllowDumpOAuth2UserData` flag again. This is only meant for debugging purposes and should not be left enabled in production environments.
+
+== Next steps
+
+Once you have OAuth2 working, you will probably want to configure access control lists in OliveTin. This is described in the xref:security/acl.adoc[Access Control Lists] documentation page.
+
diff --git a/docs/modules/ROOT/pages/security/oauth2_pocketid.adoc b/docs/modules/ROOT/pages/security/oauth2_pocketid.adoc
new file mode 100644
index 0000000..26418d1
--- /dev/null
+++ b/docs/modules/ROOT/pages/security/oauth2_pocketid.adoc
@@ -0,0 +1,55 @@
+[#oauth2-pocketid]
+= OAuth2 - Pocket ID
+
+OliveTin has been tested with Pocket ID. This documentation page describes how to configure Pocket ID for use with OliveTin and assumes you already have Pocket ID installed and running.
+
+== Configuration
+
+.config.yaml
+[source,yaml]
+----
+authRequireGuestsToLogin: true
+
+accessControlLists:
+ - name: admin
+ permissions:
+ view: true
+ exec: true
+ logs: true
+ matchUsergroups:
+ # Since you can't map properties in userinfo response from Pocket ID I am kind of cheating here:
+ # only I will be able to log in and so I return the "preferred_username" as the group, and I configure my
+ # own username as the "Usergroup" to mean "admin"
+ - myusername
+ addToEveryAction: true
+
+authLocalUsers:
+ enabled: false
+
+authOAuth2RedirectUrl: https://olivetin.example.com/oauth/callback
+
+authOAuth2Providers:
+ pocket-id:
+ name: pocket-id
+ title: Pocket ID
+ icon: '
'
+ authUrl: https://id.example.com/authorize
+ tokenUrl: https://id.example.com/api/oidc/token
+ whoamiUrl: https://id.example.com/api/oidc/userinfo
+ clientId: "[REDACTED]"
+ clientSecret: "[REDACTED]"
+ scopes:
+ - profile
+ - email
+ usernameField: preferred_username
+ userGroupField: preferred_username
+ insecureSkipVerify: true
+
+actions:
+ - title: "Hello world!"
+ shell: echo 'Hello World!'
+----
+
+== Pocket ID config
+
+image::pocketid.png[]
\ No newline at end of file
diff --git a/docs/modules/ROOT/pages/security/trusted_header.adoc b/docs/modules/ROOT/pages/security/trusted_header.adoc
new file mode 100644
index 0000000..a395238
--- /dev/null
+++ b/docs/modules/ROOT/pages/security/trusted_header.adoc
@@ -0,0 +1,34 @@
+[#trusted-header]
+= Trusted Header Authorization
+
+Trusted Header Authorization is useful if you have a proxy that handles authentication, and you just want OliveTin to trust HTTP Servers sent via that proxy.
+
+This comes with the obvious security caveat that anyone who can set HTTP headers on requests to OliveTin can impersonate any user or usergroup. Therefore, you should only use this method if you are sure that requests to OliveTin are **only coming from trusted proxies**.
+
+== Configuring your reverse proxy
+
+You will need to configure your reverse proxy to set a header for the Username (eg `X-Username`) and optionally a header for Usergroup (eg `X-Usergroup`). How you do this will depend on your reverse proxy software. It is better that you check out the documentation for your reverse proxy software for how to set HTTP headers.
+
+== Configuring OliveTin
+
+To configure Trusted Header Authorization, set the following configuration options in your `config.yaml` file:
+
+[source,yaml]
+.`config.yaml`
+----
+authHttpHeaderUsername: "X-Username"
+authHttpHeaderUsergroup: "X-Usergroup"
+----
+
+The value of `X-Username` and `X-Usergroup` can be whatever you like, as long as they match the headers set by your reverse proxy.
+
+NOTE: You *must* set `AuthHttpHeaderUsername` to some value, even if you only intend to use `AuthHttpHeaderUsergroup`, otherwise usergroups will be ignored.
+
+== Multiple usergroups
+
+OliveTin will automatically detect multiple usergroups in the `authHttpHeaderUsergroup` header if they are separated by a space. You can also set a configuration option to use a different separator string with `authHttpHeaderUsergroupSep`. For example, if you set `authHttpHeaderUsergroupSep` to `,`, then the header `X-Usergroup: group1,group2` will be interpreted as two usergroups: `group1` and `group2`.
+
+[source, yaml]
+----
+authHttpHeaderUsergroupSep: ","
+----
diff --git a/docs/modules/ROOT/pages/solutions/cloudflare_access_tunnel/index.adoc b/docs/modules/ROOT/pages/solutions/cloudflare_access_tunnel/index.adoc
new file mode 100644
index 0000000..8499835
--- /dev/null
+++ b/docs/modules/ROOT/pages/solutions/cloudflare_access_tunnel/index.adoc
@@ -0,0 +1,66 @@
+[#cloudflare-access-tunnels]
+= Cloudflare Access & Tunnels
+
+include::partial$earlydoc.adoc[]
+
+Several uses use Cloudflare Access & Tunnels to grant access to OliveTin. There is no special configuration needed for OliveTin to work in this way, simply setup your Cloudflare tunnel to connect to OliveTin on port 1337.
+
+== Trusting the Cloudflare JWT Token
+
+. Get your **AUD** Tag (`authJwtAud`)
+.. Login to your CloudFlare dashboard and go to link:https://one.dash.cloudflare.com/[**Zero Trust**]
+.. Go to **Access > Applications.**
+.. Select **Configure** for your application.
+.. On the Overview tab, copy the **Application Audience (AUD) Tag**.
+. Get your **Team Domain** (`authJwtDomain`)
+.. Login to your CloudFlare dashboard and go to link:https://one.dash.cloudflare.com/[**Zero Trust**]
+.. Go to **Settings**
+.. Go to **Custom Pages**
+.. Your **Team Domain** is shown here
+. Get your Certs URL (`authJwtCertsURL`)
+.. Simply add `cdn-cgi/access/certs` to your **Team Domain** for CloudFlare
+. CloudFlare gives you an `email` in the claim (`authJwtClaimUsername`) and the Cookie is always called `CF_Authorization` (`authJwtCookieName`)
+. Setup your OliveTin config.yaml like follows;
+
+[source,yaml]
+.`config.yaml`
+----
+authJwtAud: "asdf1234"
+authJwtDomain: "https://mydomain.cloudflareaccess.com"
+authJwtCertsURL: "https://mydomain.cloudflareaccess.com/cdn-cgi/access/certs"
+authJwtClaimUsername: email
+authJwtCookieName: "CF_Authorization"
+----
+
+You may well want to set `logLevel: DEBUG` and `insecureAllowDumpJwtClaims: true` in your config when testing JWT for the first time.
+
+== Trusting the authentication header (not recommended)
+
+If you are using Cloudflare Access, and want to use the username given by Cloudflare in OliveTin ACLs, then you can use the Cloudflare cookie like this;
+
+[source,yaml]
+.`config.yaml`
+----
+authHttpHeaderUsername: "Cf-Access-Authenticated-User-Email"
+
+defaultPermissions:
+ view: false
+ exec: false
+
+accessControlLists:
+ - name: Admins
+ addToEveryAction: true
+ matchUsernames:
+ - contact@jread.com
+ permissions:
+ view: true
+ exec: true
+
+actions:
+ - title: test apprise
+ shell: date
+ shellAfterCompleted: "apprise -c /config/apprise.yml -t 'notification: test' -b 'date is {{ stdout }}'"
+----
+
+NOTE: OliveTin does support JWT cookies that Cloudflare uses, which is arguably more secure. It's just that nobody in the Discord has worked out how to get the keys needed from Cloudflare to decrypt this cookie yet! See the xref::security/jwt.adoc[JWT] documentation for some starter points. If you figure this out, it would be most welcome to share your solution with the community.
+
diff --git a/docs/modules/ROOT/pages/solutions/container-control-panel/index.adoc b/docs/modules/ROOT/pages/solutions/container-control-panel/index.adoc
new file mode 100644
index 0000000..1f0140d
--- /dev/null
+++ b/docs/modules/ROOT/pages/solutions/container-control-panel/index.adoc
@@ -0,0 +1,32 @@
+[#container-control-panel]
+= Container Control Panel
+
+OliveTin is frequently used to create simple container control panels, this is one of the default examples that ships with the standard OliveTin config.yaml.
+
+image::solutions/container-control-panel/preview.png[]
+
+include::partial$container_socket.adoc[]
+
+== Entity file
+
+To build this Container Control dashboard, we use an xref::entities/intro.adoc[entity file] that stores and updates produced by `docker ps --format json > /etc/OliveTin/entities/containers.json`
+
+[source,yaml]
+.`/etc/OliveTin/entities/containers.json`
+----
+include::example$solutions/container-control-panel/config/containers.json[]
+----
+
+You can generate this file yourself the first time, but the `config.yaml` below shows how OliveTin can run the `docker ps` command on startup, and on a schedule to update the file.
+
+== Configuration
+
+Then use the following configuration file;
+
+[source,yaml]
+.`config.yaml`
+----
+include::example$solutions/container-control-panel/config/config.yaml[]
+----
+
+
diff --git a/docs/modules/ROOT/pages/solutions/directory-actions/index.adoc b/docs/modules/ROOT/pages/solutions/directory-actions/index.adoc
new file mode 100644
index 0000000..2568c73
--- /dev/null
+++ b/docs/modules/ROOT/pages/solutions/directory-actions/index.adoc
@@ -0,0 +1,17 @@
+[#directory-actions]
+= Directory Actions
+
+Sometimes people want to use OliveTin to run standard commands on a directory, such a cleaning out a directory of logs.
+
+image::directory-actions-screenshot.png[]
+
+== Config file
+
+This is a quick and simple way to build actions based on directories.
+
+[source,yaml]
+.`/etc/OliveTin/config.yaml`
+----
+include::example$solutions/directory-actions/config.yaml[]
+----
+
diff --git a/docs/modules/ROOT/pages/solutions/heating-control-panel/index.adoc b/docs/modules/ROOT/pages/solutions/heating-control-panel/index.adoc
new file mode 100644
index 0000000..fcd116c
--- /dev/null
+++ b/docs/modules/ROOT/pages/solutions/heating-control-panel/index.adoc
@@ -0,0 +1,28 @@
+[#heating-control-panel]
+= Heating Control Panel
+
+This was inspired by a GitHub issue to control heating; https://github.com/OliveTin/OliveTin/issues/73
+
+image::dashboard-heating-control-panel.png[]
+
+== Entity file
+
+To build this, we use an xref::entities/intro.adoc[entity file] that stores and updates the status of a heater outside of OliveTin.
+
+[source,yaml]
+.`/etc/OliveTin/entities/heating.yaml`
+----
+include::example$solutions/heating-control-panel/configs/heating.yaml[]
+----
+
+== Configuration
+
+Then use the following configuration file;
+
+[source,yaml]
+.`config.yaml`
+----
+include::example$solutions/heating-control-panel/configs/config.yaml[]
+----
+
+
diff --git a/docs/modules/ROOT/pages/solutions/intro.adoc b/docs/modules/ROOT/pages/solutions/intro.adoc
new file mode 100644
index 0000000..53741ae
--- /dev/null
+++ b/docs/modules/ROOT/pages/solutions/intro.adoc
@@ -0,0 +1,7 @@
+[#solutions]
+= Solutions
+
+OliveTin was designed to be very simple, but sometimes OliveTin can get complex - especially if you are trying to do something where you need to jump between lots of different parts of the documentation!
+
+This section of the docs is designed to bring everything into one place and present a scenario, with a single-page solution.
+
diff --git a/docs/modules/ROOT/pages/solutions/k8s-control-panel-hosted/index.adoc b/docs/modules/ROOT/pages/solutions/k8s-control-panel-hosted/index.adoc
new file mode 100644
index 0000000..09403fe
--- /dev/null
+++ b/docs/modules/ROOT/pages/solutions/k8s-control-panel-hosted/index.adoc
@@ -0,0 +1,117 @@
+= Solution: Kubernetes Control Panel (Hosted)
+
+This solution gives you quick and easy buttons to run kubectl commands, when OliveTin is running on top of Kubernetes (this means OliveTin is "hosted" by Kubernetes). This can be very easy for quick debugging purposes when you cannot type (eg from a mobile phone), or to give junior sysadmins access to do basic predefined tasks.
+
+This use case is enabled by simply providing the OliveTin pod access to talk to the kubernetes API, and using `kubectl` which is preinstalled with modern versions of OliveTin.
+
+image::solution-k8s-hosted.png[]
+
+== Requirements & Assumptions
+
+=== Time & skills
+
+* This should take approximately **10 minutes** to configure if you are comfortable in using Kubernetes - using helm, kubectl, and editing basic YAML.
+
+=== Environment
+
+* A Kubernetes cluster that is up and running.
+* Kubernetes permissions to create a helm deployment, a `ClusterRole` and `ClusterRoleBinding`.
+* A configured Ingress Controller, exposing the for web interface
+
+=== System
+
+* Approximately 128m RAM, 1vCPU to run the OliveTin pod.
+
+== Install OliveTin on top of Kubernetes
+
+* xref:install/helm.adoc[Install OliveTin on Kubernetes with Helm] (recommended)
+* xref:install/k8s.adoc[Install OliveTin on Kubernetes with Manifests]
+
+== Grant permissions to API
+
+OliveTin needs a `ClusterRole` that allow it to access resources on your Kubernetes cluster. This is because by default, pods can communicate to the API using the credentials mounted in the pod by the default `ServiceAccount`, but they don't have any permissions. This `ClusterRole` is being created to give permissions to the `ServiceAccount`. Create the `ClusterRole` like follows;
+
+[tabs]
+====
+kubectl cli::
++
+--
+[source,shell]
+----
+user@host: kubectl create clusterrole --resource=pods --verb=get,list,watch olivetin-k8s-permissions
+----
+--
+manifest::
++
+--
+[source,yaml]
+----
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ name: olivetin-k8s-permissions
+rules:
+- apiGroups: [""]
+ resources: ["pods"]
+ verbs: ["get", "list", "watch"]
+----
+--
+====
+
+Now that the `ClusterRole` has been created, we need to associate it to a `ServiceAccount` with a `ClusterRoleBinding`.
+Create a cluster role binding;
+
+[tabs]
+====
+kubectl cli::
++
+--
+[source,shell]
+----
+user@host: kubectl create clusterrolebinding --clusterrole=olivetin-k8s-permissions --serviceaccount myolivetinnamespace:default --namespace myolivetinnamespace olivetin-crb
+----
+--
+====
+
+== Build a simple kubernetes control planel
+
+Add `kubectl` job to OliveTin config with `kubectl edit cm/olivetin-config -n olivetin`;
+
+[source,yaml]
+----
+apiVersion: v1
+data:
+ config.yaml: |
+ defaultPopupOnStart: execution-dialog-output-only
+
+ actions:
+ - title: get pods
+ icon:
+ shell: kubectl get pods
+
+ - title: restart postgres deployment
+ icon:
+ shell: kubectl rollout restart deployment postgres
+
+ - title: evacuate node
+ icon:
+ shell: kubectl drain {{ NodeName }} --ignore-daemonsets --delete-emptydir-data
+ arguments:
+ - name: NodeName
+ choices:
+ - value: node1
+ - value: node2
+ - value: node3
+kind: ConfigMap
+metadata:
+ annotations:
+ meta.helm.sh/release-name: olivetin
+ meta.helm.sh/release-namespace: default
+ labels:
+ app.kubernetes.io/managed-by: Helm
+ name: olivetin-config
+ namespace: default
+----
+
+Don't forget to restart the OliveTin deployment as good measure, because Kubernetes can be slow to update configmaps.
+
diff --git a/docs/modules/ROOT/pages/solutions/on-git-push/index.adoc b/docs/modules/ROOT/pages/solutions/on-git-push/index.adoc
new file mode 100644
index 0000000..361865b
--- /dev/null
+++ b/docs/modules/ROOT/pages/solutions/on-git-push/index.adoc
@@ -0,0 +1,115 @@
+[#solution-on-git-push]
+= GitOps (run actions on Git Push)
+
+image::gitops.png[]
+
+A really helpful thing to do with OliveTin is to have it run actions when you push to a Git repository. This is a great way to automate things like running tests, building your project, or deploying your code - this turns OliveTin into a powerful GitOps tool, or even a Continuous Integration tool.
+
+This guide assumes that you are using a self-hosted Git repository, and uses a standard Git `post-receive` hook to trigger OliveTin actions.
+
+[TIP]
+====
+**Using GitHub?** OliveTin has built-in support for GitHub webhooks with templates that make configuration simple. See xref:action_execution/onwebhook_github.adoc[GitHub Webhooks] for an easier approach that doesn't require writing hook scripts.
+====
+
+To set up OliveTin to run actions on Git push, you will need to:
+
+1. Create a new OliveTin action that you want to run on push.
+2. Set up a Git `post-receive` hook to trigger the OliveTin action.
+
+== Create a New OliveTin Action
+
+First, you will need to create a new OliveTin action that you want to run when you push to your Git repository. This could be anything you like - for example, running tests, building your project, or deploying your code. The example below is a simple action that echoes a message to the console:
+
+[source,yaml]
+.OliveTin `config.yaml`
+----
+actions:
+ - title: Run on Git Push
+ id: gitops
+ icon:
+ shell: |
+ echo "You just pushed commit $COMMIT to git, running action..."
+ date
+ arguments:
+ - name: commit
+ type: ascii
+----
+
+Note that OliveTin will expose all arguments as environment variables in uppercase as shown in the example above. You can of course use the `{{ commit }}` syntax instead and it will do the same thing.
+
+== Add a Git hook script
+
+The following below assumes that you have a Git repository initialized as a bare repository, at `/opt/myrepo.git`. If you have a different repository location, you will need to adjust the paths accordingly.
+
+First, create a new file at `/opt/myrepo.git/hooks/post-receive` with the following contents:
+
+[source,bash]
+.Script: `myrepo.git/hooks/post-receive`
+----
+#!/bin/bash
+
+read OLDREV NEWREV REFNAME
+
+CHANGED_FILES=$(git diff --name-only $OLDREV $NEWREV)
+
+commit_contains_path() {
+ local filename=$1
+
+ if echo "$CHANGED_FILES" | grep -q "$filename"; then
+ return 0 # True
+ else
+ return 1 # False
+ fi
+}
+
+function run_olivetin_action() {
+ local ACTION_NAME=$1
+
+ echo "Requesting OliveTin job $ACTION_NAME"
+
+ OLIVETIN_REQUEST="$(cat <`. Just before that code statement, add this code, save and close the file.
++
+[source,html]
+----
+
+----
+You will need to this every time you upgrade OliveTin.
++
+. Create the `password.js` using the code below, at `/etc/OliveTin/custom-webui/password.js`.
+
+[source,javascript]
+.`password.js`
+----
+include::example$solutions/primitive-password/password.js[]
+----
diff --git a/docs/modules/ROOT/pages/solutions/systemd-control-panel/index.adoc b/docs/modules/ROOT/pages/solutions/systemd-control-panel/index.adoc
new file mode 100644
index 0000000..3c89135
--- /dev/null
+++ b/docs/modules/ROOT/pages/solutions/systemd-control-panel/index.adoc
@@ -0,0 +1,44 @@
+[#systemd-control-panel]
+= Systemd Control Panel
+
+OliveTin can be used to manage selected systemd units (services) as well. This is powered by OliveTin's powerful xref::entities/intro.adoc[entity support]. Here is a screenshot of what that can look like (dark theme preference enabled).
+
+image::solution-systemd-control-panel.png[]
+
+NOTE: To control systemd, you will need the root user. If you are running OliveTin systemd service itself, then OliveTin should be configured to run as root. The alternative is to use OliveTin in a container, and use xref:action_examples/ssh-easy.adoc[SSH] to connect back to the host (as a user that can manage systemd -usually root). This is not an OliveTin limitation, it's just how systemd security works.
+
+== Entity file
+
+To build this Systemd Control dashboard, we use an xref:entities/intro.adoc[entity file] that stores and updates produced by `systemctl list-units..`, output to a json format, and then filtered with jq (JSON Query). Here is the full command that we will use in our config later;
+
+----
+user@host: systemctl list-units -a -o json --no-pager | jq -c 'map(select (.unit | contains ("upsilon", "podman", "boot.mount"))) | .[]' > /etc/OliveTin/entities/systemd_units.json
+----
+
+That command will generate a file (example shown below). Let's break down that long command a little bit to explain what it is doing;
+
+. `systemctl list-units -a -o json --no-pager` - will list units, regardless of status - started, stopped, etc (`-a`), in JSON format, and output the results
+. `jq` (JSON Query) will select units from that output that match "upsilon", "podman", "boot.mount", and of course you can change this expression to add your own services.
+. The `map()` and `.[]` parts of the expression basically just put those units line by line into the file
+
+[source,json]
+.An example generated `/etc/OliveTin/entities/systemd_units.json` file
+----
+include::example$solutions/systemd-control-panel/config/systemd_units.json[]
+----
+
+You can generate this file yourself the first time, but the `config.yaml` below shows how OliveTin can run the `systemctl list-units ...` command on startup, and on a schedule to update the file.
+
+Note that if the file does not exist the first time OliveTin starts up, then OliveTin will will issue an error about not finding the file to monitor it. An easy way around this is to simply restart OliveTin a 2nd time, so that on the 2nd startup it will find the file (because it will be created the first time OliveTin starts up).
+
+== Configuration
+
+Finally, here is the example configuration file to build the dashboard;
+
+[source,yaml]
+.`config.yaml`
+----
+include::example$solutions/systemd-control-panel/config/config.yaml[]
+----
+
+
diff --git a/docs/modules/ROOT/pages/solutions/wol/index.adoc b/docs/modules/ROOT/pages/solutions/wol/index.adoc
new file mode 100644
index 0000000..49582bb
--- /dev/null
+++ b/docs/modules/ROOT/pages/solutions/wol/index.adoc
@@ -0,0 +1,54 @@
+[#wol]
+= Wake On LAN from a container
+
+This is a simple solution that provides wake on lan capabilities from inside a container. It uses the simple `ether-wake` command to send the magic packet. This can be incredibly helpful if you just need a simple button to click to wake up a machine on your network.
+
+Docker containers will normally use a docker network, which is a separate network from the host network. This means that the container will not be able to send the magic packet to the host network. Therefore, we need to create the container with the `--network host` option, which will allow the container to send the magic packet to the host network (not the docker network).
+
+
+The container is also created with the `--user root` option, which allows the container to send the magic packet as root. This is necessary because the `ether-wake` command requires root privileges to send the magic packet, and also to install the `ether-wake` command, which is bundled with `net-tools` in the container.
+
+Create the container as follows;
+
+```bash
+user@host: docker create -u root --network=host --name olivetin_wol -v /etc/OliveTin/:/config ghcr.io/olivetin/olivetin:latest
+```
+
+Create your OliveTin `config.yaml` file in the `/etc/OliveTin/` directory on the host, with the following content;
+
+[source,yaml]
+----
+include::example$solutions/wol/config.yaml[]
+----
+
+Obviously adjust the config file with your own MAC addressses, creating new actions to send WOL commands as needed.
+
+Then start the container with the following command;
+
+```
+docker start olivetin_wol
+```
+
+Then visit your OliveTin web interface at http://yourServer:1337 and you should see something that looks like this;
+
+image::solutions/wol/preview.png[]
+
+== Wake on LAN via docker container
+
+This is an alternative solution that provides WoL capabilities to the OliveTin container,
+
+but has the advantage the OliveTin container does not have to run on the host network ( `--network host` ).
+This may be useful if your networking configuration relies on docker `bridge`
+networks (or other more complex networking configurations).
+
+It requires that OliveTin is configured with permissions that allow it to control docker.
+
+include::partial$container_socket.adoc[]
+
+Now you can add actions to your OliveTin config file that use a separate
+docker container (on the `host` network) to send the WOL commands.
+
+[source,yaml]
+----
+include::example$solutions/wol/config_docker.yaml[]
+----
diff --git a/docs/modules/ROOT/pages/style.css b/docs/modules/ROOT/pages/style.css
new file mode 100644
index 0000000..4b0bf11
--- /dev/null
+++ b/docs/modules/ROOT/pages/style.css
@@ -0,0 +1,212 @@
+body {
+ font-family: sans-serif;
+ line-height: 1.5;
+ padding: 0;
+ margin: 0;
+ display: grid;
+ grid-template-areas: "nav content" "nav footer";
+ grid-template-columns: 20em auto;
+ grid-template-rows: auto min-content;
+ height: 100vh;
+ align-items: stretch;
+}
+
+a {
+ text-decoration: none;
+ color: #36f;
+ font-weight: bold;
+}
+
+a:focus, #toc a:focus {
+ background-color: black;
+ color: white;
+}
+
+a:visited {
+ color: #36f;
+ font-weight: bold;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+#header {
+ background-color: #fafafa;
+ grid-area: nav;
+ box-shadow: 0 0 10px #cecece;
+}
+
+#footer {
+ grid-area: footer;
+ justify-self: center;
+ align-self: end;
+ padding: 1em;
+}
+
+#toc {
+ padding: 1em;
+}
+
+#toc a {
+ color: black;
+ font-weight: normal;
+}
+
+#toc li {
+ padding-bottom: .6em;
+}
+
+#toc ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+#toc ul ul {
+ padding-left: 1.4em;
+}
+
+#toctitle {
+ font-weight: bold;
+ color: black;
+}
+
+#content ul {
+ list-style: '\25b9 ';
+}
+
+h1 {
+ font-size: 1.4em;
+ font-weight: lighter;
+ background-color: black;
+ padding: .6em;
+ margin: 0;
+ color: white;
+}
+
+#toc a:hover {
+ text-decoration: underline;
+}
+
+#content {
+ background-color: white;
+ grid-area: content;
+ max-width: 1000px;
+ padding: 1em;
+ justify-self: center;
+}
+
+#content img {
+ max-width: 100%;
+ vertical-align: middle;
+}
+
+code {
+ background-color: #f7f7f8;
+}
+
+pre {
+ background-color: #f7f7f8;
+ padding: 1em;
+ word-wrap: break-word;
+ white-space: no-wrap;
+ overflow: auto;
+}
+
+table {
+ border-collapse: collapse;
+}
+
+td, th {
+ border: 1px solid gray;
+ padding: .4em;
+}
+
+th {
+ background-color: #f9f9f9;
+ text-align: left;
+}
+
+.imageblock {
+ float: center;
+}
+
+.imageblock.right {
+ float: right;
+}
+
+.imageblock .title {
+ margin-bottom: 2em;
+}
+
+.imageblock img {
+ border-radius: 1em;
+ box-shadow: 0 0 10px #cecece;
+}
+
+.imageblock.right img {
+ box-shadow: none;
+}
+
+
+.listingblock {
+ box-shadow: 0 0 10px #cecece;
+ border-radius: 1em;
+}
+
+.listingblock .title {
+ border-radius: 1em 1em 0 0;
+}
+
+.listingblock .title ~ .content, .listingblock .title ~ .content pre {
+ border-radius: 0 0 1em 1em;
+}
+
+.listingblock .content, .listingblock .content pre {
+ border-radius: 1em;
+}
+
+hr {
+ clear: both;
+ border: 0;
+ border-bottom: 1px solid #efefef;
+ margin: 1em;
+}
+
+div.admonitionblock.note {
+ background-color: beige;
+}
+
+div.admonitionblock td {
+ border: 0;
+}
+
+div.admonitionblock td:first-child {
+ font-weight: bold;
+}
+
+.listingblock .title {
+ background-color: #efefef;
+ margin-bottom: -1em;
+ padding: 1em;
+}
+
+.red {
+ color: white;
+ border: 1px solid black;
+ padding: .2em;
+ font-weight: bold;
+ background-color: #cc0000;
+}
+
+.red a {
+ color: white;
+}
+
+@media (max-width: 700px) {
+ body {
+ display: block;
+ }
+}
+
diff --git a/docs/modules/ROOT/pages/troubleshooting/advanced.adoc b/docs/modules/ROOT/pages/troubleshooting/advanced.adoc
new file mode 100644
index 0000000..149e5b7
--- /dev/null
+++ b/docs/modules/ROOT/pages/troubleshooting/advanced.adoc
@@ -0,0 +1,25 @@
+[#advanced-troubleshooting]
+= Advanced Troubleshooting
+
+Sometimes you need to really see what OliveTin is doing, especially when debugging entities. OliveTin has several built-in options for advanced troubleshooting, but enabling these output options can expose sensitive information, so they can be insecure.
+
+NOTE: OliveTin itself is not "insecure" by using these options, they would not let attackers execute different commands or anything like that. It's just that using these options can expose data (like entity files) that maybe you don't want an attacker to see.
+
+All these configuration options are `false` by default, and should be deleted from the config or reset back to `false` when you are not using them.
+
+[#dump-server-diagnostics]
+== Dump server diagnostics
+`InsecureAllowDumpSos: true` - will allow dumping xref:troubleshooting/server-diagnostics.adoc[server diagnostics] as plain text when visiting `http://server:1337/api/sosreport`
+
+[#dump-action-map]
+== Dump Action Map
+`InsecureAllowDumpActionMap: true` - will allow dumping all the actions (and those generated with entities) and their public IDs, eg: `http://server:1337/api/DumpActionMap`
+
+[#dump-vars]
+== Dump Vars
+`InsecureAllowDumpVars: true` - will allow dumping all the "string variables" from a map that is mainly used for entities, eg: `http://server:1337/api/DumpVars`
+
+[#dump-jwt]
+== Dump JWT Claims
+
+`InsecureAllowDumpJwtClaims: true` - will dump all the claims from a successfully parsed JWT token. This can be useful when trying to see how OliveTin is parsing the token, and what key fields are available.
diff --git a/docs/modules/ROOT/pages/troubleshooting/browser-console-logs.adoc b/docs/modules/ROOT/pages/troubleshooting/browser-console-logs.adoc
new file mode 100644
index 0000000..a239720
--- /dev/null
+++ b/docs/modules/ROOT/pages/troubleshooting/browser-console-logs.adoc
@@ -0,0 +1,55 @@
+[#browser-console-logs]
+= Browser console logs (WebUI troubleshooting)
+:experimental:
+
+The **developer console** (or **browser console**) is a panel built into your web browser. It records technical messages from the OliveTin WebUI—errors, warnings, and network failures—that do not always appear on the page itself.
+
+When something looks wrong in the WebUI (for example a blank area, buttons that never load, or errors after clicking), sharing **console output** helps others see what the browser reported and narrow down the cause. You do **not** need to be a web developer to open it or share it.
+
+**A screenshot of the console is often enough.** If you can copy text instead, that is helpful too. Either way, include what you were doing right before the problem (for example “opened the dashboard”, “clicked Start on action X”).
+
+TIP: Before posting publicly, glance at the console for anything that looks like a password, token, or private URL. Crop or redact those lines if needed.
+
+[#open-the-console]
+== Open the console
+
+Use OliveTin in the browser where you see the problem, then open the console *before* or *right after* reproducing the issue so the messages are still visible.
+
+[#desktop-chromium]
+=== Google Chrome, Microsoft Edge, Brave, and other Chromium-based browsers
+
+. Open the OliveTin page.
+. Open the developer tools:
+** *Windows / Linux:* press kbd:[F12], or kbd:[Ctrl+Shift+J] to go straight to the *Console*.
+** *macOS:* press kbd:[Cmd+Option+J] for the *Console*, or kbd:[Cmd+Option+I] for developer tools (then click the *Console* tab).
+. If you do not see a *Console* tab, click *»* or *+* in the developer tools toolbar and choose *Console*.
+
+[#desktop-firefox]
+=== Mozilla Firefox
+
+. Open the OliveTin page.
+. Open developer tools: kbd:[F12], or kbd:[Ctrl+Shift+I] on Windows/Linux, or kbd:[Cmd+Option+I] on macOS.
+. Select the *Console* tab. (On Windows/Linux you can also use kbd:[Ctrl+Shift+K] to open the console directly.)
+
+[#desktop-safari]
+=== Safari (macOS)
+
+Safari hides the console until you turn on the Develop menu:
+
+. *Safari* → *Settings* (or *Preferences*) → *Advanced* → enable *Show features for web developers* (wording may vary slightly by Safari version).
+. In the menu bar, open *Develop* → *Show JavaScript Console*, or press kbd:[Cmd+Option+C].
+
+[#what-to-capture]
+== What to capture
+
+. Stay on the *Console* tab.
+. If there are many old messages, use the console’s *Clear* control so only new messages appear, then reload the page or repeat the steps that trigger the problem.
+. Note any lines in **red** (errors) or **yellow** (warnings)—those are usually the most useful.
+. Use your system's screenshot tool to capture the console window, *or* right-click in the console → *Save as…* / *Copy all messages* if your browser offers it.
+
+[#share-the-logs]
+== Share the logs
+
+Attach the screenshot or pasted text to a xref:troubleshooting/wheretofindhelp.adoc[Discord or GitHub support] message, along with your OliveTin version and how you access it (direct URL, reverse proxy, etc.). That combination helps diagnose WebUI issues much faster than a description of the screen alone.
+
+If the problem might be on the server (OliveTin not starting, actions failing, API errors), also collect xref:troubleshooting/service-logs.adoc[service logs] from Docker, Podman, or systemd.
diff --git a/docs/modules/ROOT/pages/troubleshooting/err-fetch-buttons.adoc b/docs/modules/ROOT/pages/troubleshooting/err-fetch-buttons.adoc
new file mode 100644
index 0000000..41ecb1a
--- /dev/null
+++ b/docs/modules/ROOT/pages/troubleshooting/err-fetch-buttons.adoc
@@ -0,0 +1,6 @@
+[#err-fetch-buttons]
+= Error Getting Buttons
+
+This is most often caused by not being able to get to /api/ properly - normally due to a bad proxy config, or your network was disconnected.
+
+
diff --git a/docs/modules/ROOT/pages/troubleshooting/err-fetch-webui-settings.adoc b/docs/modules/ROOT/pages/troubleshooting/err-fetch-webui-settings.adoc
new file mode 100644
index 0000000..6f8eaa8
--- /dev/null
+++ b/docs/modules/ROOT/pages/troubleshooting/err-fetch-webui-settings.adoc
@@ -0,0 +1,9 @@
+[#err-fetch-webui-settings]
+= Error Fetching WebUI Settings
+
+This is a less common issue, but it means that the main web HTML has loaded, but it could not get http://yourserver:1337/webUiSettings.json - most likely because of a 404 (Not Found) or JSON parse issue. You can see the exact reason if your browser as Web Developer Tools - look in the console. Firefox and Chrome both have great web developer tools, that can normally be opened with the F12 key, or from the developer tools menu.
+
+The most common cause of this issue is a broken reverse proxy configuration. To debug this, browse to http://yourserver:1337/webUiSettings.json and adjust your reverse proxy configuration until the file loads properly. See xref:reverse-proxies/intro.adoc[Reverse Proxies] for common configuration instructions.
+
+An uncommon cause of this issue is if you are self-hosting the HTML outside of OliveTin. This is supported, but it means you will need to write the webUiSettings.json file manually. This is currently not documented as very very few people really need or want to do this. It's very uncommon at the moment to self host the HTML outside of the OliveTin main server. Jump on the discord to discuss this if you want to do this, see xref:troubleshooting/wheretofindhelp.adoc[support].
+
diff --git a/docs/modules/ROOT/pages/troubleshooting/err-js-modules-not-supported.adoc b/docs/modules/ROOT/pages/troubleshooting/err-js-modules-not-supported.adoc
new file mode 100644
index 0000000..97b438d
--- /dev/null
+++ b/docs/modules/ROOT/pages/troubleshooting/err-js-modules-not-supported.adoc
@@ -0,0 +1,10 @@
+[#err-js-modules-not-supported]
+= Error: JS Modules not supported
+
+This is most likely because you are using a very old browser, or have some Javascript disabled.
+
+This page describes the compatible browsers for Javascript modules; https://caniuse.com/es6-module .
+
+There is no workaround for this apart from updating your browser / using a better browser, as OliveTin wants to be a progressive web app and doesn't intend to support browsers without module support.
+
+
diff --git a/docs/modules/ROOT/pages/troubleshooting/err-websocket-connection.adoc b/docs/modules/ROOT/pages/troubleshooting/err-websocket-connection.adoc
new file mode 100644
index 0000000..d9c7624
--- /dev/null
+++ b/docs/modules/ROOT/pages/troubleshooting/err-websocket-connection.adoc
@@ -0,0 +1,14 @@
+[#err-websocket-connection]
+= Error Connecting to WebSocket
+
+If OliveTin was working, but this error popped up, it's most likely because your reverse proxy closed the connection due to a timeout.
+
+If OliveTin is always displaying this error, it's probably your reverse proxy not handling the websocket properly. Please check the Reverse Proxy documentation for more information.
+
+== Reverse proxies will close websockets
+
+Many reverse proxies use short default timeouts for connections. Long-lived websocket connections often hit these limits and get closed by the proxy, which triggers this error. OliveTin will attempt to reconnect automatically, but you can avoid the disconnects by increasing the websocket (or proxy) timeout in your reverse proxy configuration.
+
+Check your proxy's documentation for how to raise timeouts—for example, Apache HTTPD uses `ProxyTimeout`, and other proxies have similar settings. See the individual reverse proxy pages (e.g. xref:reverse-proxies/apache.adoc[Apache]) for examples.
+
+
diff --git a/docs/modules/ROOT/pages/troubleshooting/err-webui-mismatch.adoc b/docs/modules/ROOT/pages/troubleshooting/err-webui-mismatch.adoc
new file mode 100644
index 0000000..4667855
--- /dev/null
+++ b/docs/modules/ROOT/pages/troubleshooting/err-webui-mismatch.adoc
@@ -0,0 +1,12 @@
+= Error: WebUI Version Mismatch
+
+This is most often caused by the WebUI Version not matching the server version.
+
+Every release of OliveTin will change the version number for the server, and for the webui client - these versions should match. For example, version 2024.4.1 of the server should be paired with version 2024.4.1 of the webui client. It is unlikely that the release of the server and webui client will be out of sync, but it is possible if you are using a custom build of the webui client, but the most likely cause is **aggressive caching** of the webui client files in your browser, or proxy server.
+
+There are cache-busting mechanisms built into OliveTin to try and avoid this error, but they don't always work in every environment.
+
+* Try forcing a refresh with "Ctrl+F5" in your browser.
+* If that doesn't work, try clearing your browser cache.
+* If that doesn't work, try using a different browser.
+
diff --git a/docs/modules/ROOT/pages/troubleshooting/exit127.adoc b/docs/modules/ROOT/pages/troubleshooting/exit127.adoc
new file mode 100644
index 0000000..f442904
--- /dev/null
+++ b/docs/modules/ROOT/pages/troubleshooting/exit127.adoc
@@ -0,0 +1,5 @@
+= Exit code 127
+
+Exit code 127 on Linux typically means "command not found". This can be the
+case when you need to install command in a container image for example.
+
diff --git a/docs/modules/ROOT/pages/troubleshooting/log-debug-options.adoc b/docs/modules/ROOT/pages/troubleshooting/log-debug-options.adoc
new file mode 100644
index 0000000..cf18c21
--- /dev/null
+++ b/docs/modules/ROOT/pages/troubleshooting/log-debug-options.adoc
@@ -0,0 +1,12 @@
+[#log-debug-options]
+= Debug Log Options
+
+[source,yaml]
+----
+logDebugOptions:
+ singleFrontendRequests: true
+ singleFrontendRequestHeaders: true
+ aclMatched: true
+ aclNotMatched: true
+----
+
diff --git a/docs/modules/ROOT/pages/troubleshooting/puid-pgid.adoc b/docs/modules/ROOT/pages/troubleshooting/puid-pgid.adoc
new file mode 100644
index 0000000..889799a
--- /dev/null
+++ b/docs/modules/ROOT/pages/troubleshooting/puid-pgid.adoc
@@ -0,0 +1,16 @@
+[#no-puid-pgid]
+= PUID and PGID support
+
+The OliveTin container image does not use the PUID and PGID convention to specify which user the container should run as - this is a convention that was popularized by linuxserver.io, because their container images use supervisord. Instead, simply use the `--user` argument when defining the container, to change the user OliveTin runs as.
+
+* link:https://docs.linuxserver.io/general/understanding-puid-and-pgid[LSIO documentation for PUID and PGID, that says that `--user` is the same thing]
+
+An example is shown below;
+
+[source,shell]
+.Using --user
+----
+user@host: docker create --name olivetin -p 1337:1337 -v /etc/OliveTin/:/config:ro docker.io/jamesread/olivetin --user container:container
+user@host: docker start olivetin
+----
+
diff --git a/docs/modules/ROOT/pages/troubleshooting/server-diagnostics.adoc b/docs/modules/ROOT/pages/troubleshooting/server-diagnostics.adoc
new file mode 100644
index 0000000..4ff61aa
--- /dev/null
+++ b/docs/modules/ROOT/pages/troubleshooting/server-diagnostics.adoc
@@ -0,0 +1,69 @@
+[#server-diagnostics]
+= Server diagnostics
+
+OliveTin has a useful feature to gather information about your installation when you have a support request. If you are able to provide **server diagnostics**, this generally helps others help you a lot. **Server diagnostics** does NOT send any information to the developers or anybody else—it's simply text—**copy and paste it** to where someone is trying to help you!
+
+[source,yaml]
+.Example server diagnostics output
+----
+### SOSREPORT START (copy all text to SOSREPORT END)
+# Build:
+commit: nocommit
+version: dev
+date: nodate
+
+# Runtime:
+os: linux
+osreleaseprettyname: PRETTY_NAME="Fedora Linux 37 (Workstation Edition)"
+arch: amd64
+incontainer: false
+lastbrowseruseragent: ""
+
+# Config:
+countofactions: 7
+loglevel: INFO
+
+### SOSREPORT END (copy all text from SOSREPORT START)
+----
+
+The markers and field names above reflect what OliveTin prints; copy everything from the start marker through the end marker.
+
+You can then copy and paste this text into a GitHub issue, discussion, Discord chat, or wherever else someone might be helping you.
+
+
+== How do I generate server diagnostics?
+
+OliveTin needs to be able to start and its API needs to be functional. Once OliveTin is started, open this URL in a browser (replace the host and port with yours):
+
+`http://myserver:1337/api/sosreport`
+
+The `/api/sosreport` path is the HTTP endpoint OliveTin uses for server diagnostics.
+
+== Optional: Allow insecure (but easy) dumping to the browser
+
+There is a configuration option you can set in your `config.yaml` that allows you to easily dump server diagnostics to your browser when visiting the API. This is turned off by default, as you should not allow anybody to request diagnostics at any time they like, but you can enable this option temporarily to easily get access to the text in your browser.
+
+[source,yaml]
+.`config.yaml`
+----
+InsecureAllowDumpSos: true
+----
+
+== Default: Diagnostics dump to logs
+
+You should get a simple JSON message saying something like;
+
+----
+alert: "Your SOS Report has been logged to OliveTin logs."
+----
+
+The exact wording may match your OliveTin version.
+
+If you see this, great! The actual contents of the server diagnostics are not returned to your browser for security reasons (guests could get info about your installation, etc).
+
+To find the diagnostics text depends on how you are running OliveTin. If you are running in a container, then try `docker logs olivetin` (where `olivetin` is your container name). If you are running using systemd, then try `journalctl -eu OliveTin`.
+
+== What if I cannot even get to the OliveTin API?
+
+OliveTin's web interface doesn't need to be working to get server diagnostics (the web interface and the API are separate). However, if OliveTin won't even start, or you cannot reach the API, then server diagnostics cannot be generated. Please specify that when you ask for help.
+
diff --git a/docs/modules/ROOT/pages/troubleshooting/service-logs.adoc b/docs/modules/ROOT/pages/troubleshooting/service-logs.adoc
new file mode 100644
index 0000000..4c45d82
--- /dev/null
+++ b/docs/modules/ROOT/pages/troubleshooting/service-logs.adoc
@@ -0,0 +1,118 @@
+[#service-logs]
+= Service logs (OliveTin process troubleshooting)
+:experimental:
+
+**Service logs** are the lines OliveTin prints while it runs—startup messages, configuration load, errors, and action output. They are separate from xref:troubleshooting/browser-console-logs.adoc[browser console logs], which come from the WebUI in your browser.
+
+When something fails on the server (OliveTin will not start, actions error, API or auth problems), sharing **recent service log output** helps others see what the process reported. You do **not** need to be a Linux expert to copy logs from Docker, Podman, or `journalctl`.
+
+TIP: Before posting publicly, scan the log text for passwords, API tokens, cookies, or internal hostnames. Remove or replace those strings if needed. If you enable xref:troubleshooting/log-debug-options.adoc[extra debug logging], output can be more sensitive than usual.
+
+[#where-logs-go]
+== Where logs come from
+
+How you read logs depends on how OliveTin is installed:
+
+* **Container (Docker / Podman):** logs are what the container runtime captured from OliveTin’s standard output.
+* **systemd service:** logs are stored by the system journal for the `OliveTin` unit.
+* **Manual / binary** (for example `./OliveTin` in a terminal): messages print to that terminal—copy them from there, or redirect output to a file when testing.
+
+[#manual-binary]
+== Manual or binary run
+
+If you start OliveTin directly in a terminal window, **scroll back** and copy the text from startup through the error, or run it again and capture output:
+
+[source,shell]
+----
+./OliveTin 2>&1 | tee olivetin-run.log
+----
+
+That writes everything to `olivetin-run.log` while still showing it on screen. Stop OliveTin before sharing, then attach or paste the relevant part of the file.
+
+[#docker-and-podman]
+== Docker and Podman
+
+Use the **same container name** you chose when you created the container (examples below use `olivetin`; yours may differ).
+
+.Show recent log lines (last ~200 lines)
+[source,shell]
+----
+docker logs --tail 200 olivetin
+----
+
+With Podman, use the same pattern:
+
+[source,shell]
+----
+podman logs --tail 200 olivetin
+----
+
+If OliveTin **crashes on startup** or you need the **full story from a restart**, show logs **after** reproducing the problem—either capture enough lines, or use a time window.
+
+.Limit output to the last few minutes (adjust the time as needed)
+[source,shell]
+----
+docker logs --since 10m olivetin
+----
+
+To **watch** logs live while you trigger an issue in another window:
+
+[source,shell]
+----
+docker logs -f olivetin
+----
+
+Press kbd:[Ctrl+C] to stop following; that does not stop OliveTin.
+
+[#docker-compose]
+== Docker Compose
+
+From the directory that contains your `compose.yaml` (or `docker-compose.yml`):
+
+[source,shell]
+----
+docker compose logs --tail 200 olivetin
+----
+
+Use the **service name** from the compose file, not necessarily the container name. Add `-f` to follow logs live, same idea as above.
+
+[#systemd]
+== systemd (native Linux package)
+
+Show whether the service is running and recent status:
+
+[source,shell]
+----
+systemctl status OliveTin
+----
+
+Read the journal for the OliveTin unit (scroll with arrow keys, quit with kbd:[q]):
+
+[source,shell]
+----
+journalctl -eu OliveTin
+----
+
+For a **time-bounded** slice (for example after a restart or failed action):
+
+[source,shell]
+----
+journalctl -eu OliveTin --since "30 minutes ago"
+----
+
+[#what-to-capture]
+== What to capture
+
+. Reproduce the problem if you can (restart OliveTin, click the action, etc.), then collect logs **immediately after**.
+. Include lines from **startup** through the **error**—not only the last one line.
+. If the log is huge, prefer `--since` / `--tail` so the excerpt stays readable.
+. Note your **install type** (Docker, Podman, Compose, systemd) and OliveTin **version** if you know it (from the WebUI footer, xref:troubleshooting/server-diagnostics.adoc[server diagnostics], or startup text).
+
+For deeper server-side detail, you can temporarily enable xref:troubleshooting/log-debug-options.adoc[LogDebugOptions]—then capture logs again with those settings in mind.
+
+[#share-the-logs]
+== Share the logs
+
+Paste the text into a xref:troubleshooting/wheretofindhelp.adoc[Discord or GitHub support] message (or attach a `.txt` file if it is long). Together with **how you run OliveTin** (direct port, reverse proxy, container flags) and **what you did** before the error, service logs make backend issues much easier to diagnose than a screenshot of the WebUI alone.
+
+For WebUI-only behaviour (blank page, buttons not loading), also capture xref:troubleshooting/browser-console-logs.adoc[browser console logs] when relevant.
diff --git a/docs/modules/ROOT/pages/troubleshooting/wheretofindhelp.adoc b/docs/modules/ROOT/pages/troubleshooting/wheretofindhelp.adoc
new file mode 100644
index 0000000..7ab3f3d
--- /dev/null
+++ b/docs/modules/ROOT/pages/troubleshooting/wheretofindhelp.adoc
@@ -0,0 +1,4 @@
+[#support]
+= Where to find help
+
+include::partial$support.adoc[]
diff --git a/docs/modules/ROOT/pages/upgrade/2k3k.adoc b/docs/modules/ROOT/pages/upgrade/2k3k.adoc
new file mode 100644
index 0000000..3520855
--- /dev/null
+++ b/docs/modules/ROOT/pages/upgrade/2k3k.adoc
@@ -0,0 +1,106 @@
+= Understanding OliveTin 2k vs 3k
+
+NOTE: OliveTin 3k is basically "rebuilding OliveTin for the future" - and aims to have high compatibility with OliveTin 2k while people migrate. However, as we all know, stuff will break when 3k is new - so please be patient, and report issues as you find them! OliveTin 2k isn't going anywhere anytime soon!
+
+== Quick summary of key points
+
+|===
+| Release Series | Versioning Scheme | Supported Until
+
+| **OliveTin 2k**
+
+eg: 2022.11.11
+
+eg: 2023.04.15
+| Calendar Versioning (calver)
+| At least 31st December 2028 (bug fixes and security updates only)
+
+| **OliveTin 3k**
+
+eg: 3000.0.0
+
+eg: 3000.1.0
+| Semantic Versioning (semver)
+| Ongoing - new features and updates
+|===
+
+Users who are upgrading are encouraged to try OliveTin 3k, but understand that it represents several major technology changes, so subtle things might change or break in the first few releases. The objective is to maintain as much compatibility as is pragmatically possible, and highlight any breaking changes clearly.
+
+The main reason for OliveTin 3k is to allow for major technology changes inside the project that enable new features at a later date.
+
+== Understanding why a change was necessary - from calver to semver
+
+The original version of OliveTin was released in May 2021, and it used a versioning system called link:https://calver.org[calver] (Calendar Versioning). For those of you unfamiliar with the calver standard, it means that instead of using 1.0.0 as the first version, the first version was actually called **2021-05-19**. One of the benefits of this approach is that you can determine how old the version is just by looking at the version number alone. Unfortunately this is the only real benefit.
+
+However, this worked just fine for a while, but as OliveTin grew, it became clearer that most packaging systems and users were more accustomed to the more traditional link:https://semver.org[semver] (Semantic Versioning) system. As a stopgap measure, OliveTin switched the format of it's versioning to use dots (".") instead of dashes ("-") in an attempt to make it look more like a traditional semver version. This resulted in a change with version **2022.11.11** (November 11th, 2022 - what a cool date!).
+
+As OliveTin continued to mature, and more features were added, it became clear that there were some major architecture and library choices that were no longer ideal. Switching these out (like the configuration library, Viper), is a really major change, and isn't something that is good to do without a lot of users being very aware of it. In May 2025, a proposal was launched to switch OliveTin to a more traditional semver versioning system. You can view the detail of that proposal here: https://github.com/OliveTin/OliveTin/discussions/591
+
+In short, the benefits of switching to semver are;
+
+- Easier to understand versioning for most users
+- Allows for major technology changes without confusing users (like the Viper change)
+- Better compatibility with packaging systems
+- Better reflects the maturity of versions, and allows people to opt out of minor patches for example
+
+This proposal seemed to be positively received by the community, and so the James (founder) decided to move forward with the change.
+
+== What were the major reasons for the technology changes in OliveTin 3k?
+
+OliveTin 2k has the following technology choices which limited it's potential for stability, maintainability and growth;
+
+- The communication between the client and the server uses gRPC, which is then fronted by a REST API and messages are very hackily wrapped in JSON. This has worked remarkably well, but the web has moved on to offer websockets and streaming in a far better way, and connectrpc solves many of these challenges neatly and elegantly.
+- The configuration library, Viper, is a defacto standard in Go several years ago - big projects like kubectl and others use it. However the library is extremely heavyweight (lots of dependencies), it has lots of open issues, and doesn't support some commonly requested features like configuration file splitting and inclusion - which would really help user's manage large configurations.
+- The entity management inside OliveTin was written in a weekend, and is just a large map of strings inside OliveTin2k, again this has worked surprisingly well, but there are lots of interesting use cases with entities that would be much easier to implement with a proper typed backend - and far better mapping between actions and entities (which is extremely horrible in OliveTin 2k).
+- The javascript in OliveTin 2k is raw web components, based on the "new" (at the time) specs from 2011. It's fast and standards compliant, but it's also resulted in a LOT of spaghetti code that is very hard to test and maintain.
+
+== Why version 3000.0.0 (and not 3.0.0)?
+
+If you look through the original proposal, it was to create a new package called "OliveTin-semver", and start the versioning at 1.0.0. However, after some more thought, it was decided that this would actually create a lot of confusion, as users would have to figure out if they were using "OliveTin" or "OliveTin-semver" - and it duplicates the package in in places like Linux distributions, which is not a good idea.
+
+However, starting with version "2.0.0" after versions like "2022.11.11" would break the logic of lots of update tools and scripts that people use. This is because major version "2" is below "2022", and so the version ordering, and logic would always think that version 2022.x.x is newer than 2.x.x. This would be very confusing for users, and would likely lead to people not updating, or even downgrading by mistake.
+
+To avoid this confusion, it was decided to jump straight to version "3000.0.0" - which is clearly above "2022.11.11" and any other 2k version. This way, users can easily see that 3000.x.x is newer than any 2k version, and it avoids any confusion with version ordering.
+
+- OliveTin versions like 2022.11.11 and 2023.04.15 are part of "**OliveTin 2k**" (2000 series) and use a **calendar versioning** (calver) scheme. These versions are stable and will continue to receive updates and support but will not receive new features.
+ - There is no plan to stop supporting OliveTin 2k versions until **at least 31st December 2028** (and I'll probably keep extending that date, but it's good to have a cutoff somewhere).
+- OliveTin versions starting from 3000.0.0 and onwards are part of "**OliveTin 3k**" (3000 series) and also use a **semantic versioning scheme**. These versions will receive new features, improvements, and updates.
+
+== Should I automatically update to OliveTin 3k?
+
+Users are encouraged to try OliveTin 3k, but understand that it represents several major technology changes, so subtle things might change or break in the first few releases. Please remember that OliveTin 2k will continue to be supported and receive updates until at least 31st December 2028, so there is no rush to upgrade.
+
+The developers of this project have quite a lot of test infrastructure around OliveTin 3k (more than was possible with 2k), and would like to **strongly urge problems with updates to be reported, so that they can be fixed when possible**.
+
+=== How do I stop my OliveTin 2k containers from upgrading to OliveTin 3k automatically?
+
+Change your `latest` tag to `latest-2k` in your container definitions.
+
+3 tags now exist for container images and container registries;
+
+- `olivetin/olivetin:latest-2k` - This tag will always point to the latest OliveTin 2k version (eg 2025.11.11)
+- `olivetin/olivetin:latest-3k` - This tag will always point to the latest OliveTin 3k version (eg 3000.2.0)
+- `olivetin/olivetin:latest` - This tag will always point to the latest OliveTin version (currently 3k)
+
+xref:install/container.adoc[More information about installing OliveTin in containers]
+
+=== What does GitHub "latest" point to?
+
+GitHub's "latest" release tag isn't super helpful. It currently points to the "release that was pushed last". So sometimes this will be 2k, and sometimes this will be 3k. We're trying to find a way to fix this.
+
+== What are the changes I need to make to upgrade from OliveTin 2k to 3k?
+
+=== OliveTin Configuration files
+
+To date, no changes are required to the configuration file (and configs are being automatically migrated internally to OliveTin 3k - without rewriting the original config file).
+
+However, it is possible that some configuration changes will be necessary as we learn more about people's setups with OliveTin 2k.
+
+=== Reverse Proxy configurations
+
+If you are using a reverse proxy (like Nginx, Apache, Caddy, Traefik etc), then you will need to make some changes to your configuration. This is because OliveTin 3k uses websockets for communication between the client and the server, instead of gRPC over HTTP/2. Many proxies use short default timeouts and will close websocket connections; increase the websocket or proxy timeout in your proxy config to avoid disconnects (OliveTin will reconnect automatically). See xref:troubleshooting/err-websocket-connection.adoc#reverse-proxies-will-close-websockets[Reverse proxies will close websockets] for more.
+
+Please refer to the [Reverse Proxies](reverse-proxies/intro.adoc) section of the documentation for updated configuration examples for various reverse proxies.
+
+- xref:reverse-proxies/nginx.adoc#upgrade3k[Nginx: Updating your configuration from OliveTin 2k to OliveTin 3k]
+
diff --git a/docs/modules/ROOT/pages/upgrade/github_latest.adoc b/docs/modules/ROOT/pages/upgrade/github_latest.adoc
new file mode 100644
index 0000000..376da39
--- /dev/null
+++ b/docs/modules/ROOT/pages/upgrade/github_latest.adoc
@@ -0,0 +1,28 @@
+= Warning - GitHub Latest
+
+GitHub has a handy feature where releases can be marked as "latest". However, it does not understand that some projects, like OliveTin, have **two** active release streams, xref::upgrade/2k3k.adoc[2k and 3k]. Both of those individual streams can have a "latest" - the latest 2k version and the latest 3k version. Unfortunately, GitHub does not provide a way to mark a release at "latest-2k" or "latest-3k" - it only has "latest".
+
+It often happens that a 2k release will come out AFTER a 3k release, and if we left GitHub to do what it does by default, the "latest" URL would be the 2k version - and then when a 3k release goes out, the "latest" URL would be point to a 3k version. This is probably not what you want - "latest" flipping between the "last release that went out". You want it to point to the latest 3k version.
+
+Therefore, the OliveTin project has disabled "latest" releases for all the 2k versions, so that the GitHub "latest" URL will always be the latest 3k version.
+
+This means that **GitHub "latest" URLs will now always point to a 3k version** (eg: `https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin_linux_amd64.rpm` = 3k version).
+
+[WARNING]
+If you have been using a script to download the latest version of OliveTin, **you will need to accept 3k, OR update your script to use the new workaround described below.**
+
+== Container Tags are unaffected
+
+Container registries allow any number of arbitary tags, so containers DO have a "latest-2k" and "latest-3k" tags. The breaking change described above is only for GitHub release URLs. So, you can still pull the latest 2k or 3k version of the container if you want to. Learn more about available tags in the xref::install/container.adoc[Container Installation Guide].
+
+== What if I want to use the "latest 2k" download URL in my scripts?
+
+It is understood that some users will want to hard-code a "wget URL" for the latest 2k version into their scripts. Unfortunately the GitHub "latest" URL is now no longer a reliable way to do this. However, the OliveTin project has provided you with a workaround.
+
+In the repository OliveTin/update-check.olivetin.app, there is a Python script that will return the latest 2k version number. You can use this to construct a URL for the latest 2k version. The file `versions.json` also contains the latest 3k versions, and provides download URLs and checksums for each package.
+
+* link:https://raw.githubusercontent.com/OliveTin/update-check.olivetin.app/refs/heads/main/versions.json[versions.json in the update-check.olivetin.app repository]
+
+This repository is updated automatically whenever a new 2k or 3k version is released, so you can be sure that the URL you use will always point to the latest version. Please check this repository README for details on how to use this file in your scripts to get the relevant download URLs;
+
+* link:https://github.com/OliveTin/update-check.olivetin.app/tree/main[update-check.olivetin.app README]
\ No newline at end of file
diff --git a/docs/modules/ROOT/pages/upgrade/upgrade_notes.adoc b/docs/modules/ROOT/pages/upgrade/upgrade_notes.adoc
new file mode 100644
index 0000000..8ca32ab
--- /dev/null
+++ b/docs/modules/ROOT/pages/upgrade/upgrade_notes.adoc
@@ -0,0 +1,67 @@
+[#upgrade-notes]
+= Upgrade Notes
+
+OliveTin releases are published to GitHub, and the release notes are contained there. This page includes a summary of "Upgrade Notes" for breaking changes between releases.
+
+== 2024.08.14
+
+=== Navigation change - Subpaths no longer supported
+
+In the past, OliveTin supported subpaths in the URL, for example, `http://yourServer:1337/olivetin/` would load the OliveTin web interface. This was convenient for people who wanted to run OliveTin on a subpath of their domain for some reason, but it actually creates a lot of complexity in the code, and makes it harder to maintain. One particular change that was wanted was to be able to link to specific pages in OliveTin, for example, `http://yourServer:1337/logs` or `http://yourServer:1337/myDashboard/myFolder` - this became incredibly difficult to implement with the subpath support.
+
+Therefore, OliveTin no longer supports subpaths in the URL. If you have been using OliveTin with a subpath, you will need to change your configuration to use a subdomain or a different port.
+
+== 2024.04.09
+
+=== Themes Directory (for theme users)
+
+==== Background
+
+Until now, OliveTin has served the themes directory from the "webui" directory, normally `/var/www/olivetin/themes` on most installations. If you wanted to install a theme, you would put the theme in to that directory.
+
+This was a bit cumbersome, because OliveTin treats the content of the "webui" directory as disposable / part of the system, whereas themes are much more like "user data" and "configuration". It also meant that people using Linux Containers had to bind-mount a separate directory for themes.
+
+==== Upgrade
+
+Now themes are stored in the "configdir" (wherever OliveTin finds it's config file, eg `/config` in containers), under the subdirectory `custom-webui/themes`. OliveTin will try to create the `custom-webui` folder in your configdir if it doesn't find it.
+
+The advantage of this change is that themes are stored with your config, as part of your data.
+
+To upgrade, simply move any themes you might have into your configdir, under `custom-webui/themes/`. eg:
+
+[source,yaml]
+----
+.
+├── config.yaml
+├── custom-webui
+│ └── themes
+│ └── custom-icons
+│ ├── icon.png
+│ └── theme.css
+├── entities
+│ ├── containers.json
+│ ├── heating.yaml
+│ ├── servers2.yml
+│ ├── servers.yaml
+│ └── systemd_units.json
+└── installation-id.txt
+----
+
+=== Theme Asset Paths (for theme developers)
+
+==== Background
+
+OliveTin had to send the theme name to the browser, so that some javascript could then request `http://yourServer:1337/themes/myTheme/theme.css`, and load it as a stylesheet. This had the following problems;
+
+. This was slow (could take up to a second)
+. This creates a "flash" in the browser, as the new theme.css is loaded over the top of the existing stylesheet.
+. Browsers could not cache this theme.css with the page load.
+
+==== Upgrade
+
+The new behavior is that OliveTin will always try to load `http://yourServer:1337/theme.css` as part of the static HTML that is sent to the browser. This means that regular caching can cache the theme.css, and this effectively elliminates the slowness and "flashing" when the browser renders the page.
+
+Internally, OliveTin maps `http://yourServer:1337/theme.css` to the `theme.css` of whatever the theme is set to with `themeName`, for example, it could map to `themes/myTheme/theme.css`.
+
+As a theme developer, normally you would reference a background image, or similar using `./background.png`, but OliveTin is loading the theme.css as the directory root, and the themes' assets from the custom-webui theme directory. Therefore you need to update paths to be like; `background-image: "/custom-webui/themes/myTheme/background.png`.
+
diff --git a/docs/modules/ROOT/partials/action_examples/actionHeader.adoc b/docs/modules/ROOT/partials/action_examples/actionHeader.adoc
new file mode 100644
index 0000000..76c9f12
--- /dev/null
+++ b/docs/modules/ROOT/partials/action_examples/actionHeader.adoc
@@ -0,0 +1,13 @@
+
+[%header, cols="1,4"]
+|===
+| Installation type | Difficulty to do this
+| Running as a **Systemd service** | {systemd}
+| Running in a **container** | {container}
+|===
+
+== Example config.yaml
+
+// Unset variables
+:container!:
+:systemd!:
diff --git a/docs/modules/ROOT/partials/action_examples/ssh_intro.adoc b/docs/modules/ROOT/partials/action_examples/ssh_intro.adoc
new file mode 100644
index 0000000..7826427
--- /dev/null
+++ b/docs/modules/ROOT/partials/action_examples/ssh_intro.adoc
@@ -0,0 +1,2 @@
+This is probably one of the most useful things OliveTin is used for - just plain old SSH, which allows it to easily connect from a container to any server running on your network to run commands. This is also the preferred method of running commands on the server that is hosting the OliveTin container image as well.
+
diff --git a/docs/modules/ROOT/partials/action_execution/onfileindir_arguments.adoc b/docs/modules/ROOT/partials/action_execution/onfileindir_arguments.adoc
new file mode 100644
index 0000000..c2f4347
--- /dev/null
+++ b/docs/modules/ROOT/partials/action_execution/onfileindir_arguments.adoc
@@ -0,0 +1,17 @@
+== File in dir arguments
+
+|===
+| Predefined Argument | Example
+
+| `filepath` | /Downloads/txt1.txt
+| `filedir` | /Downloads
+| `filename` | test1.txt
+| `fileext` | .txt
+| `filesizebytes` | 100
+| `filemode` | 0644
+| `filemtime` | 2024-04-27 20:09:42.465235047 +0100 BST
+| `fileisdir` | false
+|===
+
+Like all arguments, OliveTin also passes these arguments as <> if this is better for your use case.
+
diff --git a/docs/modules/ROOT/partials/api/start_action_methods.adoc b/docs/modules/ROOT/partials/api/start_action_methods.adoc
new file mode 100644
index 0000000..48a6e63
--- /dev/null
+++ b/docs/modules/ROOT/partials/api/start_action_methods.adoc
@@ -0,0 +1,30 @@
+There are 4 main methods to start an action in OliveTin, which can be used in scripts or other applications. These methods allow you to trigger actions either immediately or wait for them to complete, and they can be accessed via HTTP requests.
+
+.Webhook/API reference table
+[%header]
+|======================================================
+| Function | HTTP Method | Request Type (How to select an action) | Response Type
+
+| xref:api/method_StartAction.adoc[StartAction]
+| POST
+| xref:api/start_action.adoc#api-request-obj[OliveTin request object]
+| xref:api/start_action.adoc#api-response-trackingid[Execution Tracking ID]
+
+| xref:api/method_StartActionByGet.adoc[StartActionByGet]
+| GET
+| xref:api/start_action.adoc#api-request-idurl[Action ID in the URL]
+| xref:api/start_action.adoc#api-response-trackingid[Execution Tracking ID]
+
+| xref:api/method_StartActionAndWait.adoc[StartActionAndWait]
+| POST
+| xref:api/start_action.adoc#api-request-obj[OliveTin request object]
+| xref:api/start_action.adoc#api-response-logentry[Log Entry (waits for the action to finish)]
+
+| xref:api/method_StartActionByGetAndWait.adoc[StartActionByGetAndWait]
+| GET
+| xref:api/start_action.adoc#api-request-idurl[Action ID in the URL]
+| xref:api/start_action.adoc#api-response-logentry[Log Entry (waits for the action to finish)]
+
+|======================================================
+
+You can also browse these methods in the link:htts://docs.olivetin.app/api/swagger/[OpenAPI/Swagger] docs if you prefer.
diff --git a/docs/modules/ROOT/partials/args/reject-null.adoc b/docs/modules/ROOT/partials/args/reject-null.adoc
new file mode 100644
index 0000000..e088875
--- /dev/null
+++ b/docs/modules/ROOT/partials/args/reject-null.adoc
@@ -0,0 +1,9 @@
+== Rejecting empty values (null)
+
+```yaml
+actions:
+ - shell: echo "Hello {{ name }}!"
+ arguments:
+ - name: name
+ rejectNull: true`
+```
diff --git a/docs/modules/ROOT/partials/config-start.adoc b/docs/modules/ROOT/partials/config-start.adoc
new file mode 100644
index 0000000..aa227d4
--- /dev/null
+++ b/docs/modules/ROOT/partials/config-start.adoc
@@ -0,0 +1,2 @@
+[source,yaml]
+.`config.yaml`
diff --git a/docs/modules/ROOT/partials/container_socket.adoc b/docs/modules/ROOT/partials/container_socket.adoc
new file mode 100644
index 0000000..0b08e42
--- /dev/null
+++ b/docs/modules/ROOT/partials/container_socket.adoc
@@ -0,0 +1,74 @@
+== Setup if running inside a container
+
+You can control other containers, when running OliveTin inside a container
+itself, however you need to do some extra setup when creating the OliveTin
+container.
+
+=== Ensure your container has permissions to control docker
+
+You have two alternatives to allow OliveTin (running inside a container) to talk to the Docker daemon through the bind-mounted socket. Pick one:
+
+==== Option 1 — Use `--privileged` (simplest)
+
+NOTE: Simplest for most users. Podman does not have this requirement.
+
+- Run the container with `--privileged` and as `root` (eg `--user root`).
+- This avoids user/group permission issues on `/var/run/docker.sock`.
+
+If you are getting "permission denied" errors it is probably because OliveTin runs as user UID 1000 by default, which is not allowed by your docker host. Running with `--user root` under `--privileged` resolves this quickly. Note that xref:troubleshooting/puid-pgid.adoc[PUID and PGID variables will not work].
+
+==== Option 2 — Run as non-root in the host `docker` group (no `--privileged`)
+
+Use the standard Docker guidance to manage Docker as a non-root user (becoming a member of the `docker` group) and match the group's GID inside the container so the process can access the socket permissions.
+
+- Docs: https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user[Manage Docker as a non-root user]
+- Find the `docker` group GID on the host, for example using `getent group docker`.
+- Run the container with your user UID and the `docker` group GID, and bind-mount the socket. Using Compose:
+
+[source,yaml]
+.docker-compose.yml
+----
+services:
+ olivetin:
+ container_name: olivetin
+ image: jamesread/olivetin
+ user: ${UID}:${docker_group_id}
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+----
+
+Where `UID` and `docker_group_id` are provided via your shell environment or a `.env` file next to your Compose file, for example:
+
+[source,bash]
+.env
+----
+UID=1000
+docker_group_id=995
+----
+
+This allows you to run the container as a non-root user, while still allowing access to `/var/run/docker.sock`.
+
+=== Pass the docker socket into the container
+
+. Pass `/var/run/docker.sock` as a bind mount to the container when creating it, eg:
+
++
+----
+docker create --privileged --user root -v /var/run/docker.sock:/var/run/docker.sock ...additional args here...
+----
++
+Or, using the `docker run` syntax;
++
+----
+docker run --privileged --user root -v /var/run/docker.sock:/var/run/docker.sock --name OliveTin jamesread/olivetin
+----
++
+. The official x86_64 docker container comes with the `docker` client pre-installed. If you are using `arm` or and `arm64` container, you will need to add Docker yourself.
++
+xref:reference/containerInstallPackages.adoc[How to install additional packages in the container]
++
+NOTE: The reason that the `arm` and `arm64` containers do not include docker, is that when these images are cross-compiled at build time, it takes FOREVER because we have to emulate arm.
+
+After you have passed the socket into the container (and optionally installed docker), you should be able to setup docker actions like it's shown in the example <>.
+
+
diff --git a/docs/modules/ROOT/partials/earlydoc.adoc b/docs/modules/ROOT/partials/earlydoc.adoc
new file mode 100644
index 0000000..1e12b1f
--- /dev/null
+++ b/docs/modules/ROOT/partials/earlydoc.adoc
@@ -0,0 +1,2 @@
+
+NOTE: This page is marked as "earlydoc", which means that it more of a collection of notes and an early draft before this page turns into good documentation later on. It is hoped that this early form of documentation is useful to you, but please understand that most documentation pages are higher quality than this. If you have suggestions or comments, please do get in contact or consider contributing your suggestions to the OliveTin documentation.
diff --git a/docs/modules/ROOT/partials/install/container.adoc b/docs/modules/ROOT/partials/install/container.adoc
new file mode 100644
index 0000000..cf97445
--- /dev/null
+++ b/docs/modules/ROOT/partials/install/container.adoc
@@ -0,0 +1,6 @@
+[NOTE]
+====
+OliveTin is supported when run as a Linux Container in many different ways, and lot of OliveTin users will assume that a Linux Container is the best way to install OliveTin because Linux Containers are really popular...
+
+However, it is very common that some OliveTin use cases become overcomplicated when a container is used, compared to running as a native service. Read the <> document to understand if a container or a service is right for your use case.
+====
diff --git a/docs/modules/ROOT/partials/install/container_registries.adoc b/docs/modules/ROOT/partials/install/container_registries.adoc
new file mode 100644
index 0000000..657c27b
--- /dev/null
+++ b/docs/modules/ROOT/partials/install/container_registries.adoc
@@ -0,0 +1,6 @@
+
+The image is pushed to the following registries, pick the one that you prefer - the container images are identical so you only need one;
+
+* Dockerhub container image: link:https://hub.docker.com/r/jamesread/olivetin/[docker.io/jamesread/olivetin]
+* GitHub container image: link:https://ghcr.io/olivetin/olivetin[ghcr.io/jamesread/olivetin]
+
diff --git a/docs/modules/ROOT/partials/install/post_container.adoc b/docs/modules/ROOT/partials/install/post_container.adoc
new file mode 100644
index 0000000..ec527cb
--- /dev/null
+++ b/docs/modules/ROOT/partials/install/post_container.adoc
@@ -0,0 +1,63 @@
+== Post installation (container)
+
+You will need to write a basic configuration file before OliveTin will startup.
+
+Create a basic config file at `/etc/OliveTin/config.yaml` - the exact path depends on what directory you specified in the bind mount container creation in the last step. Note that the file must be called `config.yaml`, and `config.yml` or `mystuff.yaml` would not work. You can download a sample configuration file like this if you like;
+
+[source,shell]
+.Download the sample config.yaml file to get you started.
+----
+user@host: cd /etc/OliveTin/
+user@host: curl -O https://raw.githubusercontent.com/OliveTin/OliveTin/main/config.yaml
+----
+
+The file contents should look something like this;
+
+.The most simple `config.yaml` file.
+[source,yaml]
+----
+actions:
+ - title: "Hello world!"
+ shell: echo 'Hello World!'
+----
+
+If you are running a firewall on your server, like firewalld, you will need to
+open port 1337;
+
+== Configure your firewall
+
+[tabs]
+====
+No Firewall:: If you don't have a firewall, continue to the next section.
+
+FirewallD:: This is how you configure your firewalld firewall for OliveTin:
++
+[source, shell]
+----
+user@host: firewall-cmd --add-port 1337/tcp --permanent
+user@host: firewall-cmd --reload
+----
++
+====
+
+== Start the OliveTin service
+
+Now that you have a configuration file, and the OliveTin container created, you are now ready to start OliveTin!
+
+----
+user@host: docker start olivetin
+----
+
+include::partial$install/to_config.adoc[]
+
+== Troubleshooting podman/docker installations
+
+If you are having problems in starting OliveTin, or OliveTin is crashing on startup, then check the logs like this;
+
+----
+user@host: docker logs OliveTin
+----
+
+For more detail on what to capture and how to share logs when asking for help, see xref:troubleshooting/service-logs.adoc[Service logs (troubleshooting)].
+
+If you cannot understand the logs, or otherwise need help, see the xref:troubleshooting/wheretofindhelp.adoc[support page].
diff --git a/docs/modules/ROOT/partials/install/post_generic.adoc b/docs/modules/ROOT/partials/install/post_generic.adoc
new file mode 100644
index 0000000..a6e54c2
--- /dev/null
+++ b/docs/modules/ROOT/partials/install/post_generic.adoc
@@ -0,0 +1,25 @@
+== Post installation
+
+You will need to write a basic configuration file before OliveTin will startup.
+
+Edit the basic config file at `config.yaml` with the following contents;
+
+.The most simple `config.yaml` file.
+[source,yaml]
+----
+actions:
+ - title: "Hello world!"
+ shell: echo 'Hello World!'
+----
+
+Start OliveTin, preferably via a terminal. On Unix based systems (eg MacOS, BSD, Linux, etc) you can just run `./OliveTin`. On Windows you would run `OliveTin.exe` in windows terminal.
+
+include::partial$install/to_config.adoc[]
+
+== Troubleshooting installations
+
+If you are having problems, OliveTin will log it's status on startup. Check the log messages in the terminal.
+
+For tips on capturing and sharing that output, see xref:troubleshooting/service-logs.adoc[Service logs (troubleshooting)].
+
+If you cannot understand the logs, or otherwise need help, see the xref:troubleshooting/wheretofindhelp.adoc[support page].
diff --git a/docs/modules/ROOT/partials/install/post_systemd.adoc b/docs/modules/ROOT/partials/install/post_systemd.adoc
new file mode 100644
index 0000000..8d4c823
--- /dev/null
+++ b/docs/modules/ROOT/partials/install/post_systemd.adoc
@@ -0,0 +1,53 @@
+== Post installation
+
+You will need to write a basic configuration file before OliveTin will startup.
+
+Create the following basic config file at `/etc/OliveTin/config.yaml` with the
+following contents;
+
+.The most simple `config.yaml` file.
+[source,yaml]
+----
+actions:
+ - title: "Hello world!"
+ shell: echo 'Hello World!'
+----
+
+Now that you have a configuration file, and OliveTin is installed, start it;
+
+.Start the service (only needed once)
+[source,shell]
+----
+user@host: systemctl enable --now OliveTin
+----
+
+If you are running a firewall on your server, like firewalld, you will need to
+open port 1337;
+
+[source,shell]
+----
+user@host: firewall-cmd --add-port 1337/tcp --permanent
+user@host: firewall-cmd --reload
+----
+
+include::partial$install/to_config.adoc[]
+
+== Troubleshooting systemd installations
+
+If you are having problems, you can check if OliveTin is running like this;
+
+[source,shell]
+----
+user@host: systemctl status OliveTin
+----
+
+If the service has failed, scroll through the logs;
+
+[source,shell]
+----
+user@host: journalctl -eu OliveTin
+----
+
+If you cannot understand the logs, or otherwise need help, see the xref:troubleshooting/wheretofindhelp.adoc[support page].
+
+For more detail on what to capture and how to share logs when asking for help, see xref:troubleshooting/service-logs.adoc[Service logs (troubleshooting)].
diff --git a/docs/modules/ROOT/partials/install/to_config.adoc b/docs/modules/ROOT/partials/install/to_config.adoc
new file mode 100644
index 0000000..1f48e49
--- /dev/null
+++ b/docs/modules/ROOT/partials/install/to_config.adoc
@@ -0,0 +1,8 @@
+You should be able to browse to http://yourserver:1337 (or similar) to get to
+the web interface.
+
+If you see the OliveTin page popup in your browser, you are good to go! Here are some helpful next steps;
+
+* xref:action_execution/create_your_first.adoc[Create your first action]
+* xref:config.adoc[configuration section] for a list of all configuration options.
+
diff --git a/docs/modules/ROOT/partials/reverse-proxies/diagram.adoc b/docs/modules/ROOT/partials/reverse-proxies/diagram.adoc
new file mode 100644
index 0000000..fb37551
--- /dev/null
+++ b/docs/modules/ROOT/partials/reverse-proxies/diagram.adoc
@@ -0,0 +1,19 @@
+[mermaid,png]
+.Flow of an inbound network request
+....
+%%{init: {'theme': 'neutral'}}%%
+graph LR
+ A[Your Browser] -->|HTTPS 443/tcp| Z
+ Z["Proxy"] -->|HTTP 80/tcp| C
+ C["Single HTTP frontend"]
+ H["Prometheus"]
+ B["gRPC API"]
+
+ subgraph "OliveTin service"
+ C -->|/api/| D[REST API] --> B
+ C -->|/| E[webui]
+ C -->|/metrics/| H
+ end
+....
+
+:proxy!:
diff --git a/docs/modules/ROOT/partials/reverse-proxies/external-rest.adoc b/docs/modules/ROOT/partials/reverse-proxies/external-rest.adoc
new file mode 100644
index 0000000..5728990
--- /dev/null
+++ b/docs/modules/ROOT/partials/reverse-proxies/external-rest.adoc
@@ -0,0 +1,10 @@
+Note, because you are changing the default path (from `/` to `/OliveTin/`), you will need to tell the OliveTin webUI where to find the API.
+
+You need to also set `externalRestAddress` in your config.yaml like this;
+
+.OliveTin config.yaml
+[source,yaml]
+----
+externalRestAddress: http://myserver/OliveTin
+----
+
diff --git a/docs/modules/ROOT/partials/support.adoc b/docs/modules/ROOT/partials/support.adoc
new file mode 100644
index 0000000..8b3ef43
--- /dev/null
+++ b/docs/modules/ROOT/partials/support.adoc
@@ -0,0 +1,12 @@
+
+When something is wrong with the **WebUI** in the browser, capturing xref:troubleshooting/browser-console-logs.adoc[browser console logs] (even as a screenshot) helps diagnose the issue.
+
+When something is wrong with the **OliveTin process** (startup failures, actions, API, or auth), capturing xref:troubleshooting/service-logs.adoc[service logs] from Docker, Podman, or `journalctl` helps diagnose the issue.
+
+To get relatively quick access to help, **Discord** is where the chat community for OliveTin is. Note that this project is a free community open source project, and it relies on volenteers to spare their free time to help you. Please be patient and polite.
+
+image:icons/Discord.png[inline] link:https://discord.gg/jhYWWpNJ3v[Chat on Discord]
+
+If nobody is online, or you're not getting the right level of support, you can raise a ticket with the project's developers on GitHub. Again, please be patient and polite.
+
+image:icons/GitHub.png[inline] link:https://github.com/OliveTin/OliveTin/issues/new?assignees=&labels=support&template=support_request.md&title=[Open a support request on GitHub]
diff --git a/local-antora-playbook-ci.yml b/local-antora-playbook-ci.yml
new file mode 100644
index 0000000..b63927c
--- /dev/null
+++ b/local-antora-playbook-ci.yml
@@ -0,0 +1,21 @@
+# CI-only Antora playbook: faster and fewer network calls than local-antora-playbook.yml.
+# Skips Lunr; disables Kroki diagram fetching so diagram pages still parse without kroki.io.
+---
+site:
+ title: OliveTin docs
+ start_page: ROOT::index.adoc
+content:
+ sources:
+ - url: .
+ branches: HEAD
+ start_path: docs
+ui:
+ bundle:
+ url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/HEAD/raw/build/ui-bundle.zip?job=bundle-stable
+ snapshot: true
+asciidoc:
+ attributes:
+ kroki-fetch-diagram: false
+ extensions:
+ - '@asciidoctor/tabs'
+ - 'asciidoctor-kroki'
diff --git a/local-antora-playbook.yml b/local-antora-playbook.yml
new file mode 100644
index 0000000..0a8ac46
--- /dev/null
+++ b/local-antora-playbook.yml
@@ -0,0 +1,26 @@
+# Local / CI Antora playbook: AsciiDoc component under docs/.
+# Must live at repository root so content.sources url "." resolves to this git repo.
+# The published site is built from github.com/OliveTin/docs.olivetin.app.
+---
+site:
+ title: OliveTin docs
+ start_page: ROOT::index.adoc
+content:
+ sources:
+ - url: .
+ branches: HEAD
+ start_path: docs
+ui:
+ bundle:
+ url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/HEAD/raw/build/ui-bundle.zip?job=bundle-stable
+ snapshot: true
+antora:
+ extensions:
+ - '@antora/lunr-extension'
+asciidoc:
+ attributes:
+ kroki-fetch-diagram: true
+ kroki-default-format: png
+ extensions:
+ - '@asciidoctor/tabs'
+ - 'asciidoctor-kroki'