feat(webui): sporadic-trigger capture, CSV export and UI rework
Brings the Go hub and web SPA work developed on feature/udpscope onto main, without the udpscope client itself. The trigger engine could not capture a sporadic event: it armed on the live tail only, so a burst shorter than one push window was already past by the time the FSM looked for it. It now searches the ring history for the crossing, which also makes a capture reproducible from the same data rather than dependent on push timing (wshub/trigger.go, ringbuf.go, history.go). Adds CSV/JSON export of the visible window (wshub/export.go) and reworks the SPA: per-signal axis controls, a readable trigger panel, and a fix for the flicker caused by repainting on every push instead of on a frame tick (static/app.js, index.html, style.css). BUFFER_AND_TRIGGER.md documents the ring/decimation/trigger interaction, which is otherwise only inferable from the three files that implement it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
cf815e1d3f
commit
f334995865
@@ -221,6 +221,19 @@ func ringCoverage(bucket, capacity int) int {
|
||||
return capacity / 2 * bucket
|
||||
}
|
||||
|
||||
// captureLagSec is how much further back than the window itself a ring has to
|
||||
// reach to deliver a capture of it.
|
||||
//
|
||||
// A capture is not read out when its last sample arrives but captureMarginSec
|
||||
// later, and then only on the next push tick — so by the time the window is
|
||||
// extracted, its oldest sample is that much deeper in the ring. A ring holding
|
||||
// exactly the window has already overwritten the front of its own capture, which
|
||||
// is what made every shot at a short window come back missing its head. The
|
||||
// pre/post split does not enter into it: the harvest is a post-window after the
|
||||
// trigger and the read reaches a pre-window before it, so the two sum to the
|
||||
// window whatever the split.
|
||||
const captureLagSec = captureMarginSec + 1.0/30.0
|
||||
|
||||
// activeWindowSec is the timespan the buffers must cover. An armed trigger owns
|
||||
// it: its pre-window has to already be in the ring when the trigger fires or
|
||||
// there is nothing to back-fill the capture from. Otherwise it is the widest
|
||||
@@ -228,7 +241,7 @@ func ringCoverage(bucket, capacity int) int {
|
||||
func (h *Hub) activeWindowSec() float64 {
|
||||
if h.trigger != nil && h.trigger.Active() {
|
||||
if cfg := h.trigger.Config(); cfg.windowSec > 0 {
|
||||
return cfg.windowSec
|
||||
return cfg.windowSec + captureLagSec
|
||||
}
|
||||
}
|
||||
widest := 0.0
|
||||
|
||||
Reference in New Issue
Block a user