feat(hub): add hlsEnabled toggle; replace anpr example with loitering

- Add kerberoshub.frontend.features.liveview.hlsEnabled (default true) and
  wire it as FEATURE_HLS_ENABLED on hub-frontend and hub-frontend-demo. When
  'false' the front-end removes the HLS live-transport option.
- Replace the bundled anpr example stage/worker with a commented-out
  hub-loitering example; the chart now ships no enabled custom stage by
  default (stages are values-only, opt-in).
- Update the generic hub-stage comment to the loitering example.
- Make the queue-consistency check self-contained: it synthesises a neutral
  throwaway stage ('queuecheck') via --set instead of relying on a bundled
  example worker.

NOTE: Chart.yaml version intentionally not bumped — repo version/tag state is
already inconsistent (Chart.yaml 0.117.0 vs tags up to hub-0.119.0); pick the
next version at release time.
This commit is contained in:
Kilian
2026-06-25 16:33:16 +00:00
parent 8781ede494
commit 0e21755bb8
5 changed files with 102 additions and 89 deletions

View File

@@ -243,6 +243,8 @@ spec:
# features > liveview # features > liveview
- name: FEATURE_LIVE_STREAM_MODE - name: FEATURE_LIVE_STREAM_MODE
value: "{{ .Values.kerberoshub.frontend.features.liveview.liveStreamMode }}" value: "{{ .Values.kerberoshub.frontend.features.liveview.liveStreamMode }}"
- name: FEATURE_HLS_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.liveview.hlsEnabled }}"
- name: FEATURE_LIVEVIEW_PAGINATION_MODE - name: FEATURE_LIVEVIEW_PAGINATION_MODE
value: "{{ .Values.kerberoshub.frontend.features.liveview.paginationMode }}" value: "{{ .Values.kerberoshub.frontend.features.liveview.paginationMode }}"
- name: FEATURE_LIVEVIEW_PAGE_SIZE - name: FEATURE_LIVEVIEW_PAGE_SIZE

View File

@@ -328,6 +328,8 @@ spec:
value: "{{ .Values.kerberoshub.frontend.features.liveview.defaultStreamMode }}" value: "{{ .Values.kerberoshub.frontend.features.liveview.defaultStreamMode }}"
- name: FEATURE_LIVE_STREAM_MODE - name: FEATURE_LIVE_STREAM_MODE
value: "{{ .Values.kerberoshub.frontend.features.liveview.liveStreamMode }}" value: "{{ .Values.kerberoshub.frontend.features.liveview.liveStreamMode }}"
- name: FEATURE_HLS_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.liveview.hlsEnabled }}"
- name: FEATURE_LIVEVIEW_PAGINATION_MODE - name: FEATURE_LIVEVIEW_PAGINATION_MODE
value: "{{ .Values.kerberoshub.frontend.features.liveview.paginationMode }}" value: "{{ .Values.kerberoshub.frontend.features.liveview.paginationMode }}"
- name: FEATURE_LIVEVIEW_PAGE_SIZE - name: FEATURE_LIVEVIEW_PAGE_SIZE

View File

@@ -8,10 +8,10 @@
Every stage worker receives the same connection contract; the only value that Every stage worker receives the same connection contract; the only value that
varies by stage is the consume-queue variable name, <NAME>_QUEUE (a stage varies by stage is the consume-queue variable name, <NAME>_QUEUE (a stage
keyed "anpr" gets ANPR_QUEUE, "my-stage" gets MY_STAGE_QUEUE). To run a worker keyed "loitering" gets LOITERING_QUEUE, "my-stage" gets MY_STAGE_QUEUE). To run
outside the chart instead, leave services.<name>.enabled unset (or false) a worker outside the chart instead, leave services.<name>.enabled unset (or
while keeping the stage under workflows.stages so the engine still routes to false) while keeping the stage under workflows.stages so the engine still
the queue you publish. routes to the queue you publish.
*/ -}} */ -}}
{{- if and (or (eq .Values.mode "all") (eq .Values.mode "pipeline")) .Values.kerberoshub.workflows.enabled -}} {{- if and (or (eq .Values.mode "all") (eq .Values.mode "pipeline")) .Values.kerberoshub.workflows.enabled -}}
{{- $root := . -}} {{- $root := . -}}

