Server: rewrite in Go as an API-only service
Same endpoints, request formats, validation rules and InfluxDB layout as
the Next.js server, so the car needs no change: POST /v1/telemetry,
PUT /v1/consumption/daily, PUT /v1/charging-sessions/{id}, GET
/v1/consumption/history/{id}, GET /v1/health. The dashboard, login and
the /v1/history reads are gone; Grafana takes over the dashboards.
- Go 1.26 standard library (net/http routing, log/slog) plus the
official InfluxDB 2 client. Fails closed at startup on a missing
InfluxDB setting or an ingest token under 24 characters.
- ABRP forwarder ported: newest fresh sample, rate limited, never
queued, credentials only in the form body.
- 22 tests: validation, aggregation, forwarding, config, and the
handlers through a fake store. govulncheck clean (x/net 0.60.0,
toolchain go1.26.9).
- Image: static binary on distroless, about 11 MB, UID 65532, port
8080; the build runs vet and the tests.
- Dev shell: Go, gopls and govulncheck instead of Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
import "sort"
|
||||
|
||||
// aggregateDays folds chunks into days, newest day first, each with its running lifetime
|
||||
// total. Pure, so the arithmetic is tested without a database.
|
||||
func aggregateDays(chunks []ConsumptionChunk) []DailyConsumption {
|
||||
type day struct {
|
||||
DailyConsumption
|
||||
sohAt int64
|
||||
hasSoh bool
|
||||
}
|
||||
byDate := map[string]*day{}
|
||||
for _, chunk := range chunks {
|
||||
d, ok := byDate[chunk.Date]
|
||||
if !ok {
|
||||
d = &day{DailyConsumption: DailyConsumption{
|
||||
InstallationID: chunk.InstallationID,
|
||||
Date: chunk.Date,
|
||||
RecordedAt: chunk.RecordedAt,
|
||||
}}
|
||||
byDate[chunk.Date] = d
|
||||
}
|
||||
d.Day.add(chunk.Day)
|
||||
if chunk.RecordedAt.After(d.RecordedAt) {
|
||||
d.RecordedAt = chunk.RecordedAt
|
||||
}
|
||||
// The newest manual SoH of the day wins.
|
||||
at := chunk.RecordedAt.UnixMilli()
|
||||
if chunk.SohPercent != nil && (!d.hasSoh || at > d.sohAt) {
|
||||
soh := *chunk.SohPercent
|
||||
d.SohPercent = &soh
|
||||
d.sohAt = at
|
||||
d.hasSoh = true
|
||||
}
|
||||
}
|
||||
days := make([]DailyConsumption, 0, len(byDate))
|
||||
for _, d := range byDate {
|
||||
days = append(days, d.DailyConsumption)
|
||||
}
|
||||
sort.Slice(days, func(i, j int) bool { return days[i].Date < days[j].Date })
|
||||
var running Totals
|
||||
for i := range days {
|
||||
running.add(days[i].Day)
|
||||
days[i].Lifetime = running
|
||||
}
|
||||
for i, j := 0, len(days)-1; i < j; i, j = i+1, j-1 {
|
||||
days[i], days[j] = days[j], days[i]
|
||||
}
|
||||
return days
|
||||
}
|
||||
Reference in New Issue
Block a user