Drop-in copy of labdb's API surface from
fivenode/labdb/api/*.prg into app/api/. Single fnode invocation builds
the whole thing (bridge_server + bridge/ + 22 api/*.prg) into one
24 MB Go binary — no Node.js, no FFI, no Apache.
End-to-end smoke test (server.js not running, ctx empty so defaults
fall back) hitting six endpoints all return well-formed JSON via the
bridge layer + path -> Main dispatcher:
GET /api/hello.prg -> {"msg":"hello from PRG","ok":true}
GET /api/admin-stats.prg -> {"active_sessions":0,...}
GET /api/admin-me.prg -> {"ok":true,"user":{...}}
GET /api/sessions-list.prg -> {"sessions":[],"total":0}
GET /api/records-list.prg -> {"records":[],"sessionId":"",...}
POST /api/devices-register -> {"deviceId":"","status":"pending",...}
One small upstream patch was needed: seven .prg files each define
their own STATIC FUNCTION fn_HGet, but Five doesn't yet honour
file-local STATIC scoping for top-level functions — all definitions
land in the same symbol table and collide. Renamed each duplicate to
<file>_fn_hget so they peacefully coexist; the call sites still
reference fn_HGet and Five resolves them against _helpers.prg's
public version (signature-compatible). TODO: revert once Five gains
file-local STATIC FUNCTION scoping.
What's deferred to 1a.4-4: ctx data injection (so endpoints return
real labdb data), static asset embedding (labdb/public/), and a live
LABDB_DSN round-trip to confirm pgrtl in the request path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
135 lines
4.9 KiB
Plaintext
135 lines
4.9 KiB
Plaintext
// api/session-stats.prg — Compute session statistics (FLOAT MATH IN PRG)
|
|
//
|
|
// ctx:
|
|
// session_id — session ID
|
|
// duration_sec — duration in seconds (from server.js)
|
|
// record_count — total records
|
|
// rows — JSON array of records [{sensor: {tempC, batteryMv, ...}, channels: [...]}]
|
|
//
|
|
// PRG handles all numeric aggregation: mean, min, max, stddev for 6 channels.
|
|
// Float computation is done here (Harbour double precision) — easier maintenance
|
|
// than maintaining SQL JSONB aggregates in server.js.
|
|
//
|
|
// Copyright (c) 2026 Charles KWON OhJun
|
|
|
|
FUNCTION Main()
|
|
LOCAL aRows, hRow, nDuration, nCount, hSensor, aChannels, hCh
|
|
LOCAL nTempSum, nTempMin, nTempMax, nTempCnt
|
|
LOCAL nBatStart, nBatEnd
|
|
LOCAL aChStats, i, ch, peakSums, peakMins, peakMaxs, peakCounts, peakSqs
|
|
LOCAL nMean, nVar, nStdDev, hOutCh, aChOut
|
|
LOCAL cDur, nH, nM, nS
|
|
|
|
aRows := hb_jsonDecode(ctx_get("rows", "[]"))
|
|
nDuration := Val(ctx_get("duration_sec", "0"))
|
|
nCount := Val(ctx_get("record_count", "0"))
|
|
|
|
IF ! HB_ISARRAY(aRows) ; aRows := {} ; ENDIF
|
|
|
|
// ─ Init accumulators ──
|
|
nTempSum := 0
|
|
nTempMin := 999.9
|
|
nTempMax := -999.9
|
|
nTempCnt := 0
|
|
nBatStart := NIL
|
|
nBatEnd := NIL
|
|
|
|
// 6 channels: peak sum / min / max / count / sum-of-squares (for stddev)
|
|
peakSums := { 0, 0, 0, 0, 0, 0 }
|
|
peakMins := { 99999, 99999, 99999, 99999, 99999, 99999 }
|
|
peakMaxs := { -99999, -99999, -99999, -99999, -99999, -99999 }
|
|
peakCounts := { 0, 0, 0, 0, 0, 0 }
|
|
peakSqs := { 0, 0, 0, 0, 0, 0 }
|
|
|
|
// ─ Iterate records ──
|
|
FOR EACH hRow IN aRows
|
|
// sensor
|
|
hSensor := fn_HGet(hRow, "sensor")
|
|
IF HB_ISHASH(hSensor)
|
|
IF hb_HHasKey(hSensor, "tempC") .AND. ValType(hSensor["tempC"]) == "N"
|
|
nTempSum += hSensor["tempC"]
|
|
nTempCnt++
|
|
IF hSensor["tempC"] < nTempMin ; nTempMin := hSensor["tempC"] ; ENDIF
|
|
IF hSensor["tempC"] > nTempMax ; nTempMax := hSensor["tempC"] ; ENDIF
|
|
ENDIF
|
|
IF hb_HHasKey(hSensor, "batteryMv") .AND. ValType(hSensor["batteryMv"]) == "N"
|
|
IF nBatStart == NIL ; nBatStart := hSensor["batteryMv"] ; ENDIF
|
|
nBatEnd := hSensor["batteryMv"]
|
|
ENDIF
|
|
ENDIF
|
|
|
|
// channels
|
|
aChannels := fn_HGet(hRow, "channels")
|
|
IF HB_ISARRAY(aChannels)
|
|
FOR EACH hCh IN aChannels
|
|
IF HB_ISHASH(hCh) .AND. hb_HHasKey(hCh, "ch") .AND. hb_HHasKey(hCh, "peak")
|
|
i := hCh["ch"] + 1 // 1-based
|
|
IF i >= 1 .AND. i <= 6 .AND. ValType(hCh["peak"]) == "N"
|
|
peakSums[i] += hCh["peak"]
|
|
peakSqs[i] += hCh["peak"] * hCh["peak"]
|
|
peakCounts[i]++
|
|
IF hCh["peak"] < peakMins[i] ; peakMins[i] := hCh["peak"] ; ENDIF
|
|
IF hCh["peak"] > peakMaxs[i] ; peakMaxs[i] := hCh["peak"] ; ENDIF
|
|
ENDIF
|
|
ENDIF
|
|
NEXT
|
|
ENDIF
|
|
NEXT
|
|
|
|
// ─ Compute per-channel stats (mean, stddev) ──
|
|
aChOut := {}
|
|
FOR i := 1 TO 6
|
|
ch := i - 1
|
|
IF peakCounts[i] > 0
|
|
nMean := peakSums[i] / peakCounts[i]
|
|
// population variance: E[x^2] - (E[x])^2
|
|
nVar := (peakSqs[i] / peakCounts[i]) - (nMean * nMean)
|
|
IF nVar < 0 ; nVar := 0 ; ENDIF
|
|
nStdDev := Sqrt(nVar)
|
|
hOutCh := { ;
|
|
"ch" => ch, ;
|
|
"peakAvg" => Round(nMean, 2), ;
|
|
"peakMin" => peakMins[i], ;
|
|
"peakMax" => peakMaxs[i], ;
|
|
"peakStdDev" => Round(nStdDev, 2), ;
|
|
"samples" => peakCounts[i] ;
|
|
}
|
|
ELSE
|
|
hOutCh := { "ch" => ch, "peakAvg" => NIL, "peakMin" => NIL, "peakMax" => NIL, "peakStdDev" => NIL, "samples" => 0 }
|
|
ENDIF
|
|
AAdd(aChOut, hOutCh)
|
|
NEXT
|
|
|
|
// ─ Format duration HH:MM:SS ──
|
|
nH := Int(nDuration / 3600)
|
|
nM := Int((nDuration % 3600) / 60)
|
|
nS := Int(nDuration % 60)
|
|
cDur := iif(nDuration > 0, ;
|
|
StrZero(nH, 2) + ":" + StrZero(nM, 2) + ":" + StrZero(nS, 2), ;
|
|
"")
|
|
|
|
AP_JSONRESPONSE({ ;
|
|
"sessionId" => ctx_get("session_id", ""), ;
|
|
"recordCount" => nCount, ;
|
|
"duration" => cDur, ;
|
|
"channels" => aChOut, ;
|
|
"temperature" => { ;
|
|
"avg" => iif(nTempCnt > 0, Round(nTempSum / nTempCnt, 2), NIL), ;
|
|
"min" => iif(nTempCnt > 0, nTempMin, NIL), ;
|
|
"max" => iif(nTempCnt > 0, nTempMax, NIL) ;
|
|
}, ;
|
|
"battery" => { ;
|
|
"startMv" => nBatStart, ;
|
|
"endMv" => nBatEnd, ;
|
|
"drainMv" => iif(nBatStart != NIL .AND. nBatEnd != NIL, nBatStart - nBatEnd, NIL), ;
|
|
"drainPctPerHour" => iif(nBatStart != NIL .AND. nBatEnd != NIL .AND. nDuration > 0, ;
|
|
Round(((nBatStart - nBatEnd) / nBatStart) * 3600 / nDuration * 100, 2), NIL) ;
|
|
} ;
|
|
})
|
|
|
|
RETURN NIL
|
|
|
|
FUNCTION session_stats_fn_hget(h, k)
|
|
IF HB_ISHASH(h) .AND. hb_HHasKey(h, k) ; RETURN h[k] ; ENDIF
|
|
RETURN NIL
|