Skip to main content

Inside the Drawer

The drawer is one component swapping between three views. ActivityLogContent holds a panel integer and returns early for two of the three:

panelRendersReached from
0the list (an inline, memo'd ActivityLogList)default, and Back from either panel
1AddMessagePanelthe ✉ Create Message header icon
2ActivitySettingsPanelthe ⚙ Settings header icon

Both panels receive BackButton and CloseButton as pre-bound components. They are wrapped in useCallback so typing in the settings form does not re-render the header on every keystroke.

If you are changing behaviour rather than looking something up, read how the activity log works first — the grouping and pinning live there.

The list

Each ActivityLogGroup renders as one row, styled by sourceType:

sourceTypeRow styleExpansion shows
CameraLeasemoveRownested movement list, plus a per-entry Reset Camera button
ActivityLogchatRowreason code and message text
AIDetectionaiRowContentthe image carousel below

High-priority rows get a red stripe and an icon; rows still inside their pin window also get a Pinned badge and a data-pinned="true" attribute (which is what the tests key off).

Reset Camera to Position appears in a collapsed lease row's header — and only there — when all of these hold: ENABLE_RESET, the row is not expanded, sourceType is CameraLease, reasonCode is neither Focus Adjustment nor Brightness Adjustment, and both pan and tilt are numbers. Clicking it calls onMoveCameraTo(entry) from cameraMovementStore and closes the drawer.

The header also carries Expand All / Collapse All, and an AI checkbox that only renders when the user has AI access and the current list actually contains AI groups. When filters are hiding anything, a banner shows N of M items with a Clear Filters button that resets settings to defaultActivityLogSettings — which is the empty object, so it clears every checkbox rather than restoring the all-true defaults.

Adding a message

AddMessagePanel is a two-field form: a High Priority checkbox and a message capped at 150 characters, with a live N/150 characters counter. Confirm is enabled only when the message is non-empty, and disabled again while submitting so a double-click cannot post twice.

The payload posted to POST /api/add-message is small:

{
cameraName: cameraSelected.cId,
message,
isHighPriority,
isSystem: false, // always — this branch only creates user messages
}

On success the panel clears the field, calls setPanel(0), and flips requested back to false with loading to true, which is what makes ActivityLogContent re-fetch and show the new row. On failure it fires an error through notificationStore. Cancel just returns to the list.

Filtering and sorting

ActivitySettingsPanel writes to localStorage on every change (via saveSettingsToLocalStorage, under the key activity-log-settings) and pushes the new object up through setSettings. There is no Save button; there is no dirty state.

The settings object is one flat map of string → boolean | string, whose legal keys are enumerated in ActivityLogSettingsStructure:

KeyKindMeaning
updated-onstringISO timestamp, stamped automatically on save
sort-byradiorecent (default) or oldest
start-date / end-datedateISO strings; empty means that bound is off
type-activitylogbooleanshow sourceType === 'ActivityLog' (labelled "Message")
type-cameraleasebooleanshow sourceType === 'CameraLease' ("Camera event")
type-automatedbooleanshow isSystem === true ("Automated Camera event")
reason-<code>booleanone per REASON_CODES entry, lowercased
reason-camera resetbooleanhardcoded extra, not from REASON_CODES
urgencyradioall (default) or urgent (isHighPriority only)

filterActivityLog applies date, type, urgency, and reason as four independent passes. Each group of checkboxes is pass-through when nothing in it is checked — no type checked means all types show, no reason checked means all reasons show. That is why "Clear All" does not empty the list. Unrecognised reason codes fall into reason-other reason.

Every checkbox label carries a count in parentheses, computed by countActivityLogSettings(activitylog) over the full unfiltered log — so the number tells you what you would get, not what you currently have.

AI detection rows

AIAnomalyGroupContent renders a Swiper carousel of the snapshot images an AI group collected — group.allEntries[].message holds the image URL for AI entries, not text.

Two loading guards, because a log can hold many of these:

  • The carousel is not mounted until the row scrolls near the viewport. A one-shot IntersectionObserver with rootMargin: '200px' sets inView, then disconnects; before that the row shows a skeleton.
  • Each slide tracks its own load in a Set<number>, showing a per-image skeleton until onLoad fires.

Desktop gets arrow navigation, mobile and landscape get clickable dot pagination. A FloatingExpandButton swaps the container class to a fullscreen one — the expansion is pure CSS. Below the carousel, the timestamp of the current slide renders as 24-hour HH:mm:ss. An empty image list renders No images available instead of the carousel.

Props

ActivityLogContent is the only one of these you mount yourself:

PropTypeNotes
cameraSelectedCameraLocationAndImageModel | nullrequired; cId is used for fetch and message posts
onDrawerClose() => voidrequired
fetchedActivityLogActivityLogResponse[] | nulloptional; skips the mount fetch when the parent already has data

fetchedActivityLog only covers the first render — skipInitialFetch is a ref that clears itself, so every later re-request (after posting a message, for instance) really does hit the API. It has to, because only fetchActivityLog clears loading.

The two sub-panels take cameraSelected, the bound BackButton/CloseButton, and their own state handles: AddMessagePanel gets setPanel, setRequested and setLoading; ActivitySettingsPanel gets settings, setSettings, and the full unfiltered activitylog for its counts. AIAnomalyGroupContent takes a single group.