@yuuki824/kanshi 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +4 -4
- package/Dockerfile +24 -19
- package/README.md +139 -36
- package/docker-compose.yml +16 -7
- package/go.mod +3 -0
- package/internal/config/config.go +140 -0
- package/internal/dockerstats/dockerstats.go +501 -0
- package/internal/server/server.go +372 -0
- package/internal/storage/storage.go +586 -0
- package/internal/vitals/vitals.go +614 -0
- package/main.go +76 -0
- package/package.json +5 -3
- package/web/app.js +167 -8
- package/web/index.html +10 -0
- package/web/style.css +27 -0
- package/web/treemap.js +21 -63
- package/app/__init__.py +0 -0
- package/app/config.py +0 -72
- package/app/dockerstats.py +0 -207
- package/app/main.py +0 -174
- package/app/storage.py +0 -220
- package/app/vitals.py +0 -170
- package/requirements.txt +0 -4
package/.env.example
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Copy to .env to override. Every value here is the built-in default.
|
|
2
2
|
|
|
3
|
-
# Bind address.
|
|
4
|
-
#
|
|
5
|
-
KANSHI_HOST=
|
|
3
|
+
# Bind address. Localhost keeps the dashboard private. Use this host's
|
|
4
|
+
# Tailscale IP to share it on a tailnet; 0.0.0.0 exposes it everywhere.
|
|
5
|
+
KANSHI_HOST=127.0.0.1
|
|
6
6
|
KANSHI_PORT=8100
|
|
7
7
|
|
|
8
8
|
# Live metrics
|
|
@@ -11,7 +11,7 @@ KANSHI_DOCKER_CONCURRENCY=8
|
|
|
11
11
|
KANSHI_IDLE_TIMEOUT=30
|
|
12
12
|
|
|
13
13
|
# Storage treemap
|
|
14
|
-
KANSHI_STORAGE_ROOTS
|
|
14
|
+
KANSHI_STORAGE_ROOTS=/=/hostfs,/mnt/data=/mnt/data
|
|
15
15
|
KANSHI_STORAGE_INTERVAL=1800
|
|
16
16
|
# Paths to skip entirely, comma separated (container-side paths).
|
|
17
17
|
# The biggest win is /hostfs/var/lib/docker — ~100k overlay2 files.
|
package/Dockerfile
CHANGED
|
@@ -1,27 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
# Build stage. The module has no third-party dependencies, so there is nothing
|
|
2
|
+
# to download and nothing to vendor — go.mod is copied on its own only so the
|
|
3
|
+
# layer cache survives edits to the source.
|
|
4
|
+
FROM golang:1.25-alpine AS build
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
WORKDIR /src
|
|
7
|
+
COPY go.mod ./
|
|
8
|
+
COPY main.go ./
|
|
9
|
+
COPY internal ./internal
|
|
10
|
+
COPY web ./web
|
|
7
11
|
|
|
8
|
-
|
|
12
|
+
# CGO off because nothing here needs libc: /proc is read as plain files and the
|
|
13
|
+
# Docker socket is a plain unix socket. That is what makes a scratch image
|
|
14
|
+
# possible at all.
|
|
15
|
+
ENV CGO_ENABLED=0
|
|
16
|
+
RUN go vet ./... && \
|
|
17
|
+
go build -trimpath -ldflags="-s -w" -o /kanshi .
|
|
9
18
|
|
|
10
|
-
|
|
11
|
-
|
|
19
|
+
# Runtime stage. The web assets are embedded in the binary, so the image is one
|
|
20
|
+
# static file and nothing else — no shell, no package manager, no CVE surface.
|
|
21
|
+
FROM scratch
|
|
12
22
|
|
|
13
|
-
COPY
|
|
14
|
-
COPY web ./web
|
|
23
|
+
COPY --from=build /kanshi /kanshi
|
|
15
24
|
|
|
16
25
|
EXPOSE 8100
|
|
17
26
|
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
HEALTHCHECK --interval=60s --timeout=5s --start-period=
|
|
21
|
-
CMD
|
|
22
|
-
h=os.environ.get('KANSHI_HOST','127.0.0.1'); h='127.0.0.1' if h=='0.0.0.0' else h; \
|
|
23
|
-
sys.exit(0 if urllib.request.urlopen('http://%s:%s/healthz' % (h, os.environ.get('KANSHI_PORT','8100')), timeout=4).status==200 else 1)"
|
|
27
|
+
# The probe is the binary itself: a scratch image has no curl to call, and this
|
|
28
|
+
# way it reads KANSHI_HOST/KANSHI_PORT from the same code that binds them.
|
|
29
|
+
HEALTHCHECK --interval=60s --timeout=5s --start-period=10s --retries=3 \
|
|
30
|
+
CMD ["/kanshi", "-healthcheck"]
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
--host ${KANSHI_HOST:-0.0.0.0} --port ${KANSHI_PORT:-8100} \
|
|
27
|
-
--no-access-log --timeout-keep-alive 65"]
|
|
32
|
+
ENTRYPOINT ["/kanshi"]
|
package/README.md
CHANGED
|
@@ -1,12 +1,65 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# kanshi
|
|
2
4
|
|
|
3
|
-
A one-page, mobile-first glance at this homeserver
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
**A one-page, mobile-first glance at this homeserver.**
|
|
6
|
+
|
|
7
|
+
Live CPU and RAM, a Filelight-style storage treemap, and `docker stats` for
|
|
8
|
+
every container — no historical storage, no alerting, no external services.
|
|
9
|
+
|
|
10
|
+
[](https://go.dev/)
|
|
11
|
+
[](https://hub.docker.com/_/scratch)
|
|
12
|
+
[](https://docs.docker.com/compose/)
|
|
13
|
+
[](https://www.npmjs.com/package/@yuuki824/kanshi)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Screenshots
|
|
20
|
+
|
|
21
|
+
<table>
|
|
22
|
+
<tr>
|
|
23
|
+
<td width="60%">
|
|
24
|
+
|
|
25
|
+
**Vitals & storage** — live CPU/RAM meters and the storage treemap
|
|
26
|
+
<img src="docs/screenshots/dashboard-vitals.png" alt="Processor, memory and storage map cards" />
|
|
27
|
+
|
|
28
|
+
</td>
|
|
29
|
+
<td width="40%">
|
|
30
|
+
|
|
31
|
+
**Mobile** — the same cards, stacked for a phone screen
|
|
32
|
+
<img src="docs/screenshots/dashboard-mobile.png" alt="Dashboard on a mobile viewport" />
|
|
33
|
+
|
|
34
|
+
</td>
|
|
35
|
+
</tr>
|
|
36
|
+
</table>
|
|
37
|
+
|
|
38
|
+
By default it is reachable only from the local machine:
|
|
6
39
|
|
|
7
|
-
|
|
40
|
+
http://localhost:8100
|
|
8
41
|
|
|
9
|
-
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- 📊 **Processor** — hero utilisation %, per-core bars, load average, temperature, host net/disk throughput
|
|
45
|
+
- 🧠 **Memory & volumes** — RAM, swap, and one meter per storage root
|
|
46
|
+
- 🗺 **Storage map** — squarified treemap you can tap to drill into, with a table twin below; depth-bounded so it stays fast on large filesystems
|
|
47
|
+
- 🐳 **Containers** — per-container CPU%, memory, network rate and health straight from the Docker Engine API
|
|
48
|
+
- ⚡ **One SSE connection** — the server pushes every tick over `/api/stream`; nothing polls, nothing needs a manual reload
|
|
49
|
+
- 😴 **Idles to near-zero** — the poller and the Docker socket both go quiet after `KANSHI_IDLE_TIMEOUT` with nobody watching
|
|
50
|
+
- 🪶 **9.6 MB image, ~8 MB resident** — one static Go binary on `scratch`: no interpreter, no shell, no package manager
|
|
51
|
+
- 🔒 **Tailscale-friendly** — binds to `127.0.0.1` by default; point it at a Tailscale IP to share it on a tailnet instead of the open LAN
|
|
52
|
+
|
|
53
|
+
## Tech Stack
|
|
54
|
+
|
|
55
|
+
| Layer | Choice |
|
|
56
|
+
|---|---|
|
|
57
|
+
| Backend | Go 1.25, standard library only — zero third-party dependencies |
|
|
58
|
+
| Host metrics | `/proc` and `/sys` parsed directly; `statfs(2)` for volumes |
|
|
59
|
+
| Live updates | Server-Sent Events (`/api/stream`) |
|
|
60
|
+
| Frontend | Vanilla JS, hand-rolled SVG treemap — no build step, embedded in the binary |
|
|
61
|
+
| Container metrics | Docker Engine API (one-shot stats, not the streaming daemon default) |
|
|
62
|
+
| Packaging | `scratch` image via multi-stage build, Docker Compose, `npx` launcher |
|
|
10
63
|
|
|
11
64
|
## Install with npx
|
|
12
65
|
|
|
@@ -14,29 +67,70 @@ Reachable from any device on the tailnet:
|
|
|
14
67
|
repository includes a small npm launcher which starts the included Docker
|
|
15
68
|
Compose app, so Docker (with the Compose v2 plugin) is still required.
|
|
16
69
|
|
|
17
|
-
From
|
|
70
|
+
From any directory, run:
|
|
18
71
|
|
|
19
72
|
```sh
|
|
20
73
|
npx @yuuki824/kanshi
|
|
21
74
|
```
|
|
22
75
|
|
|
23
|
-
The
|
|
24
|
-
|
|
25
|
-
|
|
76
|
+
The first run downloads the package, builds the Docker image, and starts a
|
|
77
|
+
container named `kanshi`. Open <http://localhost:8100> once it says the
|
|
78
|
+
container started. It may take a minute for the initial health check to pass.
|
|
79
|
+
|
|
80
|
+
The command is equivalent to `docker compose up -d --build`. To run without
|
|
81
|
+
the confirmation prompt:
|
|
26
82
|
|
|
27
83
|
```sh
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
84
|
+
npx --yes @yuuki824/kanshi
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Manage the container
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
# Stop it without removing it.
|
|
91
|
+
docker stop kanshi
|
|
92
|
+
|
|
93
|
+
# Start the stopped container again.
|
|
94
|
+
docker start kanshi
|
|
95
|
+
|
|
96
|
+
# Follow application logs.
|
|
97
|
+
docker logs -f kanshi
|
|
98
|
+
|
|
99
|
+
# Remove the current container, for example before installing an update.
|
|
100
|
+
docker rm -f kanshi
|
|
101
|
+
|
|
102
|
+
# Download the newest package version and start it again.
|
|
103
|
+
npx @yuuki824/kanshi@latest
|
|
31
104
|
```
|
|
32
105
|
|
|
33
|
-
|
|
106
|
+
You can also pass Docker Compose commands after the package name:
|
|
34
107
|
|
|
35
108
|
```sh
|
|
36
109
|
npx @yuuki824/kanshi logs -f
|
|
37
110
|
npx @yuuki824/kanshi down
|
|
38
111
|
```
|
|
39
112
|
|
|
113
|
+
### Network access
|
|
114
|
+
|
|
115
|
+
Kanshi listens on `127.0.0.1:8100` by default, so it is private to the host.
|
|
116
|
+
To share it over Tailscale, use the machine's Tailscale IP:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
KANSHI_HOST="$(tailscale ip -4)" npx @yuuki824/kanshi
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Do not use `KANSHI_HOST=0.0.0.0` unless the machine is protected by an
|
|
123
|
+
authenticated reverse proxy: the dashboard can read Docker information.
|
|
124
|
+
|
|
125
|
+
### Troubleshooting
|
|
126
|
+
|
|
127
|
+
Check whether the container is running and inspect a restart or startup error:
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
docker ps --filter name=kanshi
|
|
131
|
+
docker logs --tail=100 kanshi
|
|
132
|
+
```
|
|
133
|
+
|
|
40
134
|
To publish the launcher, use:
|
|
41
135
|
|
|
42
136
|
```sh
|
|
@@ -58,13 +152,13 @@ docker compose up -d --build
|
|
|
58
152
|
Copy `.env.example` to `.env` to override anything. Every setting has a
|
|
59
153
|
conservative default; the file documents each one.
|
|
60
154
|
|
|
61
|
-
##
|
|
155
|
+
## API
|
|
62
156
|
|
|
63
157
|
| Card | Source | Refresh |
|
|
64
158
|
|---|---|---|
|
|
65
|
-
| Processor — hero %, per-core bars, load, temp, host net/disk throughput |
|
|
66
|
-
| Memory & volumes — RAM, swap, one meter per storage root | `
|
|
67
|
-
| Storage map — squarified treemap, tap to drill, table twin below | cached `
|
|
159
|
+
| Processor — hero %, per-core bars, load, temp, host net/disk throughput | `/proc/stat`, `/proc/net/dev`, `/proc/diskstats`, `/sys` hwmon | every `KANSHI_POLL_INTERVAL` (5s) |
|
|
160
|
+
| Memory & volumes — RAM, swap, one meter per storage root | `/proc/meminfo` + `statfs(2)` | same tick |
|
|
161
|
+
| Storage map — squarified treemap, tap to drill, table twin below | cached `getdents`+`lstat` walk | every `KANSHI_STORAGE_INTERVAL` (30m), or the Rescan button |
|
|
68
162
|
| Containers — CPU%, memory, network rates, health | Docker Engine API | same tick |
|
|
69
163
|
|
|
70
164
|
The browser holds **one SSE connection** (`/api/stream`) and the server pushes
|
|
@@ -91,24 +185,16 @@ fully traversed and counted; their bytes roll up into the nearest kept ancestor.
|
|
|
91
185
|
Sizes come from `st_blocks` (so they match `du`, not apparent size) and
|
|
92
186
|
hardlinked files are counted once.
|
|
93
187
|
|
|
94
|
-
**The walk runs at `nice 10`**
|
|
95
|
-
|
|
188
|
+
**The walk runs at `nice 10`** on a locked, dedicated OS thread — Linux applies
|
|
189
|
+
`setpriority(PRIO_PROCESS)` per thread, and the runtime retires that thread when
|
|
190
|
+
the walk ends rather than handing a niced thread back to the poller. A full pass
|
|
191
|
+
over `/` takes ~76s on a cold cache, and the live cards keep their exact 5s
|
|
192
|
+
cadence throughout.
|
|
96
193
|
|
|
97
194
|
**Unreadable directories are reported, not hidden.** If the walk cannot enter a
|
|
98
195
|
directory it is counted and the storage card says so — a silently truncated tree
|
|
99
196
|
that under-reports by 300 GB is worse than an obviously incomplete one.
|
|
100
197
|
|
|
101
|
-
## Access
|
|
102
|
-
|
|
103
|
-
The port is bound to the **Tailscale interface only** (`KANSHI_HOST`), so the
|
|
104
|
-
dashboard is not exposed on the LAN or the public interface and needs no auth
|
|
105
|
-
layer of its own. Set `KANSHI_HOST=0.0.0.0` to expose it on the LAN — but note
|
|
106
|
-
that anything that can reach this app can reach the Docker socket through it, so
|
|
107
|
-
add an auth proxy first if you do.
|
|
108
|
-
|
|
109
|
-
If Tailscale is down when Docker starts, the bind fails and `restart:
|
|
110
|
-
unless-stopped` retries until `tailscale0` is back.
|
|
111
|
-
|
|
112
198
|
## Permissions
|
|
113
199
|
|
|
114
200
|
Runs as root with `cap_drop: ALL` plus **`DAC_READ_SEARCH`** — read and traverse
|
|
@@ -118,12 +204,29 @@ filesystem by ~330 GB. The rootfs is read-only and every host mount is `:ro`.
|
|
|
118
204
|
|
|
119
205
|
## About kanshi's own memory number
|
|
120
206
|
|
|
121
|
-
The dashboard will show kanshi using far more memory than
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
on the cache and the kernel reclaims it
|
|
126
|
-
|
|
207
|
+
The dashboard will show kanshi using far more memory than the process actually
|
|
208
|
+
has. That figure is mostly **reclaimable kernel dentry cache** charged to its
|
|
209
|
+
cgroup — an unavoidable side effect of `lstat`-ing ~150k files during a walk.
|
|
210
|
+
Measured anonymous memory is **~8 MB at rest and ~20 MB at the peak of a full
|
|
211
|
+
walk**; `mem_limit` acts as a ceiling on the cache and the kernel reclaims it
|
|
212
|
+
under pressure. `GOMEMLIMIT` sits below `mem_limit` so an unusually large
|
|
213
|
+
filesystem makes the collector work harder instead of getting the container
|
|
214
|
+
OOM-killed. The number matches what `docker stats` reports for any container,
|
|
215
|
+
which is the point.
|
|
216
|
+
|
|
217
|
+
## Building
|
|
218
|
+
|
|
219
|
+
The only build dependency is Docker; the image compiles the binary itself.
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
docker compose up -d --build
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
With a local Go toolchain (1.22+), `go build .` and `go vet ./...` work from the
|
|
226
|
+
repository root with no module downloads — there are no third-party imports. The
|
|
227
|
+
frontend is embedded with `//go:embed`, so a rebuild is needed after editing
|
|
228
|
+
anything under `web/`; set `KANSHI_WEB_DIR=./web` to serve it from disk instead
|
|
229
|
+
while iterating.
|
|
127
230
|
|
|
128
231
|
## Tuning
|
|
129
232
|
|
package/docker-compose.yml
CHANGED
|
@@ -10,10 +10,9 @@ services:
|
|
|
10
10
|
network_mode: host
|
|
11
11
|
|
|
12
12
|
environment:
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
KANSHI_HOST: "${KANSHI_HOST:-100.73.243.14}"
|
|
13
|
+
# Localhost is safe for a package install. Set this to a Tailscale IP to
|
|
14
|
+
# share it on a tailnet, or 0.0.0.0 only behind an authenticated proxy.
|
|
15
|
+
KANSHI_HOST: "${KANSHI_HOST:-127.0.0.1}"
|
|
17
16
|
KANSHI_PORT: "${KANSHI_PORT:-8100}"
|
|
18
17
|
|
|
19
18
|
# Live poll cadence. Each tick is one /containers/json plus one /stats per
|
|
@@ -24,13 +23,20 @@ services:
|
|
|
24
23
|
KANSHI_IDLE_TIMEOUT: "${KANSHI_IDLE_TIMEOUT:-30}"
|
|
25
24
|
|
|
26
25
|
# Storage walk: label=path pairs. Cached, never recomputed per refresh.
|
|
27
|
-
KANSHI_STORAGE_ROOTS: "${KANSHI_STORAGE_ROOTS
|
|
26
|
+
KANSHI_STORAGE_ROOTS: "${KANSHI_STORAGE_ROOTS:-/=/hostfs,/mnt/data=/mnt/data}"
|
|
28
27
|
KANSHI_STORAGE_INTERVAL: "${KANSHI_STORAGE_INTERVAL:-1800}"
|
|
29
28
|
# Container-side paths to skip. /hostfs/var/lib/docker is ~48 GB across
|
|
30
29
|
# ~100k overlay2 files and dominates the walk time — uncomment to trim it.
|
|
31
30
|
KANSHI_STORAGE_EXCLUDE: "${KANSHI_STORAGE_EXCLUDE:-}"
|
|
32
31
|
KANSHI_TREE_DEPTH: "${KANSHI_TREE_DEPTH:-4}"
|
|
33
32
|
|
|
33
|
+
# Give the GC a budget below mem_limit so a walk on an unusually large
|
|
34
|
+
# filesystem degrades into more collection rather than an OOM kill.
|
|
35
|
+
GOMEMLIMIT: "80MiB"
|
|
36
|
+
# Two Ps is plenty for a 5s poll loop and one walk, and it keeps the Go
|
|
37
|
+
# scheduler from spreading across all four cores of a busy box.
|
|
38
|
+
GOMAXPROCS: "2"
|
|
39
|
+
|
|
34
40
|
volumes:
|
|
35
41
|
# Read-only on the socket is a mount flag, not an API restriction — it is
|
|
36
42
|
# the Tailscale-only bind above that keeps this out of reach.
|
|
@@ -50,9 +56,12 @@ services:
|
|
|
50
56
|
- no-new-privileges:true
|
|
51
57
|
|
|
52
58
|
# Stays out of the way of the other ~28 containers on this 4-core box.
|
|
53
|
-
# Steady state is ~
|
|
59
|
+
# Steady state is ~0.03% of one core; the ceiling is headroom for the walk.
|
|
54
60
|
cpus: "1.0"
|
|
55
|
-
|
|
61
|
+
# Measured peak during a full walk of a 2 TB host is ~20 MiB of heap, and
|
|
62
|
+
# ~8 MiB at rest. The rest of this is reclaimable page cache the walk pulls
|
|
63
|
+
# in, which the kernel drops again the moment anything else wants it.
|
|
64
|
+
mem_limit: 96m
|
|
56
65
|
|
|
57
66
|
logging:
|
|
58
67
|
driver: json-file
|
package/go.mod
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Package config holds runtime configuration, all via environment variables.
|
|
2
|
+
//
|
|
3
|
+
// Defaults are deliberately conservative: this box has 4 cores and ~28 other
|
|
4
|
+
// containers, so Kanshi should be invisible in `docker stats`.
|
|
5
|
+
package config
|
|
6
|
+
|
|
7
|
+
import (
|
|
8
|
+
"os"
|
|
9
|
+
"path"
|
|
10
|
+
"strconv"
|
|
11
|
+
"strings"
|
|
12
|
+
"time"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// Config is read once at startup and never mutated, so it needs no locking.
|
|
16
|
+
type Config struct {
|
|
17
|
+
// How often the live poller samples vitals + container stats.
|
|
18
|
+
PollInterval time.Duration
|
|
19
|
+
// Stop polling entirely once no browser has been connected for this long.
|
|
20
|
+
// Nobody is looking, so there is no reason to keep waking the Docker daemon.
|
|
21
|
+
IdleTimeout time.Duration
|
|
22
|
+
|
|
23
|
+
// Max concurrent /stats requests against the Docker socket per tick.
|
|
24
|
+
DockerConcurrency int
|
|
25
|
+
DockerSocket string
|
|
26
|
+
|
|
27
|
+
// Storage walk. Roots are "label=path" or just "path".
|
|
28
|
+
StorageRoots []string
|
|
29
|
+
StorageInterval time.Duration
|
|
30
|
+
// Absolute container-side paths to skip entirely. Their bytes vanish from
|
|
31
|
+
// the totals, so only exclude things you truly don't want counted.
|
|
32
|
+
StorageExclude []string
|
|
33
|
+
StorageMinRescan time.Duration
|
|
34
|
+
|
|
35
|
+
// Tree pruning, to keep the JSON the phone downloads small.
|
|
36
|
+
TreeDepth int
|
|
37
|
+
// Children smaller than this fraction of their parent are folded into an
|
|
38
|
+
// aggregate node rather than shipped individually.
|
|
39
|
+
TreeMinFraction float64
|
|
40
|
+
TreeMaxChildren int
|
|
41
|
+
|
|
42
|
+
Host string
|
|
43
|
+
Port int
|
|
44
|
+
|
|
45
|
+
// Serve web assets from this directory instead of the ones baked into the
|
|
46
|
+
// binary. Only useful when iterating on the frontend.
|
|
47
|
+
WebDir string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Root is one labelled entry from StorageRoots.
|
|
51
|
+
type Root struct {
|
|
52
|
+
Label string
|
|
53
|
+
Path string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Roots expands the "label=path" entries. A bare path is labelled with its
|
|
57
|
+
// own basename.
|
|
58
|
+
func (c Config) Roots() []Root {
|
|
59
|
+
out := make([]Root, 0, len(c.StorageRoots))
|
|
60
|
+
for _, entry := range c.StorageRoots {
|
|
61
|
+
label, p, found := strings.Cut(entry, "=")
|
|
62
|
+
if !found || p == "" {
|
|
63
|
+
p = label
|
|
64
|
+
if base := path.Base(strings.TrimRight(label, "/")); base != "" && base != "." {
|
|
65
|
+
label = base
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
out = append(out, Root{Label: label, Path: p})
|
|
69
|
+
}
|
|
70
|
+
return out
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Load reads the environment. Unparseable values fall back to the default
|
|
74
|
+
// rather than refusing to start — a typo in one knob should not take the
|
|
75
|
+
// dashboard down.
|
|
76
|
+
func Load() Config {
|
|
77
|
+
return Config{
|
|
78
|
+
PollInterval: envSeconds("KANSHI_POLL_INTERVAL", 5*time.Second),
|
|
79
|
+
IdleTimeout: envSeconds("KANSHI_IDLE_TIMEOUT", 30*time.Second),
|
|
80
|
+
DockerConcurrency: envInt("KANSHI_DOCKER_CONCURRENCY", 8),
|
|
81
|
+
DockerSocket: envString("KANSHI_DOCKER_SOCKET", "/var/run/docker.sock"),
|
|
82
|
+
StorageRoots: envList("KANSHI_STORAGE_ROOTS", "/=/hostfs,/mnt/data=/mnt/data"),
|
|
83
|
+
StorageInterval: envSeconds("KANSHI_STORAGE_INTERVAL", 1800*time.Second),
|
|
84
|
+
StorageExclude: envList("KANSHI_STORAGE_EXCLUDE", ""),
|
|
85
|
+
StorageMinRescan: envSeconds("KANSHI_STORAGE_MIN_RESCAN", 30*time.Second),
|
|
86
|
+
TreeDepth: envInt("KANSHI_TREE_DEPTH", 4),
|
|
87
|
+
TreeMinFraction: envFloat("KANSHI_TREE_MIN_FRACTION", 0.005),
|
|
88
|
+
TreeMaxChildren: envInt("KANSHI_TREE_MAX_CHILDREN", 24),
|
|
89
|
+
Host: envString("KANSHI_HOST", "0.0.0.0"),
|
|
90
|
+
Port: envInt("KANSHI_PORT", 8100),
|
|
91
|
+
WebDir: envString("KANSHI_WEB_DIR", ""),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func envString(name, def string) string {
|
|
96
|
+
if v := os.Getenv(name); v != "" {
|
|
97
|
+
return v
|
|
98
|
+
}
|
|
99
|
+
return def
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func envInt(name string, def int) int {
|
|
103
|
+
v, err := strconv.Atoi(strings.TrimSpace(os.Getenv(name)))
|
|
104
|
+
if err != nil {
|
|
105
|
+
return def
|
|
106
|
+
}
|
|
107
|
+
return v
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func envFloat(name string, def float64) float64 {
|
|
111
|
+
v, err := strconv.ParseFloat(strings.TrimSpace(os.Getenv(name)), 64)
|
|
112
|
+
if err != nil {
|
|
113
|
+
return def
|
|
114
|
+
}
|
|
115
|
+
return v
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// envSeconds accepts a bare number of seconds, matching the Compose file's
|
|
119
|
+
// plain integers.
|
|
120
|
+
func envSeconds(name string, def time.Duration) time.Duration {
|
|
121
|
+
v, err := strconv.ParseFloat(strings.TrimSpace(os.Getenv(name)), 64)
|
|
122
|
+
if err != nil || v <= 0 {
|
|
123
|
+
return def
|
|
124
|
+
}
|
|
125
|
+
return time.Duration(v * float64(time.Second))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
func envList(name, def string) []string {
|
|
129
|
+
raw := os.Getenv(name)
|
|
130
|
+
if raw == "" {
|
|
131
|
+
raw = def
|
|
132
|
+
}
|
|
133
|
+
var out []string
|
|
134
|
+
for _, part := range strings.Split(raw, ",") {
|
|
135
|
+
if p := strings.TrimSpace(part); p != "" {
|
|
136
|
+
out = append(out, p)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return out
|
|
140
|
+
}
|