Agent Skillslatitude-dev/latitude-llm › notifications

notifications

GitHub

处理通知系统的技能,涵盖新增通知类型、分组或渠道,支持应用内和邮件投递,管理用户偏好设置及项目级门控,确保幂等性。

.agents/skills/notifications/SKILL.md latitude-dev/latitude-llm

Trigger Scenarios

添加新的通知类型、分组或渠道 调试应用内或邮件投递问题 修改通知表或用户偏好设置

Install

npx skills add latitude-dev/latitude-llm --skill notifications -g -y
More Options

Non-standard path

npx skills add https://github.com/latitude-dev/latitude-llm/tree/development/.agents/skills/notifications -g -y

Use without installing

npx skills use latitude-dev/latitude-llm@notifications

指定 Agent (Claude Code)

npx skills add latitude-dev/latitude-llm --skill notifications -a claude-code -g -y

安装 repo 全部 skill

npx skills add latitude-dev/latitude-llm --all -g -y

预览 repo 内 skill

npx skills add latitude-dev/latitude-llm --list

SKILL.md

Frontmatter
{
    "name": "notifications",
    "description": "Multi-channel notifications. Adding a new notification kind, group, or channel; in-app + email delivery; per-user prefs; project-level gates; idempotency."
}

Notifications

When to use: Adding a notification kind / group / channel, touching the notifications table or users.notification_preferences, wiring a new source event into the notification pipeline, or debugging in-app / email delivery.

Always read dev-docs/notifications.md for the full picture before editing. This skill is the action-oriented summary.

Vocabulary (and what NOT to confuse)

Four orthogonal axes — keep them straight:

Axis Type Examples Lives in
Kind flat enum (event-type) incident.event, incident.opened, incident.closed, wrapped.report, custom.message NOTIFICATION_KIND_META in @domain/notifications
Group user-visible category signals, monitors, wrapped_reports, custom_messages, personal, destinations, billing NOTIFICATION_GROUPS in @domain/shared
Topic sub-toggle inside a group signal.discovered, signal.escalating, signal.regressed, signal.reprioritized NOTIFICATION_TOPICS in @domain/shared
Channel delivery surface email, slack per-channel worker + registry

AlertIncidentKind (issue.new / issue.regressed / issue.escalating) is a fifth axis — it lives inside the incident.* payload and gates the producer step at the project level. It is not a NotificationKind. The mapping today: issue.new and issue.regressedincident.event (one-shot, endedAt = startedAt); issue.escalatingincident.opened + later incident.closed (sustained, endedAt transitions from null). The producer derives the notification kind from incident.endedAt, so adding a new sustained or eventful alert kind is purely a @domain/alerts change.

Pipeline at a glance

source domain event → domain-events worker
                    → notifications:request-<group>-notifications
                    → notifications:create-notification (one per recipient)
                    → notification-email:send (if user prefs allow)

Project deletion cascades via a separate path: ProjectDeletednotifications:delete-by-project.

Producers compute everything; consumers act idempotently. See dev-doc for details.

Adding a new kind (existing group)

  1. Add the kind to NOTIFICATION_KIND_META (packages/domain/notifications/src/entities/notification.ts) with { group, payload }.
  2. Define the payload schema in the same file. Keep it flat — no nested event discriminator.
  3. Extend buildIdempotencyKey (helpers/idempotency-key.ts) with the new kind. Pattern: ${kind}:${naturalEntityId} if there is one, ${kind}:${entityId}:${eventTimestamp} when the same entity can legitimately fire again (the unique index is permanent), else ${kind}:${generateId()}.
  4. Add per-channel renderers (TS will fail the build until each is present):
    • In-app: apps/web/src/routes/_authenticated/-components/notifications/renderers/<kind>.tsx + entry in notification-item.tsx's dispatch.
    • Email: packages/domain/email/src/templates/notifications/<kind>/index.tsx + entry in registry.ts. The renderer is an Effect — it can yield* any services it needs (e.g. WrappedReportRepository for wrapped.report). If the renderer needs services beyond SqlClient, wire the matching *Live layer into the email worker's rendererLayer in apps/workers/src/workers/notification-emailer.ts. Renderers that only need payload + context use Effect.tryPromise(() => buildHtml(...)).
  5. If the kind has its own source flow (not just wrapping an existing one):
    • Add a request-<kind>-notifications task to the notifications queue topic.
    • Write requestXxxNotificationsUseCase in @domain/notifications.
    • Route the source domain event in apps/workers/src/workers/domain-events.ts.
    • Add a handler in apps/workers/src/workers/notifications.ts.
  6. If the kind is tied to a project, set projectId on each request so the ProjectDeleted cascade cleans it up.
  7. Tests alongside each use case + each renderer.

No user-preferences UI change needed. The new kind inherits the group's existing toggle.

Adding a new topic (sub-toggle inside a group)