View File

@@ -475,6 +475,7 @@ kerberoshub:
liveview: liveview:
defaultStreamMode: "SD" # Default stream mode 'SD' or 'HD' (will be migrated to 'preview' or 'live') defaultStreamMode: "SD" # Default stream mode 'SD' or 'HD' (will be migrated to 'preview' or 'live')
liveStreamMode: "webrtc" # Transport backing the LIVE (HD) mode 'webrtc' (default) or 'hls' (firewall-friendly, no TURN required) liveStreamMode: "webrtc" # Transport backing the LIVE (HD) mode 'webrtc' (default) or 'hls' (firewall-friendly, no TURN required)
hlsEnabled: "true" # Offer HLS as a selectable LIVE transport 'true' or 'false'. When 'false' the HLS option is removed from the front-end and streams use webrtc
paginationMode: "scroll" # Pagination mode in live view 'scroll', 'numbered' or 'maxStreams' paginationMode: "scroll" # Pagination mode in live view 'scroll', 'numbered' or 'maxStreams'
pageSize: "6" # Max streams shown per page when paginationMode is 'numbered' (4, 8, 12, 16 or 25) pageSize: "6" # Max streams shown per page when paginationMode is 'numbered' (4, 8, 12, 16 or 25)
maxStreams: "-1" # Maximum number of live streams to show in live view, -1 for unlimited maxStreams: "-1" # Maximum number of live streams to show in live view, -1 for unlimited
@@ -641,60 +642,57 @@ kerberoshub:
# workflows.stages.<name>.enabled -> include the stage in the registry (route to it) # workflows.stages.<name>.enabled -> include the stage in the registry (route to it)
# services.<name>.enabled -> deploy the worker (pipe-<name>.yaml) # services.<name>.enabled -> deploy the worker (pipe-<name>.yaml)
stages: stages:
# hub-anpr — automatic number-plate recognition stage. Routing only; its # ---------------------------------------------------------------------
# worker deployment lives under kerberoshub.services.anpr. # EXAMPLE custom stage (commented out) — hub-loitering.
anpr: #
# Include this stage in the engine's registry (route to it). # The stages below ship no enabled entries by default; this is a worked
enabled: false # example of how to register a custom workflow stage. Uncomment this block
# operation unique stage id (defaults to the key "anpr" if omitted); # (and the matching kerberoshub.services.loitering worker further down) to
# binds the queue (taken from services.anpr.queue) and how # route runs to the hub-loitering demo worker, which emits a timeline
# the result is recorded. # marker. Routing only; the worker deployment lives under
# dispatch "always" (run on every workflow) | "conditional". # kerberoshub.services.loitering. See https://github.com/uug-ai/hub-loitering.
# needs conditional stages only: upstream dependencies, each #loitering:
# {operation?, condition}. operation is the readiness GATE — # # Include this stage in the engine's registry (route to it).
# the upstream op whose data must be present before the # enabled: true
# condition is read; leave it empty for a check on the run # # operation unique stage id (defaults to the key "loitering" if omitted);
# root itself (device/user/identity), read as soon as the run # # binds the queue (taken from services.loitering.queue) and how
# opens. condition shape: # # the result is recorded.
# {path: <abs-path>, op: eq|ne|contains|in|exists|gt|gte|lt|lte, value: <operand>}. # # dispatch "always" (run on every workflow) | "conditional".
# path is ABSOLUTE from the run root and resolves through # # needs conditional stages only: upstream dependencies, each
# string-keyed maps only (it CANNOT index into arrays): # # {operation?, condition}. operation is the readiness GATE —
# inputs.<op>.<field> (a trigger result, e.g. classify), # # the upstream op whose data must be present before the
# results.<op>.<field> (a finished stage), device.<field>, # # condition is read; leave it empty for a check on the run
# user.<field>, or a top-level scalar (operation/runId/key). # # root itself (device/user/identity), read as soon as the run
# classify is the only operation teed to workflows, so it is # # opens. condition shape:
# the reliable upstream: match inputs.classify.properties (the # # {path: <abs-path>, op: eq|ne|contains|in|exists|gt|gte|lt|lte, value: <operand>}.
# detected class strings) with `contains` — there is no # # path is ABSOLUTE from the run root and resolves through
# top-level `label`, and inputs.classify.details is an array a # # string-keyed maps only (it CANNOT index into arrays):
# path cannot index into — or gate on inputs.classify.objectCount # # inputs.<op>.<field> (a trigger result, e.g. classify),
# (top-level int) numerically. The engine rejects an unknown # # results.<op>.<field> (a finished stage), device.<field>,
# path at boot. # # user.<field>, or a top-level scalar (operation/runId/key).
# needsMode conditional stages with more than one need: how they # # classify is the only operation teed to workflows, so it is
# combine. "any" (default) fires on the first matching # # the reliable upstream: match inputs.classify.properties (the
# need; "all" is a join — the stage fires only once every # # detected class strings) with `contains` — there is no
# need has resolved and each condition matches, and only once. # # top-level `label`, and inputs.classify.details is an array a
operation: anpr # # path cannot index into — or gate on inputs.classify.objectCount
dispatch: conditional # # (top-level int) numerically. The engine rejects an unknown
# The demo worker hands back a detection block (the car detection it # # path at boot.
# compiles from the classify boxes, in the run Payload) that the engine's # # needsMode conditional stages with more than one need: how they
# shared ingest core persists into the detections collection, so the # # combine. "any" (default) fires on the first matching
# recording's edit-media modal shows the car boxes. The engine routes # # need; "all" is a join — the stage fires only once every
# ingest by each block's type, so no per-stage wiring is needed here. # # need has resolved and each condition matches, and only once.
# Default routing — run anpr whenever classify reports a car (any camera). # operation: loitering
needsMode: any # dispatch: conditional
needs: # # The demo worker hands back a marker block (a named span on the
- operation: classify # # recording's timeline, measured from the classify trajectory) that the
condition: {path: "inputs.classify.properties", op: contains, value: car} # # engine's shared ingest core persists into the markers collection, so
# Restrict plate recognition to a SINGLE camera: add an ungated need on # # the marker shows on the recording's timeline. The engine routes ingest
# the recording's device (empty operation = checked as soon as the run # # by each block's type, so no per-stage wiring is needed here.
# opens) and switch needsMode to "all", so both must hold — "that camera # # Default routing — run loitering whenever classify reports a person.
# AND a car was detected". For several cameras use op: in with a list. # needsMode: any
#needsMode: all # needs:
#needs: # - operation: classify
# - operation: classify # condition: {path: "inputs.classify.properties", op: contains, value: person}
# condition: {path: "inputs.classify.properties", op: contains, value: car}
# - operation:
# condition: {path: "device.deviceKey", op: eq, value: device02}
# Workflow deployments. Every workflows-subsystem Deployment's image/tag/ # Workflow deployments. Every workflows-subsystem Deployment's image/tag/
# replicas/resources/queue lives here in a single, uniform shape: # replicas/resources/queue lives here in a single, uniform shape:
# - `workflows` is the engine itself (the orchestrator). It is deployed # - `workflows` is the engine itself (the orchestrator). It is deployed
@@ -732,33 +730,36 @@ kerberoshub:
requests: requests:
memory: 10Mi memory: 10Mi
cpu: 10m cpu: 10m
# hub-anpr — automatic number-plate recognition worker. Lives in the # ---------------------------------------------------------------------
# hub-workflows repository as its own module. See # EXAMPLE custom stage worker (commented out) — hub-loitering.
# https://github.com/uug-ai/hub-workflows/tree/main/hub-anpr. #
anpr: # Companion deployment for the kerberoshub.workflows.stages.loitering example
# Deploy the hub-anpr worker. # above. Uncomment both to deploy the demo worker. It ships as its own
enabled: false # repository/module. See https://github.com/uug-ai/hub-loitering.
repository: ghcr.io/uug-ai/hub-anpr #loitering:
pullPolicy: IfNotPresent # # Deploy the hub-loitering worker.
tag: "v1.0.0" # enabled: true
replicas: 1 # Number of pods for the worker. # repository: ghcr.io/uug-ai/hub-loitering
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none). # pullPolicy: IfNotPresent
#volumes: # tag: "v1.0.0"
# - name: extra # replicas: 1 # Number of pods for the worker.
# emptyDir: {} # topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
#volumeMounts: # #volumes:
# - name: extra # # - name: extra
# mountPath: /data # # emptyDir: {}
logLevel: "info" # possible values: trace, debug, info, warn, error # #volumeMounts:
# Queue this worker consumes dispatched messages from (ANPR_QUEUE). This # # - name: extra
# same value is read into the engine's generated registry entry for the # # mountPath: /data
# matching stage, so the engine dispatches and the worker consumes the same # logLevel: "info" # possible values: trace, debug, info, warn, error
# queue with no drift. Convention: "kcloud-<operation>-queue.fifo". # # Queue this worker consumes dispatched messages from (LOITERING_QUEUE). This
queue: "kcloud-anpr-queue.fifo" # # same value is read into the engine's generated registry entry for the
resources: # # matching stage, so the engine dispatches and the worker consumes the same
requests: # # queue with no drift. Convention: "kcloud-<operation>-queue.fifo".
memory: 10Mi # queue: "kcloud-loitering-queue.fifo"
cpu: 10m # resources:
# requests:
# memory: 10Mi
# cpu: 10m
monitordevice: monitordevice:
repository: ghcr.io/uug-ai/hub-monitor-device repository: ghcr.io/uug-ai/hub-monitor-device
pullPolicy: IfNotPresent pullPolicy: IfNotPresent

