Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Added
.dockerignoreshrinking the Docker build context (414 kB → 312 kB) and keeping the builder stage'sCOPY . .layer from being invalidated by edits to docs, CI workflows, Taskfile or shell scripts. It also stops a developer's gitignoredconfig.toml/.envfrom ever reaching the build context. Deliberately an explicit denylist:main.goembedsCHANGELOG.mdandinternal/storage/migrate.goembedsmigrations/*.sql, so a blanket*.mdrule needs the!CHANGELOG.mdnegation andqueries.sqlmust be excluded by exact path rather than a*.sqlglob — both constraints are documented in the file's header (#131)
Changed
- Published
matrixise/rmm-trackerimages are nowlinux/amd64only, matching the only architecture actually deployed to production. Dropped the QEMU setup step and thelinux/arm64platform target fromdocker-publish.yml, and thelinux/arm64target from thedocker:buildx:pushtask (#130).README.md,CLAUDE.mdanddocs/DOCKER_HUB_SETUP.mdno longer advertise multi-arch images — in particular the claim that the published image runs on Apple Silicon and Raspberry Pi, which it does not (#131) Dockerfile's builder stage buildsFROM golang:1.27-alpine(up from Go 1.26) directly, instead of from the custommatrixise/rmm-tracker-builderimage (#128). Modules are fetched by a dedicatedRUN go mod downloadlayer that bind-mounts onlygo.mod/go.sum(so it is invalidated by a dependency change, not by every source edit) into a BuildKit cache mount shared with thego buildlayer via the sameid=go-mod. Same "nogo mod downloadon the critical path" effect as the prebuilt builder image, with no registry round-trip and no cross-workflow coordination: the publish workflow no longer serialises an extra job that pushes an image to Docker Hub just so the next job can pull it back down, and the fragilegrep 'FROM' Dockerfile.buildertag-derivation shell snippet that already broke once (#129) is gone with it. Thego mod verifythatDockerfile.builderran is not carried over:go mod downloadalready checks every module againstgo.sum, so it only added build time (#131)Dockerfile's builder stage now builds with-trimpathand-ldflags "-s -w", stripping the symbol table and DWARF debug info and removing absolute build-path prefixes from the binary. Measured onlinux/amd64: 33 706 211 → 23 769 248 bytes (32.14 → 22.66 MiB, -29.5%). The four-Xversion injections (Version/GitBranch/GitCommit/BuildTime) are unaffected —-s -wstrips debug data, not the linker-set string variablescmd/version.goreads (#131)Dockerfile's builder stage setsGOTOOLCHAIN=localinstead ofauto.autolet Go silently download and switch to a different toolchain mid-build, a network dependency and a reproducibility hole in an image build;localpins the build to the base image's Go. Ago.modbump past the base image's Go version now fails the build loudly instead, and must be matched by bumping the base image (#131)Dockerfile's runtime stage pinsalpine:3.24instead ofalpine:latest. An unpinnedlatestsilently moves the runtime userland from one build to the next, which defeats reproducibility and makes "which base was in the image that broke?" unanswerable after the fact. 3.24 is the current stable branch (released 2026-06-09, supported to 2028-06-01) and is whatlatestresolved to at the time of the change (3.24.1), so the pin froze the contents rather than changing them (#131)docker-compose.yml'sapphealthcheck runs BusyBoxwgetinstead ofcurl:wget -q -O /dev/null http://localhost:8080/health. This matchescurl -f— BusyBox wget exits non-zero on any HTTP >= 400 — and it was verified to issue a realGET, which matters because/healthanswers 405 to anything that is not a GET (BusyBox's--spideralso uses GET, unlike GNU wget's HEAD, but the explicit-O /dev/nullform avoids depending on that). Errors still print under-q, sodocker inspect's healthcheck log shows the failing status line. Verified on all four paths: 200 → exit 0, 404 → exit 1, 503 → exit 1, connection refused → exit 1 (#131)docker-publish.ymlnow records why itstype=ghacache is not paired withreproducible-containers/buildkit-cache-dance.type=ghaexports image layers only, so theDockerfile'sid=go-mod/id=go-buildcache mounts do not survive between runs andgo build— whose layerCOPY . .invalidates on every commit — always compiles cold. The dance was measured rather than assumed, and rejected: it works (auto-discovery derives both mounts from theirid=, and a build on the injected builder went from 70.6s to 1.7s locally), but the mounts are 263 MB/2878 files (go-build) and 393 MB/11 596 files (go-mod), and moving them costs 76s of inject+extract for both or 31s forgo-buildalone — against ago buildthat takes only 28.1s on a real runner.skip-extractioncannot avoid that cost, since it only fires on an exact cache-key hit and the whole premise is that the source changed. The comment records the numbers and the ~90sgo buildthreshold at which the trade would flip (#131)
Removed
- Cross-compilation machinery from
Dockerfile, now that onlylinux/amd64is ever built:--platform=$BUILDPLATFORMon the builder stage, the buildx-injectedTARGETOS/TARGETARCHARGs and their non-empty assertions, the explicitGOOS/GOARCHongo build, thego version -mbuild-metadata readback, and the final stage's ELFe_machinearchitecture guard (one fewer layer in the runtime image). That guard was added to catch the issue #124 failure mode — a defaultedARG TARGETARCH=amd64masking the value buildx injects, which is what put an amd64 binary inside the publishedlinux/arm64image (#125). With a single target platform the failure mode cannot occur, so the guards go with the cross-compilation they guarded; Go now builds for the builder image's native architecture andCGO_ENABLED=0is unchanged. A header comment inDockerfilerecords that the whole pattern must be restored iflinux/arm64is ever reintroduced (#131) Dockerfile.builderand thematrixise/rmm-tracker-builderimage it produced, thedocker-builder.ymlworkflow that rebuilt it, thebuild-builderjob indocker-publish.yml(and theneeds:/BUILDER_IMAGEplumbing around it —build-and-push's ownif:gate already stands alone, so removing the dependency does not change when the publish runs), and thedocker:builder:build/docker:builder:pushtasks plus the now-unusedDOCKER_BUILDER_IMAGEandGO_VERSIONvars fromTaskfile.yml(#128, #129). All replaced by the BuildKit cache/bind mounts described above. The deadGO_VERSION=1.26variable inMakefile, which nothing consumed and which had drifted from the actual toolchain, is gone too (#131)- Optional golangci-lint pass from
Dockerfile's builder stage, along with itsARG ENABLE_LINT=falseand theRUNlayer that executed anechoon every build when disabled. It was redundant with the two places that already lint — thelintjob in.github/workflows/test.ymland thegolangci-lintpre-commit hook, both pinned tov2.10.1— and worse, it installedgolangci-lint@latestunpinned, so anENABLE_LINT=truebuild could enforce a different version than CI.docs/IMPROVEMENTS.mdupdated to point atprek run -aand the CI job instead of the removed build arg (#131) curlfromDockerfile's runtime stage. Its only in-image consumer was theapphealthcheck indocker-compose.yml, which now uses thewgetapplet BusyBox already ships; the remainingcurlinvocations inMakefile,Taskfile.yml,deploy.shandtest-health.shall run on the host, not in the container. Dropping the package takes theapk addlayer from 6.54 MB to 1.22 MB and the whole runtime image from 39.42 MB to 34.13 MB of uncompressed layers (-13.4%), and removes libcurl, nghttp2, brotli, c-ares, libidn2 and zstd from the image's CVE surface.ca-certificatesis deliberately kept — the app makes HTTPS JSON-RPC calls to Gnosis Chain and cannot verify TLS without a trust store (#131)
Fixed
docker-compose.yml'sapphealthcheck could never pass, and had been failing since it was added. The image ENTRYPOINT is./rmm-tracker run, andrun's--httpflag defaults to"", so the HTTP server never started and nothing ever listened on :8080 — the check failed with connection refused on every interval (reproduced against the pre-change image:curl -fexited 7). The service now passes--http :8080 --interval 5mascommand:. The interval has to move fromRMM_TRACKER_INTERVALto the flag becausecmd/run.godeliberately ignores the config/env interval once--httpis set, so flags are the only way to get the scheduler and the health server simultaneously (#131)- Weekly period yield box on the wallet detail page showed the same start/end date instead of the true week range
- Weekly report table now shows one row per consecutive week pair instead of a single row aggregated over the entire requested window
- Weekly report table rows spanned 14 days instead of 7:
week_start/week_endincorrectly stretched from the previous bucket to the current bucket + 7 days instead of describing the current bucket's own week (#127) - Wallet detail page made responsive on mobile: address wraps with
break-all, tables scroll horizontally, padding adapts to screen size (#52)
Security
Dockerfile's runtime stage no longer runs as root. It creates a system user and switches toUSER 65532:65532— the same numeric id distroless uses fornonroot, chosen so host-side file ownership stays valid if the stage is ever moved there. Nothing in the runtime code path writes to the filesystem and the health server binds :8080, so root bought the process nothing but blast radius; the binary is deliberately left root-owned and non-writable so the process cannot modify itself. Note the operational consequence fordocker-compose.yml's./config.toml:/app/config.toml:robind mount: the host file must be world-readable (0644) or owned by uid 65532, otherwise the app exits at startup withfailed to read config: permission denied— a 0600 config.toml, a natural mode for a file that may carry RPC credentials, will not start. Documented inREADME.mdandCLAUDE.md(#131)
0.1.0 - 2026-03-01
Added
- Version number displayed in the navigation bar
- Changelog page at
/changelogrendering this file as HTML - Yield endpoints registered and covered by tests (#41)
- Wallet address search on the
/walletspage (#35) - Current balances section on the wallet detail page (#34)
- In-memory cache for dashboard summary statistics
- Unified
/api/v1/dashboardendpoint - Real last-run timestamp tracking in the database
- Git branch name included in build info
- Period yield display on the wallet detail page (#28)
- Web UI migrated to Alpine.js with templ templating, APY panic fixed, and hot reload (#27)
- Configurable log format (text/json) via
--log-formatflag --httpflag now accepts a custom listen address- Taskfile validation in CI and build info exposed in the health endpoint (#25)
- PostgreSQL integration tests with CI service container (#14)
- Weekly report endpoint for Prefect integration
- APY calculation and configurable weekly period in JSON API (#9)
Balancefield migrated from string todecimal.Decimalfor precision- Goose database migrations and correct scheduler
RunNowordering - gocron scheduler, GitHub Actions CI/CD, and deployment tooling
- HTTP health check endpoint for daemon mode
- Automatic RPC failover client with retry logic
- Multi-endpoint RPC support with backward compatibility in config
- Multi-architecture Docker builds (AMD64 + ARM64)
- Docker buildx registry cache for faster builds
docker:tagstask to list published Docker Hub tags (#47)
Changed
- Tailwind CSS CDN updated from v3 to v4.2 (#30)
- Binary size reduced by 30% using
-s -w -trimpathbuild flags (#29) - CQRS interfaces (Commander / Querier) introduced in storage layer (#31)
--daemon,--http, and--crondecoupled as explicit CLI flagsSetLastRunrenamed toSetLastRunStatusfor clarity (#36)- Database name renamed from
realt_rmmtormm_tracker - Project renamed from
realt-rmmtormm-tracker - Docker build optimized with
BUILDPLATFORMand native cross-compilation - JSON API fields uniformized to snake_case (#8)
- Architecture modernized: Cobra, Viper, pgx, and validator (#26 tooling updated)
Fixed
- Wallet addresses normalized to lowercase throughout the codebase (#48)
- Go module cache corrected in CI builds (#33)
- Concise error message returned on database connection failure
github_tokenpassed to claude-code-action to enable PR commentslatestDocker tag added to images pushed from themainbranchconfig.tomlremoved from Dockerfile to avoid leaking credentials- Docker bridge network made explicit for reliable DNS resolution
- Pull-requests write permission added to the code-review workflow
- deploy.sh and test-env.sh scripts translated to English (#43)
- MD060 table separator spacing fixed in Web UI documentation (#42)