Files
Anthias/docs/developer-documentation.md
Viktor Petersson f421130b24 refactor(server): collapse nginx + websocket containers into uvicorn (#2757)
* refactor(server): collapse nginx + websocket containers into uvicorn

Replace the nginx + gunicorn + gevent-websocket trio with a single
uvicorn ASGI server inside `anthias-server`:

* HTTP, /static/, /anthias_assets/, /static_with_mime/, and /hotspot
  are now served from Django (WhiteNoise + small file-serving views in
  `anthias_app/views_files.py` that re-implement nginx's IP allowlists).
* WebSockets move from a separate gevent process talking ZMQ to Django
  Channels with a Redis-backed channel layer, fanned out by celery via
  `channel_layer.group_send`.
* TLS termination is handled by uvicorn directly when SSL_CERTFILE /
  SSL_KEYFILE are set; `bin/enable_ssl.sh` now writes a compose
  override (no longer ansible) and a companion `bin/disable_ssl.sh`
  removes it. Cert + key live under `~/.anthias/ssl/`.
* `bin/upgrade_containers.sh` removes the legacy `anthias-nginx` and
  `anthias-websocket` containers on upgrade so they don't linger.
* Drop `gunicorn`, `gevent`, `gevent-websocket`, and the `websocket`
  uv group from `pyproject.toml`; add `channels`, `channels-redis`,
  `daphne`, `uvicorn[standard]`, and `whitenoise`.

Notes on hardening: `--forwarded-allow-ips` defaults to off so the IP
allowlist can't be bypassed via a spoofed `X-Forwarded-For`; operators
behind a reverse proxy can opt in via the `FORWARDED_ALLOW_IPS` env
var. Backup uploads previously sized by nginx's `client_max_body_size
4G` are preserved by setting `DATA_UPLOAD_MAX_MEMORY_SIZE = None`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address review feedback on uvicorn migration

* Drop USE_X_FORWARDED_HOST (inconsistent with the deliberate
  --forwarded-allow-ips hardening; without a proxy, X-Forwarded-Host is
  client-controlled).
* Remove daphne — uvicorn runs production and the test environment now
  uses it too (bin/prepare_test_environment.sh).
* Replace _safe_join's parents-membership check with Path.is_relative_to.
* Drop AllowedHostsOriginValidator wrapper (no-op under ALLOWED_HOSTS=['*'])
  and document where to put it back if hosts are ever locked down.
* Rename DOCKER_CIDR → DOCKER_BRIDGE_CIDR with a comment that this is
  defense-in-depth, not a real perimeter (LAN clients via the published
  port also appear in 172.16/12).
* Add anthias_app/tests.py covering the IP allowlists, mime override,
  hotspot gating, and traversal/symlink rejection in _safe_join (17 tests).
* Note the single-worker ZmqPublisher bind constraint in start_server.sh
  so a future scale-up doesn't EADDRINUSE on tcp://0.0.0.0:10001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(security): clear SonarCloud hotspots on uvicorn migration

* Restrict views_files.anthias_assets / static_with_mime / hotspot to
  GET via @require_GET (Sonar S3752, x3): they are read-only file
  servers and should reject other methods at the view boundary.
* Mark RFC1918 / Docker-bridge CIDR literals as NOSONAR S1313 (x4):
  they are intentional, well-known private network ranges.
* Mark `http://*` in CSRF_TRUSTED_ORIGINS as NOSONAR S5332 with a
  comment explaining devices ship over HTTP and operators opt into TLS
  via bin/enable_ssl.sh.

Existing 17 view tests continue to pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: clear remaining static-analysis findings

* ruff format -- the previous tests.py reformatted itself; CI's
  `ruff format --check` now passes.
* CodeQL py/path-injection on _safe_join: rewrite using
  os.path.realpath + os.path.commonpath, which CodeQL recognises as a
  sanitiser for path-injection sinks. Behaviour is identical to the
  Path.is_relative_to version (both reject `..` and symlink escapes;
  the 17 tests in anthias_app/tests.py still pass).
* SonarCloud NOSONAR markers: switch to the codebase's bare `# NOSONAR`
  form (matches host_agent.py and tests/test_backup_helper.py); the
  earlier `# NOSONAR <rule>` form was not being honoured.
* Centralise the test-fixture IPs in module-level constants so S1313
  is suppressed in one place rather than at every callsite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(security): inline path-injection check in views

CodeQL only treats os.path.commonpath as a sanitiser when the check
sits in the same function as the file-system sink — calling
_safe_join() from a separate function still leaves the open()/isfile()
sinks tainted (4 alerts on PR #2757).

Repeat the realpath + commonpath check inline in anthias_assets and
static_with_mime so CodeQL can prove the post-check path stays under
the configured root. _safe_join is kept for the SafeJoinTest unit
tests and as a documented helper.

Existing 17 tests in anthias_app/tests.py continue to pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(security): use realpath+startswith path sanitiser for CodeQL

CodeQL's path-injection model recognises the canonical
`realpath(...).startswith(base + sep)` pattern but apparently not
`os.path.commonpath(...) == root` in this codepath. Switch the inline
check in anthias_assets and static_with_mime to startswith so the
analyser can prove the post-check path stays under the configured
root.

Behaviour is identical: traversal and symlink-escape still 404
(verified by SafeJoinTest + view tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address Copilot review feedback

* lib/utils.py imported channels/asgiref at module level. The viewer
  container imports lib.utils via viewer/__init__.py but its uv
  dependency group does not ship channels, so the viewer would
  ImportError on startup. Move the channels imports into
  YoutubeDownloadThread.run() (server/celery-only path) so lib.utils
  remains importable from the viewer.
* Drop the unused _safe_join() helper and its three SafeJoinTest
  cases — the views inline a realpath+startswith sanitiser (CodeQL
  needs the check in the same function as the sink), and the helper
  was only being exercised in isolation. Add an equivalent
  symlink-escape test against anthias_assets so the actual code path
  used by the views is covered.
* Refresh the anthias_django/settings.py docstring + Django doc URLs
  from /3.2/ → /4.2/ to match the pinned Django version.

15 view tests pass (was 17 — lost 3 SafeJoinTest + gained 1 symlink
test against the real view).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: refresh architecture diagram for uvicorn migration

Drop the anthias-nginx and anthias-websocket nodes (and their edges)
from docs/d2/anthias-diagram-overview.d2 — the user now talks
directly to anthias-server (uvicorn handling HTTP + /ws), Celery
fans out asset-update events through the Redis-backed Channels
layer, and the viewer fetches media from anthias-server over HTTP.

Regenerate the SVG with d2 v0.7.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address Copilot SSL + CSRF / WS-origin feedback

* Dual uvicorn listeners when SSL is enabled (Copilot #1, #2). HTTP on
  $HTTP_PORT (default 8080) for inter-container traffic — viewer +
  webview hit anthias-server over plain HTTP on the Docker network and
  cannot validate uvicorn's self-signed cert. HTTPS on $HTTPS_PORT
  (default 8443) for external clients. bin/enable_ssl.sh now appends
  443:8443 to the compose ports list (instead of using `!override` to
  swap 80:8080 for 443:8080), so port 80 stays available for backward
  compatibility and the Docker-network HTTP port keeps working.
* Drop CSRF_TRUSTED_ORIGINS = ['http://*', 'https://*'] (Copilot #3).
  Verified via Django shell: those leading wildcards are ignored by
  Django 4.2 (only subdomain wildcards like https://*.example.com are
  honoured), so the setting was a no-op. Same-origin POSTs still pass
  through Django's built-in Origin/Host check.
* Re-add channels.security.websocket.AllowedHostsOriginValidator to
  the WebSocket router (Copilot #5). Currently a no-op under
  ALLOWED_HOSTS=['*'], but tightening ALLOWED_HOSTS later will now
  also tighten /ws.

Smoke test (dev + SSL override):
- HTTP  http://localhost:8000/      -> 200
- HTTPS https://localhost:8443/     -> 200
- HTTP  http://localhost:8443/      -> 000 (TLS-only, expected)
- internal http://localhost:8080/   -> 200
- 15 view tests still pass.

Note: Copilot #4 (Docker-bridge CIDR is bypassable via the published
port) is documented in views_files.py as defense-in-depth and matches
the original nginx posture; switching to app-layer auth is out of
scope for this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(ssl): switch from in-uvicorn TLS to a Caddy sidecar

The previous SSL implementation gave anthias-server two uvicorn
listeners (HTTP + HTTPS) so the viewer/webview could keep talking
plain HTTP over the Docker network while external clients got TLS.
That dual-listener dance is non-zero overhead and complicates signal
handling. Switch to the standard reverse-proxy pattern instead.

When SSL is enabled by bin/enable_ssl.sh:

* anthias-server stays a single uvicorn listener on plain HTTP 8080
  (no SSL_CERTFILE/SSL_KEYFILE knobs, no dual-port logic).
* A Caddy sidecar (caddy:2-alpine, only present when the override is
  installed) terminates TLS on host port 443, redirects 80→443, and
  reverse-proxies to anthias-server:8080 — so X-Forwarded-Proto /
  X-Forwarded-For are forwarded as-is by Caddy.
* The override removes anthias-server's external port mapping
  (`ports: !override []`), so all external traffic must enter through
  Caddy and the IP allowlists in views_files.py see the original LAN
  client IP rather than the docker-bridge gateway. Inter-container
  traffic is unchanged.
* `FORWARDED_ALLOW_IPS=*` is set on anthias-server in the override —
  safe because anthias-server is no longer reachable from outside the
  Docker network — and `SECURE_PROXY_SSL_HEADER` is added in Django
  settings so request.is_secure() returns True for HTTPS callers.
* When SSL is *not* enabled there is zero new container, zero new
  config — the base compose file is untouched and Caddy isn't pulled
  or run.

bin/disable_ssl.sh now also removes the anthias-caddy container
before deleting the override, so HTTPS-only state is fully reversed.

Smoke-tested with a temporary Caddy override:
- HTTPS via Caddy:        200
- HTTP via Caddy:         301 → https://...
- Direct anthias-server:  refused (port mapping dropped by override)
- WebSocket upgrade:      101 Switching Protocols
- request.is_secure() with X-Forwarded-Proto=https: True
- 15 anthias_app view tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(views_files): document IP-allowlist threat model

Spell out exactly when the docker-bridge CIDR check is and isn't a
real perimeter:

* No-SSL default: anthias-server is published as 80:8080, so requests
  arrive with REMOTE_ADDR set to the docker bridge gateway (172.x) and
  LAN clients aren't actually excluded. Trying to plug the gap with
  auth would be security theatre — credentials would travel in
  plaintext over the LAN anyway.
* SSL via the Caddy sidecar: Caddy terminates TLS, rewrites
  X-Forwarded-For, uvicorn honours it (FORWARDED_ALLOW_IPS=*), and the
  check sees the real client IP — so the bypass is closed for any
  deployment that actually cares about confidentiality.

This is documentation only; no behavioural change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ssl): add --domain (auto Let's Encrypt) + drop openssl shim

bin/enable_ssl.sh now has three modes instead of two:

* Default (no args) — Caddy issues per-SNI certs lazily from its
  built-in local CA via `tls internal { on_demand }`. Drops the
  openssl self-signed-cert generation step entirely; Caddy persists
  the CA in the anthias-caddy-data volume and rotates leaf certs
  itself. Browsers still warn (CA is local) but no openssl/cert
  hygiene is needed on the host.

* `--domain example.com [--email you@example.com] [--staging]` —
  Caddy auto-issues + renews from Let's Encrypt. Caddy auto-creates
  the HTTP→HTTPS redirect for hostname sites. Use `--staging` to point
  at the ACME staging endpoint while testing, so the production rate
  limits aren't burned.

* `--cert /path/to/cert.pem --key /path/to/key.pem [--domain ...]` —
  unchanged: bring your own cert, Caddy serves it as-is with
  `auto_https off`.

Verified:
- All three Caddyfiles pass `caddy validate`.
- Default mode end-to-end: HTTPS=200 with cert from "Caddy Local
  Authority - ECC Intermediate", per-SNI SANs (DNS:localhost,
  IP Address:192.168.99.99 etc.), HTTP→HTTPS=301, /ws upgrade=101,
  anthias-server's external port mapping is dropped so direct access
  is refused.

Docs (CLAUDE.md, docs/README.md, docs/developer-documentation.md)
updated to describe the Caddy sidecar instead of in-uvicorn TLS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address self-review findings on PR #2757

* Gate SECURE_PROXY_SSL_HEADER on FORWARDED_ALLOW_IPS
  (anthias_django/settings.py): without the gate, a client on a
  plain-HTTP deploy could send `X-Forwarded-Proto: https` and flip
  `request.is_secure()`. Django reads the header from META directly,
  independent of uvicorn's --proxy-headers flag, so the previous
  unconditional setting was actually exploitable in non-SSL mode
  (secure-cookied sessions would drop on the next plain-HTTP request,
  redirects would point at https:// URLs that don't exist).

  Verified live: non-SSL → SECURE_PROXY_SSL_HEADER is None and
  is_secure() with spoofed XFP=https returns False; SSL via Caddy
  override → header is set and is_secure() returns True.

* Replace the isfile() pre-check + open() in anthias_assets and
  static_with_mime with a try/except FileNotFoundError around open()
  (anthias_app/views_files.py). Eliminates a (tiny but real) TOCTOU
  window between the stat and the open. IsADirectoryError handled
  too, since `realpath('/dir/')` resolves to the directory and open()
  would otherwise 500.

* Comment FORWARDED_ALLOW_IPS=* assumption in bin/enable_ssl.sh: the
  wildcard is only safe because the override drops anthias-server's
  external port mapping, so any future edit that re-adds a host:port
  publication has to either tighten the wildcard to Caddy's IP/CIDR
  or unset it.

* Replace ANSI-C escape sequences in the Caddyfile generator with
  plain multi-line strings. `read -r -d ''` was the first attempt
  but it strips trailing newlines, which collapsed `auto_https off`
  onto the same line as `}` in cert mode. Multi-line literals with
  echo "$VAR" are unambiguous and Caddy validates all three modes
  cleanly again.

* Add a docker-volume cleanup hint to bin/disable_ssl.sh: Caddy's
  local CA persists in anthias_anthias-caddy-data so an enable →
  disable → enable cycle reuses the same CA (intentional — browsers
  that trusted it stay trusted), and operators who want a fresh CA
  now have the exact `docker volume rm` command in the script's
  output.

15 view tests still pass; default + SSL Caddyfiles still validate;
default + SSL endpoints still return 200 / 301 / 101 in smoke tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address Copilot's host/MIME hardening feedback

Two security tightenings on top of the prior SECURE_PROXY_SSL_HEADER
gate (which Copilot flagged on a stale snapshot — that one's already
fixed in 07b784b9):

* `ALLOWED_HOSTS` is now driven by the `ALLOWED_HOSTS` env var, with
  `*` kept as the default so flexible LAN-by-IP / mDNS access still
  works out of the box. Operators on hardened LANs can opt into a
  strict allowlist (`ALLOWED_HOSTS=192.168.1.50,anthias.local,...`)
  to defend against DNS-rebinding without us guessing the right set
  of hostnames at install time. Verified the env override parses to
  `['192.168.1.50', 'anthias.local', 'localhost']`.

* `static_with_mime` now allowlists the `?mime=` query param against
  a small set of download-only types
  (`application/{gzip,octet-stream,x-gzip,x-tar,x-tgz,zip}`) instead
  of accepting whatever the caller sends. Closes the XSS footgun
  where `?mime=text/html` would have served a stored file as HTML.
  The frontend's only legitimate caller (the backup download) sends
  `application/x-tgz`, which is in the allowlist; anything else
  falls back to mimetypes.guess_type. Added
  `test_mime_override_rejects_html` to lock that behaviour in.

16 view tests pass; ruff clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 12:51:40 +01:00

286 lines
10 KiB
Markdown

# Developer documentation
## Understanding the components that make up Anthias
Here is a high-level overview of the different components that make Anthias:
![Anthias Diagram Overview](/docs/d2/anthias-diagram-overview.svg)
These components and their dependencies are mostly installed and handled with Ansible and Docker.
* The **web app** component (`anthias-server`) is the single HTTP entrypoint, served by uvicorn (ASGI). It runs the Django front-end + REST API, serves static assets via WhiteNoise, streams uploaded media at `/anthias_assets/`, and exposes the WebSocket endpoint at `/ws` via Django Channels. Always plain HTTP — TLS is opt-in via the **anthias-caddy** sidecar that `bin/enable_ssl.sh` installs (Caddy local CA by default, or Let's Encrypt with `--domain`).
* The **viewer** (`anthias-viewer`) is what drives the screen (e.g., shows web page, image or video). It fetches media from `anthias-server` over HTTP.
* The **Celery** (`anthias-celery`) component is for asynchronously queueing and executing tasks outside the HTTP request-response cycle (e.g., yt-dlp downloads, asset cleanup). It pushes asset-update events to connected WebSocket clients via the Redis-backed Channels layer.
* **Redis** (`redis`) is used as the Celery broker/result backend and as the Channels channel layer.
* The **database** component uses **SQLite** for storing the assets information.
## Dockerized development environment
To simplify development of the server module of Anthias, we've created a Docker container. This is intended to run on your local machine with the Anthias repository mounted as a volume.
> [!IMPORTANT]
> * Make sure that you have [installed Docker](https://docs.docker.com/engine/install/) on your machine before proceeding.
> * Anthias is using Docker's [buildx](https://docs.docker.com/engine/reference/commandline/buildx/) for the image builds. This is used both for cross compilation as well as for local caching. You might need to run `docker buildx create --use` first.
Assuming you're in the source code repository, simply run:
```bash
$ ./bin/start_development_server.sh
# The console output was truncated for brevity.
# ...
[+] Running 4/4
✔ Network anthias_default Created 0.1s
✔ Container anthias-redis-1 Started 0.2s
✔ Container anthias-anthias-server-1 Started 0.3s
✔ Container anthias-anthias-celery-1 Started 0.4s
```
> [!NOTE]
> Running the script will install Python 3.11, [pyenv](https://github.com/pyenv/pyenv),
> and [uv](https://docs.astral.sh/uv/) inside a Docker container on your machine.
> This is to ensure that the development environment is consistent across different
> machines.
>
> The script currently supports Debian-based systems and macOS.
unning the command above will start the development server and you should be able to
access the web interface at `http://localhost:8000`.
To stop the development server, run the following:
```bash
docker compose -f docker-compose.dev.yml down
```
## Building containers on the Raspberry Pi
> [!NOTE]
> Make sure that you have Docker installed on the device before proceeding.
```bash
$ ENVIRONMENT=production \
./bin/generate_dev_mode_dockerfiles.sh
$ MODE=build \
./bin/upgrade_containers.sh
```
## Django admin site
Create a superuser account:
```bash
$ export COMPOSE_FILE=docker-compose.dev.yml
$ docker compose exec anthias-server \
python manage.py createsuperuser
# You will be prompted to enter a username, an email address, and a password.
```
Once you have created a superuser account, you can open the Django admin site at `http://localhost:8000/admin/` (with a trailing slash)
and login with the credentials you just created.
## Testing
### Running the unit tests
Build and start the containers.
```bash
$ uv run python -m tools.image_builder \
--dockerfiles-only \
--disable-cache-mounts \
--service celery \
--service redis \
--service test
$ docker compose \
-f docker-compose.test.yml up -d --build
```
Run the unit tests.
```bash
$ docker compose \
-f docker-compose.test.yml \
exec anthias-test bash ./bin/prepare_test_environment.sh -s
# Integration and non-integration tests should be run separately as the
# former doesn't run as expected when run together with the latter.
$ docker compose \
-f docker-compose.test.yml \
exec anthias-test ./manage.py test --exclude-tag=integration
$ docker compose \
-f docker-compose.test.yml \
exec anthias-test ./manage.py test --tag=integration
```
### The QA checklist
We've also provided a [checklist](/docs/qa-checklist.md) that can serve as a guide for testing Anthias manually.
## Generating CSS and JS files
To get started, you need to start the development server first. See this [section](#dockerized-development-environment)
for details.
### Starting the bundler in development mode
To start [Bun](https://bun.sh/) in development (watch) mode, run the following command:
```bash
$ docker compose -f docker-compose.dev.yml exec anthias-server \
bun run dev
```
This runs `bun build --watch` for JS/TS and `sass --watch` for SCSS in parallel.
Making changes to the TypeScript, TSX, or SCSS files will automatically trigger a recompilation,
generating the corresponding bundle and CSS files.
### Formatting and linting TypeScript code
To run the linting and formatting checks on the TypeScript code, run the following command:
```bash
$ docker compose -f docker-compose.dev.yml exec anthias-server \
bun run lint:check
$ docker compose -f docker-compose.dev.yml exec anthias-server \
bun run format:check
```
If you want to fix the linting errors and formatting issues, run the following command:
```bash
$ docker compose -f docker-compose.dev.yml exec anthias-server \
bun run lint:fix
$ docker compose -f docker-compose.dev.yml exec anthias-server \
bun run format:fix
```
### Closing the transpiler
Just press `Ctrl-C` to close the bundler watch.
## Linting Python code locally
The project uses `ruff` for linting the Python codebase. While the linter is being run on the CI/CD pipeline,
you can also run it locally. There are several ways to do this.
### Run the linter using `act`
[`act`](https://nektosact.com/) lets you run GitHub Actions locally. This is useful for testing the CI/CD pipeline locally.
Installation instructions can be found [here](https://nektosact.com/installation/index.html).
After installing and setting up `act`, run the following command:
```bash
$ act -W .github/workflows/python-lint.yaml
```
The command above will run the linter on the all the Python files in the repository. If you want to run the linter
on a specific file, you can try the commands in the next section.
### Running the linter using `uv`
You have to install `uv` first. You can find the installation instructions
[here](https://docs.astral.sh/uv/getting-started/installation/).
After installing uv, run the following commands:
```bash
# Install the dependencies
$ uv venv
$ uv pip install --group dev-host
$ uv run ruff check .
```
To run the linter on a specific file, run the following command:
```bash
$ uv run ruff check /path/to/file.py
```
## Managing releases
### Creating a new release
Check what the latest release is:
```bash
$ git pull
$ git tag
# Running the `git tag` command should output something like this:
# 0.16
# ...
# v0.18.6
```
Create a new release:
```bash
$ git tag -a v0.18.7 -m "Test new automated disk images"
```
Push release:
```bash
$ git push origin v0.18.7
```
### Delete a broken release
```bash
$ git tag -d v0.18.5 [±master ✓]
Deleted tag 'v0.18.5' (was 9b86c39)
$ git push --delete origin v0.18.5 [±master ✓]
```
## Directories and files explained
In this section, we'll explain the different directories and files that are
present in a Raspberry Pi with Anthias installed.
### `/home/${USER}/anthias/`
* All of the files and folders from the Github repo should be cloned into this directory.
* On installations created before the rename, this directory is `/home/${USER}/screenly/` &mdash; the installer migrates it to `anthias/` on upgrade and leaves a back-compat symlink at the old path for one release.
### `/home/${USER}/.anthias/`
* `default_assets.yml` &mdash; configuration file which contains the default assets that get added to the assets list if enabled
* `initialized` &mdash; tells whether access point service (for Wi-Fi connectivity) runs or not
* `anthias.conf` &mdash; configuration file for web interface settings
* `anthias.db` &ndash; database file containing current assets information.
* On pre-rename installations this directory is `~/.screenly/` containing `screenly.conf` / `screenly.db`; the installer migrates them.
### `/etc/systemd/system/`
* `wifi-connect.service` &mdash; starts the Balena `wifi-connect` program to dynamically set the Wi-Fi config on the device via the captive portal
* `anthias-host-agent.service` &mdash; starts the Python script `host_agent.py`, which subscribes from the Redis component and performs a system call to shutdown or reboot the device when the message is received.
### `/etc/sudoers.d/anthias_overrides`
* `sudoers` configuration file that allows pi user to execute certain `sudo` commands without being a superuser (i.e., `root`)
### `/usr/share/plymouth/themes/anthias`
* `anthias.plymouth` &mdash; Plymouth config file (sets module name, `ImageDir` and `ScriptFile` dir)
* `anthias.script` &ndash; plymouth script file that loads and scales the splash screen image during the boot process
* `splashscreen.png` &mdash; the spash screen image that is displayed during the boot process
## Debugging the Anthias WebView
```
export QT_LOGGING_DEBUG=1
export QT_LOGGING_RULES="*.debug=true"
export QT_QPA_EGLFS_DEBUG=1
```
The Anthias WebView is a custom-built web browser based on the [Qt](https://www.qt.io/) toolkit framework.
The browser is assembled with a Dockerfile and built by a `webview/build_qt#.sh` script.
For further info on these files and more, visit the following link: [https://github.com/Screenly/Anthias/tree/master/webview](https://github.com/Screenly/Anthias/tree/master/webview)