View File

@@ -21,13 +21,21 @@ CHART_DIR="${1:-charts/hub}"
PROBE="drift-probe-queue-name" PROBE="drift-probe-queue-name"
# Flags that force all three deployment kinds (analysis, engine and one stage # Flags that force all three deployment kinds (analysis, engine and one stage
# worker) to render, so the check actually has something to compare. anpr is a # worker) to render, so the check actually has something to compare. The chart
# stage/worker shipped in the chart's default values. # ships NO enabled stage worker by default (custom stages are values-only and
# opt-in), so we synthesise a throwaway stage purely to exercise the generic
# hub-stage path. The name is a neutral fixture ("queuecheck") on purpose: any
# arbitrary stage key must render the same way, so the check must not depend on
# a specific bundled worker.
STAGE="queuecheck"
RENDER_FLAGS=( RENDER_FLAGS=(
--set mode=all --set mode=all
--set kerberoshub.workflows.enabled=true --set kerberoshub.workflows.enabled=true
--set kerberoshub.workflows.stages.anpr.enabled=true --set "kerberoshub.workflows.stages.${STAGE}.enabled=true"
--set kerberoshub.services.anpr.enabled=true --set "kerberoshub.services.${STAGE}.enabled=true"
--set "kerberoshub.services.${STAGE}.repository=example.invalid/queuecheck"
--set "kerberoshub.services.${STAGE}.tag=test"
--set "kerberoshub.services.${STAGE}.queue=queuecheck-fixture-queue"
) )
# Read `helm template` output on stdin and print one WORKFLOWS_QUEUE value per # Read `helm template` output on stdin and print one WORKFLOWS_QUEUE value per