Features

Notifications

In-app notifications backed by Redis, with per-user mark-read state.

telark emits in-app notifications for the events that should not be silent: a rollback completed or failed, a user was assigned a new role, a group membership changed. Notifications are owned by the exporter-service and persisted in Redis; the dashboard polls the list endpoint to render the bell badge and the notifications drawer.

There is no email gateway today, and no webhook fan-out — the NATS topic that publishes change events is separate.

What gets emitted

The events telark emits today, drawn from the dispatch sites across the services:

EventSourceNotes
rollback.completedRollback controller success path.Carries the application name, rollback ID, target generation, status success.
rollback.completedRollback controller failure path.Same type; metadata status: failure and an error message.
user.assigned-roleExporter user/group handlers on role assignment.Severity info.
group.membership-changedExporter group handler on member add/remove.Severity info.
Plan lifecycle eventsPlan controller transitions.Currently logged; emission to the user feed is on the ideas list.

Each notification has a stable shape:

FieldNotes
idServer-assigned.
userIdTarget user.
typeStable event identifier (e.g. rollback.completed).
titleShort human label.
messageOne-line body.
severityinfo | success | warning | error.
metadataPer-type structured data (resource IDs, status, etc.).
readBoolean.
createdAtISO timestamp.

ValidateForEmit rejects malformed payloads; Truncate clamps title and message to safe lengths before persistence.

Storage

Notifications live in Redis. The exporter's redis/notifications/storage.go writes each notification as a hash, plus per-user sorted-set indexes for the list endpoint (scrollable via cursor) and the unread count.

There is no CRD for notifications — they are an inherently ephemeral, per-user stream. Retention is governed by the Redis keyspace TTL, not by the snapshot retention policy.

Endpoints

All exposed by the exporter service:

MethodPathPurpose
POST/notifications/emitEmit a notification (called from other services, never from the dashboard).
GET/notifications?userId=<id>&limit=<n>&cursor=<c>List for one user, paginated.
POST/notifications/{id}/read?userId=<id>Mark one notification read.
POST/notifications/read-all?userId=<id>Mark every notification read for the user.
DELETE/notifications?userId=<id>Clear every notification for the user.

The limit default and the cursor encoding live in the exporter's notifications constants. The silent request header is used by the dashboard's polling loop to suppress error toasts on transient failures.

How discovery emits

The discovery service uses a small HTTP client (discovery/clients/notifications) to call the exporter's emit endpoint. The rollback controller dispatches asynchronously via async.Dispatch so a notification failure cannot block the rollback itself — emission is best-effort.

How the UI consumes

dashboard-ui/src/features/notifications/ is small by design:

  • A clients directory wrapping the four endpoints.
  • A Redux slice holding the current page of notifications plus the unread count.
  • A polling hook that calls fetchNotifications({ silent: true }) on an interval (configurable via userSettings.fetchIntervalSeconds in the GlobalConfig CRD).
  • A bell icon in the layout header with the unread count badge.
  • A slide-out drawer with the list, per-row Mark read, and a Clear all action.

What this is not

  • Not a webhook fan-out. External integrations subscribe to the NATS telark.applications.update topic (and any future per-event topic), not to this surface.
  • Not durable for compliance. Redis keyspace TTL applies. If you need a long-term audit log, capture the NATS stream into your own store.
  • Not multi-channel. No email, SMS, or push delivery today. The transport is in-app only.