@@ -26,9 +26,9 @@ serves a React dashboard (REST + WebSocket) from the same Go binary.
2626
2727- ** Input:** one inverter over Modbus TCP or RTU; the poller is the only Modbus user.
2828- ** Outputs:** SQLite (daily/monthly/yearly/total values, status history), REST API,
29- WebSocket live values, the SPA and Swagger UI served from disk ( ` frontend/dist ` ,
30- ` docs/dist ` ).
31- - ** Runtime:** a single binary; without a subcommand it runs the server, ` solis backfill `
29+ WebSocket live values, the SPA and Swagger UI embedded in the binary ( ` go:embed ` of
30+ the prebuilt ` frontend/dist ` , ` docs/dist ` ).
31+ - ** Runtime:** a single self-contained binary; without a subcommand it runs the server, ` solis backfill `
3232 runs a maintenance job. Shipped as binaries (linux, darwin) and a multi-arch ` scratch `
3333 image on ghcr.io.
3434
@@ -45,8 +45,9 @@ go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./... # known vulnerabilit
4545go tool mockery # regenerate mocks (.mockery.yaml; version pinned in go.mod)
4646go test -race ./... # all tests, as in CI
4747go test ./internal/solis -run TestBlockPlanGolden
48- make build # ./solis with version info (ldflags)
4948make assets # frontend/dist + docs/dist (make frontend / docs)
49+ make build # ./solis with version info (ldflags), embeds the dists
50+ make all # assets, then build
5051make docker # dev image (docker-compose.dev.yaml) with version info
5152go run ./cmd # server (reads ./config.yaml; -config <path>)
5253go run ./cmd backfill --years 0 # maintenance job (app must be stopped), exits 0/1
@@ -63,9 +64,11 @@ cd frontend && npm run typecheck && npm run lint && npm run knip
6364## 3. Project Structure
6465
6566```
66- cmd/ main.go (subcommand dispatch, signal ctx, exit code), serve.go, backfill.go
67+ cmd/ main.go (subcommand dispatch, signal ctx, exit code, ldflags build info),
68+ serve.go, backfill.go
6769internal/
6870 app/ composition root: Create* factories, health adapters, logger wiring
71+ buildinfo/ build information (version, commit, …) passed down from cmd
6972 config/ YAML/env config, structural Validate(); domain rules injected by app
7073 period/ Period (day/month/year keys from one captured instant), rollover window
7174 eventbus/ event bus (ValuesUpdated, PeriodClosed), non-blocking, per-subscriber policy
@@ -88,8 +91,8 @@ internal/
8891 util/ math, data types, Clock, Slot[T], DependencyError/RequireAll;
8992 util/clocktest is the fake Clock for tests
9093 <pkg>/mocks/ mockery-generated mocks (never hand-edit)
91- frontend/ React 19 + Vite + Tailwind 4 + zustand SPA
92- docs/ Swagger UI + openapi.yaml
94+ frontend/ React 19 + Vite + Tailwind 4 + zustand SPA; embed.go embeds dist/
95+ docs/ Swagger UI + openapi.yaml; embed.go embeds dist/
9396example/ docker-compose (TCP, RTU) and config.yaml templates for users
9497```
9598
@@ -149,8 +152,9 @@ modbus → stdlib + simonvetter + util (Clock) + an injected zerolog.Logger
149152 (external layer: no config/health)
150153service → storage (ReadStore only), health, history, period, solis + own interfaces
151154 (CacheReader, HealthSnapshotter)
152- httphandler → service, health, period, solis, util
153- app → everything (composition root)
155+ httphandler → service, health, period, solis, buildinfo, util
156+ buildinfo → stdlib only
157+ app → everything (composition root), incl. the frontend and docs embed packages
154158```
155159
156160- "storage (store types)" means the interfaces, DTOs (` PollWrite ` , ` DailyRow ` , …) and
@@ -246,14 +250,31 @@ LastBeat() time.Time // atomic timestamp, updated by the component'
246250 parse, call ` Service.Data ` /` Keys ` /` Health ` , map errors, write JSON.
247251- Middleware (` Recover(log) ` , ` Logger(log) ` , ` SecurityHeaders ` ) is
248252 ` func(http.Handler) http.Handler ` with an injected logger where it logs.
249- - Routes (` router.SetupRoutes(Deps) ` ): ` /health ` , ` /api/keys ` , ` /api/data/{key} ` , ` /ws ` ,
250- ` /docs/ ` , and the SPA. No CORS middleware.
253+ - Routes (` router.SetupRoutes(Deps) ` ): ` /health ` , ` /api/keys ` , ` /api/data/{key} ` ,
254+ ` /api/version ` , ` /ws ` , ` /docs/ ` , and the SPA. No CORS middleware.
255+ - Static files come from ` fs.FS ` values in ` router.Deps ` (` Frontend ` , ` Docs ` ); ` app ` passes
256+ the embedded ` frontend.Dist() ` / ` docs.Dist() ` , tests pass an ` fstest.MapFS ` . A tree
257+ without ` index.html ` is not mounted (binary built before ` make assets ` ; ` app ` warns).
251258- One error shape, documented in ` docs/src/openapi.yaml ` , written with
252259 ` WriteError(w, status, msg) ` : ` { "error": "Bad Request", "message": "…", "code": 400 } ` .
253260- ` service.NewReadService(service.Deps{Store, Cache, Health, Registry, Decoder, Log}) ` :
254261 every dependency is required; the service never receives a Modbus client, poller or
255262 aggregator.
256263
264+ ### Version (one version for binary, UI and docs)
265+
266+ - The binary's build information (ldflags → ` buildinfo.Info ` , ` HandlerDeps.Build ` ) is
267+ served by ` GET /api/version ` . The frontend and the Swagger UI are built with the same
268+ version (` VITE_APP_VERSION=$(VERSION) ` in ` make frontend ` /` make docs ` ; goreleaser and
269+ the Dockerfile pass the release/build version), so binary, UI and docs show one version.
270+ No ` version.json ` .
271+ - The frontend polls ` /api/version ` and compares it with its compiled version: a
272+ different one means the server was upgraded (update banner → reload). ` dev ` on either
273+ side never reports an update.
274+ - ` go:embed all:dist* ` also matches the committed ` dist.md ` , so Go builds, tests and lint
275+ work without npm; release builds always run ` make assets ` first (goreleaser before
276+ hooks, Dockerfile node stages).
277+
257278### Health endpoint (fail-closed)
258279
259280- ` GET /health ` reads the supervisor snapshot only; it never blocks on a component.
@@ -462,6 +483,8 @@ Deliberate choices, with the reason, for behaviour that is not obvious from the
462483 through a ` util.Slot ` so the poller always reads the current client after a restart.
463484- ** Derived values:** ` battery_power_signed ` is produced by ` Decoder.Derive ` after a full
464485 poll (it needs two registers).
486+ - ** Embedded web assets:** the archives and the image ship only the binary; the dist
487+ folders are build inputs, not runtime files.
465488- ** Read plan:** 3 Modbus reads (grid power at 33130 so it falls inside an existing block),
466489 pinned by the block-plan golden test.
467490- ** Shutdown:** single-phase with a hard deadline; no second-signal force mode.
0 commit comments