@cryptotaxi247 / CoPilot / commits / 4646aa48

feat: Phase 3b — embed @shuffleio/shuffle-mcps for per-org app management (#832)

* feat(shuffle): Phase 3b — embed @shuffleio/shuffle-mcps for per-org app management Path B from the planning conversation: switch to the shuffle-mcps React component as the strategic-direction package, migrate the existing /external-services/singul-app-auth page along with adding the new per-integration "Manage apps" drawer. ## Why React-in-Vue (and not a framework wrapper) @shuffleio/shuffle-mcps is React-only — peer deps require react>=18, react-dom>=18. We mount it manually via createRoot in a single Vue wrapper component (~50 lines glue) instead of pulling in vuera/veaury or similar. For one component this is cleaner: less abstraction, fewer transitive deps, easier to debug. ## Deps churn Removed: - @singulio/app-auth-search (Stencil web component, supplanted) Added: - @shuffleio/shuffle-mcps (the React picker) - react, react-dom (peer deps) - algoliasearch (peer dep — Shuffle's catalog index) - @types/react, @types/react-dom (dev) Bundle impact: ~+106 KB gzipped, lazy-loaded via the existing defineAsyncComponent pattern on the Customer detail tabs, so the main bundle is unchanged for users who never open Shuffle integrations. ## What lands - frontend/src/components/common/ShuffleMCPEmbed.vue Vue ↔ React glue. Mounts <ShuffleMCP> via react-dom/client, re- renders on prop change, unmounts cleanly. Handles the package's broken `./singul.css` export (CSS auto-imports through index.js via a hash-suffixed filename — package bug, doesn't affect us). - frontend/src/views/externalServices/SingulAppAuth.vue Migrated from <app-search-bar> (Stencil web component) to <ShuffleMCPEmbed>. Existing org-picker dropdown + auth-token fetch flow preserved. Drops the page-specific SCSS that styled the old web component's CSS variables — react-mcps brings its own styles. - frontend/src/components/customers/aiNotifications/CustomerShuffleAppsDrawer.vue NEW. Per-integration drawer that opens from the "Manage apps" button on each Shuffle integration row in the Customer detail page. Fetches the org's auth token via the existing /shuffle/organizations/{id} endpoint, passes it to ShuffleMCPEmbed so admins can search the 3,000+ catalog and authenticate apps directly. Surface a confirmation toast on selection — the new app shows up in the route form's app picker on next refresh. - frontend/src/components/customers/aiNotifications/CustomerShuffleIntegrationItem.vue Adds a "Manage apps" tooltip-button next to the existing Test/Edit/Delete row. Emits `manageApps` to the parent. - frontend/src/components/customers/aiNotifications/CustomerShuffleIntegrations.vue Wires the drawer into the integration list — listens to `manageApps`, opens the drawer scoped to the selected integration. - frontend/src/App.vue Removed the `defineCustomElements()` call from the old Stencil loader; ShuffleMCP doesn't need any global registration. - frontend/src/components/customers/aiNotifications/CustomerAiNotificationDispatchLog.vue Drive-by fix: stricter naive-ui DataTable types now reject the `string | number | Date` return from formatDate. Wrap in String() to coerce. ## Security note (per Phase 3b plan) The org auth token is exposed to the browser via the existing /shuffle/organizations/{id} endpoint. Admin scope is the gate; any admin browsing CoPilot already has effective access to the connector key in the DB. Documented as v1 trade-off in the planning doc; Phase 5 may swap to short-lived scoped tokens. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(shuffle): rename singul-app-auth → shuffle-app-auth The page name now matches the package family (@shuffleio/shuffle-mcps) and avoids the legacy "Singul" branding from when the upstream was called Singul.js. Three touchpoints: - Renamed: views/externalServices/SingulAppAuth.vue → ShuffleAppAuth.vue (git mv preserves history; component contents unchanged) - Router: path `singul-app-auth` → `shuffle-app-auth`, name `ExternalServices-SingulAppAuth` → `ExternalServices-ShuffleAppAuth`, meta.title `Singul App Auth` → `Shuffle App Auth` - Toolbar avatar nav: matching label + key updates No backend changes — page is frontend-only. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(customers): Implement nested vertical detail tabs * style(aiNotifications): Update tabs and spacing * style(aiTriggers): Update spacing and modal height * style(integrations): Adjust layout spacing * style(customers): Adjust layout spacing * style(customers): Adjust notifications layout * style(customers): Adjust provision layout * style(customers): Adjust customer info spacing * style(customers): Add AI trigger form title * style(customers): Add form title * style(customers): Adjust event source spacing * style(customers): Adjust form spacing * style(customers): Add 'Create Notification' title * style(customers): Adjust wizard spacing * Refactor(customers): Reorganize detail tabs * style(customers): Adjust detail modal layout * refactor(notifications): trigger is event-type only, not severity-band The trigger field was doing two jobs — categorizing the dispatch event AND filtering routes by severity. min_severity already does the severity-filter job better (five tiers with "and above" semantics versus trigger's two coarse buckets). Splitting them so each field has one clear responsibility. Symptom that motivated this: a route configured with trigger='severity_critical_or_high' wouldn't fire on Informational investigations even when the agent dispatched correctly — the trigger filter rejected it before min_severity got a chance to weigh in. With trigger now a pure event-type axis (single value: investigation_complete), routes filter by min_severity alone and the severity threshold works as expected. ## Backend - Schema: drop SEVERITY_CRITICAL_OR_HIGH from NotificationTrigger enum. Only INVESTIGATION_COMPLETE remains. Docstring rewritten to describe trigger as event-type, with a note about future event types (analyst-review hooks, scheduled-sweep findings, etc.). - Service: _trigger_applies() simplified to event-type equality. Backward-compat branch keeps existing routes with the legacy severity_critical_or_high value firing — they're treated as the catch-all event type so we don't go dark on rows that haven't been re-saved yet. Caller no longer passes severity to this filter. ## Frontend - Types: NotificationTrigger union narrowed to "investigation_complete" with comment explaining the event-type-not-severity semantics. - Route form: trigger select removed from the UI. Trigger field is now set programmatically on the form (always investigation_complete). When more event types arrive, this becomes a select again. min_severity field gains a feedback hint clarifying that the route is hard-bound to investigation_complete and severity does the gating. - Route item card: trigger badge dropped — every route's trigger is the same value. triggerLabel computed removed. min_severity badge remains as the visible filter dimension. ## Talon groups/copilot/notifications.md updated separately on nanoclaw main — agent always passes trigger="investigation_complete" instead of branching on severity. The dispatch tool call shape is unchanged otherwise. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(notifications): coerce legacy severity_critical_or_high trigger on read Routes saved against the previous schema have trigger='severity_critical_or_high' in MySQL. The new NotificationTrigger enum no longer accepts that value, so Pydantic 400's the route list endpoint when the UI reads them. Add a `@validator(pre=True)` on NotificationRouteBase.trigger that rewrites the legacy value to investigation_complete before the enum check runs. Mirrors the dispatcher's _trigger_applies backward-compat branch — that handles the filter-time comparison; this handles the read-API serialization. Together they keep legacy rows working without forcing a data migration on upgrade. Rows heal themselves on next save via the form (which always writes investigation_complete now). For deployments that want a clean state: UPDATE customer_notification_route SET trigger = 'investigation_complete' WHERE trigger = 'severity_critical_or_high'; Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(dispatchers): log payload details when dispatching to Shuffle Co-authored-by: Copilot <copilot@github.com> * docs(notifications): add operator guide for notification routing setup End-user walkthrough for SOC admins setting up per-customer notification routes. Lives in power-features alongside ai-analyst and ai-analyst-review — operator-tier docs, not deployment internals. Covers: - Why route notifications and where the UI lives (3 sub-tabs on the customer detail page) - One-time deployment prerequisite (Shuffle connector config) - Step-by-step walkthrough — add a Shuffle integration, authenticate apps via the Manage apps drawer, create a notification route, trigger a test investigation, verify in the dispatch log - End-to-end example: Slack #soc-alerts for High+ findings on a specific customer - Common patterns table (one channel for all severities, Critical-only paging, multi-destination, etc.) - How filtering actually works — the three checks every route must pass + idempotency rules - Custom message templates — the substitution token list, an example Slack-flavored template, and the watch-out about omitting {{summary}} (which we just hit during smoke testing) - Troubleshooting table covering the most likely failure modes I've seen during the rollout - Safety / guardrails — tenant scoping, best-effort semantics, admin-only access to routing config, org-auth-token exposure note Registered in docs.json under Power Features → Add-ons, between the existing ai-analyst-review entry and atomic-red-team. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * style(theme): Move form feedback to theme * style(notifications): Update AI route form and layout * fix(notifications): Enhance form validation and feedback for AI notification routes - Implemented field-specific error handling for channel, destination, min severity, shuffle integration, and shuffle app ID. - Updated feedback templates to conditionally display based on field errors. - Refactored validation rules to utilize custom error creation for improved clarity and maintainability. - Ensured that error states are cleared appropriately during field updates. * feat(notifications): Introduce Shuffle integration management and notification route forms - Added components for managing Shuffle integrations, including forms for adding and editing integrations. - Implemented a new route form for creating and managing AI notification routes, enhancing user experience with improved layout and validation. - Introduced a drawer for managing Shuffle apps associated with integrations, allowing for seamless authentication and app selection. - Updated existing components to accommodate new routing and integration functionalities, ensuring a cohesive user interface. * refactor(notifications): Simplify route item layout and enhance button functionality - Removed redundant badge for disabled status and replaced it with a toggle button for enabling/disabling routes. - Improved layout by adjusting spacing and grouping related information more clearly. - Added loading states for toggle and delete actions to enhance user feedback. - Updated the footer to include additional action buttons for editing and deleting routes, improving accessibility and usability. * refactor(notifications): Enhance route item display and improve editing experience - Updated the footer layout of the route item to utilize Badge components for better visual grouping of dispatch information. - Improved the edit and delete button functionality by adjusting their layout for better accessibility. - Modified the header of the route creation form to dynamically display the editing state, enhancing user clarity during route management. * refactor(notifications): Update CustomerShuffleIntegrations component for improved layout and user experience - Changed transition effect name for better consistency. - Enhanced form display with additional styling and improved header for clarity during integration editing. - Simplified layout by removing unnecessary elements and adjusting spacing for better readability. - Updated loading state handling and improved text structure for better user feedback. * refactor(notifications): Streamline CustomerShuffleIntegrationForm for improved usability - Simplified form structure by removing unnecessary elements and enhancing layout for better readability. - Improved error handling and user feedback during organization loading and selection. - Updated component imports and removed redundant code for cleaner implementation. * refactor(notifications): Enhance error handling and validation in CustomerShuffleIntegrationForm - Introduced field-specific error handling for the shuffle organization ID, improving user feedback during form submission. - Updated validation rules to utilize custom error creation, ensuring clearer messaging for required fields. - Simplified feedback template logic for better clarity when displaying error states. * refactor(notifications): Enhance manual entry input with transition effect in CustomerShuffleIntegrationForm - Wrapped the manual entry input field in a collapse transition for improved user experience. - Updated component imports to include the new transition component, ensuring a smoother visual effect when toggling the manual entry option. * refactor(notifications): Revamp CustomerShuffleIntegrationItem for enhanced usability and clarity - Replaced the disabled status badge with a toggle button for enabling/disabling integrations, improving user interaction. - Added additional badges to display integration details such as last used date and owner, enhancing information visibility. - Streamlined the layout for better organization of integration information and action buttons, improving overall user experience. - Updated error handling to provide clearer feedback during verification processes. * refactor(notifications): Update CustomerShuffleIntegrations layout and enhance information display - Added a class to the conditional rendering div for improved layout consistency. - Replaced a paragraph with an NAlert component to provide clearer information about Shuffle integrations. - Updated component imports to include NAlert, ensuring proper functionality and styling. * refactor(externalServices): Improve layout and styling in ShuffleAppAuth component - Updated the main container class for better layout consistency and overflow handling. - Enhanced the loading spinner with additional classes for improved visual presentation. - Simplified the inline property for the ShuffleMCPEmbed component, ensuring full height display. * refactor(ShuffleMCPEmbed): Update container class and optimize selectedApps handling - Changed the main container class to 'w-full' for improved layout consistency. - Removed redundant import of 'Root' from 'react-dom/client'. - Introduced a stable array for 'selectedApps' to prevent unnecessary render loops in the React component. * refactor(notifications): Improve layout and error handling in CustomerShuffleAppsDrawer component - Adjusted layout for better readability by simplifying text structure and spacing. - Enhanced error handling by specifying the type for caught errors, improving clarity in error messages. - Streamlined the inline property for the app selection input for consistency. * feat(styles): Add singul-override styles for improved layout and design - Introduced new SCSS file for singul-override styles to enhance the layout of the singul component. - Updated index.scss to include the new singul-override styles for better modularity and organization. * refactor(customers): Update CustomerItem styling for improved layout consistency - Changed the class from 'pt-0!' to 'p-4!' to enhance the padding and overall layout of the CustomerItem component. * format * precommit0fixes --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com> Co-authored-by: Copilot <copilot@github.com>

taylor_socfortress committed May 1, 2026 at 19:49 UTC 4646aa482f96eca26074ceb0b6a26f108b5157c1
44 files changed +1899 -1242
backend/app/connectors/portainer/utils/universal.py
+2 -1
@@ -84,6 +84,7 @@ async def verify_portainer_credentials(attributes: Dict[str, Any]) -> Dict[str,
84 f"{attributes['connector_url']}/api/v1/apps/authentication",
85 headers=headers,
86 verify=False,
87 + timeout=2,
88 )
89
90 # If API key auth fails, try JWT authentication
@@ -91,7 +92,7 @@ async def verify_portainer_credentials(attributes: Dict[str, Any]) -> Dict[str,
92 auth_endpoint = urljoin(attributes["connector_url"], "/api/auth")
93 auth_payload = {"username": attributes["connector_username"], "password": attributes["connector_password"]}
94
94 - jwt_response = requests.post(auth_endpoint, json=auth_payload, verify=False)
95 + jwt_response = requests.post(auth_endpoint, json=auth_payload, verify=False, timeout=2)
96
97 if jwt_response.status_code == 200:
98 jwt_token = jwt_response.json()["jwt"]
backend/app/notifications/schema/notifications.py
+26 -9
@@ -25,18 +25,18 @@ from pydantic import validator
25
26
27 class NotificationTrigger(str, Enum):
28 - """When a route should fire.
29 -
30 - `INVESTIGATION_COMPLETE` covers "tell me whenever the AI finishes,
31 - regardless of verdict" — useful for low-volume customers who want a
32 - receipt for every run. `SEVERITY_CRITICAL_OR_HIGH` is the noisier
33 - feed gated by severity for SOC teams that only want to be paged on
34 - real findings. More triggers can be added later (true-positive after
35 - review, IOC accuracy thresholds, etc.) without a schema change.
28 + """What kind of event caused this dispatch.
29 +
30 + Currently a single value — `investigation_complete` covers every
31 + Talon-driven dispatch (one per investigation that reaches the
32 + write-back step). Severity-based filtering lives entirely in the
33 + route's `min_severity` field, not here, so the trigger is purely
34 + an event-type dimension that grows when we add new dispatch
35 + sources (analyst-review hooks, scheduled-sweep findings,
36 + IOC-enrichment alerts, etc.).
37 """
38
39 INVESTIGATION_COMPLETE = "investigation_complete"
39 - SEVERITY_CRITICAL_OR_HIGH = "severity_critical_or_high"
40
41
42 class NotificationChannel(str, Enum):
@@ -96,6 +96,23 @@ class NotificationRouteBase(BaseModel):
96 name: str = Field(..., min_length=1, max_length=128, description="Human label for the rule (e.g. 'SOC team Slack #alerts').")
97 trigger: NotificationTrigger
98 channel: NotificationChannel
99 +
100 + @validator("trigger", pre=True)
101 + def _coerce_legacy_trigger(cls, v):
102 + """Coerce legacy `severity_critical_or_high` rows on read.
103 +
104 + Older versions of this schema treated trigger as a severity
105 + filter; routes saved against that schema have a stale value
106 + the new enum no longer accepts. Pydantic validates BEFORE the
107 + enum check when `pre=True`, so we rewrite the legacy value to
108 + the new event-type value here. The dispatch loop has the same
109 + backward-compat in `_trigger_applies` for the route-side
110 + comparison; this is the read-API equivalent.
111 + """
112 + if v == "severity_critical_or_high":
113 + return NotificationTrigger.INVESTIGATION_COMPLETE.value
114 + return v
115 +
116 # For SMTP: comma-separated recipient emails. For Shuffle: free-form
117 # destination hint (e.g. '#soc-alerts', 'ir@corp.com') that gets
118 # injected into Shuffle's natural-language input — Shuffle's app
backend/app/notifications/services/dispatchers.py
+1
@@ -187,6 +187,7 @@ async def dispatch_shuffle(
187 "environment": environment,
188 },
189 }
190 + logger.info(f"Dispatching payload to Shuffle body: {body}")
191 started = time.monotonic()
192 try:
193 async with httpx.AsyncClient(timeout=_SHUFFLE_TIMEOUT_S, http2=True) as client:
backend/app/notifications/services/notifications.py
+18 -14
@@ -445,18 +445,24 @@ def _severity_meets(report_severity: str, route_min: str) -> bool:
445 return False
446
447
448 -def _trigger_applies(report_trigger: str, route_trigger: str, severity: str) -> bool:
449 - """Decide whether a route's trigger matches the dispatch.
450 -
451 - `investigation_complete` always matches (it's the catch-all).
452 - `severity_critical_or_high` only matches when the report severity
453 - is Critical or High.
448 +def _trigger_applies(report_trigger: str, route_trigger: str) -> bool:
449 + """Decide whether a route's trigger matches the dispatch event type.
450 +
451 + Triggers represent the kind of event that caused the dispatch
452 + (currently only `investigation_complete`, with more event types
453 + arriving as we add hooks for analyst review / IOC enrichment /
454 + scheduled sweeps). Severity filtering lives in `min_severity`,
455 + not here — this function is purely an event-type equality check.
456 +
457 + Routes with stale `severity_critical_or_high` values from earlier
458 + schemas are treated as `investigation_complete` so they keep
459 + firing instead of being silently filtered out.
460 """
455 - if route_trigger != report_trigger:
456 - return False
457 - if route_trigger == NotificationTrigger.SEVERITY_CRITICAL_OR_HIGH.value:
458 - return severity in ("Critical", "High")
459 - return True
461 + if route_trigger == "severity_critical_or_high":
462 + # Backward compat: legacy enum value, treat as the catch-all
463 + # event type so existing routes don't go dark on upgrade.
464 + return report_trigger == NotificationTrigger.INVESTIGATION_COMPLETE.value
465 + return route_trigger == report_trigger
466
467
468 def _format_default_body(req: DispatchRequest) -> str:
@@ -611,9 +617,7 @@ async def dispatch(req: DispatchRequest, session: AsyncSession) -> DispatchRespo
617 matched_routes = [
618 r
619 for r in routes
614 - if r.enabled
615 - and _trigger_applies(req.trigger.value, r.trigger, req.severity_assessment.value)
616 - and _severity_meets(req.severity_assessment.value, r.min_severity)
620 + if r.enabled and _trigger_applies(req.trigger.value, r.trigger) and _severity_meets(req.severity_assessment.value, r.min_severity)
621 ]
622
623 outcomes: List[DispatchOutcome] = []
docs/docs.json
+1
@@ -200,6 +200,7 @@
200 "power-features/github-audit",
201 "power-features/ai-analyst",
202 "power-features/ai-analyst-review",
203 + "power-features/ai-analyst-notifications",
204 "power-features/atomic-red-team",
205 "power-features/report-creation",
206 "power-features/copilot-searches",
docs/power-features/ai-analyst-notifications.mdx new
+352
@@ -0,0 +1,352 @@
1 +---
2 +title: AI Analyst — Notification routing
3 +description: Send Talon's investigation reports to the customer's Slack, Outlook, Teams, or any of Shuffle's 3,000+ apps based on per-customer routing rules.
4 +---
5 +
6 +Every Talon investigation produces a report. The notification routing
7 +feature sends those reports out — to a Slack channel, an email
8 +distribution list, a Teams card, a ServiceNow ticket, or any of
9 +Shuffle's 3,000+ authenticated apps — based on rules a SOC admin
10 +configures per customer.
11 +
12 +This page is for **operators** (SOC admins setting up notifications).
13 +For the architecture and dispatch internals, see
14 +[AI Analyst (Talon)](/power-features/ai-analyst).
15 +
16 +---
17 +
18 +## Why route notifications
19 +
20 +Without routing, investigation reports live in CoPilot's UI and
21 +nowhere else. Routing pushes the result to wherever the SOC team
22 +already operates:
23 +
24 +- A Slack channel where the on-call sees high-severity findings
25 +- An email distribution list for after-hours escalation
26 +- A Teams card for the IR team's daily standup feed
27 +- A ServiceNow ticket auto-created on Critical alerts
28 +- Anything else Shuffle's catalog can reach
29 +
30 +CoPilot owns the routing logic (which destinations to fan out to,
31 +filtered by severity); Shuffle handles the actual delivery to each
32 +app's API.
33 +
34 +---
35 +
36 +## Where it lives in the UI
37 +
38 +Per-customer config lives under **Customers → [your customer] →
39 +Details → AI Notifications**. Three sub-tabs:
40 +
41 +| Sub-tab | Purpose |
42 +|---------|---------|
43 +| **Routes** | The actual notification rules — what severity threshold, which Shuffle integration, which app, where to send |
44 +| **Shuffle integrations** | Per-customer Shuffle org configurations + the "Manage apps" drawer for authenticating new Shuffle apps |
45 +| **Dispatch log** | Read-only audit of every notification attempt — `sent`, `failed`, `skipped`, latency, error messages |
46 +
47 +---
48 +
49 +## One-time prerequisite: deployment Shuffle connector
50 +
51 +Before any customer can use notifications, your CoPilot deployment
52 +needs the Shuffle connector configured. This is admin-tier infra
53 +that's set up once for the whole platform, not per-customer.
54 +
55 +1. **Connectors → Shuffle** (or via SQL: `SELECT * FROM connectors
56 + WHERE connector_name='Shuffle'`)
57 +2. Set:
58 + - `connector_url` — your Shuffle region (e.g. `https://shuffler.io`,
59 + `https://california.shuffler.io`)
60 + - `connector_api_key` — admin Bearer token from your Shuffle account
61 + (Shuffle profile → API key, or
62 + `POST /api/v1/users/generateapikey`)
63 + - `connector_extra_data` — your parent Org-Id (optional but helps
64 + in some flows)
65 + - `connector_enabled` = 1, `connector_verified` = 1
66 +
67 +CoPilot's notification engine reads the URL + API key fresh on every
68 +dispatch, so a key rotation takes effect without restarting the
69 +backend.
70 +
71 +---
72 +
73 +## Step-by-step setup for one customer
74 +
75 +End-to-end walkthrough — first time setting up notifications for a
76 +customer that's not yet configured.
77 +
78 +### Step 1 — Add a Shuffle integration
79 +
80 +The integration tells CoPilot **which Shuffle org** belongs to this
81 +customer. Each customer gets one or more integration rows.
82 +
83 +1. Open the customer's **AI Notifications** tab
84 +2. Click **Shuffle integrations**
85 +3. Click **Add Shuffle integration**
86 +4. Fill out:
87 + - **Display name** — human label, e.g. `Acme Production Shuffle`
88 + - **Shuffle org** — dropdown populated from your deployment's
89 + Shuffle connector. Picks the org for this customer (top-level or
90 + sub-org). Sub-orgs show a `· sub-org` hint
91 + - **Enabled** — leave checked
92 +5. Click **Add integration**
93 +
94 +After creating, click the **checkmark** icon on the new row to
95 +**Test connection** — should turn green with the count of authenticated
96 +apps in that org.
97 +
98 +> If the org dropdown is empty or fails to load, the Shuffle connector
99 +> isn't configured correctly. See [troubleshooting](#troubleshooting).
100 +
101 +### Step 2 — Authenticate the apps you want to use
102 +
103 +Each Shuffle org needs the apps you plan to notify through to be
104 +**authenticated** within Shuffle. CoPilot doesn't do the authentication
105 +itself — Shuffle owns that flow. You can authenticate apps either:
106 +
107 +- **Inside Shuffle's UI directly** (shuffler.io → Apps → connect Slack/
108 + Outlook/etc.)
109 +- **From CoPilot's "Manage apps" drawer** — clicks through to
110 + Shuffle's OAuth dance
111 +
112 +To use the drawer:
113 +
114 +1. **Shuffle integrations** sub-tab → click the **catalog icon** on
115 + the integration row
116 +2. The drawer opens with Shuffle's app picker scoped to this
117 + customer's org
118 +3. Search for the app you want (`Slack`, `Outlook`, `Teams`, `Gmail`,
119 + `ServiceNow`, etc.)
120 +4. Click the app — Shuffle opens an OAuth window in a new tab
121 +5. Authenticate in Shuffle, return to CoPilot
122 +6. The new app is now available for routes that target this org
123 +
124 +> Apps authenticated inside Shuffle directly will also appear here —
125 +> the drawer is a convenience, not a requirement.
126 +
127 +### Step 3 — Create a notification route
128 +
129 +The route is the actual rule that says "when an AI investigation
130 +matches X, send to Y."
131 +
132 +1. Open **Routes** sub-tab → **Add route**
133 +2. Fill out:
134 + - **Name** — human label, e.g. `SOC Slack #alerts`
135 + - **Minimum severity** — only investigations at this tier or
136 + higher fire this route. Pick from `Critical (only)`, `High and
137 + above`, `Medium and above`, `Low and above`, `Informational and
138 + above (everything)`
139 + - **Channel** — locked to **Shuffle**
140 + - **Shuffle integration** — pick the integration you created in
141 + Step 1
142 + - **Shuffle app** — pick from the dropdown of apps authenticated
143 + in this org (Step 2)
144 + - **Destination hint** — free-form text that's prepended to the
145 + outgoing message as a `Send to <hint>: …` instruction so
146 + Shuffle's app agent knows where to deliver inside that app:
147 + - For Slack: `#soc-alerts` or `#general`
148 + - For Outlook / Gmail: `soc@example.com`
149 + - For Teams: a channel name or webhook target
150 + - For ServiceNow: ticket queue or assignee
151 + - **Custom message template** — leave empty for the default body.
152 + If you set one, include `{{summary}}` somewhere or the AI's
153 + finding will be dropped from the message
154 + - **Enabled** — leave checked
155 +3. Click **Create route**
156 +
157 +### Step 4 — Test by triggering an investigation
158 +
159 +The notification fires automatically every time Talon completes an
160 +investigation that matches the route's filter. To test without
161 +waiting for a real alert:
162 +
163 +1. Pick any alert in **Incident Management → Alerts** for this
164 + customer
165 +2. Click the **AI Analyst** tab on the alert
166 +3. Click **Investigate with AI Analyst** if no report exists
167 +4. Wait ~30–90 seconds for Talon to finish
168 +5. Check the route's destination — Slack channel, mailbox, etc.
169 +
170 +You can also fire a dispatch manually for any alert via:
171 +
172 +```bash
173 +curl -X POST "${COPILOT_URL}/api/notifications/dispatch" \
174 + -H "Authorization: Bearer ${COPILOT_API_KEY}" \
175 + -H "Content-Type: application/json" \
176 + -d '{
177 + "customer_code": "00001",
178 + "alert_id": 147,
179 + "trigger": "investigation_complete",
180 + "severity_assessment": "Critical",
181 + "summary": "Test notification — ignore",
182 + "alert_name": "Manual smoke test"
183 + }'
184 +```
185 +
186 +The response shows `routes_matched`, `dispatched`, `failed`, and
187 +per-route outcomes including the Shuffle `execution_id` for forensics.
188 +
189 +### Step 5 — Verify in the dispatch log
190 +
191 +**Dispatch log** sub-tab shows every notification attempt for this
192 +customer:
193 +
194 +- **When** — timestamp
195 +- **Alert** — alert ID
196 +- **Trigger** — the event type (`investigation_complete`)
197 +- **Status** — `sent` (success), `failed` (provider error), or
198 + `skipped` (idempotency hit — same alert already dispatched for the
199 + same route+trigger)
200 +- **Latency** — provider call duration
201 +- **Error / Preview** — error message on failures, or the first 500
202 + chars of the rendered body
203 +
204 +Click any failed row to see why; cross-reference the
205 +`shuffle_execution_id` (if present) in Shuffle's UI to see what
206 +happened on Shuffle's side.
207 +
208 +---
209 +
210 +## End-to-end example: Slack #soc-alerts for High+ findings
211 +
212 +Putting it all together for customer code `acme`:
213 +
214 +1. **Shuffle prerequisite:** Slack workspace authenticated in Acme's
215 + Shuffle org as the `Slack` app (done either in Shuffle's UI or
216 + via Step 2 above)
217 +2. **Add Shuffle integration:**
218 + - Display name: `Acme Shuffle`
219 + - Shuffle org: `Acme Corp (3a8c…)` (picked from dropdown)
220 +3. **Add notification route:**
221 + - Name: `SOC Slack #soc-alerts`
222 + - Minimum severity: `High and above`
223 + - Shuffle integration: `Acme Shuffle`
224 + - Shuffle app: `Slack`
225 + - Destination hint: `#soc-alerts`
226 + - Custom template: empty
227 +4. **Trigger test investigation** on a High-severity alert
228 +5. **Dispatch log** shows `sent` row with ~300 ms latency,
229 + `shuffle_execution_id=exec-…`
230 +6. **Slack #soc-alerts** receives a message with the alert summary
231 + and severity
232 +
233 +Repeat steps 2–4 for additional destinations (e.g. an Outlook email
234 +distribution list for Critical-only, a Teams card for the daily
235 +standup feed).
236 +
237 +---
238 +
239 +## Common patterns
240 +
241 +| Pattern | How to configure |
242 +|---------|------------------|
243 +| **One channel, all severities** | Single route, `min_severity = Informational` |
244 +| **High-tier paging only** | Single route, `min_severity = High` (fires on High and Critical) |
245 +| **Critical to one channel, everything else to another** | Two routes — Critical-only Slack DM to on-call, separate `Informational+` route to a noisy `#all-investigations` channel |
246 +| **Multiple destinations for the same severity** | Multiple routes with the same `min_severity`, different `shuffle_app` / `destination_hint`. Each fires independently |
247 +| **Customer with no notifications** | No routes configured. Investigations still write back to CoPilot's DB; nothing fans out |
248 +
249 +---
250 +
251 +## How filtering actually works
252 +
253 +A route fires when **all** of these are true:
254 +
255 +1. Route is `enabled`
256 +2. Route's `trigger` matches the dispatch's event type
257 + (currently always `investigation_complete`)
258 +3. Investigation's severity is at-or-above the route's
259 + `min_severity` (`Informational < Low < Medium < High < Critical`)
260 +
261 +If any check fails, the route is skipped (no row in the dispatch log
262 +for skipped-by-filter — those only show idempotency hits).
263 +
264 +**Idempotency:** the same investigation cannot fire the same route
265 +twice. The dispatch log has a unique constraint on `(customer_code,
266 +alert_id, route_id, trigger)`. Re-running an investigation re-fires
267 +the route only after the initial dispatch failed (the `failed` row
268 +gets overwritten on retry; `sent` rows are immutable).
269 +
270 +---
271 +
272 +## Custom message templates
273 +
274 +By default the notification body is a simple AI-generated summary
275 +plus alert metadata. If you need a different format — branded
276 +language, specific recipient phrasing, internal ticket numbers — set
277 +a custom **format_template** on the route.
278 +
279 +Templates support these substitution tokens:
280 +
281 +| Token | Replaced with |
282 +|-------|---------------|
283 +| `{{customer_code}}` | The customer's code |
284 +| `{{alert_id}}` | The integer alert ID |
285 +| `{{alert_name}}` | The original alert title |
286 +| `{{severity}}` | `Critical` / `High` / `Medium` / `Low` / `Informational` |
287 +| `{{summary}}` | The AI investigation summary |
288 +| `{{report_url}}` | Deep link to the report in CoPilot |
289 +
290 +Example for a Slack-style banner:
291 +
292 +```
293 +:rotating_light: *{{severity}}* finding on alert #{{alert_id}}
294 +
295 +{{summary}}
296 +
297 +<{{report_url}}|Open in CoPilot>
298 +```
299 +
300 +> **Watch out:** if your template doesn't reference `{{summary}}`,
301 +> the actual investigation finding gets dropped. The route fires but
302 +> recipients see only what's in the template literal. Always include
303 +> `{{summary}}` somewhere unless you genuinely want a content-free
304 +> teaser.
305 +
306 +---
307 +
308 +## Troubleshooting
309 +
310 +| Symptom | Likely cause | Fix |
311 +|---------|--------------|-----|
312 +| Org dropdown is empty when adding a Shuffle integration | Deployment's Shuffle connector misconfigured or unreachable | Check `Connectors → Shuffle`. Confirm `connector_url`, `connector_api_key`, `connector_enabled=1`. Try `curl ${SHUFFLE_URL}/api/v1/orgs -H "Authorization: Bearer ${KEY}"` from the CoPilot host |
313 +| "Test connection" stays neutral / turns red | Org-Id is invalid or the API key doesn't have access to that org | Verify the Org-Id in Shuffle's UI. Confirm your API key is admin-scoped or has access to that org |
314 +| Route saves but dispatch log says `routes_matched: 0` | Route's `min_severity` is higher than the alert's actual severity | Edit the route, lower `min_severity`. Or trigger an investigation on a higher-severity alert |
315 +| Dispatch log shows `status=failed` with `Shuffle returned 4xx` | The Shuffle app isn't authenticated in the customer's org, or the destination is wrong | Open Shuffle UI → Apps → check the app is authenticated. For unrecognized destinations, check Shuffle's app-specific docs (e.g. Slack channel must include `#`) |
316 +| Dispatch log shows `status=failed` with `ReadTimeout` | Shuffle backend was slow or temporarily unreachable | Re-trigger the investigation. The dispatch loop overwrites the `failed` row on retry |
317 +| Recipient gets a notification with the wrong content | A custom `format_template` is set on the route and doesn't include `{{summary}}` | Edit the route, either add `{{summary}}` to the template or clear the template field entirely |
318 +| Investigation completes but nothing in the dispatch log | Talon didn't call `DispatchNotificationsTool`. Either no routes configured or Talon's `notifications.md` not deployed | Check Talon container logs for the `tool_use: DispatchNotificationsTool` line. Confirm at least one route exists for the customer |
319 +
320 +---
321 +
322 +## Safety & guardrails
323 +
324 +- **Per-customer scope** — routes attached to customer A's integrations
325 + only fire on customer A's alerts. CoPilot's dispatcher refuses to
326 + cross tenant boundaries even if a route is hand-edited to point at
327 + another customer's integration.
328 +- **Best-effort delivery** — a notification dispatch failure never
329 + fails the underlying investigation. Talon's report is written
330 + before the dispatch attempt; failures are logged but don't
331 + propagate.
332 +- **No automatic retries** — a `failed` dispatch waits for the next
333 + Talon run on the same alert (or a manual re-trigger) to overwrite
334 + the log row. Phase 4 may add automatic retry semantics for
335 + transient failures.
336 +- **Admin-tier auth** — only users with `admin` or `analyst` scope
337 + can read or modify routes. Operator-tier users see investigation
338 + reports but not the routing config.
339 +- **Org auth tokens visible to admins** — the Manage apps drawer
340 + fetches a per-org auth token from Shuffle so the embedded picker
341 + can show authenticated apps. This token is exposed to the admin's
342 + browser session for the duration of the drawer being open. Don't
343 + share screenshots of the drawer with non-admins.
344 +
345 +---
346 +
347 +## Related
348 +
349 +- [AI Analyst (Talon)](/power-features/ai-analyst) — the investigation
350 + engine that produces the reports being routed
351 +- [AI Analyst — Analyst review workflow](/power-features/ai-analyst-review)
352 + — review and grade the reports after they're produced
frontend/package.json
+6 -1
@@ -48,10 +48,11 @@
48 "@fontsource/public-sans": "^5.2.7",
49 "@microsoft/fetch-event-source": "^2.0.1",
50 "@shikijs/markdown-it": "^4.0.2",
51 - "@singulio/app-auth-search": "^0.0.3",
51 + "@shuffleio/shuffle-mcps": "^0.0.3",
52 "@types/codemirror": "^5.60.17",
53 "@vueuse/core": "^14.2.1",
54 "@vueuse/motion": "^3.0.3",
55 + "algoliasearch": "^5.51.0",
56 "axios": "^1.15.2",
57 "bytes": "^3.1.2",
58 "codemirror": "~6.0.2",
@@ -71,6 +72,8 @@
72 "password-validator": "^5.3.0",
73 "pinia": "^3.0.4",
74 "pinia-plugin-persistedstate": "^4.7.1",
75 + "react": "^19.2.5",
76 + "react-dom": "^19.2.5",
77 "secure-ls": "^2.0.0",
78 "shiki": "^4.0.2",
79 "thememirror": "^2.0.1",
@@ -106,6 +109,8 @@
109 "@types/lodash": "^4.17.24",
110 "@types/markdown-it": "^14.1.2",
111 "@types/node": "^25.6.0",
112 + "@types/react": "^19.2.14",
113 + "@types/react-dom": "^19.2.3",
114 "@types/validator": "^13.15.10",
115 "@vitejs/plugin-vue": "^6.0.6",
116 "@vitejs/plugin-vue-jsx": "^5.1.5",
frontend/pnpm-lock.yaml
+485 -470
@@ -47,18 +47,21 @@ importers:
47 '@shikijs/markdown-it':
48 specifier: ^4.0.2
49 version: 4.0.2
50 - '@singulio/app-auth-search':
50 + '@shuffleio/shuffle-mcps':
51 specifier: ^0.0.3
52 - version: 0.0.3
52 + version: 0.0.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
53 '@types/codemirror':
54 specifier: ^5.60.17
55 version: 5.60.17
56 '@vueuse/core':
57 specifier: ^14.2.1
58 - version: 14.2.1(vue@3.5.33(typescript@5.9.3))
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
64 + version: 5.52.0
65 axios:
66 specifier: ^1.15.2
67 version: 1.15.2(debug@4.4.3)
@@ -91,7 +94,7 @@ importers:
94 version: 2.6.0
95 jose:
96 specifier: ^6.2.2
94 - version: 6.2.2
97 + version: 6.2.3
98 js-md5:
99 specifier: ^0.8.3
100 version: 0.8.3
@@ -106,7 +109,7 @@ importers:
109 version: 2.44.1(vue@3.5.33(typescript@5.9.3))
110 nanoid:
111 specifier: ^5.1.9
109 - version: 5.1.9
112 + version: 5.1.11
113 password-validator:
114 specifier: ^5.3.0
115 version: 5.3.0
@@ -115,7 +118,13 @@ importers:
118 version: 3.0.4(typescript@5.9.3)(vue@3.5.33(typescript@5.9.3))
119 pinia-plugin-persistedstate:
120 specifier: ^4.7.1
118 - version: 4.7.1(@nuxt/kit@3.21.2)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.33(typescript@5.9.3)))
121 + version: 4.7.1(@nuxt/kit@3.21.4)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.33(typescript@5.9.3)))
122 + react:
123 + specifier: ^19.2.5
124 + version: 19.2.5
125 + react-dom:
126 + specifier: ^19.2.5
127 + version: 19.2.5(react@19.2.5)
128 secure-ls:
129 specifier: ^2.0.0
130 version: 2.0.0
@@ -167,10 +176,10 @@ importers:
176 devDependencies:
177 '@antfu/eslint-config':
178 specifier: ~8.2.0
170 - version: 8.2.0(@typescript-eslint/rule-tester@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.58.2(typescript@5.9.3))(@typescript-eslint/utils@8.58.2(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.0.2)(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.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)))
180 '@clack/prompts':
181 specifier: ^1.2.0
173 - version: 1.2.0
182 + version: 1.3.0
183 '@iconify/vue':
184 specifier: ^5.0.0
185 version: 5.0.0(vue@3.5.33(typescript@5.9.3))
@@ -201,6 +210,12 @@ importers:
210 '@types/node':
211 specifier: ^25.6.0
212 version: 25.6.0
213 + '@types/react':
214 + specifier: ^19.2.14
215 + version: 19.2.14
216 + '@types/react-dom':
217 + specifier: ^19.2.3
218 + version: 19.2.3(@types/react@19.2.14)
219 '@types/validator':
220 specifier: ^13.15.10
221 version: 13.15.10
@@ -212,13 +227,13 @@ importers:
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
215 - version: 2.4.9(@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))
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
221 - version: 2.10.23
236 + version: 2.10.25
237 depcheck:
238 specifier: ^1.4.7
239 version: 1.4.7
@@ -236,7 +251,7 @@ importers:
251 version: 4.0.9
252 jsdom:
253 specifier: ^29.0.2
239 - version: 29.0.2
254 + version: 29.1.1
255 npm-run-all2:
256 specifier: ^8.0.4
257 version: 8.0.4
@@ -245,7 +260,7 @@ importers:
260 version: 3.8.3
261 prettier-plugin-tailwindcss:
262 specifier: ^0.7.3
248 - version: 0.7.3(prettier@3.8.3)
263 + version: 0.7.4(prettier@3.8.3)
264 sass:
265 specifier: ^1.99.0
266 version: 1.99.0
@@ -272,16 +287,16 @@ importers:
287 version: 1.2.1(rollup@4.60.2)
288 vite-plugin-inspect:
289 specifier: ^11.3.3
275 - version: 11.3.3(@nuxt/kit@3.21.2)(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))
290 + version: 11.3.3(@nuxt/kit@3.21.4)(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))
291 vite-plugin-vue-devtools:
292 specifier: ^8.1.1
278 - version: 8.1.1(@nuxt/kit@3.21.2)(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))
293 + version: 8.1.1(@nuxt/kit@3.21.4)(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))
294 vite-svg-loader:
295 specifier: ^5.1.1
296 version: 5.1.1(vue@3.5.33(typescript@5.9.3))
297 vitest:
298 specifier: ^4.1.5
284 - version: 4.1.5(@types/node@25.6.0)(jsdom@29.0.2)(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))
299 + version: 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))
300 vue-tsc:
301 specifier: ^3.2.7
302 version: 3.2.7(typescript@5.9.3)
@@ -301,60 +316,60 @@ packages:
316 '@ajoelp/json-to-formdata@1.5.0':
317 resolution: {integrity: sha512-nrlfeTSL0X0dtx5r2KpzPiqLSIQquiiJjUKsQAKzWaCmO2QoYZCyb5ENZwF3YoffKronOCJr25mxaD8JRJmK8w==}
318
304 - '@algolia/abtesting@1.16.2':
305 - resolution: {integrity: sha512-n9s6bEV6imdtIEd+BGP7WkA4pEZ5YTdgQ05JQhHwWawHg3hyjpNwC0TShGz6zWhv+jfLDGA/6FFNbySFS0P9cw==}
319 + '@algolia/abtesting@1.18.0':
320 + resolution: {integrity: sha512-8siuLG+FIns1AjZ/g2SDVwHz9S+ObacDQISEJvS8XsNei1zl3FXqfqQrBpmrG7ACWCyesXHbicMJtvRbg00FEw==}
321 engines: {node: '>= 14.0.0'}
322
308 - '@algolia/client-abtesting@5.50.2':
309 - resolution: {integrity: sha512-52iq0vHy1sphgnwoZyx5PmbEt8hsh+m7jD123LmBs6qy4GK7LbYZIeKd+nSnSipN2zvKRZ2zScS6h9PW3J7SXg==}
323 + '@algolia/client-abtesting@5.52.0':
324 + resolution: {integrity: sha512-wtwPgyPmO7b7sQPVgoK29c1VpfS08DnnJCmxX/oU1pV2DlMRJCzQcLN7JSloYpodyKHwM8+9wOzlAM0co3TDmA==}
325 engines: {node: '>= 14.0.0'}
326
312 - '@algolia/client-analytics@5.50.2':
313 - resolution: {integrity: sha512-WpPIUg+cSG2aPUG0gS8Ko9DwRgbRPUZxJkolhL2aCsmSlcEEZT65dILrfg5ovcxtx0Kvr+xtBVsTMtsQWRtPDQ==}
327 + '@algolia/client-analytics@5.52.0':
328 + resolution: {integrity: sha512-9KY36bRl4AH7RjqSeDDOKnjsz4IxQFBEOB8/fWmEbdQe+Isbs5jGzVJu9NEPQ1Tgwxlf8Uf07Swj3jZyMNUZ2g==}
329 engines: {node: '>= 14.0.0'}
330
316 - '@algolia/client-common@5.50.2':
317 - resolution: {integrity: sha512-Gj2MgtArGcsr82kIqRlo6/dCAFjrs2gLByEqyRENuT7ugrSMFuqg1vDzeBjRL1t3EJEJCFtT0PLX3gB8A6Hq4Q==}
331 + '@algolia/client-common@5.52.0':
332 + resolution: {integrity: sha512-3a/qM3dzJqqfTx7Yrw7uGQ98I3Q0rDfb4Vkv0wEzko96l7YQMxfBVz/VbLq2N+c59GweYv6Vhp8mPeqnWJSITw==}
333 engines: {node: '>= 14.0.0'}
334
320 - '@algolia/client-insights@5.50.2':
321 - resolution: {integrity: sha512-CUqoid5jDpmrc0oK3/xuZXFt6kwT0P9Lw7/nsM14YTr6puvmi+OUKmURpmebQF22S2vCG8L1DAoXXujxQUi/ug==}
335 + '@algolia/client-insights@5.52.0':
336 + resolution: {integrity: sha512-Rki7ACbMcvbQW0BuM84x9dkGHY47ABmv4jU6tYssat2k02p3mIUms2YOLUAMeknhmnFsj6lb6ZzOXdMWMyc1sA==}
337 engines: {node: '>= 14.0.0'}
338
324 - '@algolia/client-personalization@5.50.2':
325 - resolution: {integrity: sha512-AndZWFoc0gbP5901OeQJ73BazgGgSGiBEba4ohdoJuZwHTO2Gio8Q4L1VLmytMBYcviVigB0iICToMvEJxI4ug==}
339 + '@algolia/client-personalization@5.52.0':
340 + resolution: {integrity: sha512-96s4Uzc3kk+/f4jJXIVVGWP5XlngOGNQ1x6hW9AT59pOixHlOs5tqJg+ZUS/GQ6h/iYP0ceQcmxDQeLyCLTaDQ==}
341 engines: {node: '>= 14.0.0'}
342
328 - '@algolia/client-query-suggestions@5.50.2':
329 - resolution: {integrity: sha512-NWoL+psEkz5dIzweaByVXuEB45wS8/rk0E0AhMMnaVJdVs7TcACPH2/OURm+N0xRDITkTHqCna823rd6Uqntdg==}
343 + '@algolia/client-query-suggestions@5.52.0':
344 + resolution: {integrity: sha512-lqeycNpSPe5Qa0OUWpejVvYQjQWV5nQuLT0a4aq7XzRAvCxprV/6Lf841EygdD2nrFnuS58ok7Au1uOtXzpnkg==}
345 engines: {node: '>= 14.0.0'}
346
332 - '@algolia/client-search@5.50.2':
333 - resolution: {integrity: sha512-ypSboUJ3XJoQz5DeDo82hCnrRuwq3q9ZdFhVKAik9TnZh1DvLqoQsrbBjXg7C7zQOtV/Qbge/HmyoV6V5L7MhQ==}
347 + '@algolia/client-search@5.52.0':
348 + resolution: {integrity: sha512-ly1wETVGRo30cx61O7fetESN+ElL9c9K+bD/AVgnT1ar4c6v+/Yqjrhdtu6Fm4D0s4NZP081Isf6tunH1wUXHg==}
349 engines: {node: '>= 14.0.0'}
350
336 - '@algolia/ingestion@1.50.2':
337 - resolution: {integrity: sha512-VlR2FRXLw2bCB94SQo6zxg/Qi+547aOji6Pb+dKE7h1DMCCY317St+OpjpmgzE+bT2O9ALIc0V4nVIBOd7Gy+Q==}
351 + '@algolia/ingestion@1.52.0':
352 + resolution: {integrity: sha512-U4EeTvgmluRjj39ykZSAd5X+a6LD5m7/mcOWDmB7hqm1R6QY0yT8jLxpNVEjYhzgEN5hcDGW6X67EWQY8KiYGQ==}
353 engines: {node: '>= 14.0.0'}
354
340 - '@algolia/monitoring@1.50.2':
341 - resolution: {integrity: sha512-Cmvfp2+qopzQt8OilU97rhLhosq7ZrB6uieok3EwFUqG/aalPg6DgfCmu0yJMrYe+KMC1qRVt1MTRAUwLknUMQ==}
355 + '@algolia/monitoring@1.52.0':
356 + resolution: {integrity: sha512-FCPnDcILfpTE94u7BVlV4DmnSV5wE3+j25EEF+3dYPrVzkVCSoAHs318oWDGxnxsAgiL4HpL12Jc4XHmw9shpA==}
357 engines: {node: '>= 14.0.0'}
358
344 - '@algolia/recommend@5.50.2':
345 - resolution: {integrity: sha512-jrkuyKoOM7dFWQ/6Y4hQAse2SC3L/RldG6GnPjMvAj65h+7Ubb51S0pKk4ofSStF0xm4LCNe0C4T6XX4nOFDiQ==}
359 + '@algolia/recommend@5.52.0':
360 + resolution: {integrity: sha512-br3DO7n4N8CXwTRbZS0MnB4WQ9YHfNjCwkCEzVR/wek/qNTDQKDb0nROmkFaNZ8ucUqUVKZi074dbwMwRDlK8Q==}
361 engines: {node: '>= 14.0.0'}
362
348 - '@algolia/requester-browser-xhr@5.50.2':
349 - resolution: {integrity: sha512-4107YLJqCudPiBUlwnk6oTSUVwU7ab+qL1SfQGEDYI8DZH5gsf1ekPt9JykXRKYXf2IfouFL5GiCY/PHTFIjYw==}
363 + '@algolia/requester-browser-xhr@5.52.0':
364 + resolution: {integrity: sha512-b0T/Ca2c9KyEslKsVrGZvbe1UrrKKSdfXhBZ2pbpKahFUzJfziRZ0urbOm7V65O0tO/jwU+Lo/+bIiiyhzGt8w==}
365 engines: {node: '>= 14.0.0'}
366
352 - '@algolia/requester-fetch@5.50.2':
353 - resolution: {integrity: sha512-vOrd3MQpLgmf6wXAueTuZ/cA0W4uRwIHHaxNy3h+a6YcNn6bCV/gFdZuv3F13v593zRU2k5R75NmvRWLenvMrw==}
367 + '@algolia/requester-fetch@5.52.0':
368 + resolution: {integrity: sha512-ozBT8J/mtD4H4IAojw8QPirlcL2gHrI1BGuZ4/ZXXO/rTE1yQ4VIPJj4mTTbwo4FbkS1MoJsD/DsrqLzhnc4/g==}
369 engines: {node: '>= 14.0.0'}
370
356 - '@algolia/requester-node-http@5.50.2':
357 - resolution: {integrity: sha512-Mu9BFtgzGqDUy5Bcs2nMyoILIFSN13GKQaklKAFIsd0K3/9CpNyfeBc+/+Qs6mFZLlxG9qzullO7h+bjcTBuGQ==}
371 + '@algolia/requester-node-http@5.52.0':
372 + resolution: {integrity: sha512-gyyWcLD22tnabmoit4iukCXuoRc5HYJuUjPSEa8a0D/f/NlRafpWi52AlAaa4Uu/rsl7saHsJFTNjTptWbu2+A==}
373 engines: {node: '>= 14.0.0'}
374
375 '@antfu/eslint-config@8.2.0':
@@ -421,8 +436,8 @@ packages:
436 '@antfu/install-pkg@1.1.0':
437 resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
438
424 - '@antfu/ni@30.0.0':
425 - resolution: {integrity: sha512-DqBVB3XqXH4VsDpER7iLlEtayMC98iXrY7kwBzp1v6LGc/6U6+qnN3+X0bcPK63LMXJCRG2D/XDq7dvtKDGogg==}
439 + '@antfu/ni@30.1.0':
440 + resolution: {integrity: sha512-3VuAbPjgY52rQNn4wABaXMhBU2Oq91uy6L8nX49eJ35OLI68CyckGU+HZxcaHix4ymuGM2nFL1D6sLpgODK5xw==}
441 engines: {node: '>=20.19.0'}
442 hasBin: true
443
@@ -445,8 +460,8 @@ packages:
460 resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
461 engines: {node: '>=6.9.0'}
462
448 - '@babel/compat-data@7.29.0':
449 - resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
463 + '@babel/compat-data@7.29.3':
464 + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==}
465 engines: {node: '>=6.9.0'}
466
467 '@babel/core@7.29.0':
@@ -465,8 +480,8 @@ packages:
480 resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
481 engines: {node: '>=6.9.0'}
482
468 - '@babel/helper-create-class-features-plugin@7.28.6':
469 - resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==}
483 + '@babel/helper-create-class-features-plugin@7.29.3':
484 + resolution: {integrity: sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==}
485 engines: {node: '>=6.9.0'}
486 peerDependencies:
487 '@babel/core': ^7.0.0
@@ -523,8 +538,8 @@ packages:
538 resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==}
539 engines: {node: '>=6.9.0'}
540
526 - '@babel/parser@7.29.2':
527 - resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==}
541 + '@babel/parser@7.29.3':
542 + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==}
543 engines: {node: '>=6.0.0'}
544 hasBin: true
545
@@ -585,11 +600,13 @@ packages:
600 resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
601 hasBin: true
602
588 - '@clack/core@1.2.0':
589 - resolution: {integrity: sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg==}
603 + '@clack/core@1.3.0':
604 + resolution: {integrity: sha512-xJPHpAmEQUBrXSLx0gF+q5K/IyihXpsHZcha+jB+tyahsKRK3Dxo4D0coZDewHo12NhiuzC3dTtMPbm53GEAAA==}
605 + engines: {node: '>= 20.12.0'}
606
591 - '@clack/prompts@1.2.0':
592 - resolution: {integrity: sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w==}
607 + '@clack/prompts@1.3.0':
608 + resolution: {integrity: sha512-GgcWwRCs/xPtaqlMy8qRhPnZf9vlWcWZNHAitnVQ3yk7JmSralSiq5q07yaffYE8SogtDm7zFeKccx1QNVARpw==}
609 + engines: {node: '>= 20.12.0'}
610
611 '@codemirror/autocomplete@6.20.1':
612 resolution: {integrity: sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==}
@@ -609,8 +626,8 @@ packages:
626 '@codemirror/lint@6.9.5':
627 resolution: {integrity: sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==}
628
612 - '@codemirror/search@6.6.0':
613 - resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==}
629 + '@codemirror/search@6.7.0':
630 + resolution: {integrity: sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==}
631
632 '@codemirror/state@6.6.0':
633 resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==}
@@ -1070,8 +1087,8 @@ packages:
1087 resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
1088 engines: {node: '>= 8'}
1089
1073 - '@nuxt/kit@3.21.2':
1074 - resolution: {integrity: sha512-Bd6m6mrDrqpBEbX+g0rc66/ALd1sxlgdx5nfK9MAYO0yKLTOSK7McSYz1KcOYn3LQFCXOWfvXwaqih/b+REI1g==}
1090 + '@nuxt/kit@3.21.4':
1091 + resolution: {integrity: sha512-XDWhQJsA5hpdFpVSmImQIVXcsANJI07TjT1LZC/AUKJxl/dcM52Rq4uU+b3uqyVl4LZR1fODSDEzLxcdXq4Rmg==}
1092 engines: {node: '>=18.12.0'}
1093
1094 '@one-ini/wasm@0.1.1':
@@ -1186,8 +1203,8 @@ packages:
1203 '@rolldown/pluginutils@1.0.0-rc.13':
1204 resolution: {integrity: sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==}
1205
1189 - '@rolldown/pluginutils@1.0.0-rc.16':
1190 - resolution: {integrity: sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==}
1206 + '@rolldown/pluginutils@1.0.0-rc.18':
1207 + resolution: {integrity: sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==}
1208
1209 '@rollup/rollup-android-arm-eabi@4.60.2':
1210 resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==}
@@ -1367,13 +1384,16 @@ packages:
1384 '@shikijs/vscode-textmate@10.0.2':
1385 resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
1386
1387 + '@shuffleio/shuffle-mcps@0.0.3':
1388 + resolution: {integrity: sha512-w2A2wyuxtPo70sdb/aXQPAIXQVIcUoCRdzh2EkxpCttpnPxDSBjVlx+PMCVW5kblQMIvU4Q7EQCndLvdK+FzfA==}
1389 + peerDependencies:
1390 + react: '>=18'
1391 + react-dom: '>=18'
1392 +
1393 '@sindresorhus/base62@1.0.0':
1394 resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==}
1395 engines: {node: '>=18'}
1396
1374 - '@singulio/app-auth-search@0.0.3':
1375 - resolution: {integrity: sha512-4U/of6Gry5hal3CdZoMkrdfbSYza43ZcvlJoh4pJ1UCm16Vpegmw3MwmbrZHjg8wiFWe+f5WnX/WkgQpuuZnHw==}
1376 -
1397 '@standard-schema/spec@1.1.0':
1398 resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
1399
@@ -1552,6 +1572,14 @@ packages:
1572 '@types/parse-json@4.0.2':
1573 resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
1574
1575 + '@types/react-dom@19.2.3':
1576 + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
1577 + peerDependencies:
1578 + '@types/react': ^19.2.0
1579 +
1580 + '@types/react@19.2.14':
1581 + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
1582 +
1583 '@types/sarif@2.1.7':
1584 resolution: {integrity: sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==}
1585
@@ -1570,69 +1598,69 @@ packages:
1598 '@types/web-bluetooth@0.0.21':
1599 resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
1600
1573 - '@typescript-eslint/eslint-plugin@8.58.2':
1574 - resolution: {integrity: sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==}
1601 + '@typescript-eslint/eslint-plugin@8.59.1':
1602 + resolution: {integrity: sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==}
1603 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1604 peerDependencies:
1577 - '@typescript-eslint/parser': ^8.58.2
1605 + '@typescript-eslint/parser': ^8.59.1
1606 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
1607 typescript: '>=4.8.4 <6.1.0'
1608
1581 - '@typescript-eslint/parser@8.58.2':
1582 - resolution: {integrity: sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==}
1609 + '@typescript-eslint/parser@8.59.1':
1610 + resolution: {integrity: sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==}
1611 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1612 peerDependencies:
1613 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
1614 typescript: '>=4.8.4 <6.1.0'
1615
1588 - '@typescript-eslint/project-service@8.58.2':
1589 - resolution: {integrity: sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==}
1616 + '@typescript-eslint/project-service@8.59.1':
1617 + resolution: {integrity: sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==}
1618 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1619 peerDependencies:
1620 typescript: '>=4.8.4 <6.1.0'
1621
1594 - '@typescript-eslint/rule-tester@8.58.2':
1595 - resolution: {integrity: sha512-HF7ry5ZhQSLn4JAJfQCPX39D6BE5MhYXE743I0phrJw1BvB0adH8JpsEc4owCxEESQflodyhRWNUZY+m2EVenw==}
1622 + '@typescript-eslint/rule-tester@8.59.1':
1623 + resolution: {integrity: sha512-VAlTRylRP+zh2jepYylwEllQSI8yP0QWQqUKc3xcPbuSlnDeC0CjbpWAOp5CrqfGl9ZMGw74BjZnutb7mILOzA==}
1624 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1625 peerDependencies:
1626 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
1627
1600 - '@typescript-eslint/scope-manager@8.58.2':
1601 - resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==}
1628 + '@typescript-eslint/scope-manager@8.59.1':
1629 + resolution: {integrity: sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==}
1630 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1631
1604 - '@typescript-eslint/tsconfig-utils@8.58.2':
1605 - resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==}
1632 + '@typescript-eslint/tsconfig-utils@8.59.1':
1633 + resolution: {integrity: sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==}
1634 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1635 peerDependencies:
1636 typescript: '>=4.8.4 <6.1.0'
1637
1610 - '@typescript-eslint/type-utils@8.58.2':
1611 - resolution: {integrity: sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==}
1638 + '@typescript-eslint/type-utils@8.59.1':
1639 + resolution: {integrity: sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==}
1640 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1641 peerDependencies:
1642 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
1643 typescript: '>=4.8.4 <6.1.0'
1644
1617 - '@typescript-eslint/types@8.58.2':
1618 - resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==}
1645 + '@typescript-eslint/types@8.59.1':
1646 + resolution: {integrity: sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==}
1647 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1648
1621 - '@typescript-eslint/typescript-estree@8.58.2':
1622 - resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==}
1649 + '@typescript-eslint/typescript-estree@8.59.1':
1650 + resolution: {integrity: sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==}
1651 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1652 peerDependencies:
1653 typescript: '>=4.8.4 <6.1.0'
1654
1627 - '@typescript-eslint/utils@8.58.2':
1628 - resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==}
1655 + '@typescript-eslint/utils@8.59.1':
1656 + resolution: {integrity: sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==}
1657 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1658 peerDependencies:
1659 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
1660 typescript: '>=4.8.4 <6.1.0'
1661
1634 - '@typescript-eslint/visitor-keys@8.58.2':
1635 - resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==}
1662 + '@typescript-eslint/visitor-keys@8.59.1':
1663 + resolution: {integrity: sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==}
1664 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1665
1666 '@ungap/structured-clone@1.3.0':
@@ -1747,27 +1775,15 @@ packages:
1775 peerDependencies:
1776 '@babel/core': ^7.0.0-0
1777
1750 - '@vue/compiler-core@3.5.32':
1751 - resolution: {integrity: sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==}
1752 -
1778 '@vue/compiler-core@3.5.33':
1779 resolution: {integrity: sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==}
1780
1756 - '@vue/compiler-dom@3.5.32':
1757 - resolution: {integrity: sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==}
1758 -
1781 '@vue/compiler-dom@3.5.33':
1782 resolution: {integrity: sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==}
1783
1762 - '@vue/compiler-sfc@3.5.32':
1763 - resolution: {integrity: sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==}
1764 -
1784 '@vue/compiler-sfc@3.5.33':
1785 resolution: {integrity: sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==}
1786
1768 - '@vue/compiler-ssr@3.5.32':
1769 - resolution: {integrity: sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==}
1770 -
1787 '@vue/compiler-ssr@3.5.33':
1788 resolution: {integrity: sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==}
1789
@@ -1814,14 +1830,11 @@ packages:
1830 peerDependencies:
1831 vue: 3.5.33
1832
1817 - '@vue/shared@3.5.32':
1818 - resolution: {integrity: sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==}
1819 -
1833 '@vue/shared@3.5.33':
1834 resolution: {integrity: sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==}
1835
1823 - '@vue/test-utils@2.4.9':
1824 - resolution: {integrity: sha512-YwgowiO1mPleZqpgAGfxvWu/A5A8nkLrbyH2SqiQRkyzCIaDzzo27/2uS/F1g7fRLvl8BUY0+Sr1eC+6+IHfrw==}
1836 + '@vue/test-utils@2.4.10':
1837 + resolution: {integrity: sha512-SmoZ5EA1kYiAFs9NkYdiFFQF+cSnUwnvlYEbY+DogWQZUiqOm/Y29eSbc5T6yi75SgSF9863SBeXniIEoPajCA==}
1838 peerDependencies:
1839 '@vue/compiler-dom': 3.x
1840 '@vue/server-renderer': 3.x
@@ -1846,16 +1859,16 @@ packages:
1859 peerDependencies:
1860 vue: ^3.5.0
1861
1849 - '@vueuse/core@14.2.1':
1850 - resolution: {integrity: sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==}
1862 + '@vueuse/core@14.3.0':
1863 + resolution: {integrity: sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==}
1864 peerDependencies:
1865 vue: ^3.5.0
1866
1867 '@vueuse/metadata@13.9.0':
1868 resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==}
1869
1857 - '@vueuse/metadata@14.2.1':
1858 - resolution: {integrity: sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==}
1870 + '@vueuse/metadata@14.3.0':
1871 + resolution: {integrity: sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==}
1872
1873 '@vueuse/motion@3.0.3':
1874 resolution: {integrity: sha512-4B+ITsxCI9cojikvrpaJcLXyq0spj3sdlzXjzesWdMRd99hhtFI6OJ/1JsqwtF73YooLe0hUn/xDR6qCtmn5GQ==}
@@ -1867,8 +1880,8 @@ packages:
1880 peerDependencies:
1881 vue: ^3.5.0
1882
1870 - '@vueuse/shared@14.2.1':
1871 - resolution: {integrity: sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==}
1883 + '@vueuse/shared@14.3.0':
1884 + resolution: {integrity: sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==}
1885 peerDependencies:
1886 vue: ^3.5.0
1887
@@ -1891,11 +1904,11 @@ packages:
1904 engines: {node: '>=0.4.0'}
1905 hasBin: true
1906
1894 - ajv@6.14.0:
1895 - resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
1907 + ajv@6.15.0:
1908 + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
1909
1897 - algoliasearch@5.50.2:
1898 - resolution: {integrity: sha512-Tfp26yoNWurUjfgK4GOrVJQhSNXu9tJtHfFFNosgT2YClG+vPyUjX/gbC8rG39qLncnZg8Fj34iarQWpMkqefw==}
1910 + algoliasearch@5.52.0:
1911 + resolution: {integrity: sha512-0ZzY9mjqV7gop/AH8pIBiAS8giXP7WcSiUfoFYIzYAK9QC5c37E4SIVtJVBMwlURc0/uNt2o4RcNRvdHa4CJ5w==}
1912 engines: {node: '>= 14.0.0'}
1913
1914 alien-signals@3.1.2:
@@ -1990,8 +2003,8 @@ packages:
2003 resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
2004 engines: {node: 18 || 20 || >=22}
2005
1993 - baseline-browser-mapping@2.10.23:
1994 - resolution: {integrity: sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==}
2006 + baseline-browser-mapping@2.10.25:
2007 + resolution: {integrity: sha512-QO/VHsXCQdnzADMfmkeOPvHdIAkoB7i0/rGjINPJEetLx75hNttVWGQ/jycHUDP9zZ9rupbm60WRxcwViB0MiA==}
2008 engines: {node: '>=6.0.0'}
2009 hasBin: true
2010
@@ -2077,8 +2090,8 @@ packages:
2090 resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
2091 engines: {node: '>=10'}
2092
2080 - caniuse-lite@1.0.30001788:
2081 - resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==}
2093 + caniuse-lite@1.0.30001791:
2094 + resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==}
2095
2096 ccount@2.0.1:
2097 resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@@ -2397,8 +2410,8 @@ packages:
2410 engines: {node: '>=14'}
2411 hasBin: true
2412
2400 - electron-to-chromium@1.5.340:
2401 - resolution: {integrity: sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==}
2413 + electron-to-chromium@1.5.348:
2414 + resolution: {integrity: sha512-QC2X59nRlycQQMc4ZXjSVBX+tSgJfgRtcrYHbIZLgOV2dCvefoQGegLR7lLXKgpPpSuVmJU19LMzGrSa2C7k3Q==}
2415
2416 emoji-regex@8.0.0:
2417 resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -2413,8 +2426,8 @@ packages:
2426 end-of-stream@1.4.5:
2427 resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
2428
2416 - enhanced-resolve@5.20.1:
2417 - resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
2429 + enhanced-resolve@5.21.0:
2430 + resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==}
2431 engines: {node: '>=10.13.0'}
2432
2433 entities@4.5.0:
@@ -2429,6 +2442,10 @@ packages:
2442 resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
2443 engines: {node: '>=0.12'}
2444
2445 + entities@8.0.0:
2446 + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==}
2447 + engines: {node: '>=20.19.0'}
2448 +
2449 error-ex@1.3.4:
2450 resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
2451
@@ -2446,8 +2463,8 @@ packages:
2463 resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
2464 engines: {node: '>= 0.4'}
2465
2449 - es-module-lexer@2.0.0:
2450 - resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==}
2466 + es-module-lexer@2.1.0:
2467 + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
2468
2469 es-object-atoms@1.1.1:
2470 resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
@@ -2489,8 +2506,8 @@ packages:
2506 peerDependencies:
2507 eslint: ^9.5.0 || ^10.0.0
2508
2492 - eslint-flat-config-utils@3.1.0:
2493 - resolution: {integrity: sha512-lM+Nwo2CzpuTS/RASQExlEIwk/BQoKqJWX6VbDlLMb/mveqvt9MMrRXFEkG3bseuK6g8noKZLeX82epkILtv4A==}
2509 + eslint-flat-config-utils@3.2.0:
2510 + resolution: {integrity: sha512-PHgo1X5uqIorJONLVD9BIaOSdoYFD3z/AeJljdqDPlWVRpeCYkDbK9k0AXoYVqqNJr6FEYIEr5Rm2TSktLQcHw==}
2511
2512 eslint-json-compat-utils@0.2.3:
2513 resolution: {integrity: sha512-RbBmDFyu7FqnjE8F0ZxPNzx5UaptdeS9Uu50r7A+D7s/+FCX+ybiyViYEgFUaFIFqSWJgZRTpL5d8Kanxxl2lQ==}
@@ -2556,8 +2573,8 @@ packages:
2573 peerDependencies:
2574 eslint: '>=8.23.0'
2575
2559 - eslint-plugin-no-only-tests@3.3.0:
2560 - resolution: {integrity: sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==}
2576 + eslint-plugin-no-only-tests@3.4.0:
2577 + resolution: {integrity: sha512-4S3/9Nb7A2tiMcpzEQE9bQSlpeOz6WJkgryBuou/SA8W2x2c8Zf4j0NvTKBjv6qNhF9T79tmkecm/0CHqV0UGg==}
2578 engines: {node: '>=5.0.0'}
2579
2580 eslint-plugin-perfectionist@5.9.0:
@@ -2598,8 +2615,8 @@ packages:
2615 '@typescript-eslint/eslint-plugin':
2616 optional: true
2617
2601 - eslint-plugin-vue@10.8.0:
2602 - resolution: {integrity: sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA==}
2618 + eslint-plugin-vue@10.9.0:
2619 + resolution: {integrity: sha512-EFNNzu4HqtTRb5DJINpyd+u3bDdzETWDMpCzG+UBHz1tpsnMDCeOcf61u4Wy/cbXnMymK+MT9bjH7KcG1fItSw==}
2620 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
2621 peerDependencies:
2622 '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
@@ -2612,8 +2629,8 @@ packages:
2629 '@typescript-eslint/parser':
2630 optional: true
2631
2615 - eslint-plugin-yml@3.3.1:
2616 - resolution: {integrity: sha512-isntsZchaTqDMNNkD+CakrgA/pdUoJ45USWBKpuqfAW1MCuw731xX/vrXfoJFZU3tTFr24nCbDYmDfT2+g4QtQ==}
2632 + eslint-plugin-yml@3.3.2:
2633 + resolution: {integrity: sha512-XjmOB/fLBwYHqevnpclPL938V+9ExX7xw1hPaM3IPePGyMFRV1giS16RjSTNhIyCv/Oh0G0PEdmmZPATJ02YCw==}
2634 engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
2635 peerDependencies:
2636 eslint: '>=9.38.0'
@@ -2723,14 +2740,14 @@ packages:
2740 fast-levenshtein@2.0.6:
2741 resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
2742
2726 - fast-string-truncated-width@1.2.1:
2727 - resolution: {integrity: sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==}
2743 + fast-string-truncated-width@3.0.3:
2744 + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==}
2745
2729 - fast-string-width@1.1.0:
2730 - resolution: {integrity: sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ==}
2746 + fast-string-width@3.0.2:
2747 + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
2748
2732 - fast-wrap-ansi@0.1.6:
2733 - resolution: {integrity: sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w==}
2749 + fast-wrap-ansi@0.2.0:
2750 + resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==}
2751
2752 fast-xml-builder@1.1.5:
2753 resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==}
@@ -2886,8 +2903,8 @@ packages:
2903 resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
2904 engines: {node: '>=18'}
2905
2889 - globals@17.5.0:
2890 - resolution: {integrity: sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==}
2906 + globals@17.6.0:
2907 + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==}
2908 engines: {node: '>=18'}
2909
2910 globrex@0.1.2:
@@ -3079,8 +3096,8 @@ packages:
3096 resolution: {integrity: sha512-rF5MAmps5esSlhCA+N1b6IYHDw9j/btzGaqfgie522jS02Ju/HXBxamlXVlKEHAxoMKQL77HWI8jlqWsFuekZA==}
3097 engines: {node: '>= 20'}
3098
3082 - jose@6.2.2:
3083 - resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==}
3099 + jose@6.2.3:
3100 + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==}
3101
3102 js-beautify@1.15.4:
3103 resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==}
@@ -3119,8 +3136,8 @@ packages:
3136 resolution: {integrity: sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==}
3137 engines: {node: '>=20.0.0'}
3138
3122 - jsdom@29.0.2:
3123 - resolution: {integrity: sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==}
3139 + jsdom@29.1.1:
3140 + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==}
3141 engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
3142 peerDependencies:
3143 canvas: ^3.0.0
@@ -3158,8 +3175,8 @@ packages:
3175 resolution: {integrity: sha512-75EA7EWZExL/j+MDKQrRbdzcRI2HOkRlmUw8fZJc1ioqFEOvBsq7Rt+A6yCxOt9w/TYNpkt52gC6nm/g5tFIng==}
3176 engines: {node: ^20.19.0 || ^22.13.0 || >=24}
3177
3161 - jsonfile@6.2.0:
3162 - resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
3178 + jsonfile@6.2.1:
3179 + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==}
3180
3181 jstransformer@1.0.0:
3182 resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==}
@@ -3553,13 +3570,13 @@ packages:
3570 peerDependencies:
3571 vue: ^3.0.0
3572
3556 - nanoid@3.3.11:
3557 - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
3573 + nanoid@3.3.12:
3574 + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
3575 engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
3576 hasBin: true
3577
3561 - nanoid@5.1.9:
3562 - resolution: {integrity: sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==}
3578 + nanoid@5.1.11:
3579 + resolution: {integrity: sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==}
3580 engines: {node: ^18 || >=20}
3581 hasBin: true
3582
@@ -3576,8 +3593,8 @@ packages:
3593 node-fetch-native@1.6.7:
3594 resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
3595
3579 - node-releases@2.0.37:
3580 - resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==}
3596 + node-releases@2.0.38:
3597 + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==}
3598
3599 node-sarif-builder@3.4.0:
3600 resolution: {integrity: sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==}
@@ -3688,8 +3705,8 @@ packages:
3705 parse5@7.3.0:
3706 resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
3707
3691 - parse5@8.0.0:
3692 - resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==}
3708 + parse5@8.0.1:
3709 + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==}
3710
3711 password-validator@5.3.0:
3712 resolution: {integrity: sha512-Q+bSEM5pjokZqzWGoQaoylkeWeH4+9uMYlVImiPD0EOJClQ2RPBhrJ5h0OjhMKtwOmu5rRcLaTZo5Gk9RBl0ig==}
@@ -3772,8 +3789,8 @@ packages:
3789 pkg-types@1.3.1:
3790 resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
3791
3775 - pkg-types@2.3.0:
3776 - resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
3792 + pkg-types@2.3.1:
3793 + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
3794
3795 please-upgrade-node@3.2.0:
3796 resolution: {integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==}
@@ -3792,16 +3809,16 @@ packages:
3809 resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
3810 engines: {node: '>=4'}
3811
3795 - postcss@8.5.10:
3796 - resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==}
3812 + postcss@8.5.13:
3813 + resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==}
3814 engines: {node: ^10 || ^12 || >=14}
3815
3816 prelude-ls@1.2.1:
3817 resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
3818 engines: {node: '>= 0.8.0'}
3819
3803 - prettier-plugin-tailwindcss@0.7.3:
3804 - resolution: {integrity: sha512-lckXaWWdo2ZVXoMoUO3WIBiz9hVY+YBEh1gYyMFfrWP9WZW/wpFXQKizHx7WrFQFMkcG0bGShdpp531X1n+qpg==}
3820 + prettier-plugin-tailwindcss@0.7.4:
3821 + resolution: {integrity: sha512-UKii4RjY05SNt/WQi6/NcOn/LsT0/ILLXsxygjbRg5/YZelsSu5jTqorYHPDGq4nZy5q5hpCu+XdGZ1xaJEQgw==}
3822 engines: {node: '>=20.19'}
3823 peerDependencies:
3824 '@ianvs/prettier-plugin-sort-imports': '*'
@@ -3932,6 +3949,15 @@ packages:
3949 rc9@3.0.1:
3950 resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==}
3951
3952 + react-dom@19.2.5:
3953 + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
3954 + peerDependencies:
3955 + react: ^19.2.5
3956 +
3957 + react@19.2.5:
3958 + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==}
3959 + engines: {node: '>=0.10.0'}
3960 +
3961 read-package-json-fast@4.0.0:
3962 resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==}
3963 engines: {node: ^18.17.0 || >=20.5.0}
@@ -4067,6 +4093,9 @@ packages:
4093 resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
4094 engines: {node: '>=v12.22.7'}
4095
4096 + scheduler@0.27.0:
4097 + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
4098 +
4099 scslre@0.3.0:
4100 resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==}
4101 engines: {node: ^14.0.0 || >=16.0.0}
@@ -4233,8 +4262,8 @@ packages:
4262 tailwindcss@4.2.4:
4263 resolution: {integrity: sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==}
4264
4236 - tapable@2.3.2:
4237 - resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==}
4265 + tapable@2.3.3:
4266 + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
4267 engines: {node: '>=6'}
4268
4269 taze@19.11.0:
@@ -4251,8 +4280,8 @@ packages:
4280 tinybench@2.9.0:
4281 resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
4282
4254 - tinyexec@1.1.1:
4255 - resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==}
4283 + tinyexec@1.1.2:
4284 + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==}
4285 engines: {node: '>=18'}
4286
4287 tinyglobby@0.2.16:
@@ -4263,11 +4292,11 @@ packages:
4292 resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
4293 engines: {node: '>=14.0.0'}
4294
4266 - tldts-core@7.0.28:
4267 - resolution: {integrity: sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==}
4295 + tldts-core@7.0.29:
4296 + resolution: {integrity: sha512-W99NuU7b1DcG3uJ3v9k9VztCH3WialNbBkBft5wCs8V8mexu0XQqaZEYb9l9RNNzK8+3EJ9PKWB0/RUtTQ/o+Q==}
4297
4269 - tldts@7.0.28:
4270 - resolution: {integrity: sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==}
4298 + tldts@7.0.29:
4299 + resolution: {integrity: sha512-JIXCerhudr/N6OWLwLF1HVsTTUo7ry6qHa5eWZEkiMuxsIiAACL55tGLfqfHfoH7QaMQUW8fngD7u7TxWexYQg==}
4300 hasBin: true
4301
4302 tmp@0.2.5:
@@ -4347,8 +4376,8 @@ packages:
4376 uc.micro@2.1.0:
4377 resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
4378
4350 - ufo@1.6.3:
4351 - resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
4379 + ufo@1.6.4:
4380 + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
4381
4382 unconfig-core@7.5.0:
4383 resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==}
@@ -4444,10 +4473,10 @@ packages:
4473 peerDependencies:
4474 vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0
4475
4447 - vite-hot-client@2.1.0:
4448 - resolution: {integrity: sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==}
4476 + vite-hot-client@2.2.0:
4477 + resolution: {integrity: sha512-76Zs9zrHbH7M7wqeyooGQKdX+yg0pQ0xuQ1PbFp4z5a0Lzn2e5IPFoCswnmqZ4GiwqB4Jo3WcDAMO9jARTJl8w==}
4478 peerDependencies:
4450 - vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0
4479 + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0
4480
4481 vite-plugin-inspect@11.3.3:
4482 resolution: {integrity: sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==}
@@ -4797,124 +4826,124 @@ snapshots:
4826 dependencies:
4827 lodash: 4.17.21
4828
4800 - '@algolia/abtesting@1.16.2':
4829 + '@algolia/abtesting@1.18.0':
4830 dependencies:
4802 - '@algolia/client-common': 5.50.2
4803 - '@algolia/requester-browser-xhr': 5.50.2
4804 - '@algolia/requester-fetch': 5.50.2
4805 - '@algolia/requester-node-http': 5.50.2
4831 + '@algolia/client-common': 5.52.0
4832 + '@algolia/requester-browser-xhr': 5.52.0
4833 + '@algolia/requester-fetch': 5.52.0
4834 + '@algolia/requester-node-http': 5.52.0
4835
4807 - '@algolia/client-abtesting@5.50.2':
4836 + '@algolia/client-abtesting@5.52.0':
4837 dependencies:
4809 - '@algolia/client-common': 5.50.2
4810 - '@algolia/requester-browser-xhr': 5.50.2
4811 - '@algolia/requester-fetch': 5.50.2
4812 - '@algolia/requester-node-http': 5.50.2
4838 + '@algolia/client-common': 5.52.0
4839 + '@algolia/requester-browser-xhr': 5.52.0
4840 + '@algolia/requester-fetch': 5.52.0
4841 + '@algolia/requester-node-http': 5.52.0
4842
4814 - '@algolia/client-analytics@5.50.2':
4843 + '@algolia/client-analytics@5.52.0':
4844 dependencies:
4816 - '@algolia/client-common': 5.50.2
4817 - '@algolia/requester-browser-xhr': 5.50.2
4818 - '@algolia/requester-fetch': 5.50.2
4819 - '@algolia/requester-node-http': 5.50.2
4845 + '@algolia/client-common': 5.52.0
4846 + '@algolia/requester-browser-xhr': 5.52.0
4847 + '@algolia/requester-fetch': 5.52.0
4848 + '@algolia/requester-node-http': 5.52.0
4849
4821 - '@algolia/client-common@5.50.2': {}
4850 + '@algolia/client-common@5.52.0': {}
4851
4823 - '@algolia/client-insights@5.50.2':
4852 + '@algolia/client-insights@5.52.0':
4853 dependencies:
4825 - '@algolia/client-common': 5.50.2
4826 - '@algolia/requester-browser-xhr': 5.50.2
4827 - '@algolia/requester-fetch': 5.50.2
4828 - '@algolia/requester-node-http': 5.50.2
4854 + '@algolia/client-common': 5.52.0
4855 + '@algolia/requester-browser-xhr': 5.52.0
4856 + '@algolia/requester-fetch': 5.52.0
4857 + '@algolia/requester-node-http': 5.52.0
4858
4830 - '@algolia/client-personalization@5.50.2':
4859 + '@algolia/client-personalization@5.52.0':
4860 dependencies:
4832 - '@algolia/client-common': 5.50.2
4833 - '@algolia/requester-browser-xhr': 5.50.2
4834 - '@algolia/requester-fetch': 5.50.2
4835 - '@algolia/requester-node-http': 5.50.2
4861 + '@algolia/client-common': 5.52.0
4862 + '@algolia/requester-browser-xhr': 5.52.0
4863 + '@algolia/requester-fetch': 5.52.0
4864 + '@algolia/requester-node-http': 5.52.0
4865
4837 - '@algolia/client-query-suggestions@5.50.2':
4866 + '@algolia/client-query-suggestions@5.52.0':
4867 dependencies:
4839 - '@algolia/client-common': 5.50.2
4840 - '@algolia/requester-browser-xhr': 5.50.2
4841 - '@algolia/requester-fetch': 5.50.2
4842 - '@algolia/requester-node-http': 5.50.2
4868 + '@algolia/client-common': 5.52.0
4869 + '@algolia/requester-browser-xhr': 5.52.0
4870 + '@algolia/requester-fetch': 5.52.0
4871 + '@algolia/requester-node-http': 5.52.0
4872
4844 - '@algolia/client-search@5.50.2':
4873 + '@algolia/client-search@5.52.0':
4874 dependencies:
4846 - '@algolia/client-common': 5.50.2
4847 - '@algolia/requester-browser-xhr': 5.50.2
4848 - '@algolia/requester-fetch': 5.50.2
4849 - '@algolia/requester-node-http': 5.50.2
4875 + '@algolia/client-common': 5.52.0
4876 + '@algolia/requester-browser-xhr': 5.52.0
4877 + '@algolia/requester-fetch': 5.52.0
4878 + '@algolia/requester-node-http': 5.52.0
4879
4851 - '@algolia/ingestion@1.50.2':
4880 + '@algolia/ingestion@1.52.0':
4881 dependencies:
4853 - '@algolia/client-common': 5.50.2
4854 - '@algolia/requester-browser-xhr': 5.50.2
4855 - '@algolia/requester-fetch': 5.50.2
4856 - '@algolia/requester-node-http': 5.50.2
4882 + '@algolia/client-common': 5.52.0
4883 + '@algolia/requester-browser-xhr': 5.52.0
4884 + '@algolia/requester-fetch': 5.52.0
4885 + '@algolia/requester-node-http': 5.52.0
4886
4858 - '@algolia/monitoring@1.50.2':
4887 + '@algolia/monitoring@1.52.0':
4888 dependencies:
4860 - '@algolia/client-common': 5.50.2
4861 - '@algolia/requester-browser-xhr': 5.50.2
4862 - '@algolia/requester-fetch': 5.50.2
4863 - '@algolia/requester-node-http': 5.50.2
4889 + '@algolia/client-common': 5.52.0
4890 + '@algolia/requester-browser-xhr': 5.52.0
4891 + '@algolia/requester-fetch': 5.52.0
4892 + '@algolia/requester-node-http': 5.52.0
4893
4865 - '@algolia/recommend@5.50.2':
4894 + '@algolia/recommend@5.52.0':
4895 dependencies:
4867 - '@algolia/client-common': 5.50.2
4868 - '@algolia/requester-browser-xhr': 5.50.2
4869 - '@algolia/requester-fetch': 5.50.2
4870 - '@algolia/requester-node-http': 5.50.2
4896 + '@algolia/client-common': 5.52.0
4897 + '@algolia/requester-browser-xhr': 5.52.0
4898 + '@algolia/requester-fetch': 5.52.0
4899 + '@algolia/requester-node-http': 5.52.0
4900
4872 - '@algolia/requester-browser-xhr@5.50.2':
4901 + '@algolia/requester-browser-xhr@5.52.0':
4902 dependencies:
4874 - '@algolia/client-common': 5.50.2
4903 + '@algolia/client-common': 5.52.0
4904
4876 - '@algolia/requester-fetch@5.50.2':
4905 + '@algolia/requester-fetch@5.52.0':
4906 dependencies:
4878 - '@algolia/client-common': 5.50.2
4907 + '@algolia/client-common': 5.52.0
4908
4880 - '@algolia/requester-node-http@5.50.2':
4909 + '@algolia/requester-node-http@5.52.0':
4910 dependencies:
4882 - '@algolia/client-common': 5.50.2
4911 + '@algolia/client-common': 5.52.0
4912
4884 - '@antfu/eslint-config@8.2.0(@typescript-eslint/rule-tester@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.58.2(typescript@5.9.3))(@typescript-eslint/utils@8.58.2(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.0.2)(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.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)))':
4914 dependencies:
4915 '@antfu/install-pkg': 1.1.0
4887 - '@clack/prompts': 1.2.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))
4919 '@eslint/markdown': 8.0.1
4920 '@stylistic/eslint-plugin': 5.10.0(eslint@10.2.1(jiti@2.6.1))
4892 - '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
4893 - '@typescript-eslint/parser': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
4894 - '@vitest/eslint-plugin': 1.6.16(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(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.0.2)(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)))
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)))
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))
4899 - eslint-flat-config-utils: 3.1.0
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))
4902 - eslint-plugin-command: 3.5.2(@typescript-eslint/rule-tester@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.58.2(typescript@5.9.3))(@typescript-eslint/utils@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(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)
4907 - eslint-plugin-no-only-tests: 3.3.0
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))
4913 - eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(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))
4914 - eslint-plugin-vue: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.2.1(jiti@2.6.1)))(@typescript-eslint/parser@8.58.2(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)))
4915 - eslint-plugin-yml: 3.3.1(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))
4917 - globals: 17.5.0
4946 + globals: 17.6.0
4947 local-pkg: 1.1.2
4948 parse-gitignore: 2.0.0
4949 toml-eslint-parser: 1.0.3
@@ -4934,13 +4963,13 @@ snapshots:
4963 '@antfu/install-pkg@1.1.0':
4964 dependencies:
4965 package-manager-detector: 1.6.0
4937 - tinyexec: 1.1.1
4966 + tinyexec: 1.1.2
4967
4939 - '@antfu/ni@30.0.0':
4968 + '@antfu/ni@30.1.0':
4969 dependencies:
4970 fzf: 0.5.2
4971 package-manager-detector: 1.6.0
4943 - tinyexec: 1.1.1
4972 + tinyexec: 1.1.2
4973 tinyglobby: 0.2.16
4974
4975 '@asamuzakjp/css-color@5.1.11':
@@ -4969,7 +4998,7 @@ snapshots:
4998 js-tokens: 4.0.0
4999 picocolors: 1.1.1
5000
4972 - '@babel/compat-data@7.29.0': {}
5001 + '@babel/compat-data@7.29.3': {}
5002
5003 '@babel/core@7.29.0':
5004 dependencies:
@@ -4978,7 +5007,7 @@ snapshots:
5007 '@babel/helper-compilation-targets': 7.28.6
5008 '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
5009 '@babel/helpers': 7.29.2
4981 - '@babel/parser': 7.29.2
5010 + '@babel/parser': 7.29.3
5011 '@babel/template': 7.28.6
5012 '@babel/traverse': 7.29.0
5013 '@babel/types': 7.29.0
@@ -4993,7 +5022,7 @@ snapshots:
5022
5023 '@babel/generator@7.29.1':
5024 dependencies:
4996 - '@babel/parser': 7.29.2
5025 + '@babel/parser': 7.29.3
5026 '@babel/types': 7.29.0
5027 '@jridgewell/gen-mapping': 0.3.13
5028 '@jridgewell/trace-mapping': 0.3.31
@@ -5005,13 +5034,13 @@ snapshots:
5034
5035 '@babel/helper-compilation-targets@7.28.6':
5036 dependencies:
5008 - '@babel/compat-data': 7.29.0
5037 + '@babel/compat-data': 7.29.3
5038 '@babel/helper-validator-option': 7.27.1
5039 browserslist: 4.28.2
5040 lru-cache: 5.1.1
5041 semver: 6.3.1
5042
5014 - '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)':
5043 + '@babel/helper-create-class-features-plugin@7.29.3(@babel/core@7.29.0)':
5044 dependencies:
5045 '@babel/core': 7.29.0
5046 '@babel/helper-annotate-as-pure': 7.27.3
@@ -5082,14 +5111,14 @@ snapshots:
5111 '@babel/template': 7.28.6
5112 '@babel/types': 7.29.0
5113
5085 - '@babel/parser@7.29.2':
5114 + '@babel/parser@7.29.3':
5115 dependencies:
5116 '@babel/types': 7.29.0
5117
5118 '@babel/plugin-proposal-decorators@7.29.0(@babel/core@7.29.0)':
5119 dependencies:
5120 '@babel/core': 7.29.0
5092 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0)
5121 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0)
5122 '@babel/helper-plugin-utils': 7.28.6
5123 '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0)
5124 transitivePeerDependencies:
@@ -5124,7 +5153,7 @@ snapshots:
5153 dependencies:
5154 '@babel/core': 7.29.0
5155 '@babel/helper-annotate-as-pure': 7.27.3
5127 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0)
5156 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0)
5157 '@babel/helper-plugin-utils': 7.28.6
5158 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
5159 '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
@@ -5134,7 +5163,7 @@ snapshots:
5163 '@babel/template@7.28.6':
5164 dependencies:
5165 '@babel/code-frame': 7.29.0
5137 - '@babel/parser': 7.29.2
5166 + '@babel/parser': 7.29.3
5167 '@babel/types': 7.29.0
5168
5169 '@babel/traverse@7.29.0':
@@ -5142,7 +5171,7 @@ snapshots:
5171 '@babel/code-frame': 7.29.0
5172 '@babel/generator': 7.29.1
5173 '@babel/helper-globals': 7.28.0
5145 - '@babel/parser': 7.29.2
5174 + '@babel/parser': 7.29.3
5175 '@babel/template': 7.28.6
5176 '@babel/types': 7.29.0
5177 debug: 4.4.3
@@ -5158,16 +5187,16 @@ snapshots:
5187 dependencies:
5188 css-tree: 3.2.1
5189
5161 - '@clack/core@1.2.0':
5190 + '@clack/core@1.3.0':
5191 dependencies:
5163 - fast-wrap-ansi: 0.1.6
5192 + fast-wrap-ansi: 0.2.0
5193 sisteransi: 1.0.5
5194
5166 - '@clack/prompts@1.2.0':
5195 + '@clack/prompts@1.3.0':
5196 dependencies:
5168 - '@clack/core': 1.2.0
5169 - fast-string-width: 1.1.0
5170 - fast-wrap-ansi: 0.1.6
5197 + '@clack/core': 1.3.0
5198 + fast-string-width: 3.0.2
5199 + fast-wrap-ansi: 0.2.0
5200 sisteransi: 1.0.5
5201
5202 '@codemirror/autocomplete@6.20.1':
@@ -5218,7 +5247,7 @@ snapshots:
5247 '@codemirror/view': 6.41.1
5248 crelt: 1.0.6
5249
5221 - '@codemirror/search@6.6.0':
5250 + '@codemirror/search@6.7.0':
5251 dependencies:
5252 '@codemirror/state': 6.6.0
5253 '@codemirror/view': 6.41.1
@@ -5288,7 +5317,7 @@ snapshots:
5317 '@es-joy/jsdoccomment@0.84.0':
5318 dependencies:
5319 '@types/estree': 1.0.8
5291 - '@typescript-eslint/types': 8.58.2
5320 + '@typescript-eslint/types': 8.59.1
5321 comment-parser: 1.4.5
5322 esquery: 1.7.0
5323 jsdoc-type-pratt-parser: 7.1.1
@@ -5296,7 +5325,7 @@ snapshots:
5325 '@es-joy/jsdoccomment@0.86.0':
5326 dependencies:
5327 '@types/estree': 1.0.8
5299 - '@typescript-eslint/types': 8.58.2
5328 + '@typescript-eslint/types': 8.59.1
5329 comment-parser: 1.4.6
5330 esquery: 1.7.0
5331 jsdoc-type-pratt-parser: 7.2.0
@@ -5621,7 +5650,7 @@ snapshots:
5650 '@nodelib/fs.scandir': 2.1.5
5651 fastq: 1.20.1
5652
5624 - '@nuxt/kit@3.21.2':
5653 + '@nuxt/kit@3.21.4':
5654 dependencies:
5655 c12: 3.3.4
5656 consola: 3.4.2
@@ -5636,12 +5665,12 @@ snapshots:
5665 mlly: 1.8.2
5666 ohash: 2.0.11
5667 pathe: 2.0.3
5639 - pkg-types: 2.3.0
5668 + pkg-types: 2.3.1
5669 rc9: 3.0.1
5670 scule: 1.3.0
5671 semver: 7.7.4
5672 tinyglobby: 0.2.16
5644 - ufo: 1.6.3
5673 + ufo: 1.6.4
5674 unctx: 2.5.0
5675 untyped: 2.0.0
5676 transitivePeerDependencies:
@@ -5726,7 +5755,7 @@ snapshots:
5755
5756 '@rolldown/pluginutils@1.0.0-rc.13': {}
5757
5729 - '@rolldown/pluginutils@1.0.0-rc.16': {}
5758 + '@rolldown/pluginutils@1.0.0-rc.18': {}
5759
5760 '@rollup/rollup-android-arm-eabi@4.60.2':
5761 optional: true
@@ -5848,18 +5877,20 @@ snapshots:
5877
5878 '@shikijs/vscode-textmate@10.0.2': {}
5879
5851 - '@sindresorhus/base62@1.0.0': {}
5852 -
5853 - '@singulio/app-auth-search@0.0.3':
5880 + '@shuffleio/shuffle-mcps@0.0.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
5881 dependencies:
5855 - algoliasearch: 5.50.2
5882 + algoliasearch: 5.52.0
5883 + react: 19.2.5
5884 + react-dom: 19.2.5(react@19.2.5)
5885 +
5886 + '@sindresorhus/base62@1.0.0': {}
5887
5888 '@standard-schema/spec@1.1.0': {}
5889
5890 '@stylistic/eslint-plugin@5.10.0(eslint@10.2.1(jiti@2.6.1))':
5891 dependencies:
5892 '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
5862 - '@typescript-eslint/types': 8.58.2
5893 + '@typescript-eslint/types': 8.59.1
5894 eslint: 10.2.1(jiti@2.6.1)
5895 eslint-visitor-keys: 4.2.1
5896 espree: 10.4.0
@@ -5869,7 +5900,7 @@ snapshots:
5900 '@tailwindcss/node@4.2.4':
5901 dependencies:
5902 '@jridgewell/remapping': 2.3.5
5872 - enhanced-resolve: 5.20.1
5903 + enhanced-resolve: 5.21.0
5904 jiti: 2.6.1
5905 lightningcss: 1.32.0
5906 magic-string: 0.30.21
@@ -6012,6 +6043,14 @@ snapshots:
6043
6044 '@types/parse-json@4.0.2': {}
6045
6046 + '@types/react-dom@19.2.3(@types/react@19.2.14)':
6047 + dependencies:
6048 + '@types/react': 19.2.14
6049 +
6050 + '@types/react@19.2.14':
6051 + dependencies:
6052 + csstype: 3.2.3
6053 +
6054 '@types/sarif@2.1.7': {}
6055
6056 '@types/tern@0.23.9':
@@ -6026,14 +6065,14 @@ snapshots:
6065
6066 '@types/web-bluetooth@0.0.21': {}
6067
6029 - '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(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.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)':
6069 dependencies:
6070 '@eslint-community/regexpp': 4.12.2
6032 - '@typescript-eslint/parser': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
6033 - '@typescript-eslint/scope-manager': 8.58.2
6034 - '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
6035 - '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
6036 - '@typescript-eslint/visitor-keys': 8.58.2
6071 + '@typescript-eslint/parser': 8.59.1(eslint@10.2.1(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)
6075 + '@typescript-eslint/visitor-keys': 8.59.1
6076 eslint: 10.2.1(jiti@2.6.1)
6077 ignore: 7.0.5
6078 natural-compare: 1.4.0
@@ -6042,33 +6081,33 @@ snapshots:
6081 transitivePeerDependencies:
6082 - supports-color
6083
6045 - '@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)':
6084 + '@typescript-eslint/parser@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)':
6085 dependencies:
6047 - '@typescript-eslint/scope-manager': 8.58.2
6048 - '@typescript-eslint/types': 8.58.2
6049 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3)
6050 - '@typescript-eslint/visitor-keys': 8.58.2
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)
6092 typescript: 5.9.3
6093 transitivePeerDependencies:
6094 - supports-color
6095
6057 - '@typescript-eslint/project-service@8.58.2(typescript@5.9.3)':
6096 + '@typescript-eslint/project-service@8.59.1(typescript@5.9.3)':
6097 dependencies:
6059 - '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@5.9.3)
6060 - '@typescript-eslint/types': 8.58.2
6098 + '@typescript-eslint/tsconfig-utils': 8.59.1(typescript@5.9.3)
6099 + '@typescript-eslint/types': 8.59.1
6100 debug: 4.4.3
6101 typescript: 5.9.3
6102 transitivePeerDependencies:
6103 - supports-color
6104
6066 - '@typescript-eslint/rule-tester@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)':
6105 + '@typescript-eslint/rule-tester@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)':
6106 dependencies:
6068 - '@typescript-eslint/parser': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
6069 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3)
6070 - '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
6071 - ajv: 6.14.0
6107 + '@typescript-eslint/parser': 8.59.1(eslint@10.2.1(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)
6110 + ajv: 6.15.0
6111 eslint: 10.2.1(jiti@2.6.1)
6112 json-stable-stringify-without-jsonify: 1.0.1
6113 lodash.merge: 4.6.2
@@ -6077,20 +6116,20 @@ snapshots:
6116 - supports-color
6117 - typescript
6118
6080 - '@typescript-eslint/scope-manager@8.58.2':
6119 + '@typescript-eslint/scope-manager@8.59.1':
6120 dependencies:
6082 - '@typescript-eslint/types': 8.58.2
6083 - '@typescript-eslint/visitor-keys': 8.58.2
6121 + '@typescript-eslint/types': 8.59.1
6122 + '@typescript-eslint/visitor-keys': 8.59.1
6123
6085 - '@typescript-eslint/tsconfig-utils@8.58.2(typescript@5.9.3)':
6124 + '@typescript-eslint/tsconfig-utils@8.59.1(typescript@5.9.3)':
6125 dependencies:
6126 typescript: 5.9.3
6127
6089 - '@typescript-eslint/type-utils@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)':
6128 + '@typescript-eslint/type-utils@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)':
6129 dependencies:
6091 - '@typescript-eslint/types': 8.58.2
6092 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3)
6093 - '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
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)
6133 debug: 4.4.3
6134 eslint: 10.2.1(jiti@2.6.1)
6135 ts-api-utils: 2.5.0(typescript@5.9.3)
@@ -6098,14 +6137,14 @@ snapshots:
6137 transitivePeerDependencies:
6138 - supports-color
6139
6101 - '@typescript-eslint/types@8.58.2': {}
6140 + '@typescript-eslint/types@8.59.1': {}
6141
6103 - '@typescript-eslint/typescript-estree@8.58.2(typescript@5.9.3)':
6142 + '@typescript-eslint/typescript-estree@8.59.1(typescript@5.9.3)':
6143 dependencies:
6105 - '@typescript-eslint/project-service': 8.58.2(typescript@5.9.3)
6106 - '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@5.9.3)
6107 - '@typescript-eslint/types': 8.58.2
6108 - '@typescript-eslint/visitor-keys': 8.58.2
6144 + '@typescript-eslint/project-service': 8.59.1(typescript@5.9.3)
6145 + '@typescript-eslint/tsconfig-utils': 8.59.1(typescript@5.9.3)
6146 + '@typescript-eslint/types': 8.59.1
6147 + '@typescript-eslint/visitor-keys': 8.59.1
6148 debug: 4.4.3
6149 minimatch: 10.2.5
6150 semver: 7.7.4
@@ -6115,20 +6154,20 @@ snapshots:
6154 transitivePeerDependencies:
6155 - supports-color
6156
6118 - '@typescript-eslint/utils@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)':
6157 + '@typescript-eslint/utils@8.59.1(eslint@10.2.1(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))
6121 - '@typescript-eslint/scope-manager': 8.58.2
6122 - '@typescript-eslint/types': 8.58.2
6123 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3)
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)
6164 typescript: 5.9.3
6165 transitivePeerDependencies:
6166 - supports-color
6167
6129 - '@typescript-eslint/visitor-keys@8.58.2':
6168 + '@typescript-eslint/visitor-keys@8.59.1':
6169 dependencies:
6131 - '@typescript-eslint/types': 8.58.2
6170 + '@typescript-eslint/types': 8.59.1
6171 eslint-visitor-keys: 5.0.1
6172
6173 '@ungap/structured-clone@1.3.0': {}
@@ -6138,7 +6177,7 @@ snapshots:
6177 '@babel/core': 7.29.0
6178 '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
6179 '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
6141 - '@rolldown/pluginutils': 1.0.0-rc.16
6180 + '@rolldown/pluginutils': 1.0.0-rc.18
6181 '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.0)
6182 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)
6183 vue: 3.5.33(typescript@5.9.3)
@@ -6151,15 +6190,15 @@ 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
6154 - '@vitest/eslint-plugin@1.6.16(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(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.0.2)(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.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)))':
6194 dependencies:
6156 - '@typescript-eslint/scope-manager': 8.58.2
6157 - '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
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)
6198 optionalDependencies:
6160 - '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(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.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
6200 typescript: 5.9.3
6162 - vitest: 4.1.5(@types/node@25.6.0)(jsdom@29.0.2)(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))
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:
6203 - supports-color
6204
@@ -6218,7 +6257,7 @@ snapshots:
6257
6258 '@vue-macros/common@3.1.2(vue@3.5.33(typescript@5.9.3))':
6259 dependencies:
6221 - '@vue/compiler-sfc': 3.5.32
6260 + '@vue/compiler-sfc': 3.5.33
6261 ast-kit: 2.2.0
6262 local-pkg: 1.1.2
6263 magic-string-ast: 1.0.3
@@ -6240,7 +6279,7 @@ snapshots:
6279 '@babel/types': 7.29.0
6280 '@vue/babel-helper-vue-transform-on': 1.5.0
6281 '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.29.0)
6243 - '@vue/shared': 3.5.32
6282 + '@vue/shared': 3.5.33
6283 optionalDependencies:
6284 '@babel/core': 7.29.0
6285 transitivePeerDependencies:
@@ -6256,7 +6295,7 @@ snapshots:
6295 '@babel/types': 7.29.0
6296 '@vue/babel-helper-vue-transform-on': 2.0.1
6297 '@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.29.0)
6259 - '@vue/shared': 3.5.32
6298 + '@vue/shared': 3.5.33
6299 optionalDependencies:
6300 '@babel/core': 7.29.0
6301 transitivePeerDependencies:
@@ -6268,8 +6307,8 @@ snapshots:
6307 '@babel/core': 7.29.0
6308 '@babel/helper-module-imports': 7.28.6
6309 '@babel/helper-plugin-utils': 7.28.6
6271 - '@babel/parser': 7.29.2
6272 - '@vue/compiler-sfc': 3.5.32
6310 + '@babel/parser': 7.29.3
6311 + '@vue/compiler-sfc': 3.5.33
6312 transitivePeerDependencies:
6313 - supports-color
6314
@@ -6279,66 +6318,36 @@ snapshots:
6318 '@babel/core': 7.29.0
6319 '@babel/helper-module-imports': 7.28.6
6320 '@babel/helper-plugin-utils': 7.28.6
6282 - '@babel/parser': 7.29.2
6283 - '@vue/compiler-sfc': 3.5.32
6321 + '@babel/parser': 7.29.3
6322 + '@vue/compiler-sfc': 3.5.33
6323 transitivePeerDependencies:
6324 - supports-color
6325
6287 - '@vue/compiler-core@3.5.32':
6288 - dependencies:
6289 - '@babel/parser': 7.29.2
6290 - '@vue/shared': 3.5.32
6291 - entities: 7.0.1
6292 - estree-walker: 2.0.2
6293 - source-map-js: 1.2.1
6294 -
6326 '@vue/compiler-core@3.5.33':
6327 dependencies:
6297 - '@babel/parser': 7.29.2
6328 + '@babel/parser': 7.29.3
6329 '@vue/shared': 3.5.33
6330 entities: 7.0.1
6331 estree-walker: 2.0.2
6332 source-map-js: 1.2.1
6333
6303 - '@vue/compiler-dom@3.5.32':
6304 - dependencies:
6305 - '@vue/compiler-core': 3.5.32
6306 - '@vue/shared': 3.5.32
6307 -
6334 '@vue/compiler-dom@3.5.33':
6335 dependencies:
6336 '@vue/compiler-core': 3.5.33
6337 '@vue/shared': 3.5.33
6338
6313 - '@vue/compiler-sfc@3.5.32':
6314 - dependencies:
6315 - '@babel/parser': 7.29.2
6316 - '@vue/compiler-core': 3.5.32
6317 - '@vue/compiler-dom': 3.5.32
6318 - '@vue/compiler-ssr': 3.5.32
6319 - '@vue/shared': 3.5.32
6320 - estree-walker: 2.0.2
6321 - magic-string: 0.30.21
6322 - postcss: 8.5.10
6323 - source-map-js: 1.2.1
6324 -
6339 '@vue/compiler-sfc@3.5.33':
6340 dependencies:
6327 - '@babel/parser': 7.29.2
6341 + '@babel/parser': 7.29.3
6342 '@vue/compiler-core': 3.5.33
6343 '@vue/compiler-dom': 3.5.33
6344 '@vue/compiler-ssr': 3.5.33
6345 '@vue/shared': 3.5.33
6346 estree-walker: 2.0.2
6347 magic-string: 0.30.21
6334 - postcss: 8.5.10
6348 + postcss: 8.5.13
6349 source-map-js: 1.2.1
6350
6337 - '@vue/compiler-ssr@3.5.32':
6338 - dependencies:
6339 - '@vue/compiler-dom': 3.5.32
6340 - '@vue/shared': 3.5.32
6341 -
6351 '@vue/compiler-ssr@3.5.33':
6352 dependencies:
6353 '@vue/compiler-dom': 3.5.33
@@ -6386,8 +6395,8 @@ snapshots:
6395 '@vue/language-core@3.2.7':
6396 dependencies:
6397 '@volar/language-core': 2.4.28
6389 - '@vue/compiler-dom': 3.5.32
6390 - '@vue/shared': 3.5.32
6398 + '@vue/compiler-dom': 3.5.33
6399 + '@vue/shared': 3.5.33
6400 alien-signals: 3.1.2
6401 muggle-string: 0.4.1
6402 path-browserify: 1.0.1
@@ -6415,11 +6424,9 @@ snapshots:
6424 '@vue/shared': 3.5.33
6425 vue: 3.5.33(typescript@5.9.3)
6426
6418 - '@vue/shared@3.5.32': {}
6419 -
6427 '@vue/shared@3.5.33': {}
6428
6422 - '@vue/test-utils@2.4.9(@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))':
6429 + '@vue/test-utils@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))':
6430 dependencies:
6431 '@vue/compiler-dom': 3.5.33
6432 js-beautify: 1.15.4
@@ -6440,16 +6447,16 @@ snapshots:
6447 '@vueuse/shared': 13.9.0(vue@3.5.33(typescript@5.9.3))
6448 vue: 3.5.33(typescript@5.9.3)
6449
6443 - '@vueuse/core@14.2.1(vue@3.5.33(typescript@5.9.3))':
6450 + '@vueuse/core@14.3.0(vue@3.5.33(typescript@5.9.3))':
6451 dependencies:
6452 '@types/web-bluetooth': 0.0.21
6446 - '@vueuse/metadata': 14.2.1
6447 - '@vueuse/shared': 14.2.1(vue@3.5.33(typescript@5.9.3))
6453 + '@vueuse/metadata': 14.3.0
6454 + '@vueuse/shared': 14.3.0(vue@3.5.33(typescript@5.9.3))
6455 vue: 3.5.33(typescript@5.9.3)
6456
6457 '@vueuse/metadata@13.9.0': {}
6458
6452 - '@vueuse/metadata@14.2.1': {}
6459 + '@vueuse/metadata@14.3.0': {}
6460
6461 '@vueuse/motion@3.0.3(vue@3.5.33(typescript@5.9.3))':
6462 dependencies:
@@ -6461,7 +6468,7 @@ snapshots:
6468 style-value-types: 5.1.2
6469 vue: 3.5.33(typescript@5.9.3)
6470 optionalDependencies:
6464 - '@nuxt/kit': 3.21.2
6471 + '@nuxt/kit': 3.21.4
6472 transitivePeerDependencies:
6473 - magicast
6474
@@ -6469,7 +6476,7 @@ snapshots:
6476 dependencies:
6477 vue: 3.5.33(typescript@5.9.3)
6478
6472 - '@vueuse/shared@14.2.1(vue@3.5.33(typescript@5.9.3))':
6479 + '@vueuse/shared@14.3.0(vue@3.5.33(typescript@5.9.3))':
6480 dependencies:
6481 vue: 3.5.33(typescript@5.9.3)
6482
@@ -6483,29 +6490,29 @@ snapshots:
6490
6491 acorn@8.16.0: {}
6492
6486 - ajv@6.14.0:
6493 + ajv@6.15.0:
6494 dependencies:
6495 fast-deep-equal: 3.1.3
6496 fast-json-stable-stringify: 2.1.0
6497 json-schema-traverse: 0.4.1
6498 uri-js: 4.4.1
6499
6493 - algoliasearch@5.50.2:
6494 - dependencies:
6495 - '@algolia/abtesting': 1.16.2
6496 - '@algolia/client-abtesting': 5.50.2
6497 - '@algolia/client-analytics': 5.50.2
6498 - '@algolia/client-common': 5.50.2
6499 - '@algolia/client-insights': 5.50.2
6500 - '@algolia/client-personalization': 5.50.2
6501 - '@algolia/client-query-suggestions': 5.50.2
6502 - '@algolia/client-search': 5.50.2
6503 - '@algolia/ingestion': 1.50.2
6504 - '@algolia/monitoring': 1.50.2
6505 - '@algolia/recommend': 5.50.2
6506 - '@algolia/requester-browser-xhr': 5.50.2
6507 - '@algolia/requester-fetch': 5.50.2
6508 - '@algolia/requester-node-http': 5.50.2
6500 + algoliasearch@5.52.0:
6501 + dependencies:
6502 + '@algolia/abtesting': 1.18.0
6503 + '@algolia/client-abtesting': 5.52.0
6504 + '@algolia/client-analytics': 5.52.0
6505 + '@algolia/client-common': 5.52.0
6506 + '@algolia/client-insights': 5.52.0
6507 + '@algolia/client-personalization': 5.52.0
6508 + '@algolia/client-query-suggestions': 5.52.0
6509 + '@algolia/client-search': 5.52.0
6510 + '@algolia/ingestion': 1.52.0
6511 + '@algolia/monitoring': 1.52.0
6512 + '@algolia/recommend': 5.52.0
6513 + '@algolia/requester-browser-xhr': 5.52.0
6514 + '@algolia/requester-fetch': 5.52.0
6515 + '@algolia/requester-node-http': 5.52.0
6516
6517 alien-signals@3.1.2: {}
6518
@@ -6547,12 +6554,12 @@ snapshots:
6554
6555 ast-kit@2.2.0:
6556 dependencies:
6550 - '@babel/parser': 7.29.2
6557 + '@babel/parser': 7.29.3
6558 pathe: 2.0.3
6559
6560 ast-walker-scope@0.8.3:
6561 dependencies:
6555 - '@babel/parser': 7.29.2
6562 + '@babel/parser': 7.29.3
6563 ast-kit: 2.2.0
6564
6565 async-validator@4.2.5: {}
@@ -6577,7 +6584,7 @@ snapshots:
6584
6585 balanced-match@4.0.4: {}
6586
6580 - baseline-browser-mapping@2.10.23: {}
6587 + baseline-browser-mapping@2.10.25: {}
6588
6589 bidi-js@1.0.3:
6590 dependencies:
@@ -6613,10 +6620,10 @@ snapshots:
6620
6621 browserslist@4.28.2:
6622 dependencies:
6616 - baseline-browser-mapping: 2.10.23
6617 - caniuse-lite: 1.0.30001788
6618 - electron-to-chromium: 1.5.340
6619 - node-releases: 2.0.37
6623 + baseline-browser-mapping: 2.10.25
6624 + caniuse-lite: 1.0.30001791
6625 + electron-to-chromium: 1.5.348
6626 + node-releases: 2.0.38
6627 update-browserslist-db: 1.2.3(browserslist@4.28.2)
6628
6629 builtin-modules@5.1.0: {}
@@ -6639,7 +6646,7 @@ snapshots:
6646 ohash: 2.0.11
6647 pathe: 2.0.3
6648 perfect-debounce: 2.1.0
6642 - pkg-types: 2.3.0
6649 + pkg-types: 2.3.1
6650 rc9: 3.0.1
6651 optional: true
6652
@@ -6663,7 +6670,7 @@ snapshots:
6670
6671 camelcase@6.3.0: {}
6672
6666 - caniuse-lite@1.0.30001788: {}
6673 + caniuse-lite@1.0.30001791: {}
6674
6675 ccount@2.0.1: {}
6676
@@ -6728,7 +6735,7 @@ snapshots:
6735 '@codemirror/commands': 6.10.3
6736 '@codemirror/language': 6.12.3
6737 '@codemirror/lint': 6.9.5
6731 - '@codemirror/search': 6.6.0
6738 + '@codemirror/search': 6.7.0
6739 '@codemirror/state': 6.6.0
6740 '@codemirror/view': 6.41.1
6741
@@ -6776,7 +6783,7 @@ snapshots:
6783
6784 constantinople@4.0.1:
6785 dependencies:
6779 - '@babel/parser': 7.29.2
6786 + '@babel/parser': 7.29.3
6787 '@babel/types': 7.29.0
6788
6789 convert-source-map@2.0.0: {}
@@ -6893,9 +6900,9 @@ snapshots:
6900
6901 depcheck@1.4.7:
6902 dependencies:
6896 - '@babel/parser': 7.29.2
6903 + '@babel/parser': 7.29.3
6904 '@babel/traverse': 7.29.0
6898 - '@vue/compiler-sfc': 3.5.32
6905 + '@vue/compiler-sfc': 3.5.33
6906 callsite: 1.0.0
6907 camelcase: 6.3.0
6908 cosmiconfig: 7.1.0
@@ -6982,7 +6989,7 @@ snapshots:
6989 minimatch: 9.0.9
6990 semver: 7.7.4
6991
6985 - electron-to-chromium@1.5.340: {}
6992 + electron-to-chromium@1.5.348: {}
6993
6994 emoji-regex@8.0.0: {}
6995
@@ -6994,10 +7001,10 @@ snapshots:
7001 dependencies:
7002 once: 1.4.0
7003
6997 - enhanced-resolve@5.20.1:
7004 + enhanced-resolve@5.21.0:
7005 dependencies:
7006 graceful-fs: 4.2.11
7000 - tapable: 2.3.2
7007 + tapable: 2.3.3
7008
7009 entities@4.5.0: {}
7010
@@ -7005,6 +7012,8 @@ snapshots:
7012
7013 entities@7.0.1: {}
7014
7015 + entities@8.0.0: {}
7016 +
7017 error-ex@1.3.4:
7018 dependencies:
7019 is-arrayish: 0.2.1
@@ -7018,7 +7027,7 @@ snapshots:
7027
7028 es-errors@1.3.0: {}
7029
7021 - es-module-lexer@2.0.0: {}
7030 + es-module-lexer@2.1.0: {}
7031
7032 es-object-atoms@1.1.1:
7033 dependencies:
@@ -7078,7 +7087,7 @@ snapshots:
7087 '@eslint/compat': 2.0.5(eslint@10.2.1(jiti@2.6.1))
7088 eslint: 10.2.1(jiti@2.6.1)
7089
7081 - eslint-flat-config-utils@3.1.0:
7090 + eslint-flat-config-utils@3.2.0:
7091 dependencies:
7092 '@eslint/config-helpers': 0.5.5
7093 pathe: 2.0.3
@@ -7097,12 +7106,12 @@ snapshots:
7106 dependencies:
7107 eslint: 10.2.1(jiti@2.6.1)
7108
7100 - eslint-plugin-command@3.5.2(@typescript-eslint/rule-tester@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.58.2(typescript@5.9.3))(@typescript-eslint/utils@8.58.2(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.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)):
7110 dependencies:
7111 '@es-joy/jsdoccomment': 0.84.0
7103 - '@typescript-eslint/rule-tester': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
7104 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3)
7105 - '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
7112 + '@typescript-eslint/rule-tester': 8.59.1(eslint@10.2.1(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)
7116
7117 eslint-plugin-depend@1.5.0(eslint@10.2.1(jiti@2.6.1)):
@@ -7161,7 +7170,7 @@ snapshots:
7170 eslint-plugin-n@17.24.0(eslint@10.2.1(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))
7164 - enhanced-resolve: 5.20.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))
7176 get-tsconfig: 4.14.0
@@ -7173,11 +7182,11 @@ snapshots:
7182 transitivePeerDependencies:
7183 - typescript
7184
7176 - eslint-plugin-no-only-tests@3.3.0: {}
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):
7188 dependencies:
7180 - '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
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)
7191 natural-orderby: 5.0.0
7192 transitivePeerDependencies:
@@ -7227,7 +7236,7 @@ snapshots:
7236 core-js-compat: 3.49.0
7237 eslint: 10.2.1(jiti@2.6.1)
7238 find-up-simple: 1.0.1
7230 - globals: 17.5.0
7239 + globals: 17.6.0
7240 indent-string: 5.0.0
7241 is-builtin-module: 5.0.0
7242 jsesc: 3.1.0
@@ -7237,13 +7246,13 @@ snapshots:
7246 semver: 7.7.4
7247 strip-indent: 4.1.1
7248
7240 - eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(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.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)):
7250 dependencies:
7251 eslint: 10.2.1(jiti@2.6.1)
7252 optionalDependencies:
7244 - '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(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.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
7254
7246 - eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.2.1(jiti@2.6.1)))(@typescript-eslint/parser@8.58.2(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.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))):
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)
@@ -7255,21 +7264,18 @@ snapshots:
7264 xml-name-validator: 4.0.0
7265 optionalDependencies:
7266 '@stylistic/eslint-plugin': 5.10.0(eslint@10.2.1(jiti@2.6.1))
7258 - '@typescript-eslint/parser': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
7267 + '@typescript-eslint/parser': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
7268
7260 - eslint-plugin-yml@3.3.1(eslint@10.2.1(jiti@2.6.1)):
7269 + eslint-plugin-yml@3.3.2(eslint@10.2.1(jiti@2.6.1)):
7270 dependencies:
7271 '@eslint/core': 1.2.1
7263 - '@eslint/plugin-kit': 0.6.1
7272 + '@eslint/plugin-kit': 0.7.1
7273 '@ota-meshi/ast-token-store': 0.3.0
7265 - debug: 4.4.3
7274 diff-sequences: 29.6.3
7275 escape-string-regexp: 5.0.0
7276 eslint: 10.2.1(jiti@2.6.1)
7277 natural-compare: 1.4.0
7278 yaml-eslint-parser: 2.0.0
7271 - transitivePeerDependencies:
7272 - - supports-color
7279
7280 eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.33)(eslint@10.2.1(jiti@2.6.1)):
7281 dependencies:
@@ -7301,7 +7307,7 @@ snapshots:
7307 '@humanwhocodes/module-importer': 1.0.1
7308 '@humanwhocodes/retry': 0.4.3
7309 '@types/estree': 1.0.8
7304 - ajv: 6.14.0
7310 + ajv: 6.15.0
7311 cross-spawn: 7.0.6
7312 debug: 4.4.3
7313 escape-string-regexp: 4.0.0
@@ -7408,15 +7414,15 @@ snapshots:
7414
7415 fast-levenshtein@2.0.6: {}
7416
7411 - fast-string-truncated-width@1.2.1: {}
7417 + fast-string-truncated-width@3.0.3: {}
7418
7413 - fast-string-width@1.1.0:
7419 + fast-string-width@3.0.2:
7420 dependencies:
7415 - fast-string-truncated-width: 1.2.1
7421 + fast-string-truncated-width: 3.0.3
7422
7417 - fast-wrap-ansi@0.1.6:
7423 + fast-wrap-ansi@0.2.0:
7424 dependencies:
7419 - fast-string-width: 1.1.0
7425 + fast-string-width: 3.0.2
7426
7427 fast-xml-builder@1.1.5:
7428 dependencies:
@@ -7500,7 +7506,7 @@ snapshots:
7506 fs-extra@11.3.4:
7507 dependencies:
7508 graceful-fs: 4.2.11
7503 - jsonfile: 6.2.0
7509 + jsonfile: 6.2.1
7510 universalify: 2.0.1
7511
7512 fsevents@2.3.3:
@@ -7580,7 +7586,7 @@ snapshots:
7586
7587 globals@15.15.0: {}
7588
7583 - globals@17.5.0: {}
7589 + globals@17.6.0: {}
7590
7591 globrex@0.1.2: {}
7592
@@ -7748,7 +7754,7 @@ snapshots:
7754 '@hapi/topo': 6.0.2
7755 '@standard-schema/spec': 1.1.0
7756
7751 - jose@6.2.2: {}
7757 + jose@6.2.3: {}
7758
7759 js-beautify@1.15.4:
7760 dependencies:
@@ -7793,7 +7799,7 @@ snapshots:
7799
7800 jsdoc-type-pratt-parser@7.2.0: {}
7801
7796 - jsdom@29.0.2:
7802 + jsdom@29.1.1:
7803 dependencies:
7804 '@asamuzakjp/css-color': 5.1.11
7805 '@asamuzakjp/dom-selector': 7.1.1
@@ -7806,7 +7812,7 @@ snapshots:
7812 html-encoding-sniffer: 6.0.0
7813 is-potential-custom-element-name: 1.0.1
7814 lru-cache: 11.3.5
7809 - parse5: 8.0.0
7815 + parse5: 8.0.1
7816 saxes: 6.0.0
7817 symbol-tree: 3.2.4
7818 tough-cookie: 6.0.1
@@ -7839,7 +7845,7 @@ snapshots:
7845 eslint-visitor-keys: 5.0.1
7846 semver: 7.7.4
7847
7842 - jsonfile@6.2.0:
7848 + jsonfile@6.2.1:
7849 dependencies:
7850 universalify: 2.0.1
7851 optionalDependencies:
@@ -7931,7 +7937,7 @@ snapshots:
7937 local-pkg@1.1.2:
7938 dependencies:
7939 mlly: 1.8.2
7934 - pkg-types: 2.3.0
7940 + pkg-types: 2.3.1
7941 quansync: 0.2.11
7942
7943 locate-path@6.0.0:
@@ -8384,7 +8390,7 @@ snapshots:
8390 acorn: 8.16.0
8391 pathe: 2.0.3
8392 pkg-types: 1.3.1
8387 - ufo: 1.6.3
8393 + ufo: 1.6.4
8394
8395 module-replacements@2.11.0: {}
8396
@@ -8424,9 +8430,9 @@ snapshots:
8430 vue: 3.5.33(typescript@5.9.3)
8431 vueuc: 0.4.65(vue@3.5.33(typescript@5.9.3))
8432
8427 - nanoid@3.3.11: {}
8433 + nanoid@3.3.12: {}
8434
8429 - nanoid@5.1.9: {}
8435 + nanoid@5.1.11: {}
8436
8437 natural-compare@1.4.0: {}
8438
@@ -8437,7 +8443,7 @@ snapshots:
8443
8444 node-fetch-native@1.6.7: {}
8445
8440 - node-releases@2.0.37: {}
8446 + node-releases@2.0.38: {}
8447
8448 node-sarif-builder@3.4.0:
8449 dependencies:
@@ -8479,7 +8485,7 @@ snapshots:
8485 dependencies:
8486 destr: 2.0.5
8487 node-fetch-native: 1.6.7
8482 - ufo: 1.6.3
8488 + ufo: 1.6.4
8489
8490 ohash@2.0.11: {}
8491
@@ -8562,9 +8568,9 @@ snapshots:
8568 dependencies:
8569 entities: 6.0.1
8570
8565 - parse5@8.0.0:
8571 + parse5@8.0.1:
8572 dependencies:
8567 - entities: 6.0.1
8573 + entities: 8.0.0
8574
8575 password-validator@5.3.0: {}
8576
@@ -8599,11 +8605,11 @@ snapshots:
8605
8606 pidtree@0.6.0: {}
8607
8602 - pinia-plugin-persistedstate@4.7.1(@nuxt/kit@3.21.2)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.33(typescript@5.9.3))):
8608 + pinia-plugin-persistedstate@4.7.1(@nuxt/kit@3.21.4)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.33(typescript@5.9.3))):
8609 dependencies:
8610 defu: 6.1.7
8611 optionalDependencies:
8606 - '@nuxt/kit': 3.21.2
8612 + '@nuxt/kit': 3.21.4
8613 pinia: 3.0.4(typescript@5.9.3)(vue@3.5.33(typescript@5.9.3))
8614
8615 pinia@3.0.4(typescript@5.9.3)(vue@3.5.33(typescript@5.9.3)):
@@ -8619,7 +8625,7 @@ snapshots:
8625 mlly: 1.8.2
8626 pathe: 2.0.3
8627
8622 - pkg-types@2.3.0:
8628 + pkg-types@2.3.1:
8629 dependencies:
8630 confbox: 0.2.4
8631 exsolve: 1.0.8
@@ -8647,15 +8653,15 @@ snapshots:
8653 cssesc: 3.0.0
8654 util-deprecate: 1.0.2
8655
8650 - postcss@8.5.10:
8656 + postcss@8.5.13:
8657 dependencies:
8652 - nanoid: 3.3.11
8658 + nanoid: 3.3.12
8659 picocolors: 1.1.1
8660 source-map-js: 1.2.1
8661
8662 prelude-ls@1.2.1: {}
8663
8658 - prettier-plugin-tailwindcss@0.7.3(prettier@3.8.3):
8664 + prettier-plugin-tailwindcss@0.7.4(prettier@3.8.3):
8665 dependencies:
8666 prettier: 3.8.3
8667
@@ -8759,6 +8765,13 @@ snapshots:
8765 destr: 2.0.5
8766 optional: true
8767
8768 + react-dom@19.2.5(react@19.2.5):
8769 + dependencies:
8770 + react: 19.2.5
8771 + scheduler: 0.27.0
8772 +
8773 + react@19.2.5: {}
8774 +
8775 read-package-json-fast@4.0.0:
8776 dependencies:
8777 json-parse-even-better-errors: 4.0.0
@@ -8900,6 +8913,8 @@ snapshots:
8913 dependencies:
8914 xmlchars: 2.2.0
8915
8916 + scheduler@0.27.0: {}
8917 +
8918 scslre@0.3.0:
8919 dependencies:
8920 '@eslint-community/regexpp': 4.12.2
@@ -9058,11 +9073,11 @@ snapshots:
9073
9074 tailwindcss@4.2.4: {}
9075
9061 - tapable@2.3.2: {}
9076 + tapable@2.3.3: {}
9077
9078 taze@19.11.0:
9079 dependencies:
9065 - '@antfu/ni': 30.0.0
9080 + '@antfu/ni': 30.1.0
9081 '@henrygd/queue': 1.2.0
9082 cac: 7.0.0
9083 find-up-simple: 1.0.1
@@ -9071,7 +9086,7 @@ snapshots:
9086 pathe: 2.0.3
9087 pnpm-workspace-yaml: 1.6.0
9088 restore-cursor: 5.1.0
9074 - tinyexec: 1.1.1
9089 + tinyexec: 1.1.2
9090 tinyglobby: 0.2.16
9091 unconfig: 7.5.0
9092 yaml: 2.8.3
@@ -9084,7 +9099,7 @@ snapshots:
9099
9100 tinybench@2.9.0: {}
9101
9087 - tinyexec@1.1.1: {}
9102 + tinyexec@1.1.2: {}
9103
9104 tinyglobby@0.2.16:
9105 dependencies:
@@ -9093,11 +9108,11 @@ snapshots:
9108
9109 tinyrainbow@3.1.0: {}
9110
9096 - tldts-core@7.0.28: {}
9111 + tldts-core@7.0.29: {}
9112
9098 - tldts@7.0.28:
9113 + tldts@7.0.29:
9114 dependencies:
9100 - tldts-core: 7.0.28
9115 + tldts-core: 7.0.29
9116
9117 tmp@0.2.5: {}
9118
@@ -9120,7 +9135,7 @@ snapshots:
9135
9136 tough-cookie@6.0.1:
9137 dependencies:
9123 - tldts: 7.0.28
9138 + tldts: 7.0.29
9139
9140 tr46@6.0.0:
9141 dependencies:
@@ -9159,7 +9174,7 @@ snapshots:
9174
9175 uc.micro@2.1.0: {}
9176
9162 - ufo@1.6.3: {}
9177 + ufo@1.6.4: {}
9178
9179 unconfig-core@7.5.0:
9180 dependencies:
@@ -9290,13 +9305,13 @@ snapshots:
9305 dependencies:
9306 birpc: 2.9.0
9307 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)
9293 - vite-hot-client: 2.1.0(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))
9308 + vite-hot-client: 2.2.0(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))
9309
9295 - vite-hot-client@2.1.0(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)):
9310 + vite-hot-client@2.2.0(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)):
9311 dependencies:
9312 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)
9313
9299 - vite-plugin-inspect@11.3.3(@nuxt/kit@3.21.2)(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)):
9314 + vite-plugin-inspect@11.3.3(@nuxt/kit@3.21.4)(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)):
9315 dependencies:
9316 ansis: 4.2.0
9317 debug: 4.4.3
@@ -9309,18 +9324,18 @@ snapshots:
9324 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)
9325 vite-dev-rpc: 1.1.0(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))
9326 optionalDependencies:
9312 - '@nuxt/kit': 3.21.2
9327 + '@nuxt/kit': 3.21.4
9328 transitivePeerDependencies:
9329 - supports-color
9330
9316 - vite-plugin-vue-devtools@8.1.1(@nuxt/kit@3.21.2)(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)):
9331 + vite-plugin-vue-devtools@8.1.1(@nuxt/kit@3.21.4)(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)):
9332 dependencies:
9333 '@vue/devtools-core': 8.1.1(vue@3.5.33(typescript@5.9.3))
9334 '@vue/devtools-kit': 8.1.1
9335 '@vue/devtools-shared': 8.1.1
9336 sirv: 3.0.2
9337 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)
9323 - vite-plugin-inspect: 11.3.3(@nuxt/kit@3.21.2)(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))
9338 + vite-plugin-inspect: 11.3.3(@nuxt/kit@3.21.4)(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))
9339 vite-plugin-vue-inspector: 5.4.0(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))
9340 transitivePeerDependencies:
9341 - '@nuxt/kit'
@@ -9335,7 +9350,7 @@ snapshots:
9350 '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0)
9351 '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
9352 '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0)
9338 - '@vue/compiler-dom': 3.5.32
9353 + '@vue/compiler-dom': 3.5.33
9354 kolorist: 1.8.0
9355 magic-string: 0.30.21
9356 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)
@@ -9355,7 +9370,7 @@ snapshots:
9370 esbuild: 0.27.7
9371 fdir: 6.5.0(picomatch@4.0.4)
9372 picomatch: 4.0.4
9358 - postcss: 8.5.10
9373 + postcss: 8.5.13
9374 rollup: 4.60.2
9375 tinyglobby: 0.2.16
9376 optionalDependencies:
@@ -9366,7 +9381,7 @@ snapshots:
9381 sass: 1.99.0
9382 yaml: 2.8.3
9383
9369 - vitest@4.1.5(@types/node@25.6.0)(jsdom@29.0.2)(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)):
9384 + 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)):
9385 dependencies:
9386 '@vitest/expect': 4.1.5
9387 '@vitest/mocker': 4.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))
@@ -9375,7 +9390,7 @@ snapshots:
9390 '@vitest/snapshot': 4.1.5
9391 '@vitest/spy': 4.1.5
9392 '@vitest/utils': 4.1.5
9378 - es-module-lexer: 2.0.0
9393 + es-module-lexer: 2.1.0
9394 expect-type: 1.3.0
9395 magic-string: 0.30.21
9396 obug: 2.1.1
@@ -9383,14 +9398,14 @@ snapshots:
9398 picomatch: 4.0.4
9399 std-env: 4.1.0
9400 tinybench: 2.9.0
9386 - tinyexec: 1.1.1
9401 + tinyexec: 1.1.2
9402 tinyglobby: 0.2.16
9403 tinyrainbow: 3.1.0
9404 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)
9405 why-is-node-running: 2.3.0
9406 optionalDependencies:
9407 '@types/node': 25.6.0
9393 - jsdom: 29.0.2
9408 + jsdom: 29.1.1
9409 transitivePeerDependencies:
9410 - msw
9411
@@ -9564,7 +9579,7 @@ snapshots:
9579
9580 with@7.0.2:
9581 dependencies:
9567 - '@babel/parser': 7.29.2
9582 + '@babel/parser': 7.29.3
9583 '@babel/types': 7.29.0
9584 assert-never: 1.4.0
9585 babel-walk: 3.0.0-canary-5
frontend/src/App.vue
-3
@@ -27,7 +27,6 @@
27 import type { Component } from "vue"
28 import type { RouteLocationNormalized } from "vue-router"
29 import type { Layout, RouterTransition, ThemeNameEnum } from "@/types/theme.d"
30 -import { defineCustomElements } from "@singulio/app-auth-search/loader"
30 import { computed, onBeforeMount, ref, watch } from "vue"
31 import { useRoute, useRouter } from "vue-router"
32 import Blank from "@/app-layouts/Blank"
@@ -45,8 +44,6 @@ const layoutComponents = {
44 Blank
45 }
46
48 -defineCustomElements()
49 -
47 const router = useRouter()
48 const route = useRoute()
49 const loading = ref(true)
frontend/src/api/endpoints/notifications.ts
+5 -15
@@ -1,3 +1,4 @@
1 +import type { FlaskBaseResponse } from "@/types/flask.d"
2 import type {
3 NotificationDispatchLogEntry,
4 NotificationRoute,
@@ -10,7 +11,6 @@ import type {
11 ShuffleOrg,
12 ShuffleVerifyResult
13 } from "@/types/notifications.d"
13 -import type { FlaskBaseResponse } from "@/types/flask.d"
14 import { HttpClient } from "../httpClient"
15
16 // Per-customer notification routing — wraps app/notifications/routes/notifications.py.
@@ -39,9 +39,7 @@ export default {
39 },
40
41 deleteRoute(customerCode: string, routeId: number) {
42 - return HttpClient.delete<FlaskBaseResponse>(
43 - `/customers/${customerCode}/notification_routes/${routeId}`
44 - )
42 + return HttpClient.delete<FlaskBaseResponse>(`/customers/${customerCode}/notification_routes/${routeId}`)
43 },
44
45 listDispatchLog(customerCode: string) {
@@ -65,11 +63,7 @@ export default {
63 )
64 },
65
68 - updateShuffleIntegration(
69 - customerCode: string,
70 - integrationId: number,
71 - payload: ShuffleIntegrationUpdatePayload
72 - ) {
66 + updateShuffleIntegration(customerCode: string, integrationId: number, payload: ShuffleIntegrationUpdatePayload) {
67 return HttpClient.patch<FlaskBaseResponse & { integration: ShuffleIntegration }>(
68 `/customers/${customerCode}/shuffle_integrations/${integrationId}`,
69 payload
@@ -77,9 +71,7 @@ export default {
71 },
72
73 deleteShuffleIntegration(customerCode: string, integrationId: number) {
80 - return HttpClient.delete<FlaskBaseResponse>(
81 - `/customers/${customerCode}/shuffle_integrations/${integrationId}`
82 - )
74 + return HttpClient.delete<FlaskBaseResponse>(`/customers/${customerCode}/shuffle_integrations/${integrationId}`)
75 },
76
77 listShuffleApps(customerCode: string, integrationId: number) {
@@ -98,8 +90,6 @@ export default {
90 // dropdown picker. Not customer-scoped; the admin Bearer (Shuffle
91 // connector) has access to every org we can attach.
92 listShuffleOrgs() {
101 - return HttpClient.get<FlaskBaseResponse & { orgs: ShuffleOrg[] }>(
102 - `/notifications/shuffle/orgs`
103 - )
93 + return HttpClient.get<FlaskBaseResponse & { orgs: ShuffleOrg[] }>(`/notifications/shuffle/orgs`)
94 }
95 }
frontend/src/app-layouts/common/Toolbar/Avatar.vue
+2 -2
@@ -87,8 +87,8 @@ const options = ref([
87 icon: renderIcon(ExternalServicesIcon)
88 },
89 {
90 - label: "Singul App Auth",
91 - key: "route-ExternalServices-SingulAppAuth",
90 + label: "Shuffle App Auth",
91 + key: "route-ExternalServices-ShuffleAppAuth",
92 icon: renderIcon(ExternalServicesIcon)
93 },
94 {
frontend/src/assets/scss/index.scss
+1
@@ -4,3 +4,4 @@
4 @include meta.load-css("common-animations");
5 @include meta.load-css("router-animations");
6 @include meta.load-css("overrides/naive-override");
7 +@include meta.load-css("overrides/singul-override");
frontend/src/assets/scss/overrides/naive-override.scss
+2 -35
@@ -137,44 +137,11 @@
137 }
138
139 .n-form-item {
140 - .n-form-item-feedback-wrapper:not(:empty) {
141 - transform: translateY(-7px);
142 - padding-left: 0px !important;
143 - padding-right: 0px !important;
144 - padding-top: 0 !important;
145 - z-index: 1;
146 -
140 + .n-form-item-feedback-wrapper {
141 .n-form-item-feedback {
148 - font-size: 13px;
149 - line-height: 1.2;
150 - padding: 3px 12px;
151 - border-bottom-left-radius: var(--border-radius);
152 - border-bottom-right-radius: var(--border-radius);
153 - background: color-mix(in srgb, currentColor 5%, var(--bg-default-color) 95%);
154 - border: 1px solid currentColor;
155 - border-top: 0;
156 - }
157 - }
158 -
159 - &.n-form-item--small-size {
160 - .n-form-item-feedback-wrapper:not(:empty) {
161 - transform: translateY(-6px);
162 -
163 - .n-form-item-feedback {
164 - font-size: 11px;
165 - padding: 3px 10px;
166 - }
142 + line-height: 1.1;
143 }
144 }
169 -
170 - &.n-form-item--small-large {
171 - .n-form-item-feedback-wrapper:not(:empty) {
172 - .n-form-item-feedback {
173 - padding: 3px 14px;
174 - }
175 - }
176 - }
177 -
145 .n-form-item-blank--error {
146 .n-upload-dragger {
147 border-color: var(--n-feedback-text-color-error);
frontend/src/assets/scss/overrides/singul-override.scss new
+60
@@ -0,0 +1,60 @@
1 +.singul-container {
2 + max-height: 100%;
3 + display: flex !important;
4 + flex-direction: column;
5 + max-height: 100%;
6 + overflow: hidden;
7 +}
8 +.singul-search-bar-container {
9 + --singul-input-border-radius: var(--border-radius);
10 + --singul-input-bg: var(--color-background-secondary);
11 + --singul-input-border: 1px solid var(--border-color);
12 + --singul-input-color: var(--fg-default-color);
13 + --singul-input-focus-border: var(--primary-color);
14 + --singul-input-focus-shadow: 0 0 5px 0px rgba(var(--primary-color-rgb) / 0.8);
15 + --singul-app-icon-border: 1px solid var(--border-color);
16 + --singul-item-border: var(--border-color);
17 + --singul-dropdown-border-radius: var(--border-radius);
18 + --singul-app-name-color: var(--fg-default-color);
19 + --singul-app-description-color: var(--fg-secondary-color);
20 + --singul-item-hover-bg: var(--bg-secondary-color);
21 +
22 + display: flex;
23 + flex-direction: column;
24 + max-height: 100%;
25 + flex-grow: 1;
26 + overflow: hidden;
27 +
28 + .singul-search-input {
29 + box-shadow: none;
30 + outline: none;
31 + }
32 +
33 + .singul-end-of-results {
34 + color: var(--fg-secondary-color) !important;
35 + font-size: 14px !important;
36 + }
37 +
38 + .singul-dropdown-item {
39 + border: 1px solid var(--border-color);
40 + border-radius: var(--border-radius);
41 + margin-bottom: 0.5rem;
42 + }
43 +
44 + .singul-search-bar-container {
45 + display: flex;
46 + flex-direction: column;
47 + gap: 1rem;
48 + }
49 +
50 + .singul-results-container {
51 + flex-grow: 1;
52 + mask-image: none;
53 +
54 + &.singul-results-grid {
55 + .singul-dropdown-item {
56 + margin-bottom: 0rem;
57 + }
58 + }
59 + }
60 +}
frontend/src/components/common/ShuffleMCPEmbed.vue new
+117
@@ -0,0 +1,117 @@
1 +<template>
2 + <div ref="container" class="w-full" />
3 +</template>
4 +
5 +<script setup lang="ts">
6 +// Vue wrapper around `<ShuffleMCP>` (a React component from
7 +// `@shuffleio/shuffle-mcps`). We don't pull in a Vue/React interop
8 +// library — for a single component the manual mount via
9 +// react-dom/client is cleaner and avoids the abstraction tax. Vite
10 +// already handles JSX through esbuild so no build config changes
11 +// were needed.
12 +//
13 +// Lifecycle: createRoot in onMounted, unmount in onBeforeUnmount,
14 +// re-render with `root.render(...)` on prop change so the embed
15 +// reacts to live updates (e.g. when the parent swaps the auth token
16 +// after picking a different Shuffle org).
17 +
18 +import type { Root } from "react-dom/client"
19 +// `@shuffleio/shuffle-mcps/dist/index.js` self-imports its CSS via a
20 +// hash-suffixed filename (`./singul-GZKBHJNI.css`). The package's own
21 +// `./singul.css` export entry points at `./dist/singul.css` which
22 +// doesn't exist on disk — that's a package bug. We don't need to
23 +// import the CSS here ourselves; importing `ShuffleMCP` pulls it in.
24 +import { ShuffleMCP } from "@shuffleio/shuffle-mcps"
25 +import { createElement } from "react"
26 +import { createRoot } from "react-dom/client"
27 +import { onBeforeUnmount, onMounted, ref, watch } from "vue"
28 +
29 +interface Props {
30 + authToken: string
31 + apiKey?: string
32 + apiBaseUrl?: string
33 + inline?: boolean
34 + layout?: "list" | "grid"
35 + gridColumns?: number
36 + placeholder?: string
37 + preventDefault?: boolean
38 + multiSelect?: boolean
39 + showCheckbox?: boolean
40 + initialFilterQuery?: string
41 + showSourceFilter?: boolean
42 +}
43 +
44 +const props = withDefaults(defineProps<Props>(), {
45 + apiKey: undefined,
46 + apiBaseUrl: undefined,
47 + inline: true,
48 + layout: "list",
49 + gridColumns: 3,
50 + placeholder: "Find an app…",
51 + preventDefault: false,
52 + multiSelect: false,
53 + showCheckbox: false,
54 + initialFilterQuery: undefined,
55 + showSourceFilter: true
56 +})
57 +
58 +const emit = defineEmits<{
59 + (e: "appSelected", payload: unknown): void
60 + (e: "selectionChange", payload: unknown): void
61 +}>()
62 +
63 +const container = ref<HTMLElement | null>(null)
64 +let root: Root | null = null
65 +const EMPTY_SELECTED_APPS: unknown[] = []
66 +
67 +function render() {
68 + if (!root) return
69 + // Drop undefined props so React doesn't override the package's
70 + // own defaults with our `undefined` (different semantics in TS vs
71 + // React's prop-default mechanism).
72 + const reactProps: Record<string, unknown> = {
73 + authToken: props.authToken,
74 + inline: props.inline,
75 + layout: props.layout,
76 + gridColumns: props.gridColumns,
77 + placeholder: props.placeholder,
78 + preventDefault: props.preventDefault,
79 + multiSelect: props.multiSelect,
80 + showCheckbox: props.showCheckbox,
81 + showSourceFilter: props.showSourceFilter,
82 + // The package currently defaults `selectedApps` to a fresh `[]`
83 + // inside the React component, then mirrors it into state from a
84 + // useEffect. Passing a stable array avoids that render loop.
85 + selectedApps: EMPTY_SELECTED_APPS,
86 + onAppSelected: (payload: unknown) => emit("appSelected", payload),
87 + onSelectionChange: (payload: unknown) => emit("selectionChange", payload)
88 + }
89 + if (props.apiKey) reactProps.apiKey = props.apiKey
90 + if (props.apiBaseUrl) reactProps.apiBaseUrl = props.apiBaseUrl
91 + if (props.initialFilterQuery) reactProps.initialFilterQuery = props.initialFilterQuery
92 +
93 + root.render(createElement(ShuffleMCP as never, reactProps))
94 +}
95 +
96 +onMounted(() => {
97 + if (!container.value) return
98 + root = createRoot(container.value)
99 + render()
100 +})
101 +
102 +// Re-render when any reactive prop changes. The shallow watch is fine
103 +// since all our props are primitives or strings — no nested object
104 +// identity churn to worry about.
105 +watch(
106 + () => ({ ...props }),
107 + () => render(),
108 + { deep: true }
109 +)
110 +
111 +onBeforeUnmount(() => {
112 + if (root) {
113 + root.unmount()
114 + root = null
115 + }
116 +})
117 +</script>
frontend/src/components/customers/CustomerForm.vue
+1
@@ -2,6 +2,7 @@
2 <n-spin :show="loading" class="customer-form">
3 <n-form ref="formRef" :label-width="80" :model="form" :rules>
4 <div class="flex flex-col gap-4">
5 + <h4>Edit Customer</h4>
6 <div class="flex flex-wrap gap-4">
7 <div v-for="(val, key) of fieldsMeta" :key class="grow">
8 <n-form-item :label="val.label" :path="key" class="grow">
frontend/src/components/customers/CustomerInfo.vue
+4 -4
@@ -1,6 +1,6 @@
1 <template>
2 - <div class="customer-info">
3 - <div v-if="editing" class="p-7 pt-4">
2 + <div class="customer-info flex flex-col gap-4">
3 + <div v-if="editing">
4 <CustomerForm :customer lock-code @submitted="submitted">
5 <template #additionalActions>
6 <n-button @click="editing = false">Close</n-button>
@@ -8,7 +8,7 @@
8 </CustomerForm>
9 </div>
10 <template v-else>
11 - <div class="flex items-center justify-between gap-4 px-7 pt-2">
11 + <div class="flex items-center justify-between gap-4">
12 <n-button size="small" :disabled="loadingDelete" @click="editing = true">
13 <template #icon>
14 <Icon :name="EditIcon" :size="14" />
@@ -23,7 +23,7 @@
23 </n-button>
24 </div>
25
26 - <div class="grid-auto-fit-200 grid gap-2 p-7 pt-4">
26 + <div class="grid-auto-fit-200 grid gap-2">
27 <CardKV v-for="(value, key) of customer" :key>
28 <template #key>
29 {{ key }}
frontend/src/components/customers/CustomerItem.vue
+121 -147
@@ -137,139 +137,145 @@
137 v-model:show="showDetails"
138 preset="card"
139 content-class="p-0!"
140 - :style="{ maxWidth: 'min(1100px, 90vw)', minHeight: 'min(600px, 90vh)', overflow: 'hidden' }"
140 + :style="{ maxWidth: 'min(1100px, 90vw)', minHeight: 'min(470px, 90vh)', overflow: 'hidden' }"
141 :title="customerInfo?.customer_name"
142 :bordered="false"
143 segmented
144 >
145 - <Transition :name="`slide-tabs-${selectedTabsGroup === 'customer' ? 'left' : 'right'}`">
146 - <n-tabs v-if="selectedTabsGroup === 'customer'" type="line" animated :tabs-padding="24" class="h-full">
147 - <n-tab-pane name="Info" tab="Info" display-directive="show:lazy">
148 - <CustomerInfo
149 - v-if="customerInfo"
150 - v-model:loading="loadingDelete"
151 - :customer="customerInfo"
152 - @delete="deletedItem()"
153 - @submitted="customerInfo = $event"
154 - />
155 - </n-tab-pane>
156 - <n-tab-pane name="Provision" tab="Provision" display-directive="show:lazy">
157 - <CustomerProvision
158 - :customer-meta
159 - :customer-code="customer.customer_code"
160 - :customer-name="customer.customer_name"
161 - @delete="customerMeta = null"
162 - @submitted="customerMeta = $event"
163 - />
164 - </n-tab-pane>
165 - <n-tab-pane
166 - name="3rd Party Integrations"
167 - tab="3rd Party Integrations"
168 - display-directive="show:lazy"
145 + <n-tabs type="line" animated :tabs-padding="24" class="h-full">
146 + <n-tab-pane name="Customer" tab="Customer" display-directive="show" class="pt-0!">
147 + <n-tabs
148 + type="line"
149 + animated
150 + :tabs-padding="24"
151 + class="[&_.n-tabs-nav]:bg-secondary z-20 h-full [&_.n-tabs-nav]:relative [&_.n-tabs-nav]:z-99"
152 + placement="left"
153 + :pane-style="{ minHeight: 'min(450px, 90vh)' }"
154 >
170 - <CustomerIntegrations
171 - :customer-code="customer.customer_code"
172 - :customer-name="customer.customer_name"
173 - />
174 - </n-tab-pane>
175 - <n-tab-pane name="Network Connectors" tab="Network Connectors" display-directive="show:lazy">
176 - <CustomerNetworkConnectors
177 - :customer-code="customer.customer_code"
178 - :customer-name="customer.customer_name"
179 - />
180 - </n-tab-pane>
181 - <n-tab-pane
182 - name="Notification Workflows"
183 - tab="Notification Workflows"
184 - display-directive="show:lazy"
185 - >
186 - <CustomerNotificationsWorkflows :customer-code="customer.customer_code" />
187 - </n-tab-pane>
188 - <n-tab-pane name="AI Triggers" tab="AI Triggers" display-directive="show:lazy">
189 - <CustomerAITriggers :customer-code="customer.customer_code" />
190 - </n-tab-pane>
191 - <n-tab-pane name="AI Notifications" tab="AI Notifications" display-directive="show:lazy">
192 - <CustomerAiNotifications :customer-code="customer.customer_code" />
193 - </n-tab-pane>
194 - <n-tab-pane name="Event Sources" tab="Event Sources" display-directive="show:lazy">
195 - <CustomerEventSources :customer-code="customer.customer_code" />
196 - </n-tab-pane>
197 -
198 - <template #suffix>
199 - <div
200 - v-if="customerPortainerStackId !== null"
201 - class="hover:text-primary cursor-pointer pr-8 text-sm"
202 - @click="selectedTabsGroup = 'wazuh_worker'"
155 + <n-tab-pane name="Info" tab="Info" display-directive="show:lazy" class="p-4!">
156 + <CustomerInfo
157 + v-if="customerInfo"
158 + v-model:loading="loadingDelete"
159 + :customer="customerInfo"
160 + @delete="deletedItem()"
161 + @submitted="customerInfo = $event"
162 + />
163 + </n-tab-pane>
164 + <n-tab-pane name="Provision" tab="Provision" display-directive="show:lazy" class="p-4!">
165 + <CustomerProvision
166 + :customer-meta
167 + :customer-code="customer.customer_code"
168 + :customer-name="customer.customer_name"
169 + @delete="customerMeta = null"
170 + @submitted="customerMeta = $event"
171 + />
172 + </n-tab-pane>
173 + <n-tab-pane
174 + name="3rd Party Integrations"
175 + tab="3rd Party Integrations"
176 + display-directive="show:lazy"
177 + class="p-4!"
178 >
204 - Wazuh Worker
205 - </div>
206 - <div
207 - class="hover:text-primary cursor-pointer pr-8 text-sm"
208 - @click="selectedTabsGroup = 'agents'"
179 + <CustomerIntegrations
180 + :customer-code="customer.customer_code"
181 + :customer-name="customer.customer_name"
182 + />
183 + </n-tab-pane>
184 + <n-tab-pane
185 + name="Network Connectors"
186 + tab="Network Connectors"
187 + display-directive="show:lazy"
188 + class="p-4!"
189 >
210 - Agents
211 - </div>
212 - </template>
213 - </n-tabs>
214 - <n-tabs v-else-if="selectedTabsGroup === 'agents'" type="line" animated :tabs-padding="24">
215 - <template #prefix>
216 - <div
217 - class="hover:text-primary relative top-1 cursor-pointer pl-6"
218 - @click="selectedTabsGroup = 'customer'"
190 + <CustomerNetworkConnectors
191 + :customer-code="customer.customer_code"
192 + :customer-name="customer.customer_name"
193 + />
194 + </n-tab-pane>
195 + <n-tab-pane
196 + name="Notification Workflows"
197 + tab="Notification Workflows"
198 + display-directive="show:lazy"
199 + class="p-4!"
200 >
220 - <Icon :name="ArrowIcon" :size="20" />
221 - </div>
222 - </template>
223 - <n-tab-pane name="Agents" tab="Agents" display-directive="show:lazy">
224 - <n-scrollbar style="max-height: 470px" trigger="none">
225 - <div class="p-6 pt-2">
201 + <CustomerNotificationsWorkflows :customer-code="customer.customer_code" />
202 + </n-tab-pane>
203 + <n-tab-pane name="AI Triggers" tab="AI Triggers" display-directive="show:lazy" class="p-4!">
204 + <CustomerAITriggers :customer-code="customer.customer_code" />
205 + </n-tab-pane>
206 + <n-tab-pane
207 + name="AI Notifications"
208 + tab="AI Notifications"
209 + display-directive="show:lazy"
210 + class="p-4!"
211 + >
212 + <CustomerAiNotifications :customer-code="customer.customer_code" />
213 + </n-tab-pane>
214 + <n-tab-pane name="Event Sources" tab="Event Sources" display-directive="show:lazy" class="p-4!">
215 + <CustomerEventSources :customer-code="customer.customer_code" />
216 + </n-tab-pane>
217 +
218 + <template #suffix><div class="h-4 w-full"></div></template>
219 + </n-tabs>
220 + </n-tab-pane>
221 +
222 + <n-tab-pane name="Agents" tab="Agents" display-directive="show" class="pt-0!">
223 + <n-tabs
224 + type="line"
225 + animated
226 + :tabs-padding="24"
227 + class="[&_.n-tabs-nav]:bg-secondary z-20 h-full [&_.n-tabs-nav]:relative [&_.n-tabs-nav]:z-99"
228 + placement="left"
229 + :pane-style="{ minHeight: 'min(450px, 90vh)' }"
230 + >
231 + <n-tab-pane name="Agents" tab="Agents" display-directive="show" class="p-4! pr-0!">
232 + <n-scrollbar style="max-height: 470px" trigger="none" class="pr-4">
233 <CustomerAgents v-if="customerInfo" :customer="customerInfo" />
227 - </div>
228 - </n-scrollbar>
229 - </n-tab-pane>
230 - <n-tab-pane name="Healthcheck Wazuh" tab="Healthcheck Wazuh" display-directive="show:lazy">
231 - <n-scrollbar style="max-height: 470px" trigger="none">
232 - <div class="p-6 pt-2">
234 + </n-scrollbar>
235 + </n-tab-pane>
236 + <n-tab-pane
237 + name="Healthcheck Wazuh"
238 + tab="Healthcheck Wazuh"
239 + display-directive="show:lazy"
240 + class="p-4! pr-0!"
241 + >
242 + <n-scrollbar style="max-height: 470px" trigger="none" class="pr-4">
243 <CustomerHealthcheckList
244 v-model:filters="healthcheckFilters"
245 source="wazuh"
246 :customer-code="customer.customer_code"
247 />
238 - </div>
239 - </n-scrollbar>
240 - </n-tab-pane>
241 - <n-tab-pane
242 - name="Healthcheck Velociraptor"
243 - tab="Healthcheck Velociraptor"
244 - display-directive="show:lazy"
245 - >
246 - <n-scrollbar style="max-height: 470px" trigger="none">
247 - <div class="p-6 pt-2">
248 + </n-scrollbar>
249 + </n-tab-pane>
250 + <n-tab-pane
251 + name="Healthcheck Velociraptor"
252 + tab="Healthcheck Velociraptor"
253 + display-directive="show:lazy"
254 + class="p-4! pr-0!"
255 + >
256 + <n-scrollbar style="max-height: 470px" trigger="none" class="pr-4">
257 <CustomerHealthcheckList
258 v-model:filters="healthcheckFilters"
259 source="velociraptor"
260 :customer-code="customer.customer_code"
261 />
253 - </div>
254 - </n-scrollbar>
255 - </n-tab-pane>
256 - </n-tabs>
257 - <n-tabs v-else-if="selectedTabsGroup === 'wazuh_worker'" type="line" animated :tabs-padding="24">
258 - <template #prefix>
259 - <div
260 - class="hover:text-primary relative top-1 cursor-pointer pl-6"
261 - @click="selectedTabsGroup = 'customer'"
262 - >
263 - <Icon :name="ArrowIcon" :size="20" />
264 - </div>
265 - </template>
266 - <n-tab-pane name="Wazuh Worker" tab="Wazuh Worker" display-directive="show:lazy">
267 - <div class="px-7 py-4">
268 - <CustomerWazuhWorker v-if="customerPortainerStackId" :stack-id="customerPortainerStackId" />
269 - </div>
270 - </n-tab-pane>
271 - </n-tabs>
272 - </Transition>
262 + </n-scrollbar>
263 + </n-tab-pane>
264 +
265 + <template #suffix><div class="h-4 w-full"></div></template>
266 + </n-tabs>
267 + </n-tab-pane>
268 +
269 + <n-tab-pane
270 + v-if="customerPortainerStackId !== null"
271 + name="Wazuh Worker"
272 + tab="Wazuh Worker"
273 + display-directive="show"
274 + class="p-4!"
275 + >
276 + <CustomerWazuhWorker :stack-id="customerPortainerStackId" />
277 + </n-tab-pane>
278 + </n-tabs>
279 </n-modal>
280 </div>
281 </template>
@@ -307,9 +313,7 @@ const CustomerNotificationsWorkflows = defineAsyncComponent(
313 () => import("./notifications/CustomerNotificationsWorkflows.vue")
314 )
315 const CustomerAITriggers = defineAsyncComponent(() => import("./aiTriggers/CustomerAITriggers.vue"))
310 -const CustomerAiNotifications = defineAsyncComponent(
311 - () => import("./aiNotifications/CustomerAiNotifications.vue")
312 -)
316 +const CustomerAiNotifications = defineAsyncComponent(() => import("./aiNotifications/CustomerAiNotifications.vue"))
317 const CustomerEventSources = defineAsyncComponent(() => import("./eventSources/CustomerEventSources.vue"))
318 const CustomerWazuhWorker = defineAsyncComponent(() => import("./CustomerWazuhWorker.vue"))
319
@@ -319,14 +323,12 @@ const DetailsIcon = "carbon:settings-adjust"
323 const UserTypeIcon = "solar:shield-user-linear"
324 const ParentIcon = "material-symbols-light:supervisor-account-outline-rounded"
325 const InfoIcon = "carbon:information"
322 -const ArrowIcon = "carbon:arrow-left"
326 const LocationIcon = "carbon:location"
327 const PhoneIcon = "carbon:phone"
328 const ProvisionIcon = "carbon:network-3"
329 const AgentsIcon = "carbon:devices"
330
331 const showDetails = ref(false)
329 -const selectedTabsGroup = ref<"customer" | "agents" | "wazuh_worker">("customer")
332 const loadingFull = ref(false)
333 const loadingDelete = ref(false)
334 const message = useMessage()
@@ -406,8 +408,6 @@ function deletedItem() {
408
409 watch(showDetails, val => {
410 if (val) {
409 - selectedTabsGroup.value = "customer"
410 -
411 if (
412 customer.value.customer_code &&
413 (!customer.value.customer_name || !customerMeta.value?.customer_meta_graylog_index)
@@ -433,29 +433,3 @@ onBeforeMount(() => {
433 }
434 })
435 </script>
436 -
437 -<style>
438 -.slide-tabs-right-enter-active,
439 -.slide-tabs-right-leave-active,
440 -.slide-tabs-left-enter-active,
441 -.slide-tabs-left-leave-active {
442 - transition: all 0.2s ease-out;
443 - position: absolute;
444 -}
445 -
446 -.slide-tabs-left-enter-from {
447 - transform: translateX(-100%);
448 -}
449 -
450 -.slide-tabs-left-leave-to {
451 - transform: translateX(100%);
452 -}
453 -
454 -.slide-tabs-right-enter-from {
455 - transform: translateX(100%);
456 -}
457 -
458 -.slide-tabs-right-leave-to {
459 - transform: translateX(-100%);
460 -}
461 -</style>
frontend/src/components/customers/aiNotifications/CustomerAiNotificationDispatchLog.vue
+4 -5
@@ -22,7 +22,7 @@
22
23 <n-data-table
24 v-else-if="entries.length"
25 - :columns="columns"
25 + :columns
26 :data="entries"
27 size="small"
28 :bordered="false"
@@ -34,8 +34,8 @@
34 </template>
35
36 <script setup lang="ts">
37 -import type { NotificationDispatchLogEntry as DispatchLogEntry } from "@/types/notifications.d"
37 import type { DataTableColumns } from "naive-ui"
38 +import type { NotificationDispatchLogEntry as DispatchLogEntry } from "@/types/notifications.d"
39 import { NButton, NDataTable, NEmpty, NSpin, useMessage } from "naive-ui"
40 import { computed, h, onBeforeMount, ref } from "vue"
41 import Api from "@/api"
@@ -66,7 +66,7 @@ const columns = computed<DataTableColumns<DispatchLogEntry>>(() => [
66 title: "When",
67 key: "dispatched_at",
68 width: 160,
69 - render: row => formatDate(row.dispatched_at, "MMM D, YYYY HH:mm:ss")
69 + render: row => String(formatDate(row.dispatched_at, "MMM D, YYYY HH:mm:ss"))
70 },
71 {
72 title: "Alert",
@@ -78,8 +78,7 @@ const columns = computed<DataTableColumns<DispatchLogEntry>>(() => [
78 title: "Trigger",
79 key: "trigger",
80 width: 220,
81 - render: row =>
82 - row.trigger === "investigation_complete" ? "Every investigation" : "Critical / High only"
81 + render: row => (row.trigger === "investigation_complete" ? "Every investigation" : "Critical / High only")
82 },
83 {
84 title: "Status",
frontend/src/components/customers/aiNotifications/CustomerAiNotificationRoutes/CustomerAiNotificationRouteForm.vue renamed
+110 -97
@@ -1,95 +1,81 @@
1 <template>
2 - <n-form
3 - ref="formRef"
4 - :model="form"
5 - :rules="rules"
6 - label-placement="top"
7 - class="px-7 py-4"
8 - >
9 - <div class="mb-3 flex items-center justify-between">
10 - <h3 class="text-lg font-medium">{{ editing ? "Edit route" : "Add route" }}</h3>
11 - <n-button size="small" quaternary @click="$emit('close')">
12 - <template #icon>
13 - <Icon :name="CloseIcon" :size="14" />
14 - </template>
15 - Cancel
16 - </n-button>
17 - </div>
18 -
2 + <n-form ref="formRef" :model="form" :rules label-placement="top">
3 <n-form-item label="Name" path="name">
20 - <n-input
21 - v-model:value="form.name"
22 - placeholder="e.g. SOC team Slack #alerts"
23 - :maxlength="128"
24 - show-count
25 - />
4 + <n-input v-model:value="form.name" placeholder="e.g. SOC team Slack #alerts" :maxlength="128" show-count />
5 </n-form-item>
6
28 - <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
29 - <n-form-item label="Trigger" path="trigger">
30 - <n-select v-model:value="form.trigger" :options="triggerOptions" />
31 - </n-form-item>
32 -
33 - <n-form-item label="Minimum severity" path="min_severity">
34 - <n-select v-model:value="form.min_severity" :options="severityOptions" />
35 - </n-form-item>
36 - </div>
37 -
38 - <n-form-item label="Channel" path="channel">
39 - <n-select v-model:value="form.channel" :options="channelOptions" @update:value="onChannelChange" />
40 - <template #feedback>
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">
42 - Email is direct SMTP via CoPilot's deployment config. Shuffle proxies to
43 - 3,000+ integrations through a customer's authenticated Shuffle org.
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>
16 </template>
17 </n-form-item>
18
19 + <n-form-item label="Channel" path="channel">
20 + <n-select
21 + v-model:value="form.channel"
22 + :options="channelOptions"
23 + to="body"
24 + @update:value="onChannelChange"
25 + />
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.
29 + </template>
30 + </n-form-item>
31 +
32 <!-- SMTP-specific: recipient emails -->
33 <n-form-item v-if="form.channel === 'smtp_email'" label="Recipient email(s)" path="destination">
50 - <n-input
51 - v-model:value="form.destination"
52 - placeholder="soc@example.com, ir@example.com"
53 - type="text"
54 - />
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">
60 - <div class="flex w-full flex-col gap-1">
61 - <n-select
62 - v-model:value="form.shuffle_integration_id"
63 - :options="integrationOptions"
64 - placeholder="Pick a Shuffle org for this customer"
65 - :loading="loadingIntegrations"
66 - @update:value="onIntegrationChange"
67 - />
68 - <div v-if="!integrationOptions.length && !loadingIntegrations" class="text-tertiary text-xs">
69 - No Shuffle integrations configured for this customer yet — go to the
70 - <strong>Shuffle integrations</strong> tab to add one first.
71 - </div>
72 - </div>
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">
76 - <div class="flex w-full flex-col gap-1">
77 - <n-select
78 - v-model:value="form.shuffle_app_id"
79 - :options="appOptions"
80 - placeholder="Pick an authenticated app"
81 - :loading="loadingApps"
82 - :disabled="!form.shuffle_integration_id || loadingApps"
83 - filterable
84 - @update:value="onAppChange"
85 - />
86 - <div
87 - v-if="form.shuffle_integration_id && !appOptions.length && !loadingApps && appsError"
88 - class="text-error text-xs"
89 - >
90 - Couldn't fetch apps from Shuffle: {{ appsError }}
91 - </div>
92 - </div>
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 + >
77 + Couldn't fetch apps from Shuffle: {{ appsError }}
78 + </template>
79 </n-form-item>
80
81 <n-form-item label="Destination hint" path="destination">
@@ -98,17 +84,16 @@
84 placeholder="e.g. #soc-alerts, soc@example.com, @user-id"
85 type="text"
86 />
101 - <template #feedback>
102 - <span class="text-tertiary text-xs">
103 - Free-form — gets prepended to the outgoing message as a
104 - <code>Send to &lt;destination&gt;: …</code> hint so the Shuffle app agent
105 - knows where to deliver. Channel name for Slack, email for Outlook, etc.
106 - </span>
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>
95
111 - <n-form-item label="Custom message template (optional)" path="format_template">
96 + <n-form-item label="Custom message template (optional)" path="format_template" :show-feedback="false">
97 <n-input
98 v-model:value="form.format_template"
99 type="textarea"
@@ -131,6 +116,7 @@
116 </template>
117
118 <script setup lang="ts">
119 +import type { FormInst, FormRules } from "naive-ui"
120 import type {
121 NotificationChannel,
122 NotificationRoute,
@@ -140,11 +126,9 @@ import type {
126 ShuffleApp,
127 ShuffleIntegration
128 } from "@/types/notifications.d"
143 -import type { FormInst, FormRules } from "naive-ui"
129 import { NButton, NCheckbox, NForm, NFormItem, NInput, NSelect, useMessage } from "naive-ui"
130 import { computed, onBeforeMount, reactive, ref } from "vue"
131 import Api from "@/api"
147 -import Icon from "@/components/common/Icon.vue"
132 import { getApiErrorMessage } from "@/utils"
133
134 const props = defineProps<{
@@ -157,13 +141,14 @@ const emit = defineEmits<{
141 (e: "close"): void
142 }>()
143
160 -const CloseIcon = "carbon:close"
161 -
144 const message = useMessage()
145 const formRef = ref<FormInst | null>(null)
146 const submitting = ref(false)
147
148 const editing = computed(() => props.editingRoute !== null)
149 +type FeedbackField = "channel" | "destination" | "min_severity" | "shuffle_app_id" | "shuffle_integration_id"
150 +
151 +const fieldErrors = reactive<Partial<Record<FeedbackField, string>>>({})
152
153 const form = reactive<NotificationRoutePayload>({
154 name: props.editingRoute?.name ?? "",
@@ -178,10 +163,10 @@ const form = reactive<NotificationRoutePayload>({
163 shuffle_app_name: props.editingRoute?.shuffle_app_name ?? null
164 })
165
181 -const triggerOptions = [
182 - { label: "Every investigation completes", value: "investigation_complete" },
183 - { label: "Critical / High severity only", value: "severity_critical_or_high" }
184 -]
166 +// Trigger is a single fixed value today (`investigation_complete`).
167 +// Hidden from the UI — set programmatically on the form. Will become
168 +// a select again when we add more dispatch event types (analyst-review
169 +// hooks, IOC-enrichment alerts, scheduled-sweep findings).
170
171 const channelOptions = [
172 { label: "Email (SMTP)", value: "smtp_email" },
@@ -221,6 +206,15 @@ const appOptions = computed(() =>
206 }))
207 )
208
209 +function clearFieldError(field: FeedbackField) {
210 + delete fieldErrors[field]
211 +}
212 +
213 +function createFieldError(field: FeedbackField, message: string) {
214 + fieldErrors[field] = message
215 + return new Error(message)
216 +}
217 +
218 async function loadIntegrations() {
219 loadingIntegrations.value = true
220 try {
@@ -266,6 +260,9 @@ function onChannelChange(value: NotificationChannel) {
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
268 async function onIntegrationChange(integrationId: number | null) {
@@ -287,26 +284,40 @@ function onAppChange(appId: string | null) {
284
285 const rules: FormRules = {
286 name: { required: true, message: "Name is required", trigger: ["input", "blur"] },
290 - trigger: { required: true, message: "Pick a trigger", trigger: ["change", "blur"] },
291 - channel: { required: true, message: "Pick a channel", trigger: ["change", "blur"] },
292 - min_severity: { required: true, message: "Pick a severity threshold", trigger: ["change", "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 + },
295 + min_severity: {
296 + validator: (_rule, value: NotificationSeverity | null) => {
297 + if (!value) return createFieldError("min_severity", "Pick a severity threshold")
298 + clearFieldError("min_severity")
299 + return true
300 + },
301 + trigger: ["change", "blur"]
302 + },
303 destination: {
304 required: true,
305 validator: (_rule, value: string) => {
306 if (!value || !value.trim()) {
307 return form.channel === "smtp_email"
298 - ? new Error("At least one recipient email required")
299 - : new Error("Destination hint is required")
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)
306 - if (!recipients.length) return new Error("At least one recipient email required")
316 + if (!recipients.length) return createFieldError("destination", "At least one recipient email required")
317 const bad = recipients.find(r => !r.includes("@"))
308 - if (bad) return new Error(`Invalid email: ${bad}`)
318 + if (bad) return createFieldError("destination", `Invalid email: ${bad}`)
319 }
320 + clearFieldError("destination")
321 return true
322 },
323 trigger: ["input", "blur"]
@@ -314,8 +325,9 @@ const rules: FormRules = {
325 shuffle_integration_id: {
326 validator: (_rule, value: number | null) => {
327 if (form.channel === "shuffle" && !value) {
317 - return new Error("Pick a Shuffle integration")
328 + return createFieldError("shuffle_integration_id", "Pick a Shuffle integration")
329 }
330 + clearFieldError("shuffle_integration_id")
331 return true
332 },
333 trigger: ["change", "blur"]
@@ -323,8 +335,9 @@ const rules: FormRules = {
335 shuffle_app_id: {
336 validator: (_rule, value: string | null) => {
337 if (form.channel === "shuffle" && !value) {
326 - return new Error("Pick a Shuffle app")
338 + return createFieldError("shuffle_app_id", "Pick a Shuffle app")
339 }
340 + clearFieldError("shuffle_app_id")
341 return true
342 },
343 trigger: ["change", "blur"]
frontend/src/components/customers/aiNotifications/CustomerAiNotificationRoutes/CustomerAiNotificationRouteItem.vue renamed
+95 -83
@@ -4,99 +4,109 @@
4 <div class="flex items-center gap-2">
5 <Icon :name="channelIcon" :size="16" />
6 <span class="font-medium">{{ route.name }}</span>
7 - <Badge v-if="!route.enabled" type="splitted" color="warning">
8 - <template #label>Status</template>
9 - <template #value>Disabled</template>
10 - </Badge>
7 </div>
8 </template>
9
10 <template #headerExtra>
15 - <div class="flex items-center gap-2">
16 - <n-tooltip>
17 - <template #trigger>
18 - <n-button size="tiny" quaternary circle @click="toggleEnabled">
19 - <template #icon>
20 - <Icon :name="route.enabled ? PauseIcon : PlayIcon" :size="14" />
21 - </template>
22 - </n-button>
23 - </template>
24 - {{ route.enabled ? "Disable" : "Enable" }}
25 - </n-tooltip>
26 -
27 - <n-tooltip>
28 - <template #trigger>
29 - <n-button size="tiny" quaternary circle @click="$emit('edit')">
30 - <template #icon>
31 - <Icon :name="EditIcon" :size="14" />
32 - </template>
33 - </n-button>
34 - </template>
35 - Edit
36 - </n-tooltip>
37 -
38 - <n-popconfirm @positive-click="confirmDelete">
39 - <template #trigger>
40 - <n-button size="tiny" quaternary circle>
41 - <template #icon>
42 - <Icon :name="DeleteIcon" :size="14" />
43 - </template>
44 - </n-button>
45 - </template>
46 - Delete this route? Dispatch log entries will be retained.
47 - </n-popconfirm>
48 - </div>
11 + <n-button
12 + size="small"
13 + secondary
14 + :type="route.enabled ? 'warning' : 'success'"
15 + :loading="loadingToggle"
16 + @click="toggleEnabled"
17 + >
18 + <template #icon>
19 + <Icon :name="route.enabled ? PauseIcon : PlayIcon" />
20 + </template>
21 + {{ route.enabled ? "Disable" : "Enable" }}
22 + </n-button>
23 </template>
24
25 <template #default>
52 - <div class="flex flex-col gap-2 text-sm">
26 + <div class="flex flex-col gap-3 text-sm">
27 + <div class="flex flex-col gap-0.5 text-sm">
28 + <div class="flex flex-wrap gap-1">
29 + <span class="font-medium">Destination:</span>
30 + <span class="flex flex-wrap gap-1">
31 + <code v-for="destination in destinationList" :key="destination">{{ destination }}</code>
32 + </span>
33 + </div>
34 + <div v-if="route.format_template" class="flex flex-wrap gap-1">
35 + <span class="font-medium">Custom template:</span>
36 + <span class="italic">configured</span>
37 + </div>
38 + </div>
39 <div class="flex flex-wrap items-center gap-2">
54 - <Badge type="splitted" bright>
55 - <template #label>Trigger</template>
56 - <template #value>{{ triggerLabel }}</template>
57 - </Badge>
58 - <Badge type="splitted" :color="severityColor">
40 + <Badge type="splitted" :color="severityColor" size="small">
41 <template #label>Min severity</template>
42 <template #value>{{ route.min_severity }}</template>
43 </Badge>
62 - <Badge type="splitted">
44 + <Badge type="splitted" size="small">
45 <template #label>Channel</template>
46 <template #value>{{ channelLabel }}</template>
47 </Badge>
48 </div>
67 - <div class="text-secondary">
68 - <span class="font-medium">Destination:</span>
69 - <code class="ml-1 break-all">{{ destinationDisplay }}</code>
70 - </div>
71 - <div v-if="route.format_template" class="text-secondary">
72 - <span class="font-medium">Custom template:</span>
73 - <span class="ml-1 italic">configured</span>
74 - </div>
49 </div>
50 </template>
51
78 - <template #footer>
79 - <div class="text-tertiary flex items-center gap-3 text-xs">
80 - <span>{{ route.dispatch_count }} dispatch(es)</span>
81 - <span>·</span>
82 - <span v-if="route.last_dispatched_at">
83 - last fired {{ formatDate(route.last_dispatched_at, "MMM D, YYYY HH:mm") }}
84 - </span>
85 - <span v-else>never fired</span>
86 - <span v-if="route.created_by">· created by {{ route.created_by }}</span>
52 + <template #footerMain>
53 + <div class="flex flex-wrap items-center gap-2">
54 + <Badge type="splitted">
55 + <template #label>dispatch</template>
56 + <template #value>{{ route.dispatch_count }}</template>
57 + </Badge>
58 +
59 + <Badge type="splitted">
60 + <template #label>fired</template>
61 + <template v-if="route.last_dispatched_at" #value>
62 + {{ formatDate(route.last_dispatched_at, dFormats.datetime) }}
63 + </template>
64 + <template v-else #value>never fired</template>
65 + </Badge>
66 +
67 + <Badge v-if="route.created_by" type="splitted">
68 + <template #label>owner</template>
69 + <template #value>{{ route.created_by }}</template>
70 + </Badge>
71 + </div>
72 + </template>
73 +
74 + <template #footerExtra>
75 + <div class="flex items-center justify-end gap-2">
76 + <n-button size="tiny" quaternary @click="$emit('edit')">
77 + <template #icon>
78 + <Icon :name="EditIcon" :size="14" />
79 + </template>
80 + Edit
81 + </n-button>
82 +
83 + <n-popconfirm to="body" @positive-click="confirmDelete">
84 + <template #trigger>
85 + <n-button size="tiny" quaternary :loading="loadingDelete">
86 + <template #icon>
87 + <Icon :name="DeleteIcon" :size="14" />
88 + </template>
89 + Delete
90 + </n-button>
91 + </template>
92 + Delete this route? Dispatch log entries will be retained.
93 + </n-popconfirm>
94 </div>
95 </template>
96 </CardEntity>
97 </template>
98
99 <script setup lang="ts">
100 +import type { ApiError } from "@/types/common"
101 import type { NotificationRoute } from "@/types/notifications.d"
94 -import { NButton, NPopconfirm, NTooltip, useMessage } from "naive-ui"
95 -import { computed } from "vue"
102 +import _split from "lodash/split"
103 +import { NButton, NPopconfirm, useMessage } from "naive-ui"
104 +import { computed, ref } from "vue"
105 import Api from "@/api"
106 import Badge from "@/components/common/Badge.vue"
107 import CardEntity from "@/components/common/cards/CardEntity.vue"
108 import Icon from "@/components/common/Icon.vue"
109 +import { useSettingsStore } from "@/stores/settings"
110 import { getApiErrorMessage } from "@/utils"
111 import { formatDate } from "@/utils/format"
112
@@ -115,7 +125,10 @@ const DeleteIcon = "carbon:trash-can"
125 const PauseIcon = "carbon:pause"
126 const PlayIcon = "carbon:play"
127
128 +const loadingToggle = ref(false)
129 +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
@@ -124,21 +137,14 @@ 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") {
129 - return props.route.shuffle_app_name
130 - ? `Shuffle · ${props.route.shuffle_app_name}`
131 - : "Shuffle"
143 + return props.route.shuffle_app_name ? `Shuffle · ${props.route.shuffle_app_name}` : "Shuffle"
144 }
145 return "SMTP email"
146 })
147
136 -const triggerLabel = computed(() =>
137 - props.route.trigger === "investigation_complete"
138 - ? "Every investigation"
139 - : "Critical / High only"
140 -)
141 -
148 const severityColor = computed<"danger" | "warning" | "success">(() => {
149 if (props.route.min_severity === "Critical" || props.route.min_severity === "High") return "danger"
150 if (props.route.min_severity === "Medium") return "warning"
@@ -148,27 +154,31 @@ const severityColor = computed<"danger" | "warning" | "success">(() => {
154 // SMTP recipients shown verbatim — email addresses aren't secrets the
155 // way Slack webhook URLs were. Phase 2 may reintroduce per-channel
156 // formatting logic for shuffle integrations.
151 -const destinationDisplay = computed(() => props.route.destination)
157 +const destinationList = computed(() => _split(props.route.destination, ","))
158
159 async function toggleEnabled() {
160 + loadingToggle.value = true
161 +
162 try {
155 - const res = await Api.notifications.updateRoute(
156 - props.route.customer_code,
157 - props.route.id,
158 - { enabled: !props.route.enabled }
159 - )
163 + const res = await Api.notifications.updateRoute(props.route.customer_code, props.route.id, {
164 + enabled: !props.route.enabled
165 + })
166 if (res.data.success) {
167 message.success(`Route ${res.data.route.enabled ? "enabled" : "disabled"}`)
168 emit("toggled")
169 } else {
170 message.warning(res.data.message || "Failed to toggle route")
171 }
166 - } catch (err: unknown) {
167 - message.error(getApiErrorMessage(err as never) || "Failed to toggle route")
172 + } catch (err) {
173 + message.error(getApiErrorMessage(err as ApiError) || "Failed to toggle route")
174 + } finally {
175 + loadingToggle.value = false
176 }
177 }
178
179 async function confirmDelete() {
180 + loadingDelete.value = true
181 +
182 try {
183 const res = await Api.notifications.deleteRoute(props.route.customer_code, props.route.id)
184 if (res.data.success) {
@@ -177,8 +187,10 @@ async function confirmDelete() {
187 } else {
188 message.warning(res.data.message || "Failed to delete route")
189 }
180 - } catch (err: unknown) {
181 - message.error(getApiErrorMessage(err as never) || "Failed to delete route")
190 + } catch (err) {
191 + message.error(getApiErrorMessage(err as ApiError) || "Failed to delete route")
192 + } finally {
193 + loadingDelete.value = false
194 }
195 }
196 </script>
frontend/src/components/customers/aiNotifications/CustomerAiNotificationRoutes/CustomerAiNotificationRoutes.vue renamed
+19 -14
@@ -1,7 +1,8 @@
1 <template>
2 - <div class="customer-ai-notification-routes">
3 - <transition name="form-fade" mode="out-in">
4 - <div v-if="showForm">
2 + <div class="customer-ai-notification-routes flex flex-col gap-4">
3 + <transition name="fade" mode="out-in">
4 + <div v-if="showForm" class="flex flex-col gap-4">
5 + <h4>{{ editingRoute ? `Edit ${editingRoute.name}` : "Create a notification route" }}</h4>
6 <CustomerAiNotificationRouteForm
7 :customer-code
8 :editing-route
@@ -9,29 +10,23 @@
10 @close="closeForm()"
11 />
12 </div>
12 - <div v-else>
13 - <div class="flex items-center justify-between gap-4 px-7 pt-2">
13 + <div v-else class="flex flex-col gap-4">
14 + <div class="flex items-center justify-between gap-4">
15 <n-button size="small" type="primary" @click="openForm()">
16 <template #icon>
17 <Icon :name="AddIcon" :size="14" />
18 </template>
19 Add route
20 </n-button>
20 - <n-button size="small" :disabled="loading" @click="refreshList()">
21 - <template #icon>
22 - <Icon :name="RefreshIcon" :size="14" />
23 - </template>
24 - Refresh
25 - </n-button>
21 </div>
22
23 <n-spin :show="loading">
29 - <div class="min-h-52 p-7 pt-4">
24 + <div class="min-h-52">
25 <template v-if="list.length">
26 <CustomerAiNotificationRouteItem
27 v-for="route of list"
28 :key="route.id"
34 - :route="route"
29 + :route
30 class="item-appear item-appear-bottom item-appear-005 mb-2"
31 @edit="openEdit(route)"
32 @deleted="refreshList()"
@@ -67,7 +62,6 @@ const { customerCode } = defineProps<{
62 }>()
63
64 const AddIcon = "carbon:add-alt"
70 -const RefreshIcon = "carbon:renew"
65
66 const message = useMessage()
67 const showForm = ref(false)
@@ -116,3 +110,14 @@ function onFormSubmitted() {
110
111 onBeforeMount(refreshList)
112 </script>
113 +
114 +<style lang="scss" scoped>
115 +.fade-enter-active,
116 +.fade-leave-active {
117 + transition: opacity 0.2s ease-in-out;
118 +}
119 +.fade-enter-from,
120 +.fade-leave-to {
121 + opacity: 0;
122 +}
123 +</style>
frontend/src/components/customers/aiNotifications/CustomerAiNotifications.vue
+14 -9
@@ -1,14 +1,19 @@
1 <template>
2 <div class="customer-ai-notifications">
3 - <n-tabs type="line" animated :tabs-padding="24">
4 - <n-tab-pane name="routes" tab="Routes" display-directive="show:lazy">
5 - <CustomerAiNotificationRoutes :customer-code="customerCode" />
3 + <n-tabs type="segment" animated tab-class="px-4! py-1!" class="[&_.n-tabs-nav]:mx-auto [&_.n-tabs-nav]:mb-6">
4 + <n-tab-pane name="routes" tab="Routes" display-directive="show:lazy" class="p-0!">
5 + <CustomerAiNotificationRoutes :customer-code />
6 </n-tab-pane>
7 - <n-tab-pane name="shuffle_integrations" tab="Shuffle integrations" display-directive="show:lazy">
8 - <CustomerShuffleIntegrations :customer-code="customerCode" />
7 + <n-tab-pane
8 + name="shuffle_integrations"
9 + tab="Shuffle integrations"
10 + display-directive="show:lazy"
11 + class="p-0!"
12 + >
13 + <CustomerShuffleIntegrations :customer-code />
14 </n-tab-pane>
10 - <n-tab-pane name="dispatch_log" tab="Dispatch log" display-directive="show:lazy">
11 - <CustomerAiNotificationDispatchLog :customer-code="customerCode" />
15 + <n-tab-pane name="dispatch_log" tab="Dispatch log" display-directive="show:lazy" class="pt-4!">
16 + <CustomerAiNotificationDispatchLog :customer-code />
17 </n-tab-pane>
18 </n-tabs>
19 </div>
@@ -23,8 +28,8 @@
28 // - Dispatch log: read-only audit trail of every dispatch attempt
29 import { NTabPane, NTabs } from "naive-ui"
30 import CustomerAiNotificationDispatchLog from "./CustomerAiNotificationDispatchLog.vue"
26 -import CustomerAiNotificationRoutes from "./CustomerAiNotificationRoutes.vue"
27 -import CustomerShuffleIntegrations from "./CustomerShuffleIntegrations.vue"
31 +import CustomerAiNotificationRoutes from "./CustomerAiNotificationRoutes/CustomerAiNotificationRoutes.vue"
32 +import CustomerShuffleIntegrations from "./CustomerShuffleIntegrations/CustomerShuffleIntegrations.vue"
33
34 defineProps<{
35 customerCode: string
frontend/src/components/customers/aiNotifications/CustomerShuffleIntegrations/CustomerShuffleAppsDrawer.vue new
+128
@@ -0,0 +1,128 @@
1 +<template>
2 + <n-drawer v-model:show="showLocal" :width="720" placement="right">
3 + <n-drawer-content closable>
4 + <template #header>
5 + <div class="flex items-center gap-2">
6 + <Icon name="carbon:integration" :size="18" />
7 + <span>Shuffle apps</span>
8 + <span v-if="integration" class="text-secondary">•</span>
9 + <span v-if="integration" class="text-secondary">{{ integration.display_name }}</span>
10 + </div>
11 + </template>
12 +
13 + <div class="flex flex-col gap-3">
14 + <div class="text-secondary text-sm">
15 + Browse Shuffle's catalog of 3,000+ integrations and authenticate the ones this org needs. Apps you
16 + authenticate here become available in the route form's app picker — Shuffle handles the OAuth flow;
17 + CoPilot just stores the resulting Org-Id and references it in routes.
18 + </div>
19 +
20 + <div v-if="loadingToken" class="text-tertiary py-6 text-center text-sm">
21 + Fetching org auth token from Shuffle…
22 + </div>
23 +
24 + <div v-else-if="error" class="text-error py-6 text-center text-sm">
25 + {{ error }}
26 + </div>
27 +
28 + <ShuffleMCPEmbed
29 + v-else-if="orgAuthToken"
30 + :auth-token="orgAuthToken"
31 + placeholder="Find a Shuffle app to authenticate…"
32 + inline
33 + layout="grid"
34 + :grid-columns="3"
35 + @app-selected="onAppSelected"
36 + />
37 + </div>
38 + </n-drawer-content>
39 + </n-drawer>
40 +</template>
41 +
42 +<script setup lang="ts">
43 +import type { ApiError } from "@/types/common"
44 +import type { ShuffleIntegration } from "@/types/notifications.d"
45 +import { NDrawer, NDrawerContent, useMessage } from "naive-ui"
46 +import { computed, ref, watch } from "vue"
47 +import Api from "@/api"
48 +import Icon from "@/components/common/Icon.vue"
49 +import ShuffleMCPEmbed from "@/components/common/ShuffleMCPEmbed.vue"
50 +import { getApiErrorMessage } from "@/utils"
51 +
52 +// Per-integration "Manage apps" drawer. Opened from the Shuffle
53 +// integration list when an admin wants to authenticate a new Shuffle
54 +// app for that customer's org. The actual auth UI is the embedded
55 +// ShuffleMCP picker; this drawer is responsible for:
56 +// 1. Fetching the org auth token via Shuffle's existing connector
57 +// endpoint (already exposed on CoPilot's backend)
58 +// 2. Passing the token into the embed
59 +// 3. Surfacing a confirmation message when the admin picks an app
60 +
61 +const props = defineProps<{
62 + show: boolean
63 + integration: ShuffleIntegration | null
64 +}>()
65 +
66 +const emit = defineEmits<{
67 + (e: "update:show", value: boolean): void
68 +}>()
69 +
70 +// v-model:show bridge — lets the drawer's built-in close button
71 +// propagate state back to the parent.
72 +const showLocal = computed({
73 + get: () => props.show,
74 + set: (v: boolean) => emit("update:show", v)
75 +})
76 +
77 +const message = useMessage()
78 +const orgAuthToken = ref<string | null>(null)
79 +const loadingToken = ref(false)
80 +const error = ref<string | null>(null)
81 +
82 +async function loadOrgToken(orgId: string) {
83 + loadingToken.value = true
84 + error.value = null
85 +
86 + try {
87 + const res = await Api.shuffle.getOrganization(orgId)
88 + if (res.data.success) {
89 + orgAuthToken.value = res.data.data?.org_auth?.token ?? null
90 + if (!orgAuthToken.value) {
91 + error.value =
92 + "Shuffle returned an org without an org_auth.token. Check the Shuffle connector config or contact Shuffle support."
93 + }
94 + } else {
95 + error.value = res.data.message || "Failed to fetch org auth token"
96 + }
97 + } catch (err) {
98 + error.value = getApiErrorMessage(err as ApiError) || "Failed to fetch org auth token"
99 + } finally {
100 + loadingToken.value = false
101 + }
102 +}
103 +
104 +function onAppSelected(payload: unknown) {
105 + // Shuffle handles the actual OAuth flow inside the picker — by the
106 + // time we get this callback, the user has either started or
107 + // completed authentication. Surface a confirmation; the new app
108 + // will appear in the route form's app picker on next refresh.
109 + const name = (payload as { app?: { name?: string } } | undefined)?.app?.name ?? "the selected app"
110 + message.success(`${name} authentication initiated. Refresh the route form to use it.`)
111 +}
112 +
113 +// Re-fetch the org token whenever the drawer opens or switches to a
114 +// different integration. Cleared when closing so reopening on a
115 +// different integration doesn't briefly flash stale data.
116 +watch(
117 + () => [props.show, props.integration?.id] as const,
118 + ([show, id]) => {
119 + if (show && id && props.integration?.shuffle_org_id) {
120 + loadOrgToken(props.integration.shuffle_org_id)
121 + } else if (!show) {
122 + orgAuthToken.value = null
123 + error.value = null
124 + }
125 + },
126 + { immediate: true }
127 +)
128 +</script>
frontend/src/components/customers/aiNotifications/CustomerShuffleIntegrations/CustomerShuffleIntegrationForm.vue renamed
+44 -72
@@ -1,23 +1,5 @@
1 <template>
2 - <n-form
3 - ref="formRef"
4 - :model="form"
5 - :rules="rules"
6 - label-placement="top"
7 - class="px-7 py-4"
8 - >
9 - <div class="mb-3 flex items-center justify-between">
10 - <h3 class="text-lg font-medium">
11 - {{ editing ? "Edit Shuffle integration" : "Add Shuffle integration" }}
12 - </h3>
13 - <n-button size="small" quaternary @click="$emit('close')">
14 - <template #icon>
15 - <Icon :name="CloseIcon" :size="14" />
16 - </template>
17 - Cancel
18 - </n-button>
19 - </div>
20 -
2 + <n-form ref="formRef" :model="form" :rules label-placement="top">
3 <n-form-item label="Display name" path="display_name">
4 <n-input
5 v-model:value="form.display_name"
@@ -45,34 +27,22 @@
27 @update:value="onOrgPicked"
28 />
29
48 - <div class="flex items-center gap-3 text-xs">
49 - <n-button size="tiny" quaternary :disabled="loadingOrgs" @click="loadOrgs(true)">
50 - <template #icon>
51 - <Icon :name="RefreshIcon" :size="12" />
52 - </template>
53 - Refresh list
54 - </n-button>
55 - <n-checkbox v-model:checked="manualEntry" size="small">
56 - Don't see your org? Enter the ID manually
57 - </n-checkbox>
58 - </div>
59 -
60 - <n-input
61 - v-if="manualEntry"
62 - v-model:value="form.shuffle_org_id"
63 - placeholder="6b6f65a4-d8f8-48ef-b02f-23a4a5f73e4a"
64 - :maxlength="64"
65 - />
66 -
67 - <div v-if="orgsError" class="text-error text-xs">
68 - Couldn't fetch orgs from Shuffle: {{ orgsError }}. Use manual entry above.
69 - </div>
30 + <n-checkbox v-model:checked="manualEntry" size="small">
31 + Don't see your org? Enter the ID manually
32 + </n-checkbox>
33 +
34 + <n-collapse-transition :show="manualEntry">
35 + <n-input
36 + v-model:value="form.shuffle_org_id"
37 + placeholder="6b6f65a4-d8f8-48ef-b02f-23a4a5f73e4a"
38 + :maxlength="64"
39 + />
40 + </n-collapse-transition>
41 </div>
71 - <template #feedback>
72 - <span class="text-tertiary text-xs">
73 - Sent as the <code>Org-Id</code> header on every dispatch — scopes the
74 - Shuffle call to the right org's authenticated apps.
75 - </span>
42 + <template v-if="!fieldErrors.shuffle_org_id" #feedback>
43 + Sent as the
44 + <code>Org-Id</code>
45 + header on every dispatch — scopes the Shuffle call to the right org's authenticated apps.
46 </template>
47 </n-form-item>
48
@@ -90,12 +60,12 @@
60 </template>
61
62 <script setup lang="ts">
93 -import type { ShuffleIntegration, ShuffleIntegrationPayload, ShuffleOrg } from "@/types/notifications.d"
63 import type { FormInst, FormRules } from "naive-ui"
95 -import { NButton, NCheckbox, NForm, NFormItem, NInput, NSelect, useMessage } from "naive-ui"
64 +import type { ApiError } from "@/types/common"
65 +import type { ShuffleIntegration, ShuffleIntegrationPayload, ShuffleOrg } from "@/types/notifications.d"
66 +import { NButton, NCheckbox, NCollapseTransition, NForm, NFormItem, NInput, NSelect, useMessage } from "naive-ui"
67 import { computed, onBeforeMount, reactive, ref } from "vue"
68 import Api from "@/api"
98 -import Icon from "@/components/common/Icon.vue"
69 import { getApiErrorMessage } from "@/utils"
70
71 const props = defineProps<{
@@ -108,14 +78,14 @@ const emit = defineEmits<{
78 (e: "close"): void
79 }>()
80
111 -const CloseIcon = "carbon:close"
112 -const RefreshIcon = "carbon:renew"
113 -
81 const message = useMessage()
82 const formRef = ref<FormInst | null>(null)
83 const submitting = ref(false)
84
85 const editing = computed(() => props.editingIntegration !== null)
86 +type FeedbackField = "shuffle_org_id"
87 +
88 +const fieldErrors = reactive<Partial<Record<FeedbackField, string>>>({})
89
90 const form = reactive<ShuffleIntegrationPayload>({
91 display_name: props.editingIntegration?.display_name ?? "",
@@ -128,7 +98,6 @@ const form = reactive<ShuffleIntegrationPayload>({
98 // fails or the desired org doesn't appear in the list.
99 const orgs = ref<ShuffleOrg[]>([])
100 const loadingOrgs = ref(false)
131 -const orgsError = ref<string | null>(null)
101 const manualEntry = ref(false)
102
103 const orgOptions = computed(() =>
@@ -146,36 +115,39 @@ const orgOptions = computed(() =>
115 })
116 )
117
149 -async function loadOrgs(force = false) {
118 +function clearFieldError(field: FeedbackField) {
119 + delete fieldErrors[field]
120 +}
121 +
122 +function createFieldError(field: FeedbackField, message: string) {
123 + fieldErrors[field] = message
124 + return new Error(message)
125 +}
126 +
127 +async function loadOrgs() {
128 if (loadingOrgs.value) return
129 +
130 loadingOrgs.value = true
152 - orgsError.value = null
131 +
132 try {
133 const res = await Api.notifications.listShuffleOrgs()
134 if (res.data.success) {
135 orgs.value = res.data.orgs
136 // If we're editing and the existing org_id isn't in the list,
137 // fall through to manual entry so the form stays usable.
159 - if (
160 - editing.value &&
161 - form.shuffle_org_id &&
162 - !orgs.value.some(o => o.id === form.shuffle_org_id)
163 - ) {
138 + if (editing.value && form.shuffle_org_id && !orgs.value.some(o => o.id === form.shuffle_org_id)) {
139 manualEntry.value = true
140 }
141 } else {
167 - orgsError.value = res.data.message || "Unknown error"
142 + message.warning(res.data.message || "Failed to load Shuffle orgs")
143 manualEntry.value = true
144 }
170 - } catch (err: unknown) {
171 - orgsError.value = getApiErrorMessage(err as never) || "Network error"
145 + } catch (err) {
146 + message.error(getApiErrorMessage(err as ApiError) || "Failed to load Shuffle orgs")
147 manualEntry.value = true
148 } finally {
149 loadingOrgs.value = false
150 }
176 - if (force) {
177 - message.success(`${orgs.value.length} org(s) loaded`)
178 - }
151 }
152
153 function onOrgPicked(_orgId: string | null) {
@@ -187,7 +159,11 @@ const rules: FormRules = {
159 display_name: { required: true, message: "Name is required", trigger: ["input", "blur"] },
160 shuffle_org_id: {
161 required: true,
190 - message: "Pick a Shuffle org or enter an Org-Id manually",
162 + validator: (_rule, value: string | null) => {
163 + if (!value) return createFieldError("shuffle_org_id", "Pick a Shuffle org or enter an Org-Id manually")
164 + clearFieldError("shuffle_org_id")
165 + return true
166 + },
167 trigger: ["input", "change", "blur"]
168 }
169 }
@@ -202,11 +178,7 @@ async function submit() {
178 submitting.value = true
179 try {
180 const res = props.editingIntegration
205 - ? await Api.notifications.updateShuffleIntegration(
206 - props.customerCode,
207 - props.editingIntegration.id,
208 - form
209 - )
181 + ? await Api.notifications.updateShuffleIntegration(props.customerCode, props.editingIntegration.id, form)
182 : await Api.notifications.createShuffleIntegration(props.customerCode, form)
183
184 if (res.data.success) {
frontend/src/components/customers/aiNotifications/CustomerShuffleIntegrations/CustomerShuffleIntegrationItem.vue renamed
+70 -52
@@ -4,23 +4,54 @@
4 <div class="flex items-center gap-2">
5 <Icon name="carbon:integration" :size="16" />
6 <span class="font-medium">{{ integration.display_name }}</span>
7 - <Badge v-if="!integration.enabled" type="splitted" color="warning">
8 - <template #label>Status</template>
9 - <template #value>Disabled</template>
10 - </Badge>
11 - <Badge v-if="verifyResult === 'ok'" type="splitted" color="success">
12 - <template #label>Probe</template>
13 - <template #value>{{ verifyAppCount }} app(s)</template>
14 - </Badge>
15 - <Badge v-else-if="verifyResult === 'fail'" type="splitted" color="danger">
16 - <template #label>Probe</template>
17 - <template #value>failed</template>
18 - </Badge>
7 </div>
8 </template>
9
10 <template #headerExtra>
23 - <div class="flex items-center gap-2">
11 + <n-button size="small" secondary :type="integration.enabled ? 'warning' : 'success'" @click="toggleEnabled">
12 + <template #icon>
13 + <Icon :name="integration.enabled ? PauseIcon : PlayIcon" :size="14" />
14 + </template>
15 + {{ integration.enabled ? "Disable" : "Enable" }}
16 + </n-button>
17 + </template>
18 +
19 + <template #default>
20 + <div class="flex flex-col gap-2">
21 + <div class="flex flex-col gap-1 text-sm">
22 + <div class="flex flex-wrap items-center gap-1">
23 + <span class="font-medium">Org-Id:</span>
24 + <code class="break-all">{{ integration.shuffle_org_id }}</code>
25 + </div>
26 + </div>
27 + <div class="flex flex-wrap gap-1">
28 + <Badge v-if="verifyResult === 'ok'" type="splitted" color="success" size="small">
29 + <template #label>Probe</template>
30 + <template #value>{{ verifyAppCount }} app(s)</template>
31 + </Badge>
32 + <Badge v-else-if="verifyResult === 'fail'" type="splitted" color="danger" size="small">
33 + <template #label>Probe</template>
34 + <template #value>failed</template>
35 + </Badge>
36 +
37 + <Badge type="splitted" size="small">
38 + <template #label>Last used</template>
39 + <template v-if="integration.last_used_at" #value>
40 + {{ formatDate(integration.last_used_at, dFormats.datetime) }}
41 + </template>
42 + <template v-else #value>never used</template>
43 + </Badge>
44 +
45 + <Badge type="splitted" size="small">
46 + <template #label>Owner</template>
47 + <template #value>{{ integration.created_by }}</template>
48 + </Badge>
49 + </div>
50 + </div>
51 + </template>
52 +
53 + <template #footer>
54 + <div class="flex items-center justify-end gap-2">
55 <n-tooltip>
56 <template #trigger>
57 <n-button
@@ -41,13 +72,13 @@
72
73 <n-tooltip>
74 <template #trigger>
44 - <n-button size="tiny" quaternary circle @click="toggleEnabled">
75 + <n-button size="tiny" quaternary circle @click="$emit('manageApps')">
76 <template #icon>
46 - <Icon :name="integration.enabled ? PauseIcon : PlayIcon" :size="14" />
77 + <Icon :name="ManageAppsIcon" :size="14" />
78 </template>
79 </n-button>
80 </template>
50 - {{ integration.enabled ? "Disable" : "Enable" }}
81 + Manage apps
82 </n-tooltip>
83
84 <n-tooltip>
@@ -61,47 +92,31 @@
92 Edit
93 </n-tooltip>
94
64 - <n-popconfirm @positive-click="confirmDelete">
95 + <n-tooltip>
96 <template #trigger>
66 - <n-button size="tiny" quaternary circle>
97 + <n-popconfirm to="body" @positive-click="confirmDelete">
98 + <template #trigger>
99 + <n-button size="tiny" quaternary circle>
100 + <template #icon>
101 + <Icon :name="DeleteIcon" :size="14" />
102 + </template>
103 + </n-button>
104 + </template>
105 + Delete this integration?
106 <template #icon>
68 - <Icon :name="DeleteIcon" :size="14" />
107 + <Icon :name="WarningIcon" :size="14" />
108 </template>
70 - </n-button>
71 - </template>
72 - Delete this integration?
73 - <template #icon>
74 - <Icon :name="WarningIcon" :size="14" />
109 + </n-popconfirm>
110 </template>
76 - </n-popconfirm>
77 - </div>
78 - </template>
79 -
80 - <template #default>
81 - <div class="flex flex-col gap-1 text-sm">
82 - <div class="text-secondary">
83 - <span class="font-medium">Org-Id:</span>
84 - <code class="ml-1 break-all">{{ integration.shuffle_org_id }}</code>
85 - </div>
86 - <div v-if="verifyError" class="text-error text-xs">
87 - {{ verifyError }}
88 - </div>
89 - </div>
90 - </template>
91 -
92 - <template #footer>
93 - <div class="text-tertiary flex items-center gap-3 text-xs">
94 - <span v-if="integration.last_used_at">
95 - last used {{ formatDate(integration.last_used_at, "MMM D, YYYY HH:mm") }}
96 - </span>
97 - <span v-else>never used</span>
98 - <span v-if="integration.created_by">· created by {{ integration.created_by }}</span>
111 + Delete
112 + </n-tooltip>
113 </div>
114 </template>
115 </CardEntity>
116 </template>
117
118 <script setup lang="ts">
119 +import type { ApiError } from "@/types/common"
120 import type { ShuffleIntegration } from "@/types/notifications.d"
121 import { NButton, NPopconfirm, NTooltip, useMessage } from "naive-ui"
122 import { computed, ref } from "vue"
@@ -109,6 +124,7 @@ import Api from "@/api"
124 import Badge from "@/components/common/Badge.vue"
125 import CardEntity from "@/components/common/cards/CardEntity.vue"
126 import Icon from "@/components/common/Icon.vue"
127 +import { useSettingsStore } from "@/stores/settings"
128 import { getApiErrorMessage } from "@/utils"
129 import { formatDate } from "@/utils/format"
130
@@ -120,18 +136,21 @@ const emit = defineEmits<{
136 (e: "edit"): void
137 (e: "deleted"): void
138 (e: "toggled"): void
139 + (e: "manageApps"): void
140 }>()
141
142 const EditIcon = "carbon:edit"
143 const DeleteIcon = "carbon:trash-can"
144 const PauseIcon = "carbon:pause"
145 const PlayIcon = "carbon:play"
146 +const ManageAppsIcon = "carbon:catalog"
147 const VerifyIcon = "carbon:checkmark-outline"
148 const VerifyOkIcon = "carbon:checkmark-filled"
149 const VerifyFailIcon = "carbon:misuse"
150 const WarningIcon = "carbon:warning"
151
152 const message = useMessage()
153 +const dFormats = useSettingsStore().dateFormat
154
155 // Verify state — null until the user clicks "Test connection." Drives
156 // both the inline status badge and the verify button's color/icon so
@@ -140,7 +159,6 @@ const message = useMessage()
159 const verifying = ref(false)
160 const verifyResult = ref<"ok" | "fail" | null>(null)
161 const verifyAppCount = ref<number | null>(null)
143 -const verifyError = ref<string | null>(null)
162
163 // naive-ui n-button accepts type='success'|'error'|'default'|... — we
164 // map verify result to a colored button so the icon turns green on a
@@ -167,7 +185,7 @@ const verifyTooltip = computed(() => {
185
186 async function verify() {
187 verifying.value = true
170 - verifyError.value = null
188 +
189 try {
190 const res = await Api.notifications.verifyShuffleIntegration(
191 props.integration.customer_code,
@@ -178,11 +196,11 @@ async function verify() {
196 verifyAppCount.value = res.data.app_count
197 } else {
198 verifyResult.value = "fail"
181 - verifyError.value = res.data.error || res.data.message || "Verification failed"
199 + message.warning(res.data.error || res.data.message || "Verification failed")
200 }
183 - } catch (err: unknown) {
201 + } catch (err) {
202 verifyResult.value = "fail"
185 - verifyError.value = getApiErrorMessage(err as never) || "Verification failed"
203 + message.error(getApiErrorMessage(err as ApiError) || "Verification failed")
204 } finally {
205 verifying.value = false
206 }
frontend/src/components/customers/aiNotifications/CustomerShuffleIntegrations/CustomerShuffleIntegrations.vue renamed
+48 -19
@@ -1,7 +1,10 @@
1 <template>
2 <div class="customer-shuffle-integrations">
3 - <transition name="form-fade" mode="out-in">
4 - <div v-if="showForm">
3 + <transition name="fade" mode="out-in">
4 + <div v-if="showForm" class="flex flex-col gap-4">
5 + <h4>
6 + {{ editingIntegration ? `Edit ${editingIntegration.display_name}` : "Add Shuffle integration" }}
7 + </h4>
8 <CustomerShuffleIntegrationForm
9 :customer-code
10 :editing-integration
@@ -9,40 +12,40 @@
12 @close="closeForm()"
13 />
14 </div>
12 - <div v-else>
13 - <div class="flex items-center justify-between gap-4 px-7 pt-2">
15 + <div v-else class="flex flex-col gap-4">
16 + <div class="flex items-center justify-between gap-4">
17 <n-button size="small" type="primary" @click="openForm()">
18 <template #icon>
19 <Icon :name="AddIcon" :size="14" />
20 </template>
21 Add Shuffle integration
22 </n-button>
20 - <n-button size="small" :disabled="loading" @click="refreshList()">
21 - <template #icon>
22 - <Icon :name="RefreshIcon" :size="14" />
23 - </template>
24 - Refresh
25 - </n-button>
23 </div>
24
28 - <div class="text-secondary px-7 pt-2 text-sm">
25 + <n-alert
26 + title="Shuffle integrations"
27 + type="info"
28 + class="[&_.n-alert-body\_\_content]:text-sm! [&_.n-alert-body\_\_title]:text-sm!"
29 + >
30 Each row is one of this customer's Shuffle organizations. Routes set to the
30 - <strong>Shuffle</strong> channel reference one of these to pick the right org
31 - for outbound notifications. The deployment-wide Shuffle API key lives in the
32 - CoPilot connectors table — these rows just record the per-customer Org-Id.
33 - </div>
31 + <strong>Shuffle</strong>
32 + channel reference one of these to pick the right org for outbound notifications. The deployment-wide
33 + Shuffle API key lives in the CoPilot connectors table — these rows just record the per-customer
34 + Org-Id.
35 + </n-alert>
36
37 <n-spin :show="loading">
36 - <div class="min-h-52 p-7 pt-4">
38 + <div class="min-h-52">
39 <template v-if="list.length">
40 <CustomerShuffleIntegrationItem
41 v-for="integration of list"
42 :key="integration.id"
41 - :integration="integration"
43 + :integration
44 class="item-appear item-appear-bottom item-appear-005 mb-2"
45 @edit="openEdit(integration)"
46 @deleted="refreshList()"
47 @toggled="refreshList()"
48 + @manage-apps="openAppsDrawer(integration)"
49 />
50 </template>
51 <template v-else>
@@ -56,16 +59,19 @@
59 </n-spin>
60 </div>
61 </transition>
62 +
63 + <CustomerShuffleAppsDrawer v-model:show="showAppsDrawer" :integration="appsDrawerIntegration" />
64 </div>
65 </template>
66
67 <script setup lang="ts">
68 import type { ShuffleIntegration } from "@/types/notifications.d"
64 -import { NButton, NEmpty, NSpin, useMessage } from "naive-ui"
69 +import { NAlert, NButton, NEmpty, NSpin, useMessage } from "naive-ui"
70 import { onBeforeMount, ref } from "vue"
71 import Api from "@/api"
72 import Icon from "@/components/common/Icon.vue"
73 import { getApiErrorMessage } from "@/utils"
74 +import CustomerShuffleAppsDrawer from "./CustomerShuffleAppsDrawer.vue"
75 import CustomerShuffleIntegrationForm from "./CustomerShuffleIntegrationForm.vue"
76 import CustomerShuffleIntegrationItem from "./CustomerShuffleIntegrationItem.vue"
77
@@ -74,7 +80,6 @@ const { customerCode } = defineProps<{
80 }>()
81
82 const AddIcon = "carbon:add-alt"
77 -const RefreshIcon = "carbon:renew"
83
84 const message = useMessage()
85 const showForm = ref(false)
@@ -82,6 +87,14 @@ const loading = ref(false)
87 const list = ref<ShuffleIntegration[]>([])
88 const editingIntegration = ref<ShuffleIntegration | null>(null)
89
90 +// "Manage apps" drawer state. Opened from each integration row's
91 +// catalog button — gives admins a Shuffle picker scoped to that org
92 +// for authenticating new apps. Independent from the form drawer so
93 +// both can be open at once if needed (e.g. edit metadata while the
94 +// picker is still up after an OAuth handoff).
95 +const showAppsDrawer = ref(false)
96 +const appsDrawerIntegration = ref<ShuffleIntegration | null>(null)
97 +
98 function refreshList() {
99 loading.value = true
100 Api.notifications
@@ -121,5 +134,21 @@ function onFormSubmitted() {
134 refreshList()
135 }
136
137 +function openAppsDrawer(integration: ShuffleIntegration) {
138 + appsDrawerIntegration.value = integration
139 + showAppsDrawer.value = true
140 +}
141 +
142 onBeforeMount(refreshList)
143 </script>
144 +
145 +<style lang="scss" scoped>
146 +.fade-enter-active,
147 +.fade-leave-active {
148 + transition: opacity 0.2s ease-in-out;
149 +}
150 +.fade-enter-from,
151 +.fade-leave-to {
152 + opacity: 0;
153 +}
154 +</style>
frontend/src/components/customers/aiTriggers/CustomerAITriggers.vue
+5 -4
@@ -1,17 +1,18 @@
1 <template>
2 - <div class="customer-ai-triggers">
2 + <div class="customer-ai-triggers flex flex-col gap-4">
3 <transition name="form-fade" mode="out-in">
4 - <div v-if="showForm" class="p-7 pt-4">
4 + <div v-if="showForm" class="flex flex-col gap-4">
5 + <h4>Create an AI Trigger</h4>
6 <CustomerAITriggersForm :customer-code @mounted="formCTX = $event" @submitted="refreshList()">
7 <template #additionalActions="{ loading: loadingForm }">
8 <n-button :disabled="loadingForm" @click="closeForm()">Close</n-button>
9 </template>
10 </CustomerAITriggersForm>
11 </div>
11 - <div v-else>
12 + <div v-else class="flex flex-col gap-4">
13 <n-spin :show="loading" class="min-h-48">
14 <template v-if="list.length">
14 - <div class="min-h-52 p-7 pt-4">
15 + <div class="min-h-52">
16 <CustomerAITriggersItem
17 v-for="item of list"
18 :key="item.id"
frontend/src/components/customers/aiTriggers/CustomerAITriggersItem.vue
+1 -1
@@ -19,7 +19,7 @@
19 <n-modal
20 v-model:show="showForm"
21 preset="card"
22 - :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(400px, 90vh)', overflow: 'hidden' }"
22 + :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(200px, 90vh)', overflow: 'hidden' }"
23 title="AI Trigger"
24 :bordered="false"
25 display-directive="show"
frontend/src/components/customers/eventSources/CustomerEventSourceForm.vue
+2 -4
@@ -1,8 +1,6 @@
1 <template>
2 <div class="flex min-h-120 flex-col gap-4 overflow-hidden">
3 - <div class="flex flex-col gap-4 px-7 pt-4">
4 - <div class="text-sm font-semibold">{{ isEditing ? "Edit Event Source" : "New Event Source" }}</div>
5 -
3 + <div class="flex flex-col gap-4">
4 <n-form-item label="Name" required>
5 <n-input v-model:value="form.name" placeholder="e.g. Wazuh Alerts" clearable />
6 </n-form-item>
@@ -28,7 +26,7 @@
26 </n-form-item>
27 </div>
28
31 - <div class="flex justify-between gap-4 px-7 pb-4">
29 + <div class="flex justify-between gap-4">
30 <n-button @click="close()">Close</n-button>
31 <n-button type="primary" :disabled="!isValid" :loading @click="submit()">
32 {{ isEditing ? "Update" : "Create" }}
frontend/src/components/customers/eventSources/CustomerEventSources.vue
+4 -3
@@ -1,7 +1,8 @@
1 <template>
2 <div class="customer-event-sources">
3 <transition name="form-fade" mode="out-in">
4 - <div v-if="showForm">
4 + <div v-if="showForm" class="flex flex-col gap-4">
5 + <h4>{{ !!editingSource ? "Edit Event Source" : "Add Event Source" }}</h4>
6 <CustomerEventSourceForm
7 :customer-code
8 :editing-source
@@ -10,7 +11,7 @@
11 />
12 </div>
13 <div v-else>
13 - <div class="flex items-center justify-between gap-4 px-7 pt-2">
14 + <div class="flex items-center justify-between gap-4">
15 <n-button size="small" type="primary" @click="openForm()">
16 <template #icon>
17 <Icon :name="AddIcon" :size="14" />
@@ -20,7 +21,7 @@
21 </div>
22
23 <n-spin :show="loading">
23 - <div class="min-h-52 p-7 pt-4">
24 + <div class="min-h-52">
25 <template v-if="list.length">
26 <CustomerEventSourceItem
27 v-for="source of list"
frontend/src/components/customers/integrations/CustomerIntegrationForm.vue
+7 -7
@@ -1,8 +1,8 @@
1 <template>
2 - <div class="customer-integration-form flex min-h-120 flex-col gap-4 overflow-hidden">
2 + <div class="customer-integration-form flex min-h-120 flex-col gap-6 overflow-hidden">
3 <div>
4 <n-scrollbar x-scrollable trigger="none">
5 - <div class="px-7 pt-4 pb-2">
5 + <div class="p-1 pr-4">
6 <n-steps :current size="small" :status="currentStatus">
7 <n-step title="Choose Integration" />
8 <n-step title="Set Auth Keys">
@@ -15,21 +15,21 @@
15 </n-scrollbar>
16 </div>
17
18 - <div class="flex grow flex-col overflow-hidden">
18 + <div class="flex grow flex-col gap-2 overflow-hidden">
19 <Transition :name="`slide-form-${slideFormDirection}`">
20 <div v-if="current === 1" class="available-list grow overflow-hidden">
21 - <n-scrollbar style="max-height: 355px" trigger="none">
21 + <n-scrollbar class="max-h-100" trigger="none">
22 <IntegrationsList
23 v-model:selected="selectedIntegration"
24 embedded
25 hide-totals
26 selectable
27 :disabled-ids-list
28 - class="px-7"
28 + class="pr-4"
29 />
30 </n-scrollbar>
31 </div>
32 - <div v-else class="auth-key-form flex flex-wrap gap-3 px-7">
32 + <div v-else class="auth-key-form flex flex-wrap gap-3">
33 <template v-for="ak of authKeysForm" :key="ak.key">
34 <n-form-item v-if="ak.type === 'string'" :label="ak.key" required class="grow">
35 <n-input v-model:value="ak.value" :placeholder="`Input ${ak.key}...`" clearable />
@@ -48,7 +48,7 @@
48 </Transition>
49 </div>
50
51 - <div class="flex justify-between gap-4 px-7 pb-4">
51 + <div class="flex justify-between gap-4">
52 <div class="flex gap-4">
53 <n-button @click="close()">Close</n-button>
54 </div>
frontend/src/components/customers/integrations/CustomerIntegrations.vue
+3 -3
@@ -10,8 +10,8 @@
10 @close="closeForm()"
11 />
12 </div>
13 - <div v-else>
14 - <div class="flex items-center justify-between gap-4 px-7 pt-2">
13 + <div v-else class="flex flex-col gap-4">
14 + <div class="flex items-center justify-between gap-4">
15 <n-button size="small" type="primary" @click="openForm()">
16 <template #icon>
17 <Icon :name="AddIcon" :size="14" />
@@ -21,7 +21,7 @@
21 </div>
22
23 <n-spin :show="loading">
24 - <div class="min-h-52 p-7 pt-4">
24 + <div class="min-h-52">
25 <template v-if="list.length">
26 <CustomerIntegrationItem
27 v-for="integration of list"
frontend/src/components/customers/networkConnectors/CustomerNetworkConnectorForm.vue
+7 -7
@@ -1,8 +1,8 @@
1 <template>
2 - <div class="customer-network-connector-form flex min-h-120 flex-col gap-4 overflow-hidden">
2 + <div class="customer-network-connector-form flex min-h-120 flex-col gap-6 overflow-hidden">
3 <div>
4 <n-scrollbar x-scrollable trigger="none">
5 - <div class="px-7 pt-4 pb-2">
5 + <div class="p-1 pr-4">
6 <n-steps :current size="small" :status="currentStatus">
7 <n-step title="Choose Network Connector" />
8 <n-step title="Set Auth Keys">
@@ -15,21 +15,21 @@
15 </n-scrollbar>
16 </div>
17
18 - <div class="flex grow flex-col overflow-hidden">
18 + <div class="flex grow flex-col gap-2 overflow-hidden">
19 <Transition :name="`slide-form-${slideFormDirection}`">
20 <div v-if="current === 1" class="available-list grow overflow-hidden">
21 - <n-scrollbar style="max-height: 355px" trigger="none">
21 + <n-scrollbar class="max-h-100" trigger="none">
22 <NetworkConnectorsList
23 v-model:selected="selectedNetworkConnector"
24 embedded
25 hide-totals
26 selectable
27 :disabled-ids-list
28 - class="px-7"
28 + class="pr-4"
29 />
30 </n-scrollbar>
31 </div>
32 - <div v-else class="auth-key-form flex flex-wrap gap-3 px-7">
32 + <div v-else class="auth-key-form flex flex-wrap gap-3">
33 <template v-for="ak of authKeysForm" :key="ak.key">
34 <n-form-item v-if="ak.type === 'string'" :label="ak.key" required class="grow">
35 <n-input v-model:value="ak.value" :placeholder="`Input ${ak.key}...`" clearable />
@@ -48,7 +48,7 @@
48 </Transition>
49 </div>
50
51 - <div class="flex justify-between gap-4 px-7 pb-4">
51 + <div class="flex justify-between gap-4">
52 <div class="flex gap-4">
53 <n-button @click="close()">Close</n-button>
54 </div>
frontend/src/components/customers/networkConnectors/CustomerNetworkConnectors.vue
+4 -4
@@ -1,5 +1,5 @@
1 <template>
2 - <div class="customer-network-connectors">
2 + <div class="customer-network-connectors flex flex-col gap-4">
3 <transition name="form-fade" mode="out-in">
4 <div v-if="showForm">
5 <CustomerNetworkConnectorForm
@@ -10,8 +10,8 @@
10 @close="closeForm()"
11 />
12 </div>
13 - <div v-else>
14 - <div class="flex items-center justify-between gap-4 px-7 pt-2">
13 + <div v-else class="flex flex-col gap-4">
14 + <div class="flex items-center justify-between gap-4">
15 <n-button size="small" type="primary" @click="openForm()">
16 <template #icon>
17 <Icon :name="AddIcon" :size="14" />
@@ -21,7 +21,7 @@
21 </div>
22
23 <n-spin :show="loading">
24 - <div class="min-h-52 p-7 pt-4">
24 + <div class="min-h-52">
25 <template v-if="list.length">
26 <CustomerNetworkConnectorItem
27 v-for="networkConnector of list"
frontend/src/components/customers/notifications/CustomerNotificationsWorkflows.vue
+5 -4
@@ -1,7 +1,8 @@
1 <template>
2 - <div class="customer-notifications-workflows">
2 + <div class="customer-notifications-workflows flex flex-col gap-4">
3 <transition name="form-fade" mode="out-in">
4 - <div v-if="showForm" class="p-7 pt-4">
4 + <div v-if="showForm" class="flex flex-col gap-4">
5 + <h4>Create a Notification</h4>
6 <CustomerNotificationsWorkflowsForm
7 :customer-code
8 @mounted="formCTX = $event"
@@ -12,10 +13,10 @@
13 </template>
14 </CustomerNotificationsWorkflowsForm>
15 </div>
15 - <div v-else>
16 + <div v-else class="flex flex-col gap-4">
17 <n-spin :show="loading" class="min-h-48">
18 <template v-if="list.length">
18 - <div class="min-h-52 p-7 pt-4">
19 + <div class="min-h-52">
20 <CustomerNotificationsWorkflowsItem
21 v-for="item of list"
22 :key="item.id"
frontend/src/components/customers/notifications/CustomerNotificationsWorkflowsItem.vue
+1 -1
@@ -19,7 +19,7 @@
19 <n-modal
20 v-model:show="showForm"
21 preset="card"
22 - :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(400px, 90vh)', overflow: 'hidden' }"
22 + :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
23 title="Notification"
24 :bordered="false"
25 display-directive="show"
frontend/src/components/customers/provision/CustomerProvision.vue
+5 -5
@@ -1,5 +1,5 @@
1 <template>
2 - <div class="customer-provision">
2 + <div class="customer-provision flex flex-col gap-4">
3 <transition name="form-fade" mode="out-in">
4 <div v-if="editing">
5 <CustomerProvisionWizard :customer-name="customerNameSanitized" :customer-code @submitted="submitted">
@@ -8,8 +8,8 @@
8 </template>
9 </CustomerProvisionWizard>
10 </div>
11 - <div v-else>
12 - <div v-if="customerMeta" class="flex items-center justify-end gap-4 px-7 pt-2">
11 + <div v-else class="flex flex-col gap-4">
12 + <div v-if="customerMeta" class="flex items-center justify-end gap-4">
13 <n-button size="small" type="error" ghost :loading="loadingDelete" @click="handleDelete">
14 <template #icon>
15 <Icon :name="DeleteIcon" :size="15" />
@@ -17,7 +17,7 @@
17 Decommission
18 </n-button>
19 </div>
20 - <div v-else class="flex items-center justify-between gap-4 px-7 pt-2">
20 + <div v-else class="flex items-center justify-between gap-4">
21 <n-button size="small" type="primary" @click="editing = true">
22 <template #icon>
23 <Icon :name="AddIcon" :size="14" />
@@ -26,7 +26,7 @@
26 </n-button>
27 </div>
28
29 - <div class="grid-auto-fit-200 grid gap-2 p-7 pt-4">
29 + <div class="grid-auto-fit-200 grid gap-2">
30 <CardKV v-for="(value, key) of customerMeta" :key>
31 <template #key>
32 {{ key }}
frontend/src/components/customers/provision/CustomerProvisionWizard.vue
+10 -10
@@ -1,9 +1,9 @@
1 <template>
2 <n-spin :show="loading" class="customer-provision-wizard">
3 - <div class="flex min-h-120 flex-col">
4 - <div class="grow">
3 + <div class="flex min-h-120 flex-col gap-4">
4 + <div class="flex grow flex-col gap-6">
5 <n-scrollbar x-scrollable trigger="none">
6 - <div class="p-7 pt-4">
6 + <div class="p-1 pr-4">
7 <n-steps :current size="small" :status="currentStatus">
8 <n-step title="Provisioning" />
9 <n-step title="Graylog" />
@@ -22,9 +22,9 @@
22 </div>
23 </n-scrollbar>
24
25 - <n-form ref="formRef" :label-width="80" :model="form" :rules class="form-container mt-4">
25 + <n-form ref="formRef" :label-width="80" :model="form" :rules class="form-container">
26 <Transition :name="`slide-form-${slideFormDirection}`">
27 - <div v-if="current === 1" class="flex flex-col gap-3 px-7">
27 + <div v-if="current === 1" class="flex flex-col gap-3">
28 <div class="flex flex-wrap gap-3">
29 <n-form-item label="Customer Code" path="customer_code" class="grow">
30 <n-input
@@ -68,7 +68,7 @@
68 </n-form-item>
69 </div>
70
71 - <div v-else-if="current === 2" class="flex flex-col gap-3 px-7">
71 + <div v-else-if="current === 2" class="flex flex-col gap-3">
72 <div class="flex flex-col gap-3 sm:flex-row">
73 <n-form-item
74 label="Customer Index name"
@@ -97,7 +97,7 @@
97 </div>
98 </div>
99
100 - <div v-else-if="current === 3" class="flex flex-col gap-3 px-7">
100 + <div v-else-if="current === 3" class="flex flex-col gap-3">
101 <n-form-item label="Subscriptions" path="customer_subscription" class="grow">
102 <n-select
103 v-model:value="form.customer_subscription"
@@ -148,7 +148,7 @@
148 </n-form-item>
149 </div>
150
151 - <div v-else-if="current === 4" class="flex gap-3 px-7">
151 + <div v-else-if="current === 4" class="flex gap-3">
152 <n-card class="grow">
153 <n-form-item label="Deploy Wazuh Worker" path="provision_wazuh_worker">
154 <n-switch v-model:value="form.provision_wazuh_worker" clearable />
@@ -175,7 +175,7 @@
175 </n-card>
176 </div>
177
178 - <div v-else-if="current === 5" class="flex flex-col gap-3 px-7">
178 + <div v-else-if="current === 5" class="flex flex-col gap-3">
179 <div class="flex flex-wrap gap-3">
180 <n-form-item label="Auth Password" path="wazuh_auth_password" class="grow">
181 <n-input
@@ -251,7 +251,7 @@
251 </n-form>
252 </div>
253
254 - <div class="flex justify-between gap-4 p-7 pt-4">
254 + <div class="flex justify-between gap-4">
255 <div class="flex gap-4">
256 <slot name="additionalActions"></slot>
257 </div>
frontend/src/router/index.ts
+4 -4
@@ -352,10 +352,10 @@ const router = createRouter({
352 meta: { title: "Network Connectors" }
353 },
354 {
355 - path: "singul-app-auth",
356 - name: "ExternalServices-SingulAppAuth",
357 - component: () => import("@/views/externalServices/SingulAppAuth.vue"),
358 - meta: { title: "Singul App Auth" }
355 + path: "shuffle-app-auth",
356 + name: "ExternalServices-ShuffleAppAuth",
357 + component: () => import("@/views/externalServices/ShuffleAppAuth.vue"),
358 + meta: { title: "Shuffle App Auth" }
359 }
360 ]
361 },
frontend/src/theme/index.ts
+6
@@ -120,6 +120,12 @@ export function getThemeOverrides(state: ThemeState): GlobalThemeOverrides {
120 titleFontSizeHuge: state.fontSize.cardTitle,
121 titleFontWeight: state.fontWeight.cardTitle
122 },
123 + Form: {
124 + feedbackPadding: "2px",
125 + feedbackFontSizeSmall: "10px",
126 + feedbackFontSizeMedium: "12px",
127 + feedbackFontSizeLarge: "14px"
128 + },
129 LoadingBar: {
130 colorLoading: primary
131 },
frontend/src/types/notifications.d.ts
+5 -1
@@ -2,7 +2,11 @@
2 // Kept intentionally narrow: Phase 1 ships Slack webhook + SMTP email only;
3 // Phase 2 will extend the channel union with 'shuffle'.
4
5 -export type NotificationTrigger = "investigation_complete" | "severity_critical_or_high"
5 +// Trigger represents the *event type* that caused the dispatch — not
6 +// a severity filter (severity gating lives in min_severity). Currently
7 +// just the one Talon-driven event; will grow when we add hooks for
8 +// analyst-review / IOC-enrichment / scheduled sweeps.
9 +export type NotificationTrigger = "investigation_complete"
10
11 export type NotificationChannel = "smtp_email" | "shuffle"
12
frontend/src/views/externalServices/ShuffleAppAuth.vue new
+94
@@ -0,0 +1,94 @@
1 +<template>
2 + <div class="page page-wrapped page-without-footer flex flex-col gap-2 overflow-hidden">
3 + <n-select
4 + v-model:value="selectedOrganization"
5 + :options="organizationsOptions"
6 + :loading="loadingList"
7 + :disabled="loadingList"
8 + placeholder="Select an Organization..."
9 + />
10 +
11 + <n-spin
12 + v-if="selectedOrganization"
13 + :show="loadingToken"
14 + class="flex grow flex-col overflow-hidden"
15 + content-class="h-full"
16 + >
17 + <ShuffleMCPEmbed
18 + v-if="organization?.org_auth?.token"
19 + :auth-token="organization.org_auth.token"
20 + placeholder="Find an app..."
21 + inline
22 + class="h-full"
23 + />
24 + </n-spin>
25 + </div>
26 +</template>
27 +
28 +<script setup lang="ts">
29 +import type { Organization } from "@/types/shuffle.d"
30 +import { NSelect, NSpin, useMessage } from "naive-ui"
31 +import { computed, onBeforeMount, ref, watch } from "vue"
32 +import Api from "@/api"
33 +import ShuffleMCPEmbed from "@/components/common/ShuffleMCPEmbed.vue"
34 +
35 +const loadingList = ref(false)
36 +const loadingToken = ref(false)
37 +const message = useMessage()
38 +const organizations = ref<Organization[]>([])
39 +const organization = ref<Organization | null>(null)
40 +const selectedOrganization = ref<string | null>(null)
41 +const organizationsOptions = computed(() => organizations.value.map(o => ({ value: o.id, label: o.name })))
42 +
43 +function getOrganizations() {
44 + loadingList.value = true
45 +
46 + Api.shuffle
47 + .getOrganizations()
48 + .then(res => {
49 + if (res.data.success) {
50 + organizations.value = res.data?.data || []
51 + } else {
52 + message.warning(res.data?.message || "An error occurred. Please try again later.")
53 + }
54 + })
55 + .catch(err => {
56 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
57 + })
58 + .finally(() => {
59 + loadingList.value = false
60 + })
61 +}
62 +
63 +function getOrganization(id: string) {
64 + loadingToken.value = true
65 +
66 + Api.shuffle
67 + .getOrganization(id)
68 + .then(res => {
69 + if (res.data.success) {
70 + organization.value = res.data?.data || null
71 + } else {
72 + message.warning(res.data?.message || "An error occurred. Please try again later.")
73 + }
74 + })
75 + .catch(err => {
76 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
77 + })
78 + .finally(() => {
79 + loadingToken.value = false
80 + })
81 +}
82 +
83 +watch(selectedOrganization, val => {
84 + if (val) {
85 + getOrganization(val)
86 + } else {
87 + organization.value = null
88 + }
89 +})
90 +
91 +onBeforeMount(() => {
92 + getOrganizations()
93 +})
94 +</script>
frontend/src/views/externalServices/SingulAppAuth.vue deleted
-132
@@ -1,132 +0,0 @@
1 -<template>
2 - <div class="flex flex-col gap-2">
3 - <n-select
4 - v-model:value="selectedOrganization"
5 - :options="organizationsOptions"
6 - :loading="loadingList"
7 - :disabled="loadingList"
8 - placeholder="Select an Organization..."
9 - />
10 -
11 - <n-spin v-if="selectedOrganization" :show="loadingToken" :size="14" class="app-auth-search-spinner">
12 - <n-el>
13 - <app-search-bar
14 - class="app-auth-search"
15 - :auth="organization?.org_auth.token"
16 - placeholder="Find an app..."
17 - :custom-styles="JSON.stringify({ height: '100px' })"
18 - ></app-search-bar>
19 - </n-el>
20 - </n-spin>
21 - </div>
22 -</template>
23 -
24 -<script setup lang="ts">
25 -import type { Organization } from "@/types/shuffle.d"
26 -import { NEl, NSelect, NSpin, useMessage } from "naive-ui"
27 -import { computed, onBeforeMount, ref, watch } from "vue"
28 -import Api from "@/api"
29 -
30 -const loadingList = ref(false)
31 -const loadingToken = ref(false)
32 -const message = useMessage()
33 -const organizations = ref<Organization[]>([])
34 -const organization = ref<Organization | null>(null)
35 -const selectedOrganization = ref<string | null>(null)
36 -const organizationsOptions = computed(() => organizations.value.map(o => ({ value: o.id, label: o.name })))
37 -
38 -function getOrganizations() {
39 - loadingList.value = true
40 -
41 - Api.shuffle
42 - .getOrganizations()
43 - .then(res => {
44 - if (res.data.success) {
45 - organizations.value = res.data?.data || []
46 - } else {
47 - message.warning(res.data?.message || "An error occurred. Please try again later.")
48 - }
49 - })
50 - .catch(err => {
51 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
52 - })
53 - .finally(() => {
54 - loadingList.value = false
55 - })
56 -}
57 -
58 -function getOrganization(id: string) {
59 - loadingToken.value = true
60 -
61 - Api.shuffle
62 - .getOrganization(id)
63 - .then(res => {
64 - if (res.data.success) {
65 - organization.value = res.data?.data || null
66 - } else {
67 - message.warning(res.data?.message || "An error occurred. Please try again later.")
68 - }
69 - })
70 - .catch(err => {
71 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
72 - })
73 - .finally(() => {
74 - loadingToken.value = false
75 - })
76 -}
77 -
78 -watch(selectedOrganization, val => {
79 - if (val) {
80 - getOrganization(val)
81 - } else {
82 - organization.value = null
83 - }
84 -})
85 -
86 -onBeforeMount(() => {
87 - getOrganizations()
88 -})
89 -</script>
90 -
91 -<style lang="scss" scoped>
92 -.app-auth-search {
93 - --search-input-bg: var(--input-color);
94 - --search-input-font-size: var(--font-size-medium);
95 - --search-input-border: 1px solid transparent;
96 - --search-input-border-radius: var(--border-radius);
97 - --search-input-padding: 7px 12px 6px 12px;
98 - --search-input-color: var(--text-color-base);
99 - --search-input-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0);
100 - --search-input-focus-border: var(--primary-color);
101 - --search-input-focus-shadow: 0 0 8px 0 rgba(var(--primary-color-rgb) / 0.4);
102 - --search-input-line-height: 1.3;
103 - --search-placeholder-color: var(--placeholder-color);
104 - --search-icon-color: var(--icon-color);
105 - --search-font-family: var(--font-family);
106 - --search-spinner-border: 1px solid transparent;
107 - --search-spinner-border-top: 1px solid rgba(var(--primary-color-rgb) / 0.4);
108 - --dropdown-bg: var(--input-color);
109 - --dropdown-border: 1px solid transparent;
110 - --dropdown-border-radius: var(--border-radius);
111 - --dropdown-item-border: 1px solid var(--divider-color);
112 - --dropdown-item-hover-bg: var(--button-color-2-hover);
113 - --app-icon-size: 40px;
114 - --app-icon-border: 1px solid var(--border-color);
115 - --app-icon-border-radius: var(--border-radius-small);
116 - --app-name-color: var(--text-color-base);
117 - --app-name-font-size: var(--font-size-medium);
118 - --empty-state-color: var(--placeholder-color);
119 - --empty-state-font-size: var(--font-size-medium);
120 -}
121 -.app-auth-search-spinner {
122 - :deep() {
123 - .n-spin-body .n-base-loading {
124 - position: relative;
125 - margin-top: 0;
126 - transform: translateY(-7px);
127 - justify-content: flex-end;
128 - padding-right: 16px;
129 - }
130 - }
131 -}
132 -</style>