Compare commits

..

No commits in common. "092f3368709f7c8a1a95d1b28965a763e2a4fd83" and "a5348a99875cce23625911d5bcaf521e77234775" have entirely different histories.

13 changed files with 82 additions and 697 deletions

View file

@ -17,31 +17,12 @@ RUN yarn install --production false
COPY ui ./
RUN yarn build --dest /tmp/ovdashboard_ui/html
######################
# python preparation #
######################
ARG PYTHON_VERSION
FROM python:${PYTHON_VERSION} as uvicorn-gunicorn
# where credit is due ...
LABEL maintainer="Sebastian Ramirez <tiangolo@gmail.com>"
WORKDIR /usr/local/share/uvicorn-gunicorn
# install uvicorn-gunicorn
COPY "./deploy/mini-tiangolo/" "."
RUN set -ex; \
chmod +x start.sh; \
python3 -m pip --no-cache-dir install gunicorn;
CMD ["/usr/local/share/uvicorn-gunicorn/start.sh"]
###########
# web app #
###########
FROM uvicorn-gunicorn AS production
ARG PYTHON_VERSION
FROM tiangolo/uvicorn-gunicorn:python${PYTHON_VERSION} AS production
# env setup
WORKDIR /usr/local/src/ovdashboard_api
@ -57,13 +38,13 @@ RUN set -ex; \
export DEBIAN_FRONTEND=noninteractive; \
apt-get update; apt-get install --yes --no-install-recommends \
libmagic1 \
# need to build hiredis
gcc \
libc-dev \
; rm -rf /var/lib/apt/lists/*; \
\
# remove example app
rm -rf /app; \
\
# install ovdashboard_api
python3 -m pip --no-cache-dir install ./
python -m pip --no-cache-dir install ./
# add prepared ovdashboard_ui
COPY --from=build-ui /tmp/ovdashboard_ui /usr/local/share/ovdashboard_ui

106
README.md
View file

@ -12,7 +12,7 @@ All that matters, at one glance! <br />
*Date and Time &ndash; Upcoming Events &ndash; Public Announcements &ndash; News &ndash; Pictures*
- **Easy Install** <br />
Set up a RaspberryPi, run the [installer script](./deploy/install.sh), done!
Set up a RaspberryPi, run the [installer script](TODO), done!
- **DAV Server Interface** <br />
Update your content anytime, from anywhere!
@ -36,78 +36,22 @@ The Dashboard UI is created using [Vue](https://vuejs.org/) and the [Vuetify](ht
## Quick Start
### Prerequisites
Make sure you have a WebDAV and CalDAV account available.
For an all-in-one solution, consider setting up an account on a [Nextcloud](https://nextcloud.com/) instance! <br />
On your WebDAV account, create a resource (directory) named `ovdashboard`[^1].
On your WebvDAV account, create a resource (directory) named `ovdashboard`.
Your target device should be a Raspberry Pi Model 3 or later[^2]. You will need some accessories:
- microSD card, class 10 or UHS (min. 8 GB)
- network connectivity (bring WiFi credentials if applicable)
- connection to a HDMI screen
The intended installation method is as follows:
1. Have a Raspberry Pi (3 or later) connected to a HDMI screen
1. Boot up "Raspberry Pi OS" (64 bit) and connect the Pi to your local network
1. Download (and review) the [OVDashboard installer](TODO), then run it from a terminal
> If you feel adventurous and want to skip the script review, run `curl --proto '=https' --tlsv1.2 -sSf 'TODO' | sh` instead.
1. Reboot the Raspberry Pi
It is also heavily advisable that you log into your device using SSH, so you should get another device (PC, tablet or smartphone) onto the same network as your OVDashboard.
For a better understanding of your newly created OVDashboard, refer to the [about section](TODO).
[^1]: if named differently, you will need to adjust your compose file later on
[^2]: other devices will also work, but might require extra installation steps
### Install Base System
OVDashboard is designed to run on a `DietPi` installation. Full installation documentation is available [at dietpi.com](https://dietpi.com/docs/deploy/). To quickly get up and running:
1. Download Image from [dietpi.com/#download](https://dietpi.com/#download)
1. Uncompress Image and flash onto SD card you might need "7zip", "balenaEtcher" and/or other tools
1. Check the SD card, open "dietpi.txt" and change some options (full documentation [here](https://dietpi.com/docs/usage/#options-within-the-file)):
- For WiFi, use `AUTO_SETUP_NET_WIFI_ENABLED=1` and `AUTO_SETUP_NET_WIFI_COUNTRY_CODE=DE`, and also edit "dietpi-wifi.txt" like `aWIFI_SSID[0]='OV WLAN'` and `aWIFI_KEY[0]='Strong_pa55w0rd'`
- System options, e.g. `AUTO_SETUP_AUTOMATED=1`, `AUTO_SETUP_NET_HOSTNAME=OVDashboard`, `AUTO_SETUP_GLOBAL_PASSWORD=dietpi`, `AUTO_SETUP_LOCALE=de_DE.UTF-8`, `AUTO_SETUP_KEYBOARD_LAYOUT=de`, `AUTO_SETUP_TIMEZONE=Europe/Berlin`, `CONFIG_SERIAL_CONSOLE_ENABLE=0`
1. Be sure to at least change the password (and remember it 🙂️), then put the SD card into your device and boot it. Let the first time setup finish, it will take a bit. It will let you know when it is done!
1. Log into your device using SSH. By default, that's user name `root` and password `dietpi`
### Install OVDashboard
Download (and review) the [OVDashboard install script](//code.yavook.de/OEKZident.de/ovdashboard/raw/branch/master/deploy/install.sh), then run it from a terminal.
This can all be done after logging into your prepared device:
- The safe way:
1. download: `wget 'https://code.yavook.de/OEKZident.de/ovdashboard/raw/branch/master/deploy/install.sh'`
1. read/edit: `nano install.sh`
1. execute: `sh install.sh`
- If you feel adventurous and do not want to review the script, just run `sh <( curl --proto '=https' --tlsv1.2 -sSf 'https://code.yavook.de/OEKZident.de/ovdashboard/raw/branch/master/deploy/install.sh' )`
> There will be some prompts during installation.
>
> - DietPi might ask: "Would you like DietPi to apply the recommended GPU memory split?". Choose **Yes**.
> - DietPi will ask: "Would you like to configure the DietPi-AutoStart option?". **Cancel** that, this choice does not matter as it is changed by the installer.
> - DietPi will ask: "Would you like to join DietPi-Survey?". It's up to you, but I'd suggest to **opt OUT**.
> - The installer will ask for the connected screen's resolution. The default values should be fine, you can likely just hit **Return** here.
> - The installer will ask for "display languages". This will affect some details on the connected screen. <br />
For German, enter `de-DE,de,en-US,en`.
> - The installer will ask you to "review the Docker Compose file" before starting the services. You will want to edit `WEBDAV__HOST`, `WEBDAV__USERNAME` and `WEBDAV__PASSWORD` at least. <br />
Refer to [the "Settings"](TODO) for the full list of options.
Afterwards, reboot your device (`reboot` in the terminal).
If the install was successful, your OVDashboard should be showing up on the connected screen after rebooting.
> You will also be able to view your OVDashboard on any webbrowser in your network using `http://<device-ip>`. <br />
> The device IP is displayed in the lower right region of the OVDashboard.
For a better understanding of your newly created OVDashboard, refer to the [about section](#about-the-default-ovdashboard-deployment).
## Configuration
### "Config" in your WebDAV share: `config.txt`
<!-- TODO -->
### "Settings" on your Device: `/opt/ovdashboard/docker-compose.yml`
<!-- TODO -->
## Updating your Device
<!-- TODO `/opt/ovdashboard` -->
## Setup for development and contribution
@ -116,9 +60,31 @@ Refer to the specific README files for [the API](./api/README.md) and [the UI](.
## About the "default" OVDashboard deployment
Running the installer script carries out the following actions:
The default deployment is made up of three parts: The **installer** which runs once, prepares your device for OVDashboard usage and deploys the **client** and the **service**.
- install Chromium-Browser, Docker and Docker Compose
- create the OVDashboard project at `/opt/ovdashboard`
- start the OVDashboard project, this deploys the `ovdashboard` service from [`code.yavook.de`](https://code.yavook.de/OEKZident.de/-/packages/container/ovdashboard) and a [`redis` instance](https://redis.io/) to your device
- set up your device to auto-boot into Chromium "kiosk" mode to display the local OVDashboard
The **client** will connect to and display your OVDashboard.
The **service** will run continuously and makes sure your OVDashboard is up to date.
### The OVDashboard installer will:
- install some general-purpose packages
- make you set a new password, if you haven't yet
- install and set up Docker Engine and Mozilla Firefox
- (optionally) enable SSH and/or VNC access to your device
- create the OVDashboard configuration directory `/usr/local/etc/ovdashboard` on your device
- set up the OVDashboard service
- (optionally) set up your device to auto-run the OVDashboard client
### The OVDashboard service will:
- regularly pull the latest `docker` image from [`TODO`](TODO), containing both the OVDashboard API and UI
- ensure a container of that image is always running, using your OVDashboard configuration directory
### The OVDashboard client will:
- open Mozilla Firefox in full-screen mode and browse to the local OVDashboard UI
- enable auto-hiding your mouse cursor

View file

@ -18,11 +18,12 @@ class DAVSettings(BaseModel):
Connection to a DAV server.
"""
protocol: str = "https"
host: str = "example.com"
protocol: str | None = None
host: str | None = None
path: str | None = None
username: str = "ovd_user"
password: str = "password"
username: str | None = None
password: str | None = None
cache_ttl: int = 60 * 10
@ -32,34 +33,21 @@ class DAVSettings(BaseModel):
Combined DAV URL.
"""
return f"{self.protocol}://{self.host}"
return f"{self.protocol}://{self.host}{self.path}"
_DAV_DEFAULT = DAVSettings().model_dump()
class CalDAVSettings(DAVSettings):
"""
Connection to a CalDAV server.
"""
path: str = "/remote.php/dav"
@property
def url(self) -> str:
"""
Combined DAV URL.
"""
return f"{super().url}{self.path}"
class WebDAVSettings(CalDAVSettings):
class WebDAVSettings(DAVSettings):
"""
Connection to a WebDAV server.
"""
path: str = "/remote.php/webdav"
protocol: str = "https"
host: str = "example.com"
path: str = "/remote.php/dav"
prefix: str = "/ovdashboard"
username: str = "ovd_user"
password: str = "password"
config_filename: str = "config.txt"
@ -74,10 +62,7 @@ class WebDAVSettings(CalDAVSettings):
Combined DAV URL.
"""
return f"{super().url}{self.prefix}"
_WEBDAV_DEFAULT = WebDAVSettings().model_dump()
return f"{self.protocol}://{self.host}{self.path}{self.prefix}"
class RedisSettings(BaseModel):
@ -147,7 +132,7 @@ class Settings(BaseSettings):
# caldav settings
#####
caldav: CalDAVSettings = CalDAVSettings()
caldav: DAVSettings = DAVSettings()
#####
# redis settings
@ -156,19 +141,37 @@ class Settings(BaseSettings):
redis: RedisSettings = RedisSettings()
@model_validator(mode="before")
@classmethod
def validate_dav_settings(cls, data) -> dict[str, Any]:
assert isinstance(data, dict)
# ensure both settings dicts are created
for key in ("webdav", "caldav"):
data[key] = data.get(key, {})
if key not in data:
data[key] = {}
for key in _DAV_DEFAULT:
# if "webdav" value is not specified, use default value
data["webdav"][key] = data["webdav"].get(key, _WEBDAV_DEFAULT[key])
default_dav = DAVSettings(
protocol="https",
host="example.com",
username="ovdashboard",
password="secret",
).model_dump()
for key in default_dav:
# if "webdav" value is not specified, use default
if key not in data["webdav"] or data["webdav"][key] is None:
data["webdav"][key] = default_dav[key]
# if "caldav" value is not specified, use "webdav" value
data["caldav"][key] = data["caldav"].get(key, data["webdav"][key])
if key not in data["caldav"] or data["caldav"][key] is None:
data["caldav"][key] = data["webdav"][key]
# add default "path"s if None
if data["webdav"]["path"] is None:
data["webdav"]["path"] = "/remote.php/webdav"
if data["caldav"]["path"] is None:
data["caldav"]["path"] = "/remote.php/dav"
return data

View file

@ -63,23 +63,13 @@ async def find_images_by_prefix(
response_class=StreamingResponse,
)
async def get_image_by_prefix(
cfg: Config = Depends(get_config),
remote_path: str = Depends(RP_IMAGE),
name: str = Depends(LM_IMAGE.getter.func),
) -> StreamingResponse:
cfg = await get_config()
img = Image.open(BytesIO(await WebDAV.read_bytes(f"{remote_path}/{name}")))
img_buffer = BytesIO()
width, height = img.size
target_height = cfg.image.height
target_width = int(width * target_height / height)
img = img.resize(
size=(target_width, target_height),
resample=Image.LANCZOS,
)
img.save(img_buffer, **cfg.image.save_params)
img_buffer.seek(0)

46
api/poetry.lock generated
View file

@ -522,17 +522,6 @@ files = [
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
]
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]]
name = "isort"
version = "5.12.0"
@ -798,21 +787,6 @@ files = [
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"]
[[package]]
name = "pluggy"
version = "1.3.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
{file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pycodestyle"
version = "2.11.1"
@ -987,26 +961,6 @@ files = [
{file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"},
]
[[package]]
name = "pytest"
version = "7.4.3"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"},
{file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
[package.extras]
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "python-dateutil"
version = "2.8.2"

View file

@ -26,7 +26,6 @@ black = "^23.10.1"
flake8 = "^6.1.0"
flake8-isort = "^6.1.0"
types-cachetools = "^5.3.0.6"
pytest = "^7.4.3"
[build-system]
build-backend = "poetry.core.masonry.api"

View file

@ -1,224 +0,0 @@
import os
import pytest
from ovdashboard_api.core.settings import Settings
@pytest.fixture(autouse=True, scope="function")
def patch_settings_env_file(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(os, "environ", {})
monkeypatch.delitem(Settings.model_config, "env_file")
def test_empty():
s = Settings.model_validate({})
assert s.log_level == "INFO"
assert s.production_mode is False
assert s.ui_directory == "/usr/local/share/ovdashboard_ui/html"
assert s.ping_host == "1.0.0.0"
assert s.ping_port == 1
assert s.openapi_url == "/api/openapi.json"
assert s.docs_url == "/api/docs"
assert s.redoc_url == "/api/redoc"
# webdav
assert s.webdav.protocol == "https"
assert s.webdav.host == "example.com"
assert s.webdav.username == "ovd_user"
assert s.webdav.password == "password"
assert s.webdav.cache_ttl == 600
assert s.webdav.path == "/remote.php/webdav"
assert s.webdav.config_filename == "config.txt"
assert s.webdav.disable_check is False
assert s.webdav.retries == 20
assert s.webdav.retry_delay == 30
assert s.webdav.prefix == "/ovdashboard"
# caldav
assert s.caldav.protocol == "https"
assert s.caldav.host == "example.com"
assert s.caldav.username == "ovd_user"
assert s.caldav.password == "password"
assert s.caldav.cache_ttl == 600
assert s.caldav.path == "/remote.php/dav"
def test_prod():
s = Settings.model_validate({"production_mode": True})
assert s.log_level == "INFO"
assert s.production_mode is True
assert s.openapi_url is None
assert s.docs_url is None
assert s.redoc_url is None
def test_set_caldav():
s = Settings.model_validate(
{
"caldav": {
"protocol": "cd_protocol",
"host": "cd_host",
"username": "cd_username",
"password": "cd_password",
"cache_ttl": "0",
"path": "cd_path",
}
}
)
# webdav
assert s.webdav.protocol == "https"
assert s.webdav.host == "example.com"
assert s.webdav.username == "ovd_user"
assert s.webdav.password == "password"
assert s.webdav.cache_ttl == 600
assert s.webdav.path == "/remote.php/webdav"
assert s.webdav.config_filename == "config.txt"
assert s.webdav.disable_check is False
assert s.webdav.retries == 20
assert s.webdav.retry_delay == 30
assert s.webdav.prefix == "/ovdashboard"
# caldav
assert s.caldav.protocol == "cd_protocol"
assert s.caldav.host == "cd_host"
assert s.caldav.username == "cd_username"
assert s.caldav.password == "cd_password"
assert s.caldav.cache_ttl == 0
assert s.caldav.path == "cd_path"
def test_set_webdav():
s = Settings.model_validate(
{
"webdav": {
"protocol": "wd_protocol",
"host": "wd_host",
"username": "wd_username",
"password": "wd_password",
"cache_ttl": "99",
"path": "wd_path",
"config_filename": "wd_config_filename",
"disable_check": "true",
"retries": "99",
"retry_delay": "99",
"prefix": "wd_prefix",
}
}
)
# webdav
assert s.webdav.protocol == "wd_protocol"
assert s.webdav.host == "wd_host"
assert s.webdav.username == "wd_username"
assert s.webdav.password == "wd_password"
assert s.webdav.cache_ttl == 99
assert s.webdav.path == "wd_path"
assert s.webdav.config_filename == "wd_config_filename"
assert s.webdav.disable_check is True
assert s.webdav.retries == 99
assert s.webdav.retry_delay == 99
assert s.webdav.prefix == "wd_prefix"
# caldav
assert s.caldav.protocol == "wd_protocol"
assert s.caldav.host == "wd_host"
assert s.caldav.username == "wd_username"
assert s.caldav.password == "wd_password"
assert s.caldav.cache_ttl == 99
assert s.caldav.path == "/remote.php/dav"
def test_set_caldav_webdav():
s = Settings.model_validate(
{
"webdav": {
"protocol": "wd_protocol",
"host": "wd_host",
"username": "wd_username",
"password": "wd_password",
"cache_ttl": "99",
"path": "wd_path",
"config_filename": "wd_config_filename",
"disable_check": "true",
"retries": "99",
"retry_delay": "99",
"prefix": "wd_prefix",
},
"caldav": {
"protocol": "cd_protocol",
"host": "cd_host",
"username": "cd_username",
"password": "cd_password",
"cache_ttl": "0",
"path": "cd_path",
},
}
)
# webdav
assert s.webdav.protocol == "wd_protocol"
assert s.webdav.host == "wd_host"
assert s.webdav.username == "wd_username"
assert s.webdav.password == "wd_password"
assert s.webdav.cache_ttl == 99
assert s.webdav.path == "wd_path"
assert s.webdav.config_filename == "wd_config_filename"
assert s.webdav.disable_check is True
assert s.webdav.retries == 99
assert s.webdav.retry_delay == 99
assert s.webdav.prefix == "wd_prefix"
# caldav
assert s.caldav.protocol == "cd_protocol"
assert s.caldav.host == "cd_host"
assert s.caldav.username == "cd_username"
assert s.caldav.password == "cd_password"
assert s.caldav.cache_ttl == 0
assert s.caldav.path == "cd_path"

View file

@ -1,44 +0,0 @@
#!/bin/sh
script="$( readlink -f "${0}" )"
script_dir="$( dirname "${script}" )"
git_version="$( \
git rev-parse --abbrev-ref HEAD \
| cut -d '/' -f 2
)"
api_version="$( \
grep '^version' "${script_dir}/../../api/pyproject.toml" \
| sed -E 's/^version[^0-9]*((0|[1-9][0-9]*)[0-9\.]*[0-9]).*$/\1/'
)"
ui_version="$( \
python3 -c 'import sys, json; print(json.load(sys.stdin)["version"])' \
< "${script_dir}/../../ui/package.json" \
)"
install_version="$( \
grep '^ovd_version' "${script_dir}/../install.sh" \
| sed -E 's/^ovd_version[^0-9]*((0|[1-9][0-9]*)[0-9\.]*[0-9]).*$/\1/'
)"
compose_version="$( \
grep 'image: code\.yavook\.de/oekzident\.de/ovdashboard' "${script_dir}/../docker-compose.yml" \
| sed -E 's/.*code\.yavook\.de\/oekzident\.de\/ovdashboard[^0-9]*((0|[1-9][0-9]*)[0-9\.]*[0-9]).*$/\1/'
)"
if [ "${git_version}" = "${api_version}" ] \
&& [ "${git_version}" = "${ui_version}" ] \
&& [ "${git_version}" = "${install_version}" ] \
&& [ "${git_version}" = "${compose_version}" ]; then
mark="✅️"
else
mark="❌️"
fi
echo "git: ${git_version}, api: ${api_version}, ui: ${ui_version}"
echo "installer: ${install_version}, compose: ${compose_version}"
echo ">>>>> RESULT: ${mark} <<<<<"
[ "${mark}" = "✅️" ] || exit 1

View file

@ -1,17 +0,0 @@
#!/bin/sh
script="$( readlink -f "${0}" )"
script_dir="$( dirname "${script}" )"
# shellcheck disable=SC1091
. "${script_dir}/check_version"
# defined in `check_version` script
# shellcheck disable=SC2154
echo "${git_version}" >/dev/null
docker buildx build \
--pull --push \
--tag "code.yavook.de/oekzident.de/ovdashboard:${git_version}" \
--platform "linux/amd64,linux/arm64" \
"${script_dir}/../.."

View file

@ -1,38 +0,0 @@
services:
# shared cache
redis:
image: redis:7-alpine
restart: always
pull_policy: always
# necessary for "host" network deployment,
# but only listen on localhost
ports:
- "127.0.0.1:6379:6379"
app:
image: code.yavook.de/oekzident.de/ovdashboard:0.1.0
restart: always
pull_policy: always
depends_on:
- redis
# "app" container needs host ip
network_mode: host
user: root
environment:
# necessary for "host" network deployment
PORT: "80"
REDIS__HOST: "localhost"
# >>>>> USER VARIABLES <<<<<
# you will want to adjust these!
TZ: "Europe/Berlin"
WEBDAV__HOST: "example.com"
WEBDAV__PATH: "/remote.php/webdav"
WEBDAV__PREFIX: "/ovdashboard"
WEBDAV__USERNAME: "ovd_user"
WEBDAV__PASSWORD: "password"

View file

@ -1,98 +0,0 @@
#!/bin/sh
#########
# start #
#########
# env setup
ovd_version="0.1.0"
export DEBIAN_FRONTEND="noninteractive"
set -e
# banner
echo "Installer for OVDashboard ${ovd_version}"
echo "Waiting 10 seconds, press Ctrl+C to cancel installation ..."
sleep 10
#################
# prerequisites #
#################
# 134: docker with compose
# 113: chromium browser
/boot/dietpi/dietpi-software install 134 113
# htpdate (timesync in restricted networks)
# unclutter (hides mouse cursor)
apt-get update && apt-get install --yes --no-install-recommends \
htpdate unclutter
# activate unclutter
echo '/usr/bin/unclutter -idle 0.1 &' > /etc/chromium.d/dietpi-unclutter
# chromium window size
echo "Please enter your screen resolution!"
screen_x="$( cut -d ',' -f 1 '/sys/class/graphics/fb0/virtual_size' )"
printf "Width [default: %d]: " "${screen_x}"
read -r screen_x_in
screen_x="${screen_x_in:-$screen_x}"
sed -ri "s/^(SOFTWARE_CHROMIUM_RES_X=)[0-9]+$/\1${screen_x}/" '/boot/dietpi.txt'
screen_y="$( cut -d ',' -f 2 '/sys/class/graphics/fb0/virtual_size' )"
printf "Height [default: %d]: " "${screen_y}"
read -r screen_y_in
screen_y="${screen_y_in:-$screen_y}"
sed -ri "s/^(SOFTWARE_CHROMIUM_RES_Y=)[0-9]+$/\1${screen_y}/" '/boot/dietpi.txt'
# chromium autostart
sed -ri "s/^(AUTO_SETUP_AUTOSTART_LOGIN_USER=).+$/\1dietpi/" '/boot/dietpi.txt' # run as "dietpi"
sed -ri "s/^(SOFTWARE_CHROMIUM_AUTOSTART_URL=).+$/\1http:\/\/localhost\//" '/boot/dietpi.txt' # open "localhost"
/boot/dietpi/dietpi-autostart 11 # 11: magic number for chromium autostart
# chromium language
display_lang="en-US,en"
printf "Enter display language(s) [default: %s]: " "${display_lang}"
read -r display_lang_in
display_lang="${display_lang_in:-${display_lang}}"
sudo -u dietpi mkdir -p '/home/dietpi/.config/chromium/Default'
echo '{"intl":{"selected_languages":"'"${display_lang}"'"}}' \
| sudo -u dietpi tee '/home/dietpi/.config/chromium/Default/Preferences' \
> /dev/null
#######
# app #
#######
mkdir -p /opt/ovdashboard
# prepare compose project
curl \
--proto "=https" --tlsv1.2 -sSf \
--output "/opt/ovdashboard/docker-compose.yml" \
"https://code.yavook.de/OEKZident.de/ovdashboard/raw/tag/v${ovd_version}/deploy/docker-compose.yml"
docker compose \
--project-directory "/opt/ovdashboard" \
pull
# review compose file
echo "Please review the Docker Compose file before continuing! [hit Return]"
read -r _RETURN
nano "/opt/ovdashboard/docker-compose.yml"
# start server
docker compose \
--project-directory "/opt/ovdashboard" \
up --detach
############
# finalize #
############
echo ""
echo "#########################"
echo "# OVDashboard Installed #"
echo "#########################"
echo ""
echo "You can now reboot your device."

View file

@ -1,67 +0,0 @@
import json
import multiprocessing
import os
workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1")
max_workers_str = os.getenv("MAX_WORKERS")
use_max_workers = None
if max_workers_str:
use_max_workers = int(max_workers_str)
web_concurrency_str = os.getenv("WEB_CONCURRENCY", None)
host = os.getenv("HOST", "0.0.0.0")
port = os.getenv("PORT", "80")
bind_env = os.getenv("BIND", None)
use_loglevel = os.getenv("LOG_LEVEL", "info")
if bind_env:
use_bind = bind_env
else:
use_bind = f"{host}:{port}"
cores = multiprocessing.cpu_count()
workers_per_core = float(workers_per_core_str)
default_web_concurrency = workers_per_core * cores
if web_concurrency_str:
web_concurrency = int(web_concurrency_str)
assert web_concurrency > 0
else:
web_concurrency = max(int(default_web_concurrency), 2)
if use_max_workers:
web_concurrency = min(web_concurrency, use_max_workers)
accesslog_var = os.getenv("ACCESS_LOG", "-")
use_accesslog = accesslog_var or None
errorlog_var = os.getenv("ERROR_LOG", "-")
use_errorlog = errorlog_var or None
graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "120")
timeout_str = os.getenv("TIMEOUT", "120")
keepalive_str = os.getenv("KEEP_ALIVE", "5")
# Gunicorn config variables
loglevel = use_loglevel
workers = web_concurrency
bind = use_bind
errorlog = use_errorlog
worker_tmp_dir = "/dev/shm"
accesslog = use_accesslog
graceful_timeout = int(graceful_timeout_str)
timeout = int(timeout_str)
keepalive = int(keepalive_str)
# For debugging and testing
log_data = {
"loglevel": loglevel,
"workers": workers,
"bind": bind,
"graceful_timeout": graceful_timeout,
"timeout": timeout,
"keepalive": keepalive,
"errorlog": errorlog,
"accesslog": accesslog,
# Additional, non-gunicorn variables
"workers_per_core": workers_per_core,
"use_max_workers": use_max_workers,
"host": host,
"port": port,
}
print(json.dumps(log_data))

View file

@ -1,20 +0,0 @@
#!/bin/sh
set -e
MODULE_NAME=${MODULE_NAME:-"app.main"}
VARIABLE_NAME=${VARIABLE_NAME:-"app"}
export APP_MODULE="${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"}"
export GUNICORN_CONF="${GUNICORN_CONF:-"/usr/local/share/uvicorn-gunicorn/gunicorn_conf.py"}"
export WORKER_CLASS="${WORKER_CLASS:-"uvicorn.workers.UvicornWorker"}"
if [ -f "${PRE_START_PATH}" ] ; then
echo "Running script ${PRE_START_PATH}"
# shellcheck disable=SC1090
. "${PRE_START_PATH}"
fi
# Start Gunicorn
exec gunicorn \
-k "${WORKER_CLASS}" \
-c "${GUNICORN_CONF}" \
"${APP_MODULE}"