Reach for a topic when a group's existing switch is too coarse — the recipient wants this group but not this slice of it. A topic is one entry in NOTIFICATION_TOPICS + NOTIFICATION_TOPIC_META and one entry in the owning group's topics list (all in packages/domain/shared/src/notification-preferences.ts); both settings UIs render it from that meta with no further edits.

  • defaultEnabled: true is the normal case — the topic behaves opt-out, matching every other default in the system.
  • defaultEnabled: false makes it opt-in on both channels at once. admitsTopic is the single resolver behind shouldSendEmail and the worker's Slack fan-out, so one flag covers email, Slack, and both settings screens. Use it for topics that fire on routine activity a busy project would read as spam (signal.reprioritized fires on every priority increase).
  • Read checked in the UIs through admitsTopic(...), never ?? true — a hardcoded fallback silently shows an opt-in topic as on.
  • A topic only filters delivery. The in-app bell row is always written, so a muted topic still shows up in the feed.
  • A topic filter is the last line of defence, not the first. If a whole class of source events is never worth announcing, guard the outbox write instead — updateSignalTriageUseCase only emits SignalReprioritized for priority increases, so a downgrade costs no outbox row, no queue hop, and no producer run. Filtering downstream would burn all three to reach the same silence.

Adding a new group

A new group adds a new user-visible preferences toggle and (optionally) a new project-level gate.

  1. Add the group to NOTIFICATION_GROUPS and NOTIFICATION_GROUP_META in packages/domain/shared/src/notification-preferences.ts (groups today: signals, monitors, wrapped_reports, custom_messages, personal, destinations, billing). notificationPreferencesSchema is built from NOTIFICATION_GROUPS and auto-extends. Set slackRoutable on the meta: non-routable groups (e.g. personal — single-recipient kinds) are hidden from the Slack routes settings, rejected by the route-config server fns, and skipped by the worker's Slack fan-out; the Slack renderer registry still needs a (stub) entry because it is exhaustive.
  2. The user-prefs settings page (apps/web/src/routes/_authenticated/projects/$projectSlug/settings/account.tsx) iterates NOTIFICATION_GROUPS to render toggles — the new group appears automatically with its label/description from the meta.
  3. Add at least one kind to the new group (use the "Adding a new kind" steps).
  4. Project-level gate (optional) — only if the new group should be opt-out-able per project:
    • Add a slot to notificationsSettingSchema in packages/domain/shared/src/settings.ts.
    • Define the inner shape (per-kind, per-target, simple boolean — whatever's useful at the project level).
    • Add a helper next to isIncidentNotificationEnabled and call it from the new producer use case before fan-out.
    • Update the API ProjectSettingsSchema in packages/operations/src/operations/projects.ts and regenerate openapi/mcp:
      pnpm --filter @app/api openapi:emit
      pnpm --filter @app/api mcp:emit
      
    • Wire the new toggles into apps/web/src/routes/_authenticated/projects/$projectSlug/settings.tsx.
  5. Tests: extend request-*-notifications.test.ts patterns; add a cross-group preference test (group X off, group Y still on).

Group keys are persisted in users.notification_preferences jsonb — picking a stable group key matters more than a stable label (the label is NOTIFICATION_GROUP_META[group].label and can change freely).

Adding a new channel (Slack, SMS, ...)

  1. New queue topic in packages/domain/queue/src/topic-registry.ts (e.g. notification-slack with send).
  2. New per-kind renderer registry alongside the channel adapter, keyed on NotificationKind (exhaustive Record).
  3. Extend channelPreferencesSchema in @domain/shared/notification-preferences.ts with the new channel key (jsonb — no migration).
  4. Update the creator step in apps/workers/src/workers/notifications.ts to also publish the new channel's send task when prefs[group].<channel> is true. Add a shouldSend<Channel>(prefs, kind) helper alongside shouldSendEmail if it grows non-trivial.
  5. New worker file mirroring notification-emailer.ts. Register it in apps/workers/src/server.ts.
  6. Settings UI — which surface depends on how the channel is addressed. A per-user channel (email, SMS) is a switch on the per-group block in apps/web/src/routes/_authenticated/projects/$projectSlug/settings/account.tsx, driven by channelPreferencesSchema. An org-level routed channel (Slack) is not a user preference at all: it lives in settings/-components/slack-org-settings.tsx + slack-route-row.tsx, keyed on NOTIFICATION_GROUP_META[group].slackRoutable, and skips steps 3–4 entirely.

Source events, the producer step, the in-app feed, and the kind registry are all unchanged.

Embedding server-rendered images in emails

Pattern lives in apps/web/src/routes/api/notifications/$nid/incident-trend[.]png.ts — useful when a new kind wants a richer email visual than HTML/CSS can produce.

  1. URL: build at render time from a stable id (notification id). The buildChartUrl helper in @domain/email embeds the id as a path param. No signing today — the CUID is unguessable and the chart payload is project-internal trend data. If you're embedding more sensitive data (PII, credentials, content the recipient shouldn't see), HMAC-sign the id first; the chart route's TODO points at the contained change.
  2. Render: TanStack Start file route under apps/web/src/routes/api/ (project convention for machine-facing routes in apps/web — see api/health.ts, api/auth/…). Use satori (JSX → SVG) + @resvg/resvg-js (SVG → PNG). Already in apps/web's deps because the wrapped OG card uses the same pipeline. Keeping all PNG-rendering routes in apps/web keeps apps/api strictly to the authenticated public + MCP surface.
  3. Auth: unauthenticated. The route uses the admin Postgres client (RLS bypass — no org context until the row is loaded). Read via getAdminPostgresClient() from apps/web/src/server/clients.ts.
  4. Fallback: missing id, row gone, wrong kind, unparseable payload, or render failure → 200 with a 1×1 transparent PNG so the <Img> keeps rendering an element. A broken inbox image is worse than a missing one.
  5. Cache: Cache-Control: public, max-age=31536000, immutable. Mail-client image proxies cache the response.
  6. Email side: build the URL inside the renderer Effect via buildChartUrl from @domain/email. NotificationEmailRenderContext carries notificationId + webAppUrl, both resolved once at email-worker boot.

