Shot Counter
{{ counterOnly ? 'Sign in to view the counter.' : 'Sign in to access the operator console.' }}

Access restricted

The operator console is available to members of {{ adminGroupName }}.

Signed in as {{ auth.username }}.

{{ pageTitle }}

{{ pageSubtitle }}

{{ nowLocal }} · Berlin
Live · Fleet count
Last event · {{ formatRelative(kafkaStatus.last_event_at) }}
Live · Authoritative count
Last event · {{ formatRelative(kafkaStatus.last_event_at) }}
Today’s shots
{{ dailyShots ? formatNumber(dailyShots.today_count) : '—' }}
{{ dailyShots.difference > 0 ? '+' : dailyShots.difference < 0 ? '−' : '±' }}{{ formatNumber(Math.abs(dailyShots.difference)) }} vs yesterday Yesterday · {{ formatNumber(dailyShots.yesterday_count) }}
Berlin · Recorded brews
Processed
{{ formatNumber(kafkaStatus.processed_events_total) }}
Skipped
{{ formatNumber(kafkaStatus.skipped_events_total) }}
Online devices
{{ onlineCount }}
Paused devices
{{ pausedCount }}

Recent events

Latest counted / skipped messages.

{{ event.event_id || 'no-event-id' }} {{ event.action }}
{{ formatTime(event.created_at) }} {{ event.iot_device_id || 'unknown-device' }} #{{ formatNumber(event.resulting_count) }}
No Kafka events yet.

Fleet snapshot

Current state of registered counters.

{{ deviceLabel(device) }} {{ device.connected ? 'Online' : 'Offline' }}
{{ device.device_id }}
Display count
Last edge ACK · {{ formatRelative(device.acknowledged_display_at) }}
No devices registered yet.

Startup greeting

Show HELLO before the count when a counter starts. Applies to the entire fleet on the next startup.

Loading greeting settings…

Historical count

Live counting starts at the Kafka positions captured by Reset counter. Add the earlier history here.

Reset the counter below to capture starting positions and generate the SQL query.

Danger zone

Environment: {{ countSettings?.environment || 'Loading…' }}

Erase counts, event and command history, closed sessions, and deduplication records. Clear manual display overrides and restart active event tallies at zero. Devices, credentials, pause and schedule settings, and command sequence numbers are preserved.

The counter captures the latest Kafka positions and starts at zero. Historical counting can then be completed above.

Enable Kafka to capture new starting positions.

Live · Current count
Last event · {{ formatRelative(kafkaStatus.last_event_at) }}
Processed
{{ formatNumber(kafkaStatus.processed_events_total) }}
Skipped
{{ formatNumber(kafkaStatus.skipped_events_total) }}
Total events
{{ formatNumber(kafkaStatus.total_events) }}
Last event
{{ formatTime(kafkaStatus.last_event_at) }}
{{ filteredKafkaEvents.length }} / {{ kafkaRecentEvents.length }} rows
Visible columns {{ visibleKafkaColumns.length }} / {{ kafkaColumnLayout.length }}
{{ column.label }}
Drag to reorder.
{{ column.label }}
No events match the current filter.
{{ filteredDevices.length }} / {{ devices.length }} devices
{{ device.location || device.device_id }}
{{ device.connected ? 'Online' : 'Offline' }}
Last acknowledged display Event mode
Current event count{{ formatNumber(device.actual_count) }}
Last ACK · {{ formatRelative(device.acknowledged_display_at) }}
{{ (device.event_source_device_ids || []).length }} event sources Started {{ formatClock(device.event_mode_started_at) }}
  • {{ source }}
Last seen {{ formatRelative(device.last_seen_at) }} · Agent {{ device.agent_version || 'unknown' }}
No devices match the current filter.

{{ deviceLabel(selectedDevice) }}

{{ selectedDevice.location || selectedDevice.device_id }}
{{ selectedDevice.connected ? 'Online' : 'Offline' }}
Last acknowledged displayEvent mode
Current event count{{ formatNumber(selectedDevice.actual_count) }}
Last ACK · {{ formatRelative(selectedDevice.acknowledged_display_at) }}

Commands are queued until the device reconnects.

Send count now updates the display once. Pause and quiet hours remain active.

Last seen {{ formatRelative(selectedDevice.last_seen_at) }}

Counting mode

{{ selectedDevice.event_mode ? 'Event mode' : 'Fleet count' }}

Shows the fleet count. Start an event to count brews from selected sources at zero.

Started {{ formatTime(selectedDevice.event_mode_started_at) }} · {{ (selectedDevice.event_source_device_ids || []).length }} sources. Counting continues while display updates are paused.

  • {{ source }}
Separate IDs with commas or new lines. {{ selectedDevice.event_mode ? 'Adding sources preserves the count and start time.' : 'Only brews after entry count. The fleet count continues normally.' }}

Quiet hours

{{ selectedDevice.quiet_hours_active ? 'Active' : selectedDevice.quiet_hours_start ? 'Scheduled' : 'Off' }}

{{ selectedDevice.quiet_hours_start ? `${selectedDevice.quiet_hours_start}–${selectedDevice.quiet_hours_end} every day · Europe/Berlin` : 'No daily schedule. Display updates follow the current pause state.' }}

Automatic display updates pause during the blue interval. Counts keep accumulating.

Activity

Display updates, schedule changes, and commands. Times in Berlin.

{{ activityTitle(row) }}{{ activitySummary(row) }}
{{ row.details }}
No activity yet. Schedule changes and display commands will appear here.

Send command

Dispatch a raw command payload to the selected display.

Explicit commands also run during pauses and quiet hours.

Accepts number, JSON, boolean, null, or plain text.

Device information

Device ID
{{ selectedDevice.device_id }}
Location
{{ selectedDevice.location || 'Not set' }}
Agent
{{ selectedDevice.agent_version || 'Unknown' }}
Last heartbeat
{{ formatTime(selectedDevice.last_heartbeat_at) }}
Manual override
{{ selectedDevice.manual_override == null ? 'None' : formatNumber(selectedDevice.manual_override) }}

Device history

Unified timeline — command dispatch, acknowledgements, and telemetry events.

{{ filteredDeviceTimeline.length }} entries
Time (Berlin) Kind Name Status Source
No matching history entries.
No devices registered. Devices appear here once they connect.
Target devices
{{ fleetAnimationTargets.length === devices.length ? `All devices (${devices.length})` : `${fleetAnimationTargets.length} of ${devices.length} devices` }}
Choose animation target devices

Untick devices to exclude them. Overview filters do not affect targets.

{{ t.message }}