Skip to content

Commit 435f681

Browse files
committed
Merge branch 'feat/embed-assets'
2 parents 77cf3f1 + 453b588 commit 435f681

30 files changed

Lines changed: 429 additions & 169 deletions

‎.goreleaser.yaml‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ before:
55
hooks:
66
# CI already enforces a tidy go.mod; verify only, never rewrite the tree mid-release.
77
- go mod verify
8-
# Frontend and API docs are built once here and shipped in both the archives and
9-
# the images (the binary serves ./frontend/dist and ./docs/dist from disk).
10-
- make frontend VITE_GIT_COMMIT_HASH={{ .Version }}-{{ .ShortCommit }}
11-
- make docs
8+
# Frontend and API docs are built once here, with the release version the binaries
9+
# get, before the Go builds embed them (go:embed); archives and images ship only the
10+
# binary.
11+
- make assets VERSION={{ .Version }}
1212

1313
builds:
1414
- id: solis-multi-arch
@@ -51,8 +51,6 @@ archives:
5151
- LICENSE
5252
- README.md
5353
- example/config.yaml
54-
- frontend/dist
55-
- docs/dist
5654

5755
changelog:
5856
sort: asc
@@ -137,9 +135,6 @@ dockers_v2:
137135
- linux/arm64
138136
- linux/arm/v7
139137
dockerfile: Dockerfile.goreleaser
140-
extra_files:
141-
- frontend/dist
142-
- docs/dist
143138
labels: &oci
144139
org.opencontainers.image.title: Solis Monitor
145140
org.opencontainers.image.description: >-

‎AGENTS.md‎

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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
4545
go tool mockery # regenerate mocks (.mockery.yaml; version pinned in go.mod)
4646
go test -race ./... # all tests, as in CI
4747
go test ./internal/solis -run TestBlockPlanGolden
48-
make build # ./solis with version info (ldflags)
4948
make 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
5051
make docker # dev image (docker-compose.dev.yaml) with version info
5152
go run ./cmd # server (reads ./config.yaml; -config <path>)
5253
go 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
6769
internal/
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/
9396
example/ 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)
150153
service → 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.

‎Dockerfile‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
FROM node:26-alpine AS frontend
22
WORKDIR /frontend
3-
# Accept git commit hash as build argument with default value
4-
ARG VITE_GIT_COMMIT_HASH
53
COPY frontend/package.json frontend/package-lock.json ./
64
RUN npm ci
75
COPY frontend/ .
8-
RUN npm run build
6+
# Same version as the binary (make docker passes it); the UI compares it with /api/version.
7+
ARG VERSION=dev
8+
RUN VITE_APP_VERSION=${VERSION} npm run build
99

1010
FROM node:26-alpine AS docs
1111
WORKDIR /docs
1212
COPY docs/package.json docs/package-lock.json ./
1313
RUN npm ci
1414
COPY docs/ .
15-
RUN npm run build
16-
17-
15+
ARG VERSION=dev
16+
RUN VITE_APP_VERSION=${VERSION} npm run build
1817

1918
FROM golang:1.27.1-alpine AS builder
2019
WORKDIR /app
@@ -29,6 +28,9 @@ ARG COMMIT=unknown
2928
ARG BUILD_DATE=unknown
3029

3130
COPY . .
31+
# The web assets are embedded into the binary (go:embed), so they must exist before go build.
32+
COPY --from=frontend /frontend/dist ./frontend/dist
33+
COPY --from=docs /docs/dist ./docs/dist
3234
RUN CGO_ENABLED=0 go build \
3335
-ldflags="-w -s -X main.Version=${VERSION} -X main.Commit=${COMMIT} -X main.BuildDate=${BUILD_DATE} -X main.GoVersion=$(go env GOVERSION)" \
3436
-a \
@@ -42,7 +44,5 @@ COPY --from=ghcr.io/tarampampam/microcheck:1 /bin/httpcheck /bin/httpcheck
4244
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
4345
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
4446
COPY --from=builder /app/solis /app
45-
COPY --from=frontend /frontend/dist /app/frontend/dist
46-
COPY --from=docs /docs/dist /app/docs/dist
4747
EXPOSE 8080
4848
ENTRYPOINT ["/app/solis"]

