REST API and web dashboard for an Android tablet repurposed as a home server.
The tablet runs atomic Fedora instead of Android and hosts a few household services in containers. This project exposes its state on a single HTTP endpoint, and renders it in a page served by the same backend.
The fixed point of the project is the JSON contract, written before any code. It describes nine blocks: host, battery, thermal, disk, clock, services, containers, backup and DNS. It is what the backend, the web page and the Android app share.
Each block has an independent reader. A reader that fails returns null in
its own fields without bringing down the response: if AdGuard is down, the DNS
block is empty and everything else keeps working. That property is what makes
the dashboard useful precisely when something is wrong.
The sources are of four different kinds:
| Block | Source |
|---|---|
| host, battery, thermal | files under /proc and /sys |
| disk | the JVM's FileStore |
| clock, services | chronyc, systemctl through ProcessBuilder |
| containers | podman in JSON |
| DNS | AdGuard Home HTTP API |
| backup | a state file written by the backup script |
Prefix /api/v1. /status is the one the clients use; the others are there for
debugging.
| Path | Contents |
|---|---|
/status |
every block at once |
/health |
ok |
/host |
hostname, uptime, load, memory |
/battery |
charge level, charging state, temperature |
/thermal |
SoC temperature |
/disk |
total and used space |
/clock |
NTP offset and sync state |
/services |
monitored systemd units |
/containers |
podman containers |
/backup |
result and date of the last backup |
/adguard |
total and blocked DNS queries |
The web page is served at the root and calls /api/v1/status every ten seconds.
When the backend stops responding it keeps the last data on screen and flags the
error instead of going blank.
- Java 25
- A Linux host with
/procand/sys(paths are configurable) - Optional, for the corresponding blocks:
chronyd,podman, AdGuard Home
./mvnw clean package
java -jar target/dashboard-api-0.0.1-SNAPSHOT.jarThe dev profile reads a fake battery from fake-sys/, so the project runs on
a machine that does not have those files:
./mvnw spring-boot:run -Dspring-boot.run.profiles=devOn a system without chronyc, systemctl or podman the matching blocks
return null. That is the intended behaviour, not an error.
In application.properties:
| Property | Meaning |
|---|---|
server.port |
HTTP port |
server.address |
listening interface |
dashboard.battery-path |
power_supply directory |
dashboard.proc-path |
/proc |
dashboard.thermal-path |
thermal zone under /sys/class/thermal |
dashboard.disk-path |
mount point to measure |
dashboard.backup-file |
backup state JSON |
dashboard.services |
systemd units to monitor, comma separated |
dashboard.adguard.url |
AdGuard Home URL |
dashboard.adguard.username |
AdGuard user |
dashboard.adguard.password |
read from ADGUARD_PASSWORD |
The password is not in the file: it comes from the environment variable, which
in production is provided by a systemd EnvironmentFile with restricted
permissions.
To read rootful containers without running the backend as root, a sudo rule limited to the two required commands is enough:
<user> ALL=(root) NOPASSWD: /usr/bin/podman ps --format json, /usr/bin/podman stats --no-stream --format json
Without this rule the containers block stays empty and the rest still works.
A systemd unit, with the password in a separate file:
[Service]
Type=simple
User=<user>
EnvironmentFile=/etc/dashboard-api.env
ExecStart=/usr/bin/java -jar /opt/dashboard-api/dashboard-api.jar
Restart=on-failure
RestartSec=10The service listens on loopback only and is reachable over a Tailscale VPN, which also terminates TLS. Nothing is exposed to the public internet.
Spring Boot 4, Java 25, Jackson 3. No database: the backend stores nothing, every response is a reading of the current state.
- The web page served by this backend
- An Android app in Kotlin and Compose, with a home screen widget
