@cryptotaxi247 / CoPilot / commits / ff862c92

chore: remove direct SMTP channel — Shuffle is the sole notification path (#835)

* chore(notifications): remove direct SMTP channel — Shuffle is the sole path Consolidates outbound notifications onto Shuffle's catalog. Email, chat, ticketing, and the rest of the surface area all flow through Shuffle's authenticated apps (Outlook, Gmail, SendGrid, SMTP-via- Shuffle, Slack, Teams, ServiceNow, PagerDuty, etc.) — there's no reason to keep a parallel direct-SMTP path that overlaps with what Shuffle already does well. ## Backend - Schema: drop `SMTP_EMAIL` from `NotificationChannel` enum; only `SHUFFLE` remains. Update `destination` field doc to reflect its new role as a free-form hint passed into Shuffle's natural-language input. - Dispatcher: remove `dispatch_smtp_email`, `_send_smtp_sync`, all smtplib/ssl/email.message imports, and the `_SMTP_TIMEOUT_S` constant. `DispatchResult` was only used by SMTP — removed too. - Service: remove `_format_default_subject` (SMTP-only), the SMTP branch in the dispatch loop, and the `dispatch_smtp_email` import. Tightened the connector-creds block now that SMTP-only customers no longer skip it. - universal_models: comment update on `customer_notification_route.channel` and `.destination` to reflect Shuffle-only semantics. Column shape unchanged — keeps the door open for future direct channels (raw webhook, PagerDuty REST, etc.) without a migration. ## Frontend - types: `NotificationChannel` union narrowed to `"shuffle"` only. - Route form: drop the SMTP option from the channel select (now a single-option disabled control); remove the SMTP-specific recipient email field and its validation; simplify the form's conditional layout since channel is no longer branched. Shuffle integration + app pickers always render. - Route item card: drop the SMTP icon/label branch; channel always renders as Shuffle with the cached app name. ## Drive-by `CustomerAiNotificationDispatchLog.vue` — the same `String()` coercion applied on PR #832 for naive-ui's stricter DataTable types. Pulls the fix forward to main so this branch builds clean. ## Migration No DB migration. Existing rows with `channel='smtp_email'` (only present from Phase 1 smoke testing) become legacy data — the dispatch loop will mark them as `failed` with "Unsupported channel: smtp_email" in the log. Cleanup is a one-line DELETE if it bothers anyone. Talon side updated separately on nanoclaw to drop SMTP references from `groups/copilot/notifications.md`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(notifications): add customer notification routes and Shuffle integrations management Introduces a new UI for managing customer notification routes and Shuffle integrations. This includes: - **CustomerAiNotificationRoutes**: A form for creating and editing notification routes, allowing users to specify triggers, severity levels, and destinations. - **CustomerAiNotificationRouteItem**: A component to display individual notification routes with options to edit, enable/disable, or delete. - **CustomerShuffleIntegrations**: A form for managing Shuffle integrations, including a dropdown for selecting Shuffle organizations and a manual entry option. - **CustomerShuffleIntegrationItem**: A component to display individual Shuffle integrations with verification and management options. This implementation enhances the user experience by providing a structured way to manage notification settings and integrations, ensuring that all notifications are routed through the appropriate channels. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * refactor(notifications): Enhance form validation and feedback in CustomerAiNotificationRouteForm - Improved field-specific error handling for channel, shuffle integration, and shuffle app ID. - Updated feedback templates to conditionally display based on field errors, ensuring clearer user guidance. - Refactored validation rules to utilize custom error creation for better maintainability and clarity. - Streamlined the layout of the form for improved user experience and consistency. * chore(deps): update frontend dependencies to latest versions - Upgraded @vueuse/core to ^14.3.0 - Updated algoliasearch to ^5.52.0 - Bumped axios to ^1.16.0 - Upgraded jose to ^6.2.3 - Updated nanoid to ^5.1.11 - Bumped @clack/prompts to ^1.3.0 - Upgraded @vue/test-utils to ^2.4.10 - Updated baseline-browser-mapping to ^2.10.25 - Bumped eslint to ~10.3.0 - Updated jsdom to ^29.1.1 - Upgraded prettier-plugin-tailwindcss to ^0.7.4 These updates ensure compatibility with the latest features and improvements in the respective libraries. * chore(version): update CURRENT_VERSION to 0.1.61 --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed May 3, 2026 at 12:20 UTC ff862c92e59d349e2456c81aca090818be81d6db
11 files changed +293 -447
backend/app/db/universal_models.py
+7 -4
@@ -735,12 +735,15 @@ class CustomerNotificationRoute(SQLModel, table=True):
735 # Stored as string so adding new triggers later is a data-only change.
736 trigger: str = Field(max_length=64, nullable=False, index=True)
737
738 - # 'slack_webhook' or 'smtp_email' for Phase 1. Phase 2 adds 'shuffle'
739 - # and pairs with an integration_id FK.
738 + # Always 'shuffle' on the current code path — the column kept its
739 + # varchar shape so we can re-introduce direct channels (raw webhook,
740 + # PagerDuty REST, etc.) later without a migration. Pairs with the
741 + # shuffle_integration_id FK to scope the dispatch to a specific org.
742 channel: str = Field(max_length=32, nullable=False)
743
742 - # Slack incoming-webhook URL or SMTP recipient email address
743 - # (multi-recipient = comma separated, normalized in the service).
744 + # Free-form destination hint (Slack channel, email recipient,
745 + # handle, etc.) — Shuffle's app agent figures out how to route it
746 + # within the authenticated app at dispatch time.
747 destination: str = Field(sa_column=Column(Text), nullable=False)
748
749 # 'Critical' | 'High' | 'Medium' | 'Low' | 'Informational'. Inclusive
backend/app/notifications/schema/notifications.py
+6 -5
@@ -42,15 +42,16 @@ class NotificationTrigger(str, Enum):
42 class NotificationChannel(str, Enum):
43 """Delivery channel set.
44
45 - `smtp_email` is direct SMTP via env config (CoPilot deployment-wide).
45 `shuffle` proxies to Shuffle's hosted MCP — each customer points at
46 their own Shuffle Org via `customer_shuffle_integration`, and Shuffle
47 handles the OAuth-authenticated downstream app (Slack workspace,
49 - Outlook tenant, Teams, etc.). Routes referencing `shuffle` MUST
50 - populate the `shuffle_integration_id` + `shuffle_app_id` columns.
48 + Outlook tenant, Teams, Gmail, SendGrid, etc.). Routes referencing
49 + `shuffle` MUST populate the `shuffle_integration_id` + `shuffle_app_id`
50 + columns. Email, chat, ticketing, and the rest of the catalog all
51 + flow through this single channel — there's no separate direct SMTP
52 + path because Shuffle's email apps cover that surface.
53 """
54
53 - SMTP_EMAIL = "smtp_email"
55 SHUFFLE = "shuffle"
56
57
@@ -120,7 +121,7 @@ class NotificationRouteBase(BaseModel):
121 destination: str = Field(
122 ...,
123 min_length=1,
123 - description="SMTP recipient email(s) or Shuffle destination hint (channel name / address / handle).",
124 + description="Destination hint for the Shuffle app (channel name, email address, handle).",
125 )
126 min_severity: NotificationSeverity = NotificationSeverity.MEDIUM
127 format_template: Optional[str] = Field(
backend/app/notifications/services/dispatchers.py
+18 -110
@@ -1,29 +1,25 @@
1 """
2 Channel-specific delivery helpers for the notification dispatcher.
3
4 -Each helper is async, returns a (status, error_message, latency_ms,
5 -*provider-specific extras) tuple, and never raises — failures are
6 -reported via the tuple so the caller can log them in a single shape
7 -regardless of which channel failed. This keeps the dispatch loop's
8 -try/except surface trivial.
4 +Each helper is async, returns a tuple shape that includes status,
5 +error_message, latency_ms, and optional provider-specific extras. None
6 +raise — failures are reported via the tuple so the caller can log them
7 +in a single shape. This keeps the dispatch loop's try/except surface
8 +trivial.
9
10 Channels:
11 - - smtp_email : direct SMTP via env config (deployment-wide)
12 - - shuffle : POST to https://shuffler.io/api/v1/apps/{id}/mcp using
13 - the deployment's Bearer (from the Shuffle connector)
14 - + customer's Org-Id header. Fire-and-record — we
15 - capture Shuffle's execution_id but don't poll for the
16 - downstream provider's terminal state.
11 + - shuffle : POST to https://shuffler.io/api/v1/apps/{id}/mcp using
12 + the deployment's Bearer (from the Shuffle connector)
13 + + customer's Org-Id header. Fire-and-record — we
14 + capture Shuffle's execution_id but don't poll for the
15 + downstream provider's terminal state. Email, Slack,
16 + Teams, etc. all flow through this single channel via
17 + Shuffle's catalog of authenticated apps.
18 """
19
20 from __future__ import annotations
21
21 -import asyncio
22 -import os
23 -import smtplib
24 -import ssl
22 import time
26 -from email.message import EmailMessage
23 from typing import Any
24 from typing import Dict
25 from typing import Optional
@@ -32,107 +28,19 @@ from typing import Tuple
28 import httpx
29 from loguru import logger
30
35 -# Tuple shape used by every dispatcher: (status, error_message, latency_ms)
36 -# status is one of 'sent' | 'failed'; the caller turns 'sent' into a
37 -# DispatchStatus.SENT row. error_message is None on success.
38 -DispatchResult = Tuple[str, Optional[str], int]
39 -
40 -
41 -# Shuffle's dispatcher returns a 4-tuple — the standard triple plus an
42 -# execution_id. None when the kickoff failed before Shuffle returned one.
31 +# Shuffle's dispatcher returns a 4-tuple — status, error_message,
32 +# latency_ms, and the kickoff execution_id (None if Shuffle didn't
33 +# return one before the call failed).
34 ShuffleDispatchResult = Tuple[str, Optional[str], int, Optional[str]]
35
36
46 -# Hard cap on the upstream provider call. SMTP usually completes in
47 -# <2s; Shuffle's MCP kickoff is usually <1s but cold-start paths
37 +# Hard cap on Shuffle's MCP kickoff. Usually <1s but cold-start paths
38 # (first call against an org, or Shuffle's backend warming) can spike.
49 -# Two budgets so a slow Shuffle response doesn't masquerade as a
50 -# dispatch failure while keeping SMTP failures fast.
51 -_SMTP_TIMEOUT_S = 10.0
39 +# 30s leaves enough headroom for those without letting a stuck request
40 +# stall the dispatch loop indefinitely.
41 _SHUFFLE_TIMEOUT_S = 30.0
42
43
55 -async def dispatch_smtp_email(
56 - recipients: list[str],
57 - subject: str,
58 - body: str,
59 -) -> DispatchResult:
60 - """Send a plaintext email to one or more recipients via SMTP.
61 -
62 - Configuration is read from environment variables on each call so a
63 - customer can re-point SMTP at runtime without restarting CoPilot:
64 -
65 - SMTP_HOST hostname (required)
66 - SMTP_PORT int, defaults to 587 (STARTTLS)
67 - SMTP_USER optional — when set, AUTH LOGIN is performed
68 - SMTP_PASSWORD optional — paired with SMTP_USER
69 - SMTP_FROM From: header value (required)
70 - SMTP_USE_TLS 'true' (default) | 'false' — STARTTLS toggle
71 - """
72 - started = time.monotonic()
73 - try:
74 - host = os.getenv("SMTP_HOST")
75 - from_addr = os.getenv("SMTP_FROM")
76 - if not host or not from_addr:
77 - latency_ms = int((time.monotonic() - started) * 1000)
78 - return (
79 - "failed",
80 - "SMTP not configured (set SMTP_HOST and SMTP_FROM)",
81 - latency_ms,
82 - )
83 -
84 - port = int(os.getenv("SMTP_PORT", "587"))
85 - user = os.getenv("SMTP_USER")
86 - password = os.getenv("SMTP_PASSWORD")
87 - use_tls = os.getenv("SMTP_USE_TLS", "true").lower() != "false"
88 -
89 - msg = EmailMessage()
90 - msg["Subject"] = subject
91 - msg["From"] = from_addr
92 - msg["To"] = ", ".join(recipients)
93 - msg.set_content(body)
94 -
95 - # smtplib is sync — push it to a thread so the event loop stays
96 - # responsive while we wait on the network.
97 - await asyncio.get_running_loop().run_in_executor(
98 - None,
99 - _send_smtp_sync,
100 - host,
101 - port,
102 - user,
103 - password,
104 - use_tls,
105 - msg,
106 - )
107 - latency_ms = int((time.monotonic() - started) * 1000)
108 - return ("sent", None, latency_ms)
109 - except Exception as e: # noqa: BLE001
110 - latency_ms = int((time.monotonic() - started) * 1000)
111 - logger.warning(f"SMTP email dispatch failed: {e!r}")
112 - return ("failed", f"{type(e).__name__}: {e}", latency_ms)
113 -
114 -
115 -def _send_smtp_sync(
116 - host: str,
117 - port: int,
118 - user: str | None,
119 - password: str | None,
120 - use_tls: bool,
121 - msg: EmailMessage,
122 -) -> None:
123 - """Synchronous SMTP send — invoked in a worker thread by the async
124 - wrapper. Raises on failure; the caller catches and reports."""
125 - with smtplib.SMTP(host, port, timeout=_SMTP_TIMEOUT_S) as smtp:
126 - smtp.ehlo()
127 - if use_tls:
128 - ctx = ssl.create_default_context()
129 - smtp.starttls(context=ctx)
130 - smtp.ehlo()
131 - if user and password:
132 - smtp.login(user, password)
133 - smtp.send_message(msg)
134 -
135 -
44 # ---------------------------------------------------------------------------
45 # Shuffle dispatcher (Phase 2)
46 # ---------------------------------------------------------------------------
backend/app/notifications/services/notifications.py
+12 -29
@@ -43,7 +43,6 @@ from app.notifications.schema.notifications import ShuffleIntegrationCreate
43 from app.notifications.schema.notifications import ShuffleIntegrationUpdate
44 from app.notifications.schema.notifications import ShuffleOrg
45 from app.notifications.services.dispatchers import dispatch_shuffle
46 -from app.notifications.services.dispatchers import dispatch_smtp_email
46 from app.notifications.services.dispatchers import (
47 list_shuffle_apps as shuffle_apps_client,
48 )
@@ -485,14 +484,6 @@ def _format_default_body(req: DispatchRequest) -> str:
484 return "\n".join(parts)
485
486
488 -def _format_default_subject(req: DispatchRequest) -> str:
489 - return (
490 - f"[{req.severity_assessment.value}] AI investigation — "
491 - f"alert #{req.alert_id}"
492 - f"{(' ' + req.alert_name) if req.alert_name else ''}"
493 - )
494 -
495 -
487 def _render_body(route: CustomerNotificationRoute, req: DispatchRequest) -> str:
488 """Apply the route's `format_template` if set, else fall back.
489
@@ -625,25 +616,21 @@ async def dispatch(req: DispatchRequest, session: AsyncSession) -> DispatchRespo
616
617 # Shuffle dispatch needs the deployment's connector creds. We fetch
618 # them once per dispatch call (before the per-route loop) so a
628 - # whole batch of Shuffle routes shares one DB read. The fetch
629 - # itself is gated by "is any matched route Shuffle" — for SMTP-only
630 - # customers we never touch the connector row.
619 + # whole batch shares one DB read. Only fetch if at least one
620 + # matched route uses Shuffle — early-out keeps zero-route customers
621 + # from touching the connectors table.
622 shuffle_creds: Optional[tuple[str, str]] = None
623 + shuffle_creds_error: Optional[str] = None
624 if any(r.channel == NotificationChannel.SHUFFLE.value for r in matched_routes):
625 try:
626 shuffle_creds = await _get_shuffle_connector(session)
627 except HTTPException as e:
636 - # Connector misconfigured — the dispatch endpoint exposes
637 - # the helper's 503, but for a batch dispatch we'd rather
638 - # mark each Shuffle route as failed in the log than abort
639 - # the whole loop. SMTP routes in the same batch still go.
628 + # Connector misconfigured — the dispatch endpoint surfaces
629 + # the helper's 503 to ad-hoc callers, but for a batch we'd
630 + # rather mark each route as failed in the log than abort
631 + # the whole loop.
632 logger.warning(f"Shuffle connector unavailable: {e.detail}")
641 - shuffle_creds = None
633 shuffle_creds_error = str(e.detail)
643 - else:
644 - shuffle_creds_error = None
645 - else:
646 - shuffle_creds_error = None
634
635 for route in matched_routes:
636 # Cache every route attribute we'll need into locals UP FRONT.
@@ -668,14 +655,10 @@ async def dispatch(req: DispatchRequest, session: AsyncSession) -> DispatchRespo
655 shuffle_execution_id: Optional[str] = None
656
657 try:
671 - if route_channel == NotificationChannel.SMTP_EMAIL.value:
672 - recipients = [r.strip() for r in route_destination.split(",") if r.strip()]
673 - subject = _format_default_subject(req)
674 - result_status, error_message, latency_ms = await dispatch_smtp_email(recipients, subject, body)
675 - elif route_channel == NotificationChannel.SHUFFLE.value:
676 - # Phase 2: Shuffle hosted MCP. Fire-and-record — we POST
677 - # to /api/v1/apps/{app_id}/mcp with the deployment's
678 - # admin Bearer + the customer's Org-Id, capture the
658 + if route_channel == NotificationChannel.SHUFFLE.value:
659 + # Shuffle hosted MCP. Fire-and-record — we POST to
660 + # /api/v1/apps/{app_id}/mcp with the deployment's admin
661 + # Bearer + the customer's Org-Id, capture the
662 # execution_id, and consider the dispatch "sent" on
663 # HTTP 200. We do NOT poll for the downstream app's
664 # terminal state.
backend/app/version/services/version.py
+1 -1
@@ -7,7 +7,7 @@ from loguru import logger
7 from packaging.version import Version
8
9 # Current version - update this with each release
10 -CURRENT_VERSION = "0.1.60"
10 +CURRENT_VERSION = "0.1.61"
11 VERSION_CHECK_URL = "https://api.github.com/repos/socfortress/CoPilot/releases/latest"
12
13
frontend/package.json
+12 -12
@@ -50,10 +50,10 @@
50 "@shikijs/markdown-it": "^4.0.2",
51 "@shuffleio/shuffle-mcps": "^0.0.3",
52 "@types/codemirror": "^5.60.17",
53 - "@vueuse/core": "^14.2.1",
53 + "@vueuse/core": "^14.3.0",
54 "@vueuse/motion": "^3.0.3",
55 - "algoliasearch": "^5.51.0",
56 - "axios": "^1.15.2",
55 + "algoliasearch": "^5.52.0",
56 + "axios": "^1.16.0",
57 "bytes": "^3.1.2",
58 "codemirror": "~6.0.2",
59 "colord": "^2.9.3",
@@ -63,12 +63,12 @@
63 "fast-xml-parser": "^5.7.2",
64 "file-saver": "^2.0.5",
65 "html-entities": "^2.6.0",
66 - "jose": "^6.2.2",
66 + "jose": "^6.2.3",
67 "js-md5": "^0.8.3",
68 "lodash": "^4.18.1",
69 "mitt": "^3.0.1",
70 "naive-ui": "^2.44.1",
71 - "nanoid": "^5.1.9",
71 + "nanoid": "^5.1.11",
72 "password-validator": "^5.3.0",
73 "pinia": "^3.0.4",
74 "pinia-plugin-persistedstate": "^4.7.1",
@@ -98,7 +98,7 @@
98 },
99 "devDependencies": {
100 "@antfu/eslint-config": "~8.2.0",
101 - "@clack/prompts": "^1.2.0",
101 + "@clack/prompts": "^1.3.0",
102 "@iconify/vue": "^5.0.0",
103 "@tailwindcss/vite": "^4.2.4",
104 "@tsconfig/node20": "^20.1.9",
@@ -114,18 +114,18 @@
114 "@types/validator": "^13.15.10",
115 "@vitejs/plugin-vue": "^6.0.6",
116 "@vitejs/plugin-vue-jsx": "^5.1.5",
117 - "@vue/test-utils": "^2.4.9",
117 + "@vue/test-utils": "^2.4.10",
118 "@vue/tsconfig": "~0.9.1",
119 - "baseline-browser-mapping": "^2.10.23",
119 + "baseline-browser-mapping": "^2.10.25",
120 "depcheck": "^1.4.7",
121 - "eslint": "~10.2.1",
121 + "eslint": "~10.3.0",
122 "flourite": "^1.3.0",
123 "fs-extra": "^11.3.4",
124 "jscpd": "^4.0.9",
125 - "jsdom": "^29.0.2",
125 + "jsdom": "^29.1.1",
126 "npm-run-all2": "^8.0.4",
127 "prettier": "^3.8.3",
128 - "prettier-plugin-tailwindcss": "^0.7.3",
128 + "prettier-plugin-tailwindcss": "^0.7.4",
129 "sass": "^1.99.0",
130 "start-server-and-test": "^3.0.2",
131 "tailwindcss": "^4.2.4",
@@ -153,4 +153,4 @@
153 "unrs-resolver"
154 ]
155 }
156 -}
156 +}
\ No newline at end of file
frontend/pnpm-lock.yaml
+142 -142
@@ -54,17 +54,17 @@ importers:
54 specifier: ^5.60.17
55 version: 5.60.17
56 '@vueuse/core':
57 - specifier: ^14.2.1
57 + specifier: ^14.3.0
58 version: 14.3.0(vue@3.5.33(typescript@5.9.3))
59 '@vueuse/motion':
60 specifier: ^3.0.3
61 version: 3.0.3(vue@3.5.33(typescript@5.9.3))
62 algoliasearch:
63 - specifier: ^5.51.0
63 + specifier: ^5.52.0
64 version: 5.52.0
65 axios:
66 - specifier: ^1.15.2
67 - version: 1.15.2(debug@4.4.3)
66 + specifier: ^1.16.0
67 + version: 1.16.0(debug@4.4.3)
68 bytes:
69 specifier: ^3.1.2
70 version: 3.1.2
@@ -93,7 +93,7 @@ importers:
93 specifier: ^2.6.0
94 version: 2.6.0
95 jose:
96 - specifier: ^6.2.2
96 + specifier: ^6.2.3
97 version: 6.2.3
98 js-md5:
99 specifier: ^0.8.3
@@ -108,7 +108,7 @@ importers:
108 specifier: ^2.44.1
109 version: 2.44.1(vue@3.5.33(typescript@5.9.3))
110 nanoid:
111 - specifier: ^5.1.9
111 + specifier: ^5.1.11
112 version: 5.1.11
113 password-validator:
114 specifier: ^5.3.0
@@ -176,9 +176,9 @@ importers:
176 devDependencies:
177 '@antfu/eslint-config':
178 specifier: ~8.2.0
179 - version: 8.2.0(@typescript-eslint/rule-tester@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.59.1(typescript@5.9.3))(@typescript-eslint/utils@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.33)(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.5(@types/node@25.6.0)(jsdom@29.1.1)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.99.0)(yaml@2.8.3)))
179 + version: 8.2.0(@typescript-eslint/rule-tester@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.59.1(typescript@5.9.3))(@typescript-eslint/utils@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.33)(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.5(@types/node@25.6.0)(jsdom@29.1.1)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.99.0)(yaml@2.8.3)))
180 '@clack/prompts':
181 - specifier: ^1.2.0
181 + specifier: ^1.3.0
182 version: 1.3.0
183 '@iconify/vue':
184 specifier: ^5.0.0
@@ -226,20 +226,20 @@ importers:
226 specifier: ^5.1.5
227 version: 5.1.5(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.99.0)(yaml@2.8.3))(vue@3.5.33(typescript@5.9.3))
228 '@vue/test-utils':
229 - specifier: ^2.4.9
229 + specifier: ^2.4.10
230 version: 2.4.10(@vue/compiler-dom@3.5.33)(@vue/server-renderer@3.5.33(vue@3.5.33(typescript@5.9.3)))(vue@3.5.33(typescript@5.9.3))
231 '@vue/tsconfig':
232 specifier: ~0.9.1
233 version: 0.9.1(typescript@5.9.3)(vue@3.5.33(typescript@5.9.3))
234 baseline-browser-mapping:
235 - specifier: ^2.10.23
235 + specifier: ^2.10.25
236 version: 2.10.25
237 depcheck:
238 specifier: ^1.4.7
239 version: 1.4.7
240 eslint:
241 - specifier: ~10.2.1
242 - version: 10.2.1(jiti@2.6.1)
241 + specifier: ~10.3.0
242 + version: 10.3.0(jiti@2.6.1)
243 flourite:
244 specifier: ^1.3.0
245 version: 1.3.0
@@ -250,7 +250,7 @@ importers:
250 specifier: ^4.0.9
251 version: 4.0.9
252 jsdom:
253 - specifier: ^29.0.2
253 + specifier: ^29.1.1
254 version: 29.1.1
255 npm-run-all2:
256 specifier: ^8.0.4
@@ -259,7 +259,7 @@ importers:
259 specifier: ^3.8.3
260 version: 3.8.3
261 prettier-plugin-tailwindcss:
262 - specifier: ^0.7.3
262 + specifier: ^0.7.4
263 version: 0.7.4(prettier@3.8.3)
264 sass:
265 specifier: ^1.99.0
@@ -1986,8 +1986,8 @@ packages:
1986 asynckit@0.4.0:
1987 resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
1988
1989 - axios@1.15.2:
1990 - resolution: {integrity: sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==}
1989 + axios@1.16.0:
1990 + resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==}
1991
1992 babel-walk@3.0.0-canary-5:
1993 resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==}
@@ -2657,8 +2657,8 @@ packages:
2657 resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
2658 engines: {node: ^20.19.0 || ^22.13.0 || >=24}
2659
2660 - eslint@10.2.1:
2661 - resolution: {integrity: sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==}
2660 + eslint@10.3.0:
2661 + resolution: {integrity: sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==}
2662 engines: {node: ^20.19.0 || ^22.13.0 || >=24}
2663 hasBin: true
2664 peerDependencies:
@@ -4910,44 +4910,44 @@ snapshots:
4910 dependencies:
4911 '@algolia/client-common': 5.52.0
4912
4913 - '@antfu/eslint-config@8.2.0(@typescript-eslint/rule-tester@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.59.1(typescript@5.9.3))(@typescript-eslint/utils@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.33)(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.5(@types/node@25.6.0)(jsdom@29.1.1)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.99.0)(yaml@2.8.3)))':
4913 + '@antfu/eslint-config@8.2.0(@typescript-eslint/rule-tester@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.59.1(typescript@5.9.3))(@typescript-eslint/utils@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.33)(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.5(@types/node@25.6.0)(jsdom@29.1.1)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.99.0)(yaml@2.8.3)))':
4914 dependencies:
4915 '@antfu/install-pkg': 1.1.0
4916 '@clack/prompts': 1.3.0
4917 - '@e18e/eslint-plugin': 0.3.0(eslint@10.2.1(jiti@2.6.1))
4918 - '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@10.2.1(jiti@2.6.1))
4917 + '@e18e/eslint-plugin': 0.3.0(eslint@10.3.0(jiti@2.6.1))
4918 + '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@10.3.0(jiti@2.6.1))
4919 '@eslint/markdown': 8.0.1
4920 - '@stylistic/eslint-plugin': 5.10.0(eslint@10.2.1(jiti@2.6.1))
4921 - '@typescript-eslint/eslint-plugin': 8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
4922 - '@typescript-eslint/parser': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
4923 - '@vitest/eslint-plugin': 1.6.16(@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.5(@types/node@25.6.0)(jsdom@29.1.1)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.99.0)(yaml@2.8.3)))
4920 + '@stylistic/eslint-plugin': 5.10.0(eslint@10.3.0(jiti@2.6.1))
4921 + '@typescript-eslint/eslint-plugin': 8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)
4922 + '@typescript-eslint/parser': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)
4923 + '@vitest/eslint-plugin': 1.6.16(@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.5(@types/node@25.6.0)(jsdom@29.1.1)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.99.0)(yaml@2.8.3)))
4924 ansis: 4.2.0
4925 cac: 7.0.0
4926 - eslint: 10.2.1(jiti@2.6.1)
4927 - eslint-config-flat-gitignore: 2.3.0(eslint@10.2.1(jiti@2.6.1))
4926 + eslint: 10.3.0(jiti@2.6.1)
4927 + eslint-config-flat-gitignore: 2.3.0(eslint@10.3.0(jiti@2.6.1))
4928 eslint-flat-config-utils: 3.2.0
4929 - eslint-merge-processors: 2.0.0(eslint@10.2.1(jiti@2.6.1))
4930 - eslint-plugin-antfu: 3.2.2(eslint@10.2.1(jiti@2.6.1))
4931 - eslint-plugin-command: 3.5.2(@typescript-eslint/rule-tester@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.59.1(typescript@5.9.3))(@typescript-eslint/utils@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))
4932 - eslint-plugin-import-lite: 0.6.0(eslint@10.2.1(jiti@2.6.1))
4933 - eslint-plugin-jsdoc: 62.9.0(eslint@10.2.1(jiti@2.6.1))
4934 - eslint-plugin-jsonc: 3.1.2(eslint@10.2.1(jiti@2.6.1))
4935 - eslint-plugin-n: 17.24.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
4929 + eslint-merge-processors: 2.0.0(eslint@10.3.0(jiti@2.6.1))
4930 + eslint-plugin-antfu: 3.2.2(eslint@10.3.0(jiti@2.6.1))
4931 + eslint-plugin-command: 3.5.2(@typescript-eslint/rule-tester@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.59.1(typescript@5.9.3))(@typescript-eslint/utils@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.3.0(jiti@2.6.1))
4932 + eslint-plugin-import-lite: 0.6.0(eslint@10.3.0(jiti@2.6.1))
4933 + eslint-plugin-jsdoc: 62.9.0(eslint@10.3.0(jiti@2.6.1))
4934 + eslint-plugin-jsonc: 3.1.2(eslint@10.3.0(jiti@2.6.1))
4935 + eslint-plugin-n: 17.24.0(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)
4936 eslint-plugin-no-only-tests: 3.4.0
4937 - eslint-plugin-perfectionist: 5.9.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
4938 - eslint-plugin-pnpm: 1.6.0(eslint@10.2.1(jiti@2.6.1))
4939 - eslint-plugin-regexp: 3.1.0(eslint@10.2.1(jiti@2.6.1))
4940 - eslint-plugin-toml: 1.3.1(eslint@10.2.1(jiti@2.6.1))
4941 - eslint-plugin-unicorn: 64.0.0(eslint@10.2.1(jiti@2.6.1))
4942 - eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))
4943 - eslint-plugin-vue: 10.9.0(@stylistic/eslint-plugin@5.10.0(eslint@10.2.1(jiti@2.6.1)))(@typescript-eslint/parser@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.1(jiti@2.6.1)))
4944 - eslint-plugin-yml: 3.3.2(eslint@10.2.1(jiti@2.6.1))
4945 - eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.33)(eslint@10.2.1(jiti@2.6.1))
4937 + eslint-plugin-perfectionist: 5.9.0(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)
4938 + eslint-plugin-pnpm: 1.6.0(eslint@10.3.0(jiti@2.6.1))
4939 + eslint-plugin-regexp: 3.1.0(eslint@10.3.0(jiti@2.6.1))
4940 + eslint-plugin-toml: 1.3.1(eslint@10.3.0(jiti@2.6.1))
4941 + eslint-plugin-unicorn: 64.0.0(eslint@10.3.0(jiti@2.6.1))
4942 + eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.3.0(jiti@2.6.1))
4943 + eslint-plugin-vue: 10.9.0(@stylistic/eslint-plugin@5.10.0(eslint@10.3.0(jiti@2.6.1)))(@typescript-eslint/parser@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.3.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.3.0(jiti@2.6.1)))
4944 + eslint-plugin-yml: 3.3.2(eslint@10.3.0(jiti@2.6.1))
4945 + eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.33)(eslint@10.3.0(jiti@2.6.1))
4946 globals: 17.6.0
4947 local-pkg: 1.1.2
4948 parse-gitignore: 2.0.0
4949 toml-eslint-parser: 1.0.3
4950 - vue-eslint-parser: 10.4.0(eslint@10.2.1(jiti@2.6.1))
4950 + vue-eslint-parser: 10.4.0(eslint@10.3.0(jiti@2.6.1))
4951 yaml-eslint-parser: 2.0.0
4952 transitivePeerDependencies:
4953 - '@eslint/json'
@@ -5306,11 +5306,11 @@ snapshots:
5306
5307 '@csstools/css-tokenizer@4.0.0': {}
5308
5309 - '@e18e/eslint-plugin@0.3.0(eslint@10.2.1(jiti@2.6.1))':
5309 + '@e18e/eslint-plugin@0.3.0(eslint@10.3.0(jiti@2.6.1))':
5310 dependencies:
5311 - eslint-plugin-depend: 1.5.0(eslint@10.2.1(jiti@2.6.1))
5311 + eslint-plugin-depend: 1.5.0(eslint@10.3.0(jiti@2.6.1))
5312 optionalDependencies:
5313 - eslint: 10.2.1(jiti@2.6.1)
5313 + eslint: 10.3.0(jiti@2.6.1)
5314
5315 '@emotion/hash@0.8.0': {}
5316
@@ -5410,24 +5410,24 @@ snapshots:
5410 '@esbuild/win32-x64@0.27.7':
5411 optional: true
5412
5413 - '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.2.1(jiti@2.6.1))':
5413 + '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.3.0(jiti@2.6.1))':
5414 dependencies:
5415 escape-string-regexp: 4.0.0
5416 - eslint: 10.2.1(jiti@2.6.1)
5416 + eslint: 10.3.0(jiti@2.6.1)
5417 ignore: 7.0.5
5418
5419 - '@eslint-community/eslint-utils@4.9.1(eslint@10.2.1(jiti@2.6.1))':
5419 + '@eslint-community/eslint-utils@4.9.1(eslint@10.3.0(jiti@2.6.1))':
5420 dependencies:
5421 - eslint: 10.2.1(jiti@2.6.1)
5421 + eslint: 10.3.0(jiti@2.6.1)
5422 eslint-visitor-keys: 3.4.3
5423
5424 '@eslint-community/regexpp@4.12.2': {}
5425
5426 - '@eslint/compat@2.0.5(eslint@10.2.1(jiti@2.6.1))':
5426 + '@eslint/compat@2.0.5(eslint@10.3.0(jiti@2.6.1))':
5427 dependencies:
5428 '@eslint/core': 1.2.1
5429 optionalDependencies:
5430 - eslint: 10.2.1(jiti@2.6.1)
5430 + eslint: 10.3.0(jiti@2.6.1)
5431
5432 '@eslint/config-array@0.23.5':
5433 dependencies:
@@ -5887,11 +5887,11 @@ snapshots:
5887
5888 '@standard-schema/spec@1.1.0': {}
5889
5890 - '@stylistic/eslint-plugin@5.10.0(eslint@10.2.1(jiti@2.6.1))':
5890 + '@stylistic/eslint-plugin@5.10.0(eslint@10.3.0(jiti@2.6.1))':
5891 dependencies:
5892 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
5892 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
5893 '@typescript-eslint/types': 8.59.1
5894 - eslint: 10.2.1(jiti@2.6.1)
5894 + eslint: 10.3.0(jiti@2.6.1)
5895 eslint-visitor-keys: 4.2.1
5896 espree: 10.4.0
5897 estraverse: 5.3.0
@@ -6065,15 +6065,15 @@ snapshots:
6065
6066 '@types/web-bluetooth@0.0.21': {}
6067
6068 - '@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)':
6068 + '@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)':
6069 dependencies:
6070 '@eslint-community/regexpp': 4.12.2
6071 - '@typescript-eslint/parser': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
6071 + '@typescript-eslint/parser': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)
6072 '@typescript-eslint/scope-manager': 8.59.1
6073 - '@typescript-eslint/type-utils': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
6074 - '@typescript-eslint/utils': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
6073 + '@typescript-eslint/type-utils': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)
6074 + '@typescript-eslint/utils': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)
6075 '@typescript-eslint/visitor-keys': 8.59.1
6076 - eslint: 10.2.1(jiti@2.6.1)
6076 + eslint: 10.3.0(jiti@2.6.1)
6077 ignore: 7.0.5
6078 natural-compare: 1.4.0
6079 ts-api-utils: 2.5.0(typescript@5.9.3)
@@ -6081,14 +6081,14 @@ snapshots:
6081 transitivePeerDependencies:
6082 - supports-color
6083
6084 - '@typescript-eslint/parser@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)':
6084 + '@typescript-eslint/parser@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)':
6085 dependencies:
6086 '@typescript-eslint/scope-manager': 8.59.1
6087 '@typescript-eslint/types': 8.59.1
6088 '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3)
6089 '@typescript-eslint/visitor-keys': 8.59.1
6090 debug: 4.4.3
6091 - eslint: 10.2.1(jiti@2.6.1)
6091 + eslint: 10.3.0(jiti@2.6.1)
6092 typescript: 5.9.3
6093 transitivePeerDependencies:
6094 - supports-color
@@ -6102,13 +6102,13 @@ snapshots:
6102 transitivePeerDependencies:
6103 - supports-color
6104
6105 - '@typescript-eslint/rule-tester@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)':
6105 + '@typescript-eslint/rule-tester@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)':
6106 dependencies:
6107 - '@typescript-eslint/parser': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
6107 + '@typescript-eslint/parser': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)
6108 '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3)
6109 - '@typescript-eslint/utils': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
6109 + '@typescript-eslint/utils': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)
6110 ajv: 6.15.0
6111 - eslint: 10.2.1(jiti@2.6.1)
6111 + eslint: 10.3.0(jiti@2.6.1)
6112 json-stable-stringify-without-jsonify: 1.0.1
6113 lodash.merge: 4.6.2
6114 semver: 7.7.4
@@ -6125,13 +6125,13 @@ snapshots:
6125 dependencies:
6126 typescript: 5.9.3
6127
6128 - '@typescript-eslint/type-utils@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)':
6128 + '@typescript-eslint/type-utils@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)':
6129 dependencies:
6130 '@typescript-eslint/types': 8.59.1
6131 '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3)
6132 - '@typescript-eslint/utils': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
6132 + '@typescript-eslint/utils': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)
6133 debug: 4.4.3
6134 - eslint: 10.2.1(jiti@2.6.1)
6134 + eslint: 10.3.0(jiti@2.6.1)
6135 ts-api-utils: 2.5.0(typescript@5.9.3)
6136 typescript: 5.9.3
6137 transitivePeerDependencies:
@@ -6154,13 +6154,13 @@ snapshots:
6154 transitivePeerDependencies:
6155 - supports-color
6156
6157 - '@typescript-eslint/utils@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)':
6157 + '@typescript-eslint/utils@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)':
6158 dependencies:
6159 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
6159 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
6160 '@typescript-eslint/scope-manager': 8.59.1
6161 '@typescript-eslint/types': 8.59.1
6162 '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3)
6163 - eslint: 10.2.1(jiti@2.6.1)
6163 + eslint: 10.3.0(jiti@2.6.1)
6164 typescript: 5.9.3
6165 transitivePeerDependencies:
6166 - supports-color
@@ -6190,13 +6190,13 @@ snapshots:
6190 vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.99.0)(yaml@2.8.3)
6191 vue: 3.5.33(typescript@5.9.3)
6192
6193 - '@vitest/eslint-plugin@1.6.16(@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.5(@types/node@25.6.0)(jsdom@29.1.1)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.99.0)(yaml@2.8.3)))':
6193 + '@vitest/eslint-plugin@1.6.16(@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.5(@types/node@25.6.0)(jsdom@29.1.1)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.99.0)(yaml@2.8.3)))':
6194 dependencies:
6195 '@typescript-eslint/scope-manager': 8.59.1
6196 - '@typescript-eslint/utils': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
6197 - eslint: 10.2.1(jiti@2.6.1)
6196 + '@typescript-eslint/utils': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)
6197 + eslint: 10.3.0(jiti@2.6.1)
6198 optionalDependencies:
6199 - '@typescript-eslint/eslint-plugin': 8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
6199 + '@typescript-eslint/eslint-plugin': 8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)
6200 typescript: 5.9.3
6201 vitest: 4.1.5(@types/node@25.6.0)(jsdom@29.1.1)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.99.0)(yaml@2.8.3))
6202 transitivePeerDependencies:
@@ -6566,7 +6566,7 @@ snapshots:
6566
6567 asynckit@0.4.0: {}
6568
6569 - axios@1.15.2(debug@4.4.3):
6569 + axios@1.16.0(debug@4.4.3):
6570 dependencies:
6571 follow-redirects: 1.16.0(debug@4.4.3)
6572 form-data: 4.0.5
@@ -7077,62 +7077,62 @@ snapshots:
7077
7078 escape-string-regexp@5.0.0: {}
7079
7080 - eslint-compat-utils@0.5.1(eslint@10.2.1(jiti@2.6.1)):
7080 + eslint-compat-utils@0.5.1(eslint@10.3.0(jiti@2.6.1)):
7081 dependencies:
7082 - eslint: 10.2.1(jiti@2.6.1)
7082 + eslint: 10.3.0(jiti@2.6.1)
7083 semver: 7.7.4
7084
7085 - eslint-config-flat-gitignore@2.3.0(eslint@10.2.1(jiti@2.6.1)):
7085 + eslint-config-flat-gitignore@2.3.0(eslint@10.3.0(jiti@2.6.1)):
7086 dependencies:
7087 - '@eslint/compat': 2.0.5(eslint@10.2.1(jiti@2.6.1))
7088 - eslint: 10.2.1(jiti@2.6.1)
7087 + '@eslint/compat': 2.0.5(eslint@10.3.0(jiti@2.6.1))
7088 + eslint: 10.3.0(jiti@2.6.1)
7089
7090 eslint-flat-config-utils@3.2.0:
7091 dependencies:
7092 '@eslint/config-helpers': 0.5.5
7093 pathe: 2.0.3
7094
7095 - eslint-json-compat-utils@0.2.3(eslint@10.2.1(jiti@2.6.1))(jsonc-eslint-parser@3.1.0):
7095 + eslint-json-compat-utils@0.2.3(eslint@10.3.0(jiti@2.6.1))(jsonc-eslint-parser@3.1.0):
7096 dependencies:
7097 - eslint: 10.2.1(jiti@2.6.1)
7097 + eslint: 10.3.0(jiti@2.6.1)
7098 esquery: 1.7.0
7099 jsonc-eslint-parser: 3.1.0
7100
7101 - eslint-merge-processors@2.0.0(eslint@10.2.1(jiti@2.6.1)):
7101 + eslint-merge-processors@2.0.0(eslint@10.3.0(jiti@2.6.1)):
7102 dependencies:
7103 - eslint: 10.2.1(jiti@2.6.1)
7103 + eslint: 10.3.0(jiti@2.6.1)
7104
7105 - eslint-plugin-antfu@3.2.2(eslint@10.2.1(jiti@2.6.1)):
7105 + eslint-plugin-antfu@3.2.2(eslint@10.3.0(jiti@2.6.1)):
7106 dependencies:
7107 - eslint: 10.2.1(jiti@2.6.1)
7107 + eslint: 10.3.0(jiti@2.6.1)
7108
7109 - eslint-plugin-command@3.5.2(@typescript-eslint/rule-tester@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.59.1(typescript@5.9.3))(@typescript-eslint/utils@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1)):
7109 + eslint-plugin-command@3.5.2(@typescript-eslint/rule-tester@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.59.1(typescript@5.9.3))(@typescript-eslint/utils@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.3.0(jiti@2.6.1)):
7110 dependencies:
7111 '@es-joy/jsdoccomment': 0.84.0
7112 - '@typescript-eslint/rule-tester': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
7112 + '@typescript-eslint/rule-tester': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)
7113 '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3)
7114 - '@typescript-eslint/utils': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
7115 - eslint: 10.2.1(jiti@2.6.1)
7114 + '@typescript-eslint/utils': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)
7115 + eslint: 10.3.0(jiti@2.6.1)
7116
7117 - eslint-plugin-depend@1.5.0(eslint@10.2.1(jiti@2.6.1)):
7117 + eslint-plugin-depend@1.5.0(eslint@10.3.0(jiti@2.6.1)):
7118 dependencies:
7119 empathic: 2.0.0
7120 - eslint: 10.2.1(jiti@2.6.1)
7120 + eslint: 10.3.0(jiti@2.6.1)
7121 module-replacements: 2.11.0
7122 semver: 7.7.4
7123
7124 - eslint-plugin-es-x@7.8.0(eslint@10.2.1(jiti@2.6.1)):
7124 + eslint-plugin-es-x@7.8.0(eslint@10.3.0(jiti@2.6.1)):
7125 dependencies:
7126 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
7126 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
7127 '@eslint-community/regexpp': 4.12.2
7128 - eslint: 10.2.1(jiti@2.6.1)
7129 - eslint-compat-utils: 0.5.1(eslint@10.2.1(jiti@2.6.1))
7128 + eslint: 10.3.0(jiti@2.6.1)
7129 + eslint-compat-utils: 0.5.1(eslint@10.3.0(jiti@2.6.1))
7130
7131 - eslint-plugin-import-lite@0.6.0(eslint@10.2.1(jiti@2.6.1)):
7131 + eslint-plugin-import-lite@0.6.0(eslint@10.3.0(jiti@2.6.1)):
7132 dependencies:
7133 - eslint: 10.2.1(jiti@2.6.1)
7133 + eslint: 10.3.0(jiti@2.6.1)
7134
7135 - eslint-plugin-jsdoc@62.9.0(eslint@10.2.1(jiti@2.6.1)):
7135 + eslint-plugin-jsdoc@62.9.0(eslint@10.3.0(jiti@2.6.1)):
7136 dependencies:
7137 '@es-joy/jsdoccomment': 0.86.0
7138 '@es-joy/resolve.exports': 1.2.0
@@ -7140,7 +7140,7 @@ snapshots:
7140 comment-parser: 1.4.6
7141 debug: 4.4.3
7142 escape-string-regexp: 4.0.0
7143 - eslint: 10.2.1(jiti@2.6.1)
7143 + eslint: 10.3.0(jiti@2.6.1)
7144 espree: 11.2.0
7145 esquery: 1.7.0
7146 html-entities: 2.6.0
@@ -7152,27 +7152,27 @@ snapshots:
7152 transitivePeerDependencies:
7153 - supports-color
7154
7155 - eslint-plugin-jsonc@3.1.2(eslint@10.2.1(jiti@2.6.1)):
7155 + eslint-plugin-jsonc@3.1.2(eslint@10.3.0(jiti@2.6.1)):
7156 dependencies:
7157 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
7157 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
7158 '@eslint/core': 1.2.1
7159 '@eslint/plugin-kit': 0.6.1
7160 '@ota-meshi/ast-token-store': 0.3.0
7161 diff-sequences: 29.6.3
7162 - eslint: 10.2.1(jiti@2.6.1)
7163 - eslint-json-compat-utils: 0.2.3(eslint@10.2.1(jiti@2.6.1))(jsonc-eslint-parser@3.1.0)
7162 + eslint: 10.3.0(jiti@2.6.1)
7163 + eslint-json-compat-utils: 0.2.3(eslint@10.3.0(jiti@2.6.1))(jsonc-eslint-parser@3.1.0)
7164 jsonc-eslint-parser: 3.1.0
7165 natural-compare: 1.4.0
7166 synckit: 0.11.12
7167 transitivePeerDependencies:
7168 - '@eslint/json'
7169
7170 - eslint-plugin-n@17.24.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3):
7170 + eslint-plugin-n@17.24.0(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3):
7171 dependencies:
7172 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
7172 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
7173 enhanced-resolve: 5.21.0
7174 - eslint: 10.2.1(jiti@2.6.1)
7175 - eslint-plugin-es-x: 7.8.0(eslint@10.2.1(jiti@2.6.1))
7174 + eslint: 10.3.0(jiti@2.6.1)
7175 + eslint-plugin-es-x: 7.8.0(eslint@10.3.0(jiti@2.6.1))
7176 get-tsconfig: 4.14.0
7177 globals: 15.15.0
7178 globrex: 0.1.2
@@ -7184,19 +7184,19 @@ snapshots:
7184
7185 eslint-plugin-no-only-tests@3.4.0: {}
7186
7187 - eslint-plugin-perfectionist@5.9.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3):
7187 + eslint-plugin-perfectionist@5.9.0(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3):
7188 dependencies:
7189 - '@typescript-eslint/utils': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
7190 - eslint: 10.2.1(jiti@2.6.1)
7189 + '@typescript-eslint/utils': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)
7190 + eslint: 10.3.0(jiti@2.6.1)
7191 natural-orderby: 5.0.0
7192 transitivePeerDependencies:
7193 - supports-color
7194 - typescript
7195
7196 - eslint-plugin-pnpm@1.6.0(eslint@10.2.1(jiti@2.6.1)):
7196 + eslint-plugin-pnpm@1.6.0(eslint@10.3.0(jiti@2.6.1)):
7197 dependencies:
7198 empathic: 2.0.0
7199 - eslint: 10.2.1(jiti@2.6.1)
7199 + eslint: 10.3.0(jiti@2.6.1)
7200 jsonc-eslint-parser: 3.1.0
7201 pathe: 2.0.3
7202 pnpm-workspace-yaml: 1.6.0
@@ -7204,37 +7204,37 @@ snapshots:
7204 yaml: 2.8.3
7205 yaml-eslint-parser: 2.0.0
7206
7207 - eslint-plugin-regexp@3.1.0(eslint@10.2.1(jiti@2.6.1)):
7207 + eslint-plugin-regexp@3.1.0(eslint@10.3.0(jiti@2.6.1)):
7208 dependencies:
7209 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
7209 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
7210 '@eslint-community/regexpp': 4.12.2
7211 comment-parser: 1.4.6
7212 - eslint: 10.2.1(jiti@2.6.1)
7212 + eslint: 10.3.0(jiti@2.6.1)
7213 jsdoc-type-pratt-parser: 7.2.0
7214 refa: 0.12.1
7215 regexp-ast-analysis: 0.7.1
7216 scslre: 0.3.0
7217
7218 - eslint-plugin-toml@1.3.1(eslint@10.2.1(jiti@2.6.1)):
7218 + eslint-plugin-toml@1.3.1(eslint@10.3.0(jiti@2.6.1)):
7219 dependencies:
7220 '@eslint/core': 1.2.1
7221 '@eslint/plugin-kit': 0.6.1
7222 '@ota-meshi/ast-token-store': 0.3.0
7223 debug: 4.4.3
7224 - eslint: 10.2.1(jiti@2.6.1)
7224 + eslint: 10.3.0(jiti@2.6.1)
7225 toml-eslint-parser: 1.0.3
7226 transitivePeerDependencies:
7227 - supports-color
7228
7229 - eslint-plugin-unicorn@64.0.0(eslint@10.2.1(jiti@2.6.1)):
7229 + eslint-plugin-unicorn@64.0.0(eslint@10.3.0(jiti@2.6.1)):
7230 dependencies:
7231 '@babel/helper-validator-identifier': 7.28.5
7232 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
7232 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
7233 change-case: 5.4.4
7234 ci-info: 4.4.0
7235 clean-regexp: 1.0.0
7236 core-js-compat: 3.49.0
7237 - eslint: 10.2.1(jiti@2.6.1)
7237 + eslint: 10.3.0(jiti@2.6.1)
7238 find-up-simple: 1.0.1
7239 globals: 17.6.0
7240 indent-string: 5.0.0
@@ -7246,41 +7246,41 @@ snapshots:
7246 semver: 7.7.4
7247 strip-indent: 4.1.1
7248
7249 - eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1)):
7249 + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.3.0(jiti@2.6.1)):
7250 dependencies:
7251 - eslint: 10.2.1(jiti@2.6.1)
7251 + eslint: 10.3.0(jiti@2.6.1)
7252 optionalDependencies:
7253 - '@typescript-eslint/eslint-plugin': 8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
7253 + '@typescript-eslint/eslint-plugin': 8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)
7254
7255 - eslint-plugin-vue@10.9.0(@stylistic/eslint-plugin@5.10.0(eslint@10.2.1(jiti@2.6.1)))(@typescript-eslint/parser@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.1(jiti@2.6.1))):
7255 + eslint-plugin-vue@10.9.0(@stylistic/eslint-plugin@5.10.0(eslint@10.3.0(jiti@2.6.1)))(@typescript-eslint/parser@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.3.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.3.0(jiti@2.6.1))):
7256 dependencies:
7257 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
7258 - eslint: 10.2.1(jiti@2.6.1)
7257 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
7258 + eslint: 10.3.0(jiti@2.6.1)
7259 natural-compare: 1.4.0
7260 nth-check: 2.1.1
7261 postcss-selector-parser: 7.1.1
7262 semver: 7.7.4
7263 - vue-eslint-parser: 10.4.0(eslint@10.2.1(jiti@2.6.1))
7263 + vue-eslint-parser: 10.4.0(eslint@10.3.0(jiti@2.6.1))
7264 xml-name-validator: 4.0.0
7265 optionalDependencies:
7266 - '@stylistic/eslint-plugin': 5.10.0(eslint@10.2.1(jiti@2.6.1))
7267 - '@typescript-eslint/parser': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
7266 + '@stylistic/eslint-plugin': 5.10.0(eslint@10.3.0(jiti@2.6.1))
7267 + '@typescript-eslint/parser': 8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@5.9.3)
7268
7269 - eslint-plugin-yml@3.3.2(eslint@10.2.1(jiti@2.6.1)):
7269 + eslint-plugin-yml@3.3.2(eslint@10.3.0(jiti@2.6.1)):
7270 dependencies:
7271 '@eslint/core': 1.2.1
7272 '@eslint/plugin-kit': 0.7.1
7273 '@ota-meshi/ast-token-store': 0.3.0
7274 diff-sequences: 29.6.3
7275 escape-string-regexp: 5.0.0
7276 - eslint: 10.2.1(jiti@2.6.1)
7276 + eslint: 10.3.0(jiti@2.6.1)
7277 natural-compare: 1.4.0
7278 yaml-eslint-parser: 2.0.0
7279
7280 - eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.33)(eslint@10.2.1(jiti@2.6.1)):
7280 + eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.33)(eslint@10.3.0(jiti@2.6.1)):
7281 dependencies:
7282 '@vue/compiler-sfc': 3.5.33
7283 - eslint: 10.2.1(jiti@2.6.1)
7283 + eslint: 10.3.0(jiti@2.6.1)
7284
7285 eslint-scope@9.1.2:
7286 dependencies:
@@ -7295,9 +7295,9 @@ snapshots:
7295
7296 eslint-visitor-keys@5.0.1: {}
7297
7298 - eslint@10.2.1(jiti@2.6.1):
7298 + eslint@10.3.0(jiti@2.6.1):
7299 dependencies:
7300 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
7300 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
7301 '@eslint-community/regexpp': 4.12.2
7302 '@eslint/config-array': 0.23.5
7303 '@eslint/config-helpers': 0.5.5
@@ -9436,10 +9436,10 @@ snapshots:
9436
9437 vue-component-type-helpers@3.2.7: {}
9438
9439 - vue-eslint-parser@10.4.0(eslint@10.2.1(jiti@2.6.1)):
9439 + vue-eslint-parser@10.4.0(eslint@10.3.0(jiti@2.6.1)):
9440 dependencies:
9441 debug: 4.4.3
9442 - eslint: 10.2.1(jiti@2.6.1)
9442 + eslint: 10.3.0(jiti@2.6.1)
9443 eslint-scope: 9.1.2
9444 eslint-visitor-keys: 5.0.1
9445 espree: 11.2.0
@@ -9538,7 +9538,7 @@ snapshots:
9538
9539 wait-on@9.0.5(debug@4.4.3):
9540 dependencies:
9541 - axios: 1.15.2(debug@4.4.3)
9541 + axios: 1.16.0(debug@4.4.3)
9542 joi: 18.1.2
9543 lodash: 4.18.1
9544 minimist: 1.2.8
frontend/src/components/customers/aiNotifications/CustomerAiNotificationRoutes/CustomerAiNotificationRouteForm.vue
+84 -126
@@ -1,98 +1,83 @@
1 <template>
2 - <n-form ref="formRef" :model="form" :rules label-placement="top">
2 + <n-form ref="formRef" :model="form" :rules label-placement="top" class="flex flex-col gap-1">
3 <n-form-item label="Name" path="name">
4 <n-input v-model:value="form.name" placeholder="e.g. SOC team Slack #alerts" :maxlength="128" show-count />
5 </n-form-item>
6
7 - <n-form-item label="Minimum severity" path="min_severity">
8 - <n-select v-model:value="form.min_severity" :options="severityOptions" />
9 - <template v-if="!fieldErrors.min_severity" #feedback>
10 - <span class="text-tertiary text-xs">
11 - Route fires when the investigation's severity is at this tier or higher. The route is hard-bound to
12 - the
13 - <code>investigation_complete</code>
14 - event — every Talon-driven investigation runs through these filters.
15 - </span>
7 + <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
8 + <n-form-item label="Trigger" path="trigger">
9 + <n-select v-model:value="form.trigger" :options="triggerOptions" />
10 + </n-form-item>
11 +
12 + <n-form-item label="Minimum severity" path="min_severity">
13 + <n-select v-model:value="form.min_severity" :options="severityOptions" />
14 + </n-form-item>
15 + </div>
16 +
17 + <!--
18 + Channel is now Shuffle-only — the SMTP path was removed when we
19 + consolidated email delivery into Shuffle's catalog (Outlook,
20 + Gmail, SendGrid, SMTP-via-Shuffle, etc. all live there). Field
21 + stays in the form for clarity but is fixed to a single value.
22 + -->
23 + <n-form-item label="Channel">
24 + <n-select v-model:value="form.channel" :options="channelOptions" disabled />
25 + <template v-if="!fieldErrors.channel" #feedback>
26 + Email is direct SMTP via CoPilot's deployment config. Shuffle proxies to
27 + 3,000+ integrations through a customer's authenticated Shuffle org.
28 </template>
29 </n-form-item>
30
19 - <n-form-item label="Channel" path="channel">
31 + <n-form-item label="Shuffle integration" path="shuffle_integration_id">
32 <n-select
21 - v-model:value="form.channel"
22 - :options="channelOptions"
23 - to="body"
24 - @update:value="onChannelChange"
33 + v-model:value="form.shuffle_integration_id"
34 + :options="integrationOptions"
35 + placeholder="Pick a Shuffle org for this customer"
36 + :loading="loadingIntegrations"
37 + @update:value="onIntegrationChange"
38 />
26 - <template v-if="!fieldErrors.channel" #feedback>
27 - Email is direct SMTP via CoPilot's deployment config. Shuffle proxies to 3,000+ integrations through a
28 - customer's authenticated Shuffle org.
39 + <template
40 + v-if="!fieldErrors.shuffle_integration_id && !integrationOptions.length && !loadingIntegrations"
41 + #feedback
42 + >
43 + No Shuffle integrations configured for this customer yet — go to the
44 + <strong>Shuffle integrations</strong> tab to add one first.
45 </template>
46 </n-form-item>
47
32 - <!-- SMTP-specific: recipient emails -->
33 - <n-form-item v-if="form.channel === 'smtp_email'" label="Recipient email(s)" path="destination">
34 - <n-input v-model:value="form.destination" placeholder="soc@example.com, ir@example.com" type="text" />
35 - </n-form-item>
36 -
37 - <!-- Shuffle-specific: integration picker, app picker, destination hint -->
38 - <template v-if="form.channel === 'shuffle'">
39 - <n-form-item label="Shuffle integration" path="shuffle_integration_id">
40 - <n-select
41 - v-model:value="form.shuffle_integration_id"
42 - :options="integrationOptions"
43 - placeholder="Pick a Shuffle org for this customer"
44 - :loading="loadingIntegrations"
45 - @update:value="onIntegrationChange"
46 - />
47 - <template
48 - v-if="!fieldErrors.shuffle_integration_id && !integrationOptions.length && !loadingIntegrations"
49 - #feedback
50 - >
51 - No Shuffle integrations configured for this customer yet — go to the
52 - <strong>Shuffle integrations</strong>
53 - tab to add one first.
54 - </template>
55 - </n-form-item>
56 -
57 - <n-form-item label="Shuffle app" path="shuffle_app_id">
58 - <n-select
59 - v-model:value="form.shuffle_app_id"
60 - :options="appOptions"
61 - placeholder="Pick an authenticated app"
62 - :loading="loadingApps"
63 - :disabled="!form.shuffle_integration_id || loadingApps"
64 - filterable
65 - @update:value="onAppChange"
66 - />
67 - <template
68 - v-if="
69 - !fieldErrors.shuffle_app_id &&
70 - form.shuffle_integration_id &&
71 - !appOptions.length &&
72 - !loadingApps &&
73 - appsError
74 - "
75 - #feedback
76 - >
48 + <n-form-item label="Shuffle app" path="shuffle_app_id">
49 + <n-select
50 + v-model:value="form.shuffle_app_id"
51 + :options="appOptions"
52 + placeholder="Pick an authenticated app"
53 + :loading="loadingApps"
54 + :disabled="!form.shuffle_integration_id || loadingApps"
55 + filterable
56 + @update:value="onAppChange"
57 + />
58 + <template
59 + v-if="!fieldErrors.shuffle_app_id && form.shuffle_integration_id && !appOptions.length && !loadingApps && appsError"
60 + #feedback
61 + >
62 + <div class="text-error">
63 Couldn't fetch apps from Shuffle: {{ appsError }}
78 - </template>
79 - </n-form-item>
80 -
81 - <n-form-item label="Destination hint" path="destination">
82 - <n-input
83 - v-model:value="form.destination"
84 - placeholder="e.g. #soc-alerts, soc@example.com, @user-id"
85 - type="text"
86 - />
87 - <template v-if="!fieldErrors.destination" #feedback>
88 - Free-form — gets prepended to the outgoing message as a
89 - <code>Send to &lt;destination&gt;: …</code>
90 - hint so the Shuffle app agent knows where to deliver. Channel name for Slack, email for Outlook,
91 - etc.
92 - </template>
93 - </n-form-item>
94 - </template>
64 + </div>
65 + </template>
66 + </n-form-item>
67
68 + <n-form-item label="Destination hint" path="destination">
69 + <n-input
70 + v-model:value="form.destination"
71 + placeholder="e.g. #soc-alerts, soc@example.com, @user-id"
72 + type="text"
73 + />
74 + <template v-if="!fieldErrors.destination" #feedback>
75 + Free-form — gets prepended to the outgoing message as a
76 + <code>Send to &lt;destination&gt;: …</code> hint so the Shuffle app
77 + agent knows where to deliver. Channel name for Slack, email for
78 + Outlook / Gmail, handle for chat apps, etc.
79 + </template>
80 + </n-form-item>
81 <n-form-item label="Custom message template (optional)" path="format_template" :show-feedback="false">
82 <n-input
83 v-model:value="form.format_template"
@@ -118,7 +103,6 @@
103 <script setup lang="ts">
104 import type { FormInst, FormRules } from "naive-ui"
105 import type {
121 - NotificationChannel,
106 NotificationRoute,
107 NotificationRoutePayload,
108 NotificationSeverity,
@@ -148,12 +132,16 @@ const submitting = ref(false)
132 const editing = computed(() => props.editingRoute !== null)
133 type FeedbackField = "channel" | "destination" | "min_severity" | "shuffle_app_id" | "shuffle_integration_id"
134
135 +// Channel is hard-coded to "shuffle" — the SMTP path was removed.
136 +// Existing routes loaded via the editing path may have legacy values
137 +// in the DB; we still use the route's channel as-is on edit (fall
138 +// back to "shuffle" for new routes).
139 const fieldErrors = reactive<Partial<Record<FeedbackField, string>>>({})
140
141 const form = reactive<NotificationRoutePayload>({
142 name: props.editingRoute?.name ?? "",
143 trigger: props.editingRoute?.trigger ?? ("investigation_complete" as NotificationTrigger),
156 - channel: props.editingRoute?.channel ?? ("smtp_email" as NotificationChannel),
144 + channel: props.editingRoute?.channel ?? "shuffle",
145 destination: props.editingRoute?.destination ?? "",
146 min_severity: props.editingRoute?.min_severity ?? ("Medium" as NotificationSeverity),
147 format_template: props.editingRoute?.format_template ?? "",
@@ -167,12 +155,17 @@ const form = reactive<NotificationRoutePayload>({
155 // Hidden from the UI — set programmatically on the form. Will become
156 // a select again when we add more dispatch event types (analyst-review
157 // hooks, IOC-enrichment alerts, scheduled-sweep findings).
170 -
171 -const channelOptions = [
172 - { label: "Email (SMTP)", value: "smtp_email" },
173 - { label: "Shuffle (Slack / Teams / Outlook / 3,000+ apps)", value: "shuffle" }
158 +const triggerOptions = [
159 + { label: "Every investigation completes", value: "investigation_complete" },
160 + { label: "Critical / High severity only", value: "severity_critical_or_high" }
161 ]
162
163 +// Single-option list — keeps the n-select rendering consistent with
164 +// the rest of the form even though there's nothing to pick. If we
165 +// ever add a second channel back (e.g. raw webhook), this is the line
166 +// to extend.
167 +const channelOptions = [{ label: "Shuffle (Slack / Teams / Outlook / 3,000+ apps)", value: "shuffle" }]
168 +
169 const severityOptions = [
170 { label: "Critical (only)", value: "Critical" },
171 { label: "High and above", value: "High" },
@@ -248,23 +241,6 @@ async function loadApps(integrationId: number) {
241 }
242 }
243
251 -function onChannelChange(value: NotificationChannel) {
252 - // Clear channel-specific fields when switching, so we don't carry
253 - // stale Shuffle picks into an SMTP route or vice versa.
254 - if (value === "smtp_email") {
255 - form.shuffle_integration_id = null
256 - form.shuffle_app_id = null
257 - form.shuffle_app_name = null
258 - } else {
259 - // Reset destination — SMTP recipients don't make sense as a
260 - // Shuffle channel hint.
261 - if (!editing.value) form.destination = ""
262 - }
263 - clearFieldError("destination")
264 - clearFieldError("shuffle_integration_id")
265 - clearFieldError("shuffle_app_id")
266 -}
267 -
244 async function onIntegrationChange(integrationId: number | null) {
245 apps.value = []
246 form.shuffle_app_id = null
@@ -284,14 +260,7 @@ function onAppChange(appId: string | null) {
260
261 const rules: FormRules = {
262 name: { required: true, message: "Name is required", trigger: ["input", "blur"] },
287 - channel: {
288 - validator: (_rule, value: NotificationChannel | null) => {
289 - if (!value) return createFieldError("channel", "Pick a channel")
290 - clearFieldError("channel")
291 - return true
292 - },
293 - trigger: ["change", "blur"]
294 - },
263 + trigger: { required: true, message: "Pick a trigger", trigger: ["change", "blur"] },
264 min_severity: {
265 validator: (_rule, value: NotificationSeverity | null) => {
266 if (!value) return createFieldError("min_severity", "Pick a severity threshold")
@@ -304,18 +273,7 @@ const rules: FormRules = {
273 required: true,
274 validator: (_rule, value: string) => {
275 if (!value || !value.trim()) {
307 - return form.channel === "smtp_email"
308 - ? createFieldError("destination", "At least one recipient email required")
309 - : createFieldError("destination", "Destination hint is required")
310 - }
311 - if (form.channel === "smtp_email") {
312 - const recipients = value
313 - .split(",")
314 - .map(s => s.trim())
315 - .filter(Boolean)
316 - if (!recipients.length) return createFieldError("destination", "At least one recipient email required")
317 - const bad = recipients.find(r => !r.includes("@"))
318 - if (bad) return createFieldError("destination", `Invalid email: ${bad}`)
276 + createFieldError("destination", "Destination hint is required")
277 }
278 clearFieldError("destination")
279 return true
@@ -323,6 +281,7 @@ const rules: FormRules = {
281 trigger: ["input", "blur"]
282 },
283 shuffle_integration_id: {
284 + required: true,
285 validator: (_rule, value: number | null) => {
286 if (form.channel === "shuffle" && !value) {
287 return createFieldError("shuffle_integration_id", "Pick a Shuffle integration")
@@ -333,10 +292,9 @@ const rules: FormRules = {
292 trigger: ["change", "blur"]
293 },
294 shuffle_app_id: {
295 + required: true,
296 validator: (_rule, value: string | null) => {
337 - if (form.channel === "shuffle" && !value) {
338 - return createFieldError("shuffle_app_id", "Pick a Shuffle app")
339 - }
297 + if (!value) return createFieldError("shuffle_app_id", "Pick a Shuffle app")
298 clearFieldError("shuffle_app_id")
299 return true
300 },
frontend/src/components/customers/aiNotifications/CustomerAiNotificationRoutes/CustomerAiNotificationRouteItem.vue
+7 -14
@@ -130,20 +130,13 @@ const loadingDelete = ref(false)
130 const message = useMessage()
131 const dFormats = useSettingsStore().dateFormat
132
133 -// Channel icon + label. Shuffle routes show the underlying app name
134 -// (cached on the route row at form-submit time, so we don't have to
135 -// roundtrip to Shuffle on every list render).
136 -const channelIcon = computed(() => {
137 - if (props.route.channel === "shuffle") return "carbon:integration"
138 - return "carbon:email"
139 -})
140 -
141 -const channelLabel = computed(() => {
142 - if (props.route.channel === "shuffle") {
143 - return props.route.shuffle_app_name ? `Shuffle · ${props.route.shuffle_app_name}` : "Shuffle"
144 - }
145 - return "SMTP email"
146 -})
133 +// All current routes are Shuffle-routed; the underlying Shuffle app
134 +// name is cached on the route row at form-submit time so we don't
135 +// need to roundtrip to Shuffle on every list render.
136 +const channelIcon = computed(() => "carbon:integration")
137 +const channelLabel = computed(() =>
138 + props.route.shuffle_app_name ? `Shuffle · ${props.route.shuffle_app_name}` : "Shuffle"
139 +)
140
141 const severityColor = computed<"danger" | "warning" | "success">(() => {
142 if (props.route.min_severity === "Critical" || props.route.min_severity === "High") return "danger"
frontend/src/theme/index.ts
+1 -1
@@ -121,7 +121,7 @@ export function getThemeOverrides(state: ThemeState): GlobalThemeOverrides {
121 titleFontWeight: state.fontWeight.cardTitle
122 },
123 Form: {
124 - feedbackPadding: "2px",
124 + feedbackPadding: "4px 2px 8px",
125 feedbackFontSizeSmall: "10px",
126 feedbackFontSizeMedium: "12px",
127 feedbackFontSizeLarge: "14px"
frontend/src/types/notifications.d.ts
+3 -3
@@ -1,6 +1,6 @@
1 // Notification routing — types mirror app/notifications/schema/notifications.py.
2 -// Kept intentionally narrow: Phase 1 ships Slack webhook + SMTP email only;
3 -// Phase 2 will extend the channel union with 'shuffle'.
2 +// Shuffle is the sole delivery channel; email, chat, ticketing, and the
3 +// rest flow through Shuffle's catalog of authenticated apps.
4
5 // Trigger represents the *event type* that caused the dispatch — not
6 // a severity filter (severity gating lives in min_severity). Currently
@@ -8,7 +8,7 @@
8 // analyst-review / IOC-enrichment / scheduled sweeps.
9 export type NotificationTrigger = "investigation_complete"
10
11 -export type NotificationChannel = "smtp_email" | "shuffle"
11 +export type NotificationChannel = "shuffle"
12
13 export type NotificationSeverity = "Critical" | "High" | "Medium" | "Low" | "Informational"
14