‎Dockerfile.goreleaser‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Built by goreleaser: the binary, frontend/dist and docs/dist are prebuilt
1+
# Built by goreleaser: the binary is prebuilt with frontend/dist and docs/dist embedded
22
# (see before.hooks in .goreleaser.yaml) and copied into the build context.
33
# ca-certificates and zoneinfo are arch-independent: this stage runs natively on the
44
# build host, so no step here needs QEMU (release.yml still sets it up, as goreleaser requires).
@@ -12,8 +12,6 @@ COPY --from=ghcr.io/tarampampam/microcheck:1 /bin/httpcheck /bin/httpcheck
1212
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
1313
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
1414
COPY $TARGETPLATFORM/solis /app
15-
COPY frontend/dist /app/frontend/dist
16-
COPY docs/dist /app/docs/dist
1715

1816
EXPOSE 8080
1917
ENTRYPOINT ["/app/solis"]

‎Makefile‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,28 @@ COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
66
DATE := $(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
77
GOVERSION := $(shell go version | awk '{print $$3}')
88

9-
# Version shown in the web UI (frontend/public/data/version.json); goreleaser overrides it.
10-
VITE_GIT_COMMIT_HASH ?= $(VERSION)
11-
129
# Build flags for small binary
1310
LDFLAGS := -s -w
1411
BUILD_FLAGS := -ldflags "-X main.Version=$(VERSION) -X main.Commit=$(COMMIT) -X main.BuildDate=$(DATE) -X main.GoVersion=$(GOVERSION) $(LDFLAGS)"
1512

13+
# The web assets are built with the same version (VITE_APP_VERSION) as the binary; the
14+
# frontend compares it with GET /api/version. goreleaser passes VERSION=<release version>.
15+
16+
# Embeds whatever frontend/dist and docs/dist hold: run `make assets` first (or `make all`).
1617
.PHONY: build
1718
build:
1819
CGO_ENABLED=0 go build $(BUILD_FLAGS) -o $(BINARY_NAME) ./cmd
1920

20-
# Web assets the binary serves from disk (./frontend/dist, ./docs/dist).
21+
# Web assets embedded into the binary (frontend/dist, docs/dist); build them before `build`.
2122
.PHONY: frontend
2223
frontend:
2324
npm --prefix frontend ci
24-
VITE_GIT_COMMIT_HASH=$(VITE_GIT_COMMIT_HASH) npm --prefix frontend run build
25+
VITE_APP_VERSION=$(VERSION) npm --prefix frontend run build
2526

2627
.PHONY: docs
2728
docs:
2829
npm --prefix docs ci
29-
npm --prefix docs run build
30+
VITE_APP_VERSION=$(VERSION) npm --prefix docs run build
3031

3132
.PHONY: assets
3233
assets: frontend docs
@@ -35,7 +36,6 @@ assets: frontend docs
3536
.PHONY: docker
3637
docker:
3738
VERSION=$(VERSION) COMMIT=$(COMMIT) BUILD_DATE=$(DATE) \
38-
VITE_GIT_COMMIT_HASH=$(VITE_GIT_COMMIT_HASH) \
3939
docker compose -f docker-compose.dev.yaml build
4040

4141
.PHONY: clean

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Addresses, types and scales are defined in `internal/solis/table.go`.
110110
```
111111
GET /health # 200 ok|degraded, 503 failed
112112
GET /api/keys # all registers + metadata
113+
GET /api/version # build info (binary, UI and docs)
113114
GET /api/data/{key} # current value
114115
GET /api/data/{daily_key}?start=2024-01-01&end=2024-01-31
115116
GET /api/data/{monthly_key}?start=2024-01&end=2024-12
@@ -164,7 +165,8 @@ refuses periods whose daily rows retention already deleted.
164165
## Development
165166

166167
```bash
167-
make build # binary with version info
168+
make all # web assets + binary (the UI and API docs are embedded)
169+
make build # binary with version info; embeds the assets of the last `make assets`
168170
make check # format, lint, race tests, deadcode, govulncheck (check-only)
169171
make docker # dev image with version info (docker-compose.dev.yaml)
170172
docker compose -f docker-compose.dev.yaml up

‎cmd/main.go‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
_ "time/tzdata" // fallback zoneinfo so TZ=Europe/Berlin never silently becomes UTC
1919

2020
"github.com/dombyte/solis/internal/app"
21+
"github.com/dombyte/solis/internal/buildinfo"
2122
"github.com/dombyte/solis/internal/config"
2223
"github.com/dombyte/solis/internal/logging"
2324
)
@@ -35,10 +36,11 @@ var (
3536
GoVersion = "unknown"
3637
)
3738