Idempotency rules

  • Producers publish with deterministic dedupeKey. The queue layer drops duplicate emits.
  • The creator step inserts via ON CONFLICT (organization_id, user_id, idempotency_key) DO NOTHING ... RETURNING. Only the "wrote it" branch publishes downstream channel jobs.
  • The emailer claims the row via markEmailed (UPDATE … WHERE emailed_at IS NULL RETURNING id) before sending. SMTP failures post-claim are lost emails — the trade-off is zero duplicates, which the design picked over zero misses.
  • delete-by-project is naturally idempotent (DELETE … RETURNING returns zero on re-runs).

If you change ordering (e.g. send-then-stamp): you'll get duplicate emails. Don't.

Anti-patterns

  • ❌ Filtering inside renderers ("don't send if X"). The producer/creator already decided — renderers just render.
  • ❌ Putting routing info in the kind name. incident.event describes what happened, not who needs to know.
  • ❌ Snapshotting live entity attributes (project name, issue name, project slug) in payloads. Use the row-level projectId and payload.sourceId and resolve display info downstream (bell: live query / projects collection; email: IssueRepository yielded by the renderer + ctx.project). Snapshotting derived point-in-time facts (trend buckets, breach numbers) is fine and encouraged.
  • ❌ Reading user prefs in the producer step. Prefs are per-channel and belong in the creator's "should I publish this channel's send task" decision.
  • ❌ FK constraint on project_id. Use the application-layer cascade via ProjectDeleteddelete-by-project. Per the database-postgres skill.
  • ❌ Deduping by source entity id alone. Use buildIdempotencyKey — the key must be per-occurrence, not per-entity (multiple incidents on the same issue = multiple notifications).
  • ❌ Mutating settings keys in place. NOTIFICATION_GROUPS entries are persisted in jsonb; renaming a group orphans existing user prefs. Add new groups; deprecate old ones with a no-op renderer if needed.

See also

Version History

  • 2479822 Current 2026-08-20 10:36

Same Skill Collection

.agents/skills/agentation-watch-mode/SKILL.md
.agents/skills/analyze-problem/SKILL.md
.agents/skills/api-endpoints/SKILL.md
.agents/skills/architecture-boundaries/SKILL.md
.agents/skills/artifact-designer/SKILL.md
.agents/skills/async-jobs-and-events/SKILL.md
.agents/skills/authentication/SKILL.md
.agents/skills/backoffice/SKILL.md
.agents/skills/better-auth-best-practices/SKILL.md
.agents/skills/code-style/SKILL.md
.agents/skills/database-clickhouse/SKILL.md
.agents/skills/database-postgres/SKILL.md
.agents/skills/docs/SKILL.md
.agents/skills/effect-and-errors/SKILL.md
.agents/skills/env-configuration/SKILL.md
.agents/skills/explain-diff-html/SKILL.md
.agents/skills/fix-datadog-issues/SKILL.md
.agents/skills/gh-issue/SKILL.md
.agents/skills/humanizer/SKILL.md
.agents/skills/managing-maintenance-windows/SKILL.md
.agents/skills/mintlify-preview/SKILL.md
.agents/skills/production-release/SKILL.md
.agents/skills/review-pr-comments/SKILL.md
.agents/skills/testing/SKILL.md
.agents/skills/toolchain-commands/SKILL.md
.agents/skills/web-frontend/SKILL.md
.agents/skills/ci-watchdog/SKILL.md
.agents/skills/create-pr/SKILL.md
.agents/skills/temporal-developer/SKILL.md

Metadata

Files
0
Version
2479822
Hash
70811cf2
Indexed
2026-08-20 10:36

- 위키
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-26 06:52
浙ICP备14020137号-1 $방문자$