feat: tiered rate limiting, NotificationSink protocol, in-dashboard notifications#1092
feat: tiered rate limiting, NotificationSink protocol, in-dashboard notifications#1092
Conversation
WalkthroughReplaces the single API rate-limit budget with two stacked middlewares: an outer unauthenticated IP-keyed tier ( Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Snapshot WarningsEnsure that dependencies are being submitted on PR branches. Re-running this action after a short time may resolve the issue. See the documentation for more information and troubleshooting advice. Scanned FilesNone |
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive notification subsystem, including a dispatcher, multiple sink adapters (console, ntfy, Slack, email), and frontend integration for managing and displaying notifications. The implementation includes robust error handling and rate limiting. I have identified several critical syntax errors where exception handling uses Python 2 syntax, which will fail in Python 3, and a documentation inconsistency in the NotificationSink protocol.
src/synthorg/budget/enforcer.py
Outdated
| except MemoryError, RecursionError: | ||
| raise |
src/synthorg/engine/approval_gate.py
Outdated
| except MemoryError, RecursionError: | ||
| raise |
| except MemoryError, RecursionError: | ||
| raise |
There was a problem hiding this comment.
| except MemoryError, RecursionError: | ||
| raise |
There was a problem hiding this comment.
| except MemoryError, RecursionError: | ||
| raise |
There was a problem hiding this comment.
| except MemoryError, RecursionError: | ||
| raise |
There was a problem hiding this comment.
| except MemoryError, RecursionError: | ||
| raise |
| Implementations MUST NOT raise -- errors are logged | ||
| internally and swallowed. |
There was a problem hiding this comment.
This docstring contradicts the class-level docstring. The class docstring correctly states that implementations should re-raise exceptions for the dispatcher to handle, which is how the sink adapters are implemented. This method docstring should be updated to reflect that behavior.
| Implementations MUST NOT raise -- errors are logged | |
| internally and swallowed. | |
| Implementations should log errors internally and re-raise them | |
| so the dispatcher can track delivery status. |
There was a problem hiding this comment.
Pull request overview
Implements a unified operator-notification taxonomy across backend and frontend while also splitting API rate limiting into unauthenticated vs authenticated tiers.
Changes:
- Add tiered rate limiting (unauth by IP, auth by user ID) with updated settings wiring and tests.
- Introduce backend
NotificationSinkprotocol +NotificationDispatcherwith console/ntfy/Slack/email adapters and config. - Build in-dashboard notifications (store + drawer + settings + browser Notification API integration) and wire WS events through it.
Reviewed changes
Copilot reviewed 55 out of 56 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| web/src/types/notifications.ts | Defines frontend notification taxonomy, routing config, and preferences types. |
| web/src/stores/notifications.ts | Implements unified notifications pipeline (enqueue, persistence, WS event mapping, fan-out). |
| web/src/stores/agents.ts | Removes toast-only handling for personality.trimmed in favor of unified notifications. |
| web/src/services/browser-notifications.ts | Adds browser Notification API helper with rate limiting and click-to-focus navigation event. |
| web/src/pages/SettingsPage.tsx | Adds Notifications settings section to Settings page. |
| web/src/pages/settings/NotificationsSection.tsx | UI for global mute, browser permission, and per-category route toggles. |
| web/src/pages/settings/NotificationsSection.stories.tsx | Storybook coverage for Notifications settings section. |
| web/src/hooks/useGlobalNotifications.ts | Subscribes to more WS channels and routes events via notifications store; connection failure notifications. |
| web/src/components/notifications/NotificationItemCard.tsx | Renders a single notification entry with actions (read/dismiss) and optional navigation. |
| web/src/components/notifications/NotificationItemCard.stories.tsx | Storybook variants for notification item rendering. |
| web/src/components/notifications/NotificationFilterBar.tsx | Filter UI for grouping notifications by domain. |
| web/src/components/notifications/NotificationFilterBar.stories.tsx | Storybook coverage for filter bar variants. |
| web/src/components/notifications/NotificationEmptyState.tsx | Empty-state component for notification drawer per filter. |
| web/src/components/notifications/NotificationEmptyState.stories.tsx | Storybook coverage for drawer empty state variants. |
| web/src/components/notifications/NotificationDrawer.tsx | Drawer UI for reading/clearing notifications with filtering and unread count. |
| web/src/components/notifications/NotificationDrawer.stories.tsx | Storybook coverage for notification drawer. |
| web/src/components/layout/Sidebar.tsx | Replaces placeholder bell with functional notification bell + unread badge + drawer. |
| web/src/components/layout/AppLayout.tsx | Adds Shift+N toggle + command palette entry; handles navigation events from browser notifications. |
| tests/unit/settings/test_resolver.py | Updates resolver tests for new rate-limit setting keys. |
| tests/unit/observability/test_events.py | Ensures the notification events module is discoverable. |
| tests/unit/notifications/test_protocol.py | Adds protocol structural typing tests for NotificationSink. |
| tests/unit/notifications/test_ntfy_adapter.py | Adds unit tests for ntfy sink behavior and headers/auth. |
| tests/unit/notifications/test_models.py | Adds tests for notification models (validation, defaults, immutability). |
| tests/unit/notifications/test_dispatcher.py | Adds tests for dispatcher fan-out, filtering, and failure isolation. |
| tests/unit/notifications/test_console_adapter.py | Adds basic console sink tests. |
| tests/unit/notifications/test_config.py | Adds tests for notification config models and defaults. |
| tests/unit/notifications/init.py | Establishes test package for notification subsystem. |
| tests/unit/api/test_config.py | Updates API config tests for tiered rate-limit fields + legacy rejection. |
| tests/unit/api/test_app.py | Adds middleware-stack and auth-identifier tests for tiered rate limiting. |
| tests/unit/api/controllers/test_ws.py | Updates WS auth middleware index expectations after middleware stack change. |
| tests/unit/api/conftest.py | Updates test root config to set both tiered rate limits. |
| tests/integration/settings/test_settings_integration.py | Updates integration test to persist/resolve both rate-limit settings. |
| src/synthorg/settings/resolver.py | Resolves tiered rate-limit settings concurrently via TaskGroup. |
| src/synthorg/settings/definitions/api.py | Splits rate-limit settings definitions into unauth/auth keys and YAML paths. |
| src/synthorg/security/timeout/scheduler.py | Wires approval escalation to optional notification dispatcher (best-effort). |
| src/synthorg/observability/events/notification.py | Adds notification domain event constants. |
| src/synthorg/notifications/protocol.py | Introduces NotificationSink protocol (runtime-checkable). |
| src/synthorg/notifications/models.py | Adds backend notification model + severity/category taxonomy. |
| src/synthorg/notifications/dispatcher.py | Adds async fan-out dispatcher with severity filtering and failure isolation. |
| src/synthorg/notifications/config.py | Adds config models for notification sinks and min severity. |
| src/synthorg/notifications/adapters/slack.py | Slack webhook sink adapter. |
| src/synthorg/notifications/adapters/ntfy.py | ntfy sink adapter (HTTP POST) with severity-to-priority mapping. |
| src/synthorg/notifications/adapters/email.py | SMTP email sink adapter via asyncio.to_thread. |
| src/synthorg/notifications/adapters/console.py | Console sink adapter that logs notifications to structured logging. |
| src/synthorg/notifications/adapters/init.py | Package marker for adapters module. |
| src/synthorg/notifications/init.py | Exposes notification subsystem public API. |
| src/synthorg/engine/approval_gate.py | Sends best-effort out-of-band alert when contexts are parked for approval. |
| src/synthorg/config/schema.py | Adds notifications configuration to RootConfig schema. |
| src/synthorg/config/defaults.py | Adds empty notifications section to default config dict. |
| src/synthorg/budget/enforcer.py | Sends best-effort critical notification when monthly hard stop triggers. |
| src/synthorg/api/state.py | Stores notification dispatcher on app state with accessors. |
| src/synthorg/api/config.py | Replaces single max_requests with tiered unauth/auth fields and legacy rejection. |
| src/synthorg/api/app.py | Builds notification dispatcher from config; implements tiered rate-limit middleware and auth identifier. |
| docs/security.md | Documents tiered rate limiting behavior and configuration. |
| docs/design/operations.md | Updates auth endpoint documentation to reference tiered rate limiting. |
| CLAUDE.md | Updates repo inventory + logging/event guidance references for notifications. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| """Protocol for notification delivery adapters. | ||
|
|
||
| Implementations should log errors internally and re-raise so | ||
| the ``NotificationDispatcher`` can track delivery status. | ||
| ``MemoryError`` and ``RecursionError`` must always propagate. | ||
|
|
||
| The ``sink_name`` property is used for logging and diagnostics. | ||
| """ | ||
|
|
||
| @property | ||
| def sink_name(self) -> str: | ||
| """Human-readable sink identifier for logging.""" | ||
| ... | ||
|
|
||
| async def send(self, notification: Notification) -> None: | ||
| """Deliver a notification. | ||
|
|
||
| Implementations MUST NOT raise -- errors are logged | ||
| internally and swallowed. | ||
|
|
||
| Args: | ||
| notification: The notification to deliver. | ||
| """ |
There was a problem hiding this comment.
The NotificationSink protocol docstring is internally inconsistent: the class doc says implementations should log and re-raise so NotificationDispatcher can track failures, but the send() doc says implementations MUST NOT raise and should swallow errors. This makes the contract unclear and contradicts the adapter implementations (which log then raise). Align the docs (and/or behavior) so sinks are expected to raise non-fatal exceptions to the dispatcher, while still propagating MemoryError/RecursionError as noted.
| class NotificationCategory(StrEnum): | ||
| """Notification event categories. | ||
|
|
||
| Shared with frontend ``NotificationItem.category``. | ||
| """ | ||
|
|
||
| APPROVAL = "approval" | ||
| BUDGET = "budget" | ||
| SECURITY = "security" | ||
| STAGNATION = "stagnation" | ||
| SYSTEM = "system" | ||
| AGENT = "agent" | ||
|
|
There was a problem hiding this comment.
NotificationCategory is described as shared with frontend NotificationItem.category, but the frontend uses fine-grained categories like approvals.pending while the backend enum values are coarse (approval, budget, etc.). Update the docstring to reflect that this is the backend coarse category taxonomy (shared conceptually, not 1:1 values).
docs/security.md
Outdated
| - **Forced password change** -- `must_change_password` flag blocks API access | ||
| - **One-time WebSocket tickets** -- short-lived (30 s), single-use, cryptographically random tokens exchanged via ``POST /api/v1/auth/ws-ticket`` (requires valid JWT). Prevents long-lived JWT leakage by replacing it with an ephemeral ticket in the WebSocket query parameter. In-memory store, monotonic clock expiry, per-process scope. JWT/API key auth middleware is scoped to HTTP requests only (`ScopeType.HTTP`) -- WebSocket connections bypass the middleware entirely and rely on handler-level ticket validation. | ||
| - **Rate limiting** -- configurable per-deployment (default: 100 req/min). The WebSocket path is excluded from rate limiting -- HTTP-style per-request rate limiting is inappropriate for persistent WebSocket connections. Auth-sensitive endpoints (``/auth/login``, ``/auth/setup``, ``/auth/change-password``) have a stricter route-level rate limit of 10 req/min to mitigate credential brute-forcing. | ||
| - **Tiered rate limiting** -- two separate budgets stacked around the auth middleware. **Unauthenticated** requests are limited to 20 req/min by client IP (protects against brute-force on login, setup, and health endpoints). **Authenticated** requests are limited to 600 req/min by user ID (generous budget for normal dashboard usage). Keying authenticated limits by user ID instead of IP prevents multi-user deployments behind a shared gateway or NAT from collectively exhausting a single budget. Both limits are configurable via ``api.rate_limit.unauth_max_requests`` and ``api.rate_limit.auth_max_requests`` in the YAML config (or ``SYNTHORG_API__RATE_LIMIT__UNAUTH_MAX`` / ``SYNTHORG_API__RATE_LIMIT__AUTH_MAX`` environment variables for Docker deployments). The WebSocket path is excluded from both tiers -- HTTP-style per-request rate limiting is inappropriate for persistent WebSocket connections. In-memory rate-limit storage is single-replica; multi-replica deployments with shared rate limiting require an external store (not yet supported). |
There was a problem hiding this comment.
This section documents env var overrides as SYNTHORG_API__RATE_LIMIT__UNAUTH_MAX / SYNTHORG_API__RATE_LIMIT__AUTH_MAX, but the settings system resolves env overrides as SYNTHORG_{NAMESPACE}_{KEY} (e.g. SYNTHORG_API_RATE_LIMIT_UNAUTH_MAX_REQUESTS / SYNTHORG_API_RATE_LIMIT_AUTH_MAX_REQUESTS). Either update the docs to the actual env var names or add explicit support/aliases for the documented ones.
| - **Tiered rate limiting** -- two separate budgets stacked around the auth middleware. **Unauthenticated** requests are limited to 20 req/min by client IP (protects against brute-force on login, setup, and health endpoints). **Authenticated** requests are limited to 600 req/min by user ID (generous budget for normal dashboard usage). Keying authenticated limits by user ID instead of IP prevents multi-user deployments behind a shared gateway or NAT from collectively exhausting a single budget. Both limits are configurable via ``api.rate_limit.unauth_max_requests`` and ``api.rate_limit.auth_max_requests`` in the YAML config (or ``SYNTHORG_API__RATE_LIMIT__UNAUTH_MAX`` / ``SYNTHORG_API__RATE_LIMIT__AUTH_MAX`` environment variables for Docker deployments). The WebSocket path is excluded from both tiers -- HTTP-style per-request rate limiting is inappropriate for persistent WebSocket connections. In-memory rate-limit storage is single-replica; multi-replica deployments with shared rate limiting require an external store (not yet supported). | |
| - **Tiered rate limiting** -- two separate budgets stacked around the auth middleware. **Unauthenticated** requests are limited to 20 req/min by client IP (protects against brute-force on login, setup, and health endpoints). **Authenticated** requests are limited to 600 req/min by user ID (generous budget for normal dashboard usage). Keying authenticated limits by user ID instead of IP prevents multi-user deployments behind a shared gateway or NAT from collectively exhausting a single budget. Both limits are configurable via ``api.rate_limit.unauth_max_requests`` and ``api.rate_limit.auth_max_requests`` in the YAML config (or ``SYNTHORG_API_RATE_LIMIT_UNAUTH_MAX_REQUESTS`` / ``SYNTHORG_API_RATE_LIMIT_AUTH_MAX_REQUESTS`` environment variables for Docker deployments). The WebSocket path is excluded from both tiers -- HTTP-style per-request rate limiting is inappropriate for persistent WebSocket connections. In-memory rate-limit storage is single-replica; multi-replica deployments with shared rate limiting require an external store (not yet supported). |
src/synthorg/engine/approval_gate.py
Outdated
| logger.warning( | ||
| APPROVAL_GATE_CONTEXT_PARKED, | ||
| note="notification dispatch failed after parking", | ||
| approval_id=escalation.approval_id, | ||
| exc_info=True, | ||
| ) |
There was a problem hiding this comment.
On notification dispatch failure, this logs APPROVAL_GATE_CONTEXT_PARKED (a success event) at warning level, which will make observability metrics misleading. Use a failure-typed event constant (e.g. APPROVAL_GATE_CONTEXT_PARK_FAILED) or introduce a dedicated event for notification dispatch failures after parking.
| * Mounted once at the {@link AppLayout} level so notifications render regardless | ||
| * of which page the user is currently viewing. Dispatches events to the stores | ||
| * that own the user-facing behaviour (e.g. the agents store forwards | ||
| * `personality.trimmed` events to the toast queue). | ||
| * of which page the user is currently viewing. All WS events are routed through | ||
| * the unified notification store which handles fan-out to toast, drawer, and | ||
| * browser notifications based on category routing config. | ||
| * | ||
| * Connection failures surface to the user via toast notifications so a silent | ||
| * WebSocket death does not leave users wondering why toasts stopped arriving. | ||
| * Channel-specific stores (e.g. agents) are still updated directly for their | ||
| * domain-specific state tracking (runtime statuses, etc.). | ||
| * | ||
| * Connection failures are surfaced via the notification pipeline so they appear | ||
| * in both the toast queue and the notification drawer. |
There was a problem hiding this comment.
The header comment says connection failures surface via the notification pipeline so they appear in both the toast queue and the notification drawer, but connection.lost is configured as toast-only (CATEGORY_CONFIGS['connection.lost'].defaultRoutes = ['toast']). Either adjust the routing config to include drawer for connection.lost, or update the comment to match the actual behavior.
| export function NotificationsSection() { | ||
| const preferences = useNotificationsStore((s) => s.preferences) | ||
| const setGlobalMute = useNotificationsStore((s) => s.setGlobalMute) | ||
| const setRouteOverride = useNotificationsStore((s) => s.setRouteOverride) | ||
| const setBrowserPermission = useNotificationsStore((s) => s.setBrowserPermission) | ||
|
|
||
| const permission = preferences.browserPermission | ||
|
|
||
| async function handleRequestPermission() { | ||
| const result = await browserNotifications.requestPermission() | ||
| setBrowserPermission(result) | ||
| } |
There was a problem hiding this comment.
browserPermission is stored in preferences and displayed here, but it’s never initialized from the actual current browser permission (Notification.permission). This means users who previously granted permission will still see default until they click Enable. Consider deriving the displayed value from browserNotifications.getPermission() on mount (and/or syncing it into the store) so the settings UI reflects reality.
| <div | ||
| role="listitem" | ||
| aria-label={`${item.severity} notification: ${item.title}`} | ||
| className={cn( | ||
| 'group relative flex gap-3 rounded-md border-l-2 px-3 py-2', | ||
| 'transition-colors hover:bg-card-hover', | ||
| item.read ? 'border-l-transparent' : BORDER_COLORS[item.severity], | ||
| !item.read && 'bg-accent/5', | ||
| item.href && 'cursor-pointer', | ||
| )} | ||
| onClick={handleClick} | ||
| onKeyDown={(e) => { | ||
| if (e.key === 'Enter') handleClick() | ||
| }} | ||
| tabIndex={0} | ||
| > |
There was a problem hiding this comment.
This renders a focusable, clickable <div> with tabIndex={0} and onClick, but it has role="listitem" and only handles Enter keypresses. Screen readers won’t treat it as an interactive control, and keyboard users typically expect Space to activate as well. Prefer rendering a semantic <button> (or set role="button" and handle Space+Enter) while keeping list semantics in the parent container.
docs/security.md
Outdated
| - **Forced password change** -- `must_change_password` flag blocks API access | ||
| - **One-time WebSocket tickets** -- short-lived (30 s), single-use, cryptographically random tokens exchanged via ``POST /api/v1/auth/ws-ticket`` (requires valid JWT). Prevents long-lived JWT leakage by replacing it with an ephemeral ticket in the WebSocket query parameter. In-memory store, monotonic clock expiry, per-process scope. JWT/API key auth middleware is scoped to HTTP requests only (`ScopeType.HTTP`) -- WebSocket connections bypass the middleware entirely and rely on handler-level ticket validation. | ||
| - **Rate limiting** -- configurable per-deployment (default: 100 req/min). The WebSocket path is excluded from rate limiting -- HTTP-style per-request rate limiting is inappropriate for persistent WebSocket connections. Auth-sensitive endpoints (``/auth/login``, ``/auth/setup``, ``/auth/change-password``) have a stricter route-level rate limit of 10 req/min to mitigate credential brute-forcing. | ||
| - **Tiered rate limiting** -- two separate budgets stacked around the auth middleware. **Unauthenticated** requests are limited to 20 req/min by client IP (protects against brute-force on login, setup, and health endpoints). **Authenticated** requests are limited to 600 req/min by user ID (generous budget for normal dashboard usage). Keying authenticated limits by user ID instead of IP prevents multi-user deployments behind a shared gateway or NAT from collectively exhausting a single budget. Both limits are configurable via ``api.rate_limit.unauth_max_requests`` and ``api.rate_limit.auth_max_requests`` in the YAML config (or ``SYNTHORG_API__RATE_LIMIT__UNAUTH_MAX`` / ``SYNTHORG_API__RATE_LIMIT__AUTH_MAX`` environment variables for Docker deployments). The WebSocket path is excluded from both tiers -- HTTP-style per-request rate limiting is inappropriate for persistent WebSocket connections. In-memory rate-limit storage is single-replica; multi-replica deployments with shared rate limiting require an external store (not yet supported). |
There was a problem hiding this comment.
This paragraph says the unauthenticated tier protects “login, setup, and health endpoints”, but the default RateLimitConfig.exclude_paths still includes /api/v1/health, so health is excluded from rate limiting by default. Adjust the wording (or the default exclude list) so the docs match actual behavior.
| - **Tiered rate limiting** -- two separate budgets stacked around the auth middleware. **Unauthenticated** requests are limited to 20 req/min by client IP (protects against brute-force on login, setup, and health endpoints). **Authenticated** requests are limited to 600 req/min by user ID (generous budget for normal dashboard usage). Keying authenticated limits by user ID instead of IP prevents multi-user deployments behind a shared gateway or NAT from collectively exhausting a single budget. Both limits are configurable via ``api.rate_limit.unauth_max_requests`` and ``api.rate_limit.auth_max_requests`` in the YAML config (or ``SYNTHORG_API__RATE_LIMIT__UNAUTH_MAX`` / ``SYNTHORG_API__RATE_LIMIT__AUTH_MAX`` environment variables for Docker deployments). The WebSocket path is excluded from both tiers -- HTTP-style per-request rate limiting is inappropriate for persistent WebSocket connections. In-memory rate-limit storage is single-replica; multi-replica deployments with shared rate limiting require an external store (not yet supported). | |
| - **Tiered rate limiting** -- two separate budgets stacked around the auth middleware. **Unauthenticated** requests are limited to 20 req/min by client IP (protects against brute-force on login and setup endpoints; the health endpoint is excluded from rate limiting by default). **Authenticated** requests are limited to 600 req/min by user ID (generous budget for normal dashboard usage). Keying authenticated limits by user ID instead of IP prevents multi-user deployments behind a shared gateway or NAT from collectively exhausting a single budget. Both limits are configurable via ``api.rate_limit.unauth_max_requests`` and ``api.rate_limit.auth_max_requests`` in the YAML config (or ``SYNTHORG_API__RATE_LIMIT__UNAUTH_MAX`` / ``SYNTHORG_API__RATE_LIMIT__AUTH_MAX`` environment variables for Docker deployments). The WebSocket path is excluded from both tiers -- HTTP-style per-request rate limiting is inappropriate for persistent WebSocket connections. In-memory rate-limit storage is single-replica; multi-replica deployments with shared rate limiting require an external store (not yet supported). |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1092 +/- ##
==========================================
- Coverage 89.53% 89.23% -0.30%
==========================================
Files 755 766 +11
Lines 44576 45014 +438
Branches 4487 4525 +38
==========================================
+ Hits 39909 40168 +259
- Misses 3862 4029 +167
- Partials 805 817 +12 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 14
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/security.md`:
- Line 77: Add a clear breaking-change note that the legacy config key
api.rate_limit.max_requests is no longer accepted: update the Tiered rate
limiting paragraph to state that on upgrade the old key will be rejected at
startup and administrators must replace it with the new split keys
api.rate_limit.unauth_max_requests and api.rate_limit.auth_max_requests (or the
corresponding env vars SYNTHORG_API__RATE_LIMIT__UNAUTH_MAX and
SYNTHORG_API__RATE_LIMIT__AUTH_MAX), and include a short migration instruction:
remove the old api.rate_limit.max_requests entry and set both new keys (suggest
sensible defaults if helpful).
In `@src/synthorg/engine/approval_gate.py`:
- Around line 149-155: The code currently logs APPROVAL_GATE_CONTEXT_PARKED on
notification dispatch exceptions which conflates a failure with the happy-path
event; replace that constant with a failure event (e.g.,
NOTIFICATION_DISPATCH_FAILED) and import it from the
synthorg.observability.events domain module (import NOTIFICATION_DISPATCH_FAILED
from synthorg.observability.events.approval_gate or the appropriate domain
module). Update the logger.warning call in the except block to use
NOTIFICATION_DISPATCH_FAILED (keeping the same note, approval_id, and exc_info
arguments) so the failure is emitted under a distinct event name.
In `@src/synthorg/notifications/adapters/email.py`:
- Around line 113-114: The code currently skips smtp.login when only one of
self._username or self._password is set, silently hiding likely
misconfiguration; update the block around the smtp.login call so that if exactly
one of self._username or self._password is truthy you emit a warning (e.g., via
the adapter's logger, self._logger.warning) indicating partial SMTP credentials
were provided, and only call smtp.login(self._username, self._password) when
both are present; keep existing behavior when both are None or both present.
In `@src/synthorg/notifications/adapters/slack.py`:
- Around line 46-78: The severity/title header is duplicated in the payload
construction; extract it into a single variable (e.g., header =
f"*[{notification.severity.value.upper()}]* {notification.title}") and use that
variable in both the top-level "text" and inside the section block's "text"
instead of repeating the f-string and conditional; update the section block
expression to include notification.body only when present (e.g., header + ("\n"
+ notification.body if notification.body else "")) so the duplicated header
logic in payload, the "text" key and the section block is removed and all
references use the single header variable.
In `@src/synthorg/notifications/protocol.py`:
- Around line 25-34: The send() method docstring in the NotificationSink
abstract interface contradicts the class-level docstring: update the send(self,
notification: Notification) -> None docstring to state that implementations
should propagate exceptions (i.e., may re-raise) so NotificationDispatcher can
track delivery status, matching the class docstring and existing adapters like
EmailNotificationSink; mention NotificationDispatcher and EmailNotificationSink
in the docstring to make the intended contract explicit and remove the "MUST NOT
raise" sentence.
In `@tests/unit/api/test_app.py`:
- Around line 1122-1153: The test should also assert the concrete ordering so
"unauth outside auth, auth inside" is guaranteed: in
test_unauth_and_auth_rate_limiters_have_distinct_stores, after extracting
rl_configs and stores, locate the indices of the rate-limit middleware entries
in mw by checking entry.kwargs["config"].store for "unauth" and "auth" (use the
same mw variable and entries filtering), then assert unauth_index < auth_index;
additionally assert there exists an authentication middleware instance/class
(e.g., AuthenticationMiddleware or the middleware entry whose class/name
identifies auth) at an index strictly between unauth_index and auth_index to
prove the auth sandwich ordering.
In `@tests/unit/notifications/test_models.py`:
- Around line 99-110: The test test_metadata_deep_copy_isolation currently
mutates a deepcopy of n.metadata which never affects the original; instead,
after constructing Notification(...) with the local meta dict, mutate the
original meta (the variable meta used to create Notification) — e.g. replace or
modify meta["key"] — and then assert that n.metadata still equals the original
value [1,2,3]; update the test body to mutate meta (not copied) to verify
Notification's constructor made an isolated/deep-copied metadata snapshot.
In `@web/src/components/layout/Sidebar.tsx`:
- Around line 286-291: The unread badge can overflow when unreadCount > 99
because the span uses a fixed size class ("size-4") but renders "99+"; update
the badge styling in the component around the span that references unreadCount
so it supports variable width (replace fixed size with min-width/padding or use
inline-flex with px to allow up to three characters, e.g., swap "size-4" for a
responsive class like "min-w-[var]" and keep rounded-full), ensure text remains
centered and vertical-aligned, and keep the conditional display logic (the
unreadCount check) intact so layout and accessibility (aria-hidden) are
preserved.
- Around line 264-269: The toggle-notification-drawer event listener is
registered inside the Sidebar/NotificationBell useEffect so it is removed when
the sidebar drawer unmounts on tablet; move the listener registration and the
NotificationDrawer rendering into AppLayout so the listener survives overlay
collapse. Specifically, remove the window.addEventListener/removeEventListener
block from NotificationBell/Sidebar, add an equivalent useEffect in AppLayout
that listens for 'toggle-notification-drawer' and toggles the notification state
(or calls the existing setDrawerOpen handler lifted into AppLayout), and render
<NotificationDrawer .../> from AppLayout (not inside the sidebar Drawer); keep
the same event name and state updates so Shift+N and the command palette still
work.
In `@web/src/components/notifications/NotificationDrawer.stories.tsx`:
- Around line 16-19: The story variant WithItems is identical to Empty because
meta.args already sets open:true and WithItems doesn't change inputs; update the
story to seed or reset the notifications store (or expose test props) when
rendering the WithItems story so it shows populated notifications. Locate the
NotificationDrawer story definitions (symbols meta, WithItems, Empty) and add a
decorator or loader for WithItems that uses the notifications store API
(seedNotifications, resetNotifications, or similar) to populate a few test
notifications before rendering, or modify WithItems.args to pass a prop that the
component uses to load test data.
In `@web/src/components/notifications/NotificationDrawer.tsx`:
- Around line 22-35: The unread summary and bulk actions are global but should
be scoped to the current filter; compute filteredItems (already present) and
derive a filteredUnreadCount by counting items in filteredItems with unread
flag, then wire the UI to use that instead of unreadCount; for bulk actions,
build the list of filteredIds from filteredItems and call the existing store
actions per-id (e.g., loop over filteredIds calling markRead(id)) or add/use
store helpers that accept an array (e.g., markAllRead(filteredIds) /
clearAll(filteredIds)) so that markAllRead and clearAll only affect
notifications in the active filter (refer to unreadCount, markRead, markAllRead,
clearAll, filteredItems, filter, useNotificationsStore, CATEGORY_CONFIGS,
items).
In `@web/src/components/notifications/NotificationEmptyState.stories.tsx`:
- Around line 16-28: The Storybook file only covers 'all', 'approvals',
'budget', and 'system' filters for NotificationEmptyState, leaving the 'tasks',
'agents', 'providers', and 'connection' branches untested; add four new stories
in NotificationEmptyState.stories.tsx (e.g., TasksFilter, AgentsFilter,
ProvidersFilter, ConnectionFilter) that set args: { filter: 'tasks' }, { filter:
'agents' }, { filter: 'providers' }, and { filter: 'connection' } respectively
so Storybook exercises those code paths for the NotificationEmptyState
component.
In `@web/src/components/notifications/NotificationItemCard.stories.tsx`:
- Around line 8-18: The story fixture uses a non-deterministic timestamp (new
Date().toISOString()) in baseItem which causes Storybook/screenshot tests to
drift; replace that dynamic timestamp with a fixed ISO string (e.g.,
"2023-01-01T00:00:00.000Z") in the baseItem object inside
NotificationItemCard.stories.tsx so the NotificationItem fixture (baseItem)
renders deterministically for the NotificationItemCard component.
In `@web/src/components/notifications/NotificationItemCard.tsx`:
- Around line 28-38: The formatRelativeTime function uses unnecessary String()
calls inside template literals; remove the redundant String(...) wrappers in the
returned template strings so numbers are interpolated directly (e.g., change
`${String(minutes)}m ago` and similar to `${minutes}m ago`) in the
formatRelativeTime function to simplify the code while preserving behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 5eb8def1-9e51-4699-92c4-504a44667d51
📒 Files selected for processing (56)
CLAUDE.mddocs/design/operations.mddocs/security.mdsrc/synthorg/api/app.pysrc/synthorg/api/config.pysrc/synthorg/api/state.pysrc/synthorg/budget/enforcer.pysrc/synthorg/config/defaults.pysrc/synthorg/config/schema.pysrc/synthorg/engine/approval_gate.pysrc/synthorg/notifications/__init__.pysrc/synthorg/notifications/adapters/__init__.pysrc/synthorg/notifications/adapters/console.pysrc/synthorg/notifications/adapters/email.pysrc/synthorg/notifications/adapters/ntfy.pysrc/synthorg/notifications/adapters/slack.pysrc/synthorg/notifications/config.pysrc/synthorg/notifications/dispatcher.pysrc/synthorg/notifications/models.pysrc/synthorg/notifications/protocol.pysrc/synthorg/observability/events/notification.pysrc/synthorg/security/timeout/scheduler.pysrc/synthorg/settings/definitions/api.pysrc/synthorg/settings/resolver.pytests/integration/settings/test_settings_integration.pytests/unit/api/conftest.pytests/unit/api/controllers/test_ws.pytests/unit/api/test_app.pytests/unit/api/test_config.pytests/unit/notifications/__init__.pytests/unit/notifications/test_config.pytests/unit/notifications/test_console_adapter.pytests/unit/notifications/test_dispatcher.pytests/unit/notifications/test_models.pytests/unit/notifications/test_ntfy_adapter.pytests/unit/notifications/test_protocol.pytests/unit/observability/test_events.pytests/unit/settings/test_resolver.pyweb/src/components/layout/AppLayout.tsxweb/src/components/layout/Sidebar.tsxweb/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationDrawer.tsxweb/src/components/notifications/NotificationEmptyState.stories.tsxweb/src/components/notifications/NotificationEmptyState.tsxweb/src/components/notifications/NotificationFilterBar.stories.tsxweb/src/components/notifications/NotificationFilterBar.tsxweb/src/components/notifications/NotificationItemCard.stories.tsxweb/src/components/notifications/NotificationItemCard.tsxweb/src/hooks/useGlobalNotifications.tsweb/src/pages/SettingsPage.tsxweb/src/pages/settings/NotificationsSection.stories.tsxweb/src/pages/settings/NotificationsSection.tsxweb/src/services/browser-notifications.tsweb/src/stores/agents.tsweb/src/stores/notifications.tsweb/src/types/notifications.ts
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/synthorg/api/app.py (1)
890-915:⚠️ Potential issue | 🟠 MajorClose the notification dispatcher from the app shutdown path.
create_app()now owns theNotificationDispatcher, buton_shutdown()/_safe_shutdown()never closes it. That skips the graceful-shutdown path added for this subsystem and can leave per-sink clients alive until process exit.Suggested shutdown hook
async def on_shutdown() -> None: nonlocal _ticket_cleanup_task, _auto_wired_dispatcher, _health_prober + if app_state.has_notification_dispatcher: + await _try_stop( + app_state.notification_dispatcher.close(), + API_APP_SHUTDOWN, + "Failed to stop notification dispatcher", + ) if _ticket_cleanup_task is not None: _ticket_cleanup_task.cancel() with contextlib.suppress(asyncio.CancelledError): await _ticket_cleanup_task🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/api/app.py` around lines 890 - 915, The NotificationDispatcher created by build_notification_dispatcher is stored in notification_dispatcher and passed into AppState in create_app(), but it is not closed during shutdown; update the app shutdown path (on_shutdown and/or _safe_shutdown) to call the dispatcher’s close/shutdown method (e.g., notification_dispatcher.close() or await notification_dispatcher.shutdown() depending on its API), ensuring you retrieve the dispatcher instance from AppState (app.state.notification_dispatcher or the local variable) and handle errors/await it as appropriate so the dispatcher's graceful shutdown hooks run before process exit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/synthorg/api/app.py`:
- Around line 1074-1077: The auth-rate limiter key can be mixed-type; change the
return value when a user is present to return a normalized string instead of raw
user.user_id: convert user.user_id to str (replace the current return
user.user_id and remove the "# type: ignore[no-any-return]") so the limiter key
is always a str, leaving get_remote_address(request) as the fallback.
In `@src/synthorg/budget/enforcer.py`:
- Around line 393-397: The enforcement paths currently await sink delivery via
awaiting self._notify_budget_event before raising
BudgetExhaustedError/DailyLimitExceededError, which makes slow notification
backends block enforcement; change these call sites (the await calls around
self._notify_budget_event in the methods that raise
BudgetExhaustedError/DailyLimitExceededError) to fire-and-forget the
notification instead (e.g., use
asyncio.create_task(self._notify_budget_event(...)) and do not await it) and
optionally wrap the background task creation in a short exception-guard (or
schedule with a bounded asyncio.wait_for inside the task) so any sink errors
don’t affect the immediate raising of the budget exceptions; ensure you remove
the awaiting semantics but keep logging/visibility inside _notify_budget_event
itself.
In `@src/synthorg/notifications/adapters/email.py`:
- Around line 126-131: Replace the raw string event name used in the
logger.warning call inside the email adapter with a shared event constant:
define PARTIAL_CREDENTIALS (or similar ALL_CAPS name) in
synthorg.observability.events.notification and export it, then import that
constant into src/synthorg/notifications/adapters/email.py and use it in the
logger.warning(...) call instead of "notification.email.partial_credentials";
update any existing imports in that module to include the new constant so the
adapter uses the catalog-driven event name.
- Around line 115-120: The SMTP call currently blocks indefinitely because
smtplib.SMTP(...) is created without a timeout; update the adapter to
accept/configure a timeout (e.g., self._timeout) and pass it into
smtplib.SMTP(self._host, self._port, timeout=self._timeout) in the block that
also uses self._use_tls and self._login_if_configured(smtp). Ensure the
adapter's initializer or config parsing sets self._timeout (default to a
sensible finite value or None) so the timeout is threaded through to the send
path where smtp.send_message(msg) is called.
In `@src/synthorg/notifications/adapters/slack.py`:
- Line 7: The shared URL validator _validate_outbound_url (imported into
src/synthorg/notifications/adapters/slack.py) currently returns early for
non-literal hostnames and thus is vulnerable to hostname-based SSRF; update the
validator (and any Slack adapter URL acceptance paths around the Slack adapter
constructor/initialization and the code paths noted at lines ~59-60) to perform
DNS resolution of the hostname (including following CNAMEs and collecting all
A/AAAA records) and validate every resolved IP against disallowed ranges
(RFC1918, link-local, loopback, etc.) or an explicit outbound-allowlist; if any
resolved address is private/loopback/link-local or not in the allowlist, reject
the URL (raise/return error) and ensure you handle timeouts and multiple
addresses consistently.
In `@src/synthorg/notifications/config.py`:
- Around line 38-41: The params field in NotificationSinkConfig is typed as
dict[str, str] but Pydantic v2 does not coerce non-string YAML scalars to str,
causing validation errors; fix by adding coerce_numbers_to_str=True to
NotificationSinkConfig.model_config (ConfigDict) so numeric/boolean YAML scalars
are coerced to strings, or alternatively replace the generic params with
explicit per-sink Typed models and update the factory functions that read params
(the code referencing params in the factory) to accept the typed fields instead;
ensure NotificationSinkConfig.model_config includes
ConfigDict(coerce_numbers_to_str=True) if you choose the coercion route.
In `@src/synthorg/notifications/models.py`:
- Around line 100-104: The model validator _deep_copy_metadata currently only
deep-copies when self.metadata is truthy, which skips caller-supplied empty
dicts; change _deep_copy_metadata (the `@model_validator`(mode="after") on
Notification) to unconditionally set object.__setattr__(self, "metadata",
copy.deepcopy(self.metadata)) so even {} is snapped, and add a regression test
(e.g., test_notification_metadata_deepcopy) that constructs a Notification with
metadata={} then mutates the original dict to ensure the frozen model's metadata
does not change.
In `@src/synthorg/security/timeout/scheduler.py`:
- Around line 202-203: The await on self._notify_escalation in the timeout sweep
is stalling the whole sweep because NotificationDispatcher.dispatch waits for
all sinks; change the code in _notify_escalation call sites (e.g., the
invocation in the scheduler sweep and the other occurrences around the 247-283
region) to run notification dispatch off the critical path by scheduling it as a
background task (e.g., asyncio.create_task) or by wrapping the dispatch call in
asyncio.wait_for with a short timeout, and ensure you capture and log
timeouts/exceptions inside that task so failures don't propagate back to or
block the sweep loop (also consider using asyncio.shield/run_in_executor if
needed for sync sinks).
In `@tests/unit/notifications/test_models.py`:
- Around line 17-32: Replace the hand-unrolled assertions in the tests for
NotificationCategory and NotificationSeverity with parametric tests using
pytest.mark.parametrize: for the class TestNotificationCategory change
test_values to a single parametric test that iterates over
(NotificationCategory.<MEMBER>, "<expected_value>") pairs and asserts
member.value == expected, and for TestNotificationSeverity do the same iterating
over (NotificationSeverity.<MEMBER>, "<expected_value>"); target the existing
test functions named test_values and the enum classes NotificationCategory and
NotificationSeverity so future enum additions only require adding a new tuple to
the param list.
In `@web/src/components/notifications/NotificationDrawer.stories.tsx`:
- Around line 58-69: The WithItems story seeds the Zustand store via the
SeedNotifications decorator but doesn't reset state between stories, so seeded
notifications leak into other stories like Empty; update the story setup by
resetting the notifications store after each render — either add a cleanup
effect inside SeedNotifications (call the store's reset/clear method in a
useEffect cleanup) or use Storybook's lifecycle (beforeEach/afterEach) to call
the store's reset function so WithItems, SeedNotifications, and Empty do not
share state across story switches.
In `@web/src/types/notifications.ts`:
- Around line 183-190: Replace the duplicated local ToastVariant type with the
canonical one from the toast store: import and re-export the type from the toast
store (use the exported ToastVariant from the toast store) and update
SEVERITY_TO_TOAST_VARIANT to use that imported type as its value type; ensure
NotificationSeverity remains the key type and keep the mapping values unchanged,
and remove the local ToastVariant declaration so there is a single source of
truth.
---
Outside diff comments:
In `@src/synthorg/api/app.py`:
- Around line 890-915: The NotificationDispatcher created by
build_notification_dispatcher is stored in notification_dispatcher and passed
into AppState in create_app(), but it is not closed during shutdown; update the
app shutdown path (on_shutdown and/or _safe_shutdown) to call the dispatcher’s
close/shutdown method (e.g., notification_dispatcher.close() or await
notification_dispatcher.shutdown() depending on its API), ensuring you retrieve
the dispatcher instance from AppState (app.state.notification_dispatcher or the
local variable) and handle errors/await it as appropriate so the dispatcher's
graceful shutdown hooks run before process exit.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 06415987-5313-4b23-a5f3-16014e5c6dfa
📒 Files selected for processing (37)
README.mddocs/design/operations.mddocs/design/page-structure.mddocs/security.mdsrc/synthorg/api/app.pysrc/synthorg/budget/enforcer.pysrc/synthorg/engine/approval_gate.pysrc/synthorg/notifications/__init__.pysrc/synthorg/notifications/adapters/email.pysrc/synthorg/notifications/adapters/ntfy.pysrc/synthorg/notifications/adapters/slack.pysrc/synthorg/notifications/config.pysrc/synthorg/notifications/dispatcher.pysrc/synthorg/notifications/factory.pysrc/synthorg/notifications/models.pysrc/synthorg/notifications/protocol.pysrc/synthorg/observability/events/approval_gate.pysrc/synthorg/observability/events/budget.pysrc/synthorg/security/timeout/scheduler.pysrc/synthorg/settings/definitions/api.pysrc/synthorg/settings/resolver.pytests/unit/api/test_app.pytests/unit/notifications/test_config.pytests/unit/notifications/test_console_adapter.pytests/unit/notifications/test_models.pytests/unit/notifications/test_ntfy_adapter.pyweb/src/components/layout/AppLayout.tsxweb/src/components/layout/Sidebar.tsxweb/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationDrawer.tsxweb/src/components/notifications/NotificationEmptyState.stories.tsxweb/src/components/notifications/NotificationItemCard.stories.tsxweb/src/components/notifications/NotificationItemCard.tsxweb/src/hooks/useGlobalNotifications.tsweb/src/pages/settings/NotificationsSection.tsxweb/src/stores/notifications.tsweb/src/types/notifications.ts
|
|
||
| import httpx | ||
|
|
||
| from synthorg.notifications.adapters.ntfy import _validate_outbound_url |
There was a problem hiding this comment.
The shared URL validator still allows hostname-based SSRF.
This constructor inherits _validate_outbound_url() from src/synthorg/notifications/adapters/ntfy.py:33-52, and that helper returns early for any non-literal hostname. A hostname that resolves to RFC1918, link-local, or loopback space will still pass here, so the current hardening is bypassable for both Slack and ntfy sinks. Resolve and vet DNS answers as well, or enforce an outbound allowlist, before accepting the URL.
Also applies to: 59-60
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/notifications/adapters/slack.py` at line 7, The shared URL
validator _validate_outbound_url (imported into
src/synthorg/notifications/adapters/slack.py) currently returns early for
non-literal hostnames and thus is vulnerable to hostname-based SSRF; update the
validator (and any Slack adapter URL acceptance paths around the Slack adapter
constructor/initialization and the code paths noted at lines ~59-60) to perform
DNS resolution of the hostname (including following CNAMEs and collecting all
A/AAAA records) and validate every resolved IP against disallowed ranges
(RFC1918, link-local, loopback, etc.) or an explicit outbound-allowlist; if any
resolved address is private/loopback/link-local or not in the allowlist, reject
the URL (raise/return error) and ensure you handle timeouts and multiple
addresses consistently.
| def test_values(self) -> None: | ||
| assert NotificationCategory.APPROVAL.value == "approval" | ||
| assert NotificationCategory.BUDGET.value == "budget" | ||
| assert NotificationCategory.SECURITY.value == "security" | ||
| assert NotificationCategory.STAGNATION.value == "stagnation" | ||
| assert NotificationCategory.SYSTEM.value == "system" | ||
| assert NotificationCategory.AGENT.value == "agent" | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| class TestNotificationSeverity: | ||
| def test_values(self) -> None: | ||
| assert NotificationSeverity.INFO.value == "info" | ||
| assert NotificationSeverity.WARNING.value == "warning" | ||
| assert NotificationSeverity.ERROR.value == "error" | ||
| assert NotificationSeverity.CRITICAL.value == "critical" |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Parametrize these enum value checks.
These are hand-unrolled table tests. @pytest.mark.parametrize will keep future taxonomy additions to one-line cases and matches the repo’s test conventions.
As per coding guidelines, "Use @pytest.mark.parametrize for testing similar cases in Python."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/unit/notifications/test_models.py` around lines 17 - 32, Replace the
hand-unrolled assertions in the tests for NotificationCategory and
NotificationSeverity with parametric tests using pytest.mark.parametrize: for
the class TestNotificationCategory change test_values to a single parametric
test that iterates over (NotificationCategory.<MEMBER>, "<expected_value>")
pairs and asserts member.value == expected, and for TestNotificationSeverity do
the same iterating over (NotificationSeverity.<MEMBER>, "<expected_value>");
target the existing test functions named test_values and the enum classes
NotificationCategory and NotificationSeverity so future enum additions only
require adding a new tuple to the param list.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/src/components/notifications/NotificationDrawer.tsx`:
- Around line 39-49: The loops in handleMarkAllRead and handleClearAll cause
many individual store updates and re-renders; add batch APIs to the
notifications store (e.g., markReadBatch(ids: readonly string[]) and
dismissBatch(ids: readonly string[])) that update state once and call
debouncedPersist(get()), then replace the loops in NotificationDrawer's
handleMarkAllRead and handleClearAll to collect ids from filteredItems and call
markReadBatch(ids) and dismissBatch(ids) respectively; ensure you reference the
existing state helpers (countUnread, debouncedPersist) when implementing the
batch methods.
In `@web/src/components/notifications/NotificationFilterBar.tsx`:
- Around line 5-14: The hardcoded GROUPS array duplicates values from
CATEGORY_CONFIGS and can drift; replace the literal GROUPS with a derived value
computed from CATEGORY_CONFIGS (e.g., map/collect its keys or config
identifiers) so GROUPS is generated from the source of truth used elsewhere (as
done in NotificationsSection.tsx); update any references to GROUPS to use the
new derived constant so adding/removing entries in CATEGORY_CONFIGS
automatically updates the filter groups.
In `@web/src/services/browser-notifications.ts`:
- Around line 20-27: The null-coalescing fallback in isRateLimited is redundant
because the preceding check recentTimestamps.length > 0 guarantees
recentTimestamps[0] is defined; replace the expression now -
(recentTimestamps[0] ?? now) with now - recentTimestamps[0]! (or simply now -
recentTimestamps[0]) inside the while loop so the pruning logic uses the actual
timestamp and removes the unreachable ?? now fallback; reference isRateLimited,
recentTimestamps, WINDOW_MS, and MAX_NOTIFICATIONS.
- Around line 78-83: The notification icon path is hardcoded to
'/synthorg-icon.png' which doesn't exist; update the Notification creation in
the try block (where new Notification(...) assigns to the notification variable
in this module/service) to use the existing asset path '/favicon.svg' (or
alternatively add the missing synthorg-icon.png to web/public/) so browser
notifications reference a valid icon file.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 644cf25a-7208-42ed-9559-76aafa93a420
📒 Files selected for processing (7)
web/src/__tests__/pages/SettingsPage.test.tsxweb/src/__tests__/stores/agents.test.tsweb/src/components/notifications/NotificationDrawer.tsxweb/src/components/notifications/NotificationFilterBar.tsxweb/src/pages/SettingsPage.tsxweb/src/pages/settings/NotificationsSection.tsxweb/src/services/browser-notifications.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
web/src/**/*.{ts,tsx}: TypeScript files in web dashboard must reuse existing components from web/src/components/ui/ before creating new ones.
React dashboard must use TypeScript 6.0+, React 19, react-router, shadcn/ui, Base UI, Tailwind CSS 4, Zustand,@tanstack/react-query. No hardcoded styles—use design tokens.
Linting and pre-commit checks must not be bypassed—ESLint web dashboard (zero warnings) is non-negotiable.
web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground,bg-card,text-accent,text-success,bg-danger, etc.) or CSS variables (var(--so-*)) for colors; NEVER hardcode hex values in.tsx/.tsfiles
Usefont-sansorfont-mono(Geist tokens) for typography; NEVER setfontFamilydirectly in.tsx/.tsfiles
Use density-aware tokens (p-card,gap-section-gap,gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing in components
Use token variables (var(--so-shadow-card-hover),border-border,border-bright) for shadows and borders; NEVER hardcode values in.tsx/.tsfiles
Use@/lib/motionpresets for Framer Motion transition durations; NEVER hardcode transition durations
CSS side-effect imports in TypeScript 6 require type declarations -- add/// <reference types="vite/client" />at the top of files with CSS imports
Files:
web/src/pages/SettingsPage.tsxweb/src/__tests__/pages/SettingsPage.test.tsxweb/src/__tests__/stores/agents.test.tsweb/src/components/notifications/NotificationFilterBar.tsxweb/src/components/notifications/NotificationDrawer.tsxweb/src/pages/settings/NotificationsSection.tsxweb/src/services/browser-notifications.ts
web/src/**/*.{ts,tsx,css}
📄 CodeRabbit inference engine (CLAUDE.md)
web/src/**/*.{ts,tsx,css}: Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions in web dashboard code—use design tokens and@/lib/motionpresets.
Web dashboard scripts/check_web_design_system.py enforces component reuse and design token usage on every Edit/Write to web/src/.
Files:
web/src/pages/SettingsPage.tsxweb/src/__tests__/pages/SettingsPage.test.tsxweb/src/__tests__/stores/agents.test.tsweb/src/components/notifications/NotificationFilterBar.tsxweb/src/components/notifications/NotificationDrawer.tsxweb/src/pages/settings/NotificationsSection.tsxweb/src/services/browser-notifications.ts
web/src/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx,js,jsx}: Always usecreateLoggerfrom@/lib/logger-- never bareconsole.warn/console.error/console.debugin application code
Logger variable name must always beconst log(e.g.const log = createLogger('module-name'))
Pass dynamic/untrusted values as separate arguments to logger methods (not interpolated into the message string) so they go throughsanitizeArg
Attacker-controlled fields inside structured objects must be wrapped insanitizeForLog()before embedding in log calls
Files:
web/src/pages/SettingsPage.tsxweb/src/__tests__/pages/SettingsPage.test.tsxweb/src/__tests__/stores/agents.test.tsweb/src/components/notifications/NotificationFilterBar.tsxweb/src/components/notifications/NotificationDrawer.tsxweb/src/pages/settings/NotificationsSection.tsxweb/src/services/browser-notifications.ts
web/src/**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Web dashboard components must be tested with Vitest,
@testing-library/react, and fast-check for property-based testing.
Files:
web/src/__tests__/pages/SettingsPage.test.tsxweb/src/__tests__/stores/agents.test.ts
web/src/**/*.{test,stories}.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Web dashboard must use MSW (Mock Service Worker) for API mocking in tests and Storybook.
Files:
web/src/__tests__/pages/SettingsPage.test.tsxweb/src/__tests__/stores/agents.test.ts
🧠 Learnings (11)
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/**/*.stories.{ts,tsx} : Storybook 10: Use parameters.a11y.test: 'error' | 'todo' | 'off' for a11y testing (replaces old .element and .manual); set globally in preview.tsx to enforce WCAG compliance on all stories
Applied to files:
web/src/__tests__/pages/SettingsPage.test.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/.storybook/**/*.{ts,tsx} : In Storybook 10, use `parameters.a11y.test: 'error' | 'todo' | 'off'` for accessibility testing (replaces old `.element` and `.manual`); set globally in `preview.tsx` to enforce WCAG compliance
Applied to files:
web/src/__tests__/pages/SettingsPage.test.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10: Use `parameters.a11y.test: 'error' | 'todo' | 'off'` for a11y testing configuration (replaces old `.element` and `.manual`)
Applied to files:
web/src/__tests__/pages/SettingsPage.test.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10: Import from `storybook/test` instead of `storybook/test`
Applied to files:
web/src/__tests__/pages/SettingsPage.test.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/stores/**/*.{ts,tsx} : Use Zustand stores in web dashboard for state management (auth, WebSocket, toast, analytics, domain shells)
Applied to files:
web/src/__tests__/stores/agents.test.ts
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Zustand stores for state management in the web dashboard; each domain has its own store module (auth, WebSocket, toast, analytics, setup, company, agents, budget, tasks, settings, providers, theme, per-domain stores)
Applied to files:
web/src/__tests__/stores/agents.test.ts
📚 Learning: 2026-04-06T06:43:24.031Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T06:43:24.031Z
Learning: Applies to web/src/**/*.{test,stories}.{ts,tsx} : Web dashboard must use MSW (Mock Service Worker) for API mocking in tests and Storybook.
Applied to files:
web/src/__tests__/stores/agents.test.ts
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/layout/**/*.{ts,tsx} : Mount `ToastContainer` once in AppLayout for success/error/warning/info notifications with auto-dismiss queue
Applied to files:
web/src/__tests__/stores/agents.test.tsweb/src/components/notifications/NotificationDrawer.tsxweb/src/pages/settings/NotificationsSection.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components
Applied to files:
web/src/components/notifications/NotificationFilterBar.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively
Applied to files:
web/src/components/notifications/NotificationDrawer.tsxweb/src/pages/settings/NotificationsSection.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Do NOT build card-with-header layouts from scratch -- use `<SectionCard>` from `@/components/ui/section-card`
Applied to files:
web/src/pages/settings/NotificationsSection.tsx
🔇 Additional comments (10)
web/src/__tests__/stores/agents.test.ts (1)
480-578: Looks correct: tests now match the new notifications boundary.These updates correctly assert that
personality.trimmedno longer dispatchesuseToastStoretoasts from the agents store path.web/src/pages/SettingsPage.tsx (2)
350-361: Good accessibility improvement withrole="alert".Adding the ARIA alert role ensures screen readers announce error messages immediately, improving accessibility for users relying on assistive technology.
446-448: LGTM — sensible placement for client-side notification preferences.Positioning
NotificationsSectionafter backend settings but before the save bar correctly separates client-only preferences from server-persisted settings.web/src/__tests__/pages/SettingsPage.test.tsx (1)
160-164: LGTM — improved test robustness via role-based query.Using
getByRole('alert')aligns with Testing Library best practices (query by accessibility role over text) and correctly mirrors therole="alert"addition in the component.web/src/components/notifications/NotificationDrawer.tsx (1)
78-97: LGTM — clean list rendering with accessibility considerations.Good use of
role="list"on the container andStaggerGroupfor animation. The empty state handling is clear.web/src/pages/settings/NotificationsSection.tsx (3)
33-42: Theeslint-disablefor exhaustive-deps is justified here.The effect intentionally runs only on mount to sync the browser's actual permission state with the store. The comment explains the rationale clearly.
51-61: LGTM —toggleRoutecorrectly handles override creation and updates.The logic properly falls back to spreading
defaultRouteswhen no override exists, preventing mutation of the original array.
121-128: No Checkbox component exists in the design system. The current implementation using<input type="checkbox">with theaccent-accentsemantic Tailwind class already follows design token guidelines. The available form componentToggleFieldis a toggle switch pattern unsuitable for this inline multi-option use case.> Likely an incorrect or invalid review comment.web/src/services/browser-notifications.ts (2)
85-95: LGTM — click handler correctly integrates with app navigation.The
CustomEventdispatch aligns with the listener inAppLayout.tsx(lines 105-115), and the validation there (href.startsWith('/') && !href.startsWith('//')) guards against open redirects.
63-74: LGTM — visibility and rate-limit guards are well-designed.Suppressing browser notifications when the tab is visible (toast suffices) and rate-limiting to 3/10s prevents notification spam while preserving important alerts.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/synthorg/api/app.py (1)
523-536:⚠️ Potential issue | 🟠 MajorStop producers before closing the notification dispatcher.
These notifications are now scheduled via
asyncio.create_task(...)in the budget and timeout services, so closing the dispatcher at the start ofon_shutdown()races in-flight sends and any last notifications emitted during_safe_shutdown(...).🧯 Safer shutdown order
- if app_state.has_notification_dispatcher: - await _try_stop( - app_state.notification_dispatcher.close(), - API_APP_SHUTDOWN, - "Failed to stop notification dispatcher", - ) if _health_prober is not None: await _try_stop( _health_prober.stop(), API_APP_SHUTDOWN, "Failed to stop health prober", @@ await _safe_shutdown( task_engine, meeting_scheduler, backup_service, approval_timeout_scheduler, settings_dispatcher, bridge, message_bus, persistence, performance_tracker=app_state._performance_tracker, # noqa: SLF001 ) + if app_state.has_notification_dispatcher: + await _try_stop( + app_state.notification_dispatcher.close(), + API_APP_SHUTDOWN, + "Failed to stop notification dispatcher", + )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/api/app.py` around lines 523 - 536, on_shutdown currently closes app_state.notification_dispatcher before stopping producers (tasks that schedule notifications), which can race with in-flight sends; update on_shutdown so that after cancelling _ticket_cleanup_task you first stop/await any producer services that schedule notifications (e.g., the budget/timeout producers represented by _auto_wired_dispatcher or whatever producer stop API exists on app_state, and _health_prober if it emits notifications), ensuring those producers are fully shut down/awaited via _try_stop (or explicit cancel+await) before calling app_state.notification_dispatcher.close(); then proceed to call _try_stop for notification_dispatcher as before.
♻️ Duplicate comments (1)
web/src/components/notifications/NotificationDrawer.stories.tsx (1)
28-37: 🧹 Nitpick | 🔵 TrivialStore state should be reset before seeding in both story decorators.
The
Emptystory clears on mount but doesn't clean up on unmount. If a user switches fromWithItemstoEmpty, theEmptydecorator runs afterWithItemshas already seeded, which is correct. However, switching fromEmptytoWithItemsleaves stale state from the previousWithItemsrender sinceSeedNotificationsdoesn't clear before seeding.♻️ Add cleanup and reset for reliable story isolation
export const Empty: Story = { decorators: [ (Story) => { useEffect(() => { useNotificationsStore.getState().clearAll() + return () => useNotificationsStore.getState().clearAll() }, []) return <Story /> }, ], }And in
SeedNotifications:function SeedNotifications({ children }: { readonly children: React.ReactNode }) { useEffect(() => { + const { clearAll, enqueue } = useNotificationsStore.getState() + clearAll() // Reset before seeding - const { enqueue } = useNotificationsStore.getState() enqueue({ category: 'approvals.pending', // ... }) // ... other enqueue calls + return () => clearAll() // Cleanup on unmount }, []) return <>{children}</> }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/components/notifications/NotificationDrawer.stories.tsx` around lines 28 - 37, The Empty story decorator currently clears notifications only on mount and does not clean up on unmount, and SeedNotifications seeds without clearing existing state; update both to ensure story isolation by calling useNotificationsStore.getState().clearAll() before seeding in SeedNotifications and adding a cleanup that clears the store on unmount in the Empty decorator (and likewise return a cleanup function in SeedNotifications if it mounts effects) so each story always starts with an empty store and removes its seeded state on unmount.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/synthorg/notifications/adapters/email.py`:
- Around line 83-88: The send path currently ignores SMTP.send_message()'s
return value in _send_sync, so partial recipient refusals are treated as full
success; update _send_sync (the method called via asyncio.to_thread) to capture
the return value from smtp.send_message(message) and if the returned dict is
non-empty raise smtplib.SMTPRecipientsRefused with that dict, so the outer await
asyncio.to_thread(self._send_sync, notification) will surface failures; also
apply the same check-and-raise change to the other send site handled similarly
(the second call around NOTIFICATION_EMAIL_DELIVERED in the alternate send flow)
so both code paths log NOTIFICATION_EMAIL_DELIVERED only when no recipients were
refused.
In `@src/synthorg/notifications/models.py`:
- Around line 11-18: This module is missing the standard module logger: import
get_logger from synthorg.observability and declare a module-level logger
variable; add "from synthorg.observability import get_logger" and then "logger =
get_logger(__name__)" near the top of the file (alongside existing imports) so
that the module-level logger is available for functions/classes in models.py.
In `@web/src/stores/notifications.ts`:
- Around line 303-315: Replace the O(n*m) includes-based lookup in markReadBatch
with a Set-based O(1) check and avoid mapping items twice: create a const idSet
= new Set(ids), then compute a single mappedItems = state.items.map(item =>
idSet.has(item.id) ? { ...item, read: true } : item), use mappedItems for both
the new items value and the unreadCount calculation (call
countUnread(mappedItems)), and keep the debouncedPersist(get()) call and
existing state shape unchanged; this mirrors the efficient pattern used in
dismissBatch.
---
Outside diff comments:
In `@src/synthorg/api/app.py`:
- Around line 523-536: on_shutdown currently closes
app_state.notification_dispatcher before stopping producers (tasks that schedule
notifications), which can race with in-flight sends; update on_shutdown so that
after cancelling _ticket_cleanup_task you first stop/await any producer services
that schedule notifications (e.g., the budget/timeout producers represented by
_auto_wired_dispatcher or whatever producer stop API exists on app_state, and
_health_prober if it emits notifications), ensuring those producers are fully
shut down/awaited via _try_stop (or explicit cancel+await) before calling
app_state.notification_dispatcher.close(); then proceed to call _try_stop for
notification_dispatcher as before.
---
Duplicate comments:
In `@web/src/components/notifications/NotificationDrawer.stories.tsx`:
- Around line 28-37: The Empty story decorator currently clears notifications
only on mount and does not clean up on unmount, and SeedNotifications seeds
without clearing existing state; update both to ensure story isolation by
calling useNotificationsStore.getState().clearAll() before seeding in
SeedNotifications and adding a cleanup that clears the store on unmount in the
Empty decorator (and likewise return a cleanup function in SeedNotifications if
it mounts effects) so each story always starts with an empty store and removes
its seeded state on unmount.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: ad815af6-75ed-4d8d-93b6-7e80393c3f84
📒 Files selected for processing (12)
src/synthorg/api/app.pysrc/synthorg/budget/enforcer.pysrc/synthorg/notifications/adapters/email.pysrc/synthorg/notifications/models.pysrc/synthorg/observability/events/notification.pysrc/synthorg/security/timeout/scheduler.pyweb/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationDrawer.tsxweb/src/components/notifications/NotificationFilterBar.tsxweb/src/services/browser-notifications.tsweb/src/stores/notifications.tsweb/src/types/notifications.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: Dashboard Test
- GitHub Check: Test (Python 3.14)
- GitHub Check: Build Backend
- GitHub Check: Build Web
- GitHub Check: Build Sandbox
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (8)
web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
web/src/**/*.{ts,tsx}: TypeScript files in web dashboard must reuse existing components from web/src/components/ui/ before creating new ones.
React dashboard must use TypeScript 6.0+, React 19, react-router, shadcn/ui, Base UI, Tailwind CSS 4, Zustand,@tanstack/react-query. No hardcoded styles—use design tokens.
Linting and pre-commit checks must not be bypassed—ESLint web dashboard (zero warnings) is non-negotiable.
web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground,bg-card,text-accent,text-success,bg-danger, etc.) or CSS variables (var(--so-*)) for colors; NEVER hardcode hex values in.tsx/.tsfiles
Usefont-sansorfont-mono(Geist tokens) for typography; NEVER setfontFamilydirectly in.tsx/.tsfiles
Use density-aware tokens (p-card,gap-section-gap,gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing in components
Use token variables (var(--so-shadow-card-hover),border-border,border-bright) for shadows and borders; NEVER hardcode values in.tsx/.tsfiles
Use@/lib/motionpresets for Framer Motion transition durations; NEVER hardcode transition durations
CSS side-effect imports in TypeScript 6 require type declarations -- add/// <reference types="vite/client" />at the top of files with CSS imports
Files:
web/src/components/notifications/NotificationFilterBar.tsxweb/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationDrawer.tsxweb/src/services/browser-notifications.tsweb/src/types/notifications.tsweb/src/stores/notifications.ts
web/src/**/*.{ts,tsx,css}
📄 CodeRabbit inference engine (CLAUDE.md)
web/src/**/*.{ts,tsx,css}: Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions in web dashboard code—use design tokens and@/lib/motionpresets.
Web dashboard scripts/check_web_design_system.py enforces component reuse and design token usage on every Edit/Write to web/src/.
Files:
web/src/components/notifications/NotificationFilterBar.tsxweb/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationDrawer.tsxweb/src/services/browser-notifications.tsweb/src/types/notifications.tsweb/src/stores/notifications.ts
web/src/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx,js,jsx}: Always usecreateLoggerfrom@/lib/logger-- never bareconsole.warn/console.error/console.debugin application code
Logger variable name must always beconst log(e.g.const log = createLogger('module-name'))
Pass dynamic/untrusted values as separate arguments to logger methods (not interpolated into the message string) so they go throughsanitizeArg
Attacker-controlled fields inside structured objects must be wrapped insanitizeForLog()before embedding in log calls
Files:
web/src/components/notifications/NotificationFilterBar.tsxweb/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationDrawer.tsxweb/src/services/browser-notifications.tsweb/src/types/notifications.tsweb/src/stores/notifications.ts
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py: Do NOT usefrom __future__ import annotationsin Python code—Python 3.14 has PEP 649 native lazy annotations.
Use PEP 758 except syntax:except A, B:(no parentheses) in Python 3.14 code—ruff enforces this.
All public functions in Python must have type hints. Use mypy strict mode for type-checking.
Use Google-style docstrings on all public classes and functions in Python. This is enforced by ruff D rules.
Use NotBlankStr (from core.types) for all identifier/name fields in Python—including optional (NotBlankStr | None) and tuple variants—instead of manual whitespace validators.
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in Python—prefer structured concurrency over bare create_task.
Python line length must be 88 characters (enforced by ruff).
Python functions must be under 50 lines, files under 800 lines.
Handle errors explicitly in Python, never silently swallow exceptions.
Always use variable namelogger(not_loggerorlog) for the logging instance in Python.
Lint Python withuv run ruff check src/ tests/. Auto-fix withuv run ruff check src/ tests/ --fix. Format withuv run ruff format src/ tests/.
Type-check Python withuv run mypy src/ tests/(strict mode).
Files:
src/synthorg/budget/enforcer.pysrc/synthorg/security/timeout/scheduler.pysrc/synthorg/api/app.pysrc/synthorg/notifications/adapters/email.pysrc/synthorg/observability/events/notification.pysrc/synthorg/notifications/models.py
src/synthorg/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
src/synthorg/**/*.py: Every Python module with business logic MUST have:from synthorg.observability import get_loggerthenlogger = get_logger(__name__)
Never useimport logging,logging.getLogger(), orprint()in Python application code. Exceptions: observability/setup.py, observability/sinks.py, observability/syslog_handler.py, observability/http_handler.py may use stdlib logging and print.
Use event name constants from synthorg.observability.events domain modules (e.g., API_REQUEST_STARTED from events.api, TOOL_INVOKE_START from events.tool). Import directly:from synthorg.observability.events.<domain> import EVENT_CONSTANT
Use structured logging withlogger.info(EVENT, key=value)syntax in Python—neverlogger.info('msg %s', val)
All error paths in Python must log at WARNING or ERROR with context before raising.
All state transitions in Python must log at INFO.
DEBUG logging is for object creation, internal flow, and entry/exit of key functions in Python.
Never use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in project-owned Python code, docstrings, comments, tests, or config examples. Use generic names: example-provider, example-large-001, example-medium-001, example-small-001, or large/medium/small aliases. Exceptions: Operations design page, .claude/ skill files, third-party imports, provider presets (user-facing runtime data).
Library reference in docs/api/ is auto-generated via mkdocstrings + Griffe (AST-based).
Files:
src/synthorg/budget/enforcer.pysrc/synthorg/security/timeout/scheduler.pysrc/synthorg/api/app.pysrc/synthorg/notifications/adapters/email.pysrc/synthorg/observability/events/notification.pysrc/synthorg/notifications/models.py
src/**/*.py
⚙️ CodeRabbit configuration file
This project uses Python 3.14+ with PEP 758 except syntax: "except A, B:" (comma-separated, no parentheses) is correct and mandatory -- do NOT flag it as a typo or suggest parenthesized form. The "except builtins.MemoryError, RecursionError: raise" pattern is intentional project convention for system-error propagation. When evaluating the 50-line function limit, count only the function body excluding the signature lines, decorators, and docstring. Functions 1-5 lines over due to docstrings or multi-line signatures should not be flagged. Do not suggest extracting single-use helper functions called exactly once -- this reduces readability without improving maintainability.
Files:
src/synthorg/budget/enforcer.pysrc/synthorg/security/timeout/scheduler.pysrc/synthorg/api/app.pysrc/synthorg/notifications/adapters/email.pysrc/synthorg/observability/events/notification.pysrc/synthorg/notifications/models.py
web/src/**/*.{test,stories}.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Web dashboard must use MSW (Mock Service Worker) for API mocking in tests and Storybook.
Files:
web/src/components/notifications/NotificationDrawer.stories.tsx
web/src/**/*.stories.tsx
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.stories.tsx: For Storybook stories withtags: ['autodocs'], ensure@storybook/addon-docsis installed and added to addons
Usestorybook/testandstorybook/actionsimport paths in Storybook stories (not@storybook/testor@storybook/addon-actions)
Files:
web/src/components/notifications/NotificationDrawer.stories.tsx
🧠 Learnings (77)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T06:43:24.031Z
Learning: RetryConfig and RateLimiterConfig are set per-provider in ProviderConfig in Python.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T19:13:36.562Z
Learning: Applies to src/synthorg/providers/**/*.py : RetryConfig and RateLimiterConfig are set per-provider in ProviderConfig.
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)
Applied to files:
web/src/components/notifications/NotificationFilterBar.tsxweb/src/types/notifications.ts
📚 Learning: 2026-03-19T07:13:44.964Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:13:44.964Z
Learning: Applies to src/synthorg/budget/**/*.py : Budget package (budget/): cost tracking, budget enforcement (pre-flight/in-flight checks, auto-downgrade), billing periods, cost tiers, quota/subscription tracking, CFO cost optimization (anomaly detection, efficiency analysis, downgrade recommendations, approval decisions), spending reports, budget errors (BudgetExhaustedError, DailyLimitExceededError, QuotaExhaustedError)
Applied to files:
src/synthorg/budget/enforcer.py
📚 Learning: 2026-03-31T21:07:37.470Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T21:07:37.470Z
Learning: Applies to **/*.py : Use `except A, B:` (no parentheses) per PEP 758 exception syntax on Python 3.14
Applied to files:
src/synthorg/budget/enforcer.pysrc/synthorg/security/timeout/scheduler.pysrc/synthorg/notifications/adapters/email.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to **/*.py : Use `except A, B:` syntax (without parentheses) per PEP 758 for exception handling in Python 3.14
Applied to files:
src/synthorg/budget/enforcer.pysrc/synthorg/security/timeout/scheduler.pysrc/synthorg/notifications/adapters/email.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Use `except A, B:` syntax (no parentheses) for exception handling — PEP 758 exception syntax enforced by ruff on Python 3.14
Applied to files:
src/synthorg/budget/enforcer.pysrc/synthorg/security/timeout/scheduler.pysrc/synthorg/notifications/adapters/email.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to **/*.py : Use PEP 758 except syntax with `except A, B:` (no parentheses) for multiple exceptions—ruff enforces this on Python 3.14.
Applied to files:
src/synthorg/budget/enforcer.pysrc/synthorg/security/timeout/scheduler.pysrc/synthorg/notifications/adapters/email.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Handle errors explicitly; never silently swallow exceptions
Applied to files:
src/synthorg/budget/enforcer.pysrc/synthorg/security/timeout/scheduler.pysrc/synthorg/notifications/adapters/email.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to **/*.py : Handle errors explicitly, never silently swallow exceptions
Applied to files:
src/synthorg/budget/enforcer.pysrc/synthorg/security/timeout/scheduler.pysrc/synthorg/notifications/adapters/email.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: `except A, B:` (no parentheses) — enforced by ruff on Python 3.14
Applied to files:
src/synthorg/budget/enforcer.pysrc/synthorg/security/timeout/scheduler.pysrc/synthorg/notifications/adapters/email.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to **/*.py : Handle errors explicitly—never silently swallow exceptions.
Applied to files:
src/synthorg/budget/enforcer.pysrc/synthorg/security/timeout/scheduler.pysrc/synthorg/notifications/adapters/email.py
📚 Learning: 2026-04-06T06:43:24.031Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T06:43:24.031Z
Learning: Applies to **/*.py : Handle errors explicitly in Python, never silently swallow exceptions.
Applied to files:
src/synthorg/budget/enforcer.pysrc/synthorg/security/timeout/scheduler.pysrc/synthorg/notifications/adapters/email.py
📚 Learning: 2026-04-06T06:43:24.031Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T06:43:24.031Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: `except A, B:` (no parentheses) in Python 3.14 code—ruff enforces this.
Applied to files:
src/synthorg/budget/enforcer.pysrc/synthorg/security/timeout/scheduler.pysrc/synthorg/notifications/adapters/email.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/budget/**/*.py : Budget tracking includes pre-flight/in-flight checks, auto-downgrade, billing periods, cost tiers, quota/subscription. CFO includes anomaly detection, efficiency analysis, downgrade recommendations.
Applied to files:
src/synthorg/budget/enforcer.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Budget: Cost tracking, budget enforcement (pre-flight/in-flight checks, auto-downgrade), billing periods, cost tiers, quota/subscription tracking, CFO cost optimization (anomaly detection, efficiency analysis, downgrade recommendations, approval decisions), spending reports, budget errors (BudgetExhaustedError, DailyLimitExceededError, QuotaExhaustedError).
Applied to files:
src/synthorg/budget/enforcer.py
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/ui/**/*.stories.tsx : Create a `.stories.tsx` file alongside each new shared component with all states (default, hover, loading, error, empty)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Create new shared components in `web/src/components/ui/` with `.stories.tsx` Storybook file covering all states (default, hover, loading, error, empty)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10: import from `storybook/test` (not `storybook/test`), `storybook/actions` (not `storybook/addon-actions`)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/**/*.stories.{ts,tsx} : Storybook 10: Use storybook/test (not storybook/test) and storybook/actions (not storybook/addon-actions) import paths
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10: Import from `storybook/test` instead of `storybook/test`
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/**/*.stories.tsx : Use `storybook/test` and `storybook/actions` import paths in Storybook stories (not `storybook/test` or `storybook/addon-actions`)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationDrawer.tsxweb/src/types/notifications.ts
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : When creating new shared web components, place in web/src/components/ui/ with kebab-case filename, create .stories.tsx alongside with all states (default, hover, loading, error, empty), export props as TypeScript interface, use design tokens exclusively with no hardcoded colors/fonts/spacing, and import cn from `@/lib/utils` for conditional class merging
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsxweb/src/types/notifications.ts
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/**/*.stories.{ts,tsx} : Storybook 10: Use parameters.backgrounds.options (object keyed by name) + initialGlobals.backgrounds.value for background options (replaces old default + values array)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-04-06T06:43:24.031Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T06:43:24.031Z
Learning: Applies to web/src/**/*.{test,stories}.{ts,tsx} : Web dashboard must use MSW (Mock Service Worker) for API mocking in tests and Storybook.
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/stores/**/*.{ts,tsx} : Use Zustand stores in web dashboard for state management (auth, WebSocket, toast, analytics, domain shells)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsxweb/src/types/notifications.tsweb/src/stores/notifications.ts
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Zustand stores for state management in the web dashboard; each domain has its own store module (auth, WebSocket, toast, analytics, setup, company, agents, budget, tasks, settings, providers, theme, per-domain stores)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsxweb/src/types/notifications.tsweb/src/stores/notifications.ts
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/layout/**/*.{ts,tsx} : Mount `ToastContainer` once in AppLayout for success/error/warning/info notifications with auto-dismiss queue
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationDrawer.tsxweb/src/types/notifications.tsweb/src/stores/notifications.ts
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Prefer `asyncio.TaskGroup` for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare `create_task`. Existing code is being migrated incrementally.
Applied to files:
src/synthorg/security/timeout/scheduler.py
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Every module with business logic must import `from synthorg.observability import get_logger` and define `logger = get_logger(__name__)`
Applied to files:
src/synthorg/security/timeout/scheduler.pysrc/synthorg/api/app.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Every module with business logic MUST have: `from synthorg.observability import get_logger` then `logger = get_logger(__name__)`. Never use import logging / logging.getLogger() / print() in application code.
Applied to files:
src/synthorg/security/timeout/scheduler.pysrc/synthorg/api/app.py
📚 Learning: 2026-03-19T11:33:01.580Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T11:33:01.580Z
Learning: Applies to src/synthorg/**/*.py : Every module with business logic must import logger via `from synthorg.observability import get_logger` and initialize with `logger = get_logger(__name__)`
Applied to files:
src/synthorg/security/timeout/scheduler.pysrc/synthorg/api/app.py
📚 Learning: 2026-03-17T06:43:14.114Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:43:14.114Z
Learning: Applies to src/synthorg/**/*.py : Every module with business logic MUST have: `from synthorg.observability import get_logger` then `logger = get_logger(__name__)`. Never use `import logging` / `logging.getLogger()` / `print()` in application code. Variable name: always `logger`.
Applied to files:
src/synthorg/security/timeout/scheduler.py
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/**/*.py : Every module with business logic MUST have `from synthorg.observability import get_logger` followed by `logger = get_logger(__name__)`.
Applied to files:
src/synthorg/security/timeout/scheduler.pysrc/synthorg/api/app.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-26T15:18:16.848Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-26T15:18:16.848Z
Learning: Applies to src/synthorg/api/**/*.py : Litestar API must include setup wizard, auth/, auto-wiring, and lifecycle management
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/**/*.py : Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP...
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/observability/**/*.py : Observability includes structured logging via `get_logger(__name__)`, correlation tracking, and log sinks.
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-16T06:24:56.341Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T06:24:56.341Z
Learning: Applies to src/synthorg/observability/**/*.py : Observability must use structured logging with correlation tracking and log sinks
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/observability/**/*.py : Observability package (observability/): structured logging, correlation tracking, log sinks; event constants organized by domain under observability/events/ (e.g., events.api, events.tool, events.git, events.context_budget, events.backup)
Applied to files:
src/synthorg/api/app.pysrc/synthorg/observability/events/notification.py
📚 Learning: 2026-03-16T06:24:56.341Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T06:24:56.341Z
Learning: Applies to src/synthorg/**/*.py : Every module with business logic must have: `from synthorg.observability import get_logger` then `logger = get_logger(__name__)`
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/api/**/*.py : Use Litestar for REST + WebSocket API. Controllers, guards, channels, JWT + API key + WS ticket auth, RFC 9457 structured errors.
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/api/**/*.py : Use Litestar for REST API and WebSocket API with JWT + API key + WS ticket authentication, RFC 9457 structured errors, and content negotiation.
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/api/**/*.py : REST API: Litestar framework, controllers with guards, channels for WebSocket, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint. RFC 9457 structured errors (ErrorCategory, ErrorCode, ErrorDetail, ProblemDetail, CATEGORY_TITLES, category_title, category_type_uri, content negotiation).
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-18T21:23:23.586Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T21:23:23.586Z
Learning: Applies to src/synthorg/**/*.py : Event names: always use constants from the domain-specific module under synthorg.observability.events (e.g., API_REQUEST_STARTED from events.api, TOOL_INVOKE_START from events.tool). Import directly from synthorg.observability.events.<domain>.
Applied to files:
src/synthorg/notifications/adapters/email.pysrc/synthorg/observability/events/notification.py
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Always use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `PROVIDER_CALL_START` from `events.provider`); import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`
Applied to files:
src/synthorg/notifications/adapters/email.pysrc/synthorg/observability/events/notification.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Always use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`); import directly rather than using string literals
Applied to files:
src/synthorg/notifications/adapters/email.pysrc/synthorg/observability/events/notification.py
📚 Learning: 2026-03-31T16:09:24.320Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T16:09:24.320Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from `synthorg.observability.events.<domain>` modules (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`); import directly and use in structured logging
Applied to files:
src/synthorg/notifications/adapters/email.pysrc/synthorg/observability/events/notification.py
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`). Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`.
Applied to files:
src/synthorg/notifications/adapters/email.pysrc/synthorg/observability/events/notification.py
📚 Learning: 2026-03-16T06:24:56.341Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T06:24:56.341Z
Learning: Applies to src/synthorg/**/*.py : Always use event name constants from the domain-specific module under `synthorg.observability.events` in logging calls
Applied to files:
src/synthorg/notifications/adapters/email.py
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to src/synthorg/**/*.py : Event names: always use constants from domain-specific modules under synthorg.observability.events (e.g., PROVIDER_CALL_START from events.provider, BUDGET_RECORD_ADDED from events.budget, etc.). Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`.
Applied to files:
src/synthorg/notifications/adapters/email.pysrc/synthorg/observability/events/notification.py
📚 Learning: 2026-04-06T06:43:24.031Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T06:43:24.031Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from synthorg.observability.events domain modules (e.g., API_REQUEST_STARTED from events.api, TOOL_INVOKE_START from events.tool). Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`
Applied to files:
src/synthorg/notifications/adapters/email.pysrc/synthorg/observability/events/notification.py
📚 Learning: 2026-04-02T07:18:02.381Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T07:18:02.381Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`); import directly from the domain module
Applied to files:
src/synthorg/notifications/adapters/email.pysrc/synthorg/observability/events/notification.py
📚 Learning: 2026-03-19T11:33:01.580Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T11:33:01.580Z
Learning: Applies to src/synthorg/**/*.py : Use event constants from `synthorg.observability.events.<domain>` (e.g., `API_REQUEST_STARTED` from `events.api`); import directly and log with structured kwargs: `logger.info(EVENT, key=value)`, never interpolated strings
Applied to files:
src/synthorg/notifications/adapters/email.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from synthorg.observability.events domain-specific modules (e.g., PROVIDER_CALL_START from events.provider). Import directly: from synthorg.observability.events.<domain> import EVENT_CONSTANT.
Applied to files:
src/synthorg/observability/events/notification.py
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Export component props as a TypeScript interface for all new components
Applied to files:
web/src/types/notifications.ts
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/tsconfig.json : In TypeScript 6, explicitly list needed types in the `types` array (e.g. `"types": ["vitest/globals"]`) instead of relying on auto-discovery of `types/*`
Applied to files:
web/src/types/notifications.ts
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/**/*.{ts,tsx} : CSS side-effect imports in TypeScript 6 require type declarations -- add `/// <reference types="vite/client" />` at the top of files with CSS imports
Applied to files:
web/src/types/notifications.ts
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : New shared components must be placed in `web/src/components/ui/` with descriptive kebab-case filenames
Applied to files:
web/src/types/notifications.ts
📚 Learning: 2026-03-31T14:31:11.894Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard
Applied to files:
web/src/types/notifications.ts
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components
Applied to files:
web/src/types/notifications.ts
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (via `model_copy(update=...)`) for runtime state that evolves
Applied to files:
src/synthorg/notifications/models.py
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; separate mutable-via-copy models (using `model_copy(update=...)`) for runtime state
Applied to files:
src/synthorg/notifications/models.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Applied to files:
src/synthorg/notifications/models.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`. For derived values use `computed_field` instead of storing + validating redundant fields. Use `NotBlankStr` (from `core.types`) for all identifier/name fields — including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants — instead of manual whitespace validators.
Applied to files:
src/synthorg/notifications/models.py
📚 Learning: 2026-03-15T18:42:17.990Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:42:17.990Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`
Applied to files:
src/synthorg/notifications/models.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 BaseModel, model_validator, computed_field, ConfigDict.
Applied to files:
src/synthorg/notifications/models.py
📚 Learning: 2026-03-16T06:24:56.341Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T06:24:56.341Z
Learning: Applies to src/synthorg/core/**/*.py : Core module must contain shared domain models, base classes, resilience config (RetryConfig, RateLimiterConfig)
Applied to files:
src/synthorg/notifications/models.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)
Applied to files:
src/synthorg/notifications/models.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Applied to files:
src/synthorg/notifications/models.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 with adopted conventions: use computed_field for derived values instead of storing + validating redundant fields; use NotBlankStr from core.types for all identifier/name fields (including optional and tuple variants) instead of manual whitespace validators.
Applied to files:
src/synthorg/notifications/models.py
📚 Learning: 2026-04-01T09:58:27.410Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:58:27.410Z
Learning: Applies to **/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for dict/list fields in frozen Pydantic models
Applied to files:
src/synthorg/notifications/models.py
📚 Learning: 2026-03-16T23:05:29.577Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T23:05:29.577Z
Learning: Applies to **/*.py : For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Applied to files:
src/synthorg/notifications/models.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for `dict`/`list` fields
Applied to files:
src/synthorg/notifications/models.py
📚 Learning: 2026-04-06T06:43:24.031Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T06:43:24.031Z
Learning: Use frozen Pydantic models for config/identity. Use separate mutable-via-copy models for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Applied to files:
src/synthorg/notifications/models.py
📚 Learning: 2026-04-01T09:37:49.451Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:37:49.451Z
Learning: Applies to **/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models with `model_copy(update=...)` for runtime state that evolves
Applied to files:
src/synthorg/notifications/models.py
📚 Learning: 2026-03-20T08:28:32.845Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T08:28:32.845Z
Learning: Applies to **/*.py : Config vs runtime state: use frozen Pydantic models for config/identity; separate mutable-via-copy models (using `model_copy(update=...)`) for runtime state. Never mix static config fields with mutable runtime fields in one model.
Applied to files:
src/synthorg/notifications/models.py
🔇 Additional comments (6)
web/src/components/notifications/NotificationFilterBar.tsx (1)
1-25: LGTM!Clean implementation that correctly derives filter groups from
CATEGORY_CONFIGS, addressing the previous concern about drift. The component properly reuses the existingSegmentedControlfrom@/components/ui/and follows TypeScript best practices with readonly props.web/src/components/notifications/NotificationDrawer.tsx (1)
1-115: LGTM!The implementation properly addresses previous review concerns:
- Bulk actions (
handleMarkAllRead,handleClearAll) are now correctly scoped to the filtered view- Uses batch methods (
markReadBatch,dismissBatch) for efficient single-update operations- Unread count is derived from filtered items
- Reuses existing UI components (
Drawer,Button,LiveRegion,StaggerGroup)web/src/services/browser-notifications.ts (1)
1-99: LGTM!The browser notifications service is well-structured:
- Rate limiting (3 per 10s) prevents notification spam
- Visibility check avoids duplicate alerts when the user is already viewing the dashboard
- Uses
createLoggercorrectly per coding guidelines- Previous concerns addressed: icon path fixed to
/favicon.svg, rate limit check simplified with non-null assertionweb/src/types/notifications.ts (1)
1-223: LGTM!Comprehensive notification type definitions with well-structured routing configuration. The previous concern about
ToastVariantduplication has been addressed by importing and re-exporting from@/stores/toast(lines 11, 185). TheCATEGORY_CONFIGSmapping provides clear default routing for all 17 notification categories.web/src/stores/notifications.ts (2)
1-500: Well-structured notifications store with proper logging hygiene.The implementation follows coding guidelines correctly:
- Uses
createLogger('notifications-store')withconst log- Uses
sanitizeForLog()for untrusted payload data (lines 195, 377)- Proper Zustand patterns with
create()- Comprehensive hydration, validation, deduplication, and persistence logic
371-498: The unhandled WS events are not silently ignored—they're intentionally routed to activity feeds instead of notifications.The events listed (
task.created,task.updated,task.assigned,agent.status_changed,budget.record_added,message.sent,system.startup,meeting.started,meeting.completed) are processed bywsEventToActivityItem()indashboard.tsand fed into activity streams via the budget and analytics stores. This represents a deliberate separation of concerns:handleWsEvent()routes user-facing alerts (notifications), whilewsEventToActivityItem()routes all events to activity feeds for tracking and historical records. The design is correct—not all WS events require user notifications.
| await asyncio.to_thread(self._send_sync, notification) | ||
| logger.info( | ||
| NOTIFICATION_EMAIL_DELIVERED, | ||
| notification_id=notification.id, | ||
| to_count=len(self._to_addrs), | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Python's smtplib, what does SMTP.send_message() return when some recipients are refused, and does it only raise SMTPRecipientsRefused when all recipients are refused?
💡 Result:
- If at least one recipient is accepted,
SMTP.send_message()returns adictcontaining only the refused recipients (each value is a(smtp_code, error_message)tuple). If nobody is refused, this dict is empty. (docs.python.org) SMTPRecipientsRefusedis raised only when the server rejected all recipients (so “nobody got the mail”); in that case, the exception’s.recipientsattribute is the same kind of dict described above. (docs.python.org)
Citations:
- 1: https://docs.python.org/id/3/library/smtplib.html?utm_source=openai
- 2: https://docs.python.org/3.15/library/smtplib.html?utm_source=openai
Treat partial recipient refusal as a send failure.
The SMTP.send_message() method returns a dict of refused recipients when at least one recipient is accepted; the current code ignores this return value, so partial recipient rejections are silently logged as full delivery on lines 84–88. Check the refusal dict and raise SMTPRecipientsRefused if any recipients were refused.
📫 Minimal fix
with smtplib.SMTP(self._host, self._port, timeout=10) as smtp:
if self._use_tls:
context = ssl.create_default_context()
smtp.starttls(context=context)
self._login_if_configured(smtp)
- smtp.send_message(msg)
+ refused = smtp.send_message(msg)
+ if refused:
+ raise smtplib.SMTPRecipientsRefused(refused)Also applies to: 116–121
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/notifications/adapters/email.py` around lines 83 - 88, The send
path currently ignores SMTP.send_message()'s return value in _send_sync, so
partial recipient refusals are treated as full success; update _send_sync (the
method called via asyncio.to_thread) to capture the return value from
smtp.send_message(message) and if the returned dict is non-empty raise
smtplib.SMTPRecipientsRefused with that dict, so the outer await
asyncio.to_thread(self._send_sync, notification) will surface failures; also
apply the same check-and-raise change to the other send site handled similarly
(the second call around NOTIFICATION_EMAIL_DELIVERED in the alternate send flow)
so both code paths log NOTIFICATION_EMAIL_DELIVERED only when no recipients were
refused.
| import copy | ||
| from datetime import UTC, datetime | ||
| from enum import StrEnum | ||
| from uuid import uuid4 | ||
|
|
||
| from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, model_validator | ||
|
|
||
| from synthorg.core.types import NotBlankStr # noqa: TC001 |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Add the standard module logger.
This new src/synthorg/**/*.py business-logic module is missing the required get_logger / logger = get_logger(__name__) declaration.
♻️ Minimal fix
import copy
from datetime import UTC, datetime
from enum import StrEnum
from uuid import uuid4
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, model_validator
from synthorg.core.types import NotBlankStr # noqa: TC001
+from synthorg.observability import get_logger
+
+logger = get_logger(__name__)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import copy | |
| from datetime import UTC, datetime | |
| from enum import StrEnum | |
| from uuid import uuid4 | |
| from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, model_validator | |
| from synthorg.core.types import NotBlankStr # noqa: TC001 | |
| import copy | |
| from datetime import UTC, datetime | |
| from enum import StrEnum | |
| from uuid import uuid4 | |
| from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, model_validator | |
| from synthorg.core.types import NotBlankStr # noqa: TC001 | |
| from synthorg.observability import get_logger | |
| logger = get_logger(__name__) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/notifications/models.py` around lines 11 - 18, This module is
missing the standard module logger: import get_logger from
synthorg.observability and declare a module-level logger variable; add "from
synthorg.observability import get_logger" and then "logger =
get_logger(__name__)" near the top of the file (alongside existing imports) so
that the module-level logger is available for functions/classes in models.py.
277b122 to
0b13c6d
Compare
There was a problem hiding this comment.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/synthorg/api/app.py (1)
1086-1109:⚠️ Potential issue | 🟠 MajorAlways union the fail-safe auth exclusions.
Line 1093 turns
auth.exclude_pathsinto a full replacement. A custom list can then sendauth/loginorauth/setupthrough the auth middleware and break first-run/login flows. Merge operator-provided excludes with the built-in unauthenticated routes instead of replacing them.Patch
def _build_auth_exclude_paths( auth: AuthConfig, prefix: str, ws_path: str, ) -> tuple[str, ...]: """Compute auth middleware exclude paths with fail-safe defaults.""" setup_status_path = f"^{prefix}/setup/status$" - exclude_paths = ( - auth.exclude_paths - if auth.exclude_paths is not None - else ( - f"^{prefix}/health$", - "^/docs", - "^/api$", - f"^{prefix}/auth/setup$", - f"^{prefix}/auth/login$", - setup_status_path, - ) - ) - if setup_status_path not in exclude_paths: - exclude_paths = (*exclude_paths, setup_status_path) - if ws_path not in exclude_paths: - exclude_paths = (*exclude_paths, ws_path) - return exclude_paths + base_excludes = ( + f"^{prefix}/health$", + "^/docs", + "^/api$", + f"^{prefix}/auth/setup$", + f"^{prefix}/auth/login$", + setup_status_path, + ws_path, + ) + configured = auth.exclude_paths or () + return tuple(dict.fromkeys((*base_excludes, *configured)))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/api/app.py` around lines 1086 - 1109, _build_auth_exclude_paths currently replaces the built-in unauthenticated routes when auth.exclude_paths is set, which can break setup/login flows; change it so that if auth.exclude_paths is provided you merge (union) the caller-provided tuple/list with the built-in defaults (the entries like f"^{prefix}/health$", "^/docs", "^/api$", f"^{prefix}/auth/setup$", f"^{prefix}/auth/login$", and the computed setup_status_path) rather than replacing them, and then ensure you still append setup_status_path and ws_path if missing; update references in this function (_build_auth_exclude_paths, auth.exclude_paths, prefix, ws_path) to perform a deduplicated union while preserving tuple return type.
♻️ Duplicate comments (5)
tests/unit/api/test_app.py (1)
1173-1184:⚠️ Potential issue | 🟡 MinorAuth-layer detection in sandwich test is too generic.
Using
isinstance(entry, type)can misidentify non-auth class middleware as the auth layer, so this assertion can false-pass.🔧 Suggested tightening
- for i, entry in enumerate(mw): - if hasattr(entry, "kwargs"): - cfg = entry.kwargs.get("config") - if isinstance(cfg, LsRL): - if cfg.store and "unauth" in cfg.store: - unauth_idx = i - elif cfg.store and "auth" in cfg.store: - auth_rl_idx = i - elif isinstance(entry, type): - # Auth middleware is a class, not a DefineMiddleware - auth_mw_idx = i + for i, entry in enumerate(mw): + cfg = getattr(entry, "kwargs", {}).get("config") + if isinstance(cfg, LsRL): + if cfg.store and "unauth" in cfg.store: + unauth_idx = i + elif cfg.store and "auth" in cfg.store: + auth_rl_idx = i + continue + + middleware_obj = getattr(entry, "middleware", entry) + name = getattr(middleware_obj, "__name__", "").lower() + module = getattr(middleware_obj, "__module__", "").lower() + if "auth" in name or ".auth" in module: + auth_mw_idx = iAlso applies to: 1189-1189
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/api/test_app.py` around lines 1173 - 1184, The auth-layer detection using isinstance(entry, type) is too broad and can misidentify unrelated classes; tighten it by explicitly checking for the known auth middleware class or interface (e.g., replace isinstance(entry, type) with issubclass(entry, AuthMiddleware) or a direct name check like entry.__name__ == "AuthMiddleware"), and import/reference AuthMiddleware in the test; update the branch that sets auth_mw_idx to only trigger when the entry truly represents the auth middleware (or has a unique identifying attribute/method such as "authenticate") instead of any class type.web/src/components/notifications/NotificationDrawer.stories.tsx (1)
40-63:⚠️ Potential issue | 🟡 MinorReset the notifications store before and after seeding.
This decorator appends into a global store on every mount, so
WithItemscan accumulate duplicate notifications and leak state into other stories.🧹 Suggested fix
function SeedNotifications({ children }: { readonly children: React.ReactNode }) { useEffect(() => { - const { enqueue } = useNotificationsStore.getState() + const { clearAll, enqueue } = useNotificationsStore.getState() + clearAll() enqueue({ category: 'approvals.pending', title: 'Approval requested for agent deployment', description: 'Engineering department needs approval', href: '/approvals', @@ enqueue({ category: 'agents.hired', title: 'Agent hired: Marketing Writer', }) + return () => { + useNotificationsStore.getState().clearAll() + } }, []) return <>{children}</> }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/components/notifications/NotificationDrawer.stories.tsx` around lines 40 - 63, The decorator is repeatedly appending to the global notifications store causing duplicates; before seeding notifications in the useEffect, call useNotificationsStore.getState().reset() (or clear method on the store) to wipe existing items, then enqueue the seeded notifications (categories: 'approvals.pending','budget.threshold','tasks.failed','agents.hired'), and finally ensure you clean up after the story by calling reset/clear in a cleanup effect or return function so WithItems does not leak state into other stories; reference useNotificationsStore.getState(), enqueue, and the WithItems decorator/component when locating where to add the reset/cleanup.tests/unit/notifications/test_models.py (1)
17-32: 🧹 Nitpick | 🔵 TrivialParametrize the enum value table tests.
These are hand-unrolled table cases, so every taxonomy change forces multiple assertion edits.
As per coding guidelines, "Prefer `@pytest.mark.parametrize` for testing similar cases."♻️ Suggested refactor
`@pytest.mark.unit` class TestNotificationCategory: - def test_values(self) -> None: - assert NotificationCategory.APPROVAL.value == "approval" - assert NotificationCategory.BUDGET.value == "budget" - assert NotificationCategory.SECURITY.value == "security" - assert NotificationCategory.STAGNATION.value == "stagnation" - assert NotificationCategory.SYSTEM.value == "system" - assert NotificationCategory.AGENT.value == "agent" + `@pytest.mark.parametrize`( + ("category", "expected"), + [ + (NotificationCategory.APPROVAL, "approval"), + (NotificationCategory.BUDGET, "budget"), + (NotificationCategory.SECURITY, "security"), + (NotificationCategory.STAGNATION, "stagnation"), + (NotificationCategory.SYSTEM, "system"), + (NotificationCategory.AGENT, "agent"), + ], + ) + def test_values( + self, + category: NotificationCategory, + expected: str, + ) -> None: + assert category.value == expected `@pytest.mark.unit` class TestNotificationSeverity: - def test_values(self) -> None: - assert NotificationSeverity.INFO.value == "info" - assert NotificationSeverity.WARNING.value == "warning" - assert NotificationSeverity.ERROR.value == "error" - assert NotificationSeverity.CRITICAL.value == "critical" + `@pytest.mark.parametrize`( + ("severity", "expected"), + [ + (NotificationSeverity.INFO, "info"), + (NotificationSeverity.WARNING, "warning"), + (NotificationSeverity.ERROR, "error"), + (NotificationSeverity.CRITICAL, "critical"), + ], + ) + def test_values( + self, + severity: NotificationSeverity, + expected: str, + ) -> None: + assert severity.value == expected🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/notifications/test_models.py` around lines 17 - 32, Refactor the two hand-unrolled test methods to use pytest parameterization: replace TestNotificationCategory.test_values and TestNotificationSeverity.test_values assertions with `@pytest.mark.parametrize-based` tests that iterate over tuples of (enum_member, expected_value) for NotificationCategory and NotificationSeverity respectively; update test names if desired (e.g., test_category_values, test_severity_values) and ensure you reference the enum members (NotificationCategory.APPROVAL, etc., and NotificationSeverity.INFO, etc.) in the param lists so adding/removing enum members only requires changing the param table.src/synthorg/notifications/config.py (1)
31-41:⚠️ Potential issue | 🟠 Major
params: dict[str, str]still rejects normal YAML sink values.Email and ntfy configs naturally use native YAML scalars like
port: 587oruse_tls: true. This shape rejects them before the factory can parse anything, so otherwise valid notification config becomes quote-everything fragile. Prefer typed per-sink config models, or at least widenparamsto accept YAML-native scalar types. As per coding guidelines "Validate at system boundaries (user input, external APIs, config files)."In Pydantic v2.12.x, will a field typed as `dict[str, str]` accept YAML values like `{"port": 587, "use_tls": true}` by default?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/notifications/config.py` around lines 31 - 41, The params field on the notification sink model is typed as dict[str, str], which rejects native YAML scalar types (e.g., integers and booleans) before any adapter parsing; change params to accept broader YAML-native types (e.g., dict[str, Any] with typing.Any imported) or replace with per-sink typed models, and update the Field declaration for params in the same class (referencing the params attribute and the model that uses NotificationSinkType) so configurations like port: 587 and use_tls: true are accepted at parsing time.src/synthorg/notifications/adapters/email.py (1)
83-88:⚠️ Potential issue | 🟠 MajorDon't report partial SMTP delivery as success.
Line 121 ignores
smtplib.SMTP.send_message()'s return value. That API returns a refusal map when only some recipients are rejected, so Lines 84-88 currently emitNOTIFICATION_EMAIL_DELIVEREDeven though part of the delivery failed.♻️ Minimal fix
- smtp.send_message(msg) + refused = smtp.send_message(msg) + if refused: + raise smtplib.SMTPRecipientsRefused(refused)Verification: inspect the stdlib implementation;
send_message()forwardssendmail()'s refusal dict.#!/bin/bash python - <<'PY' import inspect, smtplib print(inspect.getsource(smtplib.SMTP.send_message)) print("-----") print(inspect.getsource(smtplib.SMTP.sendmail)) PYAs per coding guidelines, "Handle errors explicitly, never silently swallow exceptions."
Also applies to: 116-121
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/notifications/adapters/email.py` around lines 83 - 88, The code treats any call to self._send_sync (which calls smtplib.SMTP.send_message) as a full success and immediately emits NOTIFICATION_EMAIL_DELIVERED even if send_message returned a refusal map; change _send_sync caller to capture the return value from send_message, detect a non-empty refusal dict (partial failures), and in that case do not emit NOTIFICATION_EMAIL_DELIVERED—instead log an error with the refusal details (include notification.id and self._to_addrs), raise or return an error outcome so the caller can retry/report partial delivery, and only call logger.info(NOTIFICATION_EMAIL_DELIVERED, ...) when the refusal map is empty (all recipients accepted). Ensure references: _send_sync, smtplib.SMTP.send_message/sendmail, NOTIFICATION_EMAIL_DELIVERED, _to_addrs, and logger.info are updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/security.md`:
- Around line 77-84: The doc incorrectly claims the unauthenticated limiter
protects "health endpoints" while later stating /api/v1/health is excluded by
default; update the paragraph to either remove "health endpoints" from the
unauthenticated protection claim or explicitly mark health protection as opt-in
and reference the config key rate_limit.exclude_paths (and the path
"/api/v1/health"); ensure the text still mentions the relevant config keys
api.rate_limit.unauth_max_requests and api.rate_limit.auth_max_requests and the
WebSocket exclusion to keep behavior clear.
In `@src/synthorg/api/config.py`:
- Around line 121-131: In _reject_legacy_max_requests (the classmethod
model_validator) add a structured log call at WARNING or ERROR level before
raising the ValueError so deprecated configs are visible in diagnostics: import
and use the appropriate event constant from synthorg.observability.events (use
the domain-specific constant name, e.g., LEGACY_CONFIG_DETECTED or the exact
event constant defined there) and include context such as the offending key
"max_requests" and the constructed msg; then raise the ValueError as before.
Ensure the log call uses the project logging/observability API consistent with
other modules (same logger or observability wrapper) and is placed immediately
before the raise in _reject_legacy_max_requests.
In `@src/synthorg/budget/enforcer.py`:
- Around line 394-400: The code currently creates a background task for every
blocked request by calling self._notify_budget_event("Monthly budget exhausted",
...), causing unbounded fan-out under retries; modify the logic in the
budget-enforcement path(s) that call self._notify_budget_event (also update the
sibling branch that handles the daily limit at the 429-435 area) to perform an
atomic dedupe/check before scheduling: add a short-lived dedupe key (per billing
window or per agent-day) in a shared store or in-process cache and only schedule
asyncio.create_task(self._notify_budget_event(...)) when the key did not already
exist (i.e., transition to exhausted); ensure the dedupe key includes the
billing window/agent identifier and expires at window end so subsequent distinct
windows can emit again.
In `@src/synthorg/notifications/adapters/ntfy.py`:
- Around line 43-49: The current SSRF guard in ntfy.py only checks literal IPs
via ipaddress.ip_address(host) and treats all non-literal hostnames as safe;
update the validation so that after parsing server_url and deriving host (the
existing host variable), you resolve DNS A/AAAA records (e.g., via
socket.getaddrinfo or a DNS resolver) and validate every returned address (the
addr/addrinfo results) to ensure none are private, link-local, loopback, or
otherwise non-global before allowing the URL; if any resolved address is
non-global, reject the server_url (or require the host to be explicitly present
in an allowlist like _BLOCKED_HOSTS inverted to an _ALLOWED_NTFY_HOSTS). Apply
this change around the current try/except block that uses
ipaddress.ip_address(host) and the subsequent
addr.is_private/is_link_local/is_loopback checks so both literal IPs and
DNS-resolved addresses are validated.
In `@src/synthorg/notifications/adapters/slack.py`:
- Around line 91-96: The current except block logs the full exception string
which can leak the Slack webhook URL; change the handler in the except Exception
as exc block to avoid serializing raw httpx exceptions (used with
response.raise_for_status) by logging only sanitized fields: set
error_type=type(exc).__name__ and if exc is an httpx.HTTPStatusError include
status_code=exc.response.status_code (or None otherwise), and remove any use of
str(exc) in the logger.warning call for NOTIFICATION_SLACK_FAILED (keep
notification_id=notification.id).
In `@src/synthorg/notifications/dispatcher.py`:
- Around line 113-123: The broad except in the sink shutdown loop is catching
system errors; update the exception handling around await sink.close() in the
loop over self._sinks so that MemoryError and RecursionError are re-raised (use
an explicit except (MemoryError, RecursionError): raise) before the generic
except Exception that logs via logger.warning with NOTIFICATION_DISPATCH_FAILED
(referencing sink.sink_name and exc_info=True) to preserve system-error
propagation the same way send()/_guarded_send() do.
In `@src/synthorg/settings/definitions/api.py`:
- Around line 4-7: The docstring's runtime/bootstrap split is inaccurate because
rate_limit_time_unit is runtime-editable but actually baked into middleware at
app construction; update the settings definition for rate_limit_time_unit to set
restart_required=True and adjust the surrounding docstring text to reflect that
it is bootstrap-only (restart-gated) alongside other rate-limit settings so the
documentation and the setting flag (rate_limit_time_unit) remain consistent.
In `@tests/unit/notifications/test_dispatcher.py`:
- Around line 119-130: Update the test_memory_error_propagates to assert that
RecursionError is also propagated: in the async test where _MemSink.send raises
MemoryError, modify the pytest.raises context to expect both MemoryError and
RecursionError (i.e., pytest.raises((MemoryError, RecursionError))) so the
NotificationDispatcher propagation contract (tested via NotificationDispatcher,
_MemSink, and _make_notification) covers both fatal system errors.
In `@web/src/components/layout/AppLayout.tsx`:
- Around line 92-99: The handler handleKeyDown currently treats any event with
e.shiftKey and e.key === 'N' as the shortcut, which also triggers when
Ctrl/Alt/Meta are pressed; update the condition inside handleKeyDown to require
Shift+N only by additionally checking that e.ctrlKey, e.metaKey, and e.altKey
are all false (e.g., require e.shiftKey && e.key === 'N' && !e.ctrlKey &&
!e.metaKey && !e.altKey) and only then call e.preventDefault() and dispatch the
'toggle-notification-drawer' CustomEvent.
In `@web/src/components/notifications/NotificationItemCard.tsx`:
- Around line 62-74: In NotificationItemCard, change the outer interactive
element from a <button> to a non-interactive container (e.g., <li> or <div>) so
we no longer nest buttons; move the onClick handler (handleClick) and the
primary navigation/mark-as-read behavior onto a single dedicated button or link
inside that container (the primary click target), remove any other nested button
elements or convert them to non-button controls, and ensure you use the existing
BORDER_COLORS and item.read logic for styling on the container; also update the
action cluster’s reveal classes to include keyboard focus by adding
group-focus-within:opacity-100 (in addition to group-hover:opacity-100) so the
mark-read/dismiss controls become visible to keyboard users and verify
tabindex/keyboard handlers (Enter/Escape) work for the primary button.
In `@web/src/pages/settings/NotificationsSection.stories.tsx`:
- Around line 1-13: Add additional Storybook stories for NotificationsSection to
showcase different states: create named exports such as SomeTogglesEnabled,
SomeTogglesDisabled, PermissionGranted, PermissionDenied, and PermissionPrompt
(in addition to Default) that pass different props or initial state into
NotificationsSection to toggle feature flags; for browser permission variations,
mock or set window.Notification.permission (or use a decorator/parameter to stub
Notification.requestPermission) inside each story so one story shows "granted",
one "denied", and one "default/prompt", and ensure story names reference the
NotificationsSection component and existing Default story for discoverability.
In `@web/src/services/browser-notifications.ts`:
- Around line 76-97: recordNotification() is being called before constructing
the browser Notification, so failures in new Notification(...) consume a
rate-limit slot; move the recordNotification() call to after the Notification is
successfully created and its onclick handler is set (i.e., call
recordNotification() after new Notification(...) and after assigning
notification.onclick), and keep the catch branch unchanged so that failed
constructions do not call recordNotification(); use the existing symbols
recordNotification(), new Notification(...), notification.onclick, and log.warn
to locate the change.
In `@web/src/stores/notifications.ts`:
- Around line 246-252: enqueue() currently always prepends the new notification
into state.items (affecting unreadCount) which causes toast-only/browser-only
categories to appear in the drawer; change enqueue() to first check whether the
drawer should persist this category (e.g., consult the route/drawer state or the
category's "drawerEnabled" flag) and only add the item to state.items when the
drawer is active/allowed; otherwise show the toast/browser notification without
mutating state.items. Keep the MAX_ITEMS/truncation and unreadCount calculation
(countUnread(newItems)) for the branch that does add to state.items.
- Around line 143-149: The persisted preferences loaded in hydratePrefs() trust
parsed.routeOverrides and parsed.browserPermission without validation, which
allows invalid values (e.g., null or malformed objects) to leak into the merged
preferences and later break computeRoutes(); update hydratePrefs to validate and
sanitize the parsed object before merging: after JSON.parse, check that
parsed.routeOverrides (if present) is a plain object mapping string->string and
filter/omit any non-string keys or non-string values, and check that
parsed.browserPermission (if present) is one of the allowed permission values
(or otherwise ignore it); only merge the sanitized subsets into
DEFAULT_PREFERENCES so routeOverrides and browserPermission cannot be null or
malformed when returned.
---
Outside diff comments:
In `@src/synthorg/api/app.py`:
- Around line 1086-1109: _build_auth_exclude_paths currently replaces the
built-in unauthenticated routes when auth.exclude_paths is set, which can break
setup/login flows; change it so that if auth.exclude_paths is provided you merge
(union) the caller-provided tuple/list with the built-in defaults (the entries
like f"^{prefix}/health$", "^/docs", "^/api$", f"^{prefix}/auth/setup$",
f"^{prefix}/auth/login$", and the computed setup_status_path) rather than
replacing them, and then ensure you still append setup_status_path and ws_path
if missing; update references in this function (_build_auth_exclude_paths,
auth.exclude_paths, prefix, ws_path) to perform a deduplicated union while
preserving tuple return type.
---
Duplicate comments:
In `@src/synthorg/notifications/adapters/email.py`:
- Around line 83-88: The code treats any call to self._send_sync (which calls
smtplib.SMTP.send_message) as a full success and immediately emits
NOTIFICATION_EMAIL_DELIVERED even if send_message returned a refusal map; change
_send_sync caller to capture the return value from send_message, detect a
non-empty refusal dict (partial failures), and in that case do not emit
NOTIFICATION_EMAIL_DELIVERED—instead log an error with the refusal details
(include notification.id and self._to_addrs), raise or return an error outcome
so the caller can retry/report partial delivery, and only call
logger.info(NOTIFICATION_EMAIL_DELIVERED, ...) when the refusal map is empty
(all recipients accepted). Ensure references: _send_sync,
smtplib.SMTP.send_message/sendmail, NOTIFICATION_EMAIL_DELIVERED, _to_addrs, and
logger.info are updated accordingly.
In `@src/synthorg/notifications/config.py`:
- Around line 31-41: The params field on the notification sink model is typed as
dict[str, str], which rejects native YAML scalar types (e.g., integers and
booleans) before any adapter parsing; change params to accept broader
YAML-native types (e.g., dict[str, Any] with typing.Any imported) or replace
with per-sink typed models, and update the Field declaration for params in the
same class (referencing the params attribute and the model that uses
NotificationSinkType) so configurations like port: 587 and use_tls: true are
accepted at parsing time.
In `@tests/unit/api/test_app.py`:
- Around line 1173-1184: The auth-layer detection using isinstance(entry, type)
is too broad and can misidentify unrelated classes; tighten it by explicitly
checking for the known auth middleware class or interface (e.g., replace
isinstance(entry, type) with issubclass(entry, AuthMiddleware) or a direct name
check like entry.__name__ == "AuthMiddleware"), and import/reference
AuthMiddleware in the test; update the branch that sets auth_mw_idx to only
trigger when the entry truly represents the auth middleware (or has a unique
identifying attribute/method such as "authenticate") instead of any class type.
In `@tests/unit/notifications/test_models.py`:
- Around line 17-32: Refactor the two hand-unrolled test methods to use pytest
parameterization: replace TestNotificationCategory.test_values and
TestNotificationSeverity.test_values assertions with
`@pytest.mark.parametrize-based` tests that iterate over tuples of (enum_member,
expected_value) for NotificationCategory and NotificationSeverity respectively;
update test names if desired (e.g., test_category_values, test_severity_values)
and ensure you reference the enum members (NotificationCategory.APPROVAL, etc.,
and NotificationSeverity.INFO, etc.) in the param lists so adding/removing enum
members only requires changing the param table.
In `@web/src/components/notifications/NotificationDrawer.stories.tsx`:
- Around line 40-63: The decorator is repeatedly appending to the global
notifications store causing duplicates; before seeding notifications in the
useEffect, call useNotificationsStore.getState().reset() (or clear method on the
store) to wipe existing items, then enqueue the seeded notifications
(categories:
'approvals.pending','budget.threshold','tasks.failed','agents.hired'), and
finally ensure you clean up after the story by calling reset/clear in a cleanup
effect or return function so WithItems does not leak state into other stories;
reference useNotificationsStore.getState(), enqueue, and the WithItems
decorator/component when locating where to add the reset/cleanup.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 2753ddb0-4ac5-4da4-94bc-17013628af9e
📒 Files selected for processing (63)
CLAUDE.mdREADME.mddocs/design/operations.mddocs/design/page-structure.mddocs/security.mdsrc/synthorg/api/app.pysrc/synthorg/api/config.pysrc/synthorg/api/state.pysrc/synthorg/budget/enforcer.pysrc/synthorg/config/defaults.pysrc/synthorg/config/schema.pysrc/synthorg/engine/approval_gate.pysrc/synthorg/notifications/__init__.pysrc/synthorg/notifications/adapters/__init__.pysrc/synthorg/notifications/adapters/console.pysrc/synthorg/notifications/adapters/email.pysrc/synthorg/notifications/adapters/ntfy.pysrc/synthorg/notifications/adapters/slack.pysrc/synthorg/notifications/config.pysrc/synthorg/notifications/dispatcher.pysrc/synthorg/notifications/factory.pysrc/synthorg/notifications/models.pysrc/synthorg/notifications/protocol.pysrc/synthorg/observability/events/approval_gate.pysrc/synthorg/observability/events/budget.pysrc/synthorg/observability/events/notification.pysrc/synthorg/security/timeout/scheduler.pysrc/synthorg/settings/definitions/api.pysrc/synthorg/settings/resolver.pytests/integration/settings/test_settings_integration.pytests/unit/api/conftest.pytests/unit/api/controllers/test_ws.pytests/unit/api/test_app.pytests/unit/api/test_config.pytests/unit/notifications/__init__.pytests/unit/notifications/test_config.pytests/unit/notifications/test_console_adapter.pytests/unit/notifications/test_dispatcher.pytests/unit/notifications/test_models.pytests/unit/notifications/test_ntfy_adapter.pytests/unit/notifications/test_protocol.pytests/unit/observability/test_events.pytests/unit/settings/test_resolver.pyweb/src/__tests__/pages/SettingsPage.test.tsxweb/src/__tests__/stores/agents.test.tsweb/src/components/layout/AppLayout.tsxweb/src/components/layout/Sidebar.tsxweb/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationDrawer.tsxweb/src/components/notifications/NotificationEmptyState.stories.tsxweb/src/components/notifications/NotificationEmptyState.tsxweb/src/components/notifications/NotificationFilterBar.stories.tsxweb/src/components/notifications/NotificationFilterBar.tsxweb/src/components/notifications/NotificationItemCard.stories.tsxweb/src/components/notifications/NotificationItemCard.tsxweb/src/hooks/useGlobalNotifications.tsweb/src/pages/SettingsPage.tsxweb/src/pages/settings/NotificationsSection.stories.tsxweb/src/pages/settings/NotificationsSection.tsxweb/src/services/browser-notifications.tsweb/src/stores/agents.tsweb/src/stores/notifications.tsweb/src/types/notifications.ts
| @model_validator(mode="before") | ||
| @classmethod | ||
| def _reject_legacy_max_requests(cls, data: Any) -> Any: | ||
| """Reject the removed ``max_requests`` field with guidance.""" | ||
| if isinstance(data, dict) and "max_requests" in data: | ||
| msg = ( | ||
| "'max_requests' was replaced by 'unauth_max_requests' " | ||
| "and 'auth_max_requests' in v0.6.3" | ||
| ) | ||
| raise ValueError(msg) | ||
| return data |
There was a problem hiding this comment.
Log the legacy-key rejection before raising.
This adds a new startup error path, but it currently emits only a validation exception. Please log a structured WARNING/ERROR here first so deprecated configs show up in normal diagnostics.
As per coding guidelines, "All error paths must log at WARNING or ERROR with context before raising" and "Event names must always use constants from domain-specific modules under synthorg.observability.events."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/api/config.py` around lines 121 - 131, In
_reject_legacy_max_requests (the classmethod model_validator) add a structured
log call at WARNING or ERROR level before raising the ValueError so deprecated
configs are visible in diagnostics: import and use the appropriate event
constant from synthorg.observability.events (use the domain-specific constant
name, e.g., LEGACY_CONFIG_DETECTED or the exact event constant defined there)
and include context such as the offending key "max_requests" and the constructed
msg; then raise the ValueError as before. Ensure the log call uses the project
logging/observability API consistent with other modules (same logger or
observability wrapper) and is placed immediately before the raise in
_reject_legacy_max_requests.
| asyncio.create_task( # noqa: RUF006 | ||
| self._notify_budget_event( | ||
| "Monthly budget exhausted", | ||
| msg, | ||
| "critical", | ||
| ), | ||
| ) |
There was a problem hiding this comment.
Coalesce exhausted-budget alerts.
Once the monthly or daily limit trips, these branches enqueue a notification task on every blocked request. Under retry traffic that turns a single exhausted state into unbounded Slack/email fan-out and background-task buildup. Emit on the threshold transition only, or dedupe per billing window / agent-day before scheduling the task.
Also applies to: 429-435
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/budget/enforcer.py` around lines 394 - 400, The code currently
creates a background task for every blocked request by calling
self._notify_budget_event("Monthly budget exhausted", ...), causing unbounded
fan-out under retries; modify the logic in the budget-enforcement path(s) that
call self._notify_budget_event (also update the sibling branch that handles the
daily limit at the 429-435 area) to perform an atomic dedupe/check before
scheduling: add a short-lived dedupe key (per billing window or per agent-day)
in a shared store or in-process cache and only schedule
asyncio.create_task(self._notify_budget_event(...)) when the key did not already
exist (i.e., transition to exhausted); ensure the dedupe key includes the
billing window/agent identifier and expires at window end so subsequent distinct
windows can emit again.
| try: | ||
| addr = ipaddress.ip_address(host) | ||
| except ValueError: | ||
| # Not a literal IP -- hostname like "ntfy.example.com". | ||
| # Already checked against _BLOCKED_HOSTS above. | ||
| return | ||
| if addr.is_private or addr.is_link_local or addr.is_loopback: |
There was a problem hiding this comment.
Resolve DNS names before trusting server_url.
Lines 45-48 treat every non-literal hostname as safe. server_url=https://internal.corp or https://localhost. will pass this check even if DNS resolves to RFC1918/loopback space, so the SSRF guard is bypassed. Resolve all A/AAAA answers and reject any non-global address, or require an explicit allowlist for ntfy hosts.
As per coding guidelines, "Validate at system boundaries (user input, external APIs, config files)."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/notifications/adapters/ntfy.py` around lines 43 - 49, The
current SSRF guard in ntfy.py only checks literal IPs via
ipaddress.ip_address(host) and treats all non-literal hostnames as safe; update
the validation so that after parsing server_url and deriving host (the existing
host variable), you resolve DNS A/AAAA records (e.g., via socket.getaddrinfo or
a DNS resolver) and validate every returned address (the addr/addrinfo results)
to ensure none are private, link-local, loopback, or otherwise non-global before
allowing the URL; if any resolved address is non-global, reject the server_url
(or require the host to be explicitly present in an allowlist like
_BLOCKED_HOSTS inverted to an _ALLOWED_NTFY_HOSTS). Apply this change around the
current try/except block that uses ipaddress.ip_address(host) and the subsequent
addr.is_private/is_link_local/is_loopback checks so both literal IPs and
DNS-resolved addresses are validated.
| except Exception as exc: | ||
| logger.warning( | ||
| NOTIFICATION_SLACK_FAILED, | ||
| notification_id=notification.id, | ||
| error=str(exc), | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does str(httpx.HTTPStatusError)orstr(httpx.RequestError) include the request URL in the formatted message?
💡 Result:
str(httpx.HTTPStatusError): Yes, typically—when raised viaresponse.raise_for_status(), the exception message includes the URL (e.g."... for url: https://httpbin.org/status/404"). [1]str(httpx.RequestError): No, not inherently—HTTPX documents that request-related exceptions expose the URL via the attached.request(e.g.exc.request.url), and examples format the URL separately rather than relying onstr(exc). [1][2]
So: don’t assume str(RequestError) contains the URL; use exc.request.url. For HTTPStatusError raised by raise_for_status(), the URL is included in the default message. [1]
Sources: [1] [2]
🏁 Script executed:
find . -name "slack.py" -path "*/notifications/adapters/*" | head -5Repository: Aureliolo/synthorg
Length of output: 108
🏁 Script executed:
cat -n src/synthorg/notifications/adapters/slack.pyRepository: Aureliolo/synthorg
Length of output: 3733
Don't serialize raw httpx exceptions into logs—they leak the Slack webhook URL.
When response.raise_for_status() fails (line 84), str(httpx.HTTPStatusError) includes the request URL. Logging this exception string at line 95 will leak the webhook credential into logs. Log only sanitized fields: error_type and status_code.
Patch
except Exception as exc:
logger.warning(
NOTIFICATION_SLACK_FAILED,
notification_id=notification.id,
- error=str(exc),
+ error_type=type(exc).__name__,
+ status_code=getattr(
+ getattr(exc, "response", None),
+ "status_code",
+ None,
+ ),
)
raise📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| except Exception as exc: | |
| logger.warning( | |
| NOTIFICATION_SLACK_FAILED, | |
| notification_id=notification.id, | |
| error=str(exc), | |
| ) | |
| except Exception as exc: | |
| logger.warning( | |
| NOTIFICATION_SLACK_FAILED, | |
| notification_id=notification.id, | |
| error_type=type(exc).__name__, | |
| status_code=getattr( | |
| getattr(exc, "response", None), | |
| "status_code", | |
| None, | |
| ), | |
| ) | |
| raise |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/notifications/adapters/slack.py` around lines 91 - 96, The
current except block logs the full exception string which can leak the Slack
webhook URL; change the handler in the except Exception as exc block to avoid
serializing raw httpx exceptions (used with response.raise_for_status) by
logging only sanitized fields: set error_type=type(exc).__name__ and if exc is
an httpx.HTTPStatusError include status_code=exc.response.status_code (or None
otherwise), and remove any use of str(exc) in the logger.warning call for
NOTIFICATION_SLACK_FAILED (keep notification_id=notification.id).
| <button | ||
| type="button" | ||
| role="listitem" | ||
| aria-label={`${item.severity} notification: ${item.title}`} | ||
| className={cn( | ||
| 'group relative flex w-full gap-3 rounded-md border-l-2 px-3 py-2 text-left', | ||
| 'transition-colors hover:bg-card-hover', | ||
| item.read ? 'border-l-transparent' : BORDER_COLORS[item.severity], | ||
| !item.read && 'bg-accent/5', | ||
| item.href && 'cursor-pointer', | ||
| )} | ||
| onClick={handleClick} | ||
| > |
There was a problem hiding this comment.
❓ Verification inconclusive
Script executed:
cat -n web/src/components/notifications/NotificationItemCard.tsxRepository: Aureliolo/synthorg
Repository: Aureliolo/synthorg
Exit code: 0
stdout:
1 import { AlertTriangle, Check, Info, X, XCircle } from 'lucide-react'
2 import { useNavigate } from 'react-router'
3
4 import { cn } from '@/lib/utils'
5 import type { NotificationItem, NotificationSeverity } from '@/types/notifications'
6
7 const SEVERITY_ICONS: Record<NotificationSeverity, React.ElementType> = {
8 info: Info,
9 warning: AlertTriangle,
10 error: XCircle,
11 critical: XCircle,
12 }
13
14 const SEVERITY_COLORS: Record<NotificationSeverity, string> = {
15 info: 'text-accent',
16 warning: 'text-warning',
17 error: 'text-danger',
18 critical: 'text-danger',
19 }
20
21 const BORDER_COLORS: Record<NotificationSeverity, string> = {
22 info: 'border-l-accent',
23 warning: 'border-l-warning',
24 error: 'border-l-danger',
25 critical: 'border-l-danger',
26 }
27
28 function formatRelativeTime(timestamp: string): string {
29 const diff = Date.now() - new Date(timestamp).getTime()
30 const seconds = Math.floor(diff / 1000)
31 if (seconds < 60) return 'just now'
32 const minutes = Math.floor(seconds / 60)
33 if (minutes < 60) return `${minutes}m ago`
34 const hours = Math.floor(minutes / 60)
35 if (hours < 24) return `${hours}h ago`
36 const days = Math.floor(hours / 24)
37 return `${days}d ago`
38 }
39
40 interface NotificationItemCardProps {
41 readonly item: NotificationItem
42 readonly onMarkRead: (id: string) => void
43 readonly onDismiss: (id: string) => void
44 }
45
46 export function NotificationItemCard({
47 item,
48 onMarkRead,
49 onDismiss,
50 }: NotificationItemCardProps) {
51 const navigate = useNavigate()
52 const Icon = SEVERITY_ICONS[item.severity]
53
54 function handleClick() {
55 if (!item.read) onMarkRead(item.id)
56 if (item.href && item.href.startsWith('/') && !item.href.startsWith('//')) {
57 void navigate(item.href)
58 }
59 }
60
61 return (
62 <button
63 type="button"
64 role="listitem"
65 aria-label={`${item.severity} notification: ${item.title}`}
66 className={cn(
67 'group relative flex w-full gap-3 rounded-md border-l-2 px-3 py-2 text-left',
68 'transition-colors hover:bg-card-hover',
69 item.read ? 'border-l-transparent' : BORDER_COLORS[item.severity],
70 !item.read && 'bg-accent/5',
71 item.href && 'cursor-pointer',
72 )}
73 onClick={handleClick}
74 >
75 <Icon className={cn('mt-0.5 size-4 shrink-0', SEVERITY_COLORS[item.severity])} />
76
77 <div className="min-w-0 flex-1">
78 <p className="truncate text-sm font-medium text-foreground">{item.title}</p>
79 {item.description && (
80 <p className="mt-0.5 truncate text-xs text-muted-foreground">
81 {item.description}
82 </p>
83 )}
84 <p className="mt-1 text-xs text-muted-foreground/70">
85 {formatRelativeTime(item.timestamp)}
86 </p>
87 </div>
88
89 <div className="flex shrink-0 items-start gap-1 opacity-0 transition-opacity group-hover:opacity-100">
90 {!item.read && (
91 <button
92 type="button"
93 className="rounded p-0.5 text-muted-foreground hover:bg-accent/10 hover:text-accent"
94 title="Mark as read"
95 onClick={(e) => {
96 e.stopPropagation()
97 onMarkRead(item.id)
98 }}
99 >
100 <Check className="size-3.5" />
101 </button>
102 )}
103 <button
104 type="button"
105 className="rounded p-0.5 text-muted-foreground hover:bg-danger/10 hover:text-danger"
106 title="Dismiss"
107 onClick={(e) => {
108 e.stopPropagation()
109 onDismiss(item.id)
110 }}
111 >
112 <X className="size-3.5" />
113 </button>
114 </div>
115 </button>
116 )
117 }
Split the row structure to separate interactive from non-interactive containers.
The root element is a <button role="listitem"> (line 62) that contains two nested <button> elements (lines 91-113). This creates invalid HTML markup and breaks keyboard navigation—nested buttons have undefined behavior across browsers. Additionally, the action cluster (lines 89-114) uses only group-hover:opacity-100 to reveal the buttons, making them invisible to keyboard users.
Restructure the component:
- Replace the outer
<button>with a non-interactive<li>or<div> - Create a dedicated wrapper button or link element for the primary click target (navigation + mark as read)
- Add
group-focus-within:opacity-100alongsidegroup-hover:opacity-100for the action cluster so keyboard-focused users can see and interact with the mark-read and dismiss buttons
Test keyboard navigation (Tab, Enter, Escape) through the notification list after making this change.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/components/notifications/NotificationItemCard.tsx` around lines 62 -
74, In NotificationItemCard, change the outer interactive element from a
<button> to a non-interactive container (e.g., <li> or <div>) so we no longer
nest buttons; move the onClick handler (handleClick) and the primary
navigation/mark-as-read behavior onto a single dedicated button or link inside
that container (the primary click target), remove any other nested button
elements or convert them to non-button controls, and ensure you use the existing
BORDER_COLORS and item.read logic for styling on the container; also update the
action cluster’s reveal classes to include keyboard focus by adding
group-focus-within:opacity-100 (in addition to group-hover:opacity-100) so the
mark-read/dismiss controls become visible to keyboard users and verify
tabindex/keyboard handlers (Enter/Escape) work for the primary button.
| import type { Meta, StoryObj } from '@storybook/react-vite' | ||
|
|
||
| import { NotificationsSection } from './NotificationsSection' | ||
|
|
||
| const meta = { | ||
| title: 'Settings/NotificationsSection', | ||
| component: NotificationsSection, | ||
| } satisfies Meta<typeof NotificationsSection> | ||
|
|
||
| export default meta | ||
| type Story = StoryObj<typeof meta> | ||
|
|
||
| export const Default: Story = {} |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider adding story variants for different states.
The story file is minimal with only a Default export. For better Storybook coverage, consider adding stories that showcase:
- Different toggle configurations (some enabled, some disabled)
- Browser notification permission states (granted/denied/prompt)
This is optional since settings sections typically have less visual variance than shared UI components.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/pages/settings/NotificationsSection.stories.tsx` around lines 1 - 13,
Add additional Storybook stories for NotificationsSection to showcase different
states: create named exports such as SomeTogglesEnabled, SomeTogglesDisabled,
PermissionGranted, PermissionDenied, and PermissionPrompt (in addition to
Default) that pass different props or initial state into NotificationsSection to
toggle feature flags; for browser permission variations, mock or set
window.Notification.permission (or use a decorator/parameter to stub
Notification.requestPermission) inside each story so one story shows "granted",
one "denied", and one "default/prompt", and ensure story names reference the
NotificationsSection component and existing Default story for discoverability.
| function hydratePrefs(): NotificationPreferences { | ||
| try { | ||
| const raw = localStorage.getItem(STORAGE_KEY_PREFS) | ||
| if (!raw) return DEFAULT_PREFERENCES | ||
| const parsed = JSON.parse(raw) as unknown | ||
| if (typeof parsed !== 'object' || parsed === null) return DEFAULT_PREFERENCES | ||
| return { ...DEFAULT_PREFERENCES, ...(parsed as Partial<NotificationPreferences>) } |
There was a problem hiding this comment.
Validate persisted preferences before merging them.
routeOverrides and browserPermission are trusted straight from localStorage. Values like routeOverrides: null or { "tasks.failed": "toast" } survive this shallow merge and later break computeRoutes() or silently corrupt routing.
🔧 Suggested fix
+const VALID_ROUTES = new Set<NotificationRoute>(['toast', 'drawer', 'browser'])
+const VALID_PERMISSIONS = new Set<NotificationPermission>([
+ 'default',
+ 'denied',
+ 'granted',
+])
function hydratePrefs(): NotificationPreferences {
try {
const raw = localStorage.getItem(STORAGE_KEY_PREFS)
if (!raw) return DEFAULT_PREFERENCES
const parsed = JSON.parse(raw) as unknown
if (typeof parsed !== 'object' || parsed === null) return DEFAULT_PREFERENCES
- return { ...DEFAULT_PREFERENCES, ...(parsed as Partial<NotificationPreferences>) }
+ const obj = parsed as Record<string, unknown>
+ const rawRouteOverrides =
+ typeof obj.routeOverrides === 'object' && obj.routeOverrides !== null
+ ? (obj.routeOverrides as Record<string, unknown>)
+ : {}
+
+ const routeOverrides = Object.fromEntries(
+ Object.entries(rawRouteOverrides).filter(
+ ([category, routes]) =>
+ VALID_CATEGORIES.has(category) &&
+ Array.isArray(routes) &&
+ routes.every(
+ (route) =>
+ typeof route === 'string' &&
+ VALID_ROUTES.has(route as NotificationRoute),
+ ),
+ ),
+ ) as Partial<Record<NotificationCategory, readonly NotificationRoute[]>>
+
+ return {
+ routeOverrides,
+ globalMute:
+ typeof obj.globalMute === 'boolean'
+ ? obj.globalMute
+ : DEFAULT_PREFERENCES.globalMute,
+ browserPermission:
+ typeof obj.browserPermission === 'string' &&
+ VALID_PERMISSIONS.has(obj.browserPermission as NotificationPermission)
+ ? (obj.browserPermission as NotificationPermission)
+ : DEFAULT_PREFERENCES.browserPermission,
+ }
} catch {
return DEFAULT_PREFERENCES
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/stores/notifications.ts` around lines 143 - 149, The persisted
preferences loaded in hydratePrefs() trust parsed.routeOverrides and
parsed.browserPermission without validation, which allows invalid values (e.g.,
null or malformed objects) to leak into the merged preferences and later break
computeRoutes(); update hydratePrefs to validate and sanitize the parsed object
before merging: after JSON.parse, check that parsed.routeOverrides (if present)
is a plain object mapping string->string and filter/omit any non-string keys or
non-string values, and check that parsed.browserPermission (if present) is one
of the allowed permission values (or otherwise ignore it); only merge the
sanitized subsets into DEFAULT_PREFERENCES so routeOverrides and
browserPermission cannot be null or malformed when returned.
| set((state) => { | ||
| const newItems = [item, ...state.items].slice(0, MAX_ITEMS) | ||
| return { | ||
| items: newItems, | ||
| unreadCount: countUnread(newItems), | ||
| } | ||
| }) |
There was a problem hiding this comment.
Honor the drawer route before persisting the item.
enqueue() always inserts the notification into state.items, so categories configured as toast-only/browser-only still appear in the drawer and increment unreadCount. That makes the per-category drawer toggle ineffective.
🔧 Suggested fix
set((state) => {
- const newItems = [item, ...state.items].slice(0, MAX_ITEMS)
+ const newItems = routes.includes('drawer')
+ ? [item, ...state.items].slice(0, MAX_ITEMS)
+ : state.items
return {
items: newItems,
unreadCount: countUnread(newItems),
}
})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/stores/notifications.ts` around lines 246 - 252, enqueue() currently
always prepends the new notification into state.items (affecting unreadCount)
which causes toast-only/browser-only categories to appear in the drawer; change
enqueue() to first check whether the drawer should persist this category (e.g.,
consult the route/drawer state or the category's "drawerEnabled" flag) and only
add the item to state.items when the drawer is active/allowed; otherwise show
the toast/browser notification without mutating state.items. Keep the
MAX_ITEMS/truncation and unreadCount calculation (countUnread(newItems)) for the
branch that does add to state.items.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/synthorg/notifications/dispatcher.py`:
- Around line 85-109: The dispatch() method must snapshot the sink registry
before any await to avoid race conditions: capture a local immutable copy (e.g.,
sinks = list(self._sinks)) at the top of dispatch(), size the errors list from
that snapshot, iterate over that local sinks when creating tasks (pass that
local list into the loop and into _guarded_send), and pass the same snapshot to
_log_result/_log_exception_group so all reporting uses the captured count; apply
the same snapshotting approach in the other similar method referenced (the block
around lines 139-159) to avoid mutating/shared registry races.
- Around line 162-176: The warning in _log_exception_group currently logs
eg.exceptions but omits the ExceptionGroup traceback; update the logger.warning
call inside _log_exception_group to pass exc_info=True (preserving existing
structured fields: NOTIFICATION_DISPATCH_FAILED, notification_id, category,
error, partial_sink_errors) so the full ExceptionGroup traceback is emitted with
the log entry.
In `@src/synthorg/settings/definitions/api.py`:
- Around line 3-7: Update the module docstring that currently starts with
"Registers 15 settings..." to reflect the new totals: change the text to
"Registers 16 settings..." and update "twelve are bootstrap-only" to "thirteen
are bootstrap-only" (the runtime-editable count should remain "three are
runtime-editable"); locate and edit the exact docstring text "Registers 15
settings covering server, TLS, CORS, rate limiting, authentication, and setup.
Three are runtime-editable; twelve are bootstrap-only" and adjust the numeric
counts accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 3cb0c554-76e6-4725-bbbf-6f4bf220d0c5
📒 Files selected for processing (3)
src/synthorg/notifications/dispatcher.pysrc/synthorg/settings/definitions/api.pyweb/src/components/layout/AppLayout.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: Test (Python 3.14)
- GitHub Check: Dashboard Test
- GitHub Check: Build Backend
- GitHub Check: Build Sandbox
- GitHub Check: Build Web
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (5)
web/src/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx,js,jsx}: Always usecreateLoggerfrom@/lib/logger-- never bareconsole.warn/console.error/console.debugin application code
Logger variable name must always beconst log(e.g.const log = createLogger('module-name'))
Pass dynamic/untrusted values as separate arguments to logger methods (not interpolated into the message string) so they go throughsanitizeArg
Attacker-controlled fields inside structured objects must be wrapped insanitizeForLog()before embedding in log calls
Files:
web/src/components/layout/AppLayout.tsx
web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground,bg-card,text-accent,text-success,bg-danger, etc.) or CSS variables (var(--so-*)) for colors; NEVER hardcode hex values in.tsx/.tsfiles
Usefont-sansorfont-mono(Geist tokens) for typography; NEVER setfontFamilydirectly in.tsx/.tsfiles
Use density-aware tokens (p-card,gap-section-gap,gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing in components
Use token variables (var(--so-shadow-card-hover),border-border,border-bright) for shadows and borders; NEVER hardcode values in.tsx/.tsfiles
Use@/lib/motionpresets for Framer Motion transition durations; NEVER hardcode transition durations
CSS side-effect imports in TypeScript 6 require type declarations -- add/// <reference types="vite/client" />at the top of files with CSS importsALWAYS reuse existing components from web/src/components/ui/ before creating new ones. NEVER hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions - use design tokens and
@/lib/motionpresets. A PostToolUse hook (scripts/check_web_design_system.py) enforces these rules on every Edit/Write to web/src/.
Files:
web/src/components/layout/AppLayout.tsx
web/src/components/layout/**/*.{ts,tsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/components/layout/**/*.{ts,tsx}: MountToastContaineronce in AppLayout for success/error/warning/info notifications with auto-dismiss queue
MountCommandPaletteonce in AppLayout; register commands viauseCommandPalettehook
Files:
web/src/components/layout/AppLayout.tsx
src/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
src/**/*.py: Nofrom __future__ import annotations- Python 3.14 has PEP 649 native lazy annotations.
Use PEP 758 except syntax: useexcept A, B:(no parentheses) - ruff enforces this on Python 3.14.
Type hints required on all public functions, with mypy strict mode enforcement.
Docstrings required on all public classes and functions using Google style - enforced by ruff D rules.
Immutability: create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement. For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries.
Config vs runtime state: use frozen Pydantic models for config/identity; separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Models: use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). Adopted conventions: use allow_inf_nan=False in all ConfigDict declarations; use@computed_fieldfor derived values instead of storing + validating redundant fields; use NotBlankStr (from core.types) for all identifier/name fields.
Async concurrency: prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare create_task.
Line length: 88 characters (ruff enforced).
Functions must be < 50 lines, files must be < 800 lines.
Handle errors explicitly, never silently swallow exceptions.
Validate at system boundaries (user input, external APIs, config files).
Every module with business logic MUST have:from synthorg.observability import get_loggerthenlogger = get_logger(__name__). Variable name must always belogger(not_logger, notlog).
Never useimport logging/logging.getLogger()/ `print()...
Files:
src/synthorg/settings/definitions/api.pysrc/synthorg/notifications/dispatcher.py
⚙️ CodeRabbit configuration file
This project uses Python 3.14+ with PEP 758 except syntax: "except A, B:" (comma-separated, no parentheses) is correct and mandatory -- do NOT flag it as a typo or suggest parenthesized form. The "except builtins.MemoryError, RecursionError: raise" pattern is intentional project convention for system-error propagation. When evaluating the 50-line function limit, count only the function body excluding the signature lines, decorators, and docstring. Functions 1-5 lines over due to docstrings or multi-line signatures should not be flagged. Do not suggest extracting single-use helper functions called exactly once -- this reduces readability without improving maintainability.
Files:
src/synthorg/settings/definitions/api.pysrc/synthorg/notifications/dispatcher.py
src/synthorg/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
Package structure follows: api/ (REST + WebSocket, RFC 9457 errors, auth/, workflows, reports), backup/ (scheduler, retention, handlers), budget/ (cost tracking, quotas, risk scoring), communication/ (message bus, channels), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task engine, workspace, workflow execution), hr/ (hiring, agent registry, performance, evaluation), memory/ (MemoryBackend, retrieval pipeline, consolidation, embedding, procedural), persistence/ (PersistenceBackend, SQLite, repositories), observability/ (logging, events, redaction, shipping), providers/ (LLM abstraction, routing, health), security/ (rule engine, audit, policy, risk scoring), templates/ (company templates, presets, packs), tools/ (registry, built-in tools, MCP, sandbox).
Files:
src/synthorg/settings/definitions/api.pysrc/synthorg/notifications/dispatcher.py
🧠 Learnings (31)
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/layout/**/*.{ts,tsx} : Mount `ToastContainer` once in AppLayout for success/error/warning/info notifications with auto-dismiss queue
Applied to files:
web/src/components/layout/AppLayout.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/layout/**/*.{ts,tsx} : Mount `CommandPalette` once in AppLayout; register commands via `useCommandPalette` hook
Applied to files:
web/src/components/layout/AppLayout.tsx
📚 Learning: 2026-03-31T14:31:11.894Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard
Applied to files:
web/src/components/layout/AppLayout.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components
Applied to files:
web/src/components/layout/AppLayout.tsx
📚 Learning: 2026-04-06T13:43:45.381Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T13:43:45.381Z
Learning: Applies to web/package.json : Web dashboard: Node.js 22+, TypeScript 6.0+. Key dependencies: React 19, react-router, shadcn/ui, Base UI, Tailwind CSS 4, Zustand, tanstack/react-query, xyflow/react, dagrejs/dagre, d3-force, dnd-kit, Recharts, Framer Motion, cmdk-base, js-yaml, Axios, Lucide React, Storybook 10, Vitest, Playwright, fast-check.
Applied to files:
web/src/components/layout/AppLayout.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem) before creating new ones
Applied to files:
web/src/components/layout/AppLayout.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)
Applied to files:
web/src/components/layout/AppLayout.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : For Base UI Dialog, AlertDialog, and Popover components, compose with `Portal` + `Backdrop` + `Popup`. Popover and Menu additionally require a `Positioner` wrapper with `side` / `align` / `sideOffset` props
Applied to files:
web/src/components/layout/AppLayout.tsx
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)
Applied to files:
src/synthorg/settings/definitions/api.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.
Applied to files:
src/synthorg/settings/definitions/api.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).
Applied to files:
src/synthorg/settings/definitions/api.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers
Applied to files:
src/synthorg/settings/definitions/api.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/api/**/*.py : REST API: Litestar framework, controllers with guards, channels for WebSocket, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint. RFC 9457 structured errors (ErrorCategory, ErrorCode, ErrorDetail, ProblemDetail, CATEGORY_TITLES, category_title, category_type_uri, content negotiation).
Applied to files:
src/synthorg/settings/definitions/api.py
📚 Learning: 2026-03-26T15:18:16.848Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-26T15:18:16.848Z
Learning: Applies to src/synthorg/api/**/*.py : Litestar API must include setup wizard, auth/, auto-wiring, and lifecycle management
Applied to files:
src/synthorg/settings/definitions/api.py
📚 Learning: 2026-03-31T21:07:37.470Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T21:07:37.470Z
Learning: Applies to **/*.py : Use `except A, B:` (no parentheses) per PEP 758 exception syntax on Python 3.14
Applied to files:
src/synthorg/notifications/dispatcher.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to **/*.py : Use `except A, B:` syntax (without parentheses) per PEP 758 for exception handling in Python 3.14
Applied to files:
src/synthorg/notifications/dispatcher.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Use `except A, B:` syntax (no parentheses) for exception handling — PEP 758 exception syntax enforced by ruff on Python 3.14
Applied to files:
src/synthorg/notifications/dispatcher.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to **/*.py : Use PEP 758 except syntax with `except A, B:` (no parentheses) for multiple exceptions—ruff enforces this on Python 3.14.
Applied to files:
src/synthorg/notifications/dispatcher.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: `except A, B:` (no parentheses) — enforced by ruff on Python 3.14
Applied to files:
src/synthorg/notifications/dispatcher.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Handle errors explicitly; never silently swallow exceptions
Applied to files:
src/synthorg/notifications/dispatcher.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to **/*.py : Handle errors explicitly, never silently swallow exceptions
Applied to files:
src/synthorg/notifications/dispatcher.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to **/*.py : Handle errors explicitly—never silently swallow exceptions.
Applied to files:
src/synthorg/notifications/dispatcher.py
📚 Learning: 2026-03-15T16:55:07.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T16:55:07.730Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: use `except A, B:` (no parentheses) — ruff enforces this on Python 3.14.
Applied to files:
src/synthorg/notifications/dispatcher.py
📚 Learning: 2026-04-06T13:43:45.380Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T13:43:45.380Z
Learning: Applies to src/**/*.py : Use PEP 758 except syntax: use `except A, B:` (no parentheses) - ruff enforces this on Python 3.14.
Applied to files:
src/synthorg/notifications/dispatcher.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Handle errors explicitly, never silently swallow. Validate at system boundaries (user input, external APIs, config files).
Applied to files:
src/synthorg/notifications/dispatcher.py
📚 Learning: 2026-03-15T16:55:07.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T16:55:07.730Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising.
Applied to files:
src/synthorg/notifications/dispatcher.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising
Applied to files:
src/synthorg/notifications/dispatcher.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising. All state transitions must log at INFO. DEBUG for object creation, internal flow, and key function entry/exit
Applied to files:
src/synthorg/notifications/dispatcher.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising. All state transitions must log at INFO. DEBUG for object creation, internal flow, entry/exit of key functions.
Applied to files:
src/synthorg/notifications/dispatcher.py
📚 Learning: 2026-03-31T20:07:03.035Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T20:07:03.035Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising; all state transitions must log at INFO; DEBUG for object creation, internal flow, entry/exit of key functions
Applied to files:
src/synthorg/notifications/dispatcher.py
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/**/*.py : Retryable errors (`is_retryable=True`): `RateLimitError`, `ProviderTimeoutError`, `ProviderConnectionError`, `ProviderInternalError`. Non-retryable errors raise immediately without retry.
Applied to files:
src/synthorg/notifications/dispatcher.py
🔇 Additional comments (6)
web/src/components/layout/AppLayout.tsx (4)
91-103: LGTM! Shift+N gating is now correct.The fix from the past review has been properly applied — the handler now checks
!e.ctrlKey && !e.metaKey && !e.altKeyto ensure only the exactShift+Ncombination triggers the drawer toggle.
105-115: LGTM! Navigation handler with appropriate security validation.The href validation (
startsWith('/') && !startsWith('//')) correctly ensures only in-app paths are navigated, preventing open redirects via protocol-relative or absolute URLs dispatched from browser notifications.
135-135: LGTM!Command palette entry correctly dispatches the same custom event as the keyboard shortcut, and the shortcut hint matches the actual keybinding.
201-207: LGTM! Correct component mounting and prop wiring.
NotificationDrawerprops match the expected interface (open: boolean,onClose: () => void)ToastContainerandCommandPaletteremain mounted once in AppLayout as requiredBased on learnings: "Mount
ToastContaineronce in AppLayout" and "MountCommandPaletteonce in AppLayout".src/synthorg/settings/definitions/api.py (2)
142-171: Tiered rate-limit settings are well-defined and consistent.The new
rate_limit_unauth_max_requestsandrate_limit_auth_max_requestssettings:
- Match the
RateLimitConfigmodel defaults and bounds (20/10000 and 600/100000 respectively)- Align with the resolver's
get_int()calls and YAML paths- Correctly use
restart_required=Truesince these values are baked into middleware at construction
174-187: Therestart_required=Truefix correctly aligns this setting with its bootstrap-only behavior.The
rate_limit_time_unitis baked into the Litestar rate-limit middleware at construction time, so marking it asrestart_required=Trueis the correct fix. This addresses the previously identified inconsistency.
| def _log_exception_group( | ||
| self, | ||
| notification: Notification, | ||
| errors: list[str | None], | ||
| eg: ExceptionGroup, | ||
| ) -> None: | ||
| """Log ExceptionGroup with per-sink context preserved.""" | ||
| partial_errors = [e for e in errors if e is not None] | ||
| logger.warning( | ||
| NOTIFICATION_DISPATCH_FAILED, | ||
| notification_id=notification.id, | ||
| category=notification.category, | ||
| error=f"TaskGroup errors: {eg.exceptions}", | ||
| partial_sink_errors=partial_errors, | ||
| ) |
There was a problem hiding this comment.
Keep the ExceptionGroup traceback in the warning log.
This is the fallback path for failures that escaped _guarded_send(), but it currently logs only eg.exceptions. Add exc_info=True here so the swallowed TaskGroup failure still leaves a traceback.
🧭 Minimal fix
logger.warning(
NOTIFICATION_DISPATCH_FAILED,
notification_id=notification.id,
category=notification.category,
error=f"TaskGroup errors: {eg.exceptions}",
partial_sink_errors=partial_errors,
+ exc_info=True,
)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def _log_exception_group( | |
| self, | |
| notification: Notification, | |
| errors: list[str | None], | |
| eg: ExceptionGroup, | |
| ) -> None: | |
| """Log ExceptionGroup with per-sink context preserved.""" | |
| partial_errors = [e for e in errors if e is not None] | |
| logger.warning( | |
| NOTIFICATION_DISPATCH_FAILED, | |
| notification_id=notification.id, | |
| category=notification.category, | |
| error=f"TaskGroup errors: {eg.exceptions}", | |
| partial_sink_errors=partial_errors, | |
| ) | |
| def _log_exception_group( | |
| self, | |
| notification: Notification, | |
| errors: list[str | None], | |
| eg: ExceptionGroup, | |
| ) -> None: | |
| """Log ExceptionGroup with per-sink context preserved.""" | |
| partial_errors = [e for e in errors if e is not None] | |
| logger.warning( | |
| NOTIFICATION_DISPATCH_FAILED, | |
| notification_id=notification.id, | |
| category=notification.category, | |
| error=f"TaskGroup errors: {eg.exceptions}", | |
| partial_sink_errors=partial_errors, | |
| exc_info=True, | |
| ) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/notifications/dispatcher.py` around lines 162 - 176, The warning
in _log_exception_group currently logs eg.exceptions but omits the
ExceptionGroup traceback; update the logger.warning call inside
_log_exception_group to pass exc_info=True (preserving existing structured
fields: NOTIFICATION_DISPATCH_FAILED, notification_id, category, error,
partial_sink_errors) so the full ExceptionGroup traceback is emitted with the
log entry.
…otifications - Split single 100/min rate limit into unauth'd (20/min by IP) and auth'd (600/min by user ID) tiers stacked around auth middleware (#1077) - Add NotificationSink protocol with console, ntfy, Slack, email adapters and NotificationDispatcher fan-out (#849) - Wire notifications into approval gate, budget enforcer, and security escalation scheduler - Add unified frontend notification pipeline: WS events route through notificationsStore.enqueue() and fan out to toast, drawer, and browser Notification API (#1078) - Implement NotificationDrawer with filter bar, per-item actions, unread badge on sidebar bell, Shift+N keyboard shortcut - Add NotificationsSection to Settings page with per-category routing and browser permission management - Expand useGlobalNotifications to subscribe to agents, approvals, budget, system, and tasks WS channels - Update settings definitions and resolver for tiered rate limit fields - Update docs/security.md with tiered rate limiting documentation Closes #1077 Closes #1078 Closes #849
Backend: - Wire notification dispatcher close() into app shutdown path - Normalize auth rate limiter key to str(user.user_id) - Fire-and-forget notification in budget enforcer and timeout scheduler - Add NOTIFICATION_EMAIL_PARTIAL_CREDENTIALS event constant - Add timeout=10 to SMTP constructor - Deep-copy metadata unconditionally (empty dict edge case) Frontend: - Fix browser notification icon path to /favicon.svg - Simplify rate limit check (non-null assertion) - Add markReadBatch/dismissBatch store methods for drawer bulk actions - Derive filter GROUPS from CATEGORY_CONFIGS - Reset store state in Empty story decorator - Import ToastVariant from toast store instead of redefining
…y, restart_required for time_unit
- Add _build_unauth_identifier that extracts real client IP from X-Forwarded-For when trusted_proxies is configured (walks hops from right, skips trusted entries) - Emit startup warning when server is non-loopback without trusted_proxies (all proxied clients share one rate bucket) - Bump auth_max_requests default from 600 to 6000/min (~100 req/sec) - Snapshot dispatcher sink list before dispatch (race safety) - Update settings docstring count (16 settings, 13 bootstrap-only) - Update all docs/tests for 6000 default
e983148 to
e1a615c
Compare
There was a problem hiding this comment.
Actionable comments posted: 17
♻️ Duplicate comments (14)
tests/unit/notifications/test_dispatcher.py (1)
119-130:⚠️ Potential issue | 🟡 MinorAlso cover
RecursionErrorpropagation in fatal-error contract tests.This test only protects half of the contract. Add
RecursionErrorcoverage so future broad catches cannot silently swallow it.🧪 Suggested update
- async def test_memory_error_propagates(self) -> None: - class _MemSink: + `@pytest.mark.parametrize`("exc_type", [MemoryError, RecursionError]) + async def test_system_errors_propagate( + self, + exc_type: type[BaseException], + ) -> None: + class _FatalSink: `@property` def sink_name(self) -> str: - return "mem" + return "fatal" async def send(self, notification: Notification) -> None: - raise MemoryError + raise exc_type - dispatcher = NotificationDispatcher(sinks=(_MemSink(),)) - with pytest.raises(MemoryError): + dispatcher = NotificationDispatcher(sinks=(_FatalSink(),)) + with pytest.raises(exc_type): await dispatcher.dispatch(_make_notification())As per coding guidelines, the
"except builtins.MemoryError, RecursionError: raise"pattern is intentional project convention for system-error propagation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/notifications/test_dispatcher.py` around lines 119 - 130, Extend the existing test_memory_error_propagates to also assert that RecursionError is propagated: add a second sink class (or reuse _MemSink renamed) whose async send raises RecursionError and then call await dispatcher.dispatch(_make_notification()) inside pytest.raises(RecursionError); ensure you reference the same NotificationDispatcher(sinks=(...)) and the existing _make_notification() to mirror the MemoryError case so both fatal system errors (MemoryError and RecursionError) are covered for the dispatch method.tests/unit/notifications/test_models.py (1)
17-32: 🧹 Nitpick | 🔵 TrivialParametrize enum value checks.
These assertions are hand-unrolled table tests. Using
@pytest.mark.parametrizekeeps future taxonomy additions to one-line cases.As per coding guidelines, "Prefer
@pytest.mark.parametrizefor testing similar cases."♻️ Parametrized version
`@pytest.mark.unit` class TestNotificationCategory: - def test_values(self) -> None: - assert NotificationCategory.APPROVAL.value == "approval" - assert NotificationCategory.BUDGET.value == "budget" - assert NotificationCategory.SECURITY.value == "security" - assert NotificationCategory.STAGNATION.value == "stagnation" - assert NotificationCategory.SYSTEM.value == "system" - assert NotificationCategory.AGENT.value == "agent" + `@pytest.mark.parametrize`( + ("member", "expected"), + [ + (NotificationCategory.APPROVAL, "approval"), + (NotificationCategory.BUDGET, "budget"), + (NotificationCategory.SECURITY, "security"), + (NotificationCategory.STAGNATION, "stagnation"), + (NotificationCategory.SYSTEM, "system"), + (NotificationCategory.AGENT, "agent"), + ], + ) + def test_values(self, member: NotificationCategory, expected: str) -> None: + assert member.value == expected🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/notifications/test_models.py` around lines 17 - 32, Replace the hand-unrolled assertions in the test_values methods with parametrized cases using pytest.mark.parametrize: for the NotificationCategory test, parametrize pairs of (enum_member, expected_value) for NotificationCategory.APPROVAL, BUDGET, SECURITY, STAGNATION, SYSTEM, AGENT and assert member.value == expected; do the same for NotificationSeverity with INFO, WARNING, ERROR, CRITICAL. Update the test functions (currently named test_values) to accept parameters and iterate via the `@pytest.mark.parametrize` decorator so adding new enum members requires only new param entries.docs/security.md (1)
77-77:⚠️ Potential issue | 🟡 MinorHealth endpoint claim is still inconsistent with exclusion.
Line 77 claims unauthenticated tier "protects against brute-force on login, setup, and health endpoints", but then states
/api/v1/healthis excluded viarate_limit.exclude_paths. Remove "health endpoints" from the protection claim or clarify that health protection is opt-in by removing it fromexclude_paths.📝 Suggested fix
-**Unauthenticated** requests are limited to 20 req/min by client IP (protects against brute-force on login, setup, and health endpoints). +**Unauthenticated** requests are limited to 20 req/min by client IP (protects against brute-force on login and setup endpoints).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/security.md` at line 77, The sentence in docs/security.md incorrectly claims the unauthenticated tier protects "login, setup, and health endpoints" while the health endpoint (/api/v1/health) is explicitly excluded via the rate_limit.exclude_paths setting; update that sentence to either remove "health endpoints" from the protected list or note that health is excluded by default and must be explicitly included by removing /api/v1/health from rate_limit.exclude_paths, and ensure references to api.rate_limit.unauth_max_requests and rate_limit.exclude_paths remain consistent with the clarified behavior.src/synthorg/budget/enforcer.py (1)
394-401:⚠️ Potential issue | 🟠 MajorCoalesce exhausted-budget alerts to prevent notification fan-out.
Once a limit trips, every blocked request schedules a notification task. Under retry traffic or multiple concurrent requests hitting the limit, this causes unbounded notification fan-out. Consider deduplicating per billing window (monthly) or per agent-day (daily) so alerts fire only on the threshold transition.
🛡️ Sketch: Add dedupe tracking
# In __init__: self._monthly_alert_sent_for_period: str | None = None self._daily_alerts_sent: set[tuple[str, str]] = set() # (agent_id, day_key) # In _check_monthly_hard_stop, before create_task: period_key = period_start.isoformat() if self._monthly_alert_sent_for_period != period_key: self._monthly_alert_sent_for_period = period_key asyncio.create_task(self._notify_budget_event(...)) # In _check_daily_limit, before create_task: day_key = day_start.isoformat() dedupe_key = (agent_id, day_key) if dedupe_key not in self._daily_alerts_sent: self._daily_alerts_sent.add(dedupe_key) asyncio.create_task(self._notify_budget_event(...))Also applies to: 429-436
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/budget/enforcer.py` around lines 394 - 401, The monthly/daily exhausted-budget notification path (in _check_monthly_hard_stop and the similar daily path that calls asyncio.create_task(self._notify_budget_event(...)) before raising BudgetExhaustedError) currently schedules a task per blocked request and must be de-duplicated; add dedupe state to the enforcer instance (e.g., self._monthly_alert_sent_for_period: Optional[str] and self._daily_alerts_sent: set[tuple[str,str]] initialized in __init__), compute a period_key/day_key (e.g., period_start.isoformat()/day_start.isoformat()) and only call asyncio.create_task(self._notify_budget_event(...)) when the corresponding key hasn’t been seen (then record it), leaving the raise BudgetExhaustedError(msg) behavior unchanged; apply the same dedupe logic to the 429-436 daily-limit notification path.src/synthorg/notifications/models.py (1)
11-19: 🛠️ Refactor suggestion | 🟠 MajorAdd the standard module logger to this new business-logic module.
This file contains validation logic but never declares the required
logger = get_logger(__name__).As per coding guidelines, "Every module with business logic MUST have: `from synthorg.observability import get_logger` then `logger = get_logger(__name__)`."🧭 Minimal guideline-compliance fix
import copy from datetime import UTC, datetime from enum import StrEnum from uuid import uuid4 from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, model_validator from synthorg.core.types import NotBlankStr # noqa: TC001 +from synthorg.observability import get_logger + +logger = get_logger(__name__)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/notifications/models.py` around lines 11 - 19, This module (src/synthorg/notifications/models.py) is missing the standard module logger; add the import "from synthorg.observability import get_logger" near the top alongside existing imports and create a module-level logger variable with "logger = get_logger(__name__)" so validation and business-logic code (e.g., code using AwareDatetime, BaseModel, model_validator, NotBlankStr) can log via the standard observability API.src/synthorg/notifications/adapters/ntfy.py (1)
33-51:⚠️ Potential issue | 🟠 MajorReject malformed hosts and resolve hostnames before trusting this URL.
Any non-literal hostname returns early, so
internal.corporlocalhost.skip the SSRF check, andhttp:///topicalso passes becauseparsed.hostnameis empty. Fail fast when the hostname is missing, then resolve A/AAAA answers and reject any non-global address before storing the sink URL. This helper is shared by the Slack sink, so the gap affects both adapters.As per coding guidelines, "Validate at system boundaries (user input, external APIs, config files)."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/notifications/adapters/ntfy.py` around lines 33 - 51, The _validate_outbound_url helper currently returns early for non-literal hostnames and when parsed.hostname is empty; change it to first fail fast when hostname is missing (raise ValueError if parsed.hostname is falsy), then resolve the hostname to A/AAAA records (e.g., via socket.getaddrinfo or dns resolution) and iterate all resolved IPs ensuring none are private, link-local, loopback, or otherwise non-global before accepting the URL; keep existing scheme check against _ALLOWED_SCHEMES and static check against _BLOCKED_HOSTS, and make sure DNS resolution errors or absence of any global address result in raising ValueError so unresolvable or internal addresses are rejected (affects _validate_outbound_url, _ALLOWED_SCHEMES, and _BLOCKED_HOSTS usage).src/synthorg/notifications/adapters/slack.py (1)
91-96:⚠️ Potential issue | 🟠 MajorDon’t serialize the raw
httpxerror into logs here.
response.raise_for_status()formats the exception with the request URL, and for Slack that URL is the webhook credential. Logerror_typeandstatus_codeinstead ofstr(exc).🔒 Minimal log-sanitization fix
except Exception as exc: logger.warning( NOTIFICATION_SLACK_FAILED, notification_id=notification.id, - error=str(exc), + error_type=type(exc).__name__, + status_code=getattr( + getattr(exc, "response", None), + "status_code", + None, + ), ) raiseDoes `str(httpx.HTTPStatusError)` include the request URL when raised by `response.raise_for_status()` in HTTPX?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/notifications/adapters/slack.py` around lines 91 - 96, The except block in the Slack adapter currently logs the raw exception string (which may include the webhook URL); change it to avoid serializing the raw httpx error by logging the exception type and an HTTP status code instead: in the except Exception as exc handler (in the Slack adapter that calls response.raise_for_status()), compute error_type = type(exc).__name__ and status_code = getattr(getattr(exc, "response", None), "status_code", None) (or None as fallback), and pass those values to logger.warning in place of str(exc) while preserving notification_id and the NOTIFICATION_SLACK_FAILED key; ensure non-HTTP errors still log their type and a null/absent status_code.web/src/components/notifications/NotificationItemCard.tsx (1)
62-115:⚠️ Potential issue | 🟠 MajorRemove the nested buttons from this row.
The outer element is a
<button>, and both row actions are nested<button>s. That is invalid HTML, breaks keyboard/focus behavior, and the action cluster still stays hidden from keyboard users because it only reveals on hover. Use a non-interactive container plus one primary button/link, and reveal the action group ongroup-focus-withintoo.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/components/notifications/NotificationItemCard.tsx` around lines 62 - 115, The row currently uses a parent <button> (in NotificationItemCard) that contains nested action <button>s, which is invalid; change the parent to a non-interactive container (e.g., a <div> with role="listitem" and same className/aria-label) and move the row click behavior (handleClick) onto a single primary interactive element (a clickable <a> or <button> inside the row, styled like the row) such that only that primary element is tabbable; keep onMarkRead and onDismiss as buttons but ensure they are not nested inside another button and continue to call e.stopPropagation(); update the visibility trigger from group-hover to also include group-focus-within so the action cluster becomes visible when the primary element receives keyboard focus; adjust className usage (the existing cn call, SEVERITY_COLORS, BORDER_COLORS, and item.read logic) to the new container and primary element as needed.src/synthorg/notifications/dispatcher.py (2)
164-178:⚠️ Potential issue | 🟡 MinorAdd
exc_info=Trueto preserve theExceptionGrouptraceback.This fallback path logs failures that escaped
_guarded_send(), but withoutexc_info=Truethe traceback is lost. This makes debugging sink failures harder.🔧 Suggested fix
logger.warning( NOTIFICATION_DISPATCH_FAILED, notification_id=notification.id, category=notification.category, error=f"TaskGroup errors: {eg.exceptions}", partial_sink_errors=partial_errors, + exc_info=True, )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/notifications/dispatcher.py` around lines 164 - 178, The _log_exception_group fallback currently logs the ExceptionGroup without its traceback; update the logger.warning call inside the _log_exception_group method to include exc_info=True (i.e., add exc_info=True as an argument to logger.warning) so the ExceptionGroup traceback is preserved when logging TaskGroup errors for notification (use the existing variables notification, eg, partial_errors as-is).
141-162:⚠️ Potential issue | 🟡 MinorUse snapshot length instead of
self._sinksin_log_result().Line 153 references
len(self._sinks)but the dispatch used a snapshot (sinks = list(self._sinks)at line 86). Ifregister()is called during dispatch,total_sinkswill be inconsistent withlen(errors). Uselen(errors)which already reflects the snapshot size.🔧 Suggested fix
if failed: logger.warning( NOTIFICATION_DISPATCH_FAILED, notification_id=notification.id, category=notification.category, - total_sinks=len(self._sinks), + total_sinks=len(errors), failed=failed, )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/notifications/dispatcher.py` around lines 141 - 162, In _log_result() the warning logs use len(self._sinks) which can differ from the snapshot used during dispatch; replace len(self._sinks) with len(errors) so total_sinks reflects the snapshot size passed into _log_result (reference function _log_result, parameter errors, and the logger.warning call that sets total_sinks).web/src/stores/notifications.ts (2)
246-252:⚠️ Potential issue | 🟠 MajorHonor the
drawerroute before persisting the item.
enqueue()always inserts the notification intostate.items, so categories configured as toast-only or browser-only still appear in the drawer and incrementunreadCount. This makes the per-category drawer toggle ineffective.🔧 Suggested fix
set((state) => { - const newItems = [item, ...state.items].slice(0, MAX_ITEMS) + const newItems = routes.includes('drawer') + ? [item, ...state.items].slice(0, MAX_ITEMS) + : state.items return { items: newItems, unreadCount: countUnread(newItems), } })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/stores/notifications.ts` around lines 246 - 252, enqueue currently always prepends the new item into state.items (using MAX_ITEMS and countUnread), which causes toast-only or browser-only categories to appear in the drawer; modify enqueue() to first check the notification's category/display rule (e.g., a function like isCategoryShownInDrawer(category) or the existing category config) and only insert the item into state.items when that check returns true, otherwise skip persisting it but still trigger any toast/browser behavior; ensure unreadCount is computed from the filtered newItems so countUnread(newItems) remains correct.
143-153:⚠️ Potential issue | 🟠 MajorValidate persisted preferences before merging.
hydratePrefs()shallow-merges parsed localStorage data without validatingrouteOverridesorbrowserPermission. Values likerouteOverrides: nullor{ "tasks.failed": "invalid" }would corrupt routing logic incomputeRoutes().🔧 Suggested fix
+const VALID_ROUTES = new Set<NotificationRoute>(['toast', 'drawer', 'browser']) +const VALID_PERMISSIONS = new Set<NotificationPermission>(['default', 'denied', 'granted']) function hydratePrefs(): NotificationPreferences { try { const raw = localStorage.getItem(STORAGE_KEY_PREFS) if (!raw) return DEFAULT_PREFERENCES const parsed = JSON.parse(raw) as unknown if (typeof parsed !== 'object' || parsed === null) return DEFAULT_PREFERENCES - return { ...DEFAULT_PREFERENCES, ...(parsed as Partial<NotificationPreferences>) } + const obj = parsed as Record<string, unknown> + + // Validate routeOverrides + let routeOverrides: NotificationPreferences['routeOverrides'] = {} + if (typeof obj.routeOverrides === 'object' && obj.routeOverrides !== null) { + const raw = obj.routeOverrides as Record<string, unknown> + for (const [cat, routes] of Object.entries(raw)) { + if ( + VALID_CATEGORIES.has(cat) && + Array.isArray(routes) && + routes.every((r) => typeof r === 'string' && VALID_ROUTES.has(r as NotificationRoute)) + ) { + routeOverrides[cat as NotificationCategory] = routes as NotificationRoute[] + } + } + } + + return { + routeOverrides, + globalMute: typeof obj.globalMute === 'boolean' ? obj.globalMute : DEFAULT_PREFERENCES.globalMute, + browserPermission: + typeof obj.browserPermission === 'string' && VALID_PERMISSIONS.has(obj.browserPermission as NotificationPermission) + ? (obj.browserPermission as NotificationPermission) + : DEFAULT_PREFERENCES.browserPermission, + } } catch { return DEFAULT_PREFERENCES } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/stores/notifications.ts` around lines 143 - 153, hydratePrefs currently shallow-merges untrusted JSON from localStorage (STORAGE_KEY_PREFS) into DEFAULT_PREFERENCES, which can corrupt fields like routeOverrides and browserPermission used by computeRoutes; change hydratePrefs to validate parsed before merging: ensure parsed is an object, validate browserPermission is one of the expected permission strings (e.g., 'granted'|'denied'|'default') and only accept it if valid, validate routeOverrides is an object and each key is a known route id with a valid notification-level value (reject or skip invalid entries), and similarly validate any other preference fields individually before merging into DEFAULT_PREFERENCES so fallback defaults are preserved for invalid/malformed values.src/synthorg/notifications/adapters/email.py (1)
116-121:⚠️ Potential issue | 🟠 MajorTreat partial recipient refusal as a delivery failure.
smtp.send_message()returns a dict of refused recipients when at least one recipient is accepted but others are rejected. The current code ignores this return value, so partial delivery failures are silently logged as successful delivery on lines 84-88.🔧 Suggested fix
with smtplib.SMTP(self._host, self._port, timeout=10) as smtp: if self._use_tls: context = ssl.create_default_context() smtp.starttls(context=context) self._login_if_configured(smtp) - smtp.send_message(msg) + refused = smtp.send_message(msg) + if refused: + raise smtplib.SMTPRecipientsRefused(refused)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/notifications/adapters/email.py` around lines 116 - 121, The SMTP send path currently ignores the return value of smtp.send_message(), so partial recipient refusals are treated as success; capture the return value from smtp.send_message(msg) (referencing smtp.send_message and msg) and if it indicates any refused recipients (non-empty dict) treat it as a delivery failure—e.g., raise smtplib.SMTPRecipientsRefused with that dict or surface an error so the caller/logging (where success is currently logged) reflects failure; keep the rest of the flow (starttls via self._use_tls, self._login_if_configured, host/port) unchanged.web/src/services/browser-notifications.ts (1)
76-98:⚠️ Potential issue | 🟡 MinorMove
recordNotification()after successful notification creation.
recordNotification()is called before thenew Notification(...)constructor (line 76 vs line 79). If the constructor throws, a rate-limit slot is consumed without actually showing a notification, which can suppress legitimate alerts for the next 10 seconds.🔧 Suggested fix
if (isRateLimited()) { log.debug('Browser notification rate-limited') return } - recordNotification() - try { const notification = new Notification(payload.title, { body: payload.body, icon: '/favicon.svg', tag: payload.tag, }) + recordNotification() notification.onclick = () => { window.focus()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/services/browser-notifications.ts` around lines 76 - 98, The call to recordNotification() is happening before the Notification constructor and can consume a rate-limit slot if construction throws; move the recordNotification() invocation to immediately after the new Notification(...) call succeeds (e.g., after const notification = new Notification(...) and before setting notification.onclick) so it only records when a notification was actually created; keep the try/catch and existing log.warn handling unchanged and ensure payload and notification variable names are used as in the current code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/design/operations.md`:
- Around line 1703-1706: The docs currently describe NotificationSink as
synchronous; update the description to state that NotificationSink is an async
protocol and that its methods are async: change `send(notification:
Notification) -> None` to `async send(notification: Notification) -> None` (or
equivalent async signature) and `close() -> None` to `async close() -> None`,
making it clear implementers must implement async methods for the extension
point (reference: NotificationSink, send, close).
- Line 1292: The docs row for the `/api/v1/auth` endpoint incorrectly lists the
authenticated rate limit as "6,000 req/min"; update that text to "600 req/min"
to match the PR summary and linked issue, and verify the same authenticated-tier
value is consistent with `docs/security.md` (adjust any mismatched mention
there) so both the `/api/v1/auth` row and the security doc show "600 req/min"
for authenticated traffic.
In `@docs/design/page-structure.md`:
- Around line 279-280: Update the sentence that currently reads 'Users can
override routing defaults per category in notification preferences (future
iteration)' to reflect that override support is now implemented—remove the
'(future iteration)' note and state that users can override routing defaults per
category via the Notification settings / notification preferences controls
introduced in this PR; ensure the surrounding text mentions that these
per-category overrides control toast vs. panel routing and unread badge
behavior.
In `@src/synthorg/api/app.py`:
- Around line 1067-1098: The current _build_unauth_identifier implementation
accepts X-Forwarded-For whenever trusted is non-empty, allowing a direct client
to spoof the header; modify _extract_forwarded_ip so it first checks the
immediate peer (use get_remote_address(request) or request.client.host) and only
parses X-Forwarded-For if that immediate peer is in the trusted set, otherwise
return the immediate peer value; keep the existing logic of walking reversed
hops to find the rightmost untrusted hop when the peer is trusted.
In `@src/synthorg/api/config.py`:
- Around line 102-111: The default for the authenticated rate limit is
inconsistent: update the settings definition for rate_limit_auth_max_requests to
match the RateLimitConfig.auth_max_requests default of 6000 (currently set to
"600"). Locate the RateLimitConfig (symbols unauth_max_requests and
auth_max_requests) and the settings definition that declares
rate_limit_auth_max_requests and change its default value from "600" to "6000"
so tests and code align.
In `@src/synthorg/notifications/adapters/slack.py`:
- Around line 20-44: The Slack payload builder _build_slack_payload interpolates
untrusted Notification fields (title, body, category, source) directly into
mrkdwn causing possible mentions and control-character issues; add a small
sanitization helper (e.g., escape_for_slack) and use it on notification.title,
notification.body, notification.category, and notification.source before
building header, body_text, and the context text: the helper should replace '&'
'<' '>' with '&', '<', '>' and neutralize Slack mention tokens like
<!here>, <!channel>, <!everyone> (e.g., by escaping the '<' or removing the '!'
inside those brackets) so no raw mentions are sent. Ensure all places in
_build_slack_payload reference the sanitized values.
In `@src/synthorg/notifications/factory.py`:
- Around line 17-27: Move the event constant imports
(NOTIFICATION_SINK_CONFIG_INVALID, NOTIFICATION_SINK_DISABLED,
NOTIFICATION_SINK_UNKNOWN_TYPE) so they appear with the other local imports
before the TYPE_CHECKING block; specifically, reorder the import block so from
synthorg.observability.events.notification import ... is placed above the "if
TYPE_CHECKING:" section while leaving get_logger and the TYPE_CHECKING import
unchanged and keeping logger = get_logger(__name__) as-is.
In `@tests/integration/settings/test_settings_integration.py`:
- Around line 120-121: The test assertion enforces the wrong default
authenticated rate limit (auth_max_requests == 6000); update the assertion in
tests/integration/settings/test_settings_integration.py to assert
result.rate_limit.auth_max_requests == 600 (to match the documented 600/min
default) and ensure any related test setup that populates result.rate_limit
(e.g., helper functions or fixtures that create the settings object) uses the
same 600 default so the test and the source-of-truth for rate limits remain
consistent.
In `@tests/unit/api/controllers/test_ws.py`:
- Around line 278-279: Replace the positional access middleware[1] with a
type-based lookup: iterate over the middleware list, use inspect.isclass and
issubclass(..., ApiAuthMiddleware) (or fallback to checking callable.__name__)
to find and assign auth_cls, then assert it's not None; do the same replacement
for the other occurrence where auth middleware is accessed by index so tests
(e.g., test_ws_rate_limit_excludes_ws_path style) no longer depend on middleware
ordering.
In `@tests/unit/api/test_app.py`:
- Around line 1149-1153: The assertions for middleware identity are too loose:
replace substring checks on stores (e.g., "auth" in stores[1]) with exact
comparisons of cfg.store values (e.g., assert stores == ["unauth", "auth"] or
assert stores[0] == "unauth" and stores[1] == "auth") to avoid matching
"unauth". Also stop using isinstance(entry, type) to detect auth middleware;
instead check the concrete class identity (e.g., assert type(entry) is
AuthMiddleware or assert entry.__class__.__name__ == "AuthMiddleware") or
compare against the expected middleware class names to ensure the ordering test
fails when the wrong middleware is present. Ensure you update the same pattern
at the other location mentioned (around the other block that inspects rl_configs
entries).
In `@tests/unit/api/test_config.py`:
- Around line 30-31: The test assertion for the authenticated rate limit is
incorrect: update the expectation in tests/unit/api/test_config.py where
rl.auth_max_requests is asserted (currently 6000) to the intended default of 600
(per the PR objective `#1077`); change the assertion to assert
rl.auth_max_requests == 600 and run related tests to ensure no other tests rely
on the old 6000 value.
In `@tests/unit/settings/test_resolver.py`:
- Around line 82-84: The default for authenticated rate limits is inconsistent:
update the RateLimitConfig class's auth_max_requests default from 6000 to 600 so
it matches the settings definition (the default="600" in the API settings
definition), and ensure any tests or usages (e.g., the test_resolver variable
unauth_max_requests/auth_max_requests declarations) are aligned to 600; modify
the auth_max_requests default in the RateLimitConfig constructor/definition and
verify the settings definition symbol and tests reference the same numeric
value.
In `@web/src/components/layout/AppLayout.tsx`:
- Line 135: The command palette entry for notifications in AppLayout currently
dispatches 'toggle-notification-drawer', which closes the drawer when it's
already open; update the action for the item with id 'notifications-open' (in
the object where label: 'Notifications') to dispatch 'open-notification-drawer'
instead (i.e., replace new CustomEvent('toggle-notification-drawer') with new
CustomEvent('open-notification-drawer')) so the command always opens the drawer
like the Bell button.
In `@web/src/components/notifications/NotificationDrawer.stories.tsx`:
- Around line 39-65: The SeedNotifications component currently enqueues sample
notifications without resetting state, causing duplicates on re-render; update
SeedNotifications to call useNotificationsStore.getState().clearAll() (or the
store's reset method) before the enqueue(...) calls so the store is cleared
prior to seeding, ensuring deterministic state across re-renders and hot
reloads.
In `@web/src/components/notifications/NotificationItemCard.tsx`:
- Around line 91-112: The icon-only action buttons in NotificationItemCard rely
solely on title attributes which are not robust for assistive tech; update the
button elements that call onMarkRead(item.id) and onDismiss(item.id) to include
explicit aria-label attributes (e.g., aria-label="Mark as read" for the Check
button and aria-label="Dismiss notification" for the X button) so screen readers
always announce the action clearly while keeping the existing title and
stopPropagation behavior unchanged.
In `@web/src/pages/settings/NotificationsSection.tsx`:
- Around line 51-61: toggleRoute can append duplicate routes because it always
spreads current and adds route; update it to avoid duplicates by checking
membership or deduplicating before calling setRouteOverride. In the toggleRoute
function (referencing preferences.routeOverrides,
CATEGORY_CONFIGS[category].defaultRoutes and setRouteOverride), when enabled use
either a conditional like current.includes(route) ? current : [...current,
route] or create a deduped array (e.g., Array.from(new Set([...current,
route]))) and then call setRouteOverride with that deduped routes array.
In `@web/src/stores/agents.ts`:
- Around line 269-270: The event handler is logging "unhandled event_type" for
the known "personality.trimmed" event; update the WebSocket event dispatch in
agents.ts (the code path that calls useNotificationsStore.handleWsEvent / the
switch/if that logs unhandled events) to explicitly return early when event_type
=== "personality.trimmed" (or handle it by calling the unified notification
pipeline if appropriate) so it does not fall through to the generic debug log;
ensure the check runs before the final unhandled-event debug branch so normal
personality.trimmed notifications are suppressed from diagnostic noise.
---
Duplicate comments:
In `@docs/security.md`:
- Line 77: The sentence in docs/security.md incorrectly claims the
unauthenticated tier protects "login, setup, and health endpoints" while the
health endpoint (/api/v1/health) is explicitly excluded via the
rate_limit.exclude_paths setting; update that sentence to either remove "health
endpoints" from the protected list or note that health is excluded by default
and must be explicitly included by removing /api/v1/health from
rate_limit.exclude_paths, and ensure references to
api.rate_limit.unauth_max_requests and rate_limit.exclude_paths remain
consistent with the clarified behavior.
In `@src/synthorg/budget/enforcer.py`:
- Around line 394-401: The monthly/daily exhausted-budget notification path (in
_check_monthly_hard_stop and the similar daily path that calls
asyncio.create_task(self._notify_budget_event(...)) before raising
BudgetExhaustedError) currently schedules a task per blocked request and must be
de-duplicated; add dedupe state to the enforcer instance (e.g.,
self._monthly_alert_sent_for_period: Optional[str] and self._daily_alerts_sent:
set[tuple[str,str]] initialized in __init__), compute a period_key/day_key
(e.g., period_start.isoformat()/day_start.isoformat()) and only call
asyncio.create_task(self._notify_budget_event(...)) when the corresponding key
hasn’t been seen (then record it), leaving the raise BudgetExhaustedError(msg)
behavior unchanged; apply the same dedupe logic to the 429-436 daily-limit
notification path.
In `@src/synthorg/notifications/adapters/email.py`:
- Around line 116-121: The SMTP send path currently ignores the return value of
smtp.send_message(), so partial recipient refusals are treated as success;
capture the return value from smtp.send_message(msg) (referencing
smtp.send_message and msg) and if it indicates any refused recipients (non-empty
dict) treat it as a delivery failure—e.g., raise smtplib.SMTPRecipientsRefused
with that dict or surface an error so the caller/logging (where success is
currently logged) reflects failure; keep the rest of the flow (starttls via
self._use_tls, self._login_if_configured, host/port) unchanged.
In `@src/synthorg/notifications/adapters/ntfy.py`:
- Around line 33-51: The _validate_outbound_url helper currently returns early
for non-literal hostnames and when parsed.hostname is empty; change it to first
fail fast when hostname is missing (raise ValueError if parsed.hostname is
falsy), then resolve the hostname to A/AAAA records (e.g., via
socket.getaddrinfo or dns resolution) and iterate all resolved IPs ensuring none
are private, link-local, loopback, or otherwise non-global before accepting the
URL; keep existing scheme check against _ALLOWED_SCHEMES and static check
against _BLOCKED_HOSTS, and make sure DNS resolution errors or absence of any
global address result in raising ValueError so unresolvable or internal
addresses are rejected (affects _validate_outbound_url, _ALLOWED_SCHEMES, and
_BLOCKED_HOSTS usage).
In `@src/synthorg/notifications/adapters/slack.py`:
- Around line 91-96: The except block in the Slack adapter currently logs the
raw exception string (which may include the webhook URL); change it to avoid
serializing the raw httpx error by logging the exception type and an HTTP status
code instead: in the except Exception as exc handler (in the Slack adapter that
calls response.raise_for_status()), compute error_type = type(exc).__name__ and
status_code = getattr(getattr(exc, "response", None), "status_code", None) (or
None as fallback), and pass those values to logger.warning in place of str(exc)
while preserving notification_id and the NOTIFICATION_SLACK_FAILED key; ensure
non-HTTP errors still log their type and a null/absent status_code.
In `@src/synthorg/notifications/dispatcher.py`:
- Around line 164-178: The _log_exception_group fallback currently logs the
ExceptionGroup without its traceback; update the logger.warning call inside the
_log_exception_group method to include exc_info=True (i.e., add exc_info=True as
an argument to logger.warning) so the ExceptionGroup traceback is preserved when
logging TaskGroup errors for notification (use the existing variables
notification, eg, partial_errors as-is).
- Around line 141-162: In _log_result() the warning logs use len(self._sinks)
which can differ from the snapshot used during dispatch; replace
len(self._sinks) with len(errors) so total_sinks reflects the snapshot size
passed into _log_result (reference function _log_result, parameter errors, and
the logger.warning call that sets total_sinks).
In `@src/synthorg/notifications/models.py`:
- Around line 11-19: This module (src/synthorg/notifications/models.py) is
missing the standard module logger; add the import "from synthorg.observability
import get_logger" near the top alongside existing imports and create a
module-level logger variable with "logger = get_logger(__name__)" so validation
and business-logic code (e.g., code using AwareDatetime, BaseModel,
model_validator, NotBlankStr) can log via the standard observability API.
In `@tests/unit/notifications/test_dispatcher.py`:
- Around line 119-130: Extend the existing test_memory_error_propagates to also
assert that RecursionError is propagated: add a second sink class (or reuse
_MemSink renamed) whose async send raises RecursionError and then call await
dispatcher.dispatch(_make_notification()) inside pytest.raises(RecursionError);
ensure you reference the same NotificationDispatcher(sinks=(...)) and the
existing _make_notification() to mirror the MemoryError case so both fatal
system errors (MemoryError and RecursionError) are covered for the dispatch
method.
In `@tests/unit/notifications/test_models.py`:
- Around line 17-32: Replace the hand-unrolled assertions in the test_values
methods with parametrized cases using pytest.mark.parametrize: for the
NotificationCategory test, parametrize pairs of (enum_member, expected_value)
for NotificationCategory.APPROVAL, BUDGET, SECURITY, STAGNATION, SYSTEM, AGENT
and assert member.value == expected; do the same for NotificationSeverity with
INFO, WARNING, ERROR, CRITICAL. Update the test functions (currently named
test_values) to accept parameters and iterate via the `@pytest.mark.parametrize`
decorator so adding new enum members requires only new param entries.
In `@web/src/components/notifications/NotificationItemCard.tsx`:
- Around line 62-115: The row currently uses a parent <button> (in
NotificationItemCard) that contains nested action <button>s, which is invalid;
change the parent to a non-interactive container (e.g., a <div> with
role="listitem" and same className/aria-label) and move the row click behavior
(handleClick) onto a single primary interactive element (a clickable <a> or
<button> inside the row, styled like the row) such that only that primary
element is tabbable; keep onMarkRead and onDismiss as buttons but ensure they
are not nested inside another button and continue to call e.stopPropagation();
update the visibility trigger from group-hover to also include
group-focus-within so the action cluster becomes visible when the primary
element receives keyboard focus; adjust className usage (the existing cn call,
SEVERITY_COLORS, BORDER_COLORS, and item.read logic) to the new container and
primary element as needed.
In `@web/src/services/browser-notifications.ts`:
- Around line 76-98: The call to recordNotification() is happening before the
Notification constructor and can consume a rate-limit slot if construction
throws; move the recordNotification() invocation to immediately after the new
Notification(...) call succeeds (e.g., after const notification = new
Notification(...) and before setting notification.onclick) so it only records
when a notification was actually created; keep the try/catch and existing
log.warn handling unchanged and ensure payload and notification variable names
are used as in the current code.
In `@web/src/stores/notifications.ts`:
- Around line 246-252: enqueue currently always prepends the new item into
state.items (using MAX_ITEMS and countUnread), which causes toast-only or
browser-only categories to appear in the drawer; modify enqueue() to first check
the notification's category/display rule (e.g., a function like
isCategoryShownInDrawer(category) or the existing category config) and only
insert the item into state.items when that check returns true, otherwise skip
persisting it but still trigger any toast/browser behavior; ensure unreadCount
is computed from the filtered newItems so countUnread(newItems) remains correct.
- Around line 143-153: hydratePrefs currently shallow-merges untrusted JSON from
localStorage (STORAGE_KEY_PREFS) into DEFAULT_PREFERENCES, which can corrupt
fields like routeOverrides and browserPermission used by computeRoutes; change
hydratePrefs to validate parsed before merging: ensure parsed is an object,
validate browserPermission is one of the expected permission strings (e.g.,
'granted'|'denied'|'default') and only accept it if valid, validate
routeOverrides is an object and each key is a known route id with a valid
notification-level value (reject or skip invalid entries), and similarly
validate any other preference fields individually before merging into
DEFAULT_PREFERENCES so fallback defaults are preserved for invalid/malformed
values.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 7b11422d-4ded-420b-bb18-e037f6129cff
📒 Files selected for processing (63)
CLAUDE.mdREADME.mddocs/design/operations.mddocs/design/page-structure.mddocs/security.mdsrc/synthorg/api/app.pysrc/synthorg/api/config.pysrc/synthorg/api/state.pysrc/synthorg/budget/enforcer.pysrc/synthorg/config/defaults.pysrc/synthorg/config/schema.pysrc/synthorg/engine/approval_gate.pysrc/synthorg/notifications/__init__.pysrc/synthorg/notifications/adapters/__init__.pysrc/synthorg/notifications/adapters/console.pysrc/synthorg/notifications/adapters/email.pysrc/synthorg/notifications/adapters/ntfy.pysrc/synthorg/notifications/adapters/slack.pysrc/synthorg/notifications/config.pysrc/synthorg/notifications/dispatcher.pysrc/synthorg/notifications/factory.pysrc/synthorg/notifications/models.pysrc/synthorg/notifications/protocol.pysrc/synthorg/observability/events/approval_gate.pysrc/synthorg/observability/events/budget.pysrc/synthorg/observability/events/notification.pysrc/synthorg/security/timeout/scheduler.pysrc/synthorg/settings/definitions/api.pysrc/synthorg/settings/resolver.pytests/integration/settings/test_settings_integration.pytests/unit/api/conftest.pytests/unit/api/controllers/test_ws.pytests/unit/api/test_app.pytests/unit/api/test_config.pytests/unit/notifications/__init__.pytests/unit/notifications/test_config.pytests/unit/notifications/test_console_adapter.pytests/unit/notifications/test_dispatcher.pytests/unit/notifications/test_models.pytests/unit/notifications/test_ntfy_adapter.pytests/unit/notifications/test_protocol.pytests/unit/observability/test_events.pytests/unit/settings/test_resolver.pyweb/src/__tests__/pages/SettingsPage.test.tsxweb/src/__tests__/stores/agents.test.tsweb/src/components/layout/AppLayout.tsxweb/src/components/layout/Sidebar.tsxweb/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationDrawer.tsxweb/src/components/notifications/NotificationEmptyState.stories.tsxweb/src/components/notifications/NotificationEmptyState.tsxweb/src/components/notifications/NotificationFilterBar.stories.tsxweb/src/components/notifications/NotificationFilterBar.tsxweb/src/components/notifications/NotificationItemCard.stories.tsxweb/src/components/notifications/NotificationItemCard.tsxweb/src/hooks/useGlobalNotifications.tsweb/src/pages/SettingsPage.tsxweb/src/pages/settings/NotificationsSection.stories.tsxweb/src/pages/settings/NotificationsSection.tsxweb/src/services/browser-notifications.tsweb/src/stores/agents.tsweb/src/stores/notifications.tsweb/src/types/notifications.ts
| |----------|---------| | ||
| | `/api/v1/health` | Health check, readiness | | ||
| | `/api/v1/auth` | Authentication: setup, login, password change, ws-ticket, session management (list/revoke), logout (login/setup/change-password rate-limited to 10 req/min) | | ||
| | `/api/v1/auth` | Authentication: setup, login, password change, ws-ticket, session management (list/revoke), logout (tiered rate limiting: 20 req/min unauth by IP, 6,000 req/min auth by user ID -- see `docs/security.md`) | |
There was a problem hiding this comment.
Fix the authenticated tier value in the auth endpoint docs.
This row says 6,000 req/min, but the PR summary and linked issue both specify 600 req/min for authenticated traffic. Leaving the extra zero here will mislead operators about the default budget.
✏️ Suggested doc correction
-| `/api/v1/auth` | Authentication: setup, login, password change, ws-ticket, session management (list/revoke), logout (tiered rate limiting: 20 req/min unauth by IP, 6,000 req/min auth by user ID -- see `docs/security.md`) |
+| `/api/v1/auth` | Authentication: setup, login, password change, ws-ticket, session management (list/revoke), logout (tiered rate limiting: 20 req/min unauth by IP, 600 req/min auth by user ID -- see `docs/security.md`) |📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| | `/api/v1/auth` | Authentication: setup, login, password change, ws-ticket, session management (list/revoke), logout (tiered rate limiting: 20 req/min unauth by IP, 6,000 req/min auth by user ID -- see `docs/security.md`) | | |
| | `/api/v1/auth` | Authentication: setup, login, password change, ws-ticket, session management (list/revoke), logout (tiered rate limiting: 20 req/min unauth by IP, 600 req/min auth by user ID -- see `docs/security.md`) | |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/design/operations.md` at line 1292, The docs row for the `/api/v1/auth`
endpoint incorrectly lists the authenticated rate limit as "6,000 req/min";
update that text to "600 req/min" to match the PR summary and linked issue, and
verify the same authenticated-tier value is consistent with `docs/security.md`
(adjust any mismatched mention there) so both the `/api/v1/auth` row and the
security doc show "600 req/min" for authenticated traffic.
…ixes - CRITICAL: Only read X-Forwarded-For when request.client.host is in trusted_proxies -- prevents arbitrary clients from spoofing the header - Fix test_app store assertion to use exact match (not substring) - Prevent duplicate route entries in toggleRoute
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
tests/unit/api/test_app.py (1)
1173-1189: 🧹 Nitpick | 🔵 TrivialMiddleware identification could be more precise.
The
elifstructure prevents the"auth" in cfg.storecheck from matching "unauth" stores, which addresses the main concern. However, Line 1181'sisinstance(entry, type)check for auth middleware is loose — it matches any class-based middleware.Consider tightening the auth middleware identification for future-proofing:
♻️ Optional: More precise auth middleware identification
elif isinstance(entry, type): - # Auth middleware is a class, not a DefineMiddleware - auth_mw_idx = i + # Auth middleware is the class returned by create_auth_middleware_class + if "Auth" in getattr(entry, "__name__", ""): + auth_mw_idx = i🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/api/test_app.py` around lines 1173 - 1189, The loop that sets unauth_idx/auth_mw_idx/auth_rl_idx is too loose because the isinstance(entry, type) check matches any class; tighten auth middleware detection by checking for the specific auth middleware symbol instead of any class — e.g., in the loop replace the broad isinstance(entry, type) branch with a targeted check such as issubclass(entry, AuthMiddleware) or verifying entry.__name__ == "AuthMiddleware" or a unique attribute/method (like has_auth_marker or requires_auth) on the middleware class; keep the existing LsRL checks for rate-limiters (cfg and "unauth"/"auth" in cfg.store) and only set auth_mw_idx when the precise auth middleware condition is met so the final sandwich assertion (unauth_idx < auth_mw_idx < auth_rl_idx) remains reliable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/src/pages/settings/NotificationsSection.tsx`:
- Around line 117-124: Checkbox inputs in NotificationsSection.tsx lack
category-specific accessible names causing ambiguous controls for screen
readers; update the <input> inside the label that currently calls
routeEnabled(category, route, preferences.routeOverrides) and
toggleRoute(category, route, ...) to include a unique accessible name (e.g.,
aria-label or aria-labelledby) that concatenates the category and route (or
references a visually-hidden span with the combined text) so each checkbox is
announced as "{category} {route}" instead of just "drawer/toast/browser". Ensure
the chosen attribute references the category and route variables and remains
unique per control.
---
Duplicate comments:
In `@tests/unit/api/test_app.py`:
- Around line 1173-1189: The loop that sets unauth_idx/auth_mw_idx/auth_rl_idx
is too loose because the isinstance(entry, type) check matches any class;
tighten auth middleware detection by checking for the specific auth middleware
symbol instead of any class — e.g., in the loop replace the broad
isinstance(entry, type) branch with a targeted check such as issubclass(entry,
AuthMiddleware) or verifying entry.__name__ == "AuthMiddleware" or a unique
attribute/method (like has_auth_marker or requires_auth) on the middleware
class; keep the existing LsRL checks for rate-limiters (cfg and "unauth"/"auth"
in cfg.store) and only set auth_mw_idx when the precise auth middleware
condition is met so the final sandwich assertion (unauth_idx < auth_mw_idx <
auth_rl_idx) remains reliable.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: e326515c-9919-4c97-8b14-2d261220a6cf
📒 Files selected for processing (3)
src/synthorg/api/app.pytests/unit/api/test_app.pyweb/src/pages/settings/NotificationsSection.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: Dashboard Test
- GitHub Check: Test (Python 3.14)
- GitHub Check: Build Web
- GitHub Check: Build Backend
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (5)
tests/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
tests/**/*.py: Use pytest markers:@pytest.mark.unit,@pytest.mark.integration,@pytest.mark.e2e,@pytest.mark.slow. Minimum coverage 80% (enforced in CI).
Async testing: asyncio_mode = 'auto' - no manual@pytest.mark.asyncioneeded. Default timeout: 30 seconds per test (do not add per-file markers; non-default overrides like timeout(60) allowed). Parallelism: ALWAYS include -n 8 when running pytest locally, never run tests sequentially.
Prefer@pytest.mark.parametrizefor testing similar cases. Use Hypothesis for property-based testing with@given+@settingsdecorators.
Vendor-agnostic everywhere: NEVER use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in project-owned code, docstrings, comments, tests, or config examples. Use generic names: example-provider, example-large-001, example-medium-001, example-small-001, test-provider, test-small-001. Vendor names may only appear in: Operations design page, .claude/ files, third-party import paths, provider presets (runtime user-facing data).
Property-based testing workflow: CI runs 10 deterministic examples per property test (derandomize=True). Random fuzzing locally: HYPOTHESIS_PROFILE=dev (1000 examples) or HYPOTHESIS_PROFILE=fuzz (10000 examples, no deadline). When Hypothesis finds a failure, fix the underlying bug and add@example(...) decorator to permanently cover the case in CI. Never skip flaky tests - fix them fundamentally by mocking time.monotonic()/asyncio.sleep() or using asyncio.Event().wait() for indefinite blocking.
Files:
tests/unit/api/test_app.py
⚙️ CodeRabbit configuration file
Test files do not require Google-style docstrings on classes or functions -- ruff D rules are only enforced on src/. A bare
@settings() decorator with no arguments on Hypothesis property tests is a no-op and should not be suggested -- the HYPOTHESIS_PROFILE env var controls example counts via registered profiles, which@given() honors automatically.
Files:
tests/unit/api/test_app.py
web/src/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx,js,jsx}: Always usecreateLoggerfrom@/lib/logger-- never bareconsole.warn/console.error/console.debugin application code
Logger variable name must always beconst log(e.g.const log = createLogger('module-name'))
Pass dynamic/untrusted values as separate arguments to logger methods (not interpolated into the message string) so they go throughsanitizeArg
Attacker-controlled fields inside structured objects must be wrapped insanitizeForLog()before embedding in log calls
Files:
web/src/pages/settings/NotificationsSection.tsx
web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground,bg-card,text-accent,text-success,bg-danger, etc.) or CSS variables (var(--so-*)) for colors; NEVER hardcode hex values in.tsx/.tsfiles
Usefont-sansorfont-mono(Geist tokens) for typography; NEVER setfontFamilydirectly in.tsx/.tsfiles
Use density-aware tokens (p-card,gap-section-gap,gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing in components
Use token variables (var(--so-shadow-card-hover),border-border,border-bright) for shadows and borders; NEVER hardcode values in.tsx/.tsfiles
Use@/lib/motionpresets for Framer Motion transition durations; NEVER hardcode transition durations
CSS side-effect imports in TypeScript 6 require type declarations -- add/// <reference types="vite/client" />at the top of files with CSS importsALWAYS reuse existing components from web/src/components/ui/ before creating new ones. NEVER hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions - use design tokens and
@/lib/motionpresets. A PostToolUse hook (scripts/check_web_design_system.py) enforces these rules on every Edit/Write to web/src/.
Files:
web/src/pages/settings/NotificationsSection.tsx
src/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
src/**/*.py: Nofrom __future__ import annotations- Python 3.14 has PEP 649 native lazy annotations.
Use PEP 758 except syntax: useexcept A, B:(no parentheses) - ruff enforces this on Python 3.14.
Type hints required on all public functions, with mypy strict mode enforcement.
Docstrings required on all public classes and functions using Google style - enforced by ruff D rules.
Immutability: create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement. For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries.
Config vs runtime state: use frozen Pydantic models for config/identity; separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Models: use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). Adopted conventions: use allow_inf_nan=False in all ConfigDict declarations; use@computed_fieldfor derived values instead of storing + validating redundant fields; use NotBlankStr (from core.types) for all identifier/name fields.
Async concurrency: prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare create_task.
Line length: 88 characters (ruff enforced).
Functions must be < 50 lines, files must be < 800 lines.
Handle errors explicitly, never silently swallow exceptions.
Validate at system boundaries (user input, external APIs, config files).
Every module with business logic MUST have:from synthorg.observability import get_loggerthenlogger = get_logger(__name__). Variable name must always belogger(not_logger, notlog).
Never useimport logging/logging.getLogger()/ `print()...
Files:
src/synthorg/api/app.py
⚙️ CodeRabbit configuration file
This project uses Python 3.14+ with PEP 758 except syntax: "except A, B:" (comma-separated, no parentheses) is correct and mandatory -- do NOT flag it as a typo or suggest parenthesized form. The "except builtins.MemoryError, RecursionError: raise" pattern is intentional project convention for system-error propagation. When evaluating the 50-line function limit, count only the function body excluding the signature lines, decorators, and docstring. Functions 1-5 lines over due to docstrings or multi-line signatures should not be flagged. Do not suggest extracting single-use helper functions called exactly once -- this reduces readability without improving maintainability.
Files:
src/synthorg/api/app.py
src/synthorg/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
Package structure follows: api/ (REST + WebSocket, RFC 9457 errors, auth/, workflows, reports), backup/ (scheduler, retention, handlers), budget/ (cost tracking, quotas, risk scoring), communication/ (message bus, channels), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task engine, workspace, workflow execution), hr/ (hiring, agent registry, performance, evaluation), memory/ (MemoryBackend, retrieval pipeline, consolidation, embedding, procedural), persistence/ (PersistenceBackend, SQLite, repositories), observability/ (logging, events, redaction, shipping), providers/ (LLM abstraction, routing, health), security/ (rule engine, audit, policy, risk scoring), templates/ (company templates, presets, packs), tools/ (registry, built-in tools, MCP, sandbox).
Files:
src/synthorg/api/app.py
🧠 Learnings (22)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/api/**/*.py : Use Litestar for REST + WebSocket API. Controllers, guards, channels, JWT + API key + WS ticket auth, RFC 9457 structured errors.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/api/**/*.py : Use Litestar for REST API and WebSocket API with JWT + API key + WS ticket authentication, RFC 9457 structured errors, and content negotiation.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/api/**/*.py : REST API: Litestar framework, controllers with guards, channels for WebSocket, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint. RFC 9457 structured errors (ErrorCategory, ErrorCode, ErrorDetail, ProblemDetail, CATEGORY_TITLES, category_title, category_type_uri, content negotiation).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers
Applied to files:
tests/unit/api/test_app.pysrc/synthorg/api/app.py
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively
Applied to files:
web/src/pages/settings/NotificationsSection.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/stores/**/*.{ts,tsx} : Use Zustand stores in web dashboard for state management (auth, WebSocket, toast, analytics, domain shells)
Applied to files:
web/src/pages/settings/NotificationsSection.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/layout/**/*.{ts,tsx} : Mount `ToastContainer` once in AppLayout for success/error/warning/info notifications with auto-dismiss queue
Applied to files:
web/src/pages/settings/NotificationsSection.tsx
📚 Learning: 2026-03-31T14:31:11.894Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard
Applied to files:
web/src/pages/settings/NotificationsSection.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Zustand stores for state management in the web dashboard; each domain has its own store module (auth, WebSocket, toast, analytics, setup, company, agents, budget, tasks, settings, providers, theme, per-domain stores)
Applied to files:
web/src/pages/settings/NotificationsSection.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components
Applied to files:
web/src/pages/settings/NotificationsSection.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Do NOT build card-with-header layouts from scratch -- use `<SectionCard>` from `@/components/ui/section-card`
Applied to files:
web/src/pages/settings/NotificationsSection.tsx
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/api/**/*.py : REST API: Litestar framework, controllers with guards, channels for WebSocket, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint. RFC 9457 structured errors (ErrorCategory, ErrorCode, ErrorDetail, ProblemDetail, CATEGORY_TITLES, category_title, category_type_uri, content negotiation).
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/api/**/*.py : Use Litestar for REST + WebSocket API. Controllers, guards, channels, JWT + API key + WS ticket auth, RFC 9457 structured errors.
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/api/**/*.py : Use Litestar for REST API and WebSocket API with JWT + API key + WS ticket authentication, RFC 9457 structured errors, and content negotiation.
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-26T15:18:16.848Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-26T15:18:16.848Z
Learning: Applies to src/synthorg/api/**/*.py : Litestar API must include setup wizard, auth/, auto-wiring, and lifecycle management
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/**/*.py : Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP...
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/observability/**/*.py : Observability includes structured logging via `get_logger(__name__)`, correlation tracking, and log sinks.
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/observability/**/*.py : Observability package (observability/): structured logging, correlation tracking, log sinks; event constants organized by domain under observability/events/ (e.g., events.api, events.tool, events.git, events.context_budget, events.backup)
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Every module with business logic must import `from synthorg.observability import get_logger` and define `logger = get_logger(__name__)`
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Every module with business logic MUST have: `from synthorg.observability import get_logger` then `logger = get_logger(__name__)`. Never use import logging / logging.getLogger() / print() in application code.
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-19T11:33:01.580Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T11:33:01.580Z
Learning: Applies to src/synthorg/**/*.py : Every module with business logic must import logger via `from synthorg.observability import get_logger` and initialize with `logger = get_logger(__name__)`
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/**/*.py : Every module with business logic MUST have `from synthorg.observability import get_logger` followed by `logger = get_logger(__name__)`.
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-03-16T06:24:56.341Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T06:24:56.341Z
Learning: Applies to src/synthorg/observability/**/*.py : Observability must use structured logging with correlation tracking and log sinks
Applied to files:
src/synthorg/api/app.py
📚 Learning: 2026-04-06T13:43:45.381Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T13:43:45.381Z
Learning: Applies to src/synthorg/**/*.py : Package structure follows: api/ (REST + WebSocket, RFC 9457 errors, auth/, workflows, reports), backup/ (scheduler, retention, handlers), budget/ (cost tracking, quotas, risk scoring), communication/ (message bus, channels), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task engine, workspace, workflow execution), hr/ (hiring, agent registry, performance, evaluation), memory/ (MemoryBackend, retrieval pipeline, consolidation, embedding, procedural), persistence/ (PersistenceBackend, SQLite, repositories), observability/ (logging, events, redaction, shipping), providers/ (LLM abstraction, routing, health), security/ (rule engine, audit, policy, risk scoring), templates/ (company templates, presets, packs), tools/ (registry, built-in tools, MCP, sandbox).
Applied to files:
src/synthorg/api/app.py
🔇 Additional comments (12)
web/src/pages/settings/NotificationsSection.tsx (2)
33-42: Mount-time browser permission reconciliation looks correct.The one-time sync on mount (Line 33) safely handles externally changed permission state without blocking the UI flow.
57-60: Duplicate route prevention is correctly implemented.The enabled-path guard at Line 58 prevents duplicate route entries before persisting overrides.
src/synthorg/api/app.py (7)
14-28: Imports are correctly organized for the new tiered rate-limiting and notification dispatcher features.The new imports support:
Callablefor type hints on identifier functionsRequestandget_remote_addressfrom Litestar for rate-limit key extractionbuild_notification_dispatcherfor the notification subsystemAPI_NETWORK_EXPOSURE_WARNINGevent constant for structured loggingAlso applies to: 88-88, 95-95
558-563: Notification dispatcher cleanup is correctly integrated.The shutdown hook properly:
- Checks
has_notification_dispatcherbefore attempting cleanup- Uses
_try_stop()wrapper for error-resilient cleanup- Correctly calls
close()which is the only cleanup method onNotificationDispatcher
898-922: Notification dispatcher correctly built and wired into AppState.The dispatcher is:
- Built from
effective_config.notifications(factory handles None gracefully)- Passed to
AppStateconstructor which stores it for later access- Available for services (approval gate, budget enforcer) to use
1067-1103: Proxy-aware IP extraction correctly validates peer trust before reading X-Forwarded-For.The implementation properly:
- Returns
get_remote_addressdirectly when no trusted proxies configured- Validates the immediate peer is trusted before parsing
X-Forwarded-For- Walks hops from right to find the rightmost untrusted hop
- Falls back to peer IP if all hops are trusted
This prevents header spoofing attacks from untrusted clients.
1106-1124: Auth identifier function correctly normalizes user ID to string.The function:
- Checks for user presence in scope with
hasattrguard- Converts
user.user_idtostrfor consistent key types- Falls back to client IP for unauthenticated paths
1127-1150: Auth exclude paths helper correctly ensures WS and setup endpoints are always excluded.The function:
- Uses config-provided exclude paths or sensible defaults
- Guarantees
setup_status_pathandws_pathare always in the exclusion list- Returns an immutable tuple
1153-1214: Middleware stack correctly implements the tiered rate-limiting "sandwich" pattern.The 4-layer stack ordering:
unauth_rate_limit.middleware— IP-keyed (20/min default)auth_middleware— populatesscope["user"]RequestLoggingMiddleware— request loggingauth_rate_limit.middleware— user-ID-keyed (6000/min default)This ensures unauthenticated traffic is limited before auth runs, and authenticated traffic gets its own higher-budget tier keyed by user ID rather than shared IP.
The network exposure warning at lines 1172-1181 is a helpful operational hint for proxy deployments.
tests/unit/api/test_app.py (3)
1122-1129: Basic structural test for middleware count is appropriate.
1131-1153: Store assertions correctly use exact equality.The test uses
stores[0] == "rate_limit_unauth"andstores[1] == "rate_limit_auth"which avoids the substring matching issue previously flagged.
1192-1231: Test coverage for_auth_identifier_for_requestis comprehensive.The three tests cover:
- User present → returns
user_id- No user in scope → falls back to IP
- User is
None→ falls back to IP
| <label | ||
| key={route} | ||
| className="flex items-center gap-1 text-xs text-muted-foreground" | ||
| > | ||
| <input | ||
| type="checkbox" | ||
| checked={routeEnabled(category, route, preferences.routeOverrides)} | ||
| onChange={(e) => toggleRoute(category, route, e.target.checked)} |
There was a problem hiding this comment.
Add category-specific accessible names to route checkboxes.
At Line 121, each checkbox is labeled only as drawer/toast/browser, so screen readers get repeated ambiguous controls. This blocks reliable navigation in the routing matrix.
♿ Suggested fix
{(['drawer', 'toast', 'browser'] as const).map((route) => (
<label
key={route}
className="flex items-center gap-1 text-xs text-muted-foreground"
>
<input
type="checkbox"
checked={routeEnabled(category, route, preferences.routeOverrides)}
onChange={(e) => toggleRoute(category, route, e.target.checked)}
className="accent-accent"
+ aria-label={`${CATEGORY_CONFIGS[category].label} → ${route}`}
/>
{route}
</label>
))}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <label | |
| key={route} | |
| className="flex items-center gap-1 text-xs text-muted-foreground" | |
| > | |
| <input | |
| type="checkbox" | |
| checked={routeEnabled(category, route, preferences.routeOverrides)} | |
| onChange={(e) => toggleRoute(category, route, e.target.checked)} | |
| <label | |
| key={route} | |
| className="flex items-center gap-1 text-xs text-muted-foreground" | |
| > | |
| <input | |
| type="checkbox" | |
| checked={routeEnabled(category, route, preferences.routeOverrides)} | |
| onChange={(e) => toggleRoute(category, route, e.target.checked)} | |
| aria-label={`${CATEGORY_CONFIGS[category].label} → ${route}`} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/pages/settings/NotificationsSection.tsx` around lines 117 - 124,
Checkbox inputs in NotificationsSection.tsx lack category-specific accessible
names causing ambiguous controls for screen readers; update the <input> inside
the label that currently calls routeEnabled(category, route,
preferences.routeOverrides) and toggleRoute(category, route, ...) to include a
unique accessible name (e.g., aria-label or aria-labelledby) that concatenates
the category and route (or references a visually-hidden span with the combined
text) so each checkbox is announced as "{category} {route}" instead of just
"drawer/toast/browser". Ensure the chosen attribute references the category and
route variables and remains unique per control.
…or review fixes - docs/design/operations.md: document NotificationSink as async protocol - docs/design/page-structure.md: remove 'future iteration' note for routing overrides - Slack adapter: escape &, <, > in mrkdwn to prevent mention injection - NotificationItemCard: add aria-label on icon-only action buttons - AppLayout: command palette uses open (not toggle) for notifications - NotificationDrawer stories: clearAll before seeding - agents.ts: early return for personality.trimmed (no unhandled log) - browser-notifications: recordNotification after Notification creation - factory.py: reorder imports (events before TYPE_CHECKING) - settings definition: align auth_max_requests default to 6000
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (3)
web/src/components/notifications/NotificationItemCard.tsx (1)
62-117:⚠️ Potential issue | 🟠 MajorAvoid nested buttons and make actions keyboard-discoverable.
Line 62 uses an outer
<button>and Lines 91-115 add nested<button>controls, which is invalid interactive nesting and causes inconsistent keyboard/screen-reader behavior. Also, Line 89 reveals actions only on hover; add keyboard-focus visibility too.♿ Suggested structure update
- <button - type="button" - role="listitem" + <li + role="listitem" aria-label={`${item.severity} notification: ${item.title}`} className={cn( 'group relative flex w-full gap-3 rounded-md border-l-2 px-3 py-2 text-left', 'transition-colors hover:bg-card-hover', item.read ? 'border-l-transparent' : BORDER_COLORS[item.severity], !item.read && 'bg-accent/5', - item.href && 'cursor-pointer', )} - onClick={handleClick} > - <Icon className={cn('mt-0.5 size-4 shrink-0', SEVERITY_COLORS[item.severity])} /> + <button + type="button" + className={cn('flex min-w-0 flex-1 items-start gap-3 text-left', item.href && 'cursor-pointer')} + onClick={handleClick} + > + <Icon className={cn('mt-0.5 size-4 shrink-0', SEVERITY_COLORS[item.severity])} /> - <div className="min-w-0 flex-1"> - <p className="truncate text-sm font-medium text-foreground">{item.title}</p> - {item.description && ( - <p className="mt-0.5 truncate text-xs text-muted-foreground"> - {item.description} + <div className="min-w-0 flex-1"> + <p className="truncate text-sm font-medium text-foreground">{item.title}</p> + {item.description && ( + <p className="mt-0.5 truncate text-xs text-muted-foreground"> + {item.description} + </p> + )} + <p className="mt-1 text-xs text-muted-foreground/70"> + {formatRelativeTime(item.timestamp)} </p> - )} - <p className="mt-1 text-xs text-muted-foreground/70"> - {formatRelativeTime(item.timestamp)} - </p> - </div> + </div> + </button> - <div className="flex shrink-0 items-start gap-1 opacity-0 transition-opacity group-hover:opacity-100"> + <div className="flex shrink-0 items-start gap-1 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100"> ... </div> - </button> + </li>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/components/notifications/NotificationItemCard.tsx` around lines 62 - 117, The outer interactive element should not be a <button> because it contains nested action <button>s; change the outer element in NotificationItemCard (the element using handleClick and aria-label) to a non-button container (e.g., <div role="listitem" tabIndex={0} aria-label={...} className="group ..." ) and keep onClick on it, then add an onKeyDown handler that triggers handleClick when Enter or Space is pressed so keyboard users can activate the item; keep inner action handlers (onMarkRead, onDismiss) with e.stopPropagation() unchanged. Also make the action button group visible on keyboard focus by replacing the visibility classes that only target hover (group-hover:opacity-100) with combined focus/hover rules (e.g., group-hover:opacity-100 group-focus-within:opacity-100) on the container so the existing inner buttons (Check, X) become discoverable via keyboard; keep using SEVERITY_COLORS, BORDER_COLORS, and formatRelativeTime as before.docs/design/operations.md (1)
1292-1292:⚠️ Potential issue | 🟡 MinorThe authenticated tier value should be 600 req/min, not 6,000.
The PR summary and linked issue
#1077specify600 req/minfor authenticated traffic, but this line documents6,000 req/min.✏️ Suggested correction
-| `/api/v1/auth` | Authentication: setup, login, password change, ws-ticket, session management (list/revoke), logout (tiered rate limiting: 20 req/min unauth by IP, 6,000 req/min auth by user ID -- see `docs/security.md`) | +| `/api/v1/auth` | Authentication: setup, login, password change, ws-ticket, session management (list/revoke), logout (tiered rate limiting: 20 req/min unauth by IP, 600 req/min auth by user ID -- see `docs/security.md`) |🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/design/operations.md` at line 1292, Update the rate limit for authenticated traffic in the `/api/v1/auth` documentation entry: replace the incorrect "6,000 req/min auth by user ID" with the correct "600 req/min auth by user ID" so the line for `/api/v1/auth` matches the PR summary and issue `#1077`.src/synthorg/notifications/adapters/slack.py (1)
95-103:⚠️ Potential issue | 🟠 MajorLogging
str(exc)leaks the Slack webhook URL into logs.When
response.raise_for_status()fails,str(httpx.HTTPStatusError)includes the request URL in the formatted message. This exposes the webhook credential in logs.🔒 Proposed fix to avoid credential leakage
except MemoryError, RecursionError: raise except Exception as exc: logger.warning( NOTIFICATION_SLACK_FAILED, notification_id=notification.id, - error=str(exc), + error_type=type(exc).__name__, + status_code=getattr( + getattr(exc, "response", None), + "status_code", + None, + ), ) raise🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/notifications/adapters/slack.py` around lines 95 - 103, The current logger.warning call in the Slack adapter logs str(exc), which can include the webhook URL; change the exception handling in the except Exception as exc block in src/synthorg/notifications/adapters/slack.py (the logger.warning call tied to NOTIFICATION_SLACK_FAILED and notification.id) to avoid logging the raw exception string. Specifically, detect httpx.HTTPStatusError (or check for exc.response/ exc.request) and log only safe fields such as exc.__class__.__name__, exc.response.status_code (or exc.response.reason_phrase) and a redacted or masked indicator for the URL (do not include exc.request.url or str(exc)); for other exceptions log the exception class name and a brief message without the full exception text. Ensure the logger.warning call retains notification_id and error fields but replace error=str(exc) with a sanitized error payload that cannot leak the webhook URL.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/design/page-structure.md`:
- Around line 257-280: Update the notification taxonomy table and descriptive
paragraph to match the implementation in CATEGORY_CONFIGS
(web/src/types/notifications.ts): change all uses of the word "panel" to
"drawer" in the paragraph and table, set approvals.pending severity to "warning"
and its Default Route to include "toast + drawer + browser", set
agents.personality_trimmed Default Route to "toast only", and set agents.fired
severity to "info" with Default Route "drawer only"; ensure the table rows for
these three categories and the routing description text are updated
consistently.
In `@src/synthorg/settings/definitions/api.py`:
- Around line 161-170: The default for the setting with key
"rate_limit_auth_max_requests" is incorrectly set to the string "6000"; change
it to the integer 600 (default=600) so the authenticated rate limit matches the
intended 600 requests/minute, and ensure this value stays in sync with the API
rate-limit configuration that uses the auth_max_requests constant.
In `@web/src/components/layout/AppLayout.tsx`:
- Around line 107-110: The handler handleNav assumes detail.href is a string and
may throw when it's missing or non-string; update the guard to first check that
detail?.href is a non-empty string (e.g. typeof detail.href === 'string' &&
detail.href) before calling startsWith, and only call navigate(detail.href) when
that check passes; reference the handleNav function and the CustomEvent<{ href:
string }>.detail.href in your change.
- Around line 92-99: The global keyboard handler in handleKeyDown currently
ignores only INPUT, TEXTAREA and contentEditable, so Shift+N still fires when a
SELECT or combobox has focus; update handleKeyDown to also return early when the
focused element is a SELECT or an element with role="combobox" (or a descendant
of one). Specifically, in function handleKeyDown add checks for tag === 'SELECT'
and for role 'combobox' (e.g., via (e.target as
HTMLElement).getAttribute('role') === 'combobox' or using
closest('[role="combobox"]')) before handling Shift+N so form controls retain
their native keyboard behavior.
In `@web/src/components/notifications/NotificationDrawer.stories.tsx`:
- Around line 19-22: The story currently uses a no-op onClose callback
(args.onClose = () => {}) which suppresses Action panel events; replace that
with a Storybook action stub by importing fn from "storybook/test" (or
"storybook/actions" per project conventions) and set args.onClose = fn(),
ensuring you add the import at the top of NotificationDrawer.stories.tsx and use
the exported fn() in the args for the NotificationDrawer story so the Actions
panel records interactions.
- Around line 31-64: The Story currently seeds and clears the notifications
store inside the SeedNotifications component's useEffect (using
useNotificationsStore.getState().clearAll and enqueue), which runs after first
paint and causes flicker; move this pre-render setup into a Storybook loader (or
a beforeEach-like pre-render hook) so the store is cleared and notifications
enqueued before the component mounts—invoke
useNotificationsStore.getState().clearAll() and then call enqueue(...) for each
notification inside the loader, and remove the useEffect from SeedNotifications
so the initial render reflects the seeded state deterministically.
In `@web/src/components/notifications/NotificationItemCard.tsx`:
- Around line 28-37: The formatRelativeTime function can produce "NaNd ago" when
new Date(timestamp).getTime() is NaN; update formatRelativeTime to validate the
parsed date (e.g., const ts = new Date(timestamp).getTime(); if
(Number.isNaN(ts)) return 'just now' or a safe fallback) before computing diff,
and also guard against negative diffs (future timestamps) by treating them as
'just now' or clamping to zero; modify the function signature/logic in
formatRelativeTime to parse once, check Number.isNaN(ts), compute diff =
Date.now() - ts, then continue with existing minute/hour/day formatting.
---
Duplicate comments:
In `@docs/design/operations.md`:
- Line 1292: Update the rate limit for authenticated traffic in the
`/api/v1/auth` documentation entry: replace the incorrect "6,000 req/min auth by
user ID" with the correct "600 req/min auth by user ID" so the line for
`/api/v1/auth` matches the PR summary and issue `#1077`.
In `@src/synthorg/notifications/adapters/slack.py`:
- Around line 95-103: The current logger.warning call in the Slack adapter logs
str(exc), which can include the webhook URL; change the exception handling in
the except Exception as exc block in
src/synthorg/notifications/adapters/slack.py (the logger.warning call tied to
NOTIFICATION_SLACK_FAILED and notification.id) to avoid logging the raw
exception string. Specifically, detect httpx.HTTPStatusError (or check for
exc.response/ exc.request) and log only safe fields such as
exc.__class__.__name__, exc.response.status_code (or exc.response.reason_phrase)
and a redacted or masked indicator for the URL (do not include exc.request.url
or str(exc)); for other exceptions log the exception class name and a brief
message without the full exception text. Ensure the logger.warning call retains
notification_id and error fields but replace error=str(exc) with a sanitized
error payload that cannot leak the webhook URL.
In `@web/src/components/notifications/NotificationItemCard.tsx`:
- Around line 62-117: The outer interactive element should not be a <button>
because it contains nested action <button>s; change the outer element in
NotificationItemCard (the element using handleClick and aria-label) to a
non-button container (e.g., <div role="listitem" tabIndex={0} aria-label={...}
className="group ..." ) and keep onClick on it, then add an onKeyDown handler
that triggers handleClick when Enter or Space is pressed so keyboard users can
activate the item; keep inner action handlers (onMarkRead, onDismiss) with
e.stopPropagation() unchanged. Also make the action button group visible on
keyboard focus by replacing the visibility classes that only target hover
(group-hover:opacity-100) with combined focus/hover rules (e.g.,
group-hover:opacity-100 group-focus-within:opacity-100) on the container so the
existing inner buttons (Check, X) become discoverable via keyboard; keep using
SEVERITY_COLORS, BORDER_COLORS, and formatRelativeTime as before.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 14102fdf-b2e0-4e01-922f-7ed4fa5ae85d
📒 Files selected for processing (10)
docs/design/operations.mddocs/design/page-structure.mdsrc/synthorg/notifications/adapters/slack.pysrc/synthorg/notifications/factory.pysrc/synthorg/settings/definitions/api.pyweb/src/components/layout/AppLayout.tsxweb/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationItemCard.tsxweb/src/services/browser-notifications.tsweb/src/stores/agents.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: Dashboard Test
- GitHub Check: Test (Python 3.14)
- GitHub Check: Build Backend
- GitHub Check: Build Web
- GitHub Check: Build Sandbox
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.md
📄 CodeRabbit inference engine (CLAUDE.md)
ALWAYS read the relevant
docs/design/page before implementing any feature or planning any issue. Design Spec (DESIGN_SPEC.md) is a pointer file linking to the 12 design pages. The design spec is the starting point for architecture, data models, and behavior.
Files:
docs/design/page-structure.mddocs/design/operations.md
web/src/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx,js,jsx}: Always usecreateLoggerfrom@/lib/logger-- never bareconsole.warn/console.error/console.debugin application code
Logger variable name must always beconst log(e.g.const log = createLogger('module-name'))
Pass dynamic/untrusted values as separate arguments to logger methods (not interpolated into the message string) so they go throughsanitizeArg
Attacker-controlled fields inside structured objects must be wrapped insanitizeForLog()before embedding in log calls
Files:
web/src/stores/agents.tsweb/src/components/layout/AppLayout.tsxweb/src/components/notifications/NotificationDrawer.stories.tsxweb/src/services/browser-notifications.tsweb/src/components/notifications/NotificationItemCard.tsx
web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground,bg-card,text-accent,text-success,bg-danger, etc.) or CSS variables (var(--so-*)) for colors; NEVER hardcode hex values in.tsx/.tsfiles
Usefont-sansorfont-mono(Geist tokens) for typography; NEVER setfontFamilydirectly in.tsx/.tsfiles
Use density-aware tokens (p-card,gap-section-gap,gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing in components
Use token variables (var(--so-shadow-card-hover),border-border,border-bright) for shadows and borders; NEVER hardcode values in.tsx/.tsfiles
Use@/lib/motionpresets for Framer Motion transition durations; NEVER hardcode transition durations
CSS side-effect imports in TypeScript 6 require type declarations -- add/// <reference types="vite/client" />at the top of files with CSS importsALWAYS reuse existing components from web/src/components/ui/ before creating new ones. NEVER hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions - use design tokens and
@/lib/motionpresets. A PostToolUse hook (scripts/check_web_design_system.py) enforces these rules on every Edit/Write to web/src/.
Files:
web/src/stores/agents.tsweb/src/components/layout/AppLayout.tsxweb/src/components/notifications/NotificationDrawer.stories.tsxweb/src/services/browser-notifications.tsweb/src/components/notifications/NotificationItemCard.tsx
web/src/components/layout/**/*.{ts,tsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/components/layout/**/*.{ts,tsx}: MountToastContaineronce in AppLayout for success/error/warning/info notifications with auto-dismiss queue
MountCommandPaletteonce in AppLayout; register commands viauseCommandPalettehook
Files:
web/src/components/layout/AppLayout.tsx
web/src/**/*.stories.tsx
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.stories.tsx: For Storybook stories withtags: ['autodocs'], ensure@storybook/addon-docsis installed and added to addons
Usestorybook/testandstorybook/actionsimport paths in Storybook stories (not@storybook/testor@storybook/addon-actions)
Files:
web/src/components/notifications/NotificationDrawer.stories.tsx
src/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
src/**/*.py: Nofrom __future__ import annotations- Python 3.14 has PEP 649 native lazy annotations.
Use PEP 758 except syntax: useexcept A, B:(no parentheses) - ruff enforces this on Python 3.14.
Type hints required on all public functions, with mypy strict mode enforcement.
Docstrings required on all public classes and functions using Google style - enforced by ruff D rules.
Immutability: create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement. For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries.
Config vs runtime state: use frozen Pydantic models for config/identity; separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Models: use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). Adopted conventions: use allow_inf_nan=False in all ConfigDict declarations; use@computed_fieldfor derived values instead of storing + validating redundant fields; use NotBlankStr (from core.types) for all identifier/name fields.
Async concurrency: prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare create_task.
Line length: 88 characters (ruff enforced).
Functions must be < 50 lines, files must be < 800 lines.
Handle errors explicitly, never silently swallow exceptions.
Validate at system boundaries (user input, external APIs, config files).
Every module with business logic MUST have:from synthorg.observability import get_loggerthenlogger = get_logger(__name__). Variable name must always belogger(not_logger, notlog).
Never useimport logging/logging.getLogger()/ `print()...
Files:
src/synthorg/settings/definitions/api.pysrc/synthorg/notifications/adapters/slack.pysrc/synthorg/notifications/factory.py
⚙️ CodeRabbit configuration file
This project uses Python 3.14+ with PEP 758 except syntax: "except A, B:" (comma-separated, no parentheses) is correct and mandatory -- do NOT flag it as a typo or suggest parenthesized form. The "except builtins.MemoryError, RecursionError: raise" pattern is intentional project convention for system-error propagation. When evaluating the 50-line function limit, count only the function body excluding the signature lines, decorators, and docstring. Functions 1-5 lines over due to docstrings or multi-line signatures should not be flagged. Do not suggest extracting single-use helper functions called exactly once -- this reduces readability without improving maintainability.
Files:
src/synthorg/settings/definitions/api.pysrc/synthorg/notifications/adapters/slack.pysrc/synthorg/notifications/factory.py
src/synthorg/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
Package structure follows: api/ (REST + WebSocket, RFC 9457 errors, auth/, workflows, reports), backup/ (scheduler, retention, handlers), budget/ (cost tracking, quotas, risk scoring), communication/ (message bus, channels), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task engine, workspace, workflow execution), hr/ (hiring, agent registry, performance, evaluation), memory/ (MemoryBackend, retrieval pipeline, consolidation, embedding, procedural), persistence/ (PersistenceBackend, SQLite, repositories), observability/ (logging, events, redaction, shipping), providers/ (LLM abstraction, routing, health), security/ (rule engine, audit, policy, risk scoring), templates/ (company templates, presets, packs), tools/ (registry, built-in tools, MCP, sandbox).
Files:
src/synthorg/settings/definitions/api.pysrc/synthorg/notifications/adapters/slack.pysrc/synthorg/notifications/factory.py
🧠 Learnings (59)
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/stores/**/*.{ts,tsx} : Use Zustand stores in web dashboard for state management (auth, WebSocket, toast, analytics, domain shells)
Applied to files:
web/src/stores/agents.tsweb/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Zustand stores for state management in the web dashboard; each domain has its own store module (auth, WebSocket, toast, analytics, setup, company, agents, budget, tasks, settings, providers, theme, per-domain stores)
Applied to files:
web/src/stores/agents.tsweb/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/layout/**/*.{ts,tsx} : Mount `ToastContainer` once in AppLayout for success/error/warning/info notifications with auto-dismiss queue
Applied to files:
web/src/components/layout/AppLayout.tsxweb/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/layout/**/*.{ts,tsx} : Mount `CommandPalette` once in AppLayout; register commands via `useCommandPalette` hook
Applied to files:
web/src/components/layout/AppLayout.tsx
📚 Learning: 2026-03-31T14:31:11.894Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard
Applied to files:
web/src/components/layout/AppLayout.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components
Applied to files:
web/src/components/layout/AppLayout.tsxweb/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-06T13:43:45.381Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T13:43:45.381Z
Learning: Applies to web/package.json : Web dashboard: Node.js 22+, TypeScript 6.0+. Key dependencies: React 19, react-router, shadcn/ui, Base UI, Tailwind CSS 4, Zustand, tanstack/react-query, xyflow/react, dagrejs/dagre, d3-force, dnd-kit, Recharts, Framer Motion, cmdk-base, js-yaml, Axios, Lucide React, Storybook 10, Vitest, Playwright, fast-check.
Applied to files:
web/src/components/layout/AppLayout.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem) before creating new ones
Applied to files:
web/src/components/layout/AppLayout.tsxweb/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)
Applied to files:
web/src/components/layout/AppLayout.tsxweb/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : For Base UI Dialog, AlertDialog, and Popover components, compose with `Portal` + `Backdrop` + `Popup`. Popover and Menu additionally require a `Positioner` wrapper with `side` / `align` / `sideOffset` props
Applied to files:
web/src/components/layout/AppLayout.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/ui/**/*.stories.tsx : Create a `.stories.tsx` file alongside each new shared component with all states (default, hover, loading, error, empty)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Create new shared components in `web/src/components/ui/` with `.stories.tsx` Storybook file covering all states (default, hover, loading, error, empty)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10: import from `storybook/test` (not `storybook/test`), `storybook/actions` (not `storybook/addon-actions`)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/**/*.stories.{ts,tsx} : Storybook 10: Use storybook/test (not storybook/test) and storybook/actions (not storybook/addon-actions) import paths
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10: Import from `storybook/test` instead of `storybook/test`
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/**/*.stories.tsx : Use `storybook/test` and `storybook/actions` import paths in Storybook stories (not `storybook/test` or `storybook/addon-actions`)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : When creating new shared web components, place in web/src/components/ui/ with kebab-case filename, create .stories.tsx alongside with all states (default, hover, loading, error, empty), export props as TypeScript interface, use design tokens exclusively with no hardcoded colors/fonts/spacing, and import cn from `@/lib/utils` for conditional class merging
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/**/*.stories.{ts,tsx} : Storybook 10: Use parameters.backgrounds.options (object keyed by name) + initialGlobals.backgrounds.value for background options (replaces old default + values array)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10: Use `parameters.a11y.test: 'error' | 'todo' | 'off'` for a11y testing configuration (replaces old `.element` and `.manual`)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Documentation source in `docs/` (Markdown, built with Zensical). Design spec in `docs/design/` (7 pages: index, agents, organization, communication, engine, memory, operations). Architecture in `docs/architecture/` (overview, tech-stack, decision log). Roadmap in `docs/roadmap/`. Security in `docs/security.md`. Licensing in `docs/licensing.md`. Reference in `docs/reference/`. REST API reference in `docs/rest-api.md`. Library reference in `docs/api/` (auto-generated from docstrings). Custom templates in `docs/overrides/`. Config in `mkdocs.yml`.
Applied to files:
docs/design/operations.md
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/observability/**/*.py : Observability includes structured logging via `get_logger(__name__)`, correlation tracking, and log sinks.
Applied to files:
docs/design/operations.md
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/observability/**/*.py : Observability package (observability/): structured logging, correlation tracking, log sinks; event constants organized by domain under observability/events/ (e.g., events.api, events.tool, events.git, events.context_budget, events.backup)
Applied to files:
docs/design/operations.mdsrc/synthorg/notifications/factory.py
📚 Learning: 2026-03-16T06:24:56.341Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T06:24:56.341Z
Learning: Applies to src/synthorg/observability/**/*.py : Observability must use structured logging with correlation tracking and log sinks
Applied to files:
docs/design/operations.md
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)
Applied to files:
src/synthorg/settings/definitions/api.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.
Applied to files:
src/synthorg/settings/definitions/api.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).
Applied to files:
src/synthorg/settings/definitions/api.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers
Applied to files:
src/synthorg/settings/definitions/api.py
📚 Learning: 2026-03-31T21:07:37.470Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T21:07:37.470Z
Learning: Applies to **/*.py : Use `except A, B:` (no parentheses) per PEP 758 exception syntax on Python 3.14
Applied to files:
src/synthorg/notifications/adapters/slack.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to **/*.py : Use `except A, B:` syntax (without parentheses) per PEP 758 for exception handling in Python 3.14
Applied to files:
src/synthorg/notifications/adapters/slack.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Use `except A, B:` syntax (no parentheses) for exception handling — PEP 758 exception syntax enforced by ruff on Python 3.14
Applied to files:
src/synthorg/notifications/adapters/slack.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to **/*.py : Use PEP 758 except syntax with `except A, B:` (no parentheses) for multiple exceptions—ruff enforces this on Python 3.14.
Applied to files:
src/synthorg/notifications/adapters/slack.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: `except A, B:` (no parentheses) — enforced by ruff on Python 3.14
Applied to files:
src/synthorg/notifications/adapters/slack.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Handle errors explicitly; never silently swallow exceptions
Applied to files:
src/synthorg/notifications/adapters/slack.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to **/*.py : Handle errors explicitly, never silently swallow exceptions
Applied to files:
src/synthorg/notifications/adapters/slack.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to **/*.py : Handle errors explicitly—never silently swallow exceptions.
Applied to files:
src/synthorg/notifications/adapters/slack.py
📚 Learning: 2026-03-15T16:55:07.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T16:55:07.730Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: use `except A, B:` (no parentheses) — ruff enforces this on Python 3.14.
Applied to files:
src/synthorg/notifications/adapters/slack.py
📚 Learning: 2026-04-06T13:43:45.380Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T13:43:45.380Z
Learning: Applies to src/**/*.py : Use PEP 758 except syntax: use `except A, B:` (no parentheses) - ruff enforces this on Python 3.14.
Applied to files:
src/synthorg/notifications/adapters/slack.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to src/ai_company/!(observability)/**/*.py : All error paths must log at WARNING or ERROR with context before raising.
Applied to files:
src/synthorg/notifications/adapters/slack.py
📚 Learning: 2026-03-15T16:55:07.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T16:55:07.730Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising.
Applied to files:
src/synthorg/notifications/adapters/slack.py
📚 Learning: 2026-04-06T13:43:45.381Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T13:43:45.381Z
Learning: Applies to src/**/*.py : Event names must always use constants from domain-specific modules under synthorg.observability.events (e.g., API_REQUEST_STARTED from events.api, TOOL_INVOKE_START from events.tool). Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`
Applied to files:
src/synthorg/notifications/factory.py
📚 Learning: 2026-03-18T21:23:23.586Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T21:23:23.586Z
Learning: Applies to src/synthorg/**/*.py : Event names: always use constants from the domain-specific module under synthorg.observability.events (e.g., API_REQUEST_STARTED from events.api, TOOL_INVOKE_START from events.tool). Import directly from synthorg.observability.events.<domain>.
Applied to files:
src/synthorg/notifications/factory.py
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Always use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `PROVIDER_CALL_START` from `events.provider`); import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`
Applied to files:
src/synthorg/notifications/factory.py
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to src/synthorg/**/*.py : Event names: always use constants from domain-specific modules under synthorg.observability.events (e.g., PROVIDER_CALL_START from events.provider, BUDGET_RECORD_ADDED from events.budget, etc.). Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`.
Applied to files:
src/synthorg/notifications/factory.py
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`). Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`.
Applied to files:
src/synthorg/notifications/factory.py
📚 Learning: 2026-03-31T16:09:24.320Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T16:09:24.320Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from `synthorg.observability.events.<domain>` modules (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`); import directly and use in structured logging
Applied to files:
src/synthorg/notifications/factory.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Always use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`); import directly rather than using string literals
Applied to files:
src/synthorg/notifications/factory.py
📚 Learning: 2026-04-02T07:18:02.381Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T07:18:02.381Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`); import directly from the domain module
Applied to files:
src/synthorg/notifications/factory.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from synthorg.observability.events domain-specific modules (e.g., PROVIDER_CALL_START from events.provider). Import directly: from synthorg.observability.events.<domain> import EVENT_CONSTANT.
Applied to files:
src/synthorg/notifications/factory.py
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT build card-with-header layouts from scratch; use `<SectionCard>`
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside .map()—extract to a shared component
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside `.map()`; extract to a shared component
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones; refer to design system inventory (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, etc.)
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : For Tabs Base UI components, use `data-[active]` attribute selector (not `data-[state=active]`) for animation state styling
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/**/*.stories.{ts,tsx} : Storybook 10: Use parameters.a11y.test: 'error' | 'todo' | 'off' for a11y testing (replaces old .element and .manual); set globally in preview.tsx to enforce WCAG compliance on all stories
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/.storybook/**/*.{ts,tsx} : In Storybook 10, use `parameters.a11y.test: 'error' | 'todo' | 'off'` for accessibility testing (replaces old `.element` and `.manual`); set globally in `preview.tsx` to enforce WCAG compliance
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Do NOT build card-with-header layouts from scratch -- use `<SectionCard>` from `@/components/ui/section-card`
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
🔇 Additional comments (21)
web/src/stores/agents.ts (1)
269-271: Good early-return for knownpersonality.trimmedevents.Line 271 correctly prevents this known event from falling through to the generic unhandled-event debug log, keeping WS diagnostics clean while notifications are handled in the unified pipeline.
web/src/components/notifications/NotificationItemCard.tsx (1)
54-58: Good guard on navigation target.Nice check to allow only app-relative paths before calling
navigate, which avoids protocol-relative redirects.src/synthorg/notifications/adapters/slack.py (4)
1-17: LGTM on module setup and imports.The module structure follows project conventions: proper imports, logger setup with
get_logger(__name__), and event constants from the domain-specific events module.
20-50: Good implementation of mrkdwn escaping and payload construction.The
_escape_mrkdwnhelper correctly escapes&,<, and>to prevent Slack mention injection (e.g.,<!channel>). The payload builder properly applies escaping to all untrusted fields (title,body,category,source) before interpolating into Block Kit mrkdwn text objects.
53-77: LGTM on sink class structure.The class correctly validates the webhook URL against SSRF at construction time, uses
__slots__for memory efficiency, and configures the httpx client with appropriate timeout and redirect settings.
105-107: LGTM on close method.Properly delegates to the async client's
aclose()method.src/synthorg/notifications/factory.py (6)
1-28: LGTM on module setup and imports.Import ordering is correct: stdlib → local imports → TYPE_CHECKING block → logger initialization. Event constants are properly imported from the domain-specific events module.
30-60: LGTM onbuild_notification_dispatcher.The factory correctly:
- Iterates configured sinks and logs disabled ones at DEBUG level
- Collects successfully created sinks
- Falls back to a console sink when no sinks are available
- Passes through the
min_severityfilter from config
63-91: LGTM on_create_notification_sink.The dispatcher correctly handles all known sink types and logs a warning for unknown types as a forward-compatibility safeguard. The
# type: ignore[unreachable]comment is appropriate since the enum exhaustiveness makes this path theoretically unreachable.
94-126: LGTM on ntfy sink creation.Good validation requiring
topic(preventing accidental use of public ntfy.sh topics), sensible default forserver_url, and optionaltokensupport.
129-152: LGTM on Slack sink creation.Proper validation requiring
webhook_urlwith appropriate error logging when missing.
155-206: LGTM on email sink creation with thorough validation.The function validates:
- Required
hostparameter- Non-empty
to_addrsafter trimming whitespace- Valid integer
portwith sensible default (587)The
use_tlsstring-to-bool conversion ("true".lower() == "true") is a reasonable pattern for YAML/env-sourced string params.docs/design/operations.md (3)
1692-1722: LGTM on the Notifications section overview.The documentation correctly describes:
- The async
NotificationSinkprotocol withasync send()andasync close()methods- The
NotificationDispatcherfan-out behavior usingasyncio.TaskGroup- Failure isolation and severity-based filtering semantics
1723-1751: LGTM on adapter and integration point documentation.The adapter table accurately reflects the four built-in adapters and their required configuration. The integration points section correctly identifies the three backend services that emit notifications.
1752-1786: LGTM on configuration documentation.The YAML example is accurate and demonstrates all sink types with appropriate params. The fallback behavior note correctly describes the factory's console sink fallback logic.
web/src/components/notifications/NotificationDrawer.stories.tsx (1)
9-17: Solid Storybook scaffold for routing-dependent UI.
meta+ globalMemoryRouterdecorator is clean and keeps story setup centralized.src/synthorg/settings/definitions/api.py (3)
3-7: LGTM.The updated totals and bootstrap/runtime split now match the definitions below.
145-155: LGTM.This unauthenticated tier is restart-gated and validated with explicit bounds.
177-185: LGTM.Marking
rate_limit_time_unitasrestart_requirednow matches the fact that the middleware window is fixed at app construction.web/src/components/layout/AppLayout.tsx (1)
201-204: Good place to mount the app-wide notification surfaces.Keeping
NotificationDrawerinAppLayoutnext to the singleToastContainerandCommandPalettepreserves their global lifecycle across route changes.Based on learnings: "Mount
ToastContaineronce in AppLayout for success/error/warning/info notifications with auto-dismiss queue" and "MountCommandPaletteonce in AppLayout; register commands viauseCommandPalettehook".web/src/services/browser-notifications.ts (1)
63-83: Solid guard sequencing aroundnew Notification(...).Support, permission, visibility, and rate-limit checks all run before the browser API call, and the rate-limit slot is only recorded after construction succeeds.
| key="rate_limit_auth_max_requests", | ||
| type=SettingType.INTEGER, | ||
| default="6000", | ||
| description="Maximum authenticated requests per time window (by user ID)", | ||
| group="Rate Limiting", | ||
| level=SettingLevel.ADVANCED, | ||
| restart_required=True, | ||
| min_value=1, | ||
| max_value=100000, | ||
| yaml_path="api.rate_limit.auth_max_requests", |
There was a problem hiding this comment.
The authenticated default is 10× higher than the stated target.
The linked objective and PR summary call for 600 requests/minute for authenticated traffic, but this definition now registers 6000. Unless that requirement changed, this ships a much looser throttle than intended and should be reverted here and kept in sync with src/synthorg/api/config.py:79-109.
🔧 Proposed fix
- default="6000",
+ default="600",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| key="rate_limit_auth_max_requests", | |
| type=SettingType.INTEGER, | |
| default="6000", | |
| description="Maximum authenticated requests per time window (by user ID)", | |
| group="Rate Limiting", | |
| level=SettingLevel.ADVANCED, | |
| restart_required=True, | |
| min_value=1, | |
| max_value=100000, | |
| yaml_path="api.rate_limit.auth_max_requests", | |
| key="rate_limit_auth_max_requests", | |
| type=SettingType.INTEGER, | |
| default="600", | |
| description="Maximum authenticated requests per time window (by user ID)", | |
| group="Rate Limiting", | |
| level=SettingLevel.ADVANCED, | |
| restart_required=True, | |
| min_value=1, | |
| max_value=100000, | |
| yaml_path="api.rate_limit.auth_max_requests", |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/settings/definitions/api.py` around lines 161 - 170, The default
for the setting with key "rate_limit_auth_max_requests" is incorrectly set to
the string "6000"; change it to the integer 600 (default=600) so the
authenticated rate limit matches the intended 600 requests/minute, and ensure
this value stays in sync with the API rate-limit configuration that uses the
auth_max_requests constant.
| useEffect(() => { | ||
| useNotificationsStore.getState().clearAll() | ||
| }, []) | ||
| return <Story /> | ||
| }, | ||
| ], | ||
| } | ||
|
|
||
| function SeedNotifications({ children }: { readonly children: React.ReactNode }) { | ||
| useEffect(() => { | ||
| const { clearAll, enqueue } = useNotificationsStore.getState() | ||
| clearAll() | ||
| enqueue({ | ||
| category: 'approvals.pending', | ||
| title: 'Approval requested for agent deployment', | ||
| description: 'Engineering department needs approval', | ||
| href: '/approvals', | ||
| }) | ||
| enqueue({ | ||
| category: 'budget.threshold', | ||
| title: 'Budget threshold crossed', | ||
| description: 'Monthly spend reached 80% of limit', | ||
| severity: 'warning', | ||
| }) | ||
| enqueue({ | ||
| category: 'tasks.failed', | ||
| title: 'Task failed: Code review', | ||
| severity: 'error', | ||
| }) | ||
| enqueue({ | ||
| category: 'agents.hired', | ||
| title: 'Agent hired: Marketing Writer', | ||
| }) | ||
| }, []) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Seed/clear store before render to avoid post-mount state flicker.
Using useEffect for story state setup applies mutations after first paint. That can briefly render stale/empty state and make visual snapshots less deterministic. Prefer Storybook loaders (or beforeEach) for pre-render setup.
♻️ Suggested patch
-import { useEffect } from 'react'
import { MemoryRouter } from 'react-router'
@@
export const Empty: Story = {
- decorators: [
- (Story) => {
- useEffect(() => {
- useNotificationsStore.getState().clearAll()
- }, [])
- return <Story />
- },
- ],
+ loaders: [
+ async () => {
+ useNotificationsStore.getState().clearAll()
+ return {}
+ },
+ ],
}
-function SeedNotifications({ children }: { readonly children: React.ReactNode }) {
- useEffect(() => {
- const { clearAll, enqueue } = useNotificationsStore.getState()
- clearAll()
- enqueue({
- category: 'approvals.pending',
- title: 'Approval requested for agent deployment',
- description: 'Engineering department needs approval',
- href: '/approvals',
- })
- enqueue({
- category: 'budget.threshold',
- title: 'Budget threshold crossed',
- description: 'Monthly spend reached 80% of limit',
- severity: 'warning',
- })
- enqueue({
- category: 'tasks.failed',
- title: 'Task failed: Code review',
- severity: 'error',
- })
- enqueue({
- category: 'agents.hired',
- title: 'Agent hired: Marketing Writer',
- })
- }, [])
- return <>{children}</>
-}
-
export const WithItems: Story = {
- decorators: [
- (Story) => (
- <SeedNotifications>
- <Story />
- </SeedNotifications>
- ),
+ loaders: [
+ async () => {
+ const { clearAll, enqueue } = useNotificationsStore.getState()
+ clearAll()
+ enqueue({
+ category: 'approvals.pending',
+ title: 'Approval requested for agent deployment',
+ description: 'Engineering department needs approval',
+ href: '/approvals',
+ })
+ enqueue({
+ category: 'budget.threshold',
+ title: 'Budget threshold crossed',
+ description: 'Monthly spend reached 80% of limit',
+ severity: 'warning',
+ })
+ enqueue({
+ category: 'tasks.failed',
+ title: 'Task failed: Code review',
+ severity: 'error',
+ })
+ enqueue({
+ category: 'agents.hired',
+ title: 'Agent hired: Marketing Writer',
+ })
+ return {}
+ },
],
args: {
open: true,
},
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/components/notifications/NotificationDrawer.stories.tsx` around lines
31 - 64, The Story currently seeds and clears the notifications store inside the
SeedNotifications component's useEffect (using
useNotificationsStore.getState().clearAll and enqueue), which runs after first
paint and causes flicker; move this pre-render setup into a Storybook loader (or
a beforeEach-like pre-render hook) so the store is cleared and notifications
enqueued before the component mounts—invoke
useNotificationsStore.getState().clearAll() and then call enqueue(...) for each
notification inside the loader, and remove the useEffect from SeedNotifications
so the initial render reflects the seeded state deterministically.
…uards, a11y polish - page-structure.md: fix all 17 category routes/severities to match CATEGORY_CONFIGS, replace 'panel' with 'drawer' throughout - AppLayout: typeof guard on detail.href, exclude SELECT/combobox from Shift+N, command palette uses open-notification-drawer - NotificationItemCard: NaN/future timestamp guard, div instead of nested button, group-focus-within for keyboard a11y - Slack adapter: escape mrkdwn to prevent mention injection - NotificationDrawer stories: use fn() for onClose action stub - settings/api.py: align auth_max_requests definition default to 6000
There was a problem hiding this comment.
♻️ Duplicate comments (1)
web/src/components/notifications/NotificationItemCard.tsx (1)
64-81:⚠️ Potential issue | 🟠 MajorPrevent parent key handler from firing when action buttons are keyboard-activated.
At Line 76, the container-level
onKeyDownhandles Enter/Space. Those key events bubble from the inner buttons (Lines 99-123), so keyboard activation of “Mark as read”/“Dismiss” can also triggerhandleClick()(Line 79), causing unintended mark-read/navigation.🔧 Suggested fix
- <div + <div role="listitem" - tabIndex={0} - aria-label={`${item.severity} notification: ${item.title}`} + aria-label={`${item.severity} notification: ${item.title}`} className={cn( 'group relative flex w-full gap-3 rounded-md border-l-2 px-3 py-2 text-left', 'transition-colors hover:bg-card-hover', item.read ? 'border-l-transparent' : BORDER_COLORS[item.severity], !item.read && 'bg-accent/5', - item.href && 'cursor-pointer', )} - onClick={handleClick} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault() - handleClick() - } - }} > - <Icon className={cn('mt-0.5 size-4 shrink-0', SEVERITY_COLORS[item.severity])} /> - - <div className="min-w-0 flex-1"> - <p className="truncate text-sm font-medium text-foreground">{item.title}</p> - {item.description && ( - <p className="mt-0.5 truncate text-xs text-muted-foreground"> - {item.description} - </p> - )} - <p className="mt-1 text-xs text-muted-foreground/70"> - {formatRelativeTime(item.timestamp)} - </p> - </div> + <button + type="button" + className={cn('flex min-w-0 flex-1 gap-3 text-left', item.href && 'cursor-pointer')} + onClick={handleClick} + aria-label={`${item.severity} notification: ${item.title}`} + > + <Icon className={cn('mt-0.5 size-4 shrink-0', SEVERITY_COLORS[item.severity])} /> + <div className="min-w-0 flex-1"> + <p className="truncate text-sm font-medium text-foreground">{item.title}</p> + {item.description && ( + <p className="mt-0.5 truncate text-xs text-muted-foreground"> + {item.description} + </p> + )} + <p className="mt-1 text-xs text-muted-foreground/70"> + {formatRelativeTime(item.timestamp)} + </p> + </div> + </button>If you keep the current structure, minimally guard the parent handler:
onKeyDown={(e) => { + if (e.target !== e.currentTarget) return if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() handleClick() } }}#!/bin/bash set -euo pipefail FILE="web/src/components/notifications/NotificationItemCard.tsx" # Verify parent key handler + focusable container + nested action buttons coexist. nl -ba "$FILE" | sed -n '60,130p' rg -n "tabIndex=|onKeyDown=|onClick=|onMarkRead\\(|onDismiss\\(" "$FILE"Expected verification outcome:
- A focusable parent container (
tabIndex) withonKeyDownhandling Enter/Space.- Nested child
<button>actions present in same subtree.- Confirms keyboard event bubbling path exists and needs isolation.
Also applies to: 99-123
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/components/notifications/NotificationItemCard.tsx` around lines 64 - 81, The parent container's onKeyDown (which calls handleClick) is firing for Enter/Space events that originate from the child action buttons; update the container onKeyDown to early-return when the key event came from a nested actionable element (e.g., a <button> or element with role="button" or an anchor), or alternatively add an onKeyDown handler to the action buttons (onMarkRead/onDismiss) that calls event.stopPropagation(); specifically modify the onKeyDown attached to the container (the anonymous handler that checks e.key === 'Enter' || e.key === ' ') to detect if (e.target as HTMLElement).closest('button, [role="button"], a') (or if e.target is a button) and return without calling handleClick, or add stopPropagation in the child buttons' keyboard handlers so their activation doesn't bubble up to handleClick.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@web/src/components/notifications/NotificationItemCard.tsx`:
- Around line 64-81: The parent container's onKeyDown (which calls handleClick)
is firing for Enter/Space events that originate from the child action buttons;
update the container onKeyDown to early-return when the key event came from a
nested actionable element (e.g., a <button> or element with role="button" or an
anchor), or alternatively add an onKeyDown handler to the action buttons
(onMarkRead/onDismiss) that calls event.stopPropagation(); specifically modify
the onKeyDown attached to the container (the anonymous handler that checks e.key
=== 'Enter' || e.key === ' ') to detect if (e.target as
HTMLElement).closest('button, [role="button"], a') (or if e.target is a button)
and return without calling handleClick, or add stopPropagation in the child
buttons' keyboard handlers so their activation doesn't bubble up to handleClick.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 264811b4-98aa-4095-a688-da407e68dbf2
📒 Files selected for processing (4)
docs/design/page-structure.mdweb/src/components/layout/AppLayout.tsxweb/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationItemCard.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: Dashboard Test
- GitHub Check: Test (Python 3.14)
- GitHub Check: Build Sandbox
- GitHub Check: Build Web
- GitHub Check: Build Backend
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.md
📄 CodeRabbit inference engine (CLAUDE.md)
ALWAYS read the relevant
docs/design/page before implementing any feature or planning any issue. Design Spec (DESIGN_SPEC.md) is a pointer file linking to the 12 design pages. The design spec is the starting point for architecture, data models, and behavior.
Files:
docs/design/page-structure.md
web/src/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx,js,jsx}: Always usecreateLoggerfrom@/lib/logger-- never bareconsole.warn/console.error/console.debugin application code
Logger variable name must always beconst log(e.g.const log = createLogger('module-name'))
Pass dynamic/untrusted values as separate arguments to logger methods (not interpolated into the message string) so they go throughsanitizeArg
Attacker-controlled fields inside structured objects must be wrapped insanitizeForLog()before embedding in log calls
Files:
web/src/components/layout/AppLayout.tsxweb/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationItemCard.tsx
web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground,bg-card,text-accent,text-success,bg-danger, etc.) or CSS variables (var(--so-*)) for colors; NEVER hardcode hex values in.tsx/.tsfiles
Usefont-sansorfont-mono(Geist tokens) for typography; NEVER setfontFamilydirectly in.tsx/.tsfiles
Use density-aware tokens (p-card,gap-section-gap,gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing in components
Use token variables (var(--so-shadow-card-hover),border-border,border-bright) for shadows and borders; NEVER hardcode values in.tsx/.tsfiles
Use@/lib/motionpresets for Framer Motion transition durations; NEVER hardcode transition durations
CSS side-effect imports in TypeScript 6 require type declarations -- add/// <reference types="vite/client" />at the top of files with CSS importsALWAYS reuse existing components from web/src/components/ui/ before creating new ones. NEVER hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions - use design tokens and
@/lib/motionpresets. A PostToolUse hook (scripts/check_web_design_system.py) enforces these rules on every Edit/Write to web/src/.
Files:
web/src/components/layout/AppLayout.tsxweb/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationItemCard.tsx
web/src/components/layout/**/*.{ts,tsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/components/layout/**/*.{ts,tsx}: MountToastContaineronce in AppLayout for success/error/warning/info notifications with auto-dismiss queue
MountCommandPaletteonce in AppLayout; register commands viauseCommandPalettehook
Files:
web/src/components/layout/AppLayout.tsx
web/src/**/*.stories.tsx
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.stories.tsx: For Storybook stories withtags: ['autodocs'], ensure@storybook/addon-docsis installed and added to addons
Usestorybook/testandstorybook/actionsimport paths in Storybook stories (not@storybook/testor@storybook/addon-actions)
Files:
web/src/components/notifications/NotificationDrawer.stories.tsx
🧠 Learnings (30)
📚 Learning: 2026-03-18T08:23:08.912Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T08:23:08.912Z
Learning: When approved deviations occur, update the relevant `docs/design/` page to reflect the new reality.
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-03-31T14:31:11.894Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to docs/design/*.md : Update the relevant `docs/design/` page when approved deviations occur to reflect the new reality
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/layout/**/*.{ts,tsx} : Mount `ToastContainer` once in AppLayout for success/error/warning/info notifications with auto-dismiss queue
Applied to files:
web/src/components/layout/AppLayout.tsxweb/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/layout/**/*.{ts,tsx} : Mount `CommandPalette` once in AppLayout; register commands via `useCommandPalette` hook
Applied to files:
web/src/components/layout/AppLayout.tsx
📚 Learning: 2026-03-31T14:31:11.894Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard
Applied to files:
web/src/components/layout/AppLayout.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components
Applied to files:
web/src/components/layout/AppLayout.tsxweb/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-06T13:43:45.381Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T13:43:45.381Z
Learning: Applies to web/package.json : Web dashboard: Node.js 22+, TypeScript 6.0+. Key dependencies: React 19, react-router, shadcn/ui, Base UI, Tailwind CSS 4, Zustand, tanstack/react-query, xyflow/react, dagrejs/dagre, d3-force, dnd-kit, Recharts, Framer Motion, cmdk-base, js-yaml, Axios, Lucide React, Storybook 10, Vitest, Playwright, fast-check.
Applied to files:
web/src/components/layout/AppLayout.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem) before creating new ones
Applied to files:
web/src/components/layout/AppLayout.tsxweb/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)
Applied to files:
web/src/components/layout/AppLayout.tsxweb/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : For Base UI Dialog, AlertDialog, and Popover components, compose with `Portal` + `Backdrop` + `Popup`. Popover and Menu additionally require a `Positioner` wrapper with `side` / `align` / `sideOffset` props
Applied to files:
web/src/components/layout/AppLayout.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/ui/**/*.stories.tsx : Create a `.stories.tsx` file alongside each new shared component with all states (default, hover, loading, error, empty)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Create new shared components in `web/src/components/ui/` with `.stories.tsx` Storybook file covering all states (default, hover, loading, error, empty)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10: import from `storybook/test` (not `storybook/test`), `storybook/actions` (not `storybook/addon-actions`)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/**/*.stories.{ts,tsx} : Storybook 10: Use storybook/test (not storybook/test) and storybook/actions (not storybook/addon-actions) import paths
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10: Import from `storybook/test` instead of `storybook/test`
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/**/*.stories.tsx : Use `storybook/test` and `storybook/actions` import paths in Storybook stories (not `storybook/test` or `storybook/addon-actions`)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : When creating new shared web components, place in web/src/components/ui/ with kebab-case filename, create .stories.tsx alongside with all states (default, hover, loading, error, empty), export props as TypeScript interface, use design tokens exclusively with no hardcoded colors/fonts/spacing, and import cn from `@/lib/utils` for conditional class merging
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/**/*.stories.{ts,tsx} : Storybook 10: Use parameters.backgrounds.options (object keyed by name) + initialGlobals.backgrounds.value for background options (replaces old default + values array)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10: Use `parameters.a11y.test: 'error' | 'todo' | 'off'` for a11y testing configuration (replaces old `.element` and `.manual`)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/stores/**/*.{ts,tsx} : Use Zustand stores in web dashboard for state management (auth, WebSocket, toast, analytics, domain shells)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Zustand stores for state management in the web dashboard; each domain has its own store module (auth, WebSocket, toast, analytics, setup, company, agents, budget, tasks, settings, providers, theme, per-domain stores)
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/**/*.stories.{ts,tsx} : Storybook 10: Use parameters.a11y.test: 'error' | 'todo' | 'off' for a11y testing (replaces old .element and .manual); set globally in preview.tsx to enforce WCAG compliance on all stories
Applied to files:
web/src/components/notifications/NotificationDrawer.stories.tsxweb/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT build card-with-header layouts from scratch; use `<SectionCard>`
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside .map()—extract to a shared component
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside `.map()`; extract to a shared component
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones; refer to design system inventory (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, etc.)
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : For Tabs Base UI components, use `data-[active]` attribute selector (not `data-[state=active]`) for animation state styling
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
🔇 Additional comments (9)
docs/design/page-structure.md (1)
257-280: Notification taxonomy section looks consistent and up to date.The category table, severity mappings, routing terminology (“drawer”), and Settings override description are coherent with the delivered notifications UX.
web/src/components/notifications/NotificationDrawer.stories.tsx (2)
1-27: LGTM! Story setup follows guidelines.The meta configuration correctly uses
fn()fromstorybook/testfor theonClosehandler, wraps stories inMemoryRouterfor routing context, and follows the project's Storybook conventions.
29-80: Stories correctly reset state to avoid cross-story pollution.Both
EmptyandWithItemsproperly callclearAll()before rendering/seeding, ensuring deterministic state across story switches. The sample notifications cover diverse categories (approvals.pending,budget.threshold,tasks.failed,agents.hired) with varying severity levels, providing good visual coverage.web/src/components/layout/AppLayout.tsx (5)
68-88: LGTM! Event listeners cleanly separated.The two
useEffecthooks fortoggle-notification-drawerandopen-notification-drawerevents are appropriately separated with clear comments explaining their distinct purposes (toggle vs. one-directional open). Both properly clean up listeners on unmount.
90-104: Keyboard shortcut handler properly guards form controls.The
Shift+Nhandler correctly:
- Skips repeated keypresses and already-prevented events
- Exempts
INPUT,TEXTAREA,SELECT,contentEditable, and[role="combobox"]elements- Verifies exact modifier combination (shift only, no ctrl/meta/alt)
- Uses
documentlistener which is appropriate for global shortcuts
106-117: Navigation handler securely validates href.The
notification-navigatehandler correctly:
- Type-guards
hrefwithtypeof href === 'string'- Requires paths to start with
/(internal only)- Rejects
//prefix to prevent protocol-relative URL attacks- Uses React Router's
navigate()for SPA navigation
137-137: Command palette entry correctly uses one-way open event.The
notifications-opencommand dispatchesopen-notification-drawer(not toggle), ensuring the command always opens the drawer regardless of current state. Theshortcutmetadata displaysShift+Nin the palette UI while the actual key binding is handled separately in theuseEffectabove.
203-206: NotificationDrawer correctly wired into layout.The drawer is rendered with controlled
openstate and anonClosehandler that sets state tofalse. Placement after</main>but beforeToastContainerandCommandPalettemaintains appropriate z-index layering.web/src/components/notifications/NotificationItemCard.tsx (1)
28-33: Good defensive timestamp handling.The invalid/future timestamp guards prevent broken labels like
NaNd agoand keep display stable.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
web/src/components/notifications/NotificationItemCard.tsx (1)
64-82:⚠️ Potential issue | 🟠 MajorUse a dedicated primary control instead of an interactive
listitem.At Line 64–82, the row is clickable/focusable but exposed as
role="listitem", so assistive tech won’t reliably announce it as actionable. It is also always in tab order (tabIndex={0}), including non-actionable rows (item.read && !item.href), creating dead tab stops.♿ Suggested structural fix
- <div - role="listitem" - tabIndex={0} - aria-label={`${item.severity} notification: ${item.title}`} + <div + role="listitem" className={cn( 'group relative flex w-full gap-3 rounded-md border-l-2 px-3 py-2 text-left', 'transition-colors hover:bg-card-hover', item.read ? 'border-l-transparent' : BORDER_COLORS[item.severity], !item.read && 'bg-accent/5', - item.href && 'cursor-pointer', + (!item.read || item.href) && 'cursor-pointer', )} - onClick={handleClick} - onKeyDown={(e) => { - if ((e.key === 'Enter' || e.key === ' ') && !(e.target as HTMLElement).closest('button')) { - e.preventDefault() - handleClick() - } - }} > + {/* Primary action target (button/link) goes here; keep action buttons as siblings */}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/components/notifications/NotificationItemCard.tsx` around lines 64 - 82, The NotificationItemCard renders a list row as a focusable/clickable listitem which miscommunicates actionability; refactor so the outer element remains a non-interactive listitem (remove role="listitem" and tabIndex from the container) and move the interactive behavior into a dedicated primary control (e.g., a button or anchor inside the card) that handles onClick, onKeyDown and receives focus only when actionable (use item.href => <a> or otherwise <button> when !item.read && actionable). Update handleClick references to be invoked from that inner control, ensure non-actionable rows are not tabbable, and preserve existing styling logic (the cn usage and BORDER_COLORS/item.read conditions) on the container while applying cursor-pointer only to the inner control when actionable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@web/src/components/notifications/NotificationItemCard.tsx`:
- Around line 64-82: The NotificationItemCard renders a list row as a
focusable/clickable listitem which miscommunicates actionability; refactor so
the outer element remains a non-interactive listitem (remove role="listitem" and
tabIndex from the container) and move the interactive behavior into a dedicated
primary control (e.g., a button or anchor inside the card) that handles onClick,
onKeyDown and receives focus only when actionable (use item.href => <a> or
otherwise <button> when !item.read && actionable). Update handleClick references
to be invoked from that inner control, ensure non-actionable rows are not
tabbable, and preserve existing styling logic (the cn usage and
BORDER_COLORS/item.read conditions) on the container while applying
cursor-pointer only to the inner control when actionable.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 260588b0-59db-4fd7-ad7f-8cd829bad1f6
📒 Files selected for processing (1)
web/src/components/notifications/NotificationItemCard.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
- GitHub Check: Deploy Preview
- GitHub Check: Test (Python 3.14)
- GitHub Check: Dashboard Test
- GitHub Check: Build Sandbox
- GitHub Check: Build Web
- GitHub Check: Build Backend
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (2)
web/src/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx,js,jsx}: Always usecreateLoggerfrom@/lib/logger-- never bareconsole.warn/console.error/console.debugin application code
Logger variable name must always beconst log(e.g.const log = createLogger('module-name'))
Pass dynamic/untrusted values as separate arguments to logger methods (not interpolated into the message string) so they go throughsanitizeArg
Attacker-controlled fields inside structured objects must be wrapped insanitizeForLog()before embedding in log calls
Files:
web/src/components/notifications/NotificationItemCard.tsx
web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground,bg-card,text-accent,text-success,bg-danger, etc.) or CSS variables (var(--so-*)) for colors; NEVER hardcode hex values in.tsx/.tsfiles
Usefont-sansorfont-mono(Geist tokens) for typography; NEVER setfontFamilydirectly in.tsx/.tsfiles
Use density-aware tokens (p-card,gap-section-gap,gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing in components
Use token variables (var(--so-shadow-card-hover),border-border,border-bright) for shadows and borders; NEVER hardcode values in.tsx/.tsfiles
Use@/lib/motionpresets for Framer Motion transition durations; NEVER hardcode transition durations
CSS side-effect imports in TypeScript 6 require type declarations -- add/// <reference types="vite/client" />at the top of files with CSS importsALWAYS reuse existing components from web/src/components/ui/ before creating new ones. NEVER hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions - use design tokens and
@/lib/motionpresets. A PostToolUse hook (scripts/check_web_design_system.py) enforces these rules on every Edit/Write to web/src/.
Files:
web/src/components/notifications/NotificationItemCard.tsx
🧠 Learnings (14)
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem) before creating new ones
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT build card-with-header layouts from scratch; use `<SectionCard>`
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside .map()—extract to a shared component
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside `.map()`; extract to a shared component
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones; refer to design system inventory (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, etc.)
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/layout/**/*.{ts,tsx} : Mount `ToastContainer` once in AppLayout for success/error/warning/info notifications with auto-dismiss queue
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : For Tabs Base UI components, use `data-[active]` attribute selector (not `data-[state=active]`) for animation state styling
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/**/*.stories.{ts,tsx} : Storybook 10: Use parameters.a11y.test: 'error' | 'todo' | 'off' for a11y testing (replaces old .element and .manual); set globally in preview.tsx to enforce WCAG compliance on all stories
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
📚 Learning: 2026-04-06T06:45:22.965Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-06T06:45:22.965Z
Learning: Do NOT build card-with-header layouts from scratch -- use `<SectionCard>` from `@/components/ui/section-card`
Applied to files:
web/src/components/notifications/NotificationItemCard.tsx
🔇 Additional comments (1)
web/src/components/notifications/NotificationItemCard.tsx (1)
28-40: Nice hardening of relative-time formatting.The
NaNguard and future-time clamp prevent broken labels and keep rendering stable.
🤖 I have created a release *beep* *boop* --- ## [0.6.3](v0.6.2...v0.6.3) (2026-04-06) ### Features * backend CRUD + multi-user permissions ([#1081](#1081), [#1082](#1082)) ([#1094](#1094)) ([93e469b](93e469b)) * in-dashboard team editing + budget rebalance on pack apply ([#1093](#1093)) ([35977c0](35977c0)), closes [#1079](#1079) [#1080](#1080) * tiered rate limiting, NotificationSink protocol, in-dashboard notifications ([#1092](#1092)) ([df2142c](df2142c)), closes [#1077](#1077) [#1078](#1078) [#849](#849) * two-stage safety classifier and cross-provider uncertainty check for approval gates ([#1090](#1090)) ([0b2edee](0b2edee)), closes [#847](#847) [#701](#701) ### Refactoring * memory pipeline improvements ([#1075](#1075), [#997](#997)) ([#1091](#1091)) ([a048a4c](a048a4c)) ### Documentation * add OpenCode parity setup and hookify rule documentation ([#1095](#1095)) ([52e877a](52e877a)) ### Maintenance * bump vite from 8.0.3 to 8.0.4 in /web in the all group across 1 directory ([#1088](#1088)) ([1e86ca6](1e86ca6)) * tune ZAP DAST scan -- auth, timeouts, rules, report artifacts ([#1097](#1097)) ([82bf0e1](82bf0e1)), closes [#1096](#1096) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Summary
Three features implemented together since #1078 and #849 share an event taxonomy:
Tiered Rate Limiting (#1077)
LitestarRateLimitConfigmiddlewares stacked around auth:[unauth_rate_limit, auth, logging, auth_rate_limit]_auth_identifier_for_requestkeys by user ID with IP fallback${SYNTHORG_API__RATE_LIMIT__UNAUTH_MAX}/${SYNTHORG_API__RATE_LIMIT__AUTH_MAX}max_requestsfield rejected with clear migration messageNotificationSink Protocol (#849)
src/synthorg/notifications/module:NotificationSinkprotocol,NotificationDispatcher(TaskGroup fan-out),Notificationmodel with category + severity taxonomyasyncio.to_thread)MemoryError/RecursionErrorpropagate,ExceptionGroupunwrappedNotificationConfiginRootConfig(YAML + env var overridable)In-Dashboard Notifications (#1078)
useNotificationsStore.enqueue()-> fan out to toast, drawer, and browser Notification APINotificationItemtype with 17 categories, per-category routing config (CATEGORY_CONFIGS)NotificationDrawerwith filter bar (8 groups), per-item actions (mark read, dismiss), unread badge on sidebar bellShift+Nkeyboard shortcut + command palette entryNotificationsSectionin Settings page with per-category routing toggles and browser permission managementuseGlobalNotificationsto subscribe to agents, approvals, budget, system, and tasks WS channelsTest Plan
Review Coverage
Pre-reviewed by 5 agents (code-reviewer, silent-failure-hunter, frontend-reviewer, docs-consistency, type-design-analyzer), 26 findings addressed.
Closes #1077
Closes #1078
Closes #849