Overview
Category: Compound / data display. Selector: sense-audit-timeline. The component is a pure renderer β no HTTP, no router, no global state. Every capability arrives as a typed input; every interaction leaves as a typed output. New behaviour is default-off, so adopting it never changes existing behaviour. The filtering, sorting, grouping, clustering, histogram, anomaly, and serialization logic are exported as independently-usable pure functions.
Feature catalogue
Severity, category, actor, date-range, and debounced full-text search β all ANDed, with an active-count badge and clear-all.
Windowed rendering keeps the DOM bounded (<100 rows) for 50k+ event histories at 60fps.
Push deltas in real time; merge + de-dupe by id, "N new events" pill, and pause-to-read.
Collapse hundreds of near-identical events into a single expandable count row.
Host-declared, scoped action buttons that emit typed events β the component never acts.
Injection-safe CSV + JSON of the filtered set, with an optional built-in download.
Emit typed navigation intents for events that reference a resource β router stays in the host.
Reflect the selected id back for a highlighted row + aria-current, even when virtualized.
A brushable minimap over the full set that drives the date-range filter.
Deterministic heuristics (error spikes, off-hours, repeated failures) flag noteworthy events.
Serialize the filter to a shareable, refresh-surviving URL; render named preset chips.
Opt-in button emits the visible window for a host-owned AI summary β no network calls inside.
Override the row, actor, metadata, empty, loading, and summary rendering via templates.
Re-label, recolour, reorder, or extend severities/categories via a DI token.
Intl-driven locale formatting, RTL, and a fixed timeZone for compliance "all times UTC".
Type-aware value rendering (JSON, URLs, booleans) with copy-to-clipboard.
Installation
npm install @artificialsenselabs/enterprisePeer dependency: @angular/core ^21.2.0. Standalone β import the component directly.
Quick start
import { Component, signal } from '@angular/core';
import { SenseAuditTimelineComponent } from '@artificialsenselabs/enterprise';
import type {
SenseAuditEvent,
SenseAuditEventSelectEvent,
SenseAuditTimelineFilter,
} from '@artificialsenselabs/enterprise';
@Component({
standalone: true,
imports: [SenseAuditTimelineComponent],
template: `
<sense-audit-timeline
[events]="events()"
[loading]="loading()"
colorScheme="auto"
(eventSelect)="onSelect($event)"
(filterChange)="onFilter($event)"
/>
`,
})
export class AuditPanel {
events = signal<readonly SenseAuditEvent[]>([]);
loading = signal(false);
onSelect(e: SenseAuditEventSelectEvent) { /* open detail drawer */ }
onFilter(f: SenseAuditTimelineFilter) { /* persist / server-fetch */ }
}Architecture & principles
- Purity is the product. No HTTP, router, or global singletons. Capabilities in as inputs; interactions out as typed outputs.
- Default-off. Streaming, clustering, histogram, anomalies, export, summarize β all gated behind inputs that default to the original behaviour.
- Pure logic layer. Filtering, clustering, histogram, anomaly, and serialization are exhaustively unit-tested pure functions.
- A11y is not a phase. Roving tabindex, aria-live announcements, focus management, and axe-clean states ship with every feature.
Inputs 23
| Name | Type | Default | Description |
|---|---|---|---|
events | readonly SenseAuditEvent[] | [] | The full set of audit events to display. The component normalises, filters, sorts, and groups this array reactively β it never mutates the input. Defaults to `[]` (empty list). |
filter | SenseAuditTimelineFilter | null | null | External filter state. When provided, the component's internal filter UI is synchronised to match these values on first render and whenever they change. Emitting `filterChange` from the component does **not** push back into this input β the host controls authority. Defaults to `null` (no external filter; internal UI is the source of truth). |
loading | boolean | false | When `true` the component shows a loading indicator instead of the event list. Pass `true` while the initial data fetch is in flight and `false` once the first batch of events arrives. Defaults to `false`. |
maxItems | number | null | null | Hard cap on the number of events passed to the filter/sort/group pipeline. Useful as a safety rail when the host cannot server-side page the result set. Events beyond `maxItems` are silently discarded **before** filtering, so filter counts may be lower than the full dataset. Defaults to `null` (no cap). |
i18nOverrides | Partial<SenseAuditTimelineI18n> | null | null | Partial overrides for every user-visible string in the component. Only the keys you supply are replaced β all others fall back to the English defaults in . Defaults to `null` (no overrides; English strings used throughout). |
styleClass | string | null | null | Additional CSS class(es) to add to the host element alongside the BEM block class `sense-audit-timeline`. Useful for layout utility classes (`w-full`, `flex-1`, etc.) without breaking encapsulation. Defaults to `null` (no extra classes). |
virtualScroll | boolean | true | When true (default), the event list is windowed once it exceeds rows, keeping the DOM node count bounded for very large histories. Set to false to always render every row (useful for print/export views or when an outer container handles scrolling). |
colorScheme | SenseAuditColorScheme | 'auto' | Colour scheme the component applies to itself. - `'auto'` (default) β follows `prefers-color-scheme`. - `'light'` / `'dark'` β forced regardless of the OS setting. The scheme is reflected as a host class (`sense-audit-timeline--dark` / `sense-audit-timeline--light`) so host-app CSS can also key off it. |
liveEvents | readonly SenseAuditEvent[] | null | null | Delta batch of incoming live events. Each time the host updates this signal the component merges and de-dupes by `id`: - **New ID** β prepended to the top of the list; the "N new events" pill appears, and the aria-live region announces the arrival. - **Existing ID (in live cache)** β the cached event is updated in-place; no duplicate is created and no new-count increment occurs. - **ID already in `events()`** β ignored (the base snapshot is authoritative). Set to `null` (default) to disable live mode. For RxJS streams, see the README section "Live streaming β RxJS adapter". |
paused | boolean | false | When `true`, incoming live events are buffered instead of merged, so an investigator can pause the feed while reading. Setting back to `false` flushes the buffer in one batch. Defaults to `false` (live mode active). |
clustering | SenseAuditClusteringOptions | null | null | Controls burst grouping. When `enabled` is `true`, consecutive events that share the same action/category (configurable via `by`) and fall within `windowMs` of each other are collapsed into a single cluster row showing a count badge. Clicking a cluster row expands it to show the individual events. Defaults to `null` (off) β the component renders each event individually, preserving backward-compatible behaviour. |
actions | readonly SenseAuditEventAction[] | [] | Contextual remediation actions to render on matching event rows. Each action defines a label and optional scope filters (severity, category, predicate) β the component shows it only on events that pass all filters. Clicking an action button emits ; it does NOT emit `eventSelect`. The component is a pure renderer and never executes the action itself. Defaults to `[]` (no action buttons rendered). |
selectedId | string | null | null | ID of the currently-selected event. When set, the matching event row receives a `sense-audit-timeline__item--selected` BEM modifier and an `aria-current="true"` attribute so both sighted and assistive-technology users can identify the selection. The component never selects an event itself β the host reflects back whichever event it received via `eventSelect`. Defaults to `null` (no row highlighted). Works with virtualization: when the selected row is scrolled off-screen the modifier is applied automatically as soon as the row re-enters the rendered viewport. |
enableExport | boolean | false | When `true`, "Export CSV" and "Export JSON" buttons appear in the toolbar. Clicking either button: 1. Emits with the currently-filtered event set. 2. Triggers a client-side file download via a Blob so the user can save the file immediately β no server round-trip required. Set to `false` (default) to hide the buttons. The host can still listen to regardless of this setting when the buttons are shown. |
showHistogram | boolean | false | When `true`, an optional compact density histogram strip is rendered above the toolbar. The histogram buckets the **full unfiltered** event set so it always reflects the complete data shape regardless of any active filters. - Click or drag across a region to apply a matching `from`/`to` date-range filter (emits `filterChange`). - Two accessible `<input type="range">` controls below the bars provide an equivalent keyboard interface. Defaults to `false` (feature is off; no DOM impact). |
histogramBuckets | unknown | DEFAULT_HISTOGRAM_BUCKETS | Number of equal-width time buckets to split the event timeline into when rendering the histogram strip. Higher values give more granular brushing; lower values produce thicker, easier-to-click bars. Only meaningful when is `true`. Defaults to `30`. |
anomalyRules | readonly AnomalyRule[] | [] | Heuristic anomaly-detection rules applied to the full unfiltered event set. When an event matches one or more rules a subtle `β ` badge is rendered on that event row carrying a tooltip that explains the reason. Each rule is a plain object with a `kind` discriminant plus optional threshold/window overrides β no callbacks or class instances required. **All rules are default-off.** Passing `[]` (default) produces no markers and zero computational overhead. |
savedViews | readonly SavedView[] | [] | Named filter presets rendered as quick-select chips above the filter bar. Clicking a chip applies its `filter` to the component's internal state and emits `filterChange` β exactly as if the user had set all the controls manually. The active chip (whose filter matches the current state) receives a visual highlight and `aria-pressed="true"`. Defaults to `[]` (no chips rendered; zero DOM impact). Use / to build the filter values from a URL query parameter for a refresh-surviving deep link. |
enableSummarize | boolean | false | When `true`, a "Summarize" button is rendered in the toolbar. Clicking it emits with the **currently-visible** (filtered) event set and the active filter β the host calls the AI service and writes the result back via and . The component is a **pure renderer** β it never makes a network request. Defaults to `false` (button hidden; zero DOM impact). |
summaryText | string | null | null | The AI-generated summary text produced by the host after receiving a event. Pass `null` (default) to show nothing or to clear a previous summary. Only meaningful when a slot is provided. |
summaryLoading | boolean | false | Set to `true` while the host is waiting for the AI service to respond. The value is forwarded to the slot context as `loading` so the host template can render a spinner. Defaults to `false`. |
locale | string | null | null | BCP 47 locale tag used for all date/time formatting within the component (absolute timestamps, relative times, and day-group headers). Defaults to `null`, which causes the component to use the Angular `LOCALE_ID` token value injected at module/component level (typically `'en-US'` unless the app provides a different value). |
timeZone | string | null | null | IANA timezone identifier for all date/time display and day-boundary grouping (e.g. `'UTC'`, `'America/New_York'`, `'Asia/Tokyo'`). When `null` (default) the browser / Node local timezone is used, which matches the existing behavior and keeps the feature default-off. Setting `timeZone="UTC"` is recommended for compliance dashboards where "all times UTC" is a regulatory requirement. |
Outputs 6
| Name | Payload | Description |
|---|---|---|
eventSelect | SenseAuditEventSelectEvent | Emitted when the user clicks (or activates via keyboard) an event row. The payload contains the raw that was activated. Reflect the `event.id` back to the component via the `selectedId` input to highlight the selected row and set `aria-current="true"`. |
filterChange | SenseAuditTimelineFilter | Emitted whenever the internal filter state changes β severity chips, category chips, actor select, date-range inputs, or search field. The payload is the full snapshot so the host can persist, share, or route it without diffing individual keys. |
actionInvoke | SenseAuditActionInvokeEvent | Emitted when the user activates a remediation action button on an event. Does NOT fire alongside `eventSelect` β propagation is stopped. |
eventExport | SenseAuditExportEvent | Emitted when an export button is clicked. The payload contains the requested format and the **currently-filtered** event set (i.e. exactly what the user sees β not the full `events()` input). The host can use the payload to drive a custom download, post the data to an audit-log API, or open a preview modal. When is `true` the component also triggers a built-in Blob download automatically. |
resourceSelect | SenseAuditResourceSelectEvent | Emitted when the user activates the "View resource" affordance on an event that carries both `resourceId` and `resourceType`. The affordance is rendered automatically β no input is required. The component never calls the router itself (ADR-0032 Β§4): the host handles routing. Clicking the affordance does **not** also emit `eventSelect`. |
summarizeRequest | SenseAuditSummarizeRequest | Emitted when the user clicks the "Summarize" button (requires `enableSummarize = true`). The payload contains the **currently-visible** (post-filter) event set and the active filter snapshot so the host has everything needed to build an AI prompt. **The component never calls any AI service.** The host owns the model interaction and writes the result back via the `summaryText` and `summaryLoading` inputs. |
Content-projection slots
Pass a TemplateRef to override the built-in rendering. With no slot supplied, output is identical to the default.
| Slot input | Context type | Description |
|---|---|---|
summaryTemplate | SenseAuditSummaryContext | Optional template rendered between the toolbar and the event list whenever the host has a summary to display (loading state, text, or both). The template receives a as context: ```html <ng-template #mySummary let-text let-loading="loading" let-filter="filter"> |
eventRowTemplate | SenseAuditEventRowContext | Optional custom template for an event row. When provided it completely replaces the built-in row UI for every event. The template receives a as context: ```html <ng-template #myRow let-event let-isSelected="isSelected" let-formattedTime="formattedTime"> <div [class.selected]="isSelected">{{ event.action }} β {{ formattedTime }}</div> </ng-template> <sense-audit-timeline [events]="events" [eventRowTemplate]="myRow" /> ``` When `null` (default) the built-in row template is used β **output is identical to before this feature was introduced**. |
actorTemplate | SenseAuditActorContext | Optional custom template for the actor chip inside an event row. Replaces the built-in avatar + name + email block. The template receives a as context: ```html <ng-template #myActor let-actor let-initials="initials"> <strong>{{ actor.name }}</strong> ({{ initials }}) </ng-template> ``` Only takes effect when `eventRowTemplate` is **not** set (a fully custom row template owns its own actor rendering). Defaults to `null`. |
metadataTemplate | SenseAuditMetadataContext | Optional custom template for the metadata content panel (the `<dl>` that appears after the user expands an event row). The expand-button affordance is still managed by the component; this slot only replaces the panel itself. The template receives a as context: ```html <ng-template #myMeta let-entries let-event="event"> <ul>@for (e of entries; track e.key) { <li>{{ e.key }}: {{ e.value }}</li> }</ul> </ng-template> ``` Only takes effect when `eventRowTemplate` is **not** set. Defaults to `null`. |
emptyTemplate | unknown | Optional custom template shown in place of the built-in "No events found" paragraph when the visible event list is empty. No context is passed. Defaults to `null` (built-in empty-state message). |
loadingTemplate | unknown | Optional custom template shown in place of the built-in "Loadingβ¦" paragraph when `loading` is `true`. No context is passed. Defaults to `null` (built-in loading message). |
Types
All exported from @artificialsenselabs/enterprise.
| Type | Shape | Description |
|---|---|---|
SenseAuditEvent | object | A single immutable audit entry: id, timestamp, action, severity, category + optional description, actor, metadata, resourceId, resourceType. |
SenseAuditActor | object | Principal responsible for an event: id (required), name, email?, avatarUrl?. |
SenseAuditEventSeverity | 'info' | 'warning' | 'error' | 'success' | Closed union of urgency levels (extendable via the config token). |
SenseAuditEventCategory | 'auth' | 'data' | 'system' | 'user' | 'config' | 'workflow' | Closed union of functional domains (extendable via the config token). |
SenseAuditTimelineFilter | object | Filter criteria (all ANDed): severity[], category[], actorId, from, to, search. Omit a key to leave it unrestricted. |
SenseAuditColorScheme | 'light' | 'dark' | 'auto' | Colour-scheme input values. |
SenseAuditClusteringOptions | object | Burst-grouping config: enabled, windowMs?, by? (action|category|actorId). |
SenseAuditEventAction | object | A host-declared remediation action: id, label, severityScope?, categoryScope?, predicate?. |
SenseAuditEventSelectEvent | { event } | eventSelect payload. |
SenseAuditActionInvokeEvent | { action, event } | actionInvoke payload. |
SenseAuditResourceSelectEvent | { resourceType, resourceId, event } | resourceSelect payload. |
SenseAuditExportEvent | { format, events } | eventExport payload (format = csv | json). |
SenseAuditExportFormat | 'csv' | 'json' | Available serialisation formats. |
SenseAuditSummarizeRequest4.4 | { events, filter } | summarizeRequest payload β visible events + active filter. May contain PII. |
SenseAuditSummaryContext4.4 | { $implicit, loading, filter } | Context for the summaryTemplate slot. |
SenseAuditEventRowContext | object | Context for eventRowTemplate. |
SenseAuditActorContext | object | Context for actorTemplate. |
SenseAuditMetadataContext | object | Context for metadataTemplate. |
SenseAuditTimelineConfig2.2 | object | Taxonomy override value for the config token: severities?, categories?. |
SenseAuditSeverityMeta2.2 | object | Per-severity display metadata: label?, color?, order?. |
SenseAuditCategoryMeta2.2 | object | Per-category display metadata: label?, color?, icon?, order?. |
SenseAuditTimelineI18n | object | All user-visible strings (18 keys). |
MetadataValueKind2.4 | union | Detected value kind: string | number | boolean | null | timestamp | url | object. |
RichMetadataEntry2.4 | object | Enriched metadata entry: key, kind, display, copyText, href. |
SavedView4.3 | { id, label, filter } | A named filter preset chip. |
AnomalyRule4.2 | union | repeated-failures | error-spike | off-hours | same-category-burst (each with threshold/window options). |
AnomalyAnnotation4.2 | { kind, reason } | One anomaly flag attached to an event. |
HistogramBucket4.1 | object | A single histogram time bucket: from, to, count, dominant severity. |
DayGroup / ClusteredDayGroup | object | A calendar-day group of events (or clustered items). |
EventCluster1.2 | object | A collapsed burst of consecutive similar events. |
TimelineRow | union | A flattened render row: header | event | cluster (virtualization). |
DI tokens & constants
| Symbol | Type | Description |
|---|---|---|
SENSE_AUDIT_TIMELINE_CONFIG | InjectionToken<SenseAuditTimelineConfig> | Optional DI token to customise taxonomy labels/colors/order and add extended severity/category values. |
SENSE_AUDIT_TIMELINE_DEFAULT_I18N | SenseAuditTimelineI18n | The default English string set. Spread it as a base when building a locale file. |
SENSE_AUDIT_SEVERITY_COLORS | Record<string, string> | Severity hex values for programmatic use (charts). The component itself uses CSS variables. |
SENSE_AUDIT_TIMELINE_PACKAGE | string | The package name constant. |
DEFAULT_HISTOGRAM_BUCKETS | number (30) | Default histogram bucket count. |
VIRTUALIZE_THRESHOLD | number (100) | Row count above which virtualization engages. |
ALL_SEVERITIES / ALL_CATEGORIES | readonly arrays | The built-in severity and category union values. |
CSV_COLUMNS | readonly CsvColumn[] | Default CSV column set used by toCsv. |
ACTOR_PALETTE_SIZE | number (8) | Number of deterministic avatar colours. |
Pure utilities
Importable independently of the component β useful for server-side export, tests, or custom UIs.
| Function | Returns | Description |
|---|---|---|
filterEvents(events, filter) | β readonly SenseAuditEvent[] | Applies a filter (all dimensions ANDed). Pure. |
sortByNewest(events) | β readonly SenseAuditEvent[] | Stable sort, newest first. Does not mutate the input. |
groupByDay(events, locale?, tz?) | β DayGroup[] | Buckets events into calendar-day groups, timezone-aware. |
normalizeEvents(events) | β NormalizedEvent[] | Pre-parses timestamps to epoch millis (_ts) for cheap downstream ops. |
clusterEvents(events, options) | β TimelineItem[] | Collapses consecutive similar events into clusters. Pure & unit-tested. |
isCluster(item) | β item is EventCluster | Type guard distinguishing a cluster from an event. |
applyClusteringToGroups(groups, opts, expandedIds) | β ClusteredDayGroup[] | Applies clustering across day groups respecting expanded state. |
toCsv(events, columns?) | β string | Injection-safe CSV (escapes , " newlines and leading = + - @). Flattens actor + metadata. |
toJson(events) | β string | Pretty-printed JSON of the event set; round-trips to equivalent events. |
buildHistogram(events, buckets) | β HistogramBucket[] | Buckets events into equal-width time bins with a dominant severity. Performant for 100k events. |
detectAnomalies(events, rules) | β Map<id, AnomalyAnnotation[]> | Runs heuristic rules; returns annotations keyed by event id. Pure & deterministic. |
serializeFilter(filter) | β string | Encodes a filter to a compact URL-safe string. Empty filter β "". |
deserializeFilter(str) | β SenseAuditTimelineFilter | Decodes the string back. serialize(deserialize(x)) === x for all shapes. |
buildRichMetadataEntries(metadata) | β RichMetadataEntry[] | Classifies each metadata value (kind, display, copyText, href). |
formatTimestamp(value, locale?, tz?) | β string | Absolute timestamp formatting via Intl.DateTimeFormat. |
formatRelative(value, now?, locale?) | β string | Relative time ("2 h ago") via Intl.RelativeTimeFormat. Pure (injectable now). |
toDate / toIsoString / toLocalIsoDay / toTzIsoDay | β Date | string | Date coercion + ISO day helpers (timezone-aware). |
actorInitials(name) / actorColor(id) | β string | number | Deterministic avatar initials and palette index. |
flattenGroups / flattenClusteredGroups / rowKey / computeWindow / buildPrefixSums / findRowAt / estimateRowHeight | virtualization helpers | Low-level windowing utilities used by the built-in virtual scroller. |
Filtering & search 0.4
The toolbar exposes severity chips, category chips, an actor dropdown (derived live from the data), a from/to date range, and a debounced free-text search across action, description, and actor name. All dimensions are ANDed.
An active-filter count badge and a "Clear all" button reset every dimension in one action. Every change emits the full filter snapshot via filterChange.
<sense-audit-timeline
[events]="events()"
[filter]="{ severity: ['error'], from: '2026-06-01' }"
(filterChange)="lastFilter.set($event)"
/>Virtualization 0.1
Above VIRTUALIZE_THRESHOLD (100) rows the list is windowed: only the visible slice plus a small overscan is in the DOM, keeping node count bounded for 50k+ events at 60fps. Variable row heights (expanded metadata) are measured after render and fed back so scroll offsets stay accurate.
Set virtualScroll="false" to always render every row β useful for print or full-page export views where an outer container scrolls.
Live streaming 1.1
Update the liveEvents signal with each delta batch. New ids prepend to the top; existing ids update in place (no duplicates); ids already present in the base events() snapshot are ignored. A "N new events β" pill appears when the user has scrolled away from the top, and arrivals are announced via the aria-live region.
Set paused="true" to buffer incoming events while an investigator reads; unpausing flushes the buffer in one batch. For RxJS sources, map your stream to a signal with toSignal.
<sense-audit-timeline [events]="snapshot()" [liveEvents]="delta()" [paused]="paused()" />Burst clustering 1.2
When a batch emits hundreds of near-identical events, clustering collapses consecutive events sharing the same key(s) within windowMs into a single row with a count badge ("500Γ Imported record Β· 2:01β2:03"). Expanding reveals the members. Off by default; total count and ordering are preserved when expanded.
<sense-audit-timeline
[events]="events()"
[clustering]="{ enabled: true, windowMs: 60000, by: ['action', 'category'] }"
/>Remediation actions 1.3
Declare contextual actions ("Revoke session", "Roll back") scoped by severity, category, or a predicate. Matching events render small action buttons; invoking emits actionInvoke with the typed payload and stops propagation so eventSelect does not also fire. The component never performs the action itself.
actions: SenseAuditEventAction[] = [
{ id: 'revoke', label: 'Revoke session',
severityScope: ['error'], categoryScope: ['auth'] },
];Export (CSV / JSON) 1.4
Set enableExport to show CSV/JSON buttons. Clicking emits eventExport with the currently-filtered set and triggers a client-side Blob download. The pure toCsv / toJson utilities are injection-safe (escaping , " newlines and leading = + - @) and flatten actor + metadata; JSON round-trips to equivalent events.
<sense-audit-timeline [events]="events()" [enableExport]="true"
(eventExport)="audit.log($event.format, $event.events)" />Resource deep-links 1.5
Events carrying both resourceId and resourceType render a subtle "View resource" affordance. Activating it emits resourceSelect with the typed payload β the component never injects the Router (ADR-004); the host routes. It does not also emit eventSelect.
<sense-audit-timeline [events]="events()"
(resourceSelect)="router.navigate(['/', $event.resourceType, $event.resourceId])" />Selected-state reflection 1.6
Reflect the id you received from eventSelect back into selectedId to highlight the matching row with the --selected modifier and aria-current="true". Works with virtualization β the highlight reapplies as the row re-enters the rendered viewport.
<sense-audit-timeline [events]="events()" [selectedId]="selected()?.id ?? null"
(eventSelect)="selected.set($event.event)" />Density histogram 4.1
Set showHistogram to render a compact density strip over the full unfiltered set, coloured by dominant severity per bucket β a navigation map for long histories. Click or drag across bars to apply a matching from/to filter; two range inputs provide a keyboard-accessible equivalent. histogramBuckets tunes granularity. Renders performantly for 100k events.
<sense-audit-timeline [events]="events()" [showHistogram]="true" [histogramBuckets]="40" />Anomaly detection 4.2
Pass deterministic, ML-free heuristic rules to flag noteworthy events with a β badge and an explanatory tooltip. Built-in rules: repeated-failures (same actor), error-spike, off-hours, and same-category-burst β each with threshold/window options. The pure detectAnomalies util is unit-tested per rule. Default-off: passing [] is zero-cost.
rules: AnomalyRule[] = [
{ kind: 'error-spike', threshold: 3, windowMs: 120000 },
{ kind: 'off-hours', businessStart: 8, businessEnd: 18, timeZone: 'UTC' },
];Saved views & shareable URLs 4.3
serializeFilter / deserializeFilter encode a filter to a compact, URL-safe string and back β serialize(deserialize(x)) === x for every shape, including dates and arrays. Wire filterChange to the Router query params for a refresh-surviving deep link. Pass savedViews to render named preset chips; the chip matching current state is highlighted with aria-pressed.
presets: SavedView[] = [
{ id: 'errors', label: 'Errors', filter: { severity: ['error'] } },
{ id: 'auth', label: 'Auth events', filter: { category: ['auth'] } },
];
// share: router.navigate([], { queryParams: { view: serializeFilter(f) || null } })AI summarize 4.4
Set enableSummarize to show a "Summarize" button. Clicking emits summarizeRequest with the currently-visible events and active filter β the component NEVER makes a network call. The host calls its model, then writes the result back via summaryText / summaryLoading, which the summaryTemplate slot renders. Raw audit data may contain PII: the host decides what to send.
async onSummarize(req: SenseAuditSummarizeRequest) {
this.loading.set(true);
const text = await myAiService.summarize(req.events, req.filter);
this.text.set(text);
this.loading.set(false);
}Content-projection slots 2.1
Override any part of the rendering with a TemplateRef. eventRowTemplate replaces the entire row (receiving a rich context); actorTemplate and metadataTemplate target sub-regions; emptyTemplate and loadingTemplate replace the placeholder states; summaryTemplate hosts the AI summary. With no slot supplied, the default output is byte-for-byte unchanged.
<ng-template #row let-event let-time="formattedTime" let-sel="isSelected">
<div [class.is-selected]="sel">{{ event.action }} Β· {{ time }}</div>
</ng-template>
<sense-audit-timeline [events]="events()" [eventRowTemplate]="row" />Theming & tokens
Every colour, radius, and spacing value is a CSS custom property on :host. Override any token from host CSS β no ::ng-deep. A prefers-color-scheme: dark block remaps the defaults; force a scheme with the colorScheme input.
/* Host CSS β override any token, no ::ng-deep needed */
sense-audit-timeline.brand {
--sat-color-surface: #ffffff;
--sat-color-border: #e2e8f0;
--sat-text-primary: #0f172a;
--sat-severity-error: #dc2626;
--sat-radius: 10px;
}Token reference
| Token | Type | Purpose |
|---|---|---|
--sat-color-surface | color | Base background surface. |
--sat-color-surface-hover | color | Row hover background. |
--sat-color-selected | color | Selected-row background. |
--sat-color-border | color | Default border colour. |
--sat-color-border-input | color | Form-control border colour. |
--sat-color-border-focus | color | Active chip / focus border. |
--sat-color-focus-ring | color | Keyboard focus ring colour. |
--sat-text-primary | color | Primary text colour. |
--sat-text-secondary | color | Secondary text colour. |
--sat-text-muted | color | Muted / tertiary text colour. |
--sat-severity-info | color | Info severity colour. |
--sat-severity-warning | color | Warning severity colour. |
--sat-severity-error | color | Error severity colour. |
--sat-severity-success | color | Success severity colour. |
--sat-severity-current | color | Active-chip colour (set per chip via config). |
--sat-actor-color-0 β¦ -7 | color | Eight-step palette for avatar fallbacks. |
--sat-radius / --sat-radius-sm | length | Corner radii. |
--sat-gap | length | Base spacing unit. |
--sat-viewport-max-height | length | Max height of the scroll viewport. |
i18n, locale & timezone
Override any of the 18 user-visible strings via i18nOverrides. Dates and relative times use Intl driven by the locale input (falling back to LOCALE_ID). Set timeZone="UTC" for compliance dashboards β it shifts both displayed times and day-grouping boundaries deterministically. Layout uses CSS logical properties, so dir="rtl" mirrors correctly.
<sense-audit-timeline
[events]="events()"
locale="de"
timeZone="UTC"
[i18nOverrides]="{ noEventsMessage: 'Keine Ereignisse gefunden.' }"
/>Extensible taxonomy
Provide SENSE_AUDIT_TIMELINE_CONFIG to relabel, recolour, reorder, or add extended severity/category values without forking. Keys matching built-in unions override; new keys append as extra chips.
providers: [
{
provide: SENSE_AUDIT_TIMELINE_CONFIG,
useValue: {
categories: {
auth: { label: 'Authentication', order: 0 },
billing: { label: 'Billing', order: 6 }, // extended value
},
severities: {
error: { label: 'Error', color: '#dc2626', order: 0 },
critical: { label: 'Critical', color: '#7f1d1d', order: 1 }, // extended
},
} satisfies SenseAuditTimelineConfig,
},
],Rich metadata
The metadata panel detects each value's kind β string, number, boolean, null, ISO timestamp, URL, or object/array β and renders it appropriately. Objects pretty-print as collapsible JSON; URLs render as safe rel="noopener noreferrer" links (never raw HTML). Each value and the whole block have a keyboard-accessible copy button.
Accessibility
- Roving tabindex across event rows β a single Tab enters the list.
aria-live="polite"region announces result counts after filtering and new live arrivals.- Selected rows expose
aria-current="true"; filter chips exposearia-pressed. - Visible focus rings from theme tokens; default tokens meet AA contrast.
- Verified with axe-core across default, loading, empty, dark, and filtered states.
Keyboard model
| Keys | Action |
|---|---|
| Tab | Enter the event list (single tab stop via roving tabindex). |
| β / β | Move focus to the previous / next event. |
| Home / End | Jump to the first / last event. |
| Enter / Space | Activate the focused event (emits eventSelect). |
| Enter / Space (on expand) | Expand / collapse the metadata panel. |
| Tab (within row) | Reach action buttons, copy buttons, resource link. |