38-
// buildInfo renders the build information for the startup log and `solis version`.
39-
func buildInfo() string {
40-
return fmt.Sprintf("solis %s (commit %s, built %s, %s)", Version, Commit, BuildDate,
41-
GoVersion)
39+
// buildInfo collects the linker-set build information.
40+
func buildInfo() buildinfo.Info {
41+
return buildinfo.Info{
42+
Version: Version, Commit: Commit, BuildDate: BuildDate, GoVersion: GoVersion,
43+
}
4244
}
4345

4446
func main() {
@@ -78,7 +80,7 @@ func runCommand(args []string, configPath string, stdout, stderr io.Writer) int
7880
printUsage(stdout)
7981
return 0
8082
case "version":
81-
_, _ = fmt.Fprintln(stdout, buildInfo())
83+
_, _ = fmt.Fprintln(stdout, buildInfo().String())
8284
return 0
8385
default:
8486
// #nosec G705 -- writes to the process's own stderr, not an HTTP response

‎cmd/main_test.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ func TestDispatch_HelpAndUnknown(t *testing.T) {
3636
assert.Contains(t, out.String(), "solis [-config PATH] backfill --years N")
3737
out.Reset()
3838
assert.Equal(t, 0, dispatch([]string{"version"}, &out, &errOut))
39-
assert.Equal(t, buildInfo()+"\n", out.String())
39+
assert.Equal(t, buildInfo().String()+"\n", out.String())
4040
assert.Contains(t, out.String(), "solis dev (commit unknown")
4141
out.Reset()
4242
assert.Equal(t, 0, dispatch([]string{"--version"}, &out, &errOut))
43-
assert.Equal(t, buildInfo()+"\n", out.String())
43+
assert.Equal(t, buildInfo().String()+"\n", out.String())
4444
assert.Equal(t, 0, dispatch([]string{"-h"}, &out, &errOut), "help is not an error")
4545
assert.Contains(t, errOut.String(), "-config PATH")
4646
assert.Equal(t, 1, dispatch([]string{"-nope"}, &out, &errOut))

‎cmd/serve.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ func runServer(ctx context.Context, configPath string) error {
1818
for _, w := range cfg.Warnings {
1919
root.Warn().Str("component", "config").Msg(w)
2020
}
21-
return app.Run(ctx, cfg, root)
21+
return app.Run(ctx, cfg, buildInfo(), root)
2222
}

‎docker-compose.dev.yaml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ services:
55
context: .
66
dockerfile: Dockerfile
77
args: # set by `make docker`; plain `docker compose build` falls back to dev/unknown
8-
VITE_GIT_COMMIT_HASH: ${VITE_GIT_COMMIT_HASH:-dev}
98
VERSION: ${VERSION:-dev}
109
COMMIT: ${COMMIT:-unknown}
1110
BUILD_DATE: ${BUILD_DATE:-unknown}

0 commit comments

Comments
 (0)