@cryptotaxi247 / CoPilot / commits / 074a377d

Customer port (#531)

* Fix error message for missing username in token verification * Fix error message for missing username in token verification * Refactor artifact collection logic and add file fetching functionality * chore: update frontend dependencies * chore: update frontend dependencies * chore: update frontend dependencies * fix: remove hotfixes option from osquery configuration * feat: add customer portal settings model and migration * feat: add customer portal settings routes and models * feat: add agent datastore model and migration * feat: implement agent data store operations and routes * feat: enhance agent data store routes and schema to include optional customer_code * feat: refactor run_file_collection and fetch_file_from_filestore functions for improved clarity and logging * feat: comment out artifact name validation in collect_file function for debugging * Chore: Adds TODO comments for code improvements Adds TODO comments to remind about renaming files to file-case and using .ts files instead of .d.ts files to improve code quality. * chore: Updates dependencies Updates pnpm version and other dependencies to their latest versions. This ensures compatibility and incorporates the latest features and bug fixes. * feat: Adds customer portal API endpoints Adds API endpoints for managing customer portal settings. This includes endpoints for retrieving and updating settings such as the portal title and logo. * Feat: Adds customer portal page Adds a new customer portal page with basic settings component. Also adds a navigation item to the navbar and a corresponding route. * Feat: Adds customer portal settings page Introduces a dedicated settings page for the customer portal. This includes a form to manage portal settings, such as title and logo, and a preview component to visualize the changes. The page fetches the current settings and displays them in the form. It also provides a "Save Changes" button to update the settings via an API call. * feat: add agent data store functionality with artifact management and UI components * feat: add Data Store tab and implement AgentDataStoreTabCompact component for artifact management * feat: add file path formatting function and new route for file collection by agent ID * feat: implement file collection form and API integration for agent file collection * feat: add File Collection tab and integrate FileCollectionForm component for agent file collection * Feat: Enables customization of customer portal Allows administrators to customize the customer portal's appearance, including the title and logo. Introduces a new image cropper component for logo selection and upload. Updates the API payload to support null values for title and logo. Updates the preview component to reflect the title and logo input * Feat: Enables logo preview in settings Adds a live preview of the logo and title settings in the customer portal settings page. This allows users to see how their changes will look before saving. Parses base64 encoded logo data from the API to display in the preview. * feat: Enhance portal settings update to restore defaults when fields are set to null * feat: Update logo handling in portal settings with base64 validation and adjust database schema * feat: Update database connection URL in alembic.ini for improved security * feat: Update security scopes for MCP server routes to include analyst role * feat: Update security dependencies to use require_any_scope for MCP server routes * refactor * refactor agents components * refactor * refactor * updated customer portal form * updated dependencies * added login customization * Fix: Handles logo loading errors Fixes an issue where the portal logo would cause a visual error if it failed to load. Now, the logo is hidden if loading fails by setting a boolean flag. * Feat: Centralizes portal settings fetching Centralizes the fetching of portal settings into a dedicated Pinia store. This change improves code maintainability and reduces redundancy by: - Creating a `portalSettings` store to manage portal settings state and fetching logic. - Moving the API call for fetching settings into the store's `fetchSettings` action. - Replacing direct API calls in components (e.g., LoginPage) with store accessors (getters). - Fetching portal settings in App.vue on mount * Feat: Persists portal settings in sessionStorage Adds sessionStorage persistence for portal settings to improve user experience by retaining settings across sessions. Implements load and save actions, with fallback to fetching from the API when sessionStorage is empty or if 'force' is enabled. * feat: Adds dynamic favicon update Implements dynamic favicon updating based on the portal logo. This enhances the user experience by visually representing the customer's brand within the application tab. It watches for changes to the portal logo and updates the favicon accordingly. * feat: Updates favicon dynamically from logo Updates the favicon dynamically based on the portal logo settings. The logo is converted to a 32x32 PNG format before being set as the favicon. This ensures the favicon accurately reflects the current portal branding. * Feat: Updates document title dynamically Adds functionality to update the document title dynamically based on changes to the portal title in the store. This improves user experience by reflecting the current portal context in the browser tab. * feat: Adds build and push workflow for customer portal Docker image * Refactors customer portal UI with Tailwind CSS This commit refactors the customer portal's UI to utilize Tailwind CSS for styling, enhancing maintainability and consistency. - Replaces custom CSS with Tailwind CSS classes. - Updates the visual appearance of the agents, alerts, and overview pages. - Introduces the use of a store for managing portal settings like title and logo. * Fix: Aligns case statuses for consistency Updates case and alert statuses to use consistent naming (UPPERCASE with underscores) across the application to avoid potential issues on comparisons and filtering. * feat: Add Dockerfile and nginx configuration for customer portal deployment * precommit-fixes * chore: Comment out optional customer portal service in docker-compose.yml --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Nov 30, 2025 at 10:03 UTC 074a377d0715489ff351c98cd6f8b917b6940ad8
224 files changed +9502 -5318
.github/workflows/docker.yml
+31
@@ -68,3 +68,34 @@ jobs:
68 webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
69 webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
70 message: "Docker image for frontend has been updated."
71 +
72 + build-customer-portal:
73 + runs-on: ubuntu-latest
74 +
75 + steps:
76 + - name: Check out code
77 + uses: actions/checkout@v2
78 +
79 + - name: Set up Docker Buildx
80 + uses: docker/setup-buildx-action@v1
81 +
82 + - name: Login to GitHub Container Registry
83 + uses: docker/login-action@v1
84 + with:
85 + registry: ghcr.io
86 + username: ${{ secrets.REPO_OWNER }}
87 + password: ${{ secrets.REPO_TOKEN }}
88 +
89 + - name: Build and push Docker image
90 + uses: docker/build-push-action@v2
91 + with:
92 + context: ./customer_portal
93 + push: true
94 + tags: ghcr.io/socfortress/copilot-customer-portal:latest
95 +
96 + - name: Notify Discord
97 + uses: appleboy/discord-action@v1.0.0
98 + with:
99 + webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
100 + webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
101 + message: "Docker image for frontend has been updated."
.pre-commit-config.yaml
+1
@@ -7,6 +7,7 @@ repos:
7 - id: check-yaml
8 args: ["--unsafe"]
9 - id: end-of-file-fixer
10 + exclude: '\.json$'
11 - id: check-json
12 - id: trailing-whitespace
13 - id: check-added-large-files
backend/alembic/versions/1dd1c055ee06_add_customer_portal_settings.py new
+40
@@ -0,0 +1,40 @@
1 +"""Add customer portal settings
2 +
3 +Revision ID: 1dd1c055ee06
4 +Revises: 47c5adfa3c7e
5 +Create Date: 2025-11-27 08:19:47.388646
6 +
7 +"""
8 +from typing import Sequence
9 +from typing import Union
10 +
11 +import sqlalchemy as sa
12 +
13 +from alembic import op
14 +
15 +# revision identifiers, used by Alembic.
16 +revision: str = "1dd1c055ee06"
17 +down_revision: Union[str, None] = "47c5adfa3c7e"
18 +branch_labels: Union[str, Sequence[str], None] = None
19 +depends_on: Union[str, Sequence[str], None] = None
20 +
21 +
22 +def upgrade() -> None:
23 + # ### commands auto generated by Alembic - please adjust! ###
24 + op.create_table(
25 + "customer_portal_settings",
26 + sa.Column("id", sa.Integer(), nullable=False),
27 + sa.Column("title", sa.String(length=255), nullable=False),
28 + sa.Column("logo_base64", sa.Text(), nullable=True),
29 + sa.Column("logo_mime_type", sa.String(length=50), nullable=True),
30 + sa.Column("updated_at", sa.DateTime(), nullable=False),
31 + sa.Column("updated_by", sa.Integer(), nullable=True),
32 + sa.PrimaryKeyConstraint("id"),
33 + )
34 + # ### end Alembic commands ###
35 +
36 +
37 +def downgrade() -> None:
38 + # ### commands auto generated by Alembic - please adjust! ###
39 + op.drop_table("customer_portal_settings")
40 + # ### end Alembic commands ###
backend/alembic/versions/47c5adfa3c7e_add_escalated_column_to_alerts_and_cases.py
+10 -9
@@ -5,28 +5,29 @@ Revises: 5599c90717d4
5 Create Date: 2025-09-19 13:55:35.348208
6
7 """
8 -from typing import Sequence, Union
8 +from typing import Sequence
9 +from typing import Union
10
10 -from alembic import op
11 import sqlalchemy as sa
12 -from sqlalchemy.dialects import mysql
12 +
13 +from alembic import op
14
15 # revision identifiers, used by Alembic.
15 -revision: str = '47c5adfa3c7e'
16 -down_revision: Union[str, None] = '5599c90717d4'
16 +revision: str = "47c5adfa3c7e"
17 +down_revision: Union[str, None] = "5599c90717d4"
18 branch_labels: Union[str, Sequence[str], None] = None
19 depends_on: Union[str, Sequence[str], None] = None
20
21
22 def upgrade() -> None:
23 # ### commands auto generated by Alembic - please adjust! ###
23 - op.add_column('incident_management_alert', sa.Column('escalated', sa.Boolean(), nullable=False))
24 - op.add_column('incident_management_case', sa.Column('escalated', sa.Boolean(), nullable=False))
24 + op.add_column("incident_management_alert", sa.Column("escalated", sa.Boolean(), nullable=False))
25 + op.add_column("incident_management_case", sa.Column("escalated", sa.Boolean(), nullable=False))
26 # ### end Alembic commands ###
27
28
29 def downgrade() -> None:
30 # ### commands auto generated by Alembic - please adjust! ###
30 - op.drop_column('incident_management_case', 'escalated')
31 - op.drop_column('incident_management_alert', 'escalated')
31 + op.drop_column("incident_management_case", "escalated")
32 + op.drop_column("incident_management_alert", "escalated")
33 # ### end Alembic commands ###
backend/alembic/versions/53e04a4c4ff3_remove_customer_code_column_from_agent_.py new
+42
@@ -0,0 +1,42 @@
1 +"""Remove customer code column from agent datastore
2 +
3 +Revision ID: 53e04a4c4ff3
4 +Revises: 5c588e7cb55c
5 +Create Date: 2025-11-27 10:04:02.283557
6 +
7 +"""
8 +from typing import Sequence
9 +from typing import Union
10 +
11 +import sqlalchemy as sa
12 +from sqlalchemy.dialects import mysql
13 +
14 +from alembic import op
15 +
16 +# revision identifiers, used by Alembic.
17 +revision: str = "53e04a4c4ff3"
18 +down_revision: Union[str, None] = "5c588e7cb55c"
19 +branch_labels: Union[str, Sequence[str], None] = None
20 +depends_on: Union[str, Sequence[str], None] = None
21 +
22 +
23 +def upgrade() -> None:
24 + # ### commands auto generated by Alembic - please adjust! ###
25 + # Drop foreign key constraint FIRST
26 + op.drop_constraint("agent_datastore_ibfk_2", "agent_datastore", type_="foreignkey")
27 + # Then drop the index
28 + op.drop_index("ix_agent_datastore_customer_code", table_name="agent_datastore")
29 + # Finally drop the column
30 + op.drop_column("agent_datastore", "customer_code")
31 + # ### end Alembic commands ###
32 +
33 +
34 +def downgrade() -> None:
35 + # ### commands auto generated by Alembic - please adjust! ###
36 + # Add column first
37 + op.add_column("agent_datastore", sa.Column("customer_code", mysql.VARCHAR(length=50), nullable=False))
38 + # Then create the index
39 + op.create_index("ix_agent_datastore_customer_code", "agent_datastore", ["customer_code"], unique=False)
40 + # Finally create the foreign key
41 + op.create_foreign_key("agent_datastore_ibfk_2", "agent_datastore", "customers", ["customer_code"], ["customer_code"])
42 + # ### end Alembic commands ###
backend/alembic/versions/5599c90717d4_add_comments_to_cases.py
+19 -14
@@ -5,29 +5,34 @@ Revises: d8f9e9ea5502
5 Create Date: 2025-09-19 08:58:00.060975
6
7 """
8 -from typing import Sequence, Union
8 +from typing import Sequence
9 +from typing import Union
10
10 -from alembic import op
11 import sqlalchemy as sa
12 -from sqlalchemy.dialects import mysql
12 +
13 +from alembic import op
14
15 # revision identifiers, used by Alembic.
15 -revision: str = '5599c90717d4'
16 -down_revision: Union[str, None] = 'd8f9e9ea5502'
16 +revision: str = "5599c90717d4"
17 +down_revision: Union[str, None] = "d8f9e9ea5502"
18 branch_labels: Union[str, Sequence[str], None] = None
19 depends_on: Union[str, Sequence[str], None] = None
20
21
22 def upgrade() -> None:
23 # ### commands auto generated by Alembic - please adjust! ###
23 - op.create_table('incident_management_case_comment',
24 - sa.Column('id', sa.Integer(), nullable=False),
25 - sa.Column('case_id', sa.Integer(), nullable=True),
26 - sa.Column('comment', sa.String(length=1064), nullable=False),
27 - sa.Column('user_name', sa.String(length=50), nullable=False),
28 - sa.Column('created_at', sa.DateTime(), nullable=False),
29 - sa.ForeignKeyConstraint(['case_id'], ['incident_management_case.id'], ),
30 - sa.PrimaryKeyConstraint('id')
24 + op.create_table(
25 + "incident_management_case_comment",
26 + sa.Column("id", sa.Integer(), nullable=False),
27 + sa.Column("case_id", sa.Integer(), nullable=True),
28 + sa.Column("comment", sa.String(length=1064), nullable=False),
29 + sa.Column("user_name", sa.String(length=50), nullable=False),
30 + sa.Column("created_at", sa.DateTime(), nullable=False),
31 + sa.ForeignKeyConstraint(
32 + ["case_id"],
33 + ["incident_management_case.id"],
34 + ),
35 + sa.PrimaryKeyConstraint("id"),
36 )
37
38 # ### end Alembic commands ###
@@ -35,5 +40,5 @@ def upgrade() -> None:
40
41 def downgrade() -> None:
42 # ### commands auto generated by Alembic - please adjust! ###
38 - op.drop_table('incident_management_case_comment')
43 + op.drop_table("incident_management_case_comment")
44 # ### end Alembic commands ###
backend/alembic/versions/5c588e7cb55c_add_agent_datastore.py new
+71
@@ -0,0 +1,71 @@
1 +"""Add agent datastore
2 +
3 +Revision ID: 5c588e7cb55c
4 +Revises: 1dd1c055ee06
5 +Create Date: 2025-11-27 09:39:47.952715
6 +
7 +"""
8 +from typing import Sequence
9 +from typing import Union
10 +
11 +import sqlalchemy as sa
12 +
13 +from alembic import op
14 +
15 +# revision identifiers, used by Alembic.
16 +revision: str = "5c588e7cb55c"
17 +down_revision: Union[str, None] = "1dd1c055ee06"
18 +branch_labels: Union[str, Sequence[str], None] = None
19 +depends_on: Union[str, Sequence[str], None] = None
20 +
21 +
22 +def upgrade() -> None:
23 + # ### commands auto generated by Alembic - please adjust! ###
24 + op.create_table(
25 + "agent_datastore",
26 + sa.Column("notes", sa.Text(), nullable=True),
27 + sa.Column("error_message", sa.Text(), nullable=True),
28 + sa.Column("id", sa.Integer(), nullable=False),
29 + sa.Column("agent_id", sa.String(length=256), nullable=False),
30 + sa.Column("velociraptor_id", sa.String(length=256), nullable=False),
31 + sa.Column("customer_code", sa.String(length=50), nullable=False),
32 + sa.Column("artifact_name", sa.String(length=255), nullable=False),
33 + sa.Column("flow_id", sa.String(length=255), nullable=False),
34 + sa.Column("collection_time", sa.DateTime(), nullable=False),
35 + sa.Column("bucket_name", sa.String(length=255), nullable=False),
36 + sa.Column("object_key", sa.String(length=1024), nullable=False),
37 + sa.Column("file_name", sa.String(length=255), nullable=False),
38 + sa.Column("content_type", sa.String(length=100), nullable=False),
39 + sa.Column("file_size", sa.Integer(), nullable=False),
40 + sa.Column("file_hash", sa.String(length=128), nullable=False),
41 + sa.Column("uploaded_by", sa.Integer(), nullable=True),
42 + sa.Column("status", sa.String(length=50), nullable=False),
43 + sa.ForeignKeyConstraint(
44 + ["agent_id"],
45 + ["agents.agent_id"],
46 + ),
47 + sa.ForeignKeyConstraint(
48 + ["customer_code"],
49 + ["customers.customer_code"],
50 + ),
51 + sa.PrimaryKeyConstraint("id"),
52 + )
53 + op.create_index(op.f("ix_agent_datastore_agent_id"), "agent_datastore", ["agent_id"], unique=False)
54 + op.create_index(op.f("ix_agent_datastore_artifact_name"), "agent_datastore", ["artifact_name"], unique=False)
55 + op.create_index(op.f("ix_agent_datastore_collection_time"), "agent_datastore", ["collection_time"], unique=False)
56 + op.create_index(op.f("ix_agent_datastore_customer_code"), "agent_datastore", ["customer_code"], unique=False)
57 + op.create_index(op.f("ix_agent_datastore_flow_id"), "agent_datastore", ["flow_id"], unique=False)
58 + op.create_index(op.f("ix_agent_datastore_status"), "agent_datastore", ["status"], unique=False)
59 + # ### end Alembic commands ###
60 +
61 +
62 +def downgrade() -> None:
63 + # ### commands auto generated by Alembic - please adjust! ###
64 + op.drop_index(op.f("ix_agent_datastore_status"), table_name="agent_datastore")
65 + op.drop_index(op.f("ix_agent_datastore_flow_id"), table_name="agent_datastore")
66 + op.drop_index(op.f("ix_agent_datastore_customer_code"), table_name="agent_datastore")
67 + op.drop_index(op.f("ix_agent_datastore_collection_time"), table_name="agent_datastore")
68 + op.drop_index(op.f("ix_agent_datastore_artifact_name"), table_name="agent_datastore")
69 + op.drop_index(op.f("ix_agent_datastore_agent_id"), table_name="agent_datastore")
70 + op.drop_table("agent_datastore")
71 + # ### end Alembic commands ###
backend/alembic/versions/74727b1f75e6_remove_logo_base64_column.py new
+32
@@ -0,0 +1,32 @@
1 +"""Remove logo_base64 column
2 +
3 +Revision ID: 74727b1f75e6
4 +Revises: 53e04a4c4ff3
5 +Create Date: 2025-11-29 09:26:08.423329
6 +
7 +"""
8 +from typing import Sequence
9 +from typing import Union
10 +
11 +import sqlalchemy as sa
12 +from sqlalchemy.dialects import mysql
13 +
14 +from alembic import op
15 +
16 +# revision identifiers, used by Alembic.
17 +revision: str = "74727b1f75e6"
18 +down_revision: Union[str, None] = "53e04a4c4ff3"
19 +branch_labels: Union[str, Sequence[str], None] = None
20 +depends_on: Union[str, Sequence[str], None] = None
21 +
22 +
23 +def upgrade() -> None:
24 + # ### commands auto generated by Alembic - please adjust! ###
25 + op.drop_column("customer_portal_settings", "logo_base64")
26 + # ### end Alembic commands ###
27 +
28 +
29 +def downgrade() -> None:
30 + # ### commands auto generated by Alembic - please adjust! ###
31 + op.add_column("customer_portal_settings", sa.Column("logo_base64", mysql.TEXT(), nullable=True))
32 + # ### end Alembic commands ###
backend/alembic/versions/e74210501112_add_logo_base64_column.py new
+32
@@ -0,0 +1,32 @@
1 +"""Add logo_base64 column
2 +
3 +Revision ID: e74210501112
4 +Revises: 74727b1f75e6
5 +Create Date: 2025-11-29 09:30:00.832420
6 +
7 +"""
8 +from typing import Sequence
9 +from typing import Union
10 +
11 +import sqlalchemy as sa
12 +from sqlalchemy.dialects import mysql
13 +
14 +from alembic import op
15 +
16 +# revision identifiers, used by Alembic.
17 +revision: str = "e74210501112"
18 +down_revision: Union[str, None] = "74727b1f75e6"
19 +branch_labels: Union[str, Sequence[str], None] = None
20 +depends_on: Union[str, Sequence[str], None] = None
21 +
22 +
23 +def upgrade() -> None:
24 + # ### commands auto generated by Alembic - please adjust! ###
25 + op.add_column("customer_portal_settings", sa.Column("logo_base64", mysql.LONGTEXT(), nullable=True))
26 + # ### end Alembic commands ###
27 +
28 +
29 +def downgrade() -> None:
30 + # ### commands auto generated by Alembic - please adjust! ###
31 + op.drop_column("customer_portal_settings", "logo_base64")
32 + # ### end Alembic commands ###
backend/app/agents/vulnerabilities/routes/vulnerabilities.py
+1 -1
@@ -36,8 +36,8 @@ from app.agents.vulnerabilities.services.vulnerabilities import sync_all_vulnera
36 from app.agents.vulnerabilities.services.vulnerabilities import (
37 sync_vulnerabilities_for_agent,
38 )
39 -from app.auth.routes.auth import AuthHandler
39 from app.auth.models.users import User
40 +from app.auth.routes.auth import AuthHandler
41 from app.db.db_session import get_db
42 from app.db.db_session import get_db_session
43
backend/app/agents/vulnerabilities/services/vulnerabilities.py
+1 -1
@@ -20,13 +20,13 @@ from app.agents.vulnerabilities.schema.vulnerabilities import (
20 from app.agents.vulnerabilities.schema.vulnerabilities import VulnerabilityStatsResponse
21 from app.agents.vulnerabilities.schema.vulnerabilities import VulnerabilitySyncResponse
22 from app.agents.vulnerabilities.schema.vulnerabilities import WazuhVulnerabilityData
23 +from app.auth.models.users import User
24 from app.connectors.wazuh_indexer.utils.universal import collect_indices
25 from app.connectors.wazuh_indexer.utils.universal import (
26 create_wazuh_indexer_client_async,
27 )
28 from app.db.universal_models import Agents
29 from app.db.universal_models import AgentVulnerabilities
29 -from app.auth.models.users import User
30 from app.middleware.customer_access import customer_access_handler
31 from app.threat_intel.schema.epss import EpssThreatIntelRequest
32 from app.threat_intel.services.epss import collect_epss_score
backend/app/connectors/velociraptor/routes/artifacts.py
+152 -22
@@ -16,6 +16,7 @@ from app.connectors.velociraptor.schema.artifacts import ArtifactsResponse
16 from app.connectors.velociraptor.schema.artifacts import CollectArtifactBody
17 from app.connectors.velociraptor.schema.artifacts import CollectArtifactResponse
18 from app.connectors.velociraptor.schema.artifacts import CollectFileBody
19 +from app.connectors.velociraptor.schema.artifacts import FileCollectionBody
20 from app.connectors.velociraptor.schema.artifacts import OSPrefixEnum
21 from app.connectors.velociraptor.schema.artifacts import OSPrefixModel
22 from app.connectors.velociraptor.schema.artifacts import QuarantineBody
@@ -167,6 +168,59 @@ async def get_velociraptor_org(session: AsyncSession, hostname: str) -> str:
168 return agent.velociraptor_org
169
170
171 +def format_file_path_for_os(file_path: str, os_prefix: str) -> str:
172 + """
173 + Format the file path based on the operating system prefix.
174 +
175 + For Windows OS:
176 + - Prepends "Glob\n" to the file path
177 + - Escapes backslashes (single \ becomes \\\\)
178 + - Appends "\n" at the end
179 +
180 + For Linux/MacOS:
181 + - Prepends "Glob\n" to the file path
182 + - Appends "\n" at the end
183 + - Forward slashes are kept as-is
184 +
185 + Args:
186 + file_path (str): The original file path to format.
187 + os_prefix (str): The OS prefix (e.g., "Windows", "Linux", "MacOS", "Generic.Client").
188 +
189 + Returns:
190 + str: The formatted file path.
191 +
192 + Examples:
193 + >>> format_file_path_for_os("Users\\Administrator\\Downloads\\LICENSE.txt", "Windows")
194 + 'Glob\\nUsers\\\\\\\\Administrator\\\\\\\\Downloads\\\\\\\\LICENSE.txt\\n'
195 +
196 + >>> format_file_path_for_os("/home/user/document.txt", "Linux")
197 + 'Glob\\n/home/user/document.txt\\n'
198 +
199 + >>> format_file_path_for_os("/Users/user/document.txt", "MacOS")
200 + 'Glob\\n/Users/user/document.txt\\n'
201 + """
202 + logger.info(f"Formatting file path '{file_path}' for OS prefix '{os_prefix}'")
203 +
204 + # Check if the OS is Windows
205 + if os_prefix.lower() in ["windows", "windows."]:
206 + # Escape backslashes: single \ becomes \\\\
207 + # This is because we need double escaping: once for Python string, once for Velociraptor
208 + escaped_path = file_path.replace("\\", "\\\\")
209 +
210 + # Format with Glob prefix and newline suffix
211 + formatted_path = f"Glob\n{escaped_path}\n"
212 +
213 + logger.info(f"Formatted Windows path: '{formatted_path}'")
214 + return formatted_path
215 +
216 + # For Linux, MacOS, or other Unix-based systems
217 + # Also add Glob prefix and newline suffix, but keep forward slashes as-is
218 + formatted_path = f"Glob\n{file_path}\n"
219 +
220 + logger.info(f"Formatted {os_prefix} path: '{formatted_path}'")
221 + return formatted_path
222 +
223 +
224 async def update_agent_quarantine_status(
225 session: AsyncSession,
226 quarantine_body: QuarantineBody,
@@ -540,25 +594,101 @@ async def collect_file(collect_artifact_body: CollectFileBody, session: AsyncSes
594 CollectArtifactResponse: The response containing the collected file.
595 """
596 logger.info(f"Received request to collect artifact {collect_artifact_body}")
543 - # result = await get_all_artifacts_for_hostname(
544 - # collect_artifact_body.hostname,
545 - # session,
546 - # )
547 - # artifact_names = [artifact.name for artifact in result.artifacts]
548 -
549 - # if collect_artifact_body.artifact_name not in artifact_names:
550 - # raise HTTPException(
551 - # status_code=400,
552 - # detail=f"Artifact name {collect_artifact_body.artifact_name} does not apply for hostname {collect_artifact_body.hostname} or does not exist",
553 - # )
554 -
555 - # collect_artifact_body.velociraptor_id = await get_velociraptor_id(
556 - # session,
557 - # collect_artifact_body.hostname,
558 - # )
559 -
560 - # collect_artifact_body.velociraptor_org = await get_velociraptor_org(
561 - # session,
562 - # collect_artifact_body.hostname,
563 - # )
564 - return await run_file_collection(collect_artifact_body)
597 +
598 + collect_artifact_body.velociraptor_id = await get_velociraptor_id(
599 + session,
600 + collect_artifact_body.hostname,
601 + )
602 +
603 + collect_artifact_body.velociraptor_org = await get_velociraptor_org(
604 + session,
605 + collect_artifact_body.hostname,
606 + )
607 + return await run_file_collection(collect_artifact_body, session)
608 +
609 +
610 +# Add this new route after the existing collect_file route
611 +
612 +
613 +@velociraptor_artifacts_router.post(
614 + "/collect/file/agent/{agent_id}",
615 + response_model=CollectArtifactResponse,
616 + description="Collect a file from an agent using agent ID",
617 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
618 +)
619 +async def collect_file_by_agent_id(
620 + agent_id: str,
621 + collect_file_body: FileCollectionBody,
622 + session: AsyncSession = Depends(get_db),
623 +) -> CollectArtifactResponse:
624 + """
625 + Collects a file from an agent using the agent ID.
626 +
627 + Args:
628 + agent_id (str): The agent ID to collect the file from.
629 + collect_file_body (FileCollectionBody): The request body containing file path and root disk.
630 + session (AsyncSession): The database session.
631 +
632 + Returns:
633 + CollectArtifactResponse: The response containing the collection status.
634 +
635 + Raises:
636 + HTTPException: If the agent is not found or velociraptor details are not available.
637 + """
638 + logger.info(f"Received request to collect file for agent ID {agent_id}")
639 +
640 + # Query the agent from the database using agent_id
641 + result = await session.execute(select(Agents).filter(Agents.agent_id == agent_id))
642 + agent = result.scalars().first()
643 +
644 + if not agent:
645 + raise HTTPException(
646 + status_code=404,
647 + detail=f"Agent with ID {agent_id} not found",
648 + )
649 +
650 + # Verify hostname exists
651 + if not agent.hostname:
652 + raise HTTPException(
653 + status_code=404,
654 + detail=f"Hostname for agent {agent_id} is not available",
655 + )
656 +
657 + # Verify velociraptor_id exists and is valid
658 + if not agent.velociraptor_id or agent.velociraptor_id == "n/a":
659 + raise HTTPException(
660 + status_code=404,
661 + detail=f"Velociraptor ID for agent {agent_id} is not available",
662 + )
663 +
664 + # Verify velociraptor_org exists
665 + if not agent.velociraptor_org:
666 + raise HTTPException(
667 + status_code=404,
668 + detail=f"Velociraptor ORG for agent {agent_id} is not available",
669 + )
670 +
671 + # Collect the agent os
672 + os_prefix = get_os_prefix_from_os_name(os_name=agent.os.lower())
673 +
674 + logger.info(f"OS prefix for agent ID {agent_id} is {os_prefix}")
675 + # Format the file path based on the OS
676 + original_file_path = collect_file_body.file
677 + formatted_file_path = format_file_path_for_os(original_file_path, os_prefix)
678 +
679 + logger.info(f"Original file path: '{original_file_path}'")
680 + logger.info(f"Formatted file path: '{formatted_file_path}'")
681 +
682 + # Update the file path in the request body
683 + collect_file_body.file = formatted_file_path
684 + return await run_file_collection(
685 + CollectFileBody(
686 + hostname=agent.hostname,
687 + velociraptor_id=agent.velociraptor_id,
688 + velociraptor_org=agent.velociraptor_org,
689 + artifact_name="Generic.Collectors.File",
690 + file=collect_file_body.file,
691 + root_disk=collect_file_body.root_disk,
692 + ),
693 + session,
694 + )
backend/app/connectors/velociraptor/schema/artifacts.py
+30
@@ -178,6 +178,32 @@ class CollectFileBody(BaseBody):
178 return value
179
180
181 +class FileCollectionBody(BaseModel):
182 + file: str = Field(..., description="File to collect")
183 + root_disk: str = Field("C:", description="Root disk to collect from")
184 +
185 +
186 +# ! Windows Example ! #
187 +# {
188 +# "hostname": "WIN-HFOU106TD7K",
189 +# "velociraptor_id": "C.475df76785008b04",
190 +# "velociraptor_org": "root",
191 +# "artifact_name": "Generic.Collectors.File",
192 +# "file": "Glob\nUsers\\\\Administrator\\\\Downloads\\\\LICENSE.txt\n",
193 +# "root_disk": "C:"
194 +# }
195 +
196 +# ! Linux Example ! #
197 +# {
198 +# "hostname": "ASHWZHMA",
199 +# "velociraptor_id": "C.c4e8798fbab1d9c6",
200 +# "velociraptor_org": "root",
201 +# "artifact_name": "Generic.Collectors.File",
202 +# "file": "Glob\n/tmp/dir/test.txt\n",
203 +# "root_disk": "/" # Always use / for linux
204 +# }
205 +
206 +
207 class RunCommandBody(BaseBody):
208 command: Optional[str] = Field(None, description="Command to run")
209 artifact_name: CommandArtifactsEnum = Field(
@@ -204,6 +230,10 @@ class BaseResponse(BaseModel):
230
231
232 class CollectArtifactResponse(BaseResponse):
233 + file_info: Optional[Dict] = Field(
234 + None,
235 + description="Information about collected files",
236 + )
237 pass # If you have additional fields, you can define them here
238
239
backend/app/connectors/velociraptor/services/artifacts.py
+294 -73
@@ -1,6 +1,10 @@
1 +from datetime import datetime
2 +from typing import Optional
3 +
4 import httpx
5 from fastapi import HTTPException
6 from loguru import logger
7 +from sqlalchemy.ext.asyncio import AsyncSession
8
9 from app.connectors.velociraptor.schema.artifacts import ArtifactParametersResponse
10 from app.connectors.velociraptor.schema.artifacts import ArtifactReccomendationRequest
@@ -358,84 +362,301 @@ async def run_artifact_collection(
362
363 async def run_file_collection(
364 collect_artifact_body: CollectFileBody,
365 + session: AsyncSession, # Add this parameter
366 ) -> CollectArtifactResponse:
367 """
363 - Run an artifact collection on a client.
368 + Run an artifact collection on a client and store the result in MinIO.
369 + """
370 + from sqlalchemy import select
371
365 - Args:
366 - run_analyzer_body (RunAnalyzerBody): The body of the request.
372 + from app.db.universal_models import AgentDataStore
373 + from app.db.universal_models import Agents
374
368 - Returns:
369 - RunAnalyzerResponse: A dictionary containing the success status and a message.
375 + velociraptor_service = await UniversalService.create("Velociraptor")
376 +
377 + try:
378 + # Get agent details from database
379 + result = await session.execute(
380 + select(Agents).where(Agents.velociraptor_id == collect_artifact_body.velociraptor_id),
381 + )
382 + agent = result.scalars().first()
383 +
384 + if not agent:
385 + raise HTTPException(
386 + status_code=404,
387 + detail=f"Agent with velociraptor_id {collect_artifact_body.velociraptor_id} not found",
388 + )
389 +
390 + # Build the query with proper VQL syntax
391 + query = create_query(
392 + f"SELECT collect_client("
393 + f"org_id='{collect_artifact_body.velociraptor_org}', "
394 + f"client_id='{collect_artifact_body.velociraptor_id}', "
395 + f"artifacts=['{collect_artifact_body.artifact_name}'], "
396 + f"spec=dict(`{collect_artifact_body.artifact_name}`=dict("
397 + f"`collectionSpec`='{collect_artifact_body.file}', "
398 + f"`Root`='{collect_artifact_body.root_disk}'"
399 + f"))) "
400 + f"FROM scope()",
401 + )
402 +
403 + logger.info(f"Query: {query}")
404 + flow = velociraptor_service.execute_query(query, org_id=collect_artifact_body.velociraptor_org)
405 + logger.info(f"Successfully ran artifact collection on {flow}")
406 +
407 + # Check if results are available
408 + if not flow.get("results") or len(flow["results"]) == 0:
409 + logger.error("No results returned from query execution")
410 + raise HTTPException(
411 + status_code=500,
412 + detail="Query execution did not return any results",
413 + )
414 +
415 + # Extract flow_id from results
416 + result_dict = flow["results"][0]
417 + flow_id = None
418 +
419 + for key, value in result_dict.items():
420 + if isinstance(value, dict) and "flow_id" in value:
421 + flow_id = value["flow_id"]
422 + logger.debug(f"Found flow_id {flow_id} in key: {key}")
423 + break
424 +
425 + if not flow_id:
426 + logger.error(f"Could not find flow_id in results: {result_dict}")
427 + raise HTTPException(
428 + status_code=500,
429 + detail="Failed to extract flow ID from results",
430 + )
431 +
432 + logger.info(f"Extracted flow_id: {flow_id}")
433 +
434 + completed = velociraptor_service.watch_flow_completion(flow_id, org_id=collect_artifact_body.velociraptor_org)
435 + logger.info(f"Successfully watched flow completion on {completed}")
436 +
437 + results = velociraptor_service.read_collection_results(
438 + client_id=collect_artifact_body.velociraptor_id,
439 + flow_id=flow_id,
440 + org_id=collect_artifact_body.velociraptor_org,
441 + artifact=collect_artifact_body.artifact_name,
442 + )
443 +
444 + logger.info("Successfully read collection results")
445 +
446 + # Fetch the collected file from filestore and upload to MinIO
447 + logger.info("Fetching collected file from filestore and uploading to MinIO")
448 + file_data = await fetch_file_from_filestore(
449 + client_id=collect_artifact_body.velociraptor_id,
450 + flow_id=flow_id,
451 + org_id=collect_artifact_body.velociraptor_org,
452 + agent_id=agent.agent_id,
453 + )
454 +
455 + # Save metadata to database
456 + if file_data.get("success"):
457 + agent_data_store = AgentDataStore(
458 + agent_id=agent.agent_id,
459 + velociraptor_id=collect_artifact_body.velociraptor_id,
460 + artifact_name=collect_artifact_body.artifact_name,
461 + flow_id=flow_id,
462 + bucket_name=file_data["bucket_name"],
463 + object_key=file_data["object_key"],
464 + file_name=file_data["file_name"],
465 + content_type=file_data.get("content_type", "application/zip"),
466 + file_size=file_data["file_size"],
467 + file_hash=file_data["file_hash"],
468 + collection_time=datetime.utcnow(),
469 + status="completed",
470 + )
471 + session.add(agent_data_store)
472 + await session.commit()
473 + await session.refresh(agent_data_store)
474 +
475 + logger.info(f"Saved artifact metadata to database with ID {agent_data_store.id}")
476 +
477 + return CollectArtifactResponse(
478 + success=results["success"],
479 + message=results["message"],
480 + results=results["results"],
481 + file_info=file_data if file_data.get("success") else None,
482 + )
483 +
484 + except HTTPException as he:
485 + logger.error(f"HTTPException while running artifact collection: {he.detail}")
486 + raise he
487 + except Exception as err:
488 + logger.error(f"Failed to run artifact collection: {err}")
489 + raise HTTPException(
490 + status_code=500,
491 + detail=f"Failed to run artifact collection: {err}",
492 + )
493 +
494 +
495 +async def fetch_file_from_filestore(
496 + client_id: str,
497 + flow_id: str,
498 + org_id: str,
499 + agent_id: str, # Add this parameter
500 + hostname: Optional[str] = None,
501 + password: Optional[str] = None,
502 + format: str = "json",
503 + expand_sparse: bool = False,
504 +) -> dict:
505 + """
506 + Fetch a file from Velociraptor's filestore and save it to MinIO.
507 + First creates a download pack for the flow, then fetches and uploads to MinIO.
508 """
371 - # velociraptor_service = await UniversalService.create("Velociraptor")
372 - return CollectArtifactResponse(
373 - success=False,
374 - message="Not yet implemented",
375 - results=[],
376 - )
377 - # ! NOT YET READY ! #
378 - # try:
379 - # # ! Can specify org_id with org_id='OL680' ! #
380 - # query = create_query(
381 - # (
382 - # f"SELECT collect_client("
383 - # f"org_id='{collect_artifact_body.velociraptor_org}', "
384 - # f"client_id='{collect_artifact_body.velociraptor_id}', "
385 - # f"artifacts=['{collect_artifact_body.artifact_name}'], "
386 - # f"specs=[{{"
387 - # f" 'artifact': '{collect_artifact_body.artifact_name}',"
388 - # f" 'parameters': {{"
389 - # f" 'env': ["
390 - # f" {{'key': 'collectionSpec', 'value': '{collect_artifact_body.file}'}},"
391 - # f" {{'key': 'Root', 'value': '{collect_artifact_body.root_disk}'}}"
392 - # f" ]"
393 - # f" }}"
394 - # f"}}
395 - # ]"
396 - # f") "
397 - # f"FROM scope()",
398 - # ),
399 - # )
400 - # logger.info(f"Query: {query}")
401 - # flow = velociraptor_service.execute_query(query, org_id=collect_artifact_body.velociraptor_org)
402 - # logger.info(f"Successfully ran artifact collection on {flow}")
403 -
404 - # artifact_key = get_artifact_key(analyzer_body=collect_artifact_body)
405 -
406 - # flow_id = flow["results"][0][artifact_key]["flow_id"]
407 - # logger.info(f"Extracted flow_id: {flow_id}")
408 -
409 - # completed = velociraptor_service.watch_flow_completion(flow_id, org_id=collect_artifact_body.velociraptor_org)
410 - # logger.info(f"Successfully watched flow completion on {completed}")
411 -
412 - # results = velociraptor_service.read_collection_results(
413 - # client_id=collect_artifact_body.velociraptor_id,
414 - # flow_id=flow_id,
415 - # org_id=collect_artifact_body.velociraptor_org,
416 - # artifact=collect_artifact_body.artifact_name,
417 - # )
418 -
419 - # logger.info(f"Successfully read collection results on {results}")
420 -
421 - # return CollectArtifactResponse(
422 - # success=results["success"],
423 - # message=results["message"],
424 - # results=results["results"],
425 - # )
426 - # except HTTPException as he: # Catch HTTPException separately to propagate the original message
427 - # logger.error(
428 - # f"HTTPException while running artifact collection on {collect_artifact_body}: {he.detail}",
429 - # )
430 - # raise he
431 - # except Exception as err:
432 - # logger.error(
433 - # f"Failed to run artifact collection on {collect_artifact_body}: {err}",
434 - # )
435 - # raise HTTPException(
436 - # status_code=500,
437 - # detail=f"Failed to run artifact collection on {collect_artifact_body}: {err}",
438 - # )
509 + import base64
510 + import os
511 +
512 + from app.data_store.data_store_operations import upload_agent_artifact_file
513 +
514 + velociraptor_service = await UniversalService.create("Velociraptor")
515 +
516 + try:
517 + # If hostname is not provided, fetch it from client metadata
518 + if not hostname:
519 + logger.info(f"Fetching hostname for client {client_id}")
520 + query = create_query(
521 + f"SELECT os_info.hostname AS Hostname "
522 + f"FROM clients(client_id='{client_id}')",
523 + )
524 + client_info = velociraptor_service.execute_query(query, org_id=org_id)
525 +
526 + if client_info.get("results") and len(client_info["results"]) > 0:
527 + hostname = client_info["results"][0].get("Hostname", client_id)
528 + else:
529 + hostname = client_id
530 + logger.warning(f"Could not fetch hostname, using client_id: {client_id}")
531 +
532 + # Step 1: Create the flow download
533 + logger.info(f"Creating flow download for client {client_id}, flow {flow_id}")
534 +
535 + download_query_parts = [
536 + f"client_id='{client_id}'",
537 + f"flow_id='{flow_id}'",
538 + "wait=true",
539 + f"format='{format}'",
540 + f"expand_sparse={str(expand_sparse).lower()}",
541 + ]
542 +
543 + if password:
544 + download_query_parts.append(f"password='{password}'")
545 +
546 + zip_filename = f"{hostname}-{client_id}-{flow_id}.zip"
547 + download_query_parts.append(f"name='{hostname}-{client_id}-{flow_id}'")
548 +
549 + create_download_query = create_query(
550 + f"SELECT create_flow_download({', '.join(download_query_parts)}) "
551 + f"FROM scope()",
552 + )
553 +
554 + logger.info(f"Create download query: {create_download_query}")
555 + download_result = velociraptor_service.execute_query(create_download_query, org_id=org_id)
556 +
557 + if not download_result.get("success"):
558 + raise HTTPException(
559 + status_code=500,
560 + detail=f"Failed to create flow download: {download_result.get('message')}",
561 + )
562 +
563 + logger.info("Flow download created successfully")
564 +
565 + # Step 2: Fetch the file from Velociraptor
566 + vfs_path = f"downloads/{client_id}/{flow_id}/{zip_filename}"
567 + logger.info(f"Fetching file from VFS path: {vfs_path}")
568 +
569 + # Save to temporary location first
570 + temp_file_path = os.path.join(os.getcwd(), zip_filename)
571 + offset = 0
572 + chunk_size = 1024 * 1024 # 1MB chunks
573 + total_bytes = 0
574 +
575 + with open(temp_file_path, 'wb') as f:
576 + while True:
577 + query = create_query(
578 + f"SELECT base64encode(string=read_file("
579 + f"accessor='fs', "
580 + f"filename='/{vfs_path}', "
581 + f"offset={offset}, "
582 + f"length={chunk_size})) AS Data "
583 + f"FROM scope()",
584 + )
585 +
586 + result = velociraptor_service.execute_query(query, org_id=org_id)
587 +
588 + if not result.get("success"):
589 + raise HTTPException(
590 + status_code=500,
591 + detail=f"Failed to fetch file chunk: {result.get('message')}",
592 + )
593 +
594 + if not result.get("results") or len(result["results"]) == 0:
595 + break
596 +
597 + data = result["results"][0].get("Data")
598 + if not data:
599 + break
600 +
601 + try:
602 + decoded_data = base64.b64decode(data)
603 + if len(decoded_data) == 0:
604 + break
605 +
606 + f.write(decoded_data)
607 + chunk_bytes = len(decoded_data)
608 + total_bytes += chunk_bytes
609 + offset += chunk_bytes
610 +
611 + logger.debug(f"Fetched {chunk_bytes} bytes, total: {total_bytes}")
612 +
613 + if chunk_bytes < chunk_size:
614 + break
615 + except Exception as e:
616 + logger.error(f"Failed to decode chunk: {e}")
617 + break
618 +
619 + if total_bytes == 0:
620 + logger.warning(f"No file data found at path: {vfs_path}")
621 + if os.path.exists(temp_file_path):
622 + os.remove(temp_file_path)
623 + return {
624 + "success": False,
625 + "message": f"No file found at path: {vfs_path}",
626 + "file_path": None,
627 + "file_size": 0,
628 + }
629 +
630 + logger.info(f"Successfully fetched file of size {total_bytes} bytes")
631 +
632 + # Step 3: Upload to MinIO
633 + logger.info(f"Uploading artifact file to MinIO for agent {agent_id}")
634 + upload_result = await upload_agent_artifact_file(
635 + agent_id=agent_id,
636 + flow_id=flow_id,
637 + file_path=temp_file_path,
638 + file_name=zip_filename,
639 + )
640 +
641 + # Remove temporary file
642 + os.remove(temp_file_path)
643 + logger.info(f"Removed temporary file {temp_file_path}")
644 +
645 + return {
646 + "success": True,
647 + "message": "Successfully fetched file from Velociraptor and uploaded to MinIO",
648 + **upload_result,
649 + }
650 +
651 + except HTTPException as he:
652 + logger.error(f"HTTPException while fetching file: {he.detail}")
653 + raise he
654 + except Exception as err:
655 + logger.error(f"Failed to fetch file from filestore: {err}")
656 + raise HTTPException(
657 + status_code=500,
658 + detail=f"Failed to fetch file from filestore: {err}",
659 + )
660
661
662 async def run_remote_command(run_command_body: RunCommandBody) -> RunCommandResponse:
backend/app/customer_portal/routes/settings.py new
+137
@@ -0,0 +1,137 @@
1 +from datetime import datetime
2 +
3 +from fastapi import APIRouter
4 +from fastapi import Depends
5 +from fastapi import HTTPException
6 +from fastapi import status
7 +from loguru import logger
8 +from sqlalchemy import select
9 +from sqlalchemy.ext.asyncio import AsyncSession
10 +
11 +from app.auth.utils import AuthHandler
12 +from app.customer_portal.schema.settings import PortalSettingsData
13 +from app.customer_portal.schema.settings import PortalSettingsResponse
14 +from app.customer_portal.schema.settings import UpdatePortalSettingsRequest
15 +from app.customer_portal.schema.settings import UpdatePortalSettingsResponse
16 +from app.db.db_session import get_db
17 +from app.db.universal_models import CustomerPortalSettings
18 +
19 +customer_portal_settings_router = APIRouter()
20 +
21 +
22 +@customer_portal_settings_router.post(
23 + "/settings",
24 + response_model=UpdatePortalSettingsResponse,
25 + description="Update customer portal settings (logo and title). Set fields to null to restore defaults.",
26 + dependencies=[Depends(AuthHandler().require_any_scope("admin"))],
27 +)
28 +async def update_portal_settings(
29 + request: UpdatePortalSettingsRequest,
30 + session: AsyncSession = Depends(get_db),
31 + auth_handler: AuthHandler = Depends(AuthHandler().get_current_user),
32 +) -> UpdatePortalSettingsResponse:
33 + """
34 + Update customer portal settings including logo and title.
35 + Set any field to null to restore its default value.
36 + Requires authentication.
37 + """
38 + try:
39 + # Check if settings exist
40 + result = await session.execute(select(CustomerPortalSettings))
41 + settings = result.scalars().first()
42 +
43 + if not settings:
44 + # Create default settings if none exist
45 + settings = CustomerPortalSettings.create_default()
46 + session.add(settings)
47 +
48 + # Get default values
49 + defaults = CustomerPortalSettings.get_default_values()
50 +
51 + # Handle title: if explicitly set to null, restore default
52 + if request.title is None:
53 + settings.title = defaults["title"]
54 + else:
55 + settings.title = request.title
56 +
57 + # Handle logo_base64: if explicitly set to null, restore default
58 + if request.logo_base64 is None:
59 + settings.logo_base64 = defaults["logo_base64"]
60 + else:
61 + settings.logo_base64 = request.logo_base64
62 +
63 + # Handle logo_mime_type: if explicitly set to null, restore default
64 + if request.logo_mime_type is None:
65 + settings.logo_mime_type = defaults["logo_mime_type"]
66 + else:
67 + settings.logo_mime_type = request.logo_mime_type
68 +
69 + # Update metadata
70 + settings.updated_by = auth_handler.user_id if hasattr(auth_handler, "user_id") else None
71 + settings.updated_at = datetime.now()
72 +
73 + await session.commit()
74 + await session.refresh(settings)
75 +
76 + logger.info(
77 + f"Portal settings updated successfully by user {auth_handler.user_id if hasattr(auth_handler, 'user_id') else 'unknown'}",
78 + )
79 +
80 + return UpdatePortalSettingsResponse(
81 + success=True,
82 + message="Portal settings updated successfully",
83 + )
84 +
85 + except Exception as e:
86 + logger.error(f"Failed to update portal settings: {e}")
87 + await session.rollback()
88 + raise HTTPException(
89 + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
90 + detail=f"Failed to update portal settings: {str(e)}",
91 + )
92 +
93 +
94 +@customer_portal_settings_router.get(
95 + "/settings",
96 + response_model=PortalSettingsResponse,
97 + description="Get customer portal settings (public endpoint)",
98 +)
99 +async def get_portal_settings(
100 + session: AsyncSession = Depends(get_db),
101 +) -> PortalSettingsResponse:
102 + """
103 + Get customer portal settings including logo and title.
104 + This is a public endpoint (no authentication required).
105 + """
106 + try:
107 + # Get settings
108 + result = await session.execute(select(CustomerPortalSettings))
109 + settings = result.scalars().first()
110 +
111 + if not settings:
112 + # Create and return default settings
113 + settings = CustomerPortalSettings.create_default()
114 + session.add(settings)
115 + await session.commit()
116 + await session.refresh(settings)
117 +
118 + settings_data = PortalSettingsData(
119 + id=settings.id,
120 + title=settings.title,
121 + logo_base64=settings.logo_base64,
122 + logo_mime_type=settings.logo_mime_type,
123 + updated_at=settings.updated_at.isoformat(),
124 + )
125 +
126 + return PortalSettingsResponse(
127 + success=True,
128 + message="Portal settings retrieved successfully",
129 + settings=settings_data,
130 + )
131 +
132 + except Exception as e:
133 + logger.error(f"Failed to get portal settings: {e}")
134 + raise HTTPException(
135 + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
136 + detail=f"Failed to get portal settings: {str(e)}",
137 + )
backend/app/customer_portal/schema/settings.py new
+78
@@ -0,0 +1,78 @@
1 +import base64
2 +import re
3 +from typing import Optional
4 +
5 +from fastapi import HTTPException
6 +from pydantic import BaseModel
7 +from pydantic import Field
8 +from pydantic import validator
9 +
10 +
11 +class UpdatePortalSettingsRequest(BaseModel):
12 + title: Optional[str] = Field(None, max_length=255, description="Portal title. Set to null to restore default.")
13 + logo_base64: Optional[str] = Field(None, description="Base64 encoded logo image. Set to null to restore default.")
14 + logo_mime_type: Optional[str] = Field(None, max_length=50, description="MIME type of the logo. Set to null to restore default.")
15 +
16 + @validator("logo_base64")
17 + def validate_base64(cls, v):
18 + if v is None:
19 + return v
20 +
21 + # Remove data URL prefix if present
22 + if v.startswith("data:"):
23 + v = v.split(",", 1)[1] if "," in v else v
24 +
25 + # Check size (limit to 5MB base64 = ~3.75MB original)
26 + max_size = 5 * 1024 * 1024 # 5MB
27 + if len(v) > max_size:
28 + raise HTTPException(
29 + status_code=400,
30 + detail=f"Logo file too large. Maximum size is {max_size // (1024 * 1024)}MB (base64-encoded)",
31 + )
32 +
33 + # Validate base64 format
34 + if not re.match(r"^[A-Za-z0-9+/]*={0,2}$", v):
35 + raise HTTPException(status_code=400, detail="Invalid base64 encoded string for logo_base64")
36 +
37 + # Optional: Try to decode to verify it's valid base64
38 + try:
39 + base64.b64decode(v)
40 + except Exception:
41 + raise HTTPException(status_code=400, detail="Invalid base64 data - cannot decode")
42 +
43 + return v
44 +
45 + @validator("logo_mime_type")
46 + def validate_mime_type(cls, v):
47 + if v is None:
48 + return v
49 +
50 + allowed_types = ["image/png", "image/jpeg", "image/jpg", "image/gif", "image/svg+xml", "image/webp"]
51 + if v not in allowed_types:
52 + raise HTTPException(status_code=400, detail=f"Invalid MIME type. Allowed types are: {', '.join(allowed_types)}")
53 + return v
54 +
55 + class Config:
56 + json_schema_extra = {"example": {"title": "My Custom Portal", "logo_base64": "iVBORw0KGgoAAAANS...", "logo_mime_type": "image/png"}}
57 +
58 +
59 +class PortalSettingsData(BaseModel):
60 + id: int
61 + title: str
62 + logo_base64: Optional[str] = None
63 + logo_mime_type: Optional[str] = None
64 + updated_at: str
65 +
66 + class Config:
67 + from_attributes = True
68 +
69 +
70 +class PortalSettingsResponse(BaseModel):
71 + success: bool
72 + message: str
73 + settings: Optional[PortalSettingsData] = None
74 +
75 +
76 +class UpdatePortalSettingsResponse(BaseModel):
77 + success: bool
78 + message: str
backend/app/customer_provisioning/templates/linux_agent.conf
-1
@@ -148,7 +148,6 @@
148 <scan_on_start>yes</scan_on_start>
149 <packages>yes</packages>
150 <os>yes</os>
151 - <hotfixes>yes</hotfixes>
151 <ports all="no">yes</ports>
152 <processes>yes</processes>
153 </wodle>
backend/app/data_store/data_store_operations.py
+115
@@ -1,3 +1,4 @@
1 +import hashlib
2 import os
3 from typing import Optional
4
@@ -217,3 +218,117 @@ async def list_sysmon_configs() -> list:
218 except Exception as e:
219 logger.error(f"Error listing sysmon configs: returning empty list - {e}")
220 return []
221 +
222 +
223 +# ! Agent Data Store Operations ! #
224 +async def upload_agent_artifact_file(
225 + agent_id: str,
226 + flow_id: str,
227 + file_path: str,
228 + file_name: str,
229 +) -> dict:
230 + """
231 + Upload a Velociraptor artifact collection file to MinIO.
232 +
233 + Args:
234 + agent_id: The agent ID
235 + flow_id: The flow ID
236 + file_path: Local path to the file to upload
237 + file_name: Name of the file
238 +
239 + Returns:
240 + dict: Upload details including object_key, file_size, and file_hash
241 + """
242 + bucket_name = "velociraptor-artifacts"
243 + client = await create_session()
244 +
245 + # Create bucket if it doesn't exist
246 + await create_bucket_if_not_exists(bucket_name)
247 +
248 + # Calculate file hash
249 + sha256_hash = hashlib.sha256()
250 + with open(file_path, "rb") as f:
251 + for byte_block in iter(lambda: f.read(4096), b""):
252 + sha256_hash.update(byte_block)
253 + file_hash = sha256_hash.hexdigest()
254 +
255 + # Get file size
256 + file_size = os.path.getsize(file_path)
257 +
258 + # Construct object key: agent_id/flow_id/filename.zip
259 + object_key = f"{agent_id}/{flow_id}/{file_name}"
260 +
261 + logger.info(f"Uploading artifact file to {bucket_name}/{object_key}")
262 +
263 + # Upload to MinIO
264 + await client.fput_object(
265 + bucket_name=bucket_name,
266 + object_name=object_key,
267 + file_path=file_path,
268 + content_type="application/zip",
269 + )
270 +
271 + logger.info(f"Successfully uploaded {file_name} ({file_size} bytes) to MinIO")
272 +
273 + return {
274 + "bucket_name": bucket_name,
275 + "object_key": object_key,
276 + "file_name": file_name,
277 + "file_size": file_size,
278 + "file_hash": file_hash,
279 + }
280 +
281 +
282 +async def download_agent_artifact_file(agent_id: str, flow_id: str, file_name: str) -> bytes:
283 + """
284 + Download a Velociraptor artifact file from MinIO.
285 +
286 + Args:
287 + agent_id: The agent ID
288 + flow_id: The flow ID
289 + file_name: Name of the file
290 +
291 + Returns:
292 + bytes: File content
293 + """
294 + bucket_name = "velociraptor-artifacts"
295 + object_key = f"{agent_id}/{flow_id}/{file_name}"
296 +
297 + return await download_data_store(bucket_name, object_key)
298 +
299 +
300 +async def list_agent_artifact_files(agent_id: str, flow_id: Optional[str] = None) -> list:
301 + """
302 + List all artifact files for an agent, optionally filtered by flow_id.
303 +
304 + Args:
305 + agent_id: The agent ID
306 + flow_id: Optional flow ID to filter by
307 +
308 + Returns:
309 + list: List of object names
310 + """
311 + bucket_name = "velociraptor-artifacts"
312 + client = await create_session()
313 +
314 + prefix = f"{agent_id}/"
315 + if flow_id:
316 + prefix = f"{agent_id}/{flow_id}/"
317 +
318 + objects = await client.list_objects(bucket_name, prefix=prefix, recursive=True)
319 + return [obj.object_name for obj in objects]
320 +
321 +
322 +async def delete_agent_artifact_file(agent_id: str, flow_id: str, file_name: str) -> None:
323 + """
324 + Delete a Velociraptor artifact file from MinIO.
325 +
326 + Args:
327 + agent_id: The agent ID
328 + flow_id: The flow ID
329 + file_name: Name of the file
330 + """
331 + bucket_name = "velociraptor-artifacts"
332 + object_key = f"{agent_id}/{flow_id}/{file_name}"
333 +
334 + await delete_file(bucket_name, object_key)
backend/app/data_store/data_store_routes.py new
+248
@@ -0,0 +1,248 @@
1 +import io
2 +from typing import Optional
3 +
4 +from fastapi import APIRouter
5 +from fastapi import Depends
6 +from fastapi import HTTPException
7 +from fastapi import status
8 +from fastapi.responses import StreamingResponse
9 +from loguru import logger
10 +from sqlalchemy import desc
11 +from sqlalchemy import select
12 +from sqlalchemy.ext.asyncio import AsyncSession
13 +from sqlalchemy.orm import selectinload
14 +
15 +from app.auth.utils import AuthHandler
16 +from app.data_store.data_store_operations import delete_agent_artifact_file
17 +from app.data_store.data_store_operations import download_agent_artifact_file
18 +from app.data_store.data_store_schema import AgentDataStoreData
19 +from app.data_store.data_store_schema import AgentDataStoreListResponse
20 +from app.data_store.data_store_schema import AgentDataStoreResponse
21 +from app.db.db_session import get_db
22 +from app.db.universal_models import AgentDataStore
23 +
24 +agent_data_store_router = APIRouter()
25 +
26 +
27 +@agent_data_store_router.get(
28 + "/agent/{agent_id}/artifacts",
29 + response_model=AgentDataStoreListResponse,
30 + description="List all artifact files for an agent",
31 + dependencies=[Depends(AuthHandler().require_any_scope("admin", "analyst"))],
32 +)
33 +async def list_agent_artifacts(
34 + agent_id: str,
35 + flow_id: Optional[str] = None,
36 + session: AsyncSession = Depends(get_db),
37 +) -> AgentDataStoreListResponse:
38 + """
39 + List all artifact collection files for a specific agent.
40 + Optionally filter by flow_id.
41 + """
42 + try:
43 + query = select(AgentDataStore).where(AgentDataStore.agent_id == agent_id)
44 + query = query.options(selectinload(AgentDataStore.agent)) # Eager load agent
45 +
46 + if flow_id:
47 + query = query.where(AgentDataStore.flow_id == flow_id)
48 +
49 + query = query.order_by(desc(AgentDataStore.collection_time))
50 +
51 + result = await session.execute(query)
52 + artifacts = result.scalars().all()
53 +
54 + # Convert to response data, accessing customer_code via relationship
55 + artifact_data = []
56 + for artifact in artifacts:
57 + # Create a dict from the artifact
58 + artifact_dict = {
59 + "id": artifact.id,
60 + "agent_id": artifact.agent_id,
61 + "velociraptor_id": artifact.velociraptor_id,
62 + "customer_code": artifact.agent.customer_code if artifact.agent else None,
63 + "artifact_name": artifact.artifact_name,
64 + "flow_id": artifact.flow_id,
65 + "bucket_name": artifact.bucket_name,
66 + "object_key": artifact.object_key,
67 + "file_name": artifact.file_name,
68 + "content_type": artifact.content_type,
69 + "file_size": artifact.file_size,
70 + "file_hash": artifact.file_hash,
71 + "collection_time": artifact.collection_time,
72 + "uploaded_by": artifact.uploaded_by,
73 + "notes": artifact.notes,
74 + "status": artifact.status,
75 + }
76 + artifact_data.append(AgentDataStoreData(**artifact_dict))
77 +
78 + return AgentDataStoreListResponse(
79 + success=True,
80 + message=f"Found {len(artifact_data)} artifacts for agent {agent_id}",
81 + data=artifact_data,
82 + total=len(artifact_data),
83 + )
84 +
85 + except Exception as e:
86 + logger.error(f"Failed to list agent artifacts: {e}")
87 + raise HTTPException(
88 + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
89 + detail=f"Failed to list agent artifacts: {str(e)}",
90 + )
91 +
92 +
93 +@agent_data_store_router.get(
94 + "/agent/{agent_id}/artifacts/{artifact_id}",
95 + response_model=AgentDataStoreResponse,
96 + description="Get details of a specific artifact file",
97 + dependencies=[Depends(AuthHandler().require_any_scope("admin", "analyst"))],
98 +)
99 +async def get_agent_artifact_details(
100 + agent_id: str,
101 + artifact_id: int,
102 + session: AsyncSession = Depends(get_db),
103 +) -> AgentDataStoreResponse:
104 + """Get details of a specific artifact collection file."""
105 + try:
106 + query = select(AgentDataStore).where(AgentDataStore.id == artifact_id, AgentDataStore.agent_id == agent_id)
107 + query = query.options(selectinload(AgentDataStore.agent))
108 +
109 + result = await session.execute(query)
110 + artifact = result.scalars().first()
111 +
112 + if not artifact:
113 + raise HTTPException(
114 + status_code=status.HTTP_404_NOT_FOUND,
115 + detail=f"Artifact {artifact_id} not found for agent {agent_id}",
116 + )
117 +
118 + # Create response data with customer_code from relationship
119 + artifact_dict = {
120 + "id": artifact.id,
121 + "agent_id": artifact.agent_id,
122 + "velociraptor_id": artifact.velociraptor_id,
123 + "customer_code": artifact.agent.customer_code if artifact.agent else None,
124 + "artifact_name": artifact.artifact_name,
125 + "flow_id": artifact.flow_id,
126 + "bucket_name": artifact.bucket_name,
127 + "object_key": artifact.object_key,
128 + "file_name": artifact.file_name,
129 + "content_type": artifact.content_type,
130 + "file_size": artifact.file_size,
131 + "file_hash": artifact.file_hash,
132 + "collection_time": artifact.collection_time,
133 + "uploaded_by": artifact.uploaded_by,
134 + "notes": artifact.notes,
135 + "status": artifact.status,
136 + }
137 +
138 + return AgentDataStoreResponse(
139 + success=True,
140 + message="Artifact details retrieved successfully",
141 + data=AgentDataStoreData(**artifact_dict),
142 + )
143 +
144 + except HTTPException:
145 + raise
146 + except Exception as e:
147 + logger.error(f"Failed to get artifact details: {e}")
148 + raise HTTPException(
149 + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
150 + detail=f"Failed to get artifact details: {str(e)}",
151 + )
152 +
153 +
154 +@agent_data_store_router.get(
155 + "/agent/{agent_id}/artifacts/{artifact_id}/download",
156 + description="Download an artifact file",
157 + dependencies=[Depends(AuthHandler().require_any_scope("admin", "analyst"))],
158 +)
159 +async def download_agent_artifact(
160 + agent_id: str,
161 + artifact_id: int,
162 + session: AsyncSession = Depends(get_db),
163 +):
164 + """Download a specific artifact collection file."""
165 + try:
166 + result = await session.execute(select(AgentDataStore).where(AgentDataStore.id == artifact_id, AgentDataStore.agent_id == agent_id))
167 + artifact = result.scalars().first()
168 +
169 + if not artifact:
170 + raise HTTPException(
171 + status_code=status.HTTP_404_NOT_FOUND,
172 + detail=f"Artifact {artifact_id} not found for agent {agent_id}",
173 + )
174 +
175 + # Download from MinIO
176 + file_data = await download_agent_artifact_file(
177 + agent_id=agent_id,
178 + flow_id=artifact.flow_id,
179 + file_name=artifact.file_name,
180 + )
181 +
182 + return StreamingResponse(
183 + io.BytesIO(file_data),
184 + media_type=artifact.content_type,
185 + headers={
186 + "Content-Disposition": f"attachment; filename={artifact.file_name}",
187 + "Content-Length": str(artifact.file_size),
188 + },
189 + )
190 +
191 + except HTTPException:
192 + raise
193 + except Exception as e:
194 + logger.error(f"Failed to download artifact: {e}")
195 + raise HTTPException(
196 + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
197 + detail=f"Failed to download artifact: {str(e)}",
198 + )
199 +
200 +
201 +@agent_data_store_router.delete(
202 + "/agent/{agent_id}/artifacts/{artifact_id}",
203 + description="Delete an artifact file",
204 + dependencies=[Depends(AuthHandler().require_any_scope("admin"))],
205 +)
206 +async def delete_agent_artifact(
207 + agent_id: str,
208 + artifact_id: int,
209 + session: AsyncSession = Depends(get_db),
210 +):
211 + """Delete a specific artifact collection file."""
212 + try:
213 + result = await session.execute(select(AgentDataStore).where(AgentDataStore.id == artifact_id, AgentDataStore.agent_id == agent_id))
214 + artifact = result.scalars().first()
215 +
216 + if not artifact:
217 + raise HTTPException(
218 + status_code=status.HTTP_404_NOT_FOUND,
219 + detail=f"Artifact {artifact_id} not found for agent {agent_id}",
220 + )
221 +
222 + # Delete from MinIO
223 + await delete_agent_artifact_file(
224 + agent_id=agent_id,
225 + flow_id=artifact.flow_id,
226 + file_name=artifact.file_name,
227 + )
228 +
229 + # Delete from database
230 + await session.delete(artifact)
231 + await session.commit()
232 +
233 + logger.info(f"Deleted artifact {artifact_id} for agent {agent_id}")
234 +
235 + return {
236 + "success": True,
237 + "message": f"Artifact {artifact_id} deleted successfully",
238 + }
239 +
240 + except HTTPException:
241 + raise
242 + except Exception as e:
243 + logger.error(f"Failed to delete artifact: {e}")
244 + await session.rollback()
245 + raise HTTPException(
246 + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
247 + detail=f"Failed to delete artifact: {str(e)}",
248 + )
backend/app/data_store/data_store_schema.py
+50
@@ -24,3 +24,53 @@ class CaseReportTemplateDataStoreCreation(BaseModel):
24 content_type: Optional[str] = Field(max_length=100, default=None)
25 upload_time: datetime = Field(default_factory=datetime.utcnow)
26 file_hash: str = Field(max_length=128)
27 +
28 +
29 +class AgentDataStoreCreation(BaseModel):
30 + agent_id: str
31 + velociraptor_id: str
32 + artifact_name: str
33 + flow_id: str
34 + bucket_name: str = "velociraptor-artifacts"
35 + file_name: str
36 + content_type: str = "application/zip"
37 + file_size: int
38 + file_hash: str
39 + uploaded_by: Optional[int] = None
40 + notes: Optional[str] = None
41 + status: str = "completed"
42 +
43 +
44 +class AgentDataStoreData(BaseModel):
45 + id: int
46 + agent_id: str
47 + velociraptor_id: str
48 + customer_code: Optional[str] = None # Made optional since it comes from relationship
49 + artifact_name: str
50 + flow_id: str
51 + bucket_name: str
52 + object_key: str
53 + file_name: str
54 + content_type: str
55 + file_size: int
56 + file_hash: str
57 + collection_time: datetime
58 + uploaded_by: Optional[int] = None
59 + notes: Optional[str] = None
60 + status: str
61 +
62 + class Config:
63 + from_attributes = True
64 +
65 +
66 +class AgentDataStoreResponse(BaseModel):
67 + success: bool
68 + message: str
69 + data: Optional[AgentDataStoreData] = None # Removed the string quotes here
70 +
71 +
72 +class AgentDataStoreListResponse(BaseModel):
73 + success: bool
74 + message: str
75 + data: list[AgentDataStoreData] = []
76 + total: int = 0
backend/app/data_store/data_store_setup.py
+1
@@ -16,3 +16,4 @@ async def create_buckets() -> None:
16 await create_bucket_if_not_exists("copilot-cases")
17 await create_bucket_if_not_exists("copilot-case-report-templates")
18 await create_bucket_if_not_exists("sysmon-configs")
19 + await create_bucket_if_not_exists("velociraptor-artifacts")
backend/app/db/universal_models.py
+103
@@ -5,6 +5,8 @@ from loguru import logger
5 from sqlalchemy import Column
6 from sqlalchemy import Float
7 from sqlalchemy import LargeBinary
8 +from sqlalchemy import Text
9 +from sqlalchemy.dialects.mysql import LONGTEXT
10 from sqlmodel import Field
11 from sqlmodel import Relationship
12 from sqlmodel import SQLModel
@@ -107,6 +109,7 @@ class Agents(SQLModel, table=True):
109
110 customer: Optional[Customers] = Relationship(back_populates="agents")
111 vulnerabilities: Optional[list["AgentVulnerabilities"]] = Relationship(back_populates="agent")
112 + data_store: list["AgentDataStore"] = Relationship(back_populates="agent")
113
114 @classmethod
115 def create_from_model(cls, wazuh_agent, velociraptor_agent, customer_code):
@@ -218,6 +221,41 @@ class Agents(SQLModel, table=True):
221 self.velociraptor_org = velociraptor_agent.client_org if velociraptor_agent and velociraptor_agent.client_org else None
222
223
224 +class AgentDataStore(SQLModel, table=True):
225 + __tablename__ = "agent_datastore"
226 +
227 + id: Optional[int] = Field(primary_key=True)
228 +
229 + # Agent information
230 + agent_id: str = Field(foreign_key="agents.agent_id", max_length=256, index=True, nullable=False)
231 + velociraptor_id: str = Field(max_length=256, nullable=False)
232 + # Removed customer_code - access via agent.customer_code relationship
233 +
234 + # Artifact collection details
235 + artifact_name: str = Field(max_length=255, nullable=False, index=True)
236 + flow_id: str = Field(max_length=255, nullable=False, index=True)
237 + collection_time: datetime = Field(default_factory=datetime.utcnow, index=True)
238 +
239 + # MinIO storage details
240 + bucket_name: str = Field(max_length=255, nullable=False, default="velociraptor-artifacts")
241 + object_key: str = Field(max_length=1024, nullable=False) # Path: agent_id/flow_id/filename.zip
242 + file_name: str = Field(max_length=255, nullable=False) # Original file name
243 + content_type: str = Field(max_length=100, default="application/zip")
244 + file_size: int = Field(nullable=False) # File size in bytes
245 + file_hash: str = Field(max_length=128, nullable=False) # SHA-256 hash
246 +
247 + # Metadata
248 + uploaded_by: Optional[int] = Field(default=None) # User ID who initiated the collection
249 + notes: Optional[str] = Field(sa_column=Column(Text), nullable=True)
250 +
251 + # Status tracking
252 + status: str = Field(max_length=50, default="completed", index=True) # completed, failed, processing
253 + error_message: Optional[str] = Field(sa_column=Column(Text), nullable=True)
254 +
255 + # Relationship to Agents table
256 + agent: Optional["Agents"] = Relationship(back_populates="data_store")
257 +
258 +
259 class LogEntry(SQLModel, table=True):
260 __tablename__ = "log_entries"
261 id: Optional[int] = Field(primary_key=True)
@@ -321,3 +359,68 @@ class AgentVulnerabilities(SQLModel, table=True):
359 agent_id=agent_id,
360 customer_code=customer_code,
361 )
362 +
363 +
364 +class CustomerPortalSettings(SQLModel, table=True):
365 + __tablename__ = "customer_portal_settings"
366 +
367 + id: Optional[int] = Field(primary_key=True)
368 + title: str = Field(max_length=255, default="CoPilot")
369 + logo_base64: Optional[str] = Field(default=None, sa_column=Column(LONGTEXT)) # Use TEXT column for large base64 data
370 + logo_mime_type: Optional[str] = Field(default=None, max_length=50) # e.g., "image/png", "image/jpeg"
371 + updated_at: datetime = Field(default_factory=datetime.utcnow)
372 + updated_by: Optional[int] = Field(default=None) # User ID who last updated
373 +
374 + def update_from_request(
375 + self,
376 + title: Optional[str] = None,
377 + logo_base64: Optional[str] = None,
378 + logo_mime_type: Optional[str] = None,
379 + user_id: Optional[int] = None,
380 + ) -> None:
381 + """
382 + Update settings from request data.
383 + If a field is explicitly None, restore it to default value.
384 + """
385 + # Get default values for restoration
386 + defaults = self.get_default_values()
387 +
388 + # Update title - if None is passed, restore to default
389 + if title is not None:
390 + self.title = title
391 + elif title is None and hasattr(self, "_explicit_none_title"):
392 + self.title = defaults["title"]
393 +
394 + # Update logo_base64 - if None is passed, restore to default
395 + if logo_base64 is not None:
396 + self.logo_base64 = logo_base64
397 + elif logo_base64 is None and hasattr(self, "_explicit_none_logo"):
398 + self.logo_base64 = defaults["logo_base64"]
399 +
400 + # Update logo_mime_type - if None is passed, restore to default
401 + if logo_mime_type is not None:
402 + self.logo_mime_type = logo_mime_type
403 + elif logo_mime_type is None and hasattr(self, "_explicit_none_mime"):
404 + self.logo_mime_type = defaults["logo_mime_type"]
405 +
406 + self.updated_by = user_id
407 + self.updated_at = datetime.now()
408 +
409 + @staticmethod
410 + def get_default_values() -> dict:
411 + """Get default values for restoration."""
412 + return {
413 + "title": "CoPilot",
414 + "logo_base64": None,
415 + "logo_mime_type": None,
416 + }
417 +
418 + @classmethod
419 + def create_default(cls) -> "CustomerPortalSettings":
420 + """Create default settings."""
421 + defaults = cls.get_default_values()
422 + return cls(
423 + title=defaults["title"],
424 + logo_base64=defaults["logo_base64"],
425 + logo_mime_type=defaults["logo_mime_type"],
426 + )
backend/app/incidents/models.py
+2
@@ -145,6 +145,7 @@ class CustomerCodeFieldName(SQLModel, table=True):
145 source: str = Field(max_length=50, nullable=False)
146 field_name: str = Field(max_length=100, nullable=False)
147
148 +
149 class CaseComment(SQLModel, table=True):
150 __tablename__ = "incident_management_case_comment"
151 id: Optional[int] = Field(default=None, primary_key=True)
@@ -155,6 +156,7 @@ class CaseComment(SQLModel, table=True):
156
157 case: "Case" = Relationship(back_populates="comments")
158
159 +
160 class Case(SQLModel, table=True):
161 __tablename__ = "incident_management_case"
162 id: Optional[int] = Field(default=None, primary_key=True)
backend/app/incidents/schema/db_operations.py
+11 -10
@@ -322,12 +322,13 @@ class LinkedCaseCreate(BaseModel):
322 assigned_to: Optional[str] = None
323 id: int
324
325 - @validator('case_creation_time', pre=True)
325 + @validator("case_creation_time", pre=True)
326 def format_case_creation_time(cls, v):
327 if isinstance(v, datetime):
328 - return v.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
328 + return v.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
329 return v
330
331 +
332 class CaseCreateFromAlert(BaseModel):
333 alert_id: int
334
@@ -385,10 +386,10 @@ class CommentBase(BaseModel):
386 comment: str
387 created_at: str
388
388 - @validator('created_at', pre=True)
389 + @validator("created_at", pre=True)
390 def format_created_at(cls, v):
391 if isinstance(v, datetime):
391 - return v.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
392 + return v.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
393 return v
394
395
@@ -399,10 +400,10 @@ class CaseCommentBase(BaseModel):
400 comment: str
401 created_at: str
402
402 - @validator('created_at', pre=True)
403 + @validator("created_at", pre=True)
404 def format_created_at(cls, v):
405 if isinstance(v, datetime):
405 - return v.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
406 + return v.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
407 return v
408
409
@@ -442,10 +443,10 @@ class AlertOut(BaseModel):
443 linked_cases: List[LinkedCaseCreate] = []
444 iocs: List[IoCBase] = []
445
445 - @validator('alert_creation_time', 'time_closed', pre=True)
446 + @validator("alert_creation_time", "time_closed", pre=True)
447 def format_datetime(cls, v):
448 if isinstance(v, datetime):
448 - return v.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
449 + return v.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
450 return v
451
452
@@ -473,10 +474,10 @@ class CaseOut(BaseModel):
474 escalated: bool = False
475 comments: List[CaseCommentBase] = []
476
476 - @validator('case_creation_time', pre=True)
477 + @validator("case_creation_time", pre=True)
478 def format_case_creation_time(cls, v):
479 if isinstance(v, datetime):
479 - return v.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
480 + return v.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
481 return v
482
483
backend/app/integrations/copilot_mcp/routes/copilot_mcp.py
+5 -5
@@ -30,7 +30,7 @@ auth_handler = AuthHandler()
30 "/servers",
31 response_model=AvailableMCPServersResponse,
32 description="Get list of available MCP servers",
33 - dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
33 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
34 )
35 async def get_available_mcp_servers() -> AvailableMCPServersResponse:
36 """
@@ -66,7 +66,7 @@ async def get_available_mcp_servers() -> AvailableMCPServersResponse:
66 "/servers/{mcp_server}",
67 response_model=dict,
68 description="Get detailed information about a specific MCP server",
69 - dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
69 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
70 )
71 async def get_mcp_server_details(mcp_server: MCPServerType) -> dict:
72 """
@@ -107,7 +107,7 @@ async def get_mcp_server_details(mcp_server: MCPServerType) -> dict:
107 "/example-questions",
108 response_model=ExampleQuestionsResponse,
109 description="Get example questions for a specific MCP server",
110 - dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
110 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
111 )
112 async def get_example_questions(
113 mcp_server: MCPServerType = Query(..., description="MCP server to get example questions for"),
@@ -160,7 +160,7 @@ async def get_example_questions(
160 @copilot_mcp_router.get(
161 "/categories",
162 description="Get available question categories for a specific MCP server",
163 - dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
163 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
164 )
165 async def get_question_categories(mcp_server: MCPServerType = Query(..., description="MCP server to get categories for")) -> dict:
166 """
@@ -199,7 +199,7 @@ async def get_question_categories(mcp_server: MCPServerType = Query(..., descrip
199 @copilot_mcp_router.post(
200 "/query",
201 description="Process a query to the appropriate MCP server",
202 - dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
202 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
203 )
204 async def query_mcp(request: MCPQueryRequest, session: AsyncSession = Depends(get_db)) -> MCPQueryResponse:
205 """
backend/app/routers/customer_portal.py new
+13
@@ -0,0 +1,13 @@
1 +from fastapi import APIRouter
2 +
3 +from app.customer_portal.routes.settings import customer_portal_settings_router
4 +
5 +# Instantiate the APIRouter
6 +router = APIRouter()
7 +
8 +# Include the customer portal settings routes
9 +router.include_router(
10 + customer_portal_settings_router,
11 + prefix="/customer_portal",
12 + tags=["Customer Portal Settings"],
13 +)
backend/app/routers/data_store.py new
+13
@@ -0,0 +1,13 @@
1 +from fastapi import APIRouter
2 +
3 +from app.data_store.data_store_routes import agent_data_store_router
4 +
5 +# Instantiate the APIRouter
6 +router = APIRouter()
7 +
8 +# Include the customer portal settings routes
9 +router.include_router(
10 + agent_data_store_router,
11 + prefix="/agent_data_store",
12 + tags=["Agent Data Store"],
13 +)
backend/copilot.py
+4
@@ -42,9 +42,11 @@ from app.routers import copilot_action
42 from app.routers import copilot_mcp
43 from app.routers import cortex
44 from app.routers import crowdstrike
45 +from app.routers import customer_portal
46 from app.routers import customer_provisioning
47 from app.routers import customers
48 from app.routers import darktrace
49 +from app.routers import data_store
50 from app.routers import defenderforendpoint
51 from app.routers import dfir_iris
52 from app.routers import dnstwist
@@ -153,6 +155,8 @@ api_router.include_router(scheduler.router)
155 api_router.include_router(monitoring_alert.router)
156 api_router.include_router(sap_siem.router)
157 api_router.include_router(stack_provisioning.router)
158 +api_router.include_router(data_store.router)
159 +api_router.include_router(customer_portal.router)
160 api_router.include_router(active_response.router)
161 api_router.include_router(huntress.router)
162 api_router.include_router(license.router)
customer_portal/.dockerignore new
+15
@@ -0,0 +1,15 @@
1 +node_modules
2 +dist
3 +.git
4 +.env.local
5 +.env.*.local
6 +*.log
7 +.DS_Store
8 +coverage
9 +.vscode
10 +.idea
11 +*.swp
12 +*.swo
13 +*~
14 +.npm
15 +.eslintcache
customer_portal/Dockerfile new
+37
@@ -0,0 +1,37 @@
1 +# Build stage
2 +FROM node:20-alpine AS build
3 +
4 +WORKDIR /app
5 +
6 +# Copy package files
7 +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
8 +
9 +# Install pnpm
10 +RUN npm install -g pnpm
11 +
12 +# Install dependencies
13 +RUN pnpm install --frozen-lockfile
14 +
15 +# Copy source files
16 +COPY . .
17 +
18 +# Build the application
19 +RUN pnpm run build
20 +
21 +# Production stage
22 +FROM nginx:alpine
23 +
24 +# Copy custom nginx config
25 +COPY nginx.conf /etc/nginx/nginx.conf
26 +
27 +# Copy built files from build stage
28 +COPY --from=build /app/dist /usr/share/nginx/html
29 +
30 +# Copy docker entrypoint script if needed for SSL
31 +COPY build/docker-entrypoint.d/90-copilot-ssl.sh /docker-entrypoint.d/ 2>/dev/null || true
32 +
33 +# Expose port 80
34 +EXPOSE 80
35 +
36 +# Start nginx
37 +CMD ["nginx", "-g", "daemon off;"]
customer_portal/nginx.conf new
+88
@@ -0,0 +1,88 @@
1 +worker_processes auto;
2 +error_log /var/log/nginx/error.log warn;
3 +pid /var/run/nginx.pid;
4 +
5 +events {
6 + worker_connections 1024;
7 +}
8 +
9 +http {
10 + include /etc/nginx/mime.types;
11 + default_type application/octet-stream;
12 +
13 + log_format main '$remote_addr - $remote_user [$time_local] "$request" '
14 + '$status $body_bytes_sent "$http_referer" '
15 + '"$http_user_agent" "$http_x_forwarded_for"';
16 +
17 + access_log /var/log/nginx/access.log main;
18 +
19 + sendfile on;
20 + tcp_nopush on;
21 + tcp_nodelay on;
22 + keepalive_timeout 65;
23 + types_hash_max_size 2048;
24 + client_max_body_size 100M;
25 +
26 + gzip on;
27 + gzip_vary on;
28 + gzip_proxied any;
29 + gzip_comp_level 6;
30 + gzip_types text/plain text/css text/xml text/javascript
31 + application/json application/javascript application/xml+rss
32 + application/rss+xml font/truetype font/opentype
33 + application/vnd.ms-fontobject image/svg+xml;
34 +
35 + server {
36 + listen 80;
37 + server_name _;
38 + root /usr/share/nginx/html;
39 + index index.html;
40 +
41 + # Security headers
42 + add_header X-Frame-Options "SAMEORIGIN" always;
43 + add_header X-Content-Type-Options "nosniff" always;
44 + add_header X-XSS-Protection "1; mode=block" always;
45 + add_header Referrer-Policy "no-referrer-when-downgrade" always;
46 +
47 + # Cache static assets
48 + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
49 + expires 1y;
50 + add_header Cache-Control "public, immutable";
51 + }
52 +
53 + # Handle SPA routing - serve index.html for all routes
54 + location / {
55 + try_files $uri $uri/ /index.html;
56 + }
57 +
58 + # API proxy (adjust backend URL as needed)
59 + location /api {
60 + proxy_pass http://copilot-backend:5000;
61 + proxy_http_version 1.1;
62 + proxy_set_header Upgrade $http_upgrade;
63 + proxy_set_header Connection 'upgrade';
64 + proxy_set_header Host $host;
65 + proxy_set_header X-Real-IP $remote_addr;
66 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
67 + proxy_set_header X-Forwarded-Proto $scheme;
68 + proxy_cache_bypass $http_upgrade;
69 + proxy_read_timeout 300;
70 + proxy_connect_timeout 300;
71 + proxy_send_timeout 300;
72 + }
73 +
74 + # Health check endpoint
75 + location /health {
76 + access_log off;
77 + return 200 "healthy\n";
78 + add_header Content-Type text/plain;
79 + }
80 +
81 + # Error pages
82 + error_page 404 /index.html;
83 + error_page 500 502 503 504 /50x.html;
84 + location = /50x.html {
85 + root /usr/share/nginx/html;
86 + }
87 + }
88 +}
customer_portal/package.json
+27 -27
@@ -23,40 +23,40 @@
23 },
24 "dependencies": {
25 "@ajoelp/json-to-formdata": "^1.5.0",
26 - "@vueuse/core": "^13.6.0",
27 - "axios": "^1.11.0",
28 - "dayjs": "^1.11.13",
29 - "jose": "^6.0.12",
26 + "@vueuse/core": "^14.1.0",
27 + "axios": "^1.13.2",
28 + "dayjs": "^1.11.19",
29 + "jose": "^6.1.2",
30 "lodash": "^4.17.21",
31 "mitt": "^3.0.1",
32 - "naive-ui": "^2.42.0",
33 - "nanoid": "^5.1.5",
34 - "pinia": "^3.0.3",
35 - "pinia-plugin-persistedstate": "^4.4.1",
32 + "naive-ui": "^2.43.2",
33 + "nanoid": "^5.1.6",
34 + "pinia": "^3.0.4",
35 + "pinia-plugin-persistedstate": "^4.7.1",
36 "secure-ls": "^2.0.0",
37 - "vue": "^3.5.18",
38 - "vue-router": "^4.5.1"
37 + "vue": "^3.5.25",
38 + "vue-router": "^4.6.3"
39 },
40 "devDependencies": {
41 - "@antfu/eslint-config": "^5.0.0",
41 + "@antfu/eslint-config": "^6.2.0",
42 "@iconify/vue": "^5.0.0",
43 - "@tailwindcss/vite": "^4.1.11",
44 - "@tsconfig/node20": "^20.1.6",
45 - "@types/lodash": "^4.17.20",
46 - "@types/node": "^24.1.0",
47 - "@vitejs/plugin-vue": "^6.0.1",
43 + "@tailwindcss/vite": "^4.1.17",
44 + "@tsconfig/node20": "^20.1.8",
45 + "@types/lodash": "^4.17.21",
46 + "@types/node": "^24.10.1",
47 + "@vitejs/plugin-vue": "^6.0.2",
48 "@vue/tsconfig": "^0.7.0",
49 - "concurrently": "^8.2.2",
50 - "eslint": "^9.32.0",
49 + "concurrently": "^9.2.1",
50 + "eslint": "^9.39.1",
51 "npm-run-all2": "^8.0.4",
52 - "prettier": "^3.6.2",
53 - "prettier-plugin-tailwindcss": "^0.6.14",
54 - "sass": "^1.89.2",
55 - "tailwindcss": "^4.1.11",
56 - "typescript": "~5.8.3",
57 - "vite": "^7.0.6",
58 - "vite-plugin-vue-devtools": "^8.0.0",
52 + "prettier": "^3.7.3",
53 + "prettier-plugin-tailwindcss": "^0.7.1",
54 + "sass": "^1.94.2",
55 + "tailwindcss": "^4.1.17",
56 + "typescript": "~5.9.3",
57 + "vite": "^7.2.4",
58 + "vite-plugin-vue-devtools": "^8.0.5",
59 "vite-svg-loader": "^5.1.0",
60 - "vue-tsc": "^3.0.4"
60 + "vue-tsc": "^3.1.5"
61 }
62 -}
62 +}
\ No newline at end of file
customer_portal/pnpm-lock.yaml
+1240 -983
@@ -12,17 +12,20 @@ importers:
12 specifier: ^1.5.0
13 version: 1.5.0
14 '@vueuse/core':
15 - specifier: ^13.6.0
16 - version: 13.9.0(vue@3.5.21(typescript@5.8.3))
15 + specifier: ^14.1.0
16 + version: 14.1.0(vue@3.5.25(typescript@5.9.3))
17 axios:
18 - specifier: ^1.11.0
19 - version: 1.12.2
18 + specifier: ^1.13.2
19 + version: 1.13.2
20 dayjs:
21 - specifier: ^1.11.13
22 - version: 1.11.18
21 + specifier: ^1.11.19
22 + version: 1.11.19
23 + icojs:
24 + specifier: ^0.20.1
25 + version: 0.20.1(@jimp/custom@0.22.12)
26 jose:
24 - specifier: ^6.0.12
25 - version: 6.1.0
27 + specifier: ^6.1.2
28 + version: 6.1.2
29 lodash:
30 specifier: ^4.17.21
31 version: 4.17.21
@@ -30,99 +33,99 @@ importers:
33 specifier: ^3.0.1
34 version: 3.0.1
35 naive-ui:
33 - specifier: ^2.42.0
34 - version: 2.43.1(vue@3.5.21(typescript@5.8.3))
36 + specifier: ^2.43.2
37 + version: 2.43.2(vue@3.5.25(typescript@5.9.3))
38 nanoid:
36 - specifier: ^5.1.5
37 - version: 5.1.5
39 + specifier: ^5.1.6
40 + version: 5.1.6
41 pinia:
39 - specifier: ^3.0.3
40 - version: 3.0.3(typescript@5.8.3)(vue@3.5.21(typescript@5.8.3))
42 + specifier: ^3.0.4
43 + version: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
44 pinia-plugin-persistedstate:
42 - specifier: ^4.4.1
43 - version: 4.5.0(pinia@3.0.3(typescript@5.8.3)(vue@3.5.21(typescript@5.8.3)))
45 + specifier: ^4.7.1
46 + version: 4.7.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)))
47 secure-ls:
48 specifier: ^2.0.0
49 version: 2.0.0
50 vue:
48 - specifier: ^3.5.18
49 - version: 3.5.21(typescript@5.8.3)
51 + specifier: ^3.5.25
52 + version: 3.5.25(typescript@5.9.3)
53 vue-router:
51 - specifier: ^4.5.1
52 - version: 4.5.1(vue@3.5.21(typescript@5.8.3))
54 + specifier: ^4.6.3
55 + version: 4.6.3(vue@3.5.25(typescript@5.9.3))
56 devDependencies:
57 '@antfu/eslint-config':
55 - specifier: ^5.0.0
56 - version: 5.3.0(@vue/compiler-sfc@3.5.21)(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)
58 + specifier: ^6.2.0
59 + version: 6.2.0(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
60 '@iconify/vue':
61 specifier: ^5.0.0
59 - version: 5.0.0(vue@3.5.21(typescript@5.8.3))
62 + version: 5.0.0(vue@3.5.25(typescript@5.9.3))
63 '@tailwindcss/vite':
61 - specifier: ^4.1.11
62 - version: 4.1.13(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1))
64 + specifier: ^4.1.17
65 + version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))
66 '@tsconfig/node20':
64 - specifier: ^20.1.6
65 - version: 20.1.6
67 + specifier: ^20.1.8
68 + version: 20.1.8
69 '@types/lodash':
67 - specifier: ^4.17.20
68 - version: 4.17.20
70 + specifier: ^4.17.21
71 + version: 4.17.21
72 '@types/node':
70 - specifier: ^24.1.0
71 - version: 24.5.0
73 + specifier: ^24.10.1
74 + version: 24.10.1
75 '@vitejs/plugin-vue':
73 - specifier: ^6.0.1
74 - version: 6.0.1(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1))(vue@3.5.21(typescript@5.8.3))
76 + specifier: ^6.0.2
77 + version: 6.0.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))
78 '@vue/tsconfig':
79 specifier: ^0.7.0
77 - version: 0.7.0(typescript@5.8.3)(vue@3.5.21(typescript@5.8.3))
80 + version: 0.7.0(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
81 concurrently:
79 - specifier: ^8.2.2
80 - version: 8.2.2
82 + specifier: ^9.2.1
83 + version: 9.2.1
84 eslint:
82 - specifier: ^9.32.0
83 - version: 9.35.0(jiti@2.5.1)
85 + specifier: ^9.39.1
86 + version: 9.39.1(jiti@2.6.1)
87 npm-run-all2:
88 specifier: ^8.0.4
89 version: 8.0.4
90 prettier:
88 - specifier: ^3.6.2
89 - version: 3.6.2
91 + specifier: ^3.7.3
92 + version: 3.7.3
93 prettier-plugin-tailwindcss:
91 - specifier: ^0.6.14
92 - version: 0.6.14(prettier@3.6.2)
94 + specifier: ^0.7.1
95 + version: 0.7.1(prettier@3.7.3)
96 sass:
94 - specifier: ^1.89.2
95 - version: 1.92.1
97 + specifier: ^1.94.2
98 + version: 1.94.2
99 tailwindcss:
97 - specifier: ^4.1.11
98 - version: 4.1.13
100 + specifier: ^4.1.17
101 + version: 4.1.17
102 typescript:
100 - specifier: ~5.8.3
101 - version: 5.8.3
103 + specifier: ~5.9.3
104 + version: 5.9.3
105 vite:
103 - specifier: ^7.0.6
104 - version: 7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1)
106 + specifier: ^7.2.4
107 + version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
108 vite-plugin-vue-devtools:
106 - specifier: ^8.0.0
107 - version: 8.0.2(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1))(vue@3.5.21(typescript@5.8.3))
109 + specifier: ^8.0.5
110 + version: 8.0.5(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))
111 vite-svg-loader:
112 specifier: ^5.1.0
110 - version: 5.1.0(vue@3.5.21(typescript@5.8.3))
113 + version: 5.1.0(vue@3.5.25(typescript@5.9.3))
114 vue-tsc:
112 - specifier: ^3.0.4
113 - version: 3.0.7(typescript@5.8.3)
115 + specifier: ^3.1.5
116 + version: 3.1.5(typescript@5.9.3)
117
118 packages:
119
120 '@ajoelp/json-to-formdata@1.5.0':
121 resolution: {integrity: sha512-nrlfeTSL0X0dtx5r2KpzPiqLSIQquiiJjUKsQAKzWaCmO2QoYZCyb5ENZwF3YoffKronOCJr25mxaD8JRJmK8w==}
122
120 - '@antfu/eslint-config@5.3.0':
121 - resolution: {integrity: sha512-VzBemSi453rd06lF6gG6VkpP3HH7XKTf+sK6frSrGm7uMFkN57jry1XB074tQRKB3qOjhpsx3kKpWtOv9e5FnQ==}
123 + '@antfu/eslint-config@6.2.0':
124 + resolution: {integrity: sha512-ksasd+mJk441HHodwPh3Nhmwo9jSHUmgQyfTxMQM05U7SjDS0fy2KnXnPx0Vhc/CqKiJnx8wGpQCCJibyASX9Q==}
125 hasBin: true
126 peerDependencies:
124 - '@eslint-react/eslint-plugin': ^1.38.4
125 - '@next/eslint-plugin-next': ^15.4.0-canary.115
127 + '@eslint-react/eslint-plugin': ^2.0.1
128 + '@next/eslint-plugin-next': '>=15.0.0'
129 '@prettier/plugin-xml': ^3.4.1
130 '@unocss/eslint-plugin': '>=0.50.0'
131 astro-eslint-parser: ^1.0.2
@@ -130,7 +133,7 @@ packages:
133 eslint-plugin-astro: ^1.2.0
134 eslint-plugin-format: '>=0.1.0'
135 eslint-plugin-jsx-a11y: '>=6.10.2'
133 - eslint-plugin-react-hooks: ^5.2.0
136 + eslint-plugin-react-hooks: ^7.0.0
137 eslint-plugin-react-refresh: ^0.4.19
138 eslint-plugin-solid: ^0.14.3
139 eslint-plugin-svelte: '>=2.35.1'
@@ -249,6 +252,10 @@ packages:
252 resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
253 engines: {node: '>=6.9.0'}
254
255 + '@babel/helper-validator-identifier@7.28.5':
256 + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
257 + engines: {node: '>=6.9.0'}
258 +
259 '@babel/helper-validator-option@7.27.1':
260 resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
261 engines: {node: '>=6.9.0'}
@@ -262,6 +269,11 @@ packages:
269 engines: {node: '>=6.0.0'}
270 hasBin: true
271
272 + '@babel/parser@7.28.5':
273 + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==}
274 + engines: {node: '>=6.0.0'}
275 + hasBin: true
276 +
277 '@babel/plugin-proposal-decorators@7.28.0':
278 resolution: {integrity: sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==}
279 engines: {node: '>=6.9.0'}
@@ -303,10 +315,6 @@ packages:
315 peerDependencies:
316 '@babel/core': ^7.0.0-0
317
306 - '@babel/runtime@7.28.4':
307 - resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
308 - engines: {node: '>=6.9.0'}
309 -
318 '@babel/template@7.27.2':
319 resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
320 engines: {node: '>=6.9.0'}
@@ -319,6 +327,16 @@ packages:
327 resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==}
328 engines: {node: '>=6.9.0'}
329
330 + '@babel/types@7.28.5':
331 + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
332 + engines: {node: '>=6.9.0'}
333 +
334 + '@borewit/text-codec@0.1.1':
335 + resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
336 +
337 + '@canvas/image-data@1.1.0':
338 + resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==}
339 +
340 '@clack/core@0.5.0':
341 resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==}
342
@@ -342,10 +360,14 @@ packages:
360 resolution: {integrity: sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA==}
361 engines: {node: '>=18'}
362
345 - '@es-joy/jsdoccomment@0.56.0':
346 - resolution: {integrity: sha512-c6EW+aA1w2rjqOMjbL93nZlwxp6c1Ln06vTYs5FjRRhmJXK8V/OrSXdT+pUr4aRYgjCgu8/OkiZr0tzeVrRSbw==}
363 + '@es-joy/jsdoccomment@0.76.0':
364 + resolution: {integrity: sha512-g+RihtzFgGTx2WYCuTHbdOXJeAlGnROws0TeALx9ow/ZmOROOZkVg5wp/B44n0WJgI4SQFP1eWM2iRPlU2Y14w==}
365 engines: {node: '>=20.11.0'}
366
367 + '@es-joy/resolve.exports@1.2.0':
368 + resolution: {integrity: sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==}
369 + engines: {node: '>=10'}
370 +
371 '@esbuild/aix-ppc64@0.25.9':
372 resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
373 engines: {node: '>=18'}
@@ -527,36 +549,36 @@ packages:
549 eslint:
550 optional: true
551
530 - '@eslint/config-array@0.21.0':
531 - resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==}
552 + '@eslint/config-array@0.21.1':
553 + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==}
554 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
555
534 - '@eslint/config-helpers@0.3.1':
535 - resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==}
556 + '@eslint/config-helpers@0.4.2':
557 + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
558 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
559
538 - '@eslint/core@0.15.2':
539 - resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==}
560 + '@eslint/core@0.17.0':
561 + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
562 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
563
564 '@eslint/eslintrc@3.3.1':
565 resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
566 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
567
546 - '@eslint/js@9.35.0':
547 - resolution: {integrity: sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==}
568 + '@eslint/js@9.39.1':
569 + resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==}
570 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
571
550 - '@eslint/markdown@7.2.0':
551 - resolution: {integrity: sha512-cmDloByulvKzofM0tIkSGWwxMcrKOLsXZC+EM0FLkRIrxKzW+2RkZAt9TAh37EtQRmx1M4vjBEmlC6R0wiGkog==}
572 + '@eslint/markdown@7.5.1':
573 + resolution: {integrity: sha512-R8uZemG9dKTbru/DQRPblbJyXpObwKzo8rv1KYGGuPUPtjM4LXBYM9q5CIZAComzZupws3tWbDwam5AFpPLyJQ==}
574 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
575
554 - '@eslint/object-schema@2.1.6':
555 - resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
576 + '@eslint/object-schema@2.1.7':
577 + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
578 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
579
558 - '@eslint/plugin-kit@0.3.5':
559 - resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==}
580 + '@eslint/plugin-kit@0.4.1':
581 + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
582 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
583
584 '@humanfs/core@0.19.1':
@@ -583,9 +605,19 @@ packages:
605 peerDependencies:
606 vue: '>=3'
607
586 - '@isaacs/fs-minipass@4.0.1':
587 - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
588 - engines: {node: '>=18.0.0'}
608 + '@jimp/bmp@0.22.12':
609 + resolution: {integrity: sha512-aeI64HD0npropd+AR76MCcvvRaa+Qck6loCOS03CkkxGHN5/r336qTM5HPUdHKMDOGzqknuVPA8+kK1t03z12g==}
610 + peerDependencies:
611 + '@jimp/custom': '>=0.3.5'
612 +
613 + '@jimp/core@0.22.12':
614 + resolution: {integrity: sha512-l0RR0dOPyzMKfjUW1uebzueFEDtCOj9fN6pyTYWWOM/VS4BciXQ1VVrJs8pO3kycGYZxncRKhCoygbNr8eEZQA==}
615 +
616 + '@jimp/custom@0.22.12':
617 + resolution: {integrity: sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==}
618 +
619 + '@jimp/utils@0.22.12':
620 + resolution: {integrity: sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q==}
621
622 '@jridgewell/gen-mapping@0.3.13':
623 resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -606,18 +638,6 @@ packages:
638 '@juggle/resize-observer@3.4.0':
639 resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
640
609 - '@nodelib/fs.scandir@2.1.5':
610 - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
611 - engines: {node: '>= 8'}
612 -
613 - '@nodelib/fs.stat@2.0.5':
614 - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
615 - engines: {node: '>= 8'}
616 -
617 - '@nodelib/fs.walk@1.2.8':
618 - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
619 - engines: {node: '>= 8'}
620 -
641 '@parcel/watcher-android-arm64@2.5.1':
642 resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
643 engines: {node: '>= 10.0.0'}
@@ -707,8 +727,8 @@ packages:
727 '@polka/url@1.0.0-next.29':
728 resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
729
710 - '@rolldown/pluginutils@1.0.0-beta.29':
711 - resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==}
730 + '@rolldown/pluginutils@1.0.0-beta.50':
731 + resolution: {integrity: sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==}
732
733 '@rollup/rollup-android-arm-eabi@4.50.2':
734 resolution: {integrity: sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==}
@@ -815,78 +835,75 @@ packages:
835 cpu: [x64]
836 os: [win32]
837
818 - '@sec-ant/readable-stream@0.4.1':
819 - resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
820 -
821 - '@sindresorhus/merge-streams@4.0.0':
822 - resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
838 + '@sindresorhus/base62@1.0.0':
839 + resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==}
840 engines: {node: '>=18'}
841
825 - '@stylistic/eslint-plugin@5.3.1':
826 - resolution: {integrity: sha512-Ykums1VYonM0TgkD0VteVq9mrlO2FhF48MDJnPyv3MktIB2ydtuhlO0AfWm7xnW1kyf5bjOqA6xc7JjviuVTxg==}
842 + '@stylistic/eslint-plugin@5.6.1':
843 + resolution: {integrity: sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw==}
844 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
845 peerDependencies:
846 eslint: '>=9.0.0'
847
831 - '@tailwindcss/node@4.1.13':
832 - resolution: {integrity: sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==}
848 + '@tailwindcss/node@4.1.17':
849 + resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==}
850
834 - '@tailwindcss/oxide-android-arm64@4.1.13':
835 - resolution: {integrity: sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==}
851 + '@tailwindcss/oxide-android-arm64@4.1.17':
852 + resolution: {integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==}
853 engines: {node: '>= 10'}
854 cpu: [arm64]
855 os: [android]
856
840 - '@tailwindcss/oxide-darwin-arm64@4.1.13':
841 - resolution: {integrity: sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==}
857 + '@tailwindcss/oxide-darwin-arm64@4.1.17':
858 + resolution: {integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==}
859 engines: {node: '>= 10'}
860 cpu: [arm64]
861 os: [darwin]
862
846 - '@tailwindcss/oxide-darwin-x64@4.1.13':
847 - resolution: {integrity: sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==}
863 + '@tailwindcss/oxide-darwin-x64@4.1.17':
864 + resolution: {integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==}
865 engines: {node: '>= 10'}
866 cpu: [x64]
867 os: [darwin]
868
852 - '@tailwindcss/oxide-freebsd-x64@4.1.13':
853 - resolution: {integrity: sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==}
869 + '@tailwindcss/oxide-freebsd-x64@4.1.17':
870 + resolution: {integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==}
871 engines: {node: '>= 10'}
872 cpu: [x64]
873 os: [freebsd]
874
858 - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13':
859 - resolution: {integrity: sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==}
875 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17':
876 + resolution: {integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==}
877 engines: {node: '>= 10'}
878 cpu: [arm]
879 os: [linux]
880
864 - '@tailwindcss/oxide-linux-arm64-gnu@4.1.13':
865 - resolution: {integrity: sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==}
881 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17':
882 + resolution: {integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==}
883 engines: {node: '>= 10'}
884 cpu: [arm64]
885 os: [linux]
886
870 - '@tailwindcss/oxide-linux-arm64-musl@4.1.13':
871 - resolution: {integrity: sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==}
887 + '@tailwindcss/oxide-linux-arm64-musl@4.1.17':
888 + resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==}
889 engines: {node: '>= 10'}
890 cpu: [arm64]
891 os: [linux]
892
876 - '@tailwindcss/oxide-linux-x64-gnu@4.1.13':
877 - resolution: {integrity: sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==}
893 + '@tailwindcss/oxide-linux-x64-gnu@4.1.17':
894 + resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==}
895 engines: {node: '>= 10'}
896 cpu: [x64]
897 os: [linux]
898
882 - '@tailwindcss/oxide-linux-x64-musl@4.1.13':
883 - resolution: {integrity: sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==}
899 + '@tailwindcss/oxide-linux-x64-musl@4.1.17':
900 + resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==}
901 engines: {node: '>= 10'}
902 cpu: [x64]
903 os: [linux]
904
888 - '@tailwindcss/oxide-wasm32-wasi@4.1.13':
889 - resolution: {integrity: sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==}
905 + '@tailwindcss/oxide-wasm32-wasi@4.1.17':
906 + resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==}
907 engines: {node: '>=14.0.0'}
908 cpu: [wasm32]
909 bundledDependencies:
@@ -897,33 +914,40 @@ packages:
914 - '@emnapi/wasi-threads'
915 - tslib
916
900 - '@tailwindcss/oxide-win32-arm64-msvc@4.1.13':
901 - resolution: {integrity: sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==}
917 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17':
918 + resolution: {integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==}
919 engines: {node: '>= 10'}
920 cpu: [arm64]
921 os: [win32]
922
906 - '@tailwindcss/oxide-win32-x64-msvc@4.1.13':
907 - resolution: {integrity: sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==}
923 + '@tailwindcss/oxide-win32-x64-msvc@4.1.17':
924 + resolution: {integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==}
925 engines: {node: '>= 10'}
926 cpu: [x64]
927 os: [win32]
928
912 - '@tailwindcss/oxide@4.1.13':
913 - resolution: {integrity: sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==}
929 + '@tailwindcss/oxide@4.1.17':
930 + resolution: {integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==}
931 engines: {node: '>= 10'}
932
916 - '@tailwindcss/vite@4.1.13':
917 - resolution: {integrity: sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==}
933 + '@tailwindcss/vite@4.1.17':
934 + resolution: {integrity: sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==}
935 peerDependencies:
936 vite: ^5.2.0 || ^6 || ^7
937
938 + '@tokenizer/inflate@0.4.1':
939 + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
940 + engines: {node: '>=18'}
941 +
942 + '@tokenizer/token@0.3.0':
943 + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
944 +
945 '@trysound/sax@0.2.0':
946 resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
947 engines: {node: '>=10.13.0'}
948
925 - '@tsconfig/node20@20.1.6':
926 - resolution: {integrity: sha512-sz+Hqx9zwZDpZIV871WSbUzSqNIsXzghZydypnfgzPKLltVJfkINfUeTct31n/tTSa9ZE1ZOfKdRre1uHHquYQ==}
949 + '@tsconfig/node20@20.1.8':
950 + resolution: {integrity: sha512-Em+IdPfByIzWRRpqWL4Z7ArLHZGxmc36BxE3jCz9nBFSm+5aLaPMZyjwu4yetvyKXeogWcxik4L1jB5JTWfw7A==}
951
952 '@types/debug@4.1.12':
953 resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@@ -940,8 +964,8 @@ packages:
964 '@types/lodash-es@4.17.12':
965 resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
966
943 - '@types/lodash@4.17.20':
944 - resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
967 + '@types/lodash@4.17.21':
968 + resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==}
969
970 '@types/mdast@4.0.4':
971 resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
@@ -949,8 +973,8 @@ packages:
973 '@types/ms@2.1.0':
974 resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
975
952 - '@types/node@24.5.0':
953 - resolution: {integrity: sha512-y1dMvuvJspJiPSDZUQ+WMBvF7dpnEqN4x9DDC9ie5Fs/HUZJA3wFp7EhHoVaKX/iI0cRoECV8X2jL8zi0xrHCg==}
976 + '@types/node@24.10.1':
977 + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
978
979 '@types/unist@3.0.3':
980 resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
@@ -958,77 +982,78 @@ packages:
982 '@types/web-bluetooth@0.0.21':
983 resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
984
961 - '@typescript-eslint/eslint-plugin@8.44.0':
962 - resolution: {integrity: sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ==}
985 + '@typescript-eslint/eslint-plugin@8.48.0':
986 + resolution: {integrity: sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==}
987 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
988 peerDependencies:
965 - '@typescript-eslint/parser': ^8.44.0
989 + '@typescript-eslint/parser': ^8.48.0
990 eslint: ^8.57.0 || ^9.0.0
991 typescript: '>=4.8.4 <6.0.0'
992
969 - '@typescript-eslint/parser@8.44.0':
970 - resolution: {integrity: sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==}
993 + '@typescript-eslint/parser@8.48.0':
994 + resolution: {integrity: sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==}
995 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
996 peerDependencies:
997 eslint: ^8.57.0 || ^9.0.0
998 typescript: '>=4.8.4 <6.0.0'
999
976 - '@typescript-eslint/project-service@8.44.0':
977 - resolution: {integrity: sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA==}
1000 + '@typescript-eslint/project-service@8.48.0':
1001 + resolution: {integrity: sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==}
1002 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1003 peerDependencies:
1004 typescript: '>=4.8.4 <6.0.0'
1005
982 - '@typescript-eslint/scope-manager@8.44.0':
983 - resolution: {integrity: sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA==}
1006 + '@typescript-eslint/scope-manager@8.48.0':
1007 + resolution: {integrity: sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==}
1008 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1009
986 - '@typescript-eslint/tsconfig-utils@8.44.0':
987 - resolution: {integrity: sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ==}
1010 + '@typescript-eslint/tsconfig-utils@8.48.0':
1011 + resolution: {integrity: sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==}
1012 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1013 peerDependencies:
1014 typescript: '>=4.8.4 <6.0.0'
1015
992 - '@typescript-eslint/type-utils@8.44.0':
993 - resolution: {integrity: sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg==}
1016 + '@typescript-eslint/type-utils@8.48.0':
1017 + resolution: {integrity: sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==}
1018 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1019 peerDependencies:
1020 eslint: ^8.57.0 || ^9.0.0
1021 typescript: '>=4.8.4 <6.0.0'
1022
999 - '@typescript-eslint/types@8.44.0':
1000 - resolution: {integrity: sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==}
1023 + '@typescript-eslint/types@8.48.0':
1024 + resolution: {integrity: sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==}
1025 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1026
1003 - '@typescript-eslint/typescript-estree@8.44.0':
1004 - resolution: {integrity: sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw==}
1027 + '@typescript-eslint/typescript-estree@8.48.0':
1028 + resolution: {integrity: sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==}
1029 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1030 peerDependencies:
1031 typescript: '>=4.8.4 <6.0.0'
1032
1009 - '@typescript-eslint/utils@8.44.0':
1010 - resolution: {integrity: sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg==}
1033 + '@typescript-eslint/utils@8.48.0':
1034 + resolution: {integrity: sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==}
1035 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1036 peerDependencies:
1037 eslint: ^8.57.0 || ^9.0.0
1038 typescript: '>=4.8.4 <6.0.0'
1039
1016 - '@typescript-eslint/visitor-keys@8.44.0':
1017 - resolution: {integrity: sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw==}
1040 + '@typescript-eslint/visitor-keys@8.48.0':
1041 + resolution: {integrity: sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==}
1042 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1043
1020 - '@vitejs/plugin-vue@6.0.1':
1021 - resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==}
1044 + '@vitejs/plugin-vue@6.0.2':
1045 + resolution: {integrity: sha512-iHmwV3QcVGGvSC1BG5bZ4z6iwa1SOpAPWmnjOErd4Ske+lZua5K9TtAVdx0gMBClJ28DViCbSmZitjWZsWO3LA==}
1046 engines: {node: ^20.19.0 || >=22.12.0}
1047 peerDependencies:
1048 vite: ^5.0.0 || ^6.0.0 || ^7.0.0
1049 vue: ^3.2.25
1050
1027 - '@vitest/eslint-plugin@1.3.10':
1028 - resolution: {integrity: sha512-tLxetdTgQJOiJQpkdPc9/jK03NCJeMZX+vX6aSGepsYJyla6UcM/G7E8I5Uui0t+LCKHGUIohV8EEuCobCnJuQ==}
1051 + '@vitest/eslint-plugin@1.5.1':
1052 + resolution: {integrity: sha512-t49CNERe/YadnLn90NTTKJLKzs99xBkXElcoUTLodG6j1G0Q7jy3mXqqiHd3N5aryG2KkgOg4UAoGwgwSrZqKQ==}
1053 + engines: {node: '>=18'}
1054 peerDependencies:
1030 - eslint: '>= 8.57.0'
1031 - typescript: '>= 5.0.0'
1055 + eslint: '>=8.57.0'
1056 + typescript: '>=5.0.0'
1057 vitest: '*'
1058 peerDependenciesMeta:
1059 typescript:
@@ -1064,17 +1089,26 @@ packages:
1089 '@vue/compiler-core@3.5.21':
1090 resolution: {integrity: sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==}
1091
1092 + '@vue/compiler-core@3.5.25':
1093 + resolution: {integrity: sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==}
1094 +
1095 '@vue/compiler-dom@3.5.21':
1096 resolution: {integrity: sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==}
1097
1098 + '@vue/compiler-dom@3.5.25':
1099 + resolution: {integrity: sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==}
1100 +
1101 '@vue/compiler-sfc@3.5.21':
1102 resolution: {integrity: sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==}
1103
1104 + '@vue/compiler-sfc@3.5.25':
1105 + resolution: {integrity: sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==}
1106 +
1107 '@vue/compiler-ssr@3.5.21':
1108 resolution: {integrity: sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==}
1109
1076 - '@vue/compiler-vue2@2.7.16':
1077 - resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
1110 + '@vue/compiler-ssr@3.5.25':
1111 + resolution: {integrity: sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==}
1112
1113 '@vue/devtools-api@6.6.4':
1114 resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
@@ -1082,48 +1116,51 @@ packages:
1116 '@vue/devtools-api@7.7.7':
1117 resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==}
1118
1085 - '@vue/devtools-core@8.0.2':
1086 - resolution: {integrity: sha512-V7eKTTHoS6KfK8PSGMLZMhGv/9yNDrmv6Qc3r71QILulnzPnqK2frsTyx3e2MrhdUZnENPEm6hcb4z0GZOqNhw==}
1119 + '@vue/devtools-core@8.0.5':
1120 + resolution: {integrity: sha512-dpCw8nl0GDBuiL9SaY0mtDxoGIEmU38w+TQiYEPOLhW03VDC0lfNMYXS/qhl4I0YlysGp04NLY4UNn6xgD0VIQ==}
1121 peerDependencies:
1122 vue: ^3.0.0
1123
1124 '@vue/devtools-kit@7.7.7':
1125 resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==}
1126
1093 - '@vue/devtools-kit@8.0.2':
1094 - resolution: {integrity: sha512-yjZKdEmhJzQqbOh4KFBfTOQjDPMrjjBNCnHBvnTGJX+YLAqoUtY2J+cg7BE+EA8KUv8LprECq04ts75wCoIGWA==}
1127 + '@vue/devtools-kit@8.0.5':
1128 + resolution: {integrity: sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==}
1129
1130 '@vue/devtools-shared@7.7.7':
1131 resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==}
1132
1099 - '@vue/devtools-shared@8.0.2':
1100 - resolution: {integrity: sha512-mLU0QVdy5Lp40PMGSixDw/Kbd6v5dkQXltd2r+mdVQV7iUog2NlZuLxFZApFZ/mObUBDhoCpf0T3zF2FWWdeHw==}
1133 + '@vue/devtools-shared@8.0.5':
1134 + resolution: {integrity: sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==}
1135
1102 - '@vue/language-core@3.0.7':
1103 - resolution: {integrity: sha512-0sqqyqJ0Gn33JH3TdIsZLCZZ8Gr4kwlg8iYOnOrDDkJKSjFurlQY/bEFQx5zs7SX2C/bjMkmPYq/NiyY1fTOkw==}
1136 + '@vue/language-core@3.1.5':
1137 + resolution: {integrity: sha512-FMcqyzWN+sYBeqRMWPGT2QY0mUasZMVIuHvmb5NT3eeqPrbHBYtCP8JWEUCDCgM+Zr62uuWY/qoeBrPrzfa78w==}
1138 peerDependencies:
1139 typescript: '*'
1140 peerDependenciesMeta:
1141 typescript:
1142 optional: true
1143
1110 - '@vue/reactivity@3.5.21':
1111 - resolution: {integrity: sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==}
1144 + '@vue/reactivity@3.5.25':
1145 + resolution: {integrity: sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==}
1146
1113 - '@vue/runtime-core@3.5.21':
1114 - resolution: {integrity: sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==}
1147 + '@vue/runtime-core@3.5.25':
1148 + resolution: {integrity: sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==}
1149
1116 - '@vue/runtime-dom@3.5.21':
1117 - resolution: {integrity: sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==}
1150 + '@vue/runtime-dom@3.5.25':
1151 + resolution: {integrity: sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==}
1152
1119 - '@vue/server-renderer@3.5.21':
1120 - resolution: {integrity: sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==}
1153 + '@vue/server-renderer@3.5.25':
1154 + resolution: {integrity: sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==}
1155 peerDependencies:
1122 - vue: 3.5.21
1156 + vue: 3.5.25
1157
1158 '@vue/shared@3.5.21':
1159 resolution: {integrity: sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==}
1160
1161 + '@vue/shared@3.5.25':
1162 + resolution: {integrity: sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==}
1163 +
1164 '@vue/tsconfig@0.7.0':
1165 resolution: {integrity: sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==}
1166 peerDependencies:
@@ -1135,19 +1172,23 @@ packages:
1172 vue:
1173 optional: true
1174
1138 - '@vueuse/core@13.9.0':
1139 - resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==}
1175 + '@vueuse/core@14.1.0':
1176 + resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==}
1177 peerDependencies:
1178 vue: ^3.5.0
1179
1143 - '@vueuse/metadata@13.9.0':
1144 - resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==}
1180 + '@vueuse/metadata@14.1.0':
1181 + resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==}
1182
1146 - '@vueuse/shared@13.9.0':
1147 - resolution: {integrity: sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==}
1183 + '@vueuse/shared@14.1.0':
1184 + resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==}
1185 peerDependencies:
1186 vue: ^3.5.0
1187
1188 + abort-controller@3.0.0:
1189 + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
1190 + engines: {node: '>=6.5'}
1191 +
1192 acorn-jsx@5.3.2:
1193 resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
1194 peerDependencies:
@@ -1161,8 +1202,8 @@ packages:
1202 ajv@6.12.6:
1203 resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
1204
1164 - alien-signals@2.0.7:
1165 - resolution: {integrity: sha512-wE7y3jmYeb0+h6mr5BOovuqhFv22O/MV9j5p0ndJsa7z1zJNPGQ4ph5pQk/kTTCWRC3xsA4SmtwmkzQO+7NCNg==}
1205 + alien-signals@3.1.1:
1206 + resolution: {integrity: sha512-ogkIWbVrLwKtHY6oOAXaYkAxP+cTH7V5FZ5+Tm4NZFd8VDZ6uNMDrfzqctTZ42eTMCSR3ne3otpcxmqSnFfPYA==}
1207
1208 ansi-regex@5.0.1:
1209 resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
@@ -1180,6 +1221,13 @@ packages:
1221 resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==}
1222 engines: {node: '>=14'}
1223
1224 + ansis@4.2.0:
1225 + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
1226 + engines: {node: '>=14'}
1227 +
1228 + any-base@1.1.0:
1229 + resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==}
1230 +
1231 are-docs-informative@0.0.2:
1232 resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==}
1233 engines: {node: '>=14'}
@@ -1193,12 +1241,19 @@ packages:
1241 asynckit@0.4.0:
1242 resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
1243
1196 - axios@1.12.2:
1197 - resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==}
1244 + axios@1.13.2:
1245 + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
1246
1247 balanced-match@1.0.2:
1248 resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
1249
1250 + base64-js@1.5.1:
1251 + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
1252 +
1253 + baseline-browser-mapping@2.8.32:
1254 + resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==}
1255 + hasBin: true
1256 +
1257 baseline-browser-mapping@2.8.4:
1258 resolution: {integrity: sha512-L+YvJwGAgwJBV1p6ffpSTa2KRc69EeeYGYjRVWKs0GKrK+LON0GC0gV+rKSNtALEDvMDqkvCFq9r1r94/Gjwxw==}
1259 hasBin: true
@@ -1206,6 +1261,12 @@ packages:
1261 birpc@2.5.0:
1262 resolution: {integrity: sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==}
1263
1264 + birpc@2.8.0:
1265 + resolution: {integrity: sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==}
1266 +
1267 + bmp-js@0.1.0:
1268 + resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==}
1269 +
1270 boolbase@1.0.0:
1271 resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
1272
@@ -1224,6 +1285,17 @@ packages:
1285 engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
1286 hasBin: true
1287
1288 + browserslist@4.28.0:
1289 + resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==}
1290 + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
1291 + hasBin: true
1292 +
1293 + buffer@5.7.1:
1294 + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
1295 +
1296 + buffer@6.0.3:
1297 + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
1298 +
1299 builtin-modules@5.0.0:
1300 resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==}
1301 engines: {node: '>=18.20'}
@@ -1247,6 +1319,9 @@ packages:
1319 caniuse-lite@1.0.30001743:
1320 resolution: {integrity: sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==}
1321
1322 + caniuse-lite@1.0.30001757:
1323 + resolution: {integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==}
1324 +
1325 ccount@2.0.1:
1326 resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
1327
@@ -1264,12 +1339,8 @@ packages:
1339 resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
1340 engines: {node: '>= 14.16.0'}
1341
1267 - chownr@3.0.0:
1268 - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
1269 - engines: {node: '>=18'}
1270 -
1271 - ci-info@4.3.0:
1272 - resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==}
1342 + ci-info@4.3.1:
1343 + resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==}
1344 engines: {node: '>=8'}
1345
1346 clean-regexp@1.0.0:
@@ -1302,9 +1373,9 @@ packages:
1373 concat-map@0.0.1:
1374 resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
1375
1305 - concurrently@8.2.2:
1306 - resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==}
1307 - engines: {node: ^14.13.0 || >=16.0.0}
1376 + concurrently@9.2.1:
1377 + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==}
1378 + engines: {node: '>=18'}
1379 hasBin: true
1380
1381 confbox@0.1.8:
@@ -1320,8 +1391,8 @@ packages:
1391 resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
1392 engines: {node: '>=12.13'}
1393
1323 - core-js-compat@3.45.1:
1324 - resolution: {integrity: sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==}
1394 + core-js-compat@3.47.0:
1395 + resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==}
1396
1397 cross-spawn@7.0.6:
1398 resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
@@ -1368,18 +1439,11 @@ packages:
1439 peerDependencies:
1440 date-fns: ^3.0.0 || ^4.0.0
1441
1371 - date-fns@2.30.0:
1372 - resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
1373 - engines: {node: '>=0.11'}
1374 -
1375 - date-fns@3.6.0:
1376 - resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
1442 + date-fns@4.1.0:
1443 + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
1444
1378 - dayjs@1.11.18:
1379 - resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==}
1380 -
1381 - de-indent@1.0.2:
1382 - resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
1445 + dayjs@1.11.19:
1446 + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
1447
1448 debug@4.4.3:
1449 resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
@@ -1390,15 +1454,20 @@ packages:
1454 supports-color:
1455 optional: true
1456
1457 + decode-bmp@0.2.1:
1458 + resolution: {integrity: sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA==}
1459 + engines: {node: '>=8.6.0'}
1460 +
1461 + decode-ico@0.4.1:
1462 + resolution: {integrity: sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA==}
1463 + engines: {node: '>=8.6'}
1464 +
1465 decode-named-character-reference@1.2.0:
1466 resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
1467
1468 deep-is@0.1.4:
1469 resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
1470
1399 - deep-pick-omit@1.2.1:
1400 - resolution: {integrity: sha512-2J6Kc/m3irCeqVG42T+SaUMesaK7oGWaedGnQQK/+O0gYc+2SP5bKh/KKTE7d7SJ+GCA9UUE1GRzh6oDe0EnGw==}
1401 -
1471 default-browser-id@5.0.0:
1472 resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==}
1473 engines: {node: '>=18'}
@@ -1422,9 +1491,6 @@ packages:
1491 resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
1492 engines: {node: '>=6'}
1493
1425 - destr@2.0.5:
1426 - resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
1427 -
1494 detect-libc@1.0.3:
1495 resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
1496 engines: {node: '>=0.10'}
@@ -1437,6 +1503,10 @@ packages:
1503 devlop@1.1.0:
1504 resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
1505
1506 + diff-sequences@27.5.1:
1507 + resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==}
1508 + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
1509 +
1510 dom-serializer@2.0.0:
1511 resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
1512
@@ -1457,6 +1527,9 @@ packages:
1527 electron-to-chromium@1.5.218:
1528 resolution: {integrity: sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==}
1529
1530 + electron-to-chromium@1.5.262:
1531 + resolution: {integrity: sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==}
1532 +
1533 emoji-regex@8.0.0:
1534 resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
1535
@@ -1529,8 +1602,8 @@ packages:
1602 peerDependencies:
1603 eslint: ^9.5.0
1604
1532 - eslint-flat-config-utils@2.1.1:
1533 - resolution: {integrity: sha512-K8eaPkBemHkfbYsZH7z4lZ/tt6gNSsVh535Wh9W9gQBS2WjvfUbbVr2NZR3L1yiRCLuOEimYfPxCxODczD4Opg==}
1605 + eslint-flat-config-utils@2.1.4:
1606 + resolution: {integrity: sha512-bEnmU5gqzS+4O+id9vrbP43vByjF+8KOs+QuuV4OlqAuXmnRW2zfI/Rza1fQvdihQ5h4DUo0NqFAiViD4mSrzQ==}
1607
1608 eslint-json-compat-utils@0.2.1:
1609 resolution: {integrity: sha512-YzEodbDyW8DX8bImKhAcCeu/L31Dd/70Bidx2Qex9OFUtgzXLqtfWL4Hr5fM/aCCB8QUZLuJur0S9k6UfgFkfg==}
@@ -1574,20 +1647,20 @@ packages:
1647 typescript:
1648 optional: true
1649
1577 - eslint-plugin-jsdoc@54.7.0:
1578 - resolution: {integrity: sha512-u5Na4he2+6kY1rWqxzbQaAwJL3/tDCuT5ElDRc5UJ9stOeQeQ5L1JJ1kRRu7ldYMlOHMCJLsY8Mg/Tu3ExdZiQ==}
1650 + eslint-plugin-jsdoc@61.4.1:
1651 + resolution: {integrity: sha512-3c1QW/bV25sJ1MsIvsvW+EtLtN6yZMduw7LVQNVt72y2/5BbV5Pg5b//TE5T48LRUxoEQGaZJejCmcj3wCxBzw==}
1652 engines: {node: '>=20.11.0'}
1653 peerDependencies:
1654 eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
1655
1583 - eslint-plugin-jsonc@2.20.1:
1584 - resolution: {integrity: sha512-gUzIwQHXx7ZPypUoadcyRi4WbHW2TPixDr0kqQ4miuJBU0emJmyGTlnaT3Og9X2a8R1CDayN9BFSq5weGWbTng==}
1656 + eslint-plugin-jsonc@2.21.0:
1657 + resolution: {integrity: sha512-HttlxdNG5ly3YjP1cFMP62R4qKLxJURfBZo2gnMY+yQojZxkLyOpY1H1KRTKBmvQeSG9pIpSGEhDjE17vvYosg==}
1658 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
1659 peerDependencies:
1660 eslint: '>=6.0.0'
1661
1589 - eslint-plugin-n@17.23.0:
1590 - resolution: {integrity: sha512-aPePGxUr5LezcXmMRBF83eK1MmqUYY1NdLdHC+jdpfc5b98eL7yDXY20gXJ6DcTxrHBhrLsfYYqo7J+m0h9YXQ==}
1662 + eslint-plugin-n@17.23.1:
1663 + resolution: {integrity: sha512-68PealUpYoHOBh332JLLD9Sj7OQUDkFpmcfqt8R9sySfFSeuGJjMTJQvCRRB96zO3A/PELRLkPrzsHmzEFQQ5A==}
1664 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1665 peerDependencies:
1666 eslint: '>=8.23.0'
@@ -1596,14 +1669,14 @@ packages:
1669 resolution: {integrity: sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==}
1670 engines: {node: '>=5.0.0'}
1671
1599 - eslint-plugin-perfectionist@4.15.0:
1600 - resolution: {integrity: sha512-pC7PgoXyDnEXe14xvRUhBII8A3zRgggKqJFx2a82fjrItDs1BSI7zdZnQtM2yQvcyod6/ujmzb7ejKPx8lZTnw==}
1672 + eslint-plugin-perfectionist@4.15.1:
1673 + resolution: {integrity: sha512-MHF0cBoOG0XyBf7G0EAFCuJJu4I18wy0zAoT1OHfx2o6EOx1EFTIzr2HGeuZa1kDcusoX0xJ9V7oZmaeFd773Q==}
1674 engines: {node: ^18.0.0 || >=20.0.0}
1675 peerDependencies:
1676 eslint: '>=8.45.0'
1677
1605 - eslint-plugin-pnpm@1.1.1:
1606 - resolution: {integrity: sha512-gNo+swrLCgvT8L6JX6hVmxuKeuStGK2l8IwVjDxmYIn+wP4SW/d0ORLKyUiYamsp+UxknQo3f2M1irrTpqahCw==}
1678 + eslint-plugin-pnpm@1.3.0:
1679 + resolution: {integrity: sha512-Lkdnj3afoeUIkDUu8X74z60nrzjQ2U55EbOeI+qz7H1He4IO4gmUKT2KQIl0It52iMHJeuyLDWWNgjr6UIK8nw==}
1680 peerDependencies:
1681 eslint: ^9.0.0
1682
@@ -1619,14 +1692,14 @@ packages:
1692 peerDependencies:
1693 eslint: '>=6.0.0'
1694
1622 - eslint-plugin-unicorn@61.0.2:
1623 - resolution: {integrity: sha512-zLihukvneYT7f74GNbVJXfWIiNQmkc/a9vYBTE4qPkQZswolWNdu+Wsp9sIXno1JOzdn6OUwLPd19ekXVkahRA==}
1695 + eslint-plugin-unicorn@62.0.0:
1696 + resolution: {integrity: sha512-HIlIkGLkvf29YEiS/ImuDZQbP12gWyx5i3C6XrRxMvVdqMroCI9qoVYCoIl17ChN+U89pn9sVwLxhIWj5nEc7g==}
1697 engines: {node: ^20.10.0 || >=21.0.0}
1698 peerDependencies:
1626 - eslint: '>=9.29.0'
1699 + eslint: '>=9.38.0'
1700
1628 - eslint-plugin-unused-imports@4.2.0:
1629 - resolution: {integrity: sha512-hLbJ2/wnjKq4kGA9AUaExVFIbNzyxYdVo49QZmKCnhk5pc9wcYRbfgLHvWJ8tnsdcseGhoUAddm9gn/lt+d74w==}
1701 + eslint-plugin-unused-imports@4.3.0:
1702 + resolution: {integrity: sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==}
1703 peerDependencies:
1704 '@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0
1705 eslint: ^9.0.0 || ^8.0.0
@@ -1634,19 +1707,22 @@ packages:
1707 '@typescript-eslint/eslint-plugin':
1708 optional: true
1709
1637 - eslint-plugin-vue@10.4.0:
1638 - resolution: {integrity: sha512-K6tP0dW8FJVZLQxa2S7LcE1lLw3X8VvB3t887Q6CLrFVxHYBXGANbXvwNzYIu6Ughx1bSJ5BDT0YB3ybPT39lw==}
1710 + eslint-plugin-vue@10.6.2:
1711 + resolution: {integrity: sha512-nA5yUs/B1KmKzvC42fyD0+l9Yd+LtEpVhWRbXuDj0e+ZURcTtyRbMDWUeJmTAh2wC6jC83raS63anNM2YT3NPw==}
1712 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1713 peerDependencies:
1714 + '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
1715 '@typescript-eslint/parser': ^7.0.0 || ^8.0.0
1716 eslint: ^8.57.0 || ^9.0.0
1717 vue-eslint-parser: ^10.0.0
1718 peerDependenciesMeta:
1719 + '@stylistic/eslint-plugin':
1720 + optional: true
1721 '@typescript-eslint/parser':
1722 optional: true
1723
1648 - eslint-plugin-yml@1.18.0:
1649 - resolution: {integrity: sha512-9NtbhHRN2NJa/s3uHchO3qVVZw0vyOIvWlXWGaKCr/6l3Go62wsvJK5byiI6ZoYztDsow4GnS69BZD3GnqH3hA==}
1724 + eslint-plugin-yml@1.19.0:
1725 + resolution: {integrity: sha512-S+4GbcCWksFKAvFJtf0vpdiCkZZvDJCV4Zsi9ahmYkYOYcf+LRqqzvzkb/ST7vTYV6sFwXOvawzYyL/jFT2nQA==}
1726 engines: {node: ^14.17.0 || >=16.0.0}
1727 peerDependencies:
1728 eslint: '>=6.0.0'
@@ -1669,8 +1745,8 @@ packages:
1745 resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
1746 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1747
1672 - eslint@9.35.0:
1673 - resolution: {integrity: sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==}
1748 + eslint@9.39.1:
1749 + resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==}
1750 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1751 hasBin: true
1752 peerDependencies:
@@ -1706,12 +1782,19 @@ packages:
1782 resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
1783 engines: {node: '>=0.10.0'}
1784
1785 + event-target-shim@5.0.1:
1786 + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
1787 + engines: {node: '>=6'}
1788 +
1789 + events@3.3.0:
1790 + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
1791 + engines: {node: '>=0.8.x'}
1792 +
1793 evtd@0.2.4:
1794 resolution: {integrity: sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==}
1795
1712 - execa@9.6.0:
1713 - resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==}
1714 - engines: {node: ^18.19.0 || >=20.5.0}
1796 + exif-parser@0.1.12:
1797 + resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==}
1798
1799 exsolve@1.0.7:
1800 resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==}
@@ -1719,19 +1802,12 @@ packages:
1802 fast-deep-equal@3.1.3:
1803 resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
1804
1722 - fast-glob@3.3.3:
1723 - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
1724 - engines: {node: '>=8.6.0'}
1725 -
1805 fast-json-stable-stringify@2.1.0:
1806 resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
1807
1808 fast-levenshtein@2.0.6:
1809 resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
1810
1732 - fastq@1.19.1:
1733 - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
1734 -
1811 fault@2.0.1:
1812 resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==}
1813
@@ -1744,14 +1820,18 @@ packages:
1820 picomatch:
1821 optional: true
1822
1747 - figures@6.1.0:
1748 - resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
1749 - engines: {node: '>=18'}
1750 -
1823 file-entry-cache@8.0.0:
1824 resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
1825 engines: {node: '>=16.0.0'}
1826
1827 + file-type@16.5.4:
1828 + resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==}
1829 + engines: {node: '>=10'}
1830 +
1831 + file-type@21.1.1:
1832 + resolution: {integrity: sha512-ifJXo8zUqbQ/bLbl9sFoqHNTNWbnPY1COImFfM6CCy7z+E+jC1eY9YfOKkx0fckIg+VljAy2/87T61fp0+eEkg==}
1833 + engines: {node: '>=20'}
1834 +
1835 fill-range@7.1.1:
1836 resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
1837 engines: {node: '>=8'}
@@ -1812,20 +1892,12 @@ packages:
1892 resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
1893 engines: {node: '>= 0.4'}
1894
1815 - get-stream@9.0.1:
1816 - resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
1817 - engines: {node: '>=18'}
1818 -
1895 get-tsconfig@4.10.1:
1896 resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
1897
1898 github-slugger@2.0.0:
1899 resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
1900
1825 - glob-parent@5.1.2:
1826 - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
1827 - engines: {node: '>= 6'}
1828 -
1901 glob-parent@6.0.2:
1902 resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
1903 engines: {node: '>=10.13.0'}
@@ -1871,10 +1943,6 @@ packages:
1943 resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
1944 engines: {node: '>= 0.4'}
1945
1874 - he@1.2.0:
1875 - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
1876 - hasBin: true
1877 -
1946 highlight.js@11.11.1:
1947 resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
1948 engines: {node: '>=12.0.0'}
@@ -1882,9 +1950,15 @@ packages:
1950 hookable@5.5.3:
1951 resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
1952
1885 - human-signals@8.0.1:
1886 - resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
1887 - engines: {node: '>=18.18.0'}
1953 + html-entities@2.6.0:
1954 + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==}
1955 +
1956 + icojs@0.20.1:
1957 + resolution: {integrity: sha512-5CgDmAMlXmqMLFSCKTKrWK5FJl9WaA5Y8y3vtuNSas0HZx9O7dtS+xgeIu7jG8W3VQSXWAXdoGaILtF6ktjE7Q==}
1958 + engines: {node: '>=20.19.4'}
1959 +
1960 + ieee754@1.2.1:
1961 + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
1962
1963 ignore@5.3.2:
1964 resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
@@ -1939,18 +2013,6 @@ packages:
2013 resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
2014 engines: {node: '>=0.12.0'}
2015
1942 - is-plain-obj@4.1.0:
1943 - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
1944 - engines: {node: '>=12'}
1945 -
1946 - is-stream@4.0.1:
1947 - resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
1948 - engines: {node: '>=18'}
1949 -
1950 - is-unicode-supported@2.1.0:
1951 - resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
1952 - engines: {node: '>=18'}
1953 -
2016 is-what@4.1.16:
2017 resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
2018 engines: {node: '>=12.13'}
@@ -1966,12 +2028,18 @@ packages:
2028 resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
2029 engines: {node: '>=16'}
2030
1969 - jiti@2.5.1:
1970 - resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
2031 + isomorphic-fetch@3.0.0:
2032 + resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==}
2033 +
2034 + jiti@2.6.1:
2035 + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
2036 hasBin: true
2037
1973 - jose@6.1.0:
1974 - resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==}
2038 + jose@6.1.2:
2039 + resolution: {integrity: sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ==}
2040 +
2041 + jpeg-js@0.4.4:
2042 + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==}
2043
2044 js-tokens@4.0.0:
2045 resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -1988,14 +2056,9 @@ packages:
2056 resolution: {integrity: sha512-iZ8Bdb84lWRuGHamRXFyML07r21pcwBrLkHEuHgEY5UbCouBwv7ECknDRKzsQIXMiqpPymqtIf8TC/shYKB5rw==}
2057 engines: {node: '>=12.0.0'}
2058
1991 - jsdoc-type-pratt-parser@5.1.1:
1992 - resolution: {integrity: sha512-DYYlVP1fe4QBMh2xTIs20/YeTz2GYVbWAEZweHSZD+qQ/Cx2d5RShuhhsdk64eTjNq0FeVnteP/qVOgaywSRbg==}
1993 - engines: {node: '>=12.0.0'}
1994 -
1995 - jsesc@3.0.2:
1996 - resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==}
1997 - engines: {node: '>=6'}
1998 - hasBin: true
2059 + jsdoc-type-pratt-parser@6.10.0:
2060 + resolution: {integrity: sha512-+LexoTRyYui5iOhJGn13N9ZazL23nAHGkXsa1p/C8yeq79WRfLBag6ZZ0FQG2aRoc9yfo59JT9EYCQonOkHKkQ==}
2061 + engines: {node: '>=20.0.0'}
2062
2063 jsesc@3.1.0:
2064 resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
@@ -2020,8 +2083,8 @@ packages:
2083 engines: {node: '>=6'}
2084 hasBin: true
2085
2023 - jsonc-eslint-parser@2.4.0:
2024 - resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==}
2086 + jsonc-eslint-parser@2.4.1:
2087 + resolution: {integrity: sha512-uuPNLJkKN8NXAlZlQ6kmUF9qO+T6Kyd7oV4+/7yy8Jz6+MZNyhPq8EdLpdfnPVzUC8qSf1b4j1azKaGnFsjmsw==}
2088 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
2089
2090 keyv@4.5.4:
@@ -2034,68 +2097,74 @@ packages:
2097 resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
2098 engines: {node: '>= 0.8.0'}
2099
2037 - lightningcss-darwin-arm64@1.30.1:
2038 - resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
2100 + lightningcss-android-arm64@1.30.2:
2101 + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
2102 + engines: {node: '>= 12.0.0'}
2103 + cpu: [arm64]
2104 + os: [android]
2105 +
2106 + lightningcss-darwin-arm64@1.30.2:
2107 + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==}
2108 engines: {node: '>= 12.0.0'}
2109 cpu: [arm64]
2110 os: [darwin]
2111
2043 - lightningcss-darwin-x64@1.30.1:
2044 - resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==}
2112 + lightningcss-darwin-x64@1.30.2:
2113 + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==}
2114 engines: {node: '>= 12.0.0'}
2115 cpu: [x64]
2116 os: [darwin]
2117
2049 - lightningcss-freebsd-x64@1.30.1:
2050 - resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==}
2118 + lightningcss-freebsd-x64@1.30.2:
2119 + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==}
2120 engines: {node: '>= 12.0.0'}
2121 cpu: [x64]
2122 os: [freebsd]
2123
2055 - lightningcss-linux-arm-gnueabihf@1.30.1:
2056 - resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==}
2124 + lightningcss-linux-arm-gnueabihf@1.30.2:
2125 + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==}
2126 engines: {node: '>= 12.0.0'}
2127 cpu: [arm]
2128 os: [linux]
2129
2061 - lightningcss-linux-arm64-gnu@1.30.1:
2062 - resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==}
2130 + lightningcss-linux-arm64-gnu@1.30.2:
2131 + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
2132 engines: {node: '>= 12.0.0'}
2133 cpu: [arm64]
2134 os: [linux]
2135
2067 - lightningcss-linux-arm64-musl@1.30.1:
2068 - resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
2136 + lightningcss-linux-arm64-musl@1.30.2:
2137 + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
2138 engines: {node: '>= 12.0.0'}
2139 cpu: [arm64]
2140 os: [linux]
2141
2073 - lightningcss-linux-x64-gnu@1.30.1:
2074 - resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
2142 + lightningcss-linux-x64-gnu@1.30.2:
2143 + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
2144 engines: {node: '>= 12.0.0'}
2145 cpu: [x64]
2146 os: [linux]
2147
2079 - lightningcss-linux-x64-musl@1.30.1:
2080 - resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
2148 + lightningcss-linux-x64-musl@1.30.2:
2149 + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
2150 engines: {node: '>= 12.0.0'}
2151 cpu: [x64]
2152 os: [linux]
2153
2085 - lightningcss-win32-arm64-msvc@1.30.1:
2086 - resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
2154 + lightningcss-win32-arm64-msvc@1.30.2:
2155 + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
2156 engines: {node: '>= 12.0.0'}
2157 cpu: [arm64]
2158 os: [win32]
2159
2091 - lightningcss-win32-x64-msvc@1.30.1:
2092 - resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==}
2160 + lightningcss-win32-x64-msvc@1.30.2:
2161 + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==}
2162 engines: {node: '>= 12.0.0'}
2163 cpu: [x64]
2164 os: [win32]
2165
2097 - lightningcss@1.30.1:
2098 - resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
2166 + lightningcss@1.30.2:
2167 + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
2168 engines: {node: '>= 12.0.0'}
2169
2170 local-pkg@1.1.2:
@@ -2128,6 +2197,9 @@ packages:
2197 magic-string@0.30.19:
2198 resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
2199
2200 + magic-string@0.30.21:
2201 + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
2202 +
2203 markdown-table@3.0.4:
2204 resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
2205
@@ -2181,10 +2253,6 @@ packages:
2253 resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
2254 engines: {node: '>= 0.10.0'}
2255
2184 - merge2@1.4.1:
2185 - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
2186 - engines: {node: '>= 8'}
2187 -
2256 micromark-core-commonmark@2.0.3:
2257 resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
2258
@@ -2291,22 +2359,9 @@ packages:
2359 resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
2360 engines: {node: '>=16 || 14 >=14.17'}
2361
2294 - minipass@7.1.2:
2295 - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
2296 - engines: {node: '>=16 || 14 >=14.17'}
2297 -
2298 - minizlib@3.0.2:
2299 - resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==}
2300 - engines: {node: '>= 18'}
2301 -
2362 mitt@3.0.1:
2363 resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
2364
2305 - mkdirp@3.0.1:
2306 - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
2307 - engines: {node: '>=10'}
2308 - hasBin: true
2309 -
2365 mlly@1.8.0:
2366 resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
2367
@@ -2320,8 +2375,8 @@ packages:
2375 muggle-string@0.4.1:
2376 resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
2377
2323 - naive-ui@2.43.1:
2324 - resolution: {integrity: sha512-w52W0mOhdOGt4uucFSZmP0DI44PCsFyuxeLSs9aoUThfIuxms90MYjv46Qrr7xprjyJRw5RU6vYpCx4o9ind3A==}
2378 + naive-ui@2.43.2:
2379 + resolution: {integrity: sha512-YlLMnGrwGTOc+zMj90sG3ubaH5/7czsgLgGcjTLA981IUaz8r6t4WIujNt8r9PNr+dqv6XNEr0vxkARgPPjfBQ==}
2380 peerDependencies:
2381 vue: ^3.0.0
2382
@@ -2330,8 +2385,8 @@ packages:
2385 engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
2386 hasBin: true
2387
2333 - nanoid@5.1.5:
2334 - resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
2388 + nanoid@5.1.6:
2389 + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==}
2390 engines: {node: ^18 || >=20}
2391 hasBin: true
2392
@@ -2345,9 +2400,21 @@ packages:
2400 node-addon-api@7.1.1:
2401 resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
2402
2403 + node-fetch@2.7.0:
2404 + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
2405 + engines: {node: 4.x || >=6.0.0}
2406 + peerDependencies:
2407 + encoding: ^0.1.0
2408 + peerDependenciesMeta:
2409 + encoding:
2410 + optional: true
2411 +
2412 node-releases@2.0.21:
2413 resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==}
2414
2415 + node-releases@2.0.27:
2416 + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
2417 +
2418 npm-normalize-package-bin@4.0.0:
2419 resolution: {integrity: sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==}
2420 engines: {node: ^18.17.0 || >=20.5.0}
@@ -2357,13 +2424,12 @@ packages:
2424 engines: {node: ^20.5.0 || >=22.0.0, npm: '>= 10'}
2425 hasBin: true
2426
2360 - npm-run-path@6.0.0:
2361 - resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
2362 - engines: {node: '>=18'}
2363 -
2427 nth-check@2.1.1:
2428 resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
2429
2430 + object-deep-merge@2.0.0:
2431 + resolution: {integrity: sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg==}
2432 +
2433 ohash@2.0.11:
2434 resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
2435
@@ -2397,10 +2463,6 @@ packages:
2463 parse-imports-exports@0.2.4:
2464 resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==}
2465
2400 - parse-ms@4.0.0:
2401 - resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
2402 - engines: {node: '>=18'}
2403 -
2466 parse-statements@1.0.11:
2467 resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==}
2468
@@ -2415,13 +2477,13 @@ packages:
2477 resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
2478 engines: {node: '>=8'}
2479
2418 - path-key@4.0.0:
2419 - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
2420 - engines: {node: '>=12'}
2421 -
2480 pathe@2.0.3:
2481 resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
2482
2483 + peek-readable@4.1.0:
2484 + resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==}
2485 + engines: {node: '>=8'}
2486 +
2487 perfect-debounce@1.0.0:
2488 resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
2489
@@ -2444,8 +2506,8 @@ packages:
2506 engines: {node: '>=0.10'}
2507 hasBin: true
2508
2447 - pinia-plugin-persistedstate@4.5.0:
2448 - resolution: {integrity: sha512-QTkP1xJVyCdr2I2p3AKUZM84/e+IS+HktRxKGAIuDzkyaKKV48mQcYkJFVVDuvTxlI5j6X3oZObpqoVB8JnWpw==}
2509 + pinia-plugin-persistedstate@4.7.1:
2510 + resolution: {integrity: sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ==}
2511 peerDependencies:
2512 '@nuxt/kit': '>=3.0.0'
2513 '@pinia/nuxt': '>=0.10.0'
@@ -2458,15 +2520,19 @@ packages:
2520 pinia:
2521 optional: true
2522
2461 - pinia@3.0.3:
2462 - resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==}
2523 + pinia@3.0.4:
2524 + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==}
2525 peerDependencies:
2464 - typescript: '>=4.4.4'
2465 - vue: ^2.7.0 || ^3.5.11
2526 + typescript: '>=4.5.0'
2527 + vue: ^3.5.11
2528 peerDependenciesMeta:
2529 typescript:
2530 optional: true
2531
2532 + pixelmatch@4.0.2:
2533 + resolution: {integrity: sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==}
2534 + hasBin: true
2535 +
2536 pkg-types@1.3.1:
2537 resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
2538
@@ -2477,11 +2543,19 @@ packages:
2543 resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
2544 engines: {node: '>=4'}
2545
2480 - pnpm-workspace-yaml@1.1.1:
2481 - resolution: {integrity: sha512-nGBB7h3Ped3g9dBrR6d3YNwXCKYsEg8K9J3GMmSrwGEXq3RHeGW44/B4MZW51p4FRMnyxJzTY5feSBbUjRhIHQ==}
2546 + pngjs@3.4.0:
2547 + resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}
2548 + engines: {node: '>=4.0.0'}
2549 +
2550 + pngjs@7.0.0:
2551 + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
2552 + engines: {node: '>=14.19.0'}
2553
2483 - postcss-selector-parser@6.1.2:
2484 - resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
2554 + pnpm-workspace-yaml@1.3.0:
2555 + resolution: {integrity: sha512-Krb5q8Totd5mVuLx7we+EFHq/AfxA75nbfTm25Q1pIf606+RlaKUG+PXH8SDihfe5b5k4H09gE+sL47L1t5lbw==}
2556 +
2557 + postcss-selector-parser@7.1.1:
2558 + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
2559 engines: {node: '>=4'}
2560
2561 postcss@8.5.6:
@@ -2492,9 +2566,9 @@ packages:
2566 resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
2567 engines: {node: '>= 0.8.0'}
2568
2495 - prettier-plugin-tailwindcss@0.6.14:
2496 - resolution: {integrity: sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==}
2497 - engines: {node: '>=14.21.3'}
2569 + prettier-plugin-tailwindcss@0.7.1:
2570 + resolution: {integrity: sha512-Bzv1LZcuiR1Sk02iJTS1QzlFNp/o5l2p3xkopwOrbPmtMeh3fK9rVW5M3neBQzHq+kGKj/4LGQMTNcTH4NGPtQ==}
2571 + engines: {node: '>=20.19'}
2572 peerDependencies:
2573 '@ianvs/prettier-plugin-sort-imports': '*'
2574 '@prettier/plugin-hermes': '*'
@@ -2506,14 +2580,12 @@ packages:
2580 prettier: ^3.0
2581 prettier-plugin-astro: '*'
2582 prettier-plugin-css-order: '*'
2509 - prettier-plugin-import-sort: '*'
2583 prettier-plugin-jsdoc: '*'
2584 prettier-plugin-marko: '*'
2585 prettier-plugin-multiline-arrays: '*'
2586 prettier-plugin-organize-attributes: '*'
2587 prettier-plugin-organize-imports: '*'
2588 prettier-plugin-sort-imports: '*'
2516 - prettier-plugin-style-order: '*'
2589 prettier-plugin-svelte: '*'
2590 peerDependenciesMeta:
2591 '@ianvs/prettier-plugin-sort-imports':
@@ -2534,8 +2606,6 @@ packages:
2606 optional: true
2607 prettier-plugin-css-order:
2608 optional: true
2537 - prettier-plugin-import-sort:
2538 - optional: true
2609 prettier-plugin-jsdoc:
2610 optional: true
2611 prettier-plugin-marko:
@@ -2548,19 +2618,17 @@ packages:
2618 optional: true
2619 prettier-plugin-sort-imports:
2620 optional: true
2551 - prettier-plugin-style-order:
2552 - optional: true
2621 prettier-plugin-svelte:
2622 optional: true
2623
2556 - prettier@3.6.2:
2557 - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
2624 + prettier@3.7.3:
2625 + resolution: {integrity: sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==}
2626 engines: {node: '>=14'}
2627 hasBin: true
2628
2561 - pretty-ms@9.3.0:
2562 - resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
2563 - engines: {node: '>=18'}
2629 + process@0.11.10:
2630 + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
2631 + engines: {node: '>= 0.6.0'}
2632
2633 proxy-from-env@1.1.0:
2634 resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
@@ -2572,13 +2640,18 @@ packages:
2640 quansync@0.2.11:
2641 resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
2642
2575 - queue-microtask@1.2.3:
2576 - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
2577 -
2643 read-package-json-fast@4.0.0:
2644 resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==}
2645 engines: {node: ^18.17.0 || >=20.5.0}
2646
2647 + readable-stream@4.7.0:
2648 + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==}
2649 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
2650 +
2651 + readable-web-to-node-stream@3.0.4:
2652 + resolution: {integrity: sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==}
2653 + engines: {node: '>=8'}
2654 +
2655 readdirp@4.1.2:
2656 resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
2657 engines: {node: '>= 14.18.0'}
@@ -2587,6 +2660,9 @@ packages:
2660 resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==}
2661 engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
2662
2663 + regenerator-runtime@0.13.11:
2664 + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
2665 +
2666 regexp-ast-analysis@0.7.1:
2667 resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==}
2668 engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
@@ -2595,14 +2671,18 @@ packages:
2671 resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==}
2672 hasBin: true
2673
2598 - regjsparser@0.12.0:
2599 - resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==}
2674 + regjsparser@0.13.0:
2675 + resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==}
2676 hasBin: true
2677
2678 require-directory@2.1.1:
2679 resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
2680 engines: {node: '>=0.10.0'}
2681
2682 + reserved-identifiers@1.2.0:
2683 + resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==}
2684 + engines: {node: '>=18'}
2685 +
2686 resolve-from@4.0.0:
2687 resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
2688 engines: {node: '>=4'}
@@ -2610,10 +2690,6 @@ packages:
2690 resolve-pkg-maps@1.0.0:
2691 resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
2692
2613 - reusify@1.1.0:
2614 - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
2615 - engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
2616 -
2693 rfdc@1.4.1:
2694 resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
2695
@@ -2626,14 +2702,14 @@ packages:
2702 resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
2703 engines: {node: '>=18'}
2704
2629 - run-parallel@1.2.0:
2630 - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
2631 -
2705 rxjs@7.8.2:
2706 resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
2707
2635 - sass@1.92.1:
2636 - resolution: {integrity: sha512-ffmsdbwqb3XeyR8jJR6KelIXARM9bFQe8A6Q3W4Klmwy5Ckd5gz7jgUNHo4UOqutU5Sk1DtKLbpDP0nLCg1xqQ==}
2708 + safe-buffer@5.2.1:
2709 + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
2710 +
2711 + sass@1.94.2:
2712 + resolution: {integrity: sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==}
2713 engines: {node: '>=14.0.0'}
2714 hasBin: true
2715
@@ -2657,6 +2733,11 @@ packages:
2733 engines: {node: '>=10'}
2734 hasBin: true
2735
2736 + semver@7.7.3:
2737 + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
2738 + engines: {node: '>=10'}
2739 + hasBin: true
2740 +
2741 shebang-command@2.0.0:
2742 resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
2743 engines: {node: '>=8'}
@@ -2669,10 +2750,6 @@ packages:
2750 resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
2751 engines: {node: '>= 0.4'}
2752
2672 - signal-exit@4.1.0:
2673 - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
2674 - engines: {node: '>=14'}
2675 -
2753 sirv@3.0.2:
2754 resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
2755 engines: {node: '>=18'}
@@ -2684,9 +2761,6 @@ packages:
2761 resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
2762 engines: {node: '>=0.10.0'}
2763
2687 - spawn-command@0.0.2:
2688 - resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==}
2689 -
2764 spdx-exceptions@2.5.0:
2765 resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==}
2766
@@ -2704,22 +2778,29 @@ packages:
2778 resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
2779 engines: {node: '>=8'}
2780
2781 + string_decoder@1.3.0:
2782 + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
2783 +
2784 strip-ansi@6.0.1:
2785 resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
2786 engines: {node: '>=8'}
2787
2711 - strip-final-newline@4.0.0:
2712 - resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==}
2713 - engines: {node: '>=18'}
2714 -
2715 - strip-indent@4.1.0:
2716 - resolution: {integrity: sha512-OA95x+JPmL7kc7zCu+e+TeYxEiaIyndRx0OrBcK2QPPH09oAndr2ALvymxWA+Lx1PYYvFUm4O63pRkdJAaW96w==}
2788 + strip-indent@4.1.1:
2789 + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==}
2790 engines: {node: '>=12'}
2791
2792 strip-json-comments@3.1.1:
2793 resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
2794 engines: {node: '>=8'}
2795
2796 + strtok3@10.3.4:
2797 + resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
2798 + engines: {node: '>=18'}
2799 +
2800 + strtok3@6.3.0:
2801 + resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==}
2802 + engines: {node: '>=10'}
2803 +
2804 superjson@2.2.2:
2805 resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}
2806 engines: {node: '>=16'}
@@ -2741,16 +2822,15 @@ packages:
2822 resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
2823 engines: {node: ^14.18.0 || >=16.0.0}
2824
2744 - tailwindcss@4.1.13:
2745 - resolution: {integrity: sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==}
2825 + tailwindcss@4.1.17:
2826 + resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==}
2827
2828 tapable@2.2.3:
2829 resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==}
2830 engines: {node: '>=6'}
2831
2751 - tar@7.4.3:
2752 - resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
2753 - engines: {node: '>=18'}
2832 + tinycolor2@1.6.0:
2833 + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
2834
2835 tinyexec@1.0.1:
2836 resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==}
@@ -2759,10 +2839,29 @@ packages:
2839 resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
2840 engines: {node: '>=12.0.0'}
2841
2842 + to-data-view@1.1.0:
2843 + resolution: {integrity: sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==}
2844 +
2845 + to-data-view@2.0.0:
2846 + resolution: {integrity: sha512-RGEM5KqlPHr+WVTPmGNAXNeFEmsBnlkxXaIfEpUYV0AST2Z5W1EGq9L/MENFrMMmL2WQr1wjkmZy/M92eKhjYA==}
2847 + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
2848 +
2849 to-regex-range@5.0.1:
2850 resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
2851 engines: {node: '>=8.0'}
2852
2853 + to-valid-identifier@1.0.0:
2854 + resolution: {integrity: sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==}
2855 + engines: {node: '>=20'}
2856 +
2857 + token-types@4.2.1:
2858 + resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==}
2859 + engines: {node: '>=10'}
2860 +
2861 + token-types@6.1.1:
2862 + resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==}
2863 + engines: {node: '>=14.16'}
2864 +
2865 toml-eslint-parser@0.10.0:
2866 resolution: {integrity: sha512-khrZo4buq4qVmsGzS5yQjKe/WsFvV8fGfOjDQN0q4iy9FjRfPWRgTFrU8u1R2iu/SfWLhY9WnCi4Jhdrcbtg+g==}
2867 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -2771,6 +2870,9 @@ packages:
2870 resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
2871 engines: {node: '>=6'}
2872
2873 + tr46@0.0.3:
2874 + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
2875 +
2876 tree-kill@1.2.2:
2877 resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
2878 hasBin: true
@@ -2796,21 +2898,21 @@ packages:
2898 resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
2899 engines: {node: '>= 0.8.0'}
2900
2799 - typescript@5.8.3:
2800 - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
2901 + typescript@5.9.3:
2902 + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
2903 engines: {node: '>=14.17'}
2904 hasBin: true
2905
2906 ufo@1.6.1:
2907 resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
2908
2807 - undici-types@7.12.0:
2808 - resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==}
2809 -
2810 - unicorn-magic@0.3.0:
2811 - resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
2909 + uint8array-extras@1.5.0:
2910 + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==}
2911 engines: {node: '>=18'}
2912
2913 + undici-types@7.16.0:
2914 + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
2915 +
2916 unist-util-is@6.0.0:
2917 resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
2918
@@ -2833,6 +2935,12 @@ packages:
2935 peerDependencies:
2936 browserslist: '>= 4.21.0'
2937
2938 + update-browserslist-db@1.1.4:
2939 + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==}
2940 + hasBin: true
2941 + peerDependencies:
2942 + browserslist: '>= 4.21.0'
2943 +
2944 uri-js@4.4.1:
2945 resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
2946
@@ -2864,8 +2972,8 @@ packages:
2972 '@nuxt/kit':
2973 optional: true
2974
2867 - vite-plugin-vue-devtools@8.0.2:
2868 - resolution: {integrity: sha512-1069qvMBcyAu3yXQlvYrkwoyLOk0lSSR/gTKy/vy+Det7TXnouGei6ZcKwr5TIe938v/14oLlp0ow6FSJkkORA==}
2975 + vite-plugin-vue-devtools@8.0.5:
2976 + resolution: {integrity: sha512-p619BlKFOqQXJ6uDWS1vUPQzuJOD6xJTfftj57JXBGoBD/yeQCowR7pnWcr/FEX4/HVkFbreI6w2uuGBmQOh6A==}
2977 engines: {node: '>=v14.21.3'}
2978 peerDependencies:
2979 vite: ^6.0.0 || ^7.0.0-0
@@ -2880,8 +2988,8 @@ packages:
2988 peerDependencies:
2989 vue: '>=3.2.13'
2990
2883 - vite@7.1.5:
2884 - resolution: {integrity: sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==}
2991 + vite@7.2.4:
2992 + resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==}
2993 engines: {node: ^20.19.0 || >=22.12.0}
2994 hasBin: true
2995 peerDependencies:
@@ -2934,19 +3042,19 @@ packages:
3042 peerDependencies:
3043 eslint: ^8.57.0 || ^9.0.0
3044
2937 - vue-router@4.5.1:
2938 - resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==}
3045 + vue-router@4.6.3:
3046 + resolution: {integrity: sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==}
3047 peerDependencies:
2940 - vue: ^3.2.0
3048 + vue: ^3.5.0
3049
2942 - vue-tsc@3.0.7:
2943 - resolution: {integrity: sha512-BSMmW8GGEgHykrv7mRk6zfTdK+tw4MBZY/x6fFa7IkdXK3s/8hQRacPjG9/8YKFDIWGhBocwi6PlkQQ/93OgIQ==}
3050 + vue-tsc@3.1.5:
3051 + resolution: {integrity: sha512-L/G9IUjOWhBU0yun89rv8fKqmKC+T0HfhrFjlIml71WpfBv9eb4E9Bev8FMbyueBIU9vxQqbd+oOsVcDa5amGw==}
3052 hasBin: true
3053 peerDependencies:
3054 typescript: '>=5.0.0'
3055
2948 - vue@3.5.21:
2949 - resolution: {integrity: sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==}
3056 + vue@3.5.25:
3057 + resolution: {integrity: sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==}
3058 peerDependencies:
3059 typescript: '*'
3060 peerDependenciesMeta:
@@ -2958,6 +3066,15 @@ packages:
3066 peerDependencies:
3067 vue: ^3.0.11
3068
3069 + webidl-conversions@3.0.1:
3070 + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
3071 +
3072 + whatwg-fetch@3.6.20:
3073 + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==}
3074 +
3075 + whatwg-url@5.0.0:
3076 + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
3077 +
3078 which@2.0.2:
3079 resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
3080 engines: {node: '>= 8'}
@@ -2991,10 +3108,6 @@ packages:
3108 yallist@3.1.1:
3109 resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
3110
2994 - yallist@5.0.0:
2995 - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
2996 - engines: {node: '>=18'}
2997 -
3111 yaml-eslint-parser@1.3.0:
3112 resolution: {integrity: sha512-E/+VitOorXSLiAqtTd7Yqax0/pAS3xaYMP+AUUJGOK1OZG3rhcj9fcJOM5HJ2VrP1FrStVCWr1muTfQCdj4tAA==}
3113 engines: {node: ^14.17.0 || >=16.0.0}
@@ -3016,10 +3129,6 @@ packages:
3129 resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
3130 engines: {node: '>=10'}
3131
3019 - yoctocolors@2.1.2:
3020 - resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
3021 - engines: {node: '>=18'}
3022 -
3132 zwitch@2.0.4:
3133 resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
3134
@@ -3029,44 +3138,44 @@ snapshots:
3138 dependencies:
3139 lodash: 4.17.21
3140
3032 - '@antfu/eslint-config@5.3.0(@vue/compiler-sfc@3.5.21)(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)':
3141 + '@antfu/eslint-config@6.2.0(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
3142 dependencies:
3143 '@antfu/install-pkg': 1.1.0
3144 '@clack/prompts': 0.11.0
3036 - '@eslint-community/eslint-plugin-eslint-comments': 4.5.0(eslint@9.35.0(jiti@2.5.1))
3037 - '@eslint/markdown': 7.2.0
3038 - '@stylistic/eslint-plugin': 5.3.1(eslint@9.35.0(jiti@2.5.1))
3039 - '@typescript-eslint/eslint-plugin': 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)
3040 - '@typescript-eslint/parser': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)
3041 - '@vitest/eslint-plugin': 1.3.10(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)
3042 - ansis: 4.1.0
3145 + '@eslint-community/eslint-plugin-eslint-comments': 4.5.0(eslint@9.39.1(jiti@2.6.1))
3146 + '@eslint/markdown': 7.5.1
3147 + '@stylistic/eslint-plugin': 5.6.1(eslint@9.39.1(jiti@2.6.1))
3148 + '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
3149 + '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
3150 + '@vitest/eslint-plugin': 1.5.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
3151 + ansis: 4.2.0
3152 cac: 6.7.14
3044 - eslint: 9.35.0(jiti@2.5.1)
3045 - eslint-config-flat-gitignore: 2.1.0(eslint@9.35.0(jiti@2.5.1))
3046 - eslint-flat-config-utils: 2.1.1
3047 - eslint-merge-processors: 2.0.0(eslint@9.35.0(jiti@2.5.1))
3048 - eslint-plugin-antfu: 3.1.1(eslint@9.35.0(jiti@2.5.1))
3049 - eslint-plugin-command: 3.3.1(eslint@9.35.0(jiti@2.5.1))
3050 - eslint-plugin-import-lite: 0.3.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)
3051 - eslint-plugin-jsdoc: 54.7.0(eslint@9.35.0(jiti@2.5.1))
3052 - eslint-plugin-jsonc: 2.20.1(eslint@9.35.0(jiti@2.5.1))
3053 - eslint-plugin-n: 17.23.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)
3153 + eslint: 9.39.1(jiti@2.6.1)
3154 + eslint-config-flat-gitignore: 2.1.0(eslint@9.39.1(jiti@2.6.1))
3155 + eslint-flat-config-utils: 2.1.4
3156 + eslint-merge-processors: 2.0.0(eslint@9.39.1(jiti@2.6.1))
3157 + eslint-plugin-antfu: 3.1.1(eslint@9.39.1(jiti@2.6.1))
3158 + eslint-plugin-command: 3.3.1(eslint@9.39.1(jiti@2.6.1))
3159 + eslint-plugin-import-lite: 0.3.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
3160 + eslint-plugin-jsdoc: 61.4.1(eslint@9.39.1(jiti@2.6.1))
3161 + eslint-plugin-jsonc: 2.21.0(eslint@9.39.1(jiti@2.6.1))
3162 + eslint-plugin-n: 17.23.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
3163 eslint-plugin-no-only-tests: 3.3.0
3055 - eslint-plugin-perfectionist: 4.15.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)
3056 - eslint-plugin-pnpm: 1.1.1(eslint@9.35.0(jiti@2.5.1))
3057 - eslint-plugin-regexp: 2.10.0(eslint@9.35.0(jiti@2.5.1))
3058 - eslint-plugin-toml: 0.12.0(eslint@9.35.0(jiti@2.5.1))
3059 - eslint-plugin-unicorn: 61.0.2(eslint@9.35.0(jiti@2.5.1))
3060 - eslint-plugin-unused-imports: 4.2.0(@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.35.0(jiti@2.5.1))
3061 - eslint-plugin-vue: 10.4.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.35.0(jiti@2.5.1))(vue-eslint-parser@10.2.0(eslint@9.35.0(jiti@2.5.1)))
3062 - eslint-plugin-yml: 1.18.0(eslint@9.35.0(jiti@2.5.1))
3063 - eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.21)(eslint@9.35.0(jiti@2.5.1))
3164 + eslint-plugin-perfectionist: 4.15.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
3165 + eslint-plugin-pnpm: 1.3.0(eslint@9.39.1(jiti@2.6.1))
3166 + eslint-plugin-regexp: 2.10.0(eslint@9.39.1(jiti@2.6.1))
3167 + eslint-plugin-toml: 0.12.0(eslint@9.39.1(jiti@2.6.1))
3168 + eslint-plugin-unicorn: 62.0.0(eslint@9.39.1(jiti@2.6.1))
3169 + eslint-plugin-unused-imports: 4.3.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))
3170 + eslint-plugin-vue: 10.6.2(@stylistic/eslint-plugin@5.6.1(eslint@9.39.1(jiti@2.6.1)))(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1)))
3171 + eslint-plugin-yml: 1.19.0(eslint@9.39.1(jiti@2.6.1))
3172 + eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1))
3173 globals: 16.4.0
3065 - jsonc-eslint-parser: 2.4.0
3174 + jsonc-eslint-parser: 2.4.1
3175 local-pkg: 1.1.2
3176 parse-gitignore: 2.0.0
3177 toml-eslint-parser: 0.10.0
3069 - vue-eslint-parser: 10.2.0(eslint@9.35.0(jiti@2.5.1))
3178 + vue-eslint-parser: 10.2.0(eslint@9.39.1(jiti@2.6.1))
3179 yaml-eslint-parser: 1.3.0
3180 transitivePeerDependencies:
3181 - '@eslint/json'
@@ -3192,6 +3301,8 @@ snapshots:
3301
3302 '@babel/helper-validator-identifier@7.27.1': {}
3303
3304 + '@babel/helper-validator-identifier@7.28.5': {}
3305 +
3306 '@babel/helper-validator-option@7.27.1': {}
3307
3308 '@babel/helpers@7.28.4':
@@ -3203,6 +3314,10 @@ snapshots:
3314 dependencies:
3315 '@babel/types': 7.28.4
3316
3317 + '@babel/parser@7.28.5':
3318 + dependencies:
3319 + '@babel/types': 7.28.5
3320 +
3321 '@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.28.4)':
3322 dependencies:
3323 '@babel/core': 7.28.4
@@ -3248,8 +3363,6 @@ snapshots:
3363 transitivePeerDependencies:
3364 - supports-color
3365
3251 - '@babel/runtime@7.28.4': {}
3252 -
3366 '@babel/template@7.27.2':
3367 dependencies:
3368 '@babel/code-frame': 7.27.1
@@ -3273,6 +3386,15 @@ snapshots:
3386 '@babel/helper-string-parser': 7.27.1
3387 '@babel/helper-validator-identifier': 7.27.1
3388
3389 + '@babel/types@7.28.5':
3390 + dependencies:
3391 + '@babel/helper-string-parser': 7.27.1
3392 + '@babel/helper-validator-identifier': 7.28.5
3393 +
3394 + '@borewit/text-codec@0.1.1': {}
3395 +
3396 + '@canvas/image-data@1.1.0': {}
3397 +
3398 '@clack/core@0.5.0':
3399 dependencies:
3400 picocolors: 1.1.1
@@ -3288,27 +3410,29 @@ snapshots:
3410 dependencies:
3411 css-render: 0.15.14
3412
3291 - '@css-render/vue3-ssr@0.15.14(vue@3.5.21(typescript@5.8.3))':
3413 + '@css-render/vue3-ssr@0.15.14(vue@3.5.25(typescript@5.9.3))':
3414 dependencies:
3293 - vue: 3.5.21(typescript@5.8.3)
3415 + vue: 3.5.25(typescript@5.9.3)
3416
3417 '@emotion/hash@0.8.0': {}
3418
3419 '@es-joy/jsdoccomment@0.50.2':
3420 dependencies:
3421 '@types/estree': 1.0.8
3300 - '@typescript-eslint/types': 8.44.0
3422 + '@typescript-eslint/types': 8.48.0
3423 comment-parser: 1.4.1
3424 esquery: 1.6.0
3425 jsdoc-type-pratt-parser: 4.1.0
3426
3305 - '@es-joy/jsdoccomment@0.56.0':
3427 + '@es-joy/jsdoccomment@0.76.0':
3428 dependencies:
3429 '@types/estree': 1.0.8
3308 - '@typescript-eslint/types': 8.44.0
3430 + '@typescript-eslint/types': 8.48.0
3431 comment-parser: 1.4.1
3432 esquery: 1.6.0
3311 - jsdoc-type-pratt-parser: 5.1.1
3433 + jsdoc-type-pratt-parser: 6.10.0
3434 +
3435 + '@es-joy/resolve.exports@1.2.0': {}
3436
3437 '@esbuild/aix-ppc64@0.25.9':
3438 optional: true
@@ -3388,34 +3512,36 @@ snapshots:
3512 '@esbuild/win32-x64@0.25.9':
3513 optional: true
3514
3391 - '@eslint-community/eslint-plugin-eslint-comments@4.5.0(eslint@9.35.0(jiti@2.5.1))':
3515 + '@eslint-community/eslint-plugin-eslint-comments@4.5.0(eslint@9.39.1(jiti@2.6.1))':
3516 dependencies:
3517 escape-string-regexp: 4.0.0
3394 - eslint: 9.35.0(jiti@2.5.1)
3518 + eslint: 9.39.1(jiti@2.6.1)
3519 ignore: 5.3.2
3520
3397 - '@eslint-community/eslint-utils@4.9.0(eslint@9.35.0(jiti@2.5.1))':
3521 + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))':
3522 dependencies:
3399 - eslint: 9.35.0(jiti@2.5.1)
3523 + eslint: 9.39.1(jiti@2.6.1)
3524 eslint-visitor-keys: 3.4.3
3525
3526 '@eslint-community/regexpp@4.12.1': {}
3527
3404 - '@eslint/compat@1.3.2(eslint@9.35.0(jiti@2.5.1))':
3528 + '@eslint/compat@1.3.2(eslint@9.39.1(jiti@2.6.1))':
3529 optionalDependencies:
3406 - eslint: 9.35.0(jiti@2.5.1)
3530 + eslint: 9.39.1(jiti@2.6.1)
3531
3408 - '@eslint/config-array@0.21.0':
3532 + '@eslint/config-array@0.21.1':
3533 dependencies:
3410 - '@eslint/object-schema': 2.1.6
3534 + '@eslint/object-schema': 2.1.7
3535 debug: 4.4.3
3536 minimatch: 3.1.2
3537 transitivePeerDependencies:
3538 - supports-color
3539
3416 - '@eslint/config-helpers@0.3.1': {}
3540 + '@eslint/config-helpers@0.4.2':
3541 + dependencies:
3542 + '@eslint/core': 0.17.0
3543
3418 - '@eslint/core@0.15.2':
3544 + '@eslint/core@0.17.0':
3545 dependencies:
3546 '@types/json-schema': 7.0.15
3547
@@ -3433,12 +3559,12 @@ snapshots:
3559 transitivePeerDependencies:
3560 - supports-color
3561
3436 - '@eslint/js@9.35.0': {}
3562 + '@eslint/js@9.39.1': {}
3563
3438 - '@eslint/markdown@7.2.0':
3564 + '@eslint/markdown@7.5.1':
3565 dependencies:
3440 - '@eslint/core': 0.15.2
3441 - '@eslint/plugin-kit': 0.3.5
3566 + '@eslint/core': 0.17.0
3567 + '@eslint/plugin-kit': 0.4.1
3568 github-slugger: 2.0.0
3569 mdast-util-from-markdown: 2.0.2
3570 mdast-util-frontmatter: 2.0.1
@@ -3449,11 +3575,11 @@ snapshots:
3575 transitivePeerDependencies:
3576 - supports-color
3577
3452 - '@eslint/object-schema@2.1.6': {}
3578 + '@eslint/object-schema@2.1.7': {}
3579
3454 - '@eslint/plugin-kit@0.3.5':
3580 + '@eslint/plugin-kit@0.4.1':
3581 dependencies:
3456 - '@eslint/core': 0.15.2
3582 + '@eslint/core': 0.17.0
3583 levn: 0.4.1
3584
3585 '@humanfs/core@0.19.1': {}
@@ -3469,14 +3595,39 @@ snapshots:
3595
3596 '@iconify/types@2.0.0': {}
3597
3472 - '@iconify/vue@5.0.0(vue@3.5.21(typescript@5.8.3))':
3598 + '@iconify/vue@5.0.0(vue@3.5.25(typescript@5.9.3))':
3599 dependencies:
3600 '@iconify/types': 2.0.0
3475 - vue: 3.5.21(typescript@5.8.3)
3601 + vue: 3.5.25(typescript@5.9.3)
3602 +
3603 + '@jimp/bmp@0.22.12(@jimp/custom@0.22.12)':
3604 + dependencies:
3605 + '@jimp/custom': 0.22.12
3606 + '@jimp/utils': 0.22.12
3607 + bmp-js: 0.1.0
3608 +
3609 + '@jimp/core@0.22.12':
3610 + dependencies:
3611 + '@jimp/utils': 0.22.12
3612 + any-base: 1.1.0
3613 + buffer: 5.7.1
3614 + exif-parser: 0.1.12
3615 + file-type: 16.5.4
3616 + isomorphic-fetch: 3.0.0
3617 + pixelmatch: 4.0.2
3618 + tinycolor2: 1.6.0
3619 + transitivePeerDependencies:
3620 + - encoding
3621
3477 - '@isaacs/fs-minipass@4.0.1':
3622 + '@jimp/custom@0.22.12':
3623 dependencies:
3479 - minipass: 7.1.2
3624 + '@jimp/core': 0.22.12
3625 + transitivePeerDependencies:
3626 + - encoding
3627 +
3628 + '@jimp/utils@0.22.12':
3629 + dependencies:
3630 + regenerator-runtime: 0.13.11
3631
3632 '@jridgewell/gen-mapping@0.3.13':
3633 dependencies:
@@ -3499,18 +3650,6 @@ snapshots:
3650
3651 '@juggle/resize-observer@3.4.0': {}
3652
3502 - '@nodelib/fs.scandir@2.1.5':
3503 - dependencies:
3504 - '@nodelib/fs.stat': 2.0.5
3505 - run-parallel: 1.2.0
3506 -
3507 - '@nodelib/fs.stat@2.0.5': {}
3508 -
3509 - '@nodelib/fs.walk@1.2.8':
3510 - dependencies:
3511 - '@nodelib/fs.scandir': 2.1.5
3512 - fastq: 1.19.1
3513 -
3653 '@parcel/watcher-android-arm64@2.5.1':
3654 optional: true
3655
@@ -3576,7 +3715,7 @@ snapshots:
3715
3716 '@polka/url@1.0.0-next.29': {}
3717
3579 - '@rolldown/pluginutils@1.0.0-beta.29': {}
3718 + '@rolldown/pluginutils@1.0.0-beta.50': {}
3719
3720 '@rollup/rollup-android-arm-eabi@4.50.2':
3721 optional: true
@@ -3641,94 +3780,98 @@ snapshots:
3780 '@rollup/rollup-win32-x64-msvc@4.50.2':
3781 optional: true
3782
3644 - '@sec-ant/readable-stream@0.4.1': {}
3645 -
3646 - '@sindresorhus/merge-streams@4.0.0': {}
3783 + '@sindresorhus/base62@1.0.0': {}
3784
3648 - '@stylistic/eslint-plugin@5.3.1(eslint@9.35.0(jiti@2.5.1))':
3785 + '@stylistic/eslint-plugin@5.6.1(eslint@9.39.1(jiti@2.6.1))':
3786 dependencies:
3650 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.5.1))
3651 - '@typescript-eslint/types': 8.44.0
3652 - eslint: 9.35.0(jiti@2.5.1)
3787 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
3788 + '@typescript-eslint/types': 8.48.0
3789 + eslint: 9.39.1(jiti@2.6.1)
3790 eslint-visitor-keys: 4.2.1
3791 espree: 10.4.0
3792 estraverse: 5.3.0
3793 picomatch: 4.0.3
3794
3658 - '@tailwindcss/node@4.1.13':
3795 + '@tailwindcss/node@4.1.17':
3796 dependencies:
3797 '@jridgewell/remapping': 2.3.5
3798 enhanced-resolve: 5.18.3
3662 - jiti: 2.5.1
3663 - lightningcss: 1.30.1
3664 - magic-string: 0.30.19
3799 + jiti: 2.6.1
3800 + lightningcss: 1.30.2
3801 + magic-string: 0.30.21
3802 source-map-js: 1.2.1
3666 - tailwindcss: 4.1.13
3803 + tailwindcss: 4.1.17
3804
3668 - '@tailwindcss/oxide-android-arm64@4.1.13':
3805 + '@tailwindcss/oxide-android-arm64@4.1.17':
3806 optional: true
3807
3671 - '@tailwindcss/oxide-darwin-arm64@4.1.13':
3808 + '@tailwindcss/oxide-darwin-arm64@4.1.17':
3809 optional: true
3810
3674 - '@tailwindcss/oxide-darwin-x64@4.1.13':
3811 + '@tailwindcss/oxide-darwin-x64@4.1.17':
3812 optional: true
3813
3677 - '@tailwindcss/oxide-freebsd-x64@4.1.13':
3814 + '@tailwindcss/oxide-freebsd-x64@4.1.17':
3815 optional: true
3816
3680 - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13':
3817 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17':
3818 optional: true
3819
3683 - '@tailwindcss/oxide-linux-arm64-gnu@4.1.13':
3820 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17':
3821 optional: true
3822
3686 - '@tailwindcss/oxide-linux-arm64-musl@4.1.13':
3823 + '@tailwindcss/oxide-linux-arm64-musl@4.1.17':
3824 optional: true
3825
3689 - '@tailwindcss/oxide-linux-x64-gnu@4.1.13':
3826 + '@tailwindcss/oxide-linux-x64-gnu@4.1.17':
3827 optional: true
3828
3692 - '@tailwindcss/oxide-linux-x64-musl@4.1.13':
3829 + '@tailwindcss/oxide-linux-x64-musl@4.1.17':
3830 optional: true
3831
3695 - '@tailwindcss/oxide-wasm32-wasi@4.1.13':
3832 + '@tailwindcss/oxide-wasm32-wasi@4.1.17':
3833 optional: true
3834
3698 - '@tailwindcss/oxide-win32-arm64-msvc@4.1.13':
3835 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17':
3836 optional: true
3837
3701 - '@tailwindcss/oxide-win32-x64-msvc@4.1.13':
3838 + '@tailwindcss/oxide-win32-x64-msvc@4.1.17':
3839 optional: true
3840
3704 - '@tailwindcss/oxide@4.1.13':
3705 - dependencies:
3706 - detect-libc: 2.1.0
3707 - tar: 7.4.3
3841 + '@tailwindcss/oxide@4.1.17':
3842 optionalDependencies:
3709 - '@tailwindcss/oxide-android-arm64': 4.1.13
3710 - '@tailwindcss/oxide-darwin-arm64': 4.1.13
3711 - '@tailwindcss/oxide-darwin-x64': 4.1.13
3712 - '@tailwindcss/oxide-freebsd-x64': 4.1.13
3713 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.13
3714 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.13
3715 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.13
3716 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.13
3717 - '@tailwindcss/oxide-linux-x64-musl': 4.1.13
3718 - '@tailwindcss/oxide-wasm32-wasi': 4.1.13
3719 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.13
3720 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.13
3721 -
3722 - '@tailwindcss/vite@4.1.13(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1))':
3723 - dependencies:
3724 - '@tailwindcss/node': 4.1.13
3725 - '@tailwindcss/oxide': 4.1.13
3726 - tailwindcss: 4.1.13
3727 - vite: 7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1)
3843 + '@tailwindcss/oxide-android-arm64': 4.1.17
3844 + '@tailwindcss/oxide-darwin-arm64': 4.1.17
3845 + '@tailwindcss/oxide-darwin-x64': 4.1.17
3846 + '@tailwindcss/oxide-freebsd-x64': 4.1.17
3847 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.17
3848 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.17
3849 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.17
3850 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.17
3851 + '@tailwindcss/oxide-linux-x64-musl': 4.1.17
3852 + '@tailwindcss/oxide-wasm32-wasi': 4.1.17
3853 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17
3854 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.17
3855 +
3856 + '@tailwindcss/vite@4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))':
3857 + dependencies:
3858 + '@tailwindcss/node': 4.1.17
3859 + '@tailwindcss/oxide': 4.1.17
3860 + tailwindcss: 4.1.17
3861 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
3862 +
3863 + '@tokenizer/inflate@0.4.1':
3864 + dependencies:
3865 + debug: 4.4.3
3866 + token-types: 6.1.1
3867 + transitivePeerDependencies:
3868 + - supports-color
3869 +
3870 + '@tokenizer/token@0.3.0': {}
3871
3872 '@trysound/sax@0.2.0': {}
3873
3731 - '@tsconfig/node20@20.1.6': {}
3874 + '@tsconfig/node20@20.1.8': {}
3875
3876 '@types/debug@4.1.12':
3877 dependencies:
@@ -3742,9 +3885,9 @@ snapshots:
3885
3886 '@types/lodash-es@4.17.12':
3887 dependencies:
3745 - '@types/lodash': 4.17.20
3888 + '@types/lodash': 4.17.21
3889
3747 - '@types/lodash@4.17.20': {}
3890 + '@types/lodash@4.17.21': {}
3891
3892 '@types/mdast@4.0.4':
3893 dependencies:
@@ -3752,120 +3895,119 @@ snapshots:
3895
3896 '@types/ms@2.1.0': {}
3897
3755 - '@types/node@24.5.0':
3898 + '@types/node@24.10.1':
3899 dependencies:
3757 - undici-types: 7.12.0
3900 + undici-types: 7.16.0
3901
3902 '@types/unist@3.0.3': {}
3903
3904 '@types/web-bluetooth@0.0.21': {}
3905
3763 - '@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)':
3906 + '@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
3907 dependencies:
3908 '@eslint-community/regexpp': 4.12.1
3766 - '@typescript-eslint/parser': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)
3767 - '@typescript-eslint/scope-manager': 8.44.0
3768 - '@typescript-eslint/type-utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)
3769 - '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)
3770 - '@typescript-eslint/visitor-keys': 8.44.0
3771 - eslint: 9.35.0(jiti@2.5.1)
3909 + '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
3910 + '@typescript-eslint/scope-manager': 8.48.0
3911 + '@typescript-eslint/type-utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
3912 + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
3913 + '@typescript-eslint/visitor-keys': 8.48.0
3914 + eslint: 9.39.1(jiti@2.6.1)
3915 graphemer: 1.4.0
3916 ignore: 7.0.5
3917 natural-compare: 1.4.0
3775 - ts-api-utils: 2.1.0(typescript@5.8.3)
3776 - typescript: 5.8.3
3918 + ts-api-utils: 2.1.0(typescript@5.9.3)
3919 + typescript: 5.9.3
3920 transitivePeerDependencies:
3921 - supports-color
3922
3780 - '@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)':
3923 + '@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
3924 dependencies:
3782 - '@typescript-eslint/scope-manager': 8.44.0
3783 - '@typescript-eslint/types': 8.44.0
3784 - '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.8.3)
3785 - '@typescript-eslint/visitor-keys': 8.44.0
3925 + '@typescript-eslint/scope-manager': 8.48.0
3926 + '@typescript-eslint/types': 8.48.0
3927 + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3)
3928 + '@typescript-eslint/visitor-keys': 8.48.0
3929 debug: 4.4.3
3787 - eslint: 9.35.0(jiti@2.5.1)
3788 - typescript: 5.8.3
3930 + eslint: 9.39.1(jiti@2.6.1)
3931 + typescript: 5.9.3
3932 transitivePeerDependencies:
3933 - supports-color
3934
3792 - '@typescript-eslint/project-service@8.44.0(typescript@5.8.3)':
3935 + '@typescript-eslint/project-service@8.48.0(typescript@5.9.3)':
3936 dependencies:
3794 - '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.8.3)
3795 - '@typescript-eslint/types': 8.44.0
3937 + '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3)
3938 + '@typescript-eslint/types': 8.48.0
3939 debug: 4.4.3
3797 - typescript: 5.8.3
3940 + typescript: 5.9.3
3941 transitivePeerDependencies:
3942 - supports-color
3943
3801 - '@typescript-eslint/scope-manager@8.44.0':
3944 + '@typescript-eslint/scope-manager@8.48.0':
3945 dependencies:
3803 - '@typescript-eslint/types': 8.44.0
3804 - '@typescript-eslint/visitor-keys': 8.44.0
3946 + '@typescript-eslint/types': 8.48.0
3947 + '@typescript-eslint/visitor-keys': 8.48.0
3948
3806 - '@typescript-eslint/tsconfig-utils@8.44.0(typescript@5.8.3)':
3949 + '@typescript-eslint/tsconfig-utils@8.48.0(typescript@5.9.3)':
3950 dependencies:
3808 - typescript: 5.8.3
3951 + typescript: 5.9.3
3952
3810 - '@typescript-eslint/type-utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)':
3953 + '@typescript-eslint/type-utils@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
3954 dependencies:
3812 - '@typescript-eslint/types': 8.44.0
3813 - '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.8.3)
3814 - '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)
3955 + '@typescript-eslint/types': 8.48.0
3956 + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3)
3957 + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
3958 debug: 4.4.3
3816 - eslint: 9.35.0(jiti@2.5.1)
3817 - ts-api-utils: 2.1.0(typescript@5.8.3)
3818 - typescript: 5.8.3
3959 + eslint: 9.39.1(jiti@2.6.1)
3960 + ts-api-utils: 2.1.0(typescript@5.9.3)
3961 + typescript: 5.9.3
3962 transitivePeerDependencies:
3963 - supports-color
3964
3822 - '@typescript-eslint/types@8.44.0': {}
3965 + '@typescript-eslint/types@8.48.0': {}
3966
3824 - '@typescript-eslint/typescript-estree@8.44.0(typescript@5.8.3)':
3967 + '@typescript-eslint/typescript-estree@8.48.0(typescript@5.9.3)':
3968 dependencies:
3826 - '@typescript-eslint/project-service': 8.44.0(typescript@5.8.3)
3827 - '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.8.3)
3828 - '@typescript-eslint/types': 8.44.0
3829 - '@typescript-eslint/visitor-keys': 8.44.0
3969 + '@typescript-eslint/project-service': 8.48.0(typescript@5.9.3)
3970 + '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3)
3971 + '@typescript-eslint/types': 8.48.0
3972 + '@typescript-eslint/visitor-keys': 8.48.0
3973 debug: 4.4.3
3831 - fast-glob: 3.3.3
3832 - is-glob: 4.0.3
3974 minimatch: 9.0.5
3975 semver: 7.7.2
3835 - ts-api-utils: 2.1.0(typescript@5.8.3)
3836 - typescript: 5.8.3
3976 + tinyglobby: 0.2.15
3977 + ts-api-utils: 2.1.0(typescript@5.9.3)
3978 + typescript: 5.9.3
3979 transitivePeerDependencies:
3980 - supports-color
3981
3840 - '@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)':
3982 + '@typescript-eslint/utils@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
3983 dependencies:
3842 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.5.1))
3843 - '@typescript-eslint/scope-manager': 8.44.0
3844 - '@typescript-eslint/types': 8.44.0
3845 - '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.8.3)
3846 - eslint: 9.35.0(jiti@2.5.1)
3847 - typescript: 5.8.3
3984 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
3985 + '@typescript-eslint/scope-manager': 8.48.0
3986 + '@typescript-eslint/types': 8.48.0
3987 + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3)
3988 + eslint: 9.39.1(jiti@2.6.1)
3989 + typescript: 5.9.3
3990 transitivePeerDependencies:
3991 - supports-color
3992
3851 - '@typescript-eslint/visitor-keys@8.44.0':
3993 + '@typescript-eslint/visitor-keys@8.48.0':
3994 dependencies:
3853 - '@typescript-eslint/types': 8.44.0
3995 + '@typescript-eslint/types': 8.48.0
3996 eslint-visitor-keys: 4.2.1
3997
3856 - '@vitejs/plugin-vue@6.0.1(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1))(vue@3.5.21(typescript@5.8.3))':
3998 + '@vitejs/plugin-vue@6.0.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))':
3999 dependencies:
3858 - '@rolldown/pluginutils': 1.0.0-beta.29
3859 - vite: 7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1)
3860 - vue: 3.5.21(typescript@5.8.3)
4000 + '@rolldown/pluginutils': 1.0.0-beta.50
4001 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
4002 + vue: 3.5.25(typescript@5.9.3)
4003
3862 - '@vitest/eslint-plugin@1.3.10(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)':
4004 + '@vitest/eslint-plugin@1.5.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
4005 dependencies:
3864 - '@typescript-eslint/scope-manager': 8.44.0
3865 - '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)
3866 - eslint: 9.35.0(jiti@2.5.1)
4006 + '@typescript-eslint/scope-manager': 8.48.0
4007 + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
4008 + eslint: 9.39.1(jiti@2.6.1)
4009 optionalDependencies:
3868 - typescript: 5.8.3
4010 + typescript: 5.9.3
4011 transitivePeerDependencies:
4012 - supports-color
4013
@@ -3918,11 +4060,24 @@ snapshots:
4060 estree-walker: 2.0.2
4061 source-map-js: 1.2.1
4062
4063 + '@vue/compiler-core@3.5.25':
4064 + dependencies:
4065 + '@babel/parser': 7.28.5
4066 + '@vue/shared': 3.5.25
4067 + entities: 4.5.0
4068 + estree-walker: 2.0.2
4069 + source-map-js: 1.2.1
4070 +
4071 '@vue/compiler-dom@3.5.21':
4072 dependencies:
4073 '@vue/compiler-core': 3.5.21
4074 '@vue/shared': 3.5.21
4075
4076 + '@vue/compiler-dom@3.5.25':
4077 + dependencies:
4078 + '@vue/compiler-core': 3.5.25
4079 + '@vue/shared': 3.5.25
4080 +
4081 '@vue/compiler-sfc@3.5.21':
4082 dependencies:
4083 '@babel/parser': 7.28.4
@@ -3935,15 +4090,27 @@ snapshots:
4090 postcss: 8.5.6
4091 source-map-js: 1.2.1
4092
4093 + '@vue/compiler-sfc@3.5.25':
4094 + dependencies:
4095 + '@babel/parser': 7.28.5
4096 + '@vue/compiler-core': 3.5.25
4097 + '@vue/compiler-dom': 3.5.25
4098 + '@vue/compiler-ssr': 3.5.25
4099 + '@vue/shared': 3.5.25
4100 + estree-walker: 2.0.2
4101 + magic-string: 0.30.21
4102 + postcss: 8.5.6
4103 + source-map-js: 1.2.1
4104 +
4105 '@vue/compiler-ssr@3.5.21':
4106 dependencies:
4107 '@vue/compiler-dom': 3.5.21
4108 '@vue/shared': 3.5.21
4109
3943 - '@vue/compiler-vue2@2.7.16':
4110 + '@vue/compiler-ssr@3.5.25':
4111 dependencies:
3945 - de-indent: 1.0.2
3946 - he: 1.2.0
4112 + '@vue/compiler-dom': 3.5.25
4113 + '@vue/shared': 3.5.25
4114
4115 '@vue/devtools-api@6.6.4': {}
4116
@@ -3951,15 +4118,15 @@ snapshots:
4118 dependencies:
4119 '@vue/devtools-kit': 7.7.7
4120
3954 - '@vue/devtools-core@8.0.2(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1))(vue@3.5.21(typescript@5.8.3))':
4121 + '@vue/devtools-core@8.0.5(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))':
4122 dependencies:
3956 - '@vue/devtools-kit': 8.0.2
3957 - '@vue/devtools-shared': 8.0.2
4123 + '@vue/devtools-kit': 8.0.5
4124 + '@vue/devtools-shared': 8.0.5
4125 mitt: 3.0.1
3959 - nanoid: 5.1.5
4126 + nanoid: 5.1.6
4127 pathe: 2.0.3
3961 - vite-hot-client: 2.1.0(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1))
3962 - vue: 3.5.21(typescript@5.8.3)
4128 + vite-hot-client: 2.1.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))
4129 + vue: 3.5.25(typescript@5.9.3)
4130 transitivePeerDependencies:
4131 - vite
4132
@@ -3973,10 +4140,10 @@ snapshots:
4140 speakingurl: 14.0.1
4141 superjson: 2.2.2
4142
3976 - '@vue/devtools-kit@8.0.2':
4143 + '@vue/devtools-kit@8.0.5':
4144 dependencies:
3978 - '@vue/devtools-shared': 8.0.2
3979 - birpc: 2.5.0
4145 + '@vue/devtools-shared': 8.0.5
4146 + birpc: 2.8.0
4147 hookable: 5.5.3
4148 mitt: 3.0.1
4149 perfect-debounce: 2.0.0
@@ -3987,64 +4154,69 @@ snapshots:
4154 dependencies:
4155 rfdc: 1.4.1
4156
3990 - '@vue/devtools-shared@8.0.2':
4157 + '@vue/devtools-shared@8.0.5':
4158 dependencies:
4159 rfdc: 1.4.1
4160
3994 - '@vue/language-core@3.0.7(typescript@5.8.3)':
4161 + '@vue/language-core@3.1.5(typescript@5.9.3)':
4162 dependencies:
4163 '@volar/language-core': 2.4.23
4164 '@vue/compiler-dom': 3.5.21
3998 - '@vue/compiler-vue2': 2.7.16
4165 '@vue/shared': 3.5.21
4000 - alien-signals: 2.0.7
4166 + alien-signals: 3.1.1
4167 muggle-string: 0.4.1
4168 path-browserify: 1.0.1
4169 picomatch: 4.0.3
4170 optionalDependencies:
4005 - typescript: 5.8.3
4171 + typescript: 5.9.3
4172
4007 - '@vue/reactivity@3.5.21':
4173 + '@vue/reactivity@3.5.25':
4174 dependencies:
4009 - '@vue/shared': 3.5.21
4175 + '@vue/shared': 3.5.25
4176
4011 - '@vue/runtime-core@3.5.21':
4177 + '@vue/runtime-core@3.5.25':
4178 dependencies:
4013 - '@vue/reactivity': 3.5.21
4014 - '@vue/shared': 3.5.21
4179 + '@vue/reactivity': 3.5.25
4180 + '@vue/shared': 3.5.25
4181
4016 - '@vue/runtime-dom@3.5.21':
4182 + '@vue/runtime-dom@3.5.25':
4183 dependencies:
4018 - '@vue/reactivity': 3.5.21
4019 - '@vue/runtime-core': 3.5.21
4020 - '@vue/shared': 3.5.21
4184 + '@vue/reactivity': 3.5.25
4185 + '@vue/runtime-core': 3.5.25
4186 + '@vue/shared': 3.5.25
4187 csstype: 3.1.3
4188
4023 - '@vue/server-renderer@3.5.21(vue@3.5.21(typescript@5.8.3))':
4189 + '@vue/server-renderer@3.5.25(vue@3.5.25(typescript@5.9.3))':
4190 dependencies:
4025 - '@vue/compiler-ssr': 3.5.21
4026 - '@vue/shared': 3.5.21
4027 - vue: 3.5.21(typescript@5.8.3)
4191 + '@vue/compiler-ssr': 3.5.25
4192 + '@vue/shared': 3.5.25
4193 + vue: 3.5.25(typescript@5.9.3)
4194
4195 '@vue/shared@3.5.21': {}
4196
4031 - '@vue/tsconfig@0.7.0(typescript@5.8.3)(vue@3.5.21(typescript@5.8.3))':
4197 + '@vue/shared@3.5.25': {}
4198 +
4199 + '@vue/tsconfig@0.7.0(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))':
4200 optionalDependencies:
4033 - typescript: 5.8.3
4034 - vue: 3.5.21(typescript@5.8.3)
4201 + typescript: 5.9.3
4202 + vue: 3.5.25(typescript@5.9.3)
4203
4036 - '@vueuse/core@13.9.0(vue@3.5.21(typescript@5.8.3))':
4204 + '@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3))':
4205 dependencies:
4206 '@types/web-bluetooth': 0.0.21
4039 - '@vueuse/metadata': 13.9.0
4040 - '@vueuse/shared': 13.9.0(vue@3.5.21(typescript@5.8.3))
4041 - vue: 3.5.21(typescript@5.8.3)
4207 + '@vueuse/metadata': 14.1.0
4208 + '@vueuse/shared': 14.1.0(vue@3.5.25(typescript@5.9.3))
4209 + vue: 3.5.25(typescript@5.9.3)
4210 +
4211 + '@vueuse/metadata@14.1.0': {}
4212
4043 - '@vueuse/metadata@13.9.0': {}
4213 + '@vueuse/shared@14.1.0(vue@3.5.25(typescript@5.9.3))':
4214 + dependencies:
4215 + vue: 3.5.25(typescript@5.9.3)
4216
4045 - '@vueuse/shared@13.9.0(vue@3.5.21(typescript@5.8.3))':
4217 + abort-controller@3.0.0:
4218 dependencies:
4047 - vue: 3.5.21(typescript@5.8.3)
4219 + event-target-shim: 5.0.1
4220
4221 acorn-jsx@5.3.2(acorn@8.15.0):
4222 dependencies:
@@ -4059,7 +4231,7 @@ snapshots:
4231 json-schema-traverse: 0.4.1
4232 uri-js: 4.4.1
4233
4062 - alien-signals@2.0.7: {}
4234 + alien-signals@3.1.1: {}
4235
4236 ansi-regex@5.0.1: {}
4237
@@ -4071,6 +4243,10 @@ snapshots:
4243
4244 ansis@4.1.0: {}
4245
4246 + ansis@4.2.0: {}
4247 +
4248 + any-base@1.1.0: {}
4249 +
4250 are-docs-informative@0.0.2: {}
4251
4252 argparse@2.0.1: {}
@@ -4079,7 +4255,7 @@ snapshots:
4255
4256 asynckit@0.4.0: {}
4257
4082 - axios@1.12.2:
4258 + axios@1.13.2:
4259 dependencies:
4260 follow-redirects: 1.15.11
4261 form-data: 4.0.4
@@ -4089,10 +4265,18 @@ snapshots:
4265
4266 balanced-match@1.0.2: {}
4267
4268 + base64-js@1.5.1: {}
4269 +
4270 + baseline-browser-mapping@2.8.32: {}
4271 +
4272 baseline-browser-mapping@2.8.4: {}
4273
4274 birpc@2.5.0: {}
4275
4276 + birpc@2.8.0: {}
4277 +
4278 + bmp-js@0.1.0: {}
4279 +
4280 boolbase@1.0.0: {}
4281
4282 brace-expansion@1.1.12:
@@ -4107,6 +4291,7 @@ snapshots:
4291 braces@3.0.3:
4292 dependencies:
4293 fill-range: 7.1.1
4294 + optional: true
4295
4296 browserslist@4.26.2:
4297 dependencies:
@@ -4116,6 +4301,24 @@ snapshots:
4301 node-releases: 2.0.21
4302 update-browserslist-db: 1.1.3(browserslist@4.26.2)
4303
4304 + browserslist@4.28.0:
4305 + dependencies:
4306 + baseline-browser-mapping: 2.8.32
4307 + caniuse-lite: 1.0.30001757
4308 + electron-to-chromium: 1.5.262
4309 + node-releases: 2.0.27
4310 + update-browserslist-db: 1.1.4(browserslist@4.28.0)
4311 +
4312 + buffer@5.7.1:
4313 + dependencies:
4314 + base64-js: 1.5.1
4315 + ieee754: 1.2.1
4316 +
4317 + buffer@6.0.3:
4318 + dependencies:
4319 + base64-js: 1.5.1
4320 + ieee754: 1.2.1
4321 +
4322 builtin-modules@5.0.0: {}
4323
4324 bundle-name@4.1.0:
@@ -4133,6 +4336,8 @@ snapshots:
4336
4337 caniuse-lite@1.0.30001743: {}
4338
4339 + caniuse-lite@1.0.30001757: {}
4340 +
4341 ccount@2.0.1: {}
4342
4343 chalk@4.1.2:
@@ -4148,9 +4353,7 @@ snapshots:
4353 dependencies:
4354 readdirp: 4.1.2
4355
4151 - chownr@3.0.0: {}
4152 -
4153 - ci-info@4.3.0: {}
4356 + ci-info@4.3.1: {}
4357
4358 clean-regexp@1.0.0:
4359 dependencies:
@@ -4178,14 +4381,11 @@ snapshots:
4381
4382 concat-map@0.0.1: {}
4383
4181 - concurrently@8.2.2:
4384 + concurrently@9.2.1:
4385 dependencies:
4386 chalk: 4.1.2
4184 - date-fns: 2.30.0
4185 - lodash: 4.17.21
4387 rxjs: 7.8.2
4388 shell-quote: 1.8.3
4188 - spawn-command: 0.0.2
4389 supports-color: 8.1.1
4390 tree-kill: 1.2.2
4391 yargs: 17.7.2
@@ -4200,9 +4400,9 @@ snapshots:
4400 dependencies:
4401 is-what: 4.1.16
4402
4203 - core-js-compat@3.45.1:
4403 + core-js-compat@3.47.0:
4404 dependencies:
4205 - browserslist: 4.26.2
4405 + browserslist: 4.28.0
4406
4407 cross-spawn@7.0.6:
4408 dependencies:
@@ -4247,32 +4447,35 @@ snapshots:
4447
4448 csstype@3.1.3: {}
4449
4250 - date-fns-tz@3.2.0(date-fns@3.6.0):
4450 + date-fns-tz@3.2.0(date-fns@4.1.0):
4451 dependencies:
4252 - date-fns: 3.6.0
4253 -
4254 - date-fns@2.30.0:
4255 - dependencies:
4256 - '@babel/runtime': 7.28.4
4257 -
4258 - date-fns@3.6.0: {}
4452 + date-fns: 4.1.0
4453
4260 - dayjs@1.11.18: {}
4454 + date-fns@4.1.0: {}
4455
4262 - de-indent@1.0.2: {}
4456 + dayjs@1.11.19: {}
4457
4458 debug@4.4.3:
4459 dependencies:
4460 ms: 2.1.3
4461
4462 + decode-bmp@0.2.1:
4463 + dependencies:
4464 + '@canvas/image-data': 1.1.0
4465 + to-data-view: 1.1.0
4466 +
4467 + decode-ico@0.4.1:
4468 + dependencies:
4469 + '@canvas/image-data': 1.1.0
4470 + decode-bmp: 0.2.1
4471 + to-data-view: 1.1.0
4472 +
4473 decode-named-character-reference@1.2.0:
4474 dependencies:
4475 character-entities: 2.0.2
4476
4477 deep-is@0.1.4: {}
4478
4274 - deep-pick-omit@1.2.1: {}
4275 -
4479 default-browser-id@5.0.0: {}
4480
4481 default-browser@5.2.1:
@@ -4288,8 +4491,6 @@ snapshots:
4491
4492 dequal@2.0.3: {}
4493
4291 - destr@2.0.5: {}
4292 -
4494 detect-libc@1.0.3:
4495 optional: true
4496
@@ -4299,6 +4500,8 @@ snapshots:
4500 dependencies:
4501 dequal: 2.0.3
4502
4503 + diff-sequences@27.5.1: {}
4504 +
4505 dom-serializer@2.0.0:
4506 dependencies:
4507 domelementtype: 2.3.0
@@ -4325,6 +4528,8 @@ snapshots:
4528
4529 electron-to-chromium@1.5.218: {}
4530
4531 + electron-to-chromium@1.5.262: {}
4532 +
4533 emoji-regex@8.0.0: {}
4534
4535 empathic@2.0.0: {}
@@ -4390,157 +4595,162 @@ snapshots:
4595
4596 escape-string-regexp@5.0.0: {}
4597
4393 - eslint-compat-utils@0.5.1(eslint@9.35.0(jiti@2.5.1)):
4598 + eslint-compat-utils@0.5.1(eslint@9.39.1(jiti@2.6.1)):
4599 dependencies:
4395 - eslint: 9.35.0(jiti@2.5.1)
4600 + eslint: 9.39.1(jiti@2.6.1)
4601 semver: 7.7.2
4602
4398 - eslint-compat-utils@0.6.5(eslint@9.35.0(jiti@2.5.1)):
4603 + eslint-compat-utils@0.6.5(eslint@9.39.1(jiti@2.6.1)):
4604 dependencies:
4400 - eslint: 9.35.0(jiti@2.5.1)
4605 + eslint: 9.39.1(jiti@2.6.1)
4606 semver: 7.7.2
4607
4403 - eslint-config-flat-gitignore@2.1.0(eslint@9.35.0(jiti@2.5.1)):
4608 + eslint-config-flat-gitignore@2.1.0(eslint@9.39.1(jiti@2.6.1)):
4609 dependencies:
4405 - '@eslint/compat': 1.3.2(eslint@9.35.0(jiti@2.5.1))
4406 - eslint: 9.35.0(jiti@2.5.1)
4610 + '@eslint/compat': 1.3.2(eslint@9.39.1(jiti@2.6.1))
4611 + eslint: 9.39.1(jiti@2.6.1)
4612
4408 - eslint-flat-config-utils@2.1.1:
4613 + eslint-flat-config-utils@2.1.4:
4614 dependencies:
4615 pathe: 2.0.3
4616
4412 - eslint-json-compat-utils@0.2.1(eslint@9.35.0(jiti@2.5.1))(jsonc-eslint-parser@2.4.0):
4617 + eslint-json-compat-utils@0.2.1(eslint@9.39.1(jiti@2.6.1))(jsonc-eslint-parser@2.4.1):
4618 dependencies:
4414 - eslint: 9.35.0(jiti@2.5.1)
4619 + eslint: 9.39.1(jiti@2.6.1)
4620 esquery: 1.6.0
4416 - jsonc-eslint-parser: 2.4.0
4621 + jsonc-eslint-parser: 2.4.1
4622
4418 - eslint-merge-processors@2.0.0(eslint@9.35.0(jiti@2.5.1)):
4623 + eslint-merge-processors@2.0.0(eslint@9.39.1(jiti@2.6.1)):
4624 dependencies:
4420 - eslint: 9.35.0(jiti@2.5.1)
4625 + eslint: 9.39.1(jiti@2.6.1)
4626
4422 - eslint-plugin-antfu@3.1.1(eslint@9.35.0(jiti@2.5.1)):
4627 + eslint-plugin-antfu@3.1.1(eslint@9.39.1(jiti@2.6.1)):
4628 dependencies:
4424 - eslint: 9.35.0(jiti@2.5.1)
4629 + eslint: 9.39.1(jiti@2.6.1)
4630
4426 - eslint-plugin-command@3.3.1(eslint@9.35.0(jiti@2.5.1)):
4631 + eslint-plugin-command@3.3.1(eslint@9.39.1(jiti@2.6.1)):
4632 dependencies:
4633 '@es-joy/jsdoccomment': 0.50.2
4429 - eslint: 9.35.0(jiti@2.5.1)
4634 + eslint: 9.39.1(jiti@2.6.1)
4635
4431 - eslint-plugin-es-x@7.8.0(eslint@9.35.0(jiti@2.5.1)):
4636 + eslint-plugin-es-x@7.8.0(eslint@9.39.1(jiti@2.6.1)):
4637 dependencies:
4433 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.5.1))
4638 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
4639 '@eslint-community/regexpp': 4.12.1
4435 - eslint: 9.35.0(jiti@2.5.1)
4436 - eslint-compat-utils: 0.5.1(eslint@9.35.0(jiti@2.5.1))
4640 + eslint: 9.39.1(jiti@2.6.1)
4641 + eslint-compat-utils: 0.5.1(eslint@9.39.1(jiti@2.6.1))
4642
4438 - eslint-plugin-import-lite@0.3.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3):
4643 + eslint-plugin-import-lite@0.3.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
4644 dependencies:
4440 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.5.1))
4441 - '@typescript-eslint/types': 8.44.0
4442 - eslint: 9.35.0(jiti@2.5.1)
4645 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
4646 + '@typescript-eslint/types': 8.48.0
4647 + eslint: 9.39.1(jiti@2.6.1)
4648 optionalDependencies:
4444 - typescript: 5.8.3
4649 + typescript: 5.9.3
4650
4446 - eslint-plugin-jsdoc@54.7.0(eslint@9.35.0(jiti@2.5.1)):
4651 + eslint-plugin-jsdoc@61.4.1(eslint@9.39.1(jiti@2.6.1)):
4652 dependencies:
4448 - '@es-joy/jsdoccomment': 0.56.0
4653 + '@es-joy/jsdoccomment': 0.76.0
4654 + '@es-joy/resolve.exports': 1.2.0
4655 are-docs-informative: 0.0.2
4656 comment-parser: 1.4.1
4657 debug: 4.4.3
4658 escape-string-regexp: 4.0.0
4453 - eslint: 9.35.0(jiti@2.5.1)
4659 + eslint: 9.39.1(jiti@2.6.1)
4660 espree: 10.4.0
4661 esquery: 1.6.0
4662 + html-entities: 2.6.0
4663 + object-deep-merge: 2.0.0
4664 parse-imports-exports: 0.2.4
4457 - semver: 7.7.2
4665 + semver: 7.7.3
4666 spdx-expression-parse: 4.0.0
4667 + to-valid-identifier: 1.0.0
4668 transitivePeerDependencies:
4669 - supports-color
4670
4462 - eslint-plugin-jsonc@2.20.1(eslint@9.35.0(jiti@2.5.1)):
4671 + eslint-plugin-jsonc@2.21.0(eslint@9.39.1(jiti@2.6.1)):
4672 dependencies:
4464 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.5.1))
4465 - eslint: 9.35.0(jiti@2.5.1)
4466 - eslint-compat-utils: 0.6.5(eslint@9.35.0(jiti@2.5.1))
4467 - eslint-json-compat-utils: 0.2.1(eslint@9.35.0(jiti@2.5.1))(jsonc-eslint-parser@2.4.0)
4673 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
4674 + diff-sequences: 27.5.1
4675 + eslint: 9.39.1(jiti@2.6.1)
4676 + eslint-compat-utils: 0.6.5(eslint@9.39.1(jiti@2.6.1))
4677 + eslint-json-compat-utils: 0.2.1(eslint@9.39.1(jiti@2.6.1))(jsonc-eslint-parser@2.4.1)
4678 espree: 10.4.0
4679 graphemer: 1.4.0
4470 - jsonc-eslint-parser: 2.4.0
4680 + jsonc-eslint-parser: 2.4.1
4681 natural-compare: 1.4.0
4682 synckit: 0.11.11
4683 transitivePeerDependencies:
4684 - '@eslint/json'
4685
4476 - eslint-plugin-n@17.23.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3):
4686 + eslint-plugin-n@17.23.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
4687 dependencies:
4478 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.5.1))
4688 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
4689 enhanced-resolve: 5.18.3
4480 - eslint: 9.35.0(jiti@2.5.1)
4481 - eslint-plugin-es-x: 7.8.0(eslint@9.35.0(jiti@2.5.1))
4690 + eslint: 9.39.1(jiti@2.6.1)
4691 + eslint-plugin-es-x: 7.8.0(eslint@9.39.1(jiti@2.6.1))
4692 get-tsconfig: 4.10.1
4693 globals: 15.15.0
4694 globrex: 0.1.2
4695 ignore: 5.3.2
4696 semver: 7.7.2
4487 - ts-declaration-location: 1.0.7(typescript@5.8.3)
4697 + ts-declaration-location: 1.0.7(typescript@5.9.3)
4698 transitivePeerDependencies:
4699 - typescript
4700
4701 eslint-plugin-no-only-tests@3.3.0: {}
4702
4493 - eslint-plugin-perfectionist@4.15.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3):
4703 + eslint-plugin-perfectionist@4.15.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
4704 dependencies:
4495 - '@typescript-eslint/types': 8.44.0
4496 - '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)
4497 - eslint: 9.35.0(jiti@2.5.1)
4705 + '@typescript-eslint/types': 8.48.0
4706 + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
4707 + eslint: 9.39.1(jiti@2.6.1)
4708 natural-orderby: 5.0.0
4709 transitivePeerDependencies:
4710 - supports-color
4711 - typescript
4712
4503 - eslint-plugin-pnpm@1.1.1(eslint@9.35.0(jiti@2.5.1)):
4713 + eslint-plugin-pnpm@1.3.0(eslint@9.39.1(jiti@2.6.1)):
4714 dependencies:
4715 empathic: 2.0.0
4506 - eslint: 9.35.0(jiti@2.5.1)
4507 - jsonc-eslint-parser: 2.4.0
4716 + eslint: 9.39.1(jiti@2.6.1)
4717 + jsonc-eslint-parser: 2.4.1
4718 pathe: 2.0.3
4509 - pnpm-workspace-yaml: 1.1.1
4719 + pnpm-workspace-yaml: 1.3.0
4720 tinyglobby: 0.2.15
4721 yaml-eslint-parser: 1.3.0
4722
4513 - eslint-plugin-regexp@2.10.0(eslint@9.35.0(jiti@2.5.1)):
4723 + eslint-plugin-regexp@2.10.0(eslint@9.39.1(jiti@2.6.1)):
4724 dependencies:
4515 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.5.1))
4725 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
4726 '@eslint-community/regexpp': 4.12.1
4727 comment-parser: 1.4.1
4518 - eslint: 9.35.0(jiti@2.5.1)
4728 + eslint: 9.39.1(jiti@2.6.1)
4729 jsdoc-type-pratt-parser: 4.8.0
4730 refa: 0.12.1
4731 regexp-ast-analysis: 0.7.1
4732 scslre: 0.3.0
4733
4524 - eslint-plugin-toml@0.12.0(eslint@9.35.0(jiti@2.5.1)):
4734 + eslint-plugin-toml@0.12.0(eslint@9.39.1(jiti@2.6.1)):
4735 dependencies:
4736 debug: 4.4.3
4527 - eslint: 9.35.0(jiti@2.5.1)
4528 - eslint-compat-utils: 0.6.5(eslint@9.35.0(jiti@2.5.1))
4737 + eslint: 9.39.1(jiti@2.6.1)
4738 + eslint-compat-utils: 0.6.5(eslint@9.39.1(jiti@2.6.1))
4739 lodash: 4.17.21
4740 toml-eslint-parser: 0.10.0
4741 transitivePeerDependencies:
4742 - supports-color
4743
4534 - eslint-plugin-unicorn@61.0.2(eslint@9.35.0(jiti@2.5.1)):
4744 + eslint-plugin-unicorn@62.0.0(eslint@9.39.1(jiti@2.6.1)):
4745 dependencies:
4536 - '@babel/helper-validator-identifier': 7.27.1
4537 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.5.1))
4538 - '@eslint/plugin-kit': 0.3.5
4746 + '@babel/helper-validator-identifier': 7.28.5
4747 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
4748 + '@eslint/plugin-kit': 0.4.1
4749 change-case: 5.4.4
4540 - ci-info: 4.3.0
4750 + ci-info: 4.3.1
4751 clean-regexp: 1.0.0
4542 - core-js-compat: 3.45.1
4543 - eslint: 9.35.0(jiti@2.5.1)
4752 + core-js-compat: 3.47.0
4753 + eslint: 9.39.1(jiti@2.6.1)
4754 esquery: 1.6.0
4755 find-up-simple: 1.0.1
4756 globals: 16.4.0
@@ -4549,44 +4759,46 @@ snapshots:
4759 jsesc: 3.1.0
4760 pluralize: 8.0.0
4761 regexp-tree: 0.1.27
4552 - regjsparser: 0.12.0
4553 - semver: 7.7.2
4554 - strip-indent: 4.1.0
4762 + regjsparser: 0.13.0
4763 + semver: 7.7.3
4764 + strip-indent: 4.1.1
4765
4556 - eslint-plugin-unused-imports@4.2.0(@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.35.0(jiti@2.5.1)):
4766 + eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)):
4767 dependencies:
4558 - eslint: 9.35.0(jiti@2.5.1)
4768 + eslint: 9.39.1(jiti@2.6.1)
4769 optionalDependencies:
4560 - '@typescript-eslint/eslint-plugin': 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)
4770 + '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
4771
4562 - eslint-plugin-vue@10.4.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.35.0(jiti@2.5.1))(vue-eslint-parser@10.2.0(eslint@9.35.0(jiti@2.5.1))):
4772 + eslint-plugin-vue@10.6.2(@stylistic/eslint-plugin@5.6.1(eslint@9.39.1(jiti@2.6.1)))(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1))):
4773 dependencies:
4564 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.5.1))
4565 - eslint: 9.35.0(jiti@2.5.1)
4774 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
4775 + eslint: 9.39.1(jiti@2.6.1)
4776 natural-compare: 1.4.0
4777 nth-check: 2.1.1
4568 - postcss-selector-parser: 6.1.2
4778 + postcss-selector-parser: 7.1.1
4779 semver: 7.7.2
4570 - vue-eslint-parser: 10.2.0(eslint@9.35.0(jiti@2.5.1))
4780 + vue-eslint-parser: 10.2.0(eslint@9.39.1(jiti@2.6.1))
4781 xml-name-validator: 4.0.0
4782 optionalDependencies:
4573 - '@typescript-eslint/parser': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.8.3)
4783 + '@stylistic/eslint-plugin': 5.6.1(eslint@9.39.1(jiti@2.6.1))
4784 + '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
4785
4575 - eslint-plugin-yml@1.18.0(eslint@9.35.0(jiti@2.5.1)):
4786 + eslint-plugin-yml@1.19.0(eslint@9.39.1(jiti@2.6.1)):
4787 dependencies:
4788 debug: 4.4.3
4789 + diff-sequences: 27.5.1
4790 escape-string-regexp: 4.0.0
4579 - eslint: 9.35.0(jiti@2.5.1)
4580 - eslint-compat-utils: 0.6.5(eslint@9.35.0(jiti@2.5.1))
4791 + eslint: 9.39.1(jiti@2.6.1)
4792 + eslint-compat-utils: 0.6.5(eslint@9.39.1(jiti@2.6.1))
4793 natural-compare: 1.4.0
4794 yaml-eslint-parser: 1.3.0
4795 transitivePeerDependencies:
4796 - supports-color
4797
4586 - eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.21)(eslint@9.35.0(jiti@2.5.1)):
4798 + eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1)):
4799 dependencies:
4588 - '@vue/compiler-sfc': 3.5.21
4589 - eslint: 9.35.0(jiti@2.5.1)
4800 + '@vue/compiler-sfc': 3.5.25
4801 + eslint: 9.39.1(jiti@2.6.1)
4802
4803 eslint-scope@8.4.0:
4804 dependencies:
@@ -4597,21 +4809,20 @@ snapshots:
4809
4810 eslint-visitor-keys@4.2.1: {}
4811
4600 - eslint@9.35.0(jiti@2.5.1):
4812 + eslint@9.39.1(jiti@2.6.1):
4813 dependencies:
4602 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.5.1))
4814 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
4815 '@eslint-community/regexpp': 4.12.1
4604 - '@eslint/config-array': 0.21.0
4605 - '@eslint/config-helpers': 0.3.1
4606 - '@eslint/core': 0.15.2
4816 + '@eslint/config-array': 0.21.1
4817 + '@eslint/config-helpers': 0.4.2
4818 + '@eslint/core': 0.17.0
4819 '@eslint/eslintrc': 3.3.1
4608 - '@eslint/js': 9.35.0
4609 - '@eslint/plugin-kit': 0.3.5
4820 + '@eslint/js': 9.39.1
4821 + '@eslint/plugin-kit': 0.4.1
4822 '@humanfs/node': 0.16.7
4823 '@humanwhocodes/module-importer': 1.0.1
4824 '@humanwhocodes/retry': 0.4.3
4825 '@types/estree': 1.0.8
4614 - '@types/json-schema': 7.0.15
4826 ajv: 6.12.6
4827 chalk: 4.1.2
4828 cross-spawn: 7.0.6
@@ -4635,7 +4846,7 @@ snapshots:
4846 natural-compare: 1.4.0
4847 optionator: 0.9.4
4848 optionalDependencies:
4638 - jiti: 2.5.1
4849 + jiti: 2.6.1
4850 transitivePeerDependencies:
4851 - supports-color
4852
@@ -4665,43 +4876,22 @@ snapshots:
4876
4877 esutils@2.0.3: {}
4878
4879 + event-target-shim@5.0.1: {}
4880 +
4881 + events@3.3.0: {}
4882 +
4883 evtd@0.2.4: {}
4884
4670 - execa@9.6.0:
4671 - dependencies:
4672 - '@sindresorhus/merge-streams': 4.0.0
4673 - cross-spawn: 7.0.6
4674 - figures: 6.1.0
4675 - get-stream: 9.0.1
4676 - human-signals: 8.0.1
4677 - is-plain-obj: 4.1.0
4678 - is-stream: 4.0.1
4679 - npm-run-path: 6.0.0
4680 - pretty-ms: 9.3.0
4681 - signal-exit: 4.1.0
4682 - strip-final-newline: 4.0.0
4683 - yoctocolors: 2.1.2
4885 + exif-parser@0.1.12: {}
4886
4887 exsolve@1.0.7: {}
4888
4889 fast-deep-equal@3.1.3: {}
4890
4689 - fast-glob@3.3.3:
4690 - dependencies:
4691 - '@nodelib/fs.stat': 2.0.5
4692 - '@nodelib/fs.walk': 1.2.8
4693 - glob-parent: 5.1.2
4694 - merge2: 1.4.1
4695 - micromatch: 4.0.8
4696 -
4891 fast-json-stable-stringify@2.1.0: {}
4892
4893 fast-levenshtein@2.0.6: {}
4894
4701 - fastq@1.19.1:
4702 - dependencies:
4703 - reusify: 1.1.0
4704 -
4895 fault@2.0.1:
4896 dependencies:
4897 format: 0.2.2
@@ -4710,17 +4900,29 @@ snapshots:
4900 optionalDependencies:
4901 picomatch: 4.0.3
4902
4713 - figures@6.1.0:
4714 - dependencies:
4715 - is-unicode-supported: 2.1.0
4716 -
4903 file-entry-cache@8.0.0:
4904 dependencies:
4905 flat-cache: 4.0.1
4906
4907 + file-type@16.5.4:
4908 + dependencies:
4909 + readable-web-to-node-stream: 3.0.4
4910 + strtok3: 6.3.0
4911 + token-types: 4.2.1
4912 +
4913 + file-type@21.1.1:
4914 + dependencies:
4915 + '@tokenizer/inflate': 0.4.1
4916 + strtok3: 10.3.4
4917 + token-types: 6.1.1
4918 + uint8array-extras: 1.5.0
4919 + transitivePeerDependencies:
4920 + - supports-color
4921 +
4922 fill-range@7.1.1:
4923 dependencies:
4924 to-regex-range: 5.0.1
4925 + optional: true
4926
4927 find-up-simple@1.0.1: {}
4928
@@ -4775,21 +4977,12 @@ snapshots:
4977 dunder-proto: 1.0.1
4978 es-object-atoms: 1.1.1
4979
4778 - get-stream@9.0.1:
4779 - dependencies:
4780 - '@sec-ant/readable-stream': 0.4.1
4781 - is-stream: 4.0.1
4782 -
4980 get-tsconfig@4.10.1:
4981 dependencies:
4982 resolve-pkg-maps: 1.0.0
4983
4984 github-slugger@2.0.0: {}
4985
4789 - glob-parent@5.1.2:
4790 - dependencies:
4791 - is-glob: 4.0.3
4792 -
4986 glob-parent@6.0.2:
4987 dependencies:
4988 is-glob: 4.0.3
@@ -4820,13 +5013,25 @@ snapshots:
5013 dependencies:
5014 function-bind: 1.1.2
5015
4823 - he@1.2.0: {}
4824 -
5016 highlight.js@11.11.1: {}
5017
5018 hookable@5.5.3: {}
5019
4829 - human-signals@8.0.1: {}
5020 + html-entities@2.6.0: {}
5021 +
5022 + icojs@0.20.1(@jimp/custom@0.22.12):
5023 + dependencies:
5024 + '@jimp/bmp': 0.22.12(@jimp/custom@0.22.12)
5025 + decode-ico: 0.4.1
5026 + file-type: 21.1.1
5027 + jpeg-js: 0.4.4
5028 + pngjs: 7.0.0
5029 + to-data-view: 2.0.0
5030 + transitivePeerDependencies:
5031 + - '@jimp/custom'
5032 + - supports-color
5033 +
5034 + ieee754@1.2.1: {}
5035
5036 ignore@5.3.2: {}
5037
@@ -4861,13 +5066,8 @@ snapshots:
5066 dependencies:
5067 is-docker: 3.0.0
5068
4864 - is-number@7.0.0: {}
4865 -
4866 - is-plain-obj@4.1.0: {}
4867 -
4868 - is-stream@4.0.1: {}
4869 -
4870 - is-unicode-supported@2.1.0: {}
5069 + is-number@7.0.0:
5070 + optional: true
5071
5072 is-what@4.1.16: {}
5073
@@ -4879,9 +5079,18 @@ snapshots:
5079
5080 isexe@3.1.1: {}
5081
4882 - jiti@2.5.1: {}
5082 + isomorphic-fetch@3.0.0:
5083 + dependencies:
5084 + node-fetch: 2.7.0
5085 + whatwg-fetch: 3.6.20
5086 + transitivePeerDependencies:
5087 + - encoding
5088 +
5089 + jiti@2.6.1: {}
5090 +
5091 + jose@6.1.2: {}
5092
4884 - jose@6.1.0: {}
5093 + jpeg-js@0.4.4: {}
5094
5095 js-tokens@4.0.0: {}
5096
@@ -4893,9 +5102,7 @@ snapshots:
5102
5103 jsdoc-type-pratt-parser@4.8.0: {}
5104
4896 - jsdoc-type-pratt-parser@5.1.1: {}
4897 -
4898 - jsesc@3.0.2: {}
5105 + jsdoc-type-pratt-parser@6.10.0: {}
5106
5107 jsesc@3.1.0: {}
5108
@@ -4909,7 +5116,7 @@ snapshots:
5116
5117 json5@2.2.3: {}
5118
4912 - jsonc-eslint-parser@2.4.0:
5119 + jsonc-eslint-parser@2.4.1:
5120 dependencies:
5121 acorn: 8.15.0
5122 eslint-visitor-keys: 3.4.3
@@ -4927,50 +5134,54 @@ snapshots:
5134 prelude-ls: 1.2.1
5135 type-check: 0.4.0
5136
4930 - lightningcss-darwin-arm64@1.30.1:
5137 + lightningcss-android-arm64@1.30.2:
5138 + optional: true
5139 +
5140 + lightningcss-darwin-arm64@1.30.2:
5141 optional: true
5142
4933 - lightningcss-darwin-x64@1.30.1:
5143 + lightningcss-darwin-x64@1.30.2:
5144 optional: true
5145
4936 - lightningcss-freebsd-x64@1.30.1:
5146 + lightningcss-freebsd-x64@1.30.2:
5147 optional: true
5148
4939 - lightningcss-linux-arm-gnueabihf@1.30.1:
5149 + lightningcss-linux-arm-gnueabihf@1.30.2:
5150 optional: true
5151
4942 - lightningcss-linux-arm64-gnu@1.30.1:
5152 + lightningcss-linux-arm64-gnu@1.30.2:
5153 optional: true
5154
4945 - lightningcss-linux-arm64-musl@1.30.1:
5155 + lightningcss-linux-arm64-musl@1.30.2:
5156 optional: true
5157
4948 - lightningcss-linux-x64-gnu@1.30.1:
5158 + lightningcss-linux-x64-gnu@1.30.2:
5159 optional: true
5160
4951 - lightningcss-linux-x64-musl@1.30.1:
5161 + lightningcss-linux-x64-musl@1.30.2:
5162 optional: true
5163
4954 - lightningcss-win32-arm64-msvc@1.30.1:
5164 + lightningcss-win32-arm64-msvc@1.30.2:
5165 optional: true
5166
4957 - lightningcss-win32-x64-msvc@1.30.1:
5167 + lightningcss-win32-x64-msvc@1.30.2:
5168 optional: true
5169
4960 - lightningcss@1.30.1:
5170 + lightningcss@1.30.2:
5171 dependencies:
5172 detect-libc: 2.1.0
5173 optionalDependencies:
4964 - lightningcss-darwin-arm64: 1.30.1
4965 - lightningcss-darwin-x64: 1.30.1
4966 - lightningcss-freebsd-x64: 1.30.1
4967 - lightningcss-linux-arm-gnueabihf: 1.30.1
4968 - lightningcss-linux-arm64-gnu: 1.30.1
4969 - lightningcss-linux-arm64-musl: 1.30.1
4970 - lightningcss-linux-x64-gnu: 1.30.1
4971 - lightningcss-linux-x64-musl: 1.30.1
4972 - lightningcss-win32-arm64-msvc: 1.30.1
4973 - lightningcss-win32-x64-msvc: 1.30.1
5174 + lightningcss-android-arm64: 1.30.2
5175 + lightningcss-darwin-arm64: 1.30.2
5176 + lightningcss-darwin-x64: 1.30.2
5177 + lightningcss-freebsd-x64: 1.30.2
5178 + lightningcss-linux-arm-gnueabihf: 1.30.2
5179 + lightningcss-linux-arm64-gnu: 1.30.2
5180 + lightningcss-linux-arm64-musl: 1.30.2
5181 + lightningcss-linux-x64-gnu: 1.30.2
5182 + lightningcss-linux-x64-musl: 1.30.2
5183 + lightningcss-win32-arm64-msvc: 1.30.2
5184 + lightningcss-win32-x64-msvc: 1.30.2
5185
5186 local-pkg@1.1.2:
5187 dependencies:
@@ -5000,6 +5211,10 @@ snapshots:
5211 dependencies:
5212 '@jridgewell/sourcemap-codec': 1.5.5
5213
5214 + magic-string@0.30.21:
5215 + dependencies:
5216 + '@jridgewell/sourcemap-codec': 1.5.5
5217 +
5218 markdown-table@3.0.4: {}
5219
5220 math-intrinsics@1.1.0: {}
@@ -5123,8 +5338,6 @@ snapshots:
5338
5339 memorystream@0.3.1: {}
5340
5126 - merge2@1.4.1: {}
5127 -
5341 micromark-core-commonmark@2.0.3:
5342 dependencies:
5343 decode-named-character-reference: 1.2.0
@@ -5327,6 +5540,7 @@ snapshots:
5540 dependencies:
5541 braces: 3.0.3
5542 picomatch: 2.3.1
5543 + optional: true
5544
5545 mime-db@1.52.0: {}
5546
@@ -5342,16 +5556,8 @@ snapshots:
5556 dependencies:
5557 brace-expansion: 2.0.2
5558
5345 - minipass@7.1.2: {}
5346 -
5347 - minizlib@3.0.2:
5348 - dependencies:
5349 - minipass: 7.1.2
5350 -
5559 mitt@3.0.1: {}
5560
5353 - mkdirp@3.0.1: {}
5354 -
5561 mlly@1.8.0:
5562 dependencies:
5563 acorn: 8.15.0
@@ -5365,32 +5571,32 @@ snapshots:
5571
5572 muggle-string@0.4.1: {}
5573
5368 - naive-ui@2.43.1(vue@3.5.21(typescript@5.8.3)):
5574 + naive-ui@2.43.2(vue@3.5.25(typescript@5.9.3)):
5575 dependencies:
5576 '@css-render/plugin-bem': 0.15.14(css-render@0.15.14)
5371 - '@css-render/vue3-ssr': 0.15.14(vue@3.5.21(typescript@5.8.3))
5577 + '@css-render/vue3-ssr': 0.15.14(vue@3.5.25(typescript@5.9.3))
5578 '@types/katex': 0.16.7
5373 - '@types/lodash': 4.17.20
5579 + '@types/lodash': 4.17.21
5580 '@types/lodash-es': 4.17.12
5581 async-validator: 4.2.5
5582 css-render: 0.15.14
5583 csstype: 3.1.3
5378 - date-fns: 3.6.0
5379 - date-fns-tz: 3.2.0(date-fns@3.6.0)
5584 + date-fns: 4.1.0
5585 + date-fns-tz: 3.2.0(date-fns@4.1.0)
5586 evtd: 0.2.4
5587 highlight.js: 11.11.1
5588 lodash: 4.17.21
5589 lodash-es: 4.17.21
5590 seemly: 0.3.10
5591 treemate: 0.3.11
5386 - vdirs: 0.1.8(vue@3.5.21(typescript@5.8.3))
5387 - vooks: 0.2.12(vue@3.5.21(typescript@5.8.3))
5388 - vue: 3.5.21(typescript@5.8.3)
5389 - vueuc: 0.4.65(vue@3.5.21(typescript@5.8.3))
5592 + vdirs: 0.1.8(vue@3.5.25(typescript@5.9.3))
5593 + vooks: 0.2.12(vue@3.5.25(typescript@5.9.3))
5594 + vue: 3.5.25(typescript@5.9.3)
5595 + vueuc: 0.4.65(vue@3.5.25(typescript@5.9.3))
5596
5597 nanoid@3.3.11: {}
5598
5393 - nanoid@5.1.5: {}
5599 + nanoid@5.1.6: {}
5600
5601 natural-compare@1.4.0: {}
5602
@@ -5399,8 +5605,14 @@ snapshots:
5605 node-addon-api@7.1.1:
5606 optional: true
5607
5608 + node-fetch@2.7.0:
5609 + dependencies:
5610 + whatwg-url: 5.0.0
5611 +
5612 node-releases@2.0.21: {}
5613
5614 + node-releases@2.0.27: {}
5615 +
5616 npm-normalize-package-bin@4.0.0: {}
5617
5618 npm-run-all2@8.0.4:
@@ -5414,15 +5626,12 @@ snapshots:
5626 shell-quote: 1.8.3
5627 which: 5.0.0
5628
5417 - npm-run-path@6.0.0:
5418 - dependencies:
5419 - path-key: 4.0.0
5420 - unicorn-magic: 0.3.0
5421 -
5629 nth-check@2.1.1:
5630 dependencies:
5631 boolbase: 1.0.0
5632
5633 + object-deep-merge@2.0.0: {}
5634 +
5635 ohash@2.0.11: {}
5636
5637 open@10.2.0:
@@ -5461,8 +5670,6 @@ snapshots:
5670 dependencies:
5671 parse-statements: 1.0.11
5672
5464 - parse-ms@4.0.0: {}
5465 -
5673 parse-statements@1.0.11: {}
5674
5675 path-browserify@1.0.1: {}
@@ -5471,36 +5678,39 @@ snapshots:
5678
5679 path-key@3.1.1: {}
5680
5474 - path-key@4.0.0: {}
5475 -
5681 pathe@2.0.3: {}
5682
5683 + peek-readable@4.1.0: {}
5684 +
5685 perfect-debounce@1.0.0: {}
5686
5687 perfect-debounce@2.0.0: {}
5688
5689 picocolors@1.1.1: {}
5690
5484 - picomatch@2.3.1: {}
5691 + picomatch@2.3.1:
5692 + optional: true
5693
5694 picomatch@4.0.3: {}
5695
5696 pidtree@0.6.0: {}
5697
5490 - pinia-plugin-persistedstate@4.5.0(pinia@3.0.3(typescript@5.8.3)(vue@3.5.21(typescript@5.8.3))):
5698 + pinia-plugin-persistedstate@4.7.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))):
5699 dependencies:
5492 - deep-pick-omit: 1.2.1
5700 defu: 6.1.4
5494 - destr: 2.0.5
5701 optionalDependencies:
5496 - pinia: 3.0.3(typescript@5.8.3)(vue@3.5.21(typescript@5.8.3))
5702 + pinia: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
5703
5498 - pinia@3.0.3(typescript@5.8.3)(vue@3.5.21(typescript@5.8.3)):
5704 + pinia@3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)):
5705 dependencies:
5706 '@vue/devtools-api': 7.7.7
5501 - vue: 3.5.21(typescript@5.8.3)
5707 + vue: 3.5.25(typescript@5.9.3)
5708 optionalDependencies:
5503 - typescript: 5.8.3
5709 + typescript: 5.9.3
5710 +
5711 + pixelmatch@4.0.2:
5712 + dependencies:
5713 + pngjs: 3.4.0
5714
5715 pkg-types@1.3.1:
5716 dependencies:
@@ -5516,11 +5726,15 @@ snapshots:
5726
5727 pluralize@8.0.0: {}
5728
5519 - pnpm-workspace-yaml@1.1.1:
5729 + pngjs@3.4.0: {}
5730 +
5731 + pngjs@7.0.0: {}
5732 +
5733 + pnpm-workspace-yaml@1.3.0:
5734 dependencies:
5735 yaml: 2.8.1
5736
5523 - postcss-selector-parser@6.1.2:
5737 + postcss-selector-parser@7.1.1:
5738 dependencies:
5739 cssesc: 3.0.0
5740 util-deprecate: 1.0.2
@@ -5533,15 +5747,13 @@ snapshots:
5747
5748 prelude-ls@1.2.1: {}
5749
5536 - prettier-plugin-tailwindcss@0.6.14(prettier@3.6.2):
5750 + prettier-plugin-tailwindcss@0.7.1(prettier@3.7.3):
5751 dependencies:
5538 - prettier: 3.6.2
5752 + prettier: 3.7.3
5753
5540 - prettier@3.6.2: {}
5754 + prettier@3.7.3: {}
5755
5542 - pretty-ms@9.3.0:
5543 - dependencies:
5544 - parse-ms: 4.0.0
5756 + process@0.11.10: {}
5757
5758 proxy-from-env@1.1.0: {}
5759
@@ -5549,19 +5761,31 @@ snapshots:
5761
5762 quansync@0.2.11: {}
5763
5552 - queue-microtask@1.2.3: {}
5553 -
5764 read-package-json-fast@4.0.0:
5765 dependencies:
5766 json-parse-even-better-errors: 4.0.0
5767 npm-normalize-package-bin: 4.0.0
5768
5769 + readable-stream@4.7.0:
5770 + dependencies:
5771 + abort-controller: 3.0.0
5772 + buffer: 6.0.3
5773 + events: 3.3.0
5774 + process: 0.11.10
5775 + string_decoder: 1.3.0
5776 +
5777 + readable-web-to-node-stream@3.0.4:
5778 + dependencies:
5779 + readable-stream: 4.7.0
5780 +
5781 readdirp@4.1.2: {}
5782
5783 refa@0.12.1:
5784 dependencies:
5785 '@eslint-community/regexpp': 4.12.1
5786
5787 + regenerator-runtime@0.13.11: {}
5788 +
5789 regexp-ast-analysis@0.7.1:
5790 dependencies:
5791 '@eslint-community/regexpp': 4.12.1
@@ -5569,18 +5793,18 @@ snapshots:
5793
5794 regexp-tree@0.1.27: {}
5795
5572 - regjsparser@0.12.0:
5796 + regjsparser@0.13.0:
5797 dependencies:
5574 - jsesc: 3.0.2
5798 + jsesc: 3.1.0
5799
5800 require-directory@2.1.1: {}
5801
5802 + reserved-identifiers@1.2.0: {}
5803 +
5804 resolve-from@4.0.0: {}
5805
5806 resolve-pkg-maps@1.0.0: {}
5807
5582 - reusify@1.1.0: {}
5583 -
5808 rfdc@1.4.1: {}
5809
5810 rollup@4.50.2:
@@ -5612,15 +5836,13 @@ snapshots:
5836
5837 run-applescript@7.1.0: {}
5838
5615 - run-parallel@1.2.0:
5616 - dependencies:
5617 - queue-microtask: 1.2.3
5618 -
5839 rxjs@7.8.2:
5840 dependencies:
5841 tslib: 2.8.1
5842
5623 - sass@1.92.1:
5843 + safe-buffer@5.2.1: {}
5844 +
5845 + sass@1.94.2:
5846 dependencies:
5847 chokidar: 4.0.3
5848 immutable: 5.1.3
@@ -5645,6 +5867,8 @@ snapshots:
5867
5868 semver@7.7.2: {}
5869
5870 + semver@7.7.3: {}
5871 +
5872 shebang-command@2.0.0:
5873 dependencies:
5874 shebang-regex: 3.0.0
@@ -5653,8 +5877,6 @@ snapshots:
5877
5878 shell-quote@1.8.3: {}
5879
5656 - signal-exit@4.1.0: {}
5657 -
5880 sirv@3.0.2:
5881 dependencies:
5882 '@polka/url': 1.0.0-next.29
@@ -5665,8 +5887,6 @@ snapshots:
5887
5888 source-map-js@1.2.1: {}
5889
5668 - spawn-command@0.0.2: {}
5669 -
5890 spdx-exceptions@2.5.0: {}
5891
5892 spdx-expression-parse@4.0.0:
@@ -5684,16 +5904,27 @@ snapshots:
5904 is-fullwidth-code-point: 3.0.0
5905 strip-ansi: 6.0.1
5906
5907 + string_decoder@1.3.0:
5908 + dependencies:
5909 + safe-buffer: 5.2.1
5910 +
5911 strip-ansi@6.0.1:
5912 dependencies:
5913 ansi-regex: 5.0.1
5914
5691 - strip-final-newline@4.0.0: {}
5692 -
5693 - strip-indent@4.1.0: {}
5915 + strip-indent@4.1.1: {}
5916
5917 strip-json-comments@3.1.1: {}
5918
5919 + strtok3@10.3.4:
5920 + dependencies:
5921 + '@tokenizer/token': 0.3.0
5922 +
5923 + strtok3@6.3.0:
5924 + dependencies:
5925 + '@tokenizer/token': 0.3.0
5926 + peek-readable: 4.1.0
5927 +
5928 superjson@2.2.2:
5929 dependencies:
5930 copy-anything: 3.0.5
@@ -5720,18 +5951,11 @@ snapshots:
5951 dependencies:
5952 '@pkgr/core': 0.2.9
5953
5723 - tailwindcss@4.1.13: {}
5954 + tailwindcss@4.1.17: {}
5955
5956 tapable@2.2.3: {}
5957
5727 - tar@7.4.3:
5728 - dependencies:
5729 - '@isaacs/fs-minipass': 4.0.1
5730 - chownr: 3.0.0
5731 - minipass: 7.1.2
5732 - minizlib: 3.0.2
5733 - mkdirp: 3.0.1
5734 - yallist: 5.0.0
5958 + tinycolor2@1.6.0: {}
5959
5960 tinyexec@1.0.1: {}
5961
@@ -5740,9 +5964,30 @@ snapshots:
5964 fdir: 6.5.0(picomatch@4.0.3)
5965 picomatch: 4.0.3
5966
5967 + to-data-view@1.1.0: {}
5968 +
5969 + to-data-view@2.0.0: {}
5970 +
5971 to-regex-range@5.0.1:
5972 dependencies:
5973 is-number: 7.0.0
5974 + optional: true
5975 +
5976 + to-valid-identifier@1.0.0:
5977 + dependencies:
5978 + '@sindresorhus/base62': 1.0.0
5979 + reserved-identifiers: 1.2.0
5980 +
5981 + token-types@4.2.1:
5982 + dependencies:
5983 + '@tokenizer/token': 0.3.0
5984 + ieee754: 1.2.1
5985 +
5986 + token-types@6.1.1:
5987 + dependencies:
5988 + '@borewit/text-codec': 0.1.1
5989 + '@tokenizer/token': 0.3.0
5990 + ieee754: 1.2.1
5991
5992 toml-eslint-parser@0.10.0:
5993 dependencies:
@@ -5750,18 +5995,20 @@ snapshots:
5995
5996 totalist@3.0.1: {}
5997
5998 + tr46@0.0.3: {}
5999 +
6000 tree-kill@1.2.2: {}
6001
6002 treemate@0.3.11: {}
6003
5757 - ts-api-utils@2.1.0(typescript@5.8.3):
6004 + ts-api-utils@2.1.0(typescript@5.9.3):
6005 dependencies:
5759 - typescript: 5.8.3
6006 + typescript: 5.9.3
6007
5761 - ts-declaration-location@1.0.7(typescript@5.8.3):
6008 + ts-declaration-location@1.0.7(typescript@5.9.3):
6009 dependencies:
6010 picomatch: 4.0.3
5764 - typescript: 5.8.3
6011 + typescript: 5.9.3
6012
6013 tslib@2.8.1: {}
6014
@@ -5769,13 +6016,13 @@ snapshots:
6016 dependencies:
6017 prelude-ls: 1.2.1
6018
5772 - typescript@5.8.3: {}
6019 + typescript@5.9.3: {}
6020
6021 ufo@1.6.1: {}
6022
5776 - undici-types@7.12.0: {}
6023 + uint8array-extras@1.5.0: {}
6024
5778 - unicorn-magic@0.3.0: {}
6025 + undici-types@7.16.0: {}
6026
6027 unist-util-is@6.0.0:
6028 dependencies:
@@ -5807,28 +6054,34 @@ snapshots:
6054 escalade: 3.2.0
6055 picocolors: 1.1.1
6056
6057 + update-browserslist-db@1.1.4(browserslist@4.28.0):
6058 + dependencies:
6059 + browserslist: 4.28.0
6060 + escalade: 3.2.0
6061 + picocolors: 1.1.1
6062 +
6063 uri-js@4.4.1:
6064 dependencies:
6065 punycode: 2.3.1
6066
6067 util-deprecate@1.0.2: {}
6068
5816 - vdirs@0.1.8(vue@3.5.21(typescript@5.8.3)):
6069 + vdirs@0.1.8(vue@3.5.25(typescript@5.9.3)):
6070 dependencies:
6071 evtd: 0.2.4
5819 - vue: 3.5.21(typescript@5.8.3)
6072 + vue: 3.5.25(typescript@5.9.3)
6073
5821 - vite-dev-rpc@1.1.0(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1)):
6074 + vite-dev-rpc@1.1.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)):
6075 dependencies:
6076 birpc: 2.5.0
5824 - vite: 7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1)
5825 - vite-hot-client: 2.1.0(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1))
6077 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
6078 + vite-hot-client: 2.1.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))
6079
5827 - vite-hot-client@2.1.0(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1)):
6080 + vite-hot-client@2.1.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)):
6081 dependencies:
5829 - vite: 7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1)
6082 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
6083
5831 - vite-plugin-inspect@11.3.3(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1)):
6084 + vite-plugin-inspect@11.3.3(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)):
6085 dependencies:
6086 ansis: 4.1.0
6087 debug: 4.4.3
@@ -5838,27 +6091,26 @@ snapshots:
6091 perfect-debounce: 2.0.0
6092 sirv: 3.0.2
6093 unplugin-utils: 0.3.0
5841 - vite: 7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1)
5842 - vite-dev-rpc: 1.1.0(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1))
6094 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
6095 + vite-dev-rpc: 1.1.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))
6096 transitivePeerDependencies:
6097 - supports-color
6098
5846 - vite-plugin-vue-devtools@8.0.2(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1))(vue@3.5.21(typescript@5.8.3)):
6099 + vite-plugin-vue-devtools@8.0.5(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3)):
6100 dependencies:
5848 - '@vue/devtools-core': 8.0.2(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1))(vue@3.5.21(typescript@5.8.3))
5849 - '@vue/devtools-kit': 8.0.2
5850 - '@vue/devtools-shared': 8.0.2
5851 - execa: 9.6.0
6101 + '@vue/devtools-core': 8.0.5(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))
6102 + '@vue/devtools-kit': 8.0.5
6103 + '@vue/devtools-shared': 8.0.5
6104 sirv: 3.0.2
5853 - vite: 7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1)
5854 - vite-plugin-inspect: 11.3.3(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1))
5855 - vite-plugin-vue-inspector: 5.3.2(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1))
6105 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
6106 + vite-plugin-inspect: 11.3.3(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))
6107 + vite-plugin-vue-inspector: 5.3.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))
6108 transitivePeerDependencies:
6109 - '@nuxt/kit'
6110 - supports-color
6111 - vue
6112
5861 - vite-plugin-vue-inspector@5.3.2(vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1)):
6113 + vite-plugin-vue-inspector@5.3.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)):
6114 dependencies:
6115 '@babel/core': 7.28.4
6116 '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.4)
@@ -5869,16 +6121,16 @@ snapshots:
6121 '@vue/compiler-dom': 3.5.21
6122 kolorist: 1.8.0
6123 magic-string: 0.30.19
5872 - vite: 7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1)
6124 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
6125 transitivePeerDependencies:
6126 - supports-color
6127
5876 - vite-svg-loader@5.1.0(vue@3.5.21(typescript@5.8.3)):
6128 + vite-svg-loader@5.1.0(vue@3.5.25(typescript@5.9.3)):
6129 dependencies:
6130 svgo: 3.3.2
5879 - vue: 3.5.21(typescript@5.8.3)
6131 + vue: 3.5.25(typescript@5.9.3)
6132
5881 - vite@7.1.5(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1):
6133 + vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1):
6134 dependencies:
6135 esbuild: 0.25.9
6136 fdir: 6.5.0(picomatch@4.0.3)
@@ -5887,24 +6139,24 @@ snapshots:
6139 rollup: 4.50.2
6140 tinyglobby: 0.2.15
6141 optionalDependencies:
5890 - '@types/node': 24.5.0
6142 + '@types/node': 24.10.1
6143 fsevents: 2.3.3
5892 - jiti: 2.5.1
5893 - lightningcss: 1.30.1
5894 - sass: 1.92.1
6144 + jiti: 2.6.1
6145 + lightningcss: 1.30.2
6146 + sass: 1.94.2
6147 yaml: 2.8.1
6148
5897 - vooks@0.2.12(vue@3.5.21(typescript@5.8.3)):
6149 + vooks@0.2.12(vue@3.5.25(typescript@5.9.3)):
6150 dependencies:
6151 evtd: 0.2.4
5900 - vue: 3.5.21(typescript@5.8.3)
6152 + vue: 3.5.25(typescript@5.9.3)
6153
6154 vscode-uri@3.1.0: {}
6155
5904 - vue-eslint-parser@10.2.0(eslint@9.35.0(jiti@2.5.1)):
6156 + vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1)):
6157 dependencies:
6158 debug: 4.4.3
5907 - eslint: 9.35.0(jiti@2.5.1)
6159 + eslint: 9.39.1(jiti@2.6.1)
6160 eslint-scope: 8.4.0
6161 eslint-visitor-keys: 4.2.1
6162 espree: 10.4.0
@@ -5913,37 +6165,46 @@ snapshots:
6165 transitivePeerDependencies:
6166 - supports-color
6167
5916 - vue-router@4.5.1(vue@3.5.21(typescript@5.8.3)):
6168 + vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)):
6169 dependencies:
6170 '@vue/devtools-api': 6.6.4
5919 - vue: 3.5.21(typescript@5.8.3)
6171 + vue: 3.5.25(typescript@5.9.3)
6172
5921 - vue-tsc@3.0.7(typescript@5.8.3):
6173 + vue-tsc@3.1.5(typescript@5.9.3):
6174 dependencies:
6175 '@volar/typescript': 2.4.23
5924 - '@vue/language-core': 3.0.7(typescript@5.8.3)
5925 - typescript: 5.8.3
6176 + '@vue/language-core': 3.1.5(typescript@5.9.3)
6177 + typescript: 5.9.3
6178
5927 - vue@3.5.21(typescript@5.8.3):
6179 + vue@3.5.25(typescript@5.9.3):
6180 dependencies:
5929 - '@vue/compiler-dom': 3.5.21
5930 - '@vue/compiler-sfc': 3.5.21
5931 - '@vue/runtime-dom': 3.5.21
5932 - '@vue/server-renderer': 3.5.21(vue@3.5.21(typescript@5.8.3))
5933 - '@vue/shared': 3.5.21
6181 + '@vue/compiler-dom': 3.5.25
6182 + '@vue/compiler-sfc': 3.5.25
6183 + '@vue/runtime-dom': 3.5.25
6184 + '@vue/server-renderer': 3.5.25(vue@3.5.25(typescript@5.9.3))
6185 + '@vue/shared': 3.5.25
6186 optionalDependencies:
5935 - typescript: 5.8.3
6187 + typescript: 5.9.3
6188
5937 - vueuc@0.4.65(vue@3.5.21(typescript@5.8.3)):
6189 + vueuc@0.4.65(vue@3.5.25(typescript@5.9.3)):
6190 dependencies:
5939 - '@css-render/vue3-ssr': 0.15.14(vue@3.5.21(typescript@5.8.3))
6191 + '@css-render/vue3-ssr': 0.15.14(vue@3.5.25(typescript@5.9.3))
6192 '@juggle/resize-observer': 3.4.0
6193 css-render: 0.15.14
6194 evtd: 0.2.4
6195 seemly: 0.3.10
5944 - vdirs: 0.1.8(vue@3.5.21(typescript@5.8.3))
5945 - vooks: 0.2.12(vue@3.5.21(typescript@5.8.3))
5946 - vue: 3.5.21(typescript@5.8.3)
6196 + vdirs: 0.1.8(vue@3.5.25(typescript@5.9.3))
6197 + vooks: 0.2.12(vue@3.5.25(typescript@5.9.3))
6198 + vue: 3.5.25(typescript@5.9.3)
6199 +
6200 + webidl-conversions@3.0.1: {}
6201 +
6202 + whatwg-fetch@3.6.20: {}
6203 +
6204 + whatwg-url@5.0.0:
6205 + dependencies:
6206 + tr46: 0.0.3
6207 + webidl-conversions: 3.0.1
6208
6209 which@2.0.2:
6210 dependencies:
@@ -5971,8 +6232,6 @@ snapshots:
6232
6233 yallist@3.1.1: {}
6234
5974 - yallist@5.0.0: {}
5975 -
6235 yaml-eslint-parser@1.3.0:
6236 dependencies:
6237 eslint-visitor-keys: 3.4.3
@@ -5994,6 +6253,4 @@ snapshots:
6253
6254 yocto-queue@0.1.0: {}
6255
5997 - yoctocolors@2.1.2: {}
5998 -
6256 zwitch@2.0.4: {}
customer_portal/src/App.vue
+74 -1
@@ -3,7 +3,80 @@
3 </template>
4
5 <script setup lang="ts">
6 -// This is the root component for the customer portal
6 +import { onMounted, watch } from "vue"
7 +import { usePortalSettingsStore } from "./stores/portalSettings"
8 +
9 +const portalSettingsStore = usePortalSettingsStore()
10 +
11 +// Function to convert logo to favicon format (32x32 PNG)
12 +async function logoToFavicon(logoDataUri: string) {
13 + const img = await createImageBitmap(await (await fetch(logoDataUri)).blob())
14 + // Canvas a 32x32
15 + const canvas = new OffscreenCanvas(32, 32)
16 + const ctx = canvas.getContext("2d")
17 + if (!ctx) throw new Error("Failed to get canvas context")
18 +
19 + ctx.drawImage(img, 0, 0, 32, 32)
20 + const blob = await canvas.convertToBlob({ type: "image/png" })
21 +
22 + // Convert blob to data URL
23 + return new Promise<string>((resolve, reject) => {
24 + const reader = new FileReader()
25 + reader.onloadend = () => resolve(reader.result as string)
26 + reader.onerror = reject
27 + reader.readAsDataURL(blob)
28 + })
29 +}
30 +
31 +// Function to update favicon
32 +const updateFavicon = async (logoDataUrl: string | null) => {
33 + if (!logoDataUrl) return
34 +
35 + try {
36 + // Convert logo to ICO format
37 + const faviconDataUrl = await logoToFavicon(logoDataUrl)
38 +
39 + // Remove existing favicon links
40 + const existingLinks = document.querySelectorAll("link[rel*='icon']")
41 + existingLinks.forEach(link => link.remove())
42 +
43 + // Create new favicon link
44 + const link = document.createElement("link")
45 + link.rel = "icon"
46 + link.type = "image/png"
47 + link.href = faviconDataUrl
48 + document.head.appendChild(link)
49 + } catch (error) {
50 + console.error("Failed to update favicon:", error)
51 + }
52 +}
53 +
54 +// Fetch portal settings on app mount
55 +onMounted(async () => {
56 + await portalSettingsStore.fetchSettings()
57 +})
58 +
59 +// Watch for logo changes and update favicon
60 +watch(
61 + () => portalSettingsStore.portalLogo,
62 + newLogo => {
63 + if (newLogo) {
64 + updateFavicon(newLogo)
65 + }
66 + },
67 + { immediate: true }
68 +)
69 +
70 +// Watch for title changes and update document title
71 +watch(
72 + () => portalSettingsStore.portalTitle,
73 + newTitle => {
74 + if (newTitle) {
75 + document.title = newTitle
76 + }
77 + },
78 + { immediate: true }
79 +)
80 </script>
81
82 <style scoped>
customer_portal/src/api/cases.ts
+179 -174
@@ -1,212 +1,217 @@
1 -import { httpClient } from '@/utils/httpClient'
1 +import { httpClient } from "@/utils/httpClient"
2
3 export interface CaseComment {
4 - id: number
5 - case_id: number
6 - user_name: string
7 - comment: string
8 - created_at: string
4 + id: number
5 + case_id: number
6 + user_name: string
7 + comment: string
8 + created_at: string
9 }
10
11 export interface Case {
12 - id: number
13 - case_creation_time: string
14 - case_description: string
15 - case_name: string
16 - case_status: 'open' | 'in_progress' | 'closed'
17 - assigned_to: string | null
18 - customer_code: string
19 - alert_ids: number[]
20 - alerts?: Alert[]
21 - comments?: CaseComment[]
12 + id: number
13 + case_creation_time: string
14 + case_description: string
15 + case_name: string
16 + case_status: "OPEN" | "IN_PROGRESS" | "CLOSED"
17 + assigned_to: string | null
18 + customer_code: string
19 + alert_ids: number[]
20 + alerts?: Alert[]
21 + comments?: CaseComment[]
22 }
23
24 export interface Alert {
25 - id: number
26 - alert_name: string
27 - asset_name: string
28 - status: 'open' | 'in_progress' | 'closed'
29 - time_stamp: string
25 + id: number
26 + alert_name: string
27 + asset_name: string
28 + status: "OPEN" | "IN_PROGRESS" | "CLOSED"
29 + time_stamp: string
30 }
31
32 export interface CasesResponse {
33 - cases: Case[]
34 - success: boolean
35 - message: string
33 + cases: Case[]
34 + success: boolean
35 + message: string
36 }
37
38 export interface CaseResponse {
39 - cases: Case[]
40 - success: boolean
41 - message: string
39 + cases: Case[]
40 + success: boolean
41 + message: string
42 }
43
44 export interface CaseStatusUpdate {
45 - case_id: number
46 - status: 'open' | 'in_progress' | 'closed'
45 + case_id: number
46 + status: "open" | "in_progress" | "closed"
47 }
48
49 export interface CaseAssignedToUpdate {
50 - case_id: number
51 - assigned_to: string
50 + case_id: number
51 + assigned_to: string
52 }
53
54 export interface CasePayload {
55 - case_name: string
56 - case_description: string
57 - assigned_to?: string
55 + case_name: string
56 + case_description: string
57 + assigned_to?: string
58 }
59
60 export interface CaseCommentCreate {
61 - case_id: number
62 - comment: string
61 + case_id: number
62 + comment: string
63 }
64
65 export interface CaseCommentEdit {
66 - id: number
67 - case_id: number
68 - comment: string
66 + id: number
67 + case_id: number
68 + comment: string
69 }
70
71 export interface CaseCommentResponse {
72 - comment: CaseComment
73 - success: boolean
74 - message: string
72 + comment: CaseComment
73 + success: boolean
74 + message: string
75 }
76
77 export class CasesAPI {
78 - /**
79 - * Get all cases with customer access control
80 - */
81 - static async getCases(): Promise<CasesResponse> {
82 - const response = await httpClient.get('/incidents/db_operations/cases')
83 - return response.data
84 - }
85 -
86 - /**
87 - * Get specific case by ID (with customer access validation)
88 - */
89 - static async getCase(caseId: number): Promise<CaseResponse> {
90 - const response = await httpClient.get(`/incidents/db_operations/case/${caseId}`)
91 - return response.data
92 - }
93 -
94 - /**
95 - * Update case status (customer access controlled)
96 - */
97 - static async updateCaseStatus(caseId: number, status: 'open' | 'in_progress' | 'closed'): Promise<CaseResponse> {
98 - const response = await httpClient.put('/incidents/db_operations/case/status', {
99 - case_id: caseId,
100 - status
101 - })
102 - return response.data
103 - }
104 -
105 - /**
106 - * Update case assigned user (customer access controlled)
107 - */
108 - static async updateCaseAssignedTo(caseId: number, assignedTo: string): Promise<CaseResponse> {
109 - const response = await httpClient.put('/incidents/db_operations/case/assigned-to', {
110 - case_id: caseId,
111 - assigned_to: assignedTo
112 - })
113 - return response.data
114 - }
115 -
116 - /**
117 - * Create new case (customer access controlled)
118 - */
119 - static async createCase(payload: CasePayload): Promise<{ case: Case; success: boolean; message: string }> {
120 - const response = await httpClient.post('/incidents/db_operations/case/create', payload)
121 - return response.data
122 - }
123 -
124 - /**
125 - * Delete case (customer access controlled)
126 - */
127 - static async deleteCase(caseId: number): Promise<{ success: boolean; message: string }> {
128 - const response = await httpClient.delete(`/incidents/db_operations/case/${caseId}`)
129 - return response.data
130 - }
131 -
132 - /**
133 - * Get cases by status with customer filtering
134 - */
135 - static async getCasesByStatus(status: string): Promise<CasesResponse> {
136 - const response = await httpClient.get(`/incidents/db_operations/case/status/${status}`)
137 - return response.data
138 - }
139 -
140 - /**
141 - * Get cases by assigned user with customer filtering
142 - */
143 - static async getCasesByAssignedTo(assignedTo: string): Promise<CasesResponse> {
144 - const response = await httpClient.get(`/incidents/db_operations/case/assigned-to/${assignedTo}`)
145 - return response.data
146 - }
147 -
148 - /**
149 - * Create case from alert (customer access controlled)
150 - */
151 - static async createCaseFromAlert(alertId: number): Promise<{ case_alert_link: { case_id: number; alert_id: number }; success: boolean; message: string }> {
152 - const response = await httpClient.post('/incidents/db_operations/case/from-alert', {
153 - alert_id: alertId
154 - })
155 - return response.data
156 - }
157 -
158 - /**
159 - * Link case to alert (customer access controlled)
160 - */
161 - static async linkCaseToAlert(caseId: number, alertId: number): Promise<{ case_alert_link: { case_id: number; alert_id: number }; success: boolean; message: string }> {
162 - const response = await httpClient.post('/incidents/db_operations/case/alert-link', {
163 - case_id: caseId,
164 - alert_id: alertId
165 - })
166 - return response.data
167 - }
168 -
169 - /**
170 - * Unlink case from alert (customer access controlled)
171 - */
172 - static async unlinkCaseFromAlert(caseId: number, alertId: number): Promise<{ success: boolean; message: string }> {
173 - const response = await httpClient.post('/incidents/db_operations/case/alert-unlink', {
174 - case_id: caseId,
175 - alert_id: alertId
176 - })
177 - return response.data
178 - }
179 -
180 - /**
181 - * Create a new case comment
182 - */
183 - static async createCaseComment(caseId: number, comment: string): Promise<CaseCommentResponse> {
184 - const response = await httpClient.post('/incidents/db_operations/case/comment', {
185 - case_id: caseId,
186 - comment
187 - })
188 - return response.data
189 - }
190 -
191 - /**
192 - * Update an existing case comment
193 - */
194 - static async updateCaseComment(id: number, caseId: number, comment: string): Promise<CaseCommentResponse> {
195 - const response = await httpClient.put('/incidents/db_operations/case/comment', {
196 - id,
197 - case_id: caseId,
198 - comment
199 - })
200 - return response.data
201 - }
202 -
203 - /**
204 - * Delete a case comment
205 - */
206 - static async deleteCaseComment(commentId: number): Promise<{ success: boolean; message: string }> {
207 - const response = await httpClient.delete(`/incidents/db_operations/case/comment/${commentId}`)
208 - return response.data
209 - }
78 + /**
79 + * Get all cases with customer access control
80 + */
81 + static async getCases(): Promise<CasesResponse> {
82 + const response = await httpClient.get("/incidents/db_operations/cases")
83 + return response.data
84 + }
85 +
86 + /**
87 + * Get specific case by ID (with customer access validation)
88 + */
89 + static async getCase(caseId: number): Promise<CaseResponse> {
90 + const response = await httpClient.get(`/incidents/db_operations/case/${caseId}`)
91 + return response.data
92 + }
93 +
94 + /**
95 + * Update case status (customer access controlled)
96 + */
97 + static async updateCaseStatus(caseId: number, status: "open" | "in_progress" | "closed"): Promise<CaseResponse> {
98 + const response = await httpClient.put("/incidents/db_operations/case/status", {
99 + case_id: caseId,
100 + status
101 + })
102 + return response.data
103 + }
104 +
105 + /**
106 + * Update case assigned user (customer access controlled)
107 + */
108 + static async updateCaseAssignedTo(caseId: number, assignedTo: string): Promise<CaseResponse> {
109 + const response = await httpClient.put("/incidents/db_operations/case/assigned-to", {
110 + case_id: caseId,
111 + assigned_to: assignedTo
112 + })
113 + return response.data
114 + }
115 +
116 + /**
117 + * Create new case (customer access controlled)
118 + */
119 + static async createCase(payload: CasePayload): Promise<{ case: Case; success: boolean; message: string }> {
120 + const response = await httpClient.post("/incidents/db_operations/case/create", payload)
121 + return response.data
122 + }
123 +
124 + /**
125 + * Delete case (customer access controlled)
126 + */
127 + static async deleteCase(caseId: number): Promise<{ success: boolean; message: string }> {
128 + const response = await httpClient.delete(`/incidents/db_operations/case/${caseId}`)
129 + return response.data
130 + }
131 +
132 + /**
133 + * Get cases by status with customer filtering
134 + */
135 + static async getCasesByStatus(status: string): Promise<CasesResponse> {
136 + const response = await httpClient.get(`/incidents/db_operations/case/status/${status}`)
137 + return response.data
138 + }
139 +
140 + /**
141 + * Get cases by assigned user with customer filtering
142 + */
143 + static async getCasesByAssignedTo(assignedTo: string): Promise<CasesResponse> {
144 + const response = await httpClient.get(`/incidents/db_operations/case/assigned-to/${assignedTo}`)
145 + return response.data
146 + }
147 +
148 + /**
149 + * Create case from alert (customer access controlled)
150 + */
151 + static async createCaseFromAlert(
152 + alertId: number
153 + ): Promise<{ case_alert_link: { case_id: number; alert_id: number }; success: boolean; message: string }> {
154 + const response = await httpClient.post("/incidents/db_operations/case/from-alert", {
155 + alert_id: alertId
156 + })
157 + return response.data
158 + }
159 +
160 + /**
161 + * Link case to alert (customer access controlled)
162 + */
163 + static async linkCaseToAlert(
164 + caseId: number,
165 + alertId: number
166 + ): Promise<{ case_alert_link: { case_id: number; alert_id: number }; success: boolean; message: string }> {
167 + const response = await httpClient.post("/incidents/db_operations/case/alert-link", {
168 + case_id: caseId,
169 + alert_id: alertId
170 + })
171 + return response.data
172 + }
173 +
174 + /**
175 + * Unlink case from alert (customer access controlled)
176 + */
177 + static async unlinkCaseFromAlert(caseId: number, alertId: number): Promise<{ success: boolean; message: string }> {
178 + const response = await httpClient.post("/incidents/db_operations/case/alert-unlink", {
179 + case_id: caseId,
180 + alert_id: alertId
181 + })
182 + return response.data
183 + }
184 +
185 + /**
186 + * Create a new case comment
187 + */
188 + static async createCaseComment(caseId: number, comment: string): Promise<CaseCommentResponse> {
189 + const response = await httpClient.post("/incidents/db_operations/case/comment", {
190 + case_id: caseId,
191 + comment
192 + })
193 + return response.data
194 + }
195 +
196 + /**
197 + * Update an existing case comment
198 + */
199 + static async updateCaseComment(id: number, caseId: number, comment: string): Promise<CaseCommentResponse> {
200 + const response = await httpClient.put("/incidents/db_operations/case/comment", {
201 + id,
202 + case_id: caseId,
203 + comment
204 + })
205 + return response.data
206 + }
207 +
208 + /**
209 + * Delete a case comment
210 + */
211 + static async deleteCaseComment(commentId: number): Promise<{ success: boolean; message: string }> {
212 + const response = await httpClient.delete(`/incidents/db_operations/case/comment/${commentId}`)
213 + return response.data
214 + }
215 }
216
217 export default CasesAPI
customer_portal/src/components/LoginPage.vue
+178 -164
@@ -1,176 +1,190 @@
1 <template>
2 - <div class="min-h-screen flex">
3 - <!-- Left side - Login Form -->
4 - <div class="flex-1 flex items-center justify-center px-4 sm:px-6 lg:px-8 bg-gray-50">
5 - <div class="max-w-md w-full space-y-8">
6 - <!-- Logo and Title -->
7 - <div class="text-center">
8 - <div class="mb-6">
9 - <img
10 - class="mx-auto h-12 w-auto"
11 - src="/logo.svg"
12 - alt="SOCFortress Logo"
13 - @error="$event.target.style.display = 'none'"
14 - />
15 - </div>
16 - <h2 class="text-4xl font-bold text-gray-900 mb-2">
17 - SOCFortress Customer Portal
18 - </h2>
19 - <p class="text-lg text-gray-600">
20 - Access your security dashboard and reports
21 - </p>
22 - </div>
23 -
24 - <!-- Login Form -->
25 - <div class="mt-8 bg-white py-8 px-6 shadow-lg rounded-lg">
26 - <form @submit.prevent="handleLogin" class="space-y-6">
27 - <div v-if="error" class="bg-red-50 border border-red-200 rounded-md p-3">
28 - <div class="text-sm text-red-700">{{ error }}</div>
29 - </div>
30 -
31 - <div>
32 - <label for="username" class="block text-sm font-medium text-gray-700 mb-2">
33 - Username
34 - </label>
35 - <input
36 - id="username"
37 - v-model="username"
38 - type="text"
39 - required
40 - autocomplete="username"
41 - class="block w-full px-3 py-3 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-base"
42 - placeholder="Enter your username"
43 - />
44 - </div>
45 -
46 - <div>
47 - <label for="password" class="block text-sm font-medium text-gray-700 mb-2">
48 - Password
49 - </label>
50 - <input
51 - id="password"
52 - v-model="password"
53 - type="password"
54 - required
55 - autocomplete="current-password"
56 - class="block w-full px-3 py-3 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-base"
57 - placeholder="Enter your password"
58 - />
59 - </div>
60 -
61 - <div>
62 - <button
63 - type="submit"
64 - :disabled="loading || !username || !password"
65 - class="group relative w-full flex justify-center py-3 px-4 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
66 - >
67 - <span v-if="loading" class="flex items-center">
68 - <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
69 - <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
70 - <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
71 - </svg>
72 - Signing in...
73 - </span>
74 - <span v-else>Sign in</span>
75 - </button>
76 - </div>
77 - </form>
78 - </div>
79 -
80 - <!-- Footer -->
81 - <div class="text-center text-sm text-gray-500">
82 - <p>For customer users only</p>
83 - </div>
84 - </div>
85 - </div>
86 -
87 - <!-- Right side - Background Image/Color -->
88 - <div class="hidden lg:block relative flex-1">
89 - <div class="absolute inset-0 bg-gradient-to-br from-indigo-600 to-purple-700">
90 - <div class="absolute inset-0 bg-black bg-opacity-20"></div>
91 - <div class="relative h-full flex items-center justify-center p-12">
92 - <div class="text-center text-white">
93 - <h3 class="text-3xl font-bold mb-4">
94 - Welcome to Your Security Dashboard
95 - </h3>
96 - <p class="text-xl opacity-90">
97 - Monitor alerts, track cases, and stay informed about your organization's security posture.
98 - </p>
99 - </div>
100 - </div>
101 - </div>
102 - </div>
103 - </div>
2 + <div class="flex min-h-screen">
3 + <!-- Left side - Login Form -->
4 + <div class="flex flex-1 items-center justify-center bg-gray-50 px-4 sm:px-6 lg:px-8">
5 + <div class="w-full max-w-md space-y-8">
6 + <!-- Logo and Title -->
7 + <div class="text-center">
8 + <div class="mb-6 min-h-12">
9 + <img
10 + v-if="portalLogo && showLogo"
11 + class="mx-auto h-12 w-auto"
12 + :src="portalLogo"
13 + :alt="portalTitle"
14 + @error="showLogo = false"
15 + />
16 + </div>
17 + <h2 class="mb-2 min-h-10 text-4xl font-bold text-gray-900">{{ portalTitle }}</h2>
18 + <p class="text-lg text-gray-600">Access your security dashboard and reports</p>
19 + </div>
20 +
21 + <!-- Login Form -->
22 + <div class="mt-8 rounded-lg bg-white px-6 py-8 shadow-lg">
23 + <form @submit.prevent="handleLogin" class="space-y-6">
24 + <div v-if="error" class="rounded-md border border-red-200 bg-red-50 p-3">
25 + <div class="text-sm text-red-700">{{ error }}</div>
26 + </div>
27 +
28 + <div>
29 + <label for="username" class="mb-2 block text-sm font-medium text-gray-700">Username</label>
30 + <input
31 + id="username"
32 + v-model="username"
33 + type="text"
34 + required
35 + autocomplete="username"
36 + class="block w-full rounded-md border border-gray-300 px-3 py-3 text-base placeholder-gray-400 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
37 + placeholder="Enter your username"
38 + />
39 + </div>
40 +
41 + <div>
42 + <label for="password" class="mb-2 block text-sm font-medium text-gray-700">Password</label>
43 + <input
44 + id="password"
45 + v-model="password"
46 + type="password"
47 + required
48 + autocomplete="current-password"
49 + class="block w-full rounded-md border border-gray-300 px-3 py-3 text-base placeholder-gray-400 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
50 + placeholder="Enter your password"
51 + />
52 + </div>
53 +
54 + <div>
55 + <button
56 + type="submit"
57 + :disabled="loading || !username || !password"
58 + class="group relative flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-3 text-base font-medium text-white transition-colors duration-200 hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
59 + >
60 + <span v-if="loading" class="flex items-center">
61 + <svg
62 + class="mr-3 -ml-1 h-5 w-5 animate-spin text-white"
63 + xmlns="http://www.w3.org/2000/svg"
64 + fill="none"
65 + viewBox="0 0 24 24"
66 + >
67 + <circle
68 + class="opacity-25"
69 + cx="12"
70 + cy="12"
71 + r="10"
72 + stroke="currentColor"
73 + stroke-width="4"
74 + ></circle>
75 + <path
76 + class="opacity-75"
77 + fill="currentColor"
78 + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
79 + ></path>
80 + </svg>
81 + Signing in...
82 + </span>
83 + <span v-else>Sign in</span>
84 + </button>
85 + </div>
86 + </form>
87 + </div>
88 +
89 + <!-- Footer -->
90 + <div class="text-center text-sm text-gray-500">
91 + <p>For customer users only</p>
92 + </div>
93 + </div>
94 + </div>
95 +
96 + <!-- Right side - Background Image/Color -->
97 + <div class="relative hidden flex-1 lg:block">
98 + <div class="absolute inset-0 bg-linear-to-br from-indigo-600 to-purple-700">
99 + <div class="bg-opacity-20 absolute inset-0 bg-black"></div>
100 + <div class="relative flex h-full items-center justify-center p-12">
101 + <div class="text-center text-white">
102 + <h3 class="mb-4 text-3xl font-bold">Welcome to Your Security Dashboard</h3>
103 + <p class="text-xl opacity-90">
104 + Monitor alerts, track cases, and stay informed about your organization's security posture.
105 + </p>
106 + </div>
107 + </div>
108 + </div>
109 + </div>
110 + </div>
111 </template>
112
113 <script setup lang="ts">
107 -import { ref } from 'vue'
108 -import { useRouter } from 'vue-router'
109 -import { useAuthStore } from '@/stores/auth'
114 +import { ref, computed } from "vue"
115 +import { useRouter } from "vue-router"
116 +import { usePortalSettingsStore } from "../stores/portalSettings"
117
118 const router = useRouter()
112 -const authStore = useAuthStore()
119 +const portalSettingsStore = usePortalSettingsStore()
120
114 -const username = ref('')
115 -const password = ref('')
121 +const username = ref("")
122 +const password = ref("")
123 const loading = ref(false)
117 -const error = ref('')
124 +const error = ref("")
125 +const showLogo = ref(true)
126 +
127 +const portalTitle = computed(() => portalSettingsStore.portalTitle)
128 +const portalLogo = computed(() => portalSettingsStore.portalLogo)
129
130 const handleLogin = async () => {
120 - loading.value = true
121 - error.value = ''
122 -
123 - try {
124 - // Use the Vite proxy in development, direct URL in production
125 - const apiUrl = import.meta.env.DEV ? '' : (import.meta.env.VITE_API_URL || 'http://localhost:5000')
126 - const response = await fetch(`${apiUrl}/api/auth/token`, {
127 - method: 'POST',
128 - headers: {
129 - 'Content-Type': 'application/x-www-form-urlencoded',
130 - },
131 - body: new URLSearchParams({
132 - username: username.value,
133 - password: password.value
134 - })
135 - })
136 -
137 - if (response.ok) {
138 - const data = await response.json() as { access_token: string; token_type: string }
139 -
140 - // Check if user is customer_user by decoding the token
141 - if (data.access_token) {
142 - try {
143 - const payload = JSON.parse(atob(data.access_token.split('.')[1]))
144 - const userScopes = payload.scopes || []
145 -
146 - // Check if user has customer_user scope
147 - if (userScopes.includes('customer_user')) {
148 - // Store the token and user info
149 - localStorage.setItem('customer-portal-auth-token', data.access_token)
150 - localStorage.setItem('customer-portal-user', JSON.stringify({
151 - username: username.value,
152 - scopes: userScopes
153 - }))
154 -
155 - router.push('/')
156 - } else {
157 - error.value = 'Access denied. Customer portal is for customer users only.'
158 - }
159 - } catch (err) {
160 - error.value = 'Invalid token received'
161 - }
162 - } else {
163 - error.value = 'Login failed'
164 - }
165 - } else {
166 - const errorData = await response.json() as { detail?: string }
167 - error.value = errorData.detail || 'Login failed'
168 - }
169 - } catch (err) {
170 - error.value = 'Network error. Please try again.'
171 - console.error('Login error:', err)
172 - } finally {
173 - loading.value = false
174 - }
131 + loading.value = true
132 + error.value = ""
133 +
134 + try {
135 + // Use the Vite proxy in development, direct URL in production
136 + const apiUrl = import.meta.env.DEV ? "" : import.meta.env.VITE_API_URL || "http://localhost:5000"
137 + const response = await fetch(`${apiUrl}/api/auth/token`, {
138 + method: "POST",
139 + headers: {
140 + "Content-Type": "application/x-www-form-urlencoded"
141 + },
142 + body: new URLSearchParams({
143 + username: username.value,
144 + password: password.value
145 + })
146 + })
147 +
148 + if (response.ok) {
149 + const data = (await response.json()) as { access_token: string; token_type: string }
150 +
151 + // Check if user is customer_user by decoding the token
152 + if (data.access_token) {
153 + try {
154 + const payload = JSON.parse(atob(data.access_token.split(".")[1]))
155 + const userScopes = payload.scopes || []
156 +
157 + // Check if user has customer_user scope
158 + if (userScopes.includes("customer_user")) {
159 + // Store the token and user info
160 + localStorage.setItem("customer-portal-auth-token", data.access_token)
161 + localStorage.setItem(
162 + "customer-portal-user",
163 + JSON.stringify({
164 + username: username.value,
165 + scopes: userScopes
166 + })
167 + )
168 +
169 + router.push("/")
170 + } else {
171 + error.value = "Access denied. Customer portal is for customer users only."
172 + }
173 + } catch (err) {
174 + error.value = "Invalid token received"
175 + }
176 + } else {
177 + error.value = "Login failed"
178 + }
179 + } else {
180 + const errorData = (await response.json()) as { detail?: string }
181 + error.value = errorData.detail || "Login failed"
182 + }
183 + } catch (err) {
184 + error.value = "Network error. Please try again."
185 + console.error("Login error:", err)
186 + } finally {
187 + loading.value = false
188 + }
189 }
190 </script>
customer_portal/src/stores/portalSettings.ts new
+88
@@ -0,0 +1,88 @@
1 +import { defineStore } from "pinia"
2 +import axios from "axios"
3 +
4 +interface PortalSettings {
5 + id: number
6 + title: string
7 + logo_base64: string
8 + logo_mime_type: string
9 + updated_at: string
10 +}
11 +
12 +interface PortalSettingsState {
13 + settings: PortalSettings | null
14 + loading: boolean
15 + error: string | null
16 +}
17 +
18 +const STORAGE_KEY = "customer-portal-settings"
19 +
20 +export const usePortalSettingsStore = defineStore("portalSettings", {
21 + state: (): PortalSettingsState => ({
22 + settings: null,
23 + loading: false,
24 + error: null
25 + }),
26 +
27 + getters: {
28 + portalTitle: state => state.settings?.title || "",
29 + portalLogo: state => {
30 + if (state.settings?.logo_base64 && state.settings?.logo_mime_type) {
31 + return `data:${state.settings.logo_mime_type};base64,${state.settings.logo_base64}`
32 + }
33 + return null
34 + }
35 + },
36 +
37 + actions: {
38 + loadFromSessionStorage() {
39 + try {
40 + const stored = sessionStorage.getItem(STORAGE_KEY)
41 + if (stored) {
42 + const data = JSON.parse(stored)
43 + this.settings = data.settings
44 + return true
45 + }
46 + } catch (error) {
47 + console.error("Failed to load settings from sessionStorage:", error)
48 + sessionStorage.removeItem(STORAGE_KEY)
49 + }
50 + return false
51 + },
52 +
53 + saveToSessionStorage() {
54 + try {
55 + const data = {
56 + settings: this.settings
57 + }
58 + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data))
59 + } catch (error) {
60 + console.error("Failed to save settings to sessionStorage:", error)
61 + }
62 + },
63 +
64 + async fetchSettings(force = false) {
65 + // Try to load from sessionStorage first
66 + if (!force && this.loadFromSessionStorage()) {
67 + return
68 + }
69 +
70 + this.loading = true
71 + this.error = null
72 +
73 + try {
74 + const response = await axios.get("/api/customer_portal/settings")
75 +
76 + if (response.data.success && response.data.settings) {
77 + this.settings = response.data.settings
78 + this.saveToSessionStorage()
79 + }
80 + } catch (error: any) {
81 + this.error = error.response?.data?.message || "Failed to load portal settings"
82 + console.error("Failed to load portal settings:", error)
83 + } finally {
84 + this.loading = false
85 + }
86 + }
87 + }
88 +})
customer_portal/src/views/AgentsPage.vue
+805 -699
@@ -1,565 +1,666 @@
1 <template>
2 - <div class="min-h-screen bg-gray-50">
3 - <!-- Header -->
4 - <header class="bg-white shadow-sm border-b">
5 - <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
6 - <div class="flex justify-between h-16">
7 - <div class="flex items-center">
8 - <img
9 - class="h-8 w-auto mr-3"
10 - src="/logo.svg"
11 - alt="SOCFortress Logo"
12 - />
13 - <h1 class="text-xl font-semibold text-gray-900">Customer Portal</h1>
14 - <nav class="ml-8 flex space-x-8">
15 - <router-link
16 - to="/"
17 - class="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
18 - >
19 - Overview
20 - </router-link>
21 - <router-link
22 - to="/alerts"
23 - class="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
24 - >
25 - Alerts
26 - </router-link>
27 - <router-link
28 - to="/cases"
29 - class="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
30 - >
31 - Cases
32 - </router-link>
33 - <router-link
34 - to="/agents"
35 - class="text-indigo-600 border-b-2 border-indigo-600 px-3 py-2 rounded-md text-sm font-medium"
36 - >
37 - Agents
38 - </router-link>
39 - </nav>
40 - </div>
41 - <div class="flex items-center space-x-4">
42 - <div class="text-sm text-gray-700">
43 - Welcome, <span class="font-medium">{{ username }}</span>
44 - </div>
45 - <button
46 - @click="logout"
47 - class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors"
48 - >
49 - Logout
50 - </button>
51 - </div>
52 - </div>
53 - </div>
54 - </header>
55 -
56 - <!-- Main Content -->
57 - <main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
58 - <div class="px-4 py-6 sm:px-0">
59 - <!-- Page Header -->
60 - <div class="mb-8">
61 - <h2 class="text-2xl font-bold text-gray-900 mb-2">Agents</h2>
62 - <p class="text-gray-600">Monitor and manage your organization's security agents</p>
63 - </div>
64 -
65 - <!-- Loading State -->
66 - <div v-if="loading" class="flex justify-center items-center py-12">
67 - <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
68 - <span class="ml-3 text-gray-600">Loading agents...</span>
69 - </div>
70 -
71 - <!-- Error State -->
72 - <div v-else-if="error" class="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
73 - <div class="flex">
74 - <div class="flex-shrink-0">
75 - <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
76 - <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
77 - </svg>
78 - </div>
79 - <div class="ml-3">
80 - <h3 class="text-sm font-medium text-red-800">Error Loading Agents</h3>
81 - <div class="mt-2 text-sm text-red-700">{{ error }}</div>
82 - <div class="mt-3">
83 - <button
84 - @click="loadAgents"
85 - class="bg-red-100 hover:bg-red-200 text-red-800 px-3 py-1 rounded text-sm font-medium"
86 - >
87 - Try Again
88 - </button>
89 - </div>
90 - </div>
91 - </div>
92 - </div>
93 -
94 - <!-- Content -->
95 - <div v-else>
96 - <!-- Stats Summary -->
97 - <div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
98 - <div class="bg-white overflow-hidden shadow-sm rounded-lg">
99 - <div class="p-6">
100 - <div class="flex items-center">
101 - <div class="flex-shrink-0">
102 - <div class="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
103 - <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
104 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"></path>
105 - </svg>
106 - </div>
107 - </div>
108 - <div class="ml-5 w-0 flex-1">
109 - <dl>
110 - <dt class="text-sm font-medium text-gray-500 truncate">Total Agents</dt>
111 - <dd class="text-2xl font-semibold text-gray-900">{{ agents.length }}</dd>
112 - </dl>
113 - </div>
114 - </div>
115 - </div>
116 - </div>
117 -
118 - <div class="bg-white overflow-hidden shadow-sm rounded-lg">
119 - <div class="p-6">
120 - <div class="flex items-center">
121 - <div class="flex-shrink-0">
122 - <div class="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
123 - <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
124 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
125 - </svg>
126 - </div>
127 - </div>
128 - <div class="ml-5 w-0 flex-1">
129 - <dl>
130 - <dt class="text-sm font-medium text-gray-500 truncate">Active Agents</dt>
131 - <dd class="text-2xl font-semibold text-gray-900">{{ activeAgents }}</dd>
132 - </dl>
133 - </div>
134 - </div>
135 - </div>
136 - </div>
137 -
138 - <div class="bg-white overflow-hidden shadow-sm rounded-lg">
139 - <div class="p-6">
140 - <div class="flex items-center">
141 - <div class="flex-shrink-0">
142 - <div class="w-8 h-8 bg-yellow-500 rounded-md flex items-center justify-center">
143 - <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
144 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
145 - </svg>
146 - </div>
147 - </div>
148 - <div class="ml-5 w-0 flex-1">
149 - <dl>
150 - <dt class="text-sm font-medium text-gray-500 truncate">Critical Assets</dt>
151 - <dd class="text-2xl font-semibold text-gray-900">{{ criticalAgents }}</dd>
152 - </dl>
153 - </div>
154 - </div>
155 - </div>
156 - </div>
157 -
158 - <div class="bg-white overflow-hidden shadow-sm rounded-lg">
159 - <div class="p-6">
160 - <div class="flex items-center">
161 - <div class="flex-shrink-0">
162 - <div class="w-8 h-8 bg-red-500 rounded-md flex items-center justify-center">
163 - <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
164 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
165 - </svg>
166 - </div>
167 - </div>
168 - <div class="ml-5 w-0 flex-1">
169 - <dl>
170 - <dt class="text-sm font-medium text-gray-500 truncate">Offline Agents</dt>
171 - <dd class="text-2xl font-semibold text-gray-900">{{ offlineAgents }}</dd>
172 - </dl>
173 - </div>
174 - </div>
175 - </div>
176 - </div>
177 - </div>
178 -
179 - <!-- Filters -->
180 - <div class="bg-white shadow-sm rounded-lg mb-6">
181 - <div class="px-6 py-4 border-b border-gray-200">
182 - <h3 class="text-lg font-medium text-gray-900">Filters</h3>
183 - </div>
184 - <div class="p-6">
185 - <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
186 - <div>
187 - <label class="block text-sm font-medium text-gray-700 mb-2">Status</label>
188 - <select
189 - v-model="filters.status"
190 - class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
191 - >
192 - <option value="">All Statuses</option>
193 - <option value="active">Active</option>
194 - <option value="never_connected">Never Connected</option>
195 - <option value="disconnected">Disconnected</option>
196 - <option value="pending">Pending</option>
197 - </select>
198 - </div>
199 - <div>
200 - <label class="block text-sm font-medium text-gray-700 mb-2">Critical Asset</label>
201 - <select
202 - v-model="filters.critical"
203 - class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
204 - >
205 - <option value="">All Assets</option>
206 - <option value="true">Critical Assets</option>
207 - <option value="false">Regular Assets</option>
208 - </select>
209 - </div>
210 - <div>
211 - <label class="block text-sm font-medium text-gray-700 mb-2">Operating System</label>
212 - <select
213 - v-model="filters.os"
214 - class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
215 - >
216 - <option value="">All OS</option>
217 - <option v-for="os in uniqueOperatingSystems" :key="os" :value="os">{{ os }}</option>
218 - </select>
219 - </div>
220 - <div>
221 - <label class="block text-sm font-medium text-gray-700 mb-2">Search</label>
222 - <input
223 - v-model="filters.search"
224 - type="text"
225 - placeholder="Search hostname, IP, agent ID..."
226 - class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
227 - />
228 - </div>
229 - </div>
230 - <div class="mt-4 flex justify-end">
231 - <button
232 - @click="clearFilters"
233 - class="text-indigo-600 hover:text-indigo-500 font-medium text-sm"
234 - >
235 - Clear Filters
236 - </button>
237 - </div>
238 - </div>
239 - </div>
240 -
241 - <!-- Agents Table -->
242 - <div class="bg-white shadow-sm rounded-lg overflow-hidden">
243 - <div class="px-6 py-4 border-b border-gray-200">
244 - <h3 class="text-lg font-medium text-gray-900">Agents ({{ filteredAgents.length }})</h3>
245 - </div>
246 - <div v-if="filteredAgents.length === 0" class="p-6 text-center text-gray-500">
247 - No agents found matching your criteria.
248 - </div>
249 - <div v-else class="overflow-x-auto">
250 - <table class="min-w-full divide-y divide-gray-200">
251 - <thead class="bg-gray-50">
252 - <tr>
253 - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
254 - Agent
255 - </th>
256 - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
257 - Status
258 - </th>
259 - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
260 - Operating System
261 - </th>
262 - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
263 - Last Seen
264 - </th>
265 - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
266 - Version
267 - </th>
268 - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
269 - Actions
270 - </th>
271 - </tr>
272 - </thead>
273 - <tbody class="bg-white divide-y divide-gray-200">
274 - <tr v-for="agent in paginatedAgents" :key="agent.id" class="hover:bg-gray-50">
275 - <td class="px-6 py-4 whitespace-nowrap">
276 - <div class="flex items-center">
277 - <div class="flex-shrink-0 h-10 w-10">
278 - <div
279 - class="h-10 w-10 rounded-full flex items-center justify-center text-sm font-medium text-white"
280 - :class="{
281 - 'bg-green-500': agent.wazuh_agent_status === 'active',
282 - 'bg-red-500': agent.wazuh_agent_status === 'disconnected',
283 - 'bg-yellow-500': agent.wazuh_agent_status === 'never_connected',
284 - 'bg-gray-500': agent.wazuh_agent_status === 'pending'
285 - }"
286 - >
287 - {{ agent.hostname.charAt(0).toUpperCase() }}
288 - </div>
289 - </div>
290 - <div class="ml-4">
291 - <div class="text-sm font-medium text-gray-900">{{ agent.hostname }}</div>
292 - <div class="text-sm text-gray-500">{{ agent.ip_address }}</div>
293 - <div class="text-xs text-gray-400">ID: {{ agent.agent_id }}</div>
294 - </div>
295 - </div>
296 - </td>
297 - <td class="px-6 py-4 whitespace-nowrap">
298 - <div class="flex items-center">
299 - <span
300 - class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
301 - :class="{
302 - 'bg-green-100 text-green-800': agent.wazuh_agent_status === 'active',
303 - 'bg-red-100 text-red-800': agent.wazuh_agent_status === 'disconnected',
304 - 'bg-yellow-100 text-yellow-800': agent.wazuh_agent_status === 'never_connected',
305 - 'bg-gray-100 text-gray-800': agent.wazuh_agent_status === 'pending'
306 - }"
307 - >
308 - {{ agent.wazuh_agent_status }}
309 - </span>
310 - <span
311 - v-if="agent.critical_asset"
312 - class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800"
313 - >
314 - Critical
315 - </span>
316 - <span
317 - v-if="agent.quarantined"
318 - class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800"
319 - >
320 - Quarantined
321 - </span>
322 - </div>
323 - </td>
324 - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
325 - {{ agent.os }}
326 - </td>
327 - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
328 - {{ formatTimeAgo(agent.wazuh_last_seen) }}
329 - </td>
330 - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
331 - <div>Wazuh: {{ agent.wazuh_agent_version }}</div>
332 - <div v-if="agent.velociraptor_agent_version" class="text-xs">
333 - VR: {{ agent.velociraptor_agent_version }}
334 - </div>
335 - </td>
336 - <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
337 - <div class="flex space-x-2">
338 - <button
339 - v-if="!agent.critical_asset"
340 - @click="markAsCritical(agent)"
341 - class="text-orange-600 hover:text-orange-900 text-xs"
342 - :disabled="updatingAgent === agent.agent_id"
343 - >
344 - Mark Critical
345 - </button>
346 - <button
347 - v-else
348 - @click="markAsNotCritical(agent)"
349 - class="text-gray-600 hover:text-gray-900 text-xs"
350 - :disabled="updatingAgent === agent.agent_id"
351 - >
352 - Remove Critical
353 - </button>
354 - <button
355 - @click="viewAgentDetails(agent)"
356 - class="text-indigo-600 hover:text-indigo-900 text-xs"
357 - >
358 - Details
359 - </button>
360 - </div>
361 - </td>
362 - </tr>
363 - </tbody>
364 - </table>
365 - </div>
366 -
367 - <!-- Pagination -->
368 - <div v-if="totalPages > 1" class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
369 - <div class="flex-1 flex justify-between sm:hidden">
370 - <button
371 - @click="previousPage"
372 - :disabled="currentPage <= 1"
373 - class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
374 - >
375 - Previous
376 - </button>
377 - <button
378 - @click="nextPage"
379 - :disabled="currentPage >= totalPages"
380 - class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
381 - >
382 - Next
383 - </button>
384 - </div>
385 - <div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
386 - <div>
387 - <p class="text-sm text-gray-700">
388 - Showing
389 - <span class="font-medium">{{ (currentPage - 1) * pageSize + 1 }}</span>
390 - to
391 - <span class="font-medium">{{ Math.min(currentPage * pageSize, filteredAgents.length) }}</span>
392 - of
393 - <span class="font-medium">{{ filteredAgents.length }}</span>
394 - results
395 - </p>
396 - </div>
397 - <div>
398 - <nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
399 - <button
400 - @click="previousPage"
401 - :disabled="currentPage <= 1"
402 - class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
403 - >
404 - <span class="sr-only">Previous</span>
405 - <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
406 - <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
407 - </svg>
408 - </button>
409 - <button
410 - v-for="page in visiblePages"
411 - :key="page"
412 - @click="typeof page === 'number' ? currentPage = page : null"
413 - :class="{
414 - 'z-10 bg-indigo-50 border-indigo-500 text-indigo-600': page === currentPage,
415 - 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50': page !== currentPage
416 - }"
417 - class="relative inline-flex items-center px-4 py-2 border text-sm font-medium"
418 - :disabled="typeof page === 'string'"
419 - >
420 - {{ page }}
421 - </button>
422 - <button
423 - @click="nextPage"
424 - :disabled="currentPage >= totalPages"
425 - class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
426 - >
427 - <span class="sr-only">Next</span>
428 - <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
429 - <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
430 - </svg>
431 - </button>
432 - </nav>
433 - </div>
434 - </div>
435 - </div>
436 - </div>
437 - </div>
438 - </div>
439 - </main>
440 -
441 - <!-- Agent Details Modal -->
442 - <div
443 - v-if="selectedAgent"
444 - class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50"
445 - @click="closeAgentDetails"
446 - >
447 - <div
448 - class="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white"
449 - @click.stop
450 - >
451 - <div class="flex justify-between items-center mb-4">
452 - <h3 class="text-lg font-bold text-gray-900">Agent Details</h3>
453 - <button
454 - @click="closeAgentDetails"
455 - class="text-gray-400 hover:text-gray-600"
456 - >
457 - <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
458 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
459 - </svg>
460 - </button>
461 - </div>
462 -
463 - <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
464 - <div>
465 - <label class="block text-sm font-medium text-gray-700">Hostname</label>
466 - <p class="mt-1 text-sm text-gray-900">{{ selectedAgent.hostname }}</p>
467 - </div>
468 - <div>
469 - <label class="block text-sm font-medium text-gray-700">Agent ID</label>
470 - <p class="mt-1 text-sm text-gray-900">{{ selectedAgent.agent_id }}</p>
471 - </div>
472 - <div>
473 - <label class="block text-sm font-medium text-gray-700">IP Address</label>
474 - <p class="mt-1 text-sm text-gray-900">{{ selectedAgent.ip_address }}</p>
475 - </div>
476 - <div>
477 - <label class="block text-sm font-medium text-gray-700">Operating System</label>
478 - <p class="mt-1 text-sm text-gray-900">{{ selectedAgent.os }}</p>
479 - </div>
480 - <div>
481 - <label class="block text-sm font-medium text-gray-700">Wazuh Status</label>
482 - <span
483 - class="mt-1 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
484 - :class="{
485 - 'bg-green-100 text-green-800': selectedAgent.wazuh_agent_status === 'active',
486 - 'bg-red-100 text-red-800': selectedAgent.wazuh_agent_status === 'disconnected',
487 - 'bg-yellow-100 text-yellow-800': selectedAgent.wazuh_agent_status === 'never_connected',
488 - 'bg-gray-100 text-gray-800': selectedAgent.wazuh_agent_status === 'pending'
489 - }"
490 - >
491 - {{ selectedAgent.wazuh_agent_status }}
492 - </span>
493 - </div>
494 - <div>
495 - <label class="block text-sm font-medium text-gray-700">Wazuh Version</label>
496 - <p class="mt-1 text-sm text-gray-900">{{ selectedAgent.wazuh_agent_version }}</p>
497 - </div>
498 - <div>
499 - <label class="block text-sm font-medium text-gray-700">Last Seen (Wazuh)</label>
500 - <p class="mt-1 text-sm text-gray-900">{{ formatDateTime(selectedAgent.wazuh_last_seen) }}</p>
501 - </div>
502 - <div>
503 - <label class="block text-sm font-medium text-gray-700">Velociraptor ID</label>
504 - <p class="mt-1 text-sm text-gray-900">{{ selectedAgent.velociraptor_id || 'N/A' }}</p>
505 - </div>
506 - <div v-if="selectedAgent.velociraptor_agent_version">
507 - <label class="block text-sm font-medium text-gray-700">Velociraptor Version</label>
508 - <p class="mt-1 text-sm text-gray-900">{{ selectedAgent.velociraptor_agent_version }}</p>
509 - </div>
510 - <div v-if="selectedAgent.velociraptor_last_seen">
511 - <label class="block text-sm font-medium text-gray-700">Last Seen (Velociraptor)</label>
512 - <p class="mt-1 text-sm text-gray-900">{{ formatDateTime(selectedAgent.velociraptor_last_seen) }}</p>
513 - </div>
514 - <div>
515 - <label class="block text-sm font-medium text-gray-700">Critical Asset</label>
516 - <span
517 - class="mt-1 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
518 - :class="{
519 - 'bg-orange-100 text-orange-800': selectedAgent.critical_asset,
520 - 'bg-gray-100 text-gray-800': !selectedAgent.critical_asset
521 - }"
522 - >
523 - {{ selectedAgent.critical_asset ? 'Yes' : 'No' }}
524 - </span>
525 - </div>
526 - <div>
527 - <label class="block text-sm font-medium text-gray-700">Quarantined</label>
528 - <span
529 - class="mt-1 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
530 - :class="{
531 - 'bg-red-100 text-red-800': selectedAgent.quarantined,
532 - 'bg-gray-100 text-gray-800': !selectedAgent.quarantined
533 - }"
534 - >
535 - {{ selectedAgent.quarantined ? 'Yes' : 'No' }}
536 - </span>
537 - </div>
538 - </div>
539 - </div>
540 - </div>
541 - </div>
2 + <div class="min-h-screen bg-gray-50">
3 + <!-- Header -->
4 + <header class="border-b bg-white shadow-sm">
5 + <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
6 + <div class="flex h-16 justify-between">
7 + <div class="flex items-center">
8 + <div class="mr-3 min-h-8">
9 + <img
10 + v-if="portalLogo && showLogo"
11 + class="h-8 w-auto"
12 + :src="portalLogo"
13 + :alt="portalTitle"
14 + @error="showLogo = false"
15 + />
16 + </div>
17 + <h1 class="text-xl font-semibold text-gray-900">{{ portalTitle }}</h1>
18 + <nav class="ml-8 flex space-x-8">
19 + <router-link
20 + to="/"
21 + class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-900"
22 + >
23 + Overview
24 + </router-link>
25 + <router-link
26 + to="/alerts"
27 + class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-900"
28 + >
29 + Alerts
30 + </router-link>
31 + <router-link
32 + to="/cases"
33 + class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-900"
34 + >
35 + Cases
36 + </router-link>
37 + <router-link
38 + to="/agents"
39 + class="rounded-md border-b-2 border-indigo-600 px-3 py-2 text-sm font-medium text-indigo-600"
40 + >
41 + Agents
42 + </router-link>
43 + </nav>
44 + </div>
45 + <div class="flex items-center space-x-4">
46 + <div class="text-sm text-gray-700">
47 + Welcome,
48 + <span class="font-medium">{{ username }}</span>
49 + </div>
50 + <button
51 + @click="logout"
52 + class="rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700"
53 + >
54 + Logout
55 + </button>
56 + </div>
57 + </div>
58 + </div>
59 + </header>
60 +
61 + <!-- Main Content -->
62 + <main class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
63 + <div class="px-4 py-6 sm:px-0">
64 + <!-- Page Header -->
65 + <div class="mb-8">
66 + <h2 class="mb-2 text-2xl font-bold text-gray-900">Agents</h2>
67 + <p class="text-gray-600">Monitor and manage your organization's security agents</p>
68 + </div>
69 +
70 + <!-- Loading State -->
71 + <div v-if="loading" class="flex items-center justify-center py-12">
72 + <div class="h-12 w-12 animate-spin rounded-full border-b-2 border-indigo-600"></div>
73 + <span class="ml-3 text-gray-600">Loading agents...</span>
74 + </div>
75 +
76 + <!-- Error State -->
77 + <div v-else-if="error" class="mb-6 rounded-lg border border-red-200 bg-red-50 p-4">
78 + <div class="flex">
79 + <div class="flex-shrink-0">
80 + <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
81 + <path
82 + fill-rule="evenodd"
83 + d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
84 + clip-rule="evenodd"
85 + />
86 + </svg>
87 + </div>
88 + <div class="ml-3">
89 + <h3 class="text-sm font-medium text-red-800">Error Loading Agents</h3>
90 + <div class="mt-2 text-sm text-red-700">{{ error }}</div>
91 + <div class="mt-3">
92 + <button
93 + @click="loadAgents"
94 + class="rounded bg-red-100 px-3 py-1 text-sm font-medium text-red-800 hover:bg-red-200"
95 + >
96 + Try Again
97 + </button>
98 + </div>
99 + </div>
100 + </div>
101 + </div>
102 +
103 + <!-- Content -->
104 + <div v-else>
105 + <!-- Stats Summary -->
106 + <div class="mb-8 grid grid-cols-1 gap-6 md:grid-cols-4">
107 + <div class="overflow-hidden rounded-lg bg-white shadow-sm">
108 + <div class="p-6">
109 + <div class="flex items-center">
110 + <div class="flex-shrink-0">
111 + <div class="flex h-8 w-8 items-center justify-center rounded-md bg-blue-500">
112 + <svg
113 + class="h-5 w-5 text-white"
114 + fill="none"
115 + stroke="currentColor"
116 + viewBox="0 0 24 24"
117 + >
118 + <path
119 + stroke-linecap="round"
120 + stroke-linejoin="round"
121 + stroke-width="2"
122 + d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
123 + ></path>
124 + </svg>
125 + </div>
126 + </div>
127 + <div class="ml-5 w-0 flex-1">
128 + <dl>
129 + <dt class="truncate text-sm font-medium text-gray-500">Total Agents</dt>
130 + <dd class="text-2xl font-semibold text-gray-900">{{ agents.length }}</dd>
131 + </dl>
132 + </div>
133 + </div>
134 + </div>
135 + </div>
136 +
137 + <div class="overflow-hidden rounded-lg bg-white shadow-sm">
138 + <div class="p-6">
139 + <div class="flex items-center">
140 + <div class="flex-shrink-0">
141 + <div class="flex h-8 w-8 items-center justify-center rounded-md bg-green-500">
142 + <svg
143 + class="h-5 w-5 text-white"
144 + fill="none"
145 + stroke="currentColor"
146 + viewBox="0 0 24 24"
147 + >
148 + <path
149 + stroke-linecap="round"
150 + stroke-linejoin="round"
151 + stroke-width="2"
152 + d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
153 + ></path>
154 + </svg>
155 + </div>
156 + </div>
157 + <div class="ml-5 w-0 flex-1">
158 + <dl>
159 + <dt class="truncate text-sm font-medium text-gray-500">Active Agents</dt>
160 + <dd class="text-2xl font-semibold text-gray-900">{{ activeAgents }}</dd>
161 + </dl>
162 + </div>
163 + </div>
164 + </div>
165 + </div>
166 +
167 + <div class="overflow-hidden rounded-lg bg-white shadow-sm">
168 + <div class="p-6">
169 + <div class="flex items-center">
170 + <div class="flex-shrink-0">
171 + <div class="flex h-8 w-8 items-center justify-center rounded-md bg-yellow-500">
172 + <svg
173 + class="h-5 w-5 text-white"
174 + fill="none"
175 + stroke="currentColor"
176 + viewBox="0 0 24 24"
177 + >
178 + <path
179 + stroke-linecap="round"
180 + stroke-linejoin="round"
181 + stroke-width="2"
182 + d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
183 + ></path>
184 + </svg>
185 + </div>
186 + </div>
187 + <div class="ml-5 w-0 flex-1">
188 + <dl>
189 + <dt class="truncate text-sm font-medium text-gray-500">Critical Assets</dt>
190 + <dd class="text-2xl font-semibold text-gray-900">{{ criticalAgents }}</dd>
191 + </dl>
192 + </div>
193 + </div>
194 + </div>
195 + </div>
196 +
197 + <div class="overflow-hidden rounded-lg bg-white shadow-sm">
198 + <div class="p-6">
199 + <div class="flex items-center">
200 + <div class="flex-shrink-0">
201 + <div class="flex h-8 w-8 items-center justify-center rounded-md bg-red-500">
202 + <svg
203 + class="h-5 w-5 text-white"
204 + fill="none"
205 + stroke="currentColor"
206 + viewBox="0 0 24 24"
207 + >
208 + <path
209 + stroke-linecap="round"
210 + stroke-linejoin="round"
211 + stroke-width="2"
212 + d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
213 + ></path>
214 + </svg>
215 + </div>
216 + </div>
217 + <div class="ml-5 w-0 flex-1">
218 + <dl>
219 + <dt class="truncate text-sm font-medium text-gray-500">Offline Agents</dt>
220 + <dd class="text-2xl font-semibold text-gray-900">{{ offlineAgents }}</dd>
221 + </dl>
222 + </div>
223 + </div>
224 + </div>
225 + </div>
226 + </div>
227 +
228 + <!-- Filters -->
229 + <div class="mb-6 rounded-lg bg-white shadow-sm">
230 + <div class="border-b border-gray-200 px-6 py-4">
231 + <h3 class="text-lg font-medium text-gray-900">Filters</h3>
232 + </div>
233 + <div class="p-6">
234 + <div class="grid grid-cols-1 gap-4 md:grid-cols-4">
235 + <div>
236 + <label class="mb-2 block text-sm font-medium text-gray-700">Status</label>
237 + <select
238 + v-model="filters.status"
239 + class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-indigo-500 focus:outline-none"
240 + >
241 + <option value="">All Statuses</option>
242 + <option value="active">Active</option>
243 + <option value="never_connected">Never Connected</option>
244 + <option value="disconnected">Disconnected</option>
245 + <option value="pending">Pending</option>
246 + </select>
247 + </div>
248 + <div>
249 + <label class="mb-2 block text-sm font-medium text-gray-700">Critical Asset</label>
250 + <select
251 + v-model="filters.critical"
252 + class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-indigo-500 focus:outline-none"
253 + >
254 + <option value="">All Assets</option>
255 + <option value="true">Critical Assets</option>
256 + <option value="false">Regular Assets</option>
257 + </select>
258 + </div>
259 + <div>
260 + <label class="mb-2 block text-sm font-medium text-gray-700">Operating System</label>
261 + <select
262 + v-model="filters.os"
263 + class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-indigo-500 focus:outline-none"
264 + >
265 + <option value="">All OS</option>
266 + <option v-for="os in uniqueOperatingSystems" :key="os" :value="os">
267 + {{ os }}
268 + </option>
269 + </select>
270 + </div>
271 + <div>
272 + <label class="mb-2 block text-sm font-medium text-gray-700">Search</label>
273 + <input
274 + v-model="filters.search"
275 + type="text"
276 + placeholder="Search hostname, IP, agent ID..."
277 + class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-indigo-500 focus:outline-none"
278 + />
279 + </div>
280 + </div>
281 + <div class="mt-4 flex justify-end">
282 + <button
283 + @click="clearFilters"
284 + class="text-sm font-medium text-indigo-600 hover:text-indigo-500"
285 + >
286 + Clear Filters
287 + </button>
288 + </div>
289 + </div>
290 + </div>
291 +
292 + <!-- Agents Table -->
293 + <div class="overflow-hidden rounded-lg bg-white shadow-sm">
294 + <div class="border-b border-gray-200 px-6 py-4">
295 + <h3 class="text-lg font-medium text-gray-900">Agents ({{ filteredAgents.length }})</h3>
296 + </div>
297 + <div v-if="filteredAgents.length === 0" class="p-6 text-center text-gray-500">
298 + No agents found matching your criteria.
299 + </div>
300 + <div v-else class="overflow-x-auto">
301 + <table class="min-w-full divide-y divide-gray-200">
302 + <thead class="bg-gray-50">
303 + <tr>
304 + <th
305 + scope="col"
306 + class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
307 + >
308 + Agent
309 + </th>
310 + <th
311 + scope="col"
312 + class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
313 + >
314 + Status
315 + </th>
316 + <th
317 + scope="col"
318 + class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
319 + >
320 + Operating System
321 + </th>
322 + <th
323 + scope="col"
324 + class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
325 + >
326 + Last Seen
327 + </th>
328 + <th
329 + scope="col"
330 + class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
331 + >
332 + Version
333 + </th>
334 + <th
335 + scope="col"
336 + class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
337 + >
338 + Actions
339 + </th>
340 + </tr>
341 + </thead>
342 + <tbody class="divide-y divide-gray-200 bg-white">
343 + <tr v-for="agent in paginatedAgents" :key="agent.id" class="hover:bg-gray-50">
344 + <td class="px-6 py-4 whitespace-nowrap">
345 + <div class="flex items-center">
346 + <div class="h-10 w-10 flex-shrink-0">
347 + <div
348 + class="flex h-10 w-10 items-center justify-center rounded-full text-sm font-medium text-white"
349 + :class="{
350 + 'bg-green-500': agent.wazuh_agent_status === 'active',
351 + 'bg-red-500': agent.wazuh_agent_status === 'disconnected',
352 + 'bg-yellow-500':
353 + agent.wazuh_agent_status === 'never_connected',
354 + 'bg-gray-500': agent.wazuh_agent_status === 'pending'
355 + }"
356 + >
357 + {{ agent.hostname.charAt(0).toUpperCase() }}
358 + </div>
359 + </div>
360 + <div class="ml-4">
361 + <div class="text-sm font-medium text-gray-900">
362 + {{ agent.hostname }}
363 + </div>
364 + <div class="text-sm text-gray-500">{{ agent.ip_address }}</div>
365 + <div class="text-xs text-gray-400">ID: {{ agent.agent_id }}</div>
366 + </div>
367 + </div>
368 + </td>
369 + <td class="px-6 py-4 whitespace-nowrap">
370 + <div class="flex items-center">
371 + <span
372 + class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
373 + :class="{
374 + 'bg-green-100 text-green-800':
375 + agent.wazuh_agent_status === 'active',
376 + 'bg-red-100 text-red-800':
377 + agent.wazuh_agent_status === 'disconnected',
378 + 'bg-yellow-100 text-yellow-800':
379 + agent.wazuh_agent_status === 'never_connected',
380 + 'bg-gray-100 text-gray-800':
381 + agent.wazuh_agent_status === 'pending'
382 + }"
383 + >
384 + {{ agent.wazuh_agent_status }}
385 + </span>
386 + <span
387 + v-if="agent.critical_asset"
388 + class="ml-2 inline-flex items-center rounded-full bg-orange-100 px-2.5 py-0.5 text-xs font-medium text-orange-800"
389 + >
390 + Critical
391 + </span>
392 + <span
393 + v-if="agent.quarantined"
394 + class="ml-2 inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800"
395 + >
396 + Quarantined
397 + </span>
398 + </div>
399 + </td>
400 + <td class="px-6 py-4 text-sm whitespace-nowrap text-gray-900">
401 + {{ agent.os }}
402 + </td>
403 + <td class="px-6 py-4 text-sm whitespace-nowrap text-gray-500">
404 + {{ formatTimeAgo(agent.wazuh_last_seen) }}
405 + </td>
406 + <td class="px-6 py-4 text-sm whitespace-nowrap text-gray-500">
407 + <div>Wazuh: {{ agent.wazuh_agent_version }}</div>
408 + <div v-if="agent.velociraptor_agent_version" class="text-xs">
409 + VR: {{ agent.velociraptor_agent_version }}
410 + </div>
411 + </td>
412 + <td class="px-6 py-4 text-right text-sm font-medium whitespace-nowrap">
413 + <div class="flex space-x-2">
414 + <button
415 + v-if="!agent.critical_asset"
416 + @click="markAsCritical(agent)"
417 + class="text-xs text-orange-600 hover:text-orange-900"
418 + :disabled="updatingAgent === agent.agent_id"
419 + >
420 + Mark Critical
421 + </button>
422 + <button
423 + v-else
424 + @click="markAsNotCritical(agent)"
425 + class="text-xs text-gray-600 hover:text-gray-900"
426 + :disabled="updatingAgent === agent.agent_id"
427 + >
428 + Remove Critical
429 + </button>
430 + <button
431 + @click="viewAgentDetails(agent)"
432 + class="text-xs text-indigo-600 hover:text-indigo-900"
433 + >
434 + Details
435 + </button>
436 + </div>
437 + </td>
438 + </tr>
439 + </tbody>
440 + </table>
441 + </div>
442 +
443 + <!-- Pagination -->
444 + <div
445 + v-if="totalPages > 1"
446 + class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6"
447 + >
448 + <div class="flex flex-1 justify-between sm:hidden">
449 + <button
450 + @click="previousPage"
451 + :disabled="currentPage <= 1"
452 + class="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
453 + >
454 + Previous
455 + </button>
456 + <button
457 + @click="nextPage"
458 + :disabled="currentPage >= totalPages"
459 + class="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
460 + >
461 + Next
462 + </button>
463 + </div>
464 + <div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
465 + <div>
466 + <p class="text-sm text-gray-700">
467 + Showing
468 + <span class="font-medium">{{ (currentPage - 1) * pageSize + 1 }}</span>
469 + to
470 + <span class="font-medium">
471 + {{ Math.min(currentPage * pageSize, filteredAgents.length) }}
472 + </span>
473 + of
474 + <span class="font-medium">{{ filteredAgents.length }}</span>
475 + results
476 + </p>
477 + </div>
478 + <div>
479 + <nav
480 + class="relative z-0 inline-flex -space-x-px rounded-md shadow-sm"
481 + aria-label="Pagination"
482 + >
483 + <button
484 + @click="previousPage"
485 + :disabled="currentPage <= 1"
486 + class="relative inline-flex items-center rounded-l-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
487 + >
488 + <span class="sr-only">Previous</span>
489 + <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
490 + <path
491 + fill-rule="evenodd"
492 + d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
493 + clip-rule="evenodd"
494 + />
495 + </svg>
496 + </button>
497 + <button
498 + v-for="page in visiblePages"
499 + :key="page"
500 + @click="typeof page === 'number' ? (currentPage = page) : null"
501 + :class="{
502 + 'z-10 border-indigo-500 bg-indigo-50 text-indigo-600':
503 + page === currentPage,
504 + 'border-gray-300 bg-white text-gray-500 hover:bg-gray-50':
505 + page !== currentPage
506 + }"
507 + class="relative inline-flex items-center border px-4 py-2 text-sm font-medium"
508 + :disabled="typeof page === 'string'"
509 + >
510 + {{ page }}
511 + </button>
512 + <button
513 + @click="nextPage"
514 + :disabled="currentPage >= totalPages"
515 + class="relative inline-flex items-center rounded-r-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
516 + >
517 + <span class="sr-only">Next</span>
518 + <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
519 + <path
520 + fill-rule="evenodd"
521 + d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
522 + clip-rule="evenodd"
523 + />
524 + </svg>
525 + </button>
526 + </nav>
527 + </div>
528 + </div>
529 + </div>
530 + </div>
531 + </div>
532 + </div>
533 + </main>
534 +
535 + <!-- Agent Details Modal -->
536 + <div
537 + v-if="selectedAgent"
538 + class="bg-opacity-50 fixed inset-0 z-50 h-full w-full overflow-y-auto bg-gray-600"
539 + @click="closeAgentDetails"
540 + >
541 + <div
542 + class="relative top-20 mx-auto w-11/12 rounded-md border bg-white p-5 shadow-lg md:w-3/4 lg:w-1/2"
543 + @click.stop
544 + >
545 + <div class="mb-4 flex items-center justify-between">
546 + <h3 class="text-lg font-bold text-gray-900">Agent Details</h3>
547 + <button @click="closeAgentDetails" class="text-gray-400 hover:text-gray-600">
548 + <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
549 + <path
550 + stroke-linecap="round"
551 + stroke-linejoin="round"
552 + stroke-width="2"
553 + d="M6 18L18 6M6 6l12 12"
554 + ></path>
555 + </svg>
556 + </button>
557 + </div>
558 +
559 + <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
560 + <div>
561 + <label class="block text-sm font-medium text-gray-700">Hostname</label>
562 + <p class="mt-1 text-sm text-gray-900">{{ selectedAgent.hostname }}</p>
563 + </div>
564 + <div>
565 + <label class="block text-sm font-medium text-gray-700">Agent ID</label>
566 + <p class="mt-1 text-sm text-gray-900">{{ selectedAgent.agent_id }}</p>
567 + </div>
568 + <div>
569 + <label class="block text-sm font-medium text-gray-700">IP Address</label>
570 + <p class="mt-1 text-sm text-gray-900">{{ selectedAgent.ip_address }}</p>
571 + </div>
572 + <div>
573 + <label class="block text-sm font-medium text-gray-700">Operating System</label>
574 + <p class="mt-1 text-sm text-gray-900">{{ selectedAgent.os }}</p>
575 + </div>
576 + <div>
577 + <label class="block text-sm font-medium text-gray-700">Wazuh Status</label>
578 + <span
579 + class="mt-1 inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
580 + :class="{
581 + 'bg-green-100 text-green-800': selectedAgent.wazuh_agent_status === 'active',
582 + 'bg-red-100 text-red-800': selectedAgent.wazuh_agent_status === 'disconnected',
583 + 'bg-yellow-100 text-yellow-800': selectedAgent.wazuh_agent_status === 'never_connected',
584 + 'bg-gray-100 text-gray-800': selectedAgent.wazuh_agent_status === 'pending'
585 + }"
586 + >
587 + {{ selectedAgent.wazuh_agent_status }}
588 + </span>
589 + </div>
590 + <div>
591 + <label class="block text-sm font-medium text-gray-700">Wazuh Version</label>
592 + <p class="mt-1 text-sm text-gray-900">{{ selectedAgent.wazuh_agent_version }}</p>
593 + </div>
594 + <div>
595 + <label class="block text-sm font-medium text-gray-700">Last Seen (Wazuh)</label>
596 + <p class="mt-1 text-sm text-gray-900">{{ formatDateTime(selectedAgent.wazuh_last_seen) }}</p>
597 + </div>
598 + <div>
599 + <label class="block text-sm font-medium text-gray-700">Velociraptor ID</label>
600 + <p class="mt-1 text-sm text-gray-900">{{ selectedAgent.velociraptor_id || "N/A" }}</p>
601 + </div>
602 + <div v-if="selectedAgent.velociraptor_agent_version">
603 + <label class="block text-sm font-medium text-gray-700">Velociraptor Version</label>
604 + <p class="mt-1 text-sm text-gray-900">{{ selectedAgent.velociraptor_agent_version }}</p>
605 + </div>
606 + <div v-if="selectedAgent.velociraptor_last_seen">
607 + <label class="block text-sm font-medium text-gray-700">Last Seen (Velociraptor)</label>
608 + <p class="mt-1 text-sm text-gray-900">
609 + {{ formatDateTime(selectedAgent.velociraptor_last_seen) }}
610 + </p>
611 + </div>
612 + <div>
613 + <label class="block text-sm font-medium text-gray-700">Critical Asset</label>
614 + <span
615 + class="mt-1 inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
616 + :class="{
617 + 'bg-orange-100 text-orange-800': selectedAgent.critical_asset,
618 + 'bg-gray-100 text-gray-800': !selectedAgent.critical_asset
619 + }"
620 + >
621 + {{ selectedAgent.critical_asset ? "Yes" : "No" }}
622 + </span>
623 + </div>
624 + <div>
625 + <label class="block text-sm font-medium text-gray-700">Quarantined</label>
626 + <span
627 + class="mt-1 inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
628 + :class="{
629 + 'bg-red-100 text-red-800': selectedAgent.quarantined,
630 + 'bg-gray-100 text-gray-800': !selectedAgent.quarantined
631 + }"
632 + >
633 + {{ selectedAgent.quarantined ? "Yes" : "No" }}
634 + </span>
635 + </div>
636 + </div>
637 + </div>
638 + </div>
639 + </div>
640 </template>
641
642 <script setup lang="ts">
545 -import { ref, computed, onMounted } from 'vue'
546 -import { useRouter } from 'vue-router'
547 -import AgentsAPI, { type Agent } from '@/api/agents'
643 +import { ref, computed, onMounted } from "vue"
644 +import { useRouter } from "vue-router"
645 +import { usePortalSettingsStore } from "@/stores/portalSettings"
646 +import AgentsAPI, { type Agent } from "@/api/agents"
647
648 const router = useRouter()
649 +const portalSettingsStore = usePortalSettingsStore()
650
651 const loading = ref(true)
552 -const error = ref('')
652 +const error = ref("")
653 +const showLogo = ref(true)
654 const agents = ref<Agent[]>([])
655 const selectedAgent = ref<Agent | null>(null)
656 const updatingAgent = ref<string | null>(null)
657
658 // Filters
659 const filters = ref({
559 - status: '',
560 - critical: '',
561 - os: '',
562 - search: ''
660 + status: "",
661 + critical: "",
662 + os: "",
663 + search: ""
664 })
665
666 // Pagination
@@ -567,216 +668,221 @@ const currentPage = ref(1)
668 const pageSize = ref(20)
669
670 const username = computed(() => {
570 - try {
571 - const user = JSON.parse(localStorage.getItem('customer-portal-user') || '{}')
572 - return user.username || 'User'
573 - } catch {
574 - return 'User'
575 - }
671 + try {
672 + const user = JSON.parse(localStorage.getItem("customer-portal-user") || "{}")
673 + return user.username || "User"
674 + } catch {
675 + return "User"
676 + }
677 })
678
679 +const portalTitle = computed(() => portalSettingsStore.portalTitle || "Customer Portal")
680 +const portalLogo = computed(() => portalSettingsStore.portalLogo)
681 +
682 const uniqueOperatingSystems = computed(() => {
579 - const osSet = new Set(agents.value.map(agent => agent.os))
580 - return Array.from(osSet).sort()
683 + const osSet = new Set(agents.value.map(agent => agent.os))
684 + return Array.from(osSet).sort()
685 })
686
687 const activeAgents = computed(() => {
584 - return agents.value.filter(agent => agent.wazuh_agent_status === 'active').length
688 + return agents.value.filter(agent => agent.wazuh_agent_status === "active").length
689 })
690
691 const criticalAgents = computed(() => {
588 - return agents.value.filter(agent => agent.critical_asset).length
692 + return agents.value.filter(agent => agent.critical_asset).length
693 })
694
695 const offlineAgents = computed(() => {
592 - return agents.value.filter(agent =>
593 - agent.wazuh_agent_status === 'disconnected' ||
594 - agent.wazuh_agent_status === 'never_connected'
595 - ).length
696 + return agents.value.filter(
697 + agent => agent.wazuh_agent_status === "disconnected" || agent.wazuh_agent_status === "never_connected"
698 + ).length
699 })
700
701 const filteredAgents = computed(() => {
599 - let filtered = agents.value
600 -
601 - if (filters.value.status) {
602 - filtered = filtered.filter(agent => agent.wazuh_agent_status === filters.value.status)
603 - }
604 -
605 - if (filters.value.critical) {
606 - const isCritical = filters.value.critical === 'true'
607 - filtered = filtered.filter(agent => agent.critical_asset === isCritical)
608 - }
609 -
610 - if (filters.value.os) {
611 - filtered = filtered.filter(agent => agent.os === filters.value.os)
612 - }
613 -
614 - if (filters.value.search) {
615 - const searchTerm = filters.value.search.toLowerCase()
616 - filtered = filtered.filter(agent =>
617 - agent.hostname.toLowerCase().includes(searchTerm) ||
618 - agent.ip_address.toLowerCase().includes(searchTerm) ||
619 - agent.agent_id.toLowerCase().includes(searchTerm)
620 - )
621 - }
622 -
623 - return filtered
702 + let filtered = agents.value
703 +
704 + if (filters.value.status) {
705 + filtered = filtered.filter(agent => agent.wazuh_agent_status === filters.value.status)
706 + }
707 +
708 + if (filters.value.critical) {
709 + const isCritical = filters.value.critical === "true"
710 + filtered = filtered.filter(agent => agent.critical_asset === isCritical)
711 + }
712 +
713 + if (filters.value.os) {
714 + filtered = filtered.filter(agent => agent.os === filters.value.os)
715 + }
716 +
717 + if (filters.value.search) {
718 + const searchTerm = filters.value.search.toLowerCase()
719 + filtered = filtered.filter(
720 + agent =>
721 + agent.hostname.toLowerCase().includes(searchTerm) ||
722 + agent.ip_address.toLowerCase().includes(searchTerm) ||
723 + agent.agent_id.toLowerCase().includes(searchTerm)
724 + )
725 + }
726 +
727 + return filtered
728 })
729
730 const totalPages = computed(() => {
627 - return Math.ceil(filteredAgents.value.length / pageSize.value)
731 + return Math.ceil(filteredAgents.value.length / pageSize.value)
732 })
733
734 const paginatedAgents = computed(() => {
631 - const start = (currentPage.value - 1) * pageSize.value
632 - const end = start + pageSize.value
633 - return filteredAgents.value.slice(start, end)
735 + const start = (currentPage.value - 1) * pageSize.value
736 + const end = start + pageSize.value
737 + return filteredAgents.value.slice(start, end)
738 })
739
740 const visiblePages = computed(() => {
637 - const total = totalPages.value
638 - const current = currentPage.value
639 - const delta = 2
640 -
641 - const range = []
642 - const rangeWithDots = []
643 -
644 - for (let i = Math.max(2, current - delta); i <= Math.min(total - 1, current + delta); i++) {
645 - range.push(i)
646 - }
647 -
648 - if (current - delta > 2) {
649 - rangeWithDots.push(1, '...')
650 - } else {
651 - rangeWithDots.push(1)
652 - }
653 -
654 - rangeWithDots.push(...range)
655 -
656 - if (current + delta < total - 1) {
657 - rangeWithDots.push('...', total)
658 - } else {
659 - rangeWithDots.push(total)
660 - }
661 -
662 - return rangeWithDots.filter((page, index, arr) => arr.indexOf(page) === index && page !== current - 1 && page !== current + 1).slice(0, 7)
741 + const total = totalPages.value
742 + const current = currentPage.value
743 + const delta = 2
744 +
745 + const range = []
746 + const rangeWithDots = []
747 +
748 + for (let i = Math.max(2, current - delta); i <= Math.min(total - 1, current + delta); i++) {
749 + range.push(i)
750 + }
751 +
752 + if (current - delta > 2) {
753 + rangeWithDots.push(1, "...")
754 + } else {
755 + rangeWithDots.push(1)
756 + }
757 +
758 + rangeWithDots.push(...range)
759 +
760 + if (current + delta < total - 1) {
761 + rangeWithDots.push("...", total)
762 + } else {
763 + rangeWithDots.push(total)
764 + }
765 +
766 + return rangeWithDots
767 + .filter((page, index, arr) => arr.indexOf(page) === index && page !== current - 1 && page !== current + 1)
768 + .slice(0, 7)
769 })
770
771 const formatTimeAgo = (dateString: string) => {
666 - if (!dateString) return 'Never'
667 -
668 - try {
669 - const date = new Date(dateString)
670 - const now = new Date()
671 - const diffInMs = now.getTime() - date.getTime()
672 - const diffInMinutes = diffInMs / (1000 * 60)
673 - const diffInHours = diffInMs / (1000 * 60 * 60)
674 - const diffInDays = diffInMs / (1000 * 60 * 60 * 24)
675 -
676 - if (diffInMinutes < 60) {
677 - return `${Math.floor(diffInMinutes)} minutes ago`
678 - } else if (diffInHours < 24) {
679 - return `${Math.floor(diffInHours)} hours ago`
680 - } else if (diffInDays < 30) {
681 - return `${Math.floor(diffInDays)} days ago`
682 - } else {
683 - return date.toLocaleDateString()
684 - }
685 - } catch {
686 - return 'Invalid date'
687 - }
772 + if (!dateString) return "Never"
773 +
774 + try {
775 + const date = new Date(dateString)
776 + const now = new Date()
777 + const diffInMs = now.getTime() - date.getTime()
778 + const diffInMinutes = diffInMs / (1000 * 60)
779 + const diffInHours = diffInMs / (1000 * 60 * 60)
780 + const diffInDays = diffInMs / (1000 * 60 * 60 * 24)
781 +
782 + if (diffInMinutes < 60) {
783 + return `${Math.floor(diffInMinutes)} minutes ago`
784 + } else if (diffInHours < 24) {
785 + return `${Math.floor(diffInHours)} hours ago`
786 + } else if (diffInDays < 30) {
787 + return `${Math.floor(diffInDays)} days ago`
788 + } else {
789 + return date.toLocaleDateString()
790 + }
791 + } catch {
792 + return "Invalid date"
793 + }
794 }
795
796 const formatDateTime = (dateString: string) => {
691 - if (!dateString) return 'N/A'
692 -
693 - try {
694 - const date = new Date(dateString)
695 - return date.toLocaleString()
696 - } catch {
697 - return 'Invalid date'
698 - }
797 + if (!dateString) return "N/A"
798 +
799 + try {
800 + const date = new Date(dateString)
801 + return date.toLocaleString()
802 + } catch {
803 + return "Invalid date"
804 + }
805 }
806
807 const loadAgents = async () => {
702 - loading.value = true
703 - error.value = ''
704 -
705 - try {
706 - const response = await AgentsAPI.getAgents()
707 - agents.value = response.agents || []
708 - } catch (err: any) {
709 - console.error('Failed to load agents:', err)
710 - error.value = err.response?.data?.detail || err.message || 'Failed to load agents'
711 - agents.value = []
712 - } finally {
713 - loading.value = false
714 - }
808 + loading.value = true
809 + error.value = ""
810 +
811 + try {
812 + const response = await AgentsAPI.getAgents()
813 + agents.value = response.agents || []
814 + } catch (err: any) {
815 + console.error("Failed to load agents:", err)
816 + error.value = err.response?.data?.detail || err.message || "Failed to load agents"
817 + agents.value = []
818 + } finally {
819 + loading.value = false
820 + }
821 }
822
823 const markAsCritical = async (agent: Agent) => {
718 - updatingAgent.value = agent.agent_id
719 - try {
720 - await AgentsAPI.markAgentAsCritical(agent.agent_id)
721 - agent.critical_asset = true
722 - } catch (err: any) {
723 - console.error('Failed to mark agent as critical:', err)
724 - error.value = err.response?.data?.detail || err.message || 'Failed to update agent'
725 - } finally {
726 - updatingAgent.value = null
727 - }
824 + updatingAgent.value = agent.agent_id
825 + try {
826 + await AgentsAPI.markAgentAsCritical(agent.agent_id)
827 + agent.critical_asset = true
828 + } catch (err: any) {
829 + console.error("Failed to mark agent as critical:", err)
830 + error.value = err.response?.data?.detail || err.message || "Failed to update agent"
831 + } finally {
832 + updatingAgent.value = null
833 + }
834 }
835
836 const markAsNotCritical = async (agent: Agent) => {
731 - updatingAgent.value = agent.agent_id
732 - try {
733 - await AgentsAPI.markAgentAsNotCritical(agent.agent_id)
734 - agent.critical_asset = false
735 - } catch (err: any) {
736 - console.error('Failed to mark agent as not critical:', err)
737 - error.value = err.response?.data?.detail || err.message || 'Failed to update agent'
738 - } finally {
739 - updatingAgent.value = null
740 - }
837 + updatingAgent.value = agent.agent_id
838 + try {
839 + await AgentsAPI.markAgentAsNotCritical(agent.agent_id)
840 + agent.critical_asset = false
841 + } catch (err: any) {
842 + console.error("Failed to mark agent as not critical:", err)
843 + error.value = err.response?.data?.detail || err.message || "Failed to update agent"
844 + } finally {
845 + updatingAgent.value = null
846 + }
847 }
848
849 const viewAgentDetails = (agent: Agent) => {
744 - selectedAgent.value = agent
850 + selectedAgent.value = agent
851 }
852
853 const closeAgentDetails = () => {
748 - selectedAgent.value = null
854 + selectedAgent.value = null
855 }
856
857 const clearFilters = () => {
752 - filters.value = {
753 - status: '',
754 - critical: '',
755 - os: '',
756 - search: ''
757 - }
758 - currentPage.value = 1
858 + filters.value = {
859 + status: "",
860 + critical: "",
861 + os: "",
862 + search: ""
863 + }
864 + currentPage.value = 1
865 }
866
867 const nextPage = () => {
762 - if (currentPage.value < totalPages.value) {
763 - currentPage.value++
764 - }
868 + if (currentPage.value < totalPages.value) {
869 + currentPage.value++
870 + }
871 }
872
873 const previousPage = () => {
768 - if (currentPage.value > 1) {
769 - currentPage.value--
770 - }
874 + if (currentPage.value > 1) {
875 + currentPage.value--
876 + }
877 }
878
879 const logout = () => {
774 - localStorage.removeItem('customer-portal-auth-token')
775 - localStorage.removeItem('customer-portal-user')
776 - router.push('/login')
880 + localStorage.removeItem("customer-portal-auth-token")
881 + localStorage.removeItem("customer-portal-user")
882 + router.push("/login")
883 }
884
885 onMounted(() => {
780 - loadAgents()
886 + loadAgents()
887 })
888 </script>
customer_portal/src/views/AlertsPage.vue
+779 -663
@@ -1,584 +1,698 @@
1 <template>
2 - <div class="min-h-screen bg-gray-50">
3 - <!-- Header -->
4 - <header class="bg-white shadow-sm border-b">
5 - <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
6 - <div class="flex justify-between h-16">
7 - <div class="flex items-center">
8 - <img
9 - class="h-8 w-auto mr-3"
10 - src="/logo.svg"
11 - alt="SOCFortress Logo"
12 - />
13 - <h1 class="text-xl font-semibold text-gray-900">Customer Portal</h1>
14 - <nav class="ml-8 flex space-x-8">
15 - <router-link
16 - to="/"
17 - class="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
18 - >
19 - Overview
20 - </router-link>
21 - <router-link
22 - to="/alerts"
23 - class="text-indigo-600 border-b-2 border-indigo-600 px-3 py-2 rounded-md text-sm font-medium"
24 - >
25 - Alerts
26 - </router-link>
27 - <router-link
28 - to="/cases"
29 - class="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
30 - >
31 - Cases
32 - </router-link>
33 - <router-link
34 - to="/agents"
35 - class="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
36 - >
37 - Agents
38 - </router-link>
39 - </nav>
40 - </div>
41 - <div class="flex items-center space-x-4">
42 - <button
43 - @click="refreshAlerts"
44 - :disabled="loading"
45 - class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
46 - >
47 - <svg class="w-4 h-4 mr-2" :class="{ 'animate-spin': loading }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
48 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
49 - </svg>
50 - Refresh
51 - </button>
52 - </div>
53 - </div>
54 - </div>
55 - </header>
56 -
57 - <!-- Stats Cards -->
58 - <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
59 - <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
60 - <div class="bg-white overflow-hidden shadow rounded-lg">
61 - <div class="p-5">
62 - <div class="flex items-center">
63 - <div class="flex-shrink-0">
64 - <div class="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
65 - <svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
66 - <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
67 - </svg>
68 - </div>
69 - </div>
70 - <div class="ml-5 w-0 flex-1">
71 - <dl>
72 - <dt class="text-sm font-medium text-gray-500 truncate">Total Alerts</dt>
73 - <dd class="text-lg font-medium text-gray-900">{{ stats.total }}</dd>
74 - </dl>
75 - </div>
76 - </div>
77 - </div>
78 - </div>
79 -
80 - <div class="bg-white overflow-hidden shadow rounded-lg">
81 - <div class="p-5">
82 - <div class="flex items-center">
83 - <div class="flex-shrink-0">
84 - <div class="w-8 h-8 bg-red-500 rounded-md flex items-center justify-center">
85 - <svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
86 - <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
87 - </svg>
88 - </div>
89 - </div>
90 - <div class="ml-5 w-0 flex-1">
91 - <dl>
92 - <dt class="text-sm font-medium text-gray-500 truncate">Open</dt>
93 - <dd class="text-lg font-medium text-gray-900">{{ stats.open }}</dd>
94 - </dl>
95 - </div>
96 - </div>
97 - </div>
98 - </div>
99 -
100 - <div class="bg-white overflow-hidden shadow rounded-lg">
101 - <div class="p-5">
102 - <div class="flex items-center">
103 - <div class="flex-shrink-0">
104 - <div class="w-8 h-8 bg-yellow-500 rounded-md flex items-center justify-center">
105 - <svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
106 - <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"></path>
107 - </svg>
108 - </div>
109 - </div>
110 - <div class="ml-5 w-0 flex-1">
111 - <dl>
112 - <dt class="text-sm font-medium text-gray-500 truncate">In Progress</dt>
113 - <dd class="text-lg font-medium text-gray-900">{{ stats.in_progress }}</dd>
114 - </dl>
115 - </div>
116 - </div>
117 - </div>
118 - </div>
119 -
120 - <div class="bg-white overflow-hidden shadow rounded-lg">
121 - <div class="p-5">
122 - <div class="flex items-center">
123 - <div class="flex-shrink-0">
124 - <div class="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
125 - <svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
126 - <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
127 - </svg>
128 - </div>
129 - </div>
130 - <div class="ml-5 w-0 flex-1">
131 - <dl>
132 - <dt class="text-sm font-medium text-gray-500 truncate">Closed</dt>
133 - <dd class="text-lg font-medium text-gray-900">{{ stats.closed }}</dd>
134 - </dl>
135 - </div>
136 - </div>
137 - </div>
138 - </div>
139 - </div>
140 -
141 - <!-- Filters -->
142 - <div class="bg-white shadow rounded-lg mb-6">
143 - <div class="px-4 py-5 sm:p-6">
144 - <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
145 - <div>
146 - <label for="status-filter" class="block text-sm font-medium text-gray-700">Status</label>
147 - <select
148 - id="status-filter"
149 - v-model="filters.status"
150 - @change="applyFilters"
151 - class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
152 - >
153 - <option value="">All Statuses</option>
154 - <option value="open">Open</option>
155 - <option value="in_progress">In Progress</option>
156 - <option value="closed">Closed</option>
157 - </select>
158 - </div>
159 - <div>
160 - <label for="source-filter" class="block text-sm font-medium text-gray-700">Source</label>
161 - <select
162 - id="source-filter"
163 - v-model="filters.source"
164 - @change="applyFilters"
165 - class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
166 - >
167 - <option value="">All Sources</option>
168 - <option v-for="source in availableSources" :key="source" :value="source">{{ source }}</option>
169 - </select>
170 - </div>
171 - <div>
172 - <label for="asset-filter" class="block text-sm font-medium text-gray-700">Asset</label>
173 - <select
174 - id="asset-filter"
175 - v-model="filters.asset"
176 - @change="applyFilters"
177 - class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
178 - >
179 - <option value="">All Assets</option>
180 - <option v-for="asset in availableAssets" :key="asset" :value="asset">{{ asset }}</option>
181 - </select>
182 - </div>
183 - </div>
184 - </div>
185 - </div>
186 -
187 - <!-- Alerts List -->
188 - <div class="bg-white shadow overflow-hidden sm:rounded-md">
189 - <div v-if="loading" class="px-4 py-5 sm:p-6 text-center">
190 - <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600 mx-auto"></div>
191 - <p class="mt-2 text-sm text-gray-500">Loading alerts...</p>
192 - </div>
193 -
194 - <div v-else-if="error" class="px-4 py-5 sm:p-6 text-center">
195 - <div class="text-red-500 mb-2">
196 - <svg class="w-8 h-8 mx-auto" fill="currentColor" viewBox="0 0 20 20">
197 - <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
198 - </svg>
199 - </div>
200 - <p class="text-sm text-red-600">{{ error }}</p>
201 - <button
202 - @click="loadAlerts"
203 - class="mt-2 inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
204 - >
205 - Try Again
206 - </button>
207 - </div>
208 -
209 - <ul v-else-if="alerts.length > 0" role="list" class="divide-y divide-gray-200">
210 - <li v-for="alert in alerts" :key="alert.id" class="px-4 py-4 sm:px-6 hover:bg-gray-50">
211 - <div class="flex items-center justify-between">
212 - <div class="flex items-center">
213 - <div class="flex-shrink-0">
214 - <span
215 - class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
216 - :class="{
217 - 'bg-red-100 text-red-800': alert.status === 'OPEN',
218 - 'bg-yellow-100 text-yellow-800': alert.status === 'IN_PROGRESS',
219 - 'bg-green-100 text-green-800': alert.status === 'CLOSED'
220 - }"
221 - >
222 - {{ alert.status.replace('_', ' ').toUpperCase() }}
223 - </span>
224 - </div>
225 - <div class="ml-4">
226 - <div class="text-sm font-medium text-gray-900">
227 - {{ alert.alert_name }}
228 - </div>
229 - <div class="text-sm text-gray-500">
230 - <span v-if="alert.assets.length > 0">
231 - Asset: {{ alert.assets[0].asset_name }}
232 - </span>
233 - <span v-else-if="alert.asset_name">
234 - Asset: {{ alert.asset_name }}
235 - </span>
236 - | Source: {{ alert.source }}
237 - </div>
238 - <div class="text-xs text-gray-400">
239 - {{ formatDate(alert.alert_creation_time) }}
240 - </div>
241 - </div>
242 - </div>
243 - <div class="flex items-center space-x-2">
244 - <select
245 - :value="alert.status"
246 - @change="updateAlertStatus(alert.id, ($event.target as HTMLSelectElement).value)"
247 - class="text-sm border-gray-300 rounded-md focus:ring-indigo-500 focus:border-indigo-500"
248 - :disabled="updatingStatus === alert.id"
249 - >
250 - <option value="OPEN">Open</option>
251 - <option value="IN_PROGRESS">In Progress</option>
252 - <option value="CLOSED">Closed</option>
253 - </select>
254 - <button
255 - @click="viewAlert(alert)"
256 - class="inline-flex items-center px-3 py-1 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
257 - >
258 - View Details
259 - </button>
260 - </div>
261 - </div>
262 - <div v-if="alert.alert_description" class="mt-2 text-sm text-gray-600">
263 - {{ alert.alert_description }}
264 - </div>
265 - </li>
266 - </ul>
267 -
268 - <div v-else class="px-4 py-5 sm:p-6 text-center">
269 - <svg class="w-12 h-12 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
270 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
271 - </svg>
272 - <h3 class="mt-2 text-sm font-medium text-gray-900">No alerts found</h3>
273 - <p class="mt-1 text-sm text-gray-500">No security alerts match your current filters.</p>
274 - </div>
275 - </div>
276 -
277 - <!-- Pagination -->
278 - <div v-if="alerts.length > 0" class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6 mt-6">
279 - <div class="flex-1 flex justify-between sm:hidden">
280 - <button
281 - @click="previousPage"
282 - :disabled="currentPage <= 1"
283 - class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
284 - >
285 - Previous
286 - </button>
287 - <button
288 - @click="nextPage"
289 - :disabled="currentPage >= totalPages"
290 - class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
291 - >
292 - Next
293 - </button>
294 - </div>
295 - <div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
296 - <div>
297 - <p class="text-sm text-gray-700">
298 - Showing <span class="font-medium">{{ (currentPage - 1) * pageSize + 1 }}</span>
299 - to <span class="font-medium">{{ Math.min(currentPage * pageSize, stats.total) }}</span>
300 - of <span class="font-medium">{{ stats.total }}</span> results
301 - </p>
302 - </div>
303 - <div>
304 - <nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
305 - <button
306 - @click="previousPage"
307 - :disabled="currentPage <= 1"
308 - class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
309 - >
310 - Previous
311 - </button>
312 - <button
313 - @click="nextPage"
314 - :disabled="currentPage >= totalPages"
315 - class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
316 - >
317 - Next
318 - </button>
319 - </nav>
320 - </div>
321 - </div>
322 - </div>
323 - </div>
324 -
325 - <!-- Alert Details Modal -->
326 - <div v-if="selectedAlert" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50" @click="closeModal">
327 - <div class="relative top-10 mx-auto p-5 border w-11/12 md:w-4/5 lg:w-3/4 shadow-lg rounded-md bg-white max-h-screen overflow-y-auto" @click.stop>
328 - <div class="flex justify-between items-center mb-4">
329 - <h3 class="text-lg font-medium text-gray-900">Alert Details</h3>
330 - <button @click="closeModal" class="text-gray-400 hover:text-gray-600">
331 - <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
332 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
333 - </svg>
334 - </button>
335 - </div>
336 -
337 - <div class="space-y-6">
338 - <!-- Basic Alert Information -->
339 - <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
340 - <div>
341 - <label class="block text-sm font-medium text-gray-700">Alert Name</label>
342 - <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.alert_name }}</p>
343 - </div>
344 - <div>
345 - <label class="block text-sm font-medium text-gray-700">Status</label>
346 - <span
347 - class="mt-1 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
348 - :class="{
349 - 'bg-red-100 text-red-800': selectedAlert.status === 'OPEN',
350 - 'bg-yellow-100 text-yellow-800': selectedAlert.status === 'IN_PROGRESS',
351 - 'bg-green-100 text-green-800': selectedAlert.status === 'CLOSED'
352 - }"
353 - >
354 - {{ selectedAlert.status.replace('_', ' ').toUpperCase() }}
355 - </span>
356 - </div>
357 - <div>
358 - <label class="block text-sm font-medium text-gray-700">Source</label>
359 - <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.source }}</p>
360 - </div>
361 - <div>
362 - <label class="block text-sm font-medium text-gray-700">Customer</label>
363 - <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.customer_code }}</p>
364 - </div>
365 - <div>
366 - <label class="block text-sm font-medium text-gray-700">Created</label>
367 - <p class="mt-1 text-sm text-gray-900">{{ formatDate(selectedAlert.alert_creation_time) }}</p>
368 - </div>
369 - <div v-if="selectedAlert.assigned_to">
370 - <label class="block text-sm font-medium text-gray-700">Assigned To</label>
371 - <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.assigned_to }}</p>
372 - </div>
373 - </div>
374 -
375 - <div v-if="selectedAlert.alert_description">
376 - <label class="block text-sm font-medium text-gray-700">Description</label>
377 - <p class="mt-1 text-sm text-gray-900 whitespace-pre-wrap">{{ selectedAlert.alert_description }}</p>
378 - </div>
379 -
380 - <!-- Assets Section -->
381 - <div v-if="selectedAlert.assets && selectedAlert.assets.length > 0">
382 - <label class="block text-sm font-medium text-gray-700 mb-2">Assets</label>
383 - <div class="bg-gray-50 rounded-lg p-4">
384 - <div v-for="asset in selectedAlert.assets" :key="asset.id" class="border-b border-gray-200 pb-3 mb-3 last:border-b-0 last:mb-0">
385 - <div class="grid grid-cols-1 md:grid-cols-3 gap-2 text-sm">
386 - <div>
387 - <span class="font-medium">Asset Name:</span> {{ asset.asset_name }}
388 - </div>
389 - <div>
390 - <span class="font-medium">Agent ID:</span> {{ asset.agent_id }}
391 - </div>
392 - <div v-if="asset.velociraptor_id">
393 - <span class="font-medium">Velociraptor ID:</span> {{ asset.velociraptor_id }}
394 - </div>
395 - <div>
396 - <span class="font-medium">Index:</span> {{ asset.index_name }}
397 - </div>
398 - <div>
399 - <span class="font-medium">Index ID:</span> {{ asset.index_id.substring(0, 20) }}...
400 - </div>
401 - </div>
402 - </div>
403 - </div>
404 - </div>
405 -
406 - <!-- Fallback for legacy asset_name -->
407 - <div v-else-if="selectedAlert.asset_name">
408 - <label class="block text-sm font-medium text-gray-700">Asset</label>
409 - <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.asset_name }}</p>
410 - </div>
411 -
412 - <!-- Tags Section -->
413 - <div v-if="selectedAlert.tags && selectedAlert.tags.length > 0">
414 - <label class="block text-sm font-medium text-gray-700">Tags</label>
415 - <div class="mt-1 flex flex-wrap gap-2">
416 - <span
417 - v-for="tag in selectedAlert.tags"
418 - :key="tag.id"
419 - class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
420 - >
421 - {{ tag.tag }}
422 - </span>
423 - </div>
424 - </div>
425 -
426 - <!-- Legacy Tags (for backward compatibility) -->
427 - <div v-else-if="selectedAlert.tag && selectedAlert.tag.length > 0">
428 - <label class="block text-sm font-medium text-gray-700">Tags</label>
429 - <div class="mt-1 flex flex-wrap gap-2">
430 - <span
431 - v-for="tag in selectedAlert.tag"
432 - :key="tag"
433 - class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
434 - >
435 - {{ tag }}
436 - </span>
437 - </div>
438 - </div>
439 -
440 - <!-- Linked Cases Section -->
441 - <div v-if="selectedAlert.linked_cases && selectedAlert.linked_cases.length > 0">
442 - <label class="block text-sm font-medium text-gray-700 mb-2">Linked Cases</label>
443 - <div class="bg-gray-50 rounded-lg p-4">
444 - <div v-for="linkedCase in selectedAlert.linked_cases" :key="linkedCase.id" class="border-b border-gray-200 pb-3 mb-3 last:border-b-0 last:mb-0">
445 - <div class="flex justify-between items-start">
446 - <div class="flex-1">
447 - <h4 class="text-sm font-medium text-gray-900">{{ linkedCase.case_name }}</h4>
448 - <p class="text-xs text-gray-600 mt-1">{{ linkedCase.case_description }}</p>
449 - <div class="flex items-center space-x-4 mt-2 text-xs text-gray-500">
450 - <span>Case #{{ linkedCase.id }}</span>
451 - <span>Created: {{ formatDate(linkedCase.case_creation_time) }}</span>
452 - <span v-if="linkedCase.assigned_to">Assigned to: {{ linkedCase.assigned_to }}</span>
453 - </div>
454 - </div>
455 - <span
456 - class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
457 - :class="{
458 - 'bg-red-100 text-red-800': linkedCase.case_status === 'OPEN',
459 - 'bg-yellow-100 text-yellow-800': linkedCase.case_status === 'IN_PROGRESS',
460 - 'bg-green-100 text-green-800': linkedCase.case_status === 'CLOSED'
461 - }"
462 - >
463 - {{ linkedCase.case_status.replace('_', ' ').toUpperCase() }}
464 - </span>
465 - </div>
466 - </div>
467 - </div>
468 - </div>
469 -
470 - <!-- Legacy Case IDs (for backward compatibility) -->
471 - <div v-else-if="selectedAlert.case_ids && selectedAlert.case_ids.length > 0">
472 - <label class="block text-sm font-medium text-gray-700">Linked Cases</label>
473 - <div class="mt-1 flex flex-wrap gap-2">
474 - <span
475 - v-for="caseId in selectedAlert.case_ids"
476 - :key="caseId"
477 - class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800"
478 - >
479 - Case #{{ caseId }}
480 - </span>
481 - </div>
482 - </div>
483 -
484 - <!-- IoCs Section -->
485 - <div v-if="selectedAlert.iocs && selectedAlert.iocs.length > 0">
486 - <label class="block text-sm font-medium text-gray-700 mb-2">Indicators of Compromise (IoCs)</label>
487 - <div class="bg-gray-50 rounded-lg p-4">
488 - <div v-for="ioc in selectedAlert.iocs" :key="ioc.id" class="border-b border-gray-200 pb-3 mb-3 last:border-b-0 last:mb-0">
489 - <div class="grid grid-cols-1 md:grid-cols-3 gap-2 text-sm">
490 - <div>
491 - <span class="font-medium">Value:</span>
492 - <code class="bg-gray-100 px-1 rounded text-xs">{{ ioc.ioc_value }}</code>
493 - </div>
494 - <div>
495 - <span class="font-medium">Type:</span> {{ ioc.ioc_type }}
496 - </div>
497 - <div>
498 - <span class="font-medium">Description:</span> {{ ioc.ioc_description }}
499 - </div>
500 - </div>
501 - </div>
502 - </div>
503 - </div>
504 -
505 - <!-- Comments Section -->
506 - <div>
507 - <label class="block text-sm font-medium text-gray-700 mb-2">
508 - Comments
509 - <span v-if="selectedAlert.comments && selectedAlert.comments.length > 0" class="text-gray-500 font-normal">
510 - ({{ selectedAlert.comments.length }})
511 - </span>
512 - </label>
513 -
514 - <!-- Existing Comments -->
515 - <div v-if="selectedAlert.comments && selectedAlert.comments.length > 0" class="bg-gray-50 rounded-lg p-4 max-h-64 overflow-y-auto mb-4">
516 - <div v-for="comment in selectedAlert.comments" :key="comment.id" class="border-b border-gray-200 pb-3 mb-3 last:border-b-0 last:mb-0">
517 - <div class="flex justify-between items-start mb-2">
518 - <span class="text-sm font-medium text-gray-900">{{ comment.user_name }}</span>
519 - <span class="text-xs text-gray-500">{{ formatDate(comment.created_at) }}</span>
520 - </div>
521 - <p class="text-sm text-gray-700 whitespace-pre-wrap">{{ comment.comment }}</p>
522 - </div>
523 - </div>
524 -
525 - <!-- No Comments Message -->
526 - <div v-else class="bg-gray-50 rounded-lg p-4 mb-4 text-center">
527 - <p class="text-sm text-gray-500">No comments yet</p>
528 - </div>
529 -
530 - <!-- Add Comment Form -->
531 - <div class="border rounded-lg p-4 bg-white">
532 - <label class="block text-sm font-medium text-gray-700 mb-2">Add Comment</label>
533 - <textarea
534 - v-model="newComment"
535 - placeholder="Enter your comment..."
536 - rows="3"
537 - class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
538 - ></textarea>
539 - <div class="flex justify-end mt-3">
540 - <button
541 - @click="addComment"
542 - :disabled="!newComment.trim() || isAddingComment"
543 - class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
544 - >
545 - <svg v-if="isAddingComment" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
546 - <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
547 - <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
548 - </svg>
549 - {{ isAddingComment ? 'Adding...' : 'Add Comment' }}
550 - </button>
551 - </div>
552 - </div>
553 - </div>
554 - </div>
555 - </div>
556 - </div>
557 - </div>
2 + <div class="min-h-screen bg-gray-50">
3 + <!-- Header -->
4 + <header class="border-b bg-white shadow-sm">
5 + <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
6 + <div class="flex h-16 justify-between">
7 + <div class="flex items-center">
8 + <div class="mr-3 min-h-8">
9 + <img
10 + v-if="portalLogo && showLogo"
11 + class="h-8 w-auto"
12 + :src="portalLogo"
13 + :alt="portalTitle"
14 + @error="showLogo = false"
15 + />
16 + </div>
17 + <h1 class="text-xl font-semibold text-gray-900">{{ portalTitle }}</h1>
18 + <nav class="ml-8 flex space-x-8">
19 + <router-link
20 + to="/"
21 + class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-900"
22 + >
23 + Overview
24 + </router-link>
25 + <router-link
26 + to="/alerts"
27 + class="rounded-md border-b-2 border-indigo-600 px-3 py-2 text-sm font-medium text-indigo-600"
28 + >
29 + Alerts
30 + </router-link>
31 + <router-link
32 + to="/cases"
33 + class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-900"
34 + >
35 + Cases
36 + </router-link>
37 + <router-link
38 + to="/agents"
39 + class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-900"
40 + >
41 + Agents
42 + </router-link>
43 + </nav>
44 + </div>
45 + <div class="flex items-center space-x-4">
46 + <button
47 + @click="refreshAlerts"
48 + :disabled="loading"
49 + class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm leading-4 font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:opacity-50"
50 + >
51 + <svg
52 + class="mr-2 h-4 w-4"
53 + :class="{ 'animate-spin': loading }"
54 + fill="none"
55 + stroke="currentColor"
56 + viewBox="0 0 24 24"
57 + >
58 + <path
59 + stroke-linecap="round"
60 + stroke-linejoin="round"
61 + stroke-width="2"
62 + d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
63 + ></path>
64 + </svg>
65 + Refresh
66 + </button>
67 + </div>
68 + </div>
69 + </div>
70 + </header>
71 +
72 + <!-- Stats Cards -->
73 + <div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
74 + <div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-4">
75 + <div class="overflow-hidden rounded-lg bg-white shadow">
76 + <div class="p-5">
77 + <div class="flex items-center">
78 + <div class="flex-shrink-0">
79 + <div class="flex h-8 w-8 items-center justify-center rounded-md bg-blue-500">
80 + <svg class="h-5 w-5 text-white" fill="currentColor" viewBox="0 0 20 20">
81 + <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
82 + </svg>
83 + </div>
84 + </div>
85 + <div class="ml-5 w-0 flex-1">
86 + <dl>
87 + <dt class="truncate text-sm font-medium text-gray-500">Total Alerts</dt>
88 + <dd class="text-lg font-medium text-gray-900">{{ stats.total }}</dd>
89 + </dl>
90 + </div>
91 + </div>
92 + </div>
93 + </div>
94 +
95 + <div class="overflow-hidden rounded-lg bg-white shadow">
96 + <div class="p-5">
97 + <div class="flex items-center">
98 + <div class="flex-shrink-0">
99 + <div class="flex h-8 w-8 items-center justify-center rounded-md bg-red-500">
100 + <svg class="h-5 w-5 text-white" fill="currentColor" viewBox="0 0 20 20">
101 + <path
102 + fill-rule="evenodd"
103 + d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
104 + clip-rule="evenodd"
105 + ></path>
106 + </svg>
107 + </div>
108 + </div>
109 + <div class="ml-5 w-0 flex-1">
110 + <dl>
111 + <dt class="truncate text-sm font-medium text-gray-500">Open</dt>
112 + <dd class="text-lg font-medium text-gray-900">{{ stats.open }}</dd>
113 + </dl>
114 + </div>
115 + </div>
116 + </div>
117 + </div>
118 +
119 + <div class="overflow-hidden rounded-lg bg-white shadow">
120 + <div class="p-5">
121 + <div class="flex items-center">
122 + <div class="flex-shrink-0">
123 + <div class="flex h-8 w-8 items-center justify-center rounded-md bg-yellow-500">
124 + <svg class="h-5 w-5 text-white" fill="currentColor" viewBox="0 0 20 20">
125 + <path
126 + fill-rule="evenodd"
127 + d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"
128 + clip-rule="evenodd"
129 + ></path>
130 + </svg>
131 + </div>
132 + </div>
133 + <div class="ml-5 w-0 flex-1">
134 + <dl>
135 + <dt class="truncate text-sm font-medium text-gray-500">In Progress</dt>
136 + <dd class="text-lg font-medium text-gray-900">{{ stats.in_progress }}</dd>
137 + </dl>
138 + </div>
139 + </div>
140 + </div>
141 + </div>
142 +
143 + <div class="overflow-hidden rounded-lg bg-white shadow">
144 + <div class="p-5">
145 + <div class="flex items-center">
146 + <div class="flex-shrink-0">
147 + <div class="flex h-8 w-8 items-center justify-center rounded-md bg-green-500">
148 + <svg class="h-5 w-5 text-white" fill="currentColor" viewBox="0 0 20 20">
149 + <path
150 + fill-rule="evenodd"
151 + d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
152 + clip-rule="evenodd"
153 + ></path>
154 + </svg>
155 + </div>
156 + </div>
157 + <div class="ml-5 w-0 flex-1">
158 + <dl>
159 + <dt class="truncate text-sm font-medium text-gray-500">Closed</dt>
160 + <dd class="text-lg font-medium text-gray-900">{{ stats.closed }}</dd>
161 + </dl>
162 + </div>
163 + </div>
164 + </div>
165 + </div>
166 + </div>
167 +
168 + <!-- Filters -->
169 + <div class="mb-6 rounded-lg bg-white shadow">
170 + <div class="px-4 py-5 sm:p-6">
171 + <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
172 + <div>
173 + <label for="status-filter" class="block text-sm font-medium text-gray-700">Status</label>
174 + <select
175 + id="status-filter"
176 + v-model="filters.status"
177 + @change="applyFilters"
178 + class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
179 + >
180 + <option value="">All Statuses</option>
181 + <option value="open">Open</option>
182 + <option value="in_progress">In Progress</option>
183 + <option value="closed">Closed</option>
184 + </select>
185 + </div>
186 + <div>
187 + <label for="source-filter" class="block text-sm font-medium text-gray-700">Source</label>
188 + <select
189 + id="source-filter"
190 + v-model="filters.source"
191 + @change="applyFilters"
192 + class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
193 + >
194 + <option value="">All Sources</option>
195 + <option v-for="source in availableSources" :key="source" :value="source">
196 + {{ source }}
197 + </option>
198 + </select>
199 + </div>
200 + <div>
201 + <label for="asset-filter" class="block text-sm font-medium text-gray-700">Asset</label>
202 + <select
203 + id="asset-filter"
204 + v-model="filters.asset"
205 + @change="applyFilters"
206 + class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
207 + >
208 + <option value="">All Assets</option>
209 + <option v-for="asset in availableAssets" :key="asset" :value="asset">
210 + {{ asset }}
211 + </option>
212 + </select>
213 + </div>
214 + </div>
215 + </div>
216 + </div>
217 +
218 + <!-- Alerts List -->
219 + <div class="overflow-hidden bg-white shadow sm:rounded-md">
220 + <div v-if="loading" class="px-4 py-5 text-center sm:p-6">
221 + <div class="mx-auto h-8 w-8 animate-spin rounded-full border-b-2 border-indigo-600"></div>
222 + <p class="mt-2 text-sm text-gray-500">Loading alerts...</p>
223 + </div>
224 +
225 + <div v-else-if="error" class="px-4 py-5 text-center sm:p-6">
226 + <div class="mb-2 text-red-500">
227 + <svg class="mx-auto h-8 w-8" fill="currentColor" viewBox="0 0 20 20">
228 + <path
229 + fill-rule="evenodd"
230 + d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
231 + clip-rule="evenodd"
232 + ></path>
233 + </svg>
234 + </div>
235 + <p class="text-sm text-red-600">{{ error }}</p>
236 + <button
237 + @click="loadAlerts"
238 + class="mt-2 inline-flex items-center rounded-md border border-transparent bg-indigo-100 px-3 py-2 text-sm leading-4 font-medium text-indigo-700 hover:bg-indigo-200 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none"
239 + >
240 + Try Again
241 + </button>
242 + </div>
243 +
244 + <ul v-else-if="alerts.length > 0" role="list" class="divide-y divide-gray-200">
245 + <li v-for="alert in alerts" :key="alert.id" class="px-4 py-4 hover:bg-gray-50 sm:px-6">
246 + <div class="flex items-center justify-between">
247 + <div class="flex items-center">
248 + <div class="flex-shrink-0">
249 + <span
250 + class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
251 + :class="{
252 + 'bg-red-100 text-red-800': alert.status === 'OPEN',
253 + 'bg-yellow-100 text-yellow-800': alert.status === 'IN_PROGRESS',
254 + 'bg-green-100 text-green-800': alert.status === 'CLOSED'
255 + }"
256 + >
257 + {{ alert.status.replace("_", " ").toUpperCase() }}
258 + </span>
259 + </div>
260 + <div class="ml-4">
261 + <div class="text-sm font-medium text-gray-900">
262 + {{ alert.alert_name }}
263 + </div>
264 + <div class="text-sm text-gray-500">
265 + <span v-if="alert.assets.length > 0">
266 + Asset: {{ alert.assets[0].asset_name }}
267 + </span>
268 + <span v-else-if="alert.asset_name">Asset: {{ alert.asset_name }}</span>
269 + | Source: {{ alert.source }}
270 + </div>
271 + <div class="text-xs text-gray-400">
272 + {{ formatDate(alert.alert_creation_time) }}
273 + </div>
274 + </div>
275 + </div>
276 + <div class="flex items-center space-x-2">
277 + <select
278 + :value="alert.status"
279 + @change="updateAlertStatus(alert.id, ($event.target as HTMLSelectElement).value)"
280 + class="rounded-md border-gray-300 text-sm focus:border-indigo-500 focus:ring-indigo-500"
281 + :disabled="updatingStatus === alert.id"
282 + >
283 + <option value="OPEN">Open</option>
284 + <option value="IN_PROGRESS">In Progress</option>
285 + <option value="CLOSED">Closed</option>
286 + </select>
287 + <button
288 + @click="viewAlert(alert)"
289 + class="inline-flex items-center rounded border border-transparent bg-indigo-100 px-3 py-1 text-xs font-medium text-indigo-700 hover:bg-indigo-200 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none"
290 + >
291 + View Details
292 + </button>
293 + </div>
294 + </div>
295 + <div v-if="alert.alert_description" class="mt-2 text-sm text-gray-600">
296 + {{ alert.alert_description }}
297 + </div>
298 + </li>
299 + </ul>
300 +
301 + <div v-else class="px-4 py-5 text-center sm:p-6">
302 + <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
303 + <path
304 + stroke-linecap="round"
305 + stroke-linejoin="round"
306 + stroke-width="2"
307 + d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
308 + ></path>
309 + </svg>
310 + <h3 class="mt-2 text-sm font-medium text-gray-900">No alerts found</h3>
311 + <p class="mt-1 text-sm text-gray-500">No security alerts match your current filters.</p>
312 + </div>
313 + </div>
314 +
315 + <!-- Pagination -->
316 + <div
317 + v-if="alerts.length > 0"
318 + class="mt-6 flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6"
319 + >
320 + <div class="flex flex-1 justify-between sm:hidden">
321 + <button
322 + @click="previousPage"
323 + :disabled="currentPage <= 1"
324 + class="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
325 + >
326 + Previous
327 + </button>
328 + <button
329 + @click="nextPage"
330 + :disabled="currentPage >= totalPages"
331 + class="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
332 + >
333 + Next
334 + </button>
335 + </div>
336 + <div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
337 + <div>
338 + <p class="text-sm text-gray-700">
339 + Showing
340 + <span class="font-medium">{{ (currentPage - 1) * pageSize + 1 }}</span>
341 + to
342 + <span class="font-medium">{{ Math.min(currentPage * pageSize, stats.total) }}</span>
343 + of
344 + <span class="font-medium">{{ stats.total }}</span>
345 + results
346 + </p>
347 + </div>
348 + <div>
349 + <nav class="relative z-0 inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
350 + <button
351 + @click="previousPage"
352 + :disabled="currentPage <= 1"
353 + class="relative inline-flex items-center rounded-l-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
354 + >
355 + Previous
356 + </button>
357 + <button
358 + @click="nextPage"
359 + :disabled="currentPage >= totalPages"
360 + class="relative inline-flex items-center rounded-r-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
361 + >
362 + Next
363 + </button>
364 + </nav>
365 + </div>
366 + </div>
367 + </div>
368 + </div>
369 +
370 + <!-- Alert Details Modal -->
371 + <div
372 + v-if="selectedAlert"
373 + class="bg-opacity-50 fixed inset-0 z-50 h-full w-full overflow-y-auto bg-gray-600"
374 + @click="closeModal"
375 + >
376 + <div
377 + class="relative top-10 mx-auto max-h-screen w-11/12 overflow-y-auto rounded-md border bg-white p-5 shadow-lg md:w-4/5 lg:w-3/4"
378 + @click.stop
379 + >
380 + <div class="mb-4 flex items-center justify-between">
381 + <h3 class="text-lg font-medium text-gray-900">Alert Details</h3>
382 + <button @click="closeModal" class="text-gray-400 hover:text-gray-600">
383 + <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
384 + <path
385 + stroke-linecap="round"
386 + stroke-linejoin="round"
387 + stroke-width="2"
388 + d="M6 18L18 6M6 6l12 12"
389 + ></path>
390 + </svg>
391 + </button>
392 + </div>
393 +
394 + <div class="space-y-6">
395 + <!-- Basic Alert Information -->
396 + <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
397 + <div>
398 + <label class="block text-sm font-medium text-gray-700">Alert Name</label>
399 + <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.alert_name }}</p>
400 + </div>
401 + <div>
402 + <label class="block text-sm font-medium text-gray-700">Status</label>
403 + <span
404 + class="mt-1 inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
405 + :class="{
406 + 'bg-red-100 text-red-800': selectedAlert.status === 'OPEN',
407 + 'bg-yellow-100 text-yellow-800': selectedAlert.status === 'IN_PROGRESS',
408 + 'bg-green-100 text-green-800': selectedAlert.status === 'CLOSED'
409 + }"
410 + >
411 + {{ selectedAlert.status.replace("_", " ").toUpperCase() }}
412 + </span>
413 + </div>
414 + <div>
415 + <label class="block text-sm font-medium text-gray-700">Source</label>
416 + <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.source }}</p>
417 + </div>
418 + <div>
419 + <label class="block text-sm font-medium text-gray-700">Customer</label>
420 + <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.customer_code }}</p>
421 + </div>
422 + <div>
423 + <label class="block text-sm font-medium text-gray-700">Created</label>
424 + <p class="mt-1 text-sm text-gray-900">
425 + {{ formatDate(selectedAlert.alert_creation_time) }}
426 + </p>
427 + </div>
428 + <div v-if="selectedAlert.assigned_to">
429 + <label class="block text-sm font-medium text-gray-700">Assigned To</label>
430 + <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.assigned_to }}</p>
431 + </div>
432 + </div>
433 +
434 + <div v-if="selectedAlert.alert_description">
435 + <label class="block text-sm font-medium text-gray-700">Description</label>
436 + <p class="mt-1 text-sm whitespace-pre-wrap text-gray-900">
437 + {{ selectedAlert.alert_description }}
438 + </p>
439 + </div>
440 +
441 + <!-- Assets Section -->
442 + <div v-if="selectedAlert.assets && selectedAlert.assets.length > 0">
443 + <label class="mb-2 block text-sm font-medium text-gray-700">Assets</label>
444 + <div class="rounded-lg bg-gray-50 p-4">
445 + <div
446 + v-for="asset in selectedAlert.assets"
447 + :key="asset.id"
448 + class="mb-3 border-b border-gray-200 pb-3 last:mb-0 last:border-b-0"
449 + >
450 + <div class="grid grid-cols-1 gap-2 text-sm md:grid-cols-3">
451 + <div>
452 + <span class="font-medium">Asset Name:</span>
453 + {{ asset.asset_name }}
454 + </div>
455 + <div>
456 + <span class="font-medium">Agent ID:</span>
457 + {{ asset.agent_id }}
458 + </div>
459 + <div v-if="asset.velociraptor_id">
460 + <span class="font-medium">Velociraptor ID:</span>
461 + {{ asset.velociraptor_id }}
462 + </div>
463 + <div>
464 + <span class="font-medium">Index:</span>
465 + {{ asset.index_name }}
466 + </div>
467 + <div>
468 + <span class="font-medium">Index ID:</span>
469 + {{ asset.index_id.substring(0, 20) }}...
470 + </div>
471 + </div>
472 + </div>
473 + </div>
474 + </div>
475 +
476 + <!-- Fallback for legacy asset_name -->
477 + <div v-else-if="selectedAlert.asset_name">
478 + <label class="block text-sm font-medium text-gray-700">Asset</label>
479 + <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.asset_name }}</p>
480 + </div>
481 +
482 + <!-- Tags Section -->
483 + <div v-if="selectedAlert.tags && selectedAlert.tags.length > 0">
484 + <label class="block text-sm font-medium text-gray-700">Tags</label>
485 + <div class="mt-1 flex flex-wrap gap-2">
486 + <span
487 + v-for="tag in selectedAlert.tags"
488 + :key="tag.id"
489 + class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800"
490 + >
491 + {{ tag.tag }}
492 + </span>
493 + </div>
494 + </div>
495 +
496 + <!-- Legacy Tags (for backward compatibility) -->
497 + <div v-else-if="selectedAlert.tag && selectedAlert.tag.length > 0">
498 + <label class="block text-sm font-medium text-gray-700">Tags</label>
499 + <div class="mt-1 flex flex-wrap gap-2">
500 + <span
501 + v-for="tag in selectedAlert.tag"
502 + :key="tag"
503 + class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800"
504 + >
505 + {{ tag }}
506 + </span>
507 + </div>
508 + </div>
509 +
510 + <!-- Linked Cases Section -->
511 + <div v-if="selectedAlert.linked_cases && selectedAlert.linked_cases.length > 0">
512 + <label class="mb-2 block text-sm font-medium text-gray-700">Linked Cases</label>
513 + <div class="rounded-lg bg-gray-50 p-4">
514 + <div
515 + v-for="linkedCase in selectedAlert.linked_cases"
516 + :key="linkedCase.id"
517 + class="mb-3 border-b border-gray-200 pb-3 last:mb-0 last:border-b-0"
518 + >
519 + <div class="flex items-start justify-between">
520 + <div class="flex-1">
521 + <h4 class="text-sm font-medium text-gray-900">{{ linkedCase.case_name }}</h4>
522 + <p class="mt-1 text-xs text-gray-600">{{ linkedCase.case_description }}</p>
523 + <div class="mt-2 flex items-center space-x-4 text-xs text-gray-500">
524 + <span>Case #{{ linkedCase.id }}</span>
525 + <span>Created: {{ formatDate(linkedCase.case_creation_time) }}</span>
526 + <span v-if="linkedCase.assigned_to">
527 + Assigned to: {{ linkedCase.assigned_to }}
528 + </span>
529 + </div>
530 + </div>
531 + <span
532 + class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium"
533 + :class="{
534 + 'bg-red-100 text-red-800': linkedCase.case_status === 'OPEN',
535 + 'bg-yellow-100 text-yellow-800': linkedCase.case_status === 'IN_PROGRESS',
536 + 'bg-green-100 text-green-800': linkedCase.case_status === 'CLOSED'
537 + }"
538 + >
539 + {{ linkedCase.case_status.replace("_", " ").toUpperCase() }}
540 + </span>
541 + </div>
542 + </div>
543 + </div>
544 + </div>
545 +
546 + <!-- Legacy Case IDs (for backward compatibility) -->
547 + <div v-else-if="selectedAlert.case_ids && selectedAlert.case_ids.length > 0">
548 + <label class="block text-sm font-medium text-gray-700">Linked Cases</label>
549 + <div class="mt-1 flex flex-wrap gap-2">
550 + <span
551 + v-for="caseId in selectedAlert.case_ids"
552 + :key="caseId"
553 + class="inline-flex items-center rounded-full bg-purple-100 px-2.5 py-0.5 text-xs font-medium text-purple-800"
554 + >
555 + Case #{{ caseId }}
556 + </span>
557 + </div>
558 + </div>
559 +
560 + <!-- IoCs Section -->
561 + <div v-if="selectedAlert.iocs && selectedAlert.iocs.length > 0">
562 + <label class="mb-2 block text-sm font-medium text-gray-700">
563 + Indicators of Compromise (IoCs)
564 + </label>
565 + <div class="rounded-lg bg-gray-50 p-4">
566 + <div
567 + v-for="ioc in selectedAlert.iocs"
568 + :key="ioc.id"
569 + class="mb-3 border-b border-gray-200 pb-3 last:mb-0 last:border-b-0"
570 + >
571 + <div class="grid grid-cols-1 gap-2 text-sm md:grid-cols-3">
572 + <div>
573 + <span class="font-medium">Value:</span>
574 + <code class="rounded bg-gray-100 px-1 text-xs">{{ ioc.ioc_value }}</code>
575 + </div>
576 + <div>
577 + <span class="font-medium">Type:</span>
578 + {{ ioc.ioc_type }}
579 + </div>
580 + <div>
581 + <span class="font-medium">Description:</span>
582 + {{ ioc.ioc_description }}
583 + </div>
584 + </div>
585 + </div>
586 + </div>
587 + </div>
588 +
589 + <!-- Comments Section -->
590 + <div>
591 + <label class="mb-2 block text-sm font-medium text-gray-700">
592 + Comments
593 + <span
594 + v-if="selectedAlert.comments && selectedAlert.comments.length > 0"
595 + class="font-normal text-gray-500"
596 + >
597 + ({{ selectedAlert.comments.length }})
598 + </span>
599 + </label>
600 +
601 + <!-- Existing Comments -->
602 + <div
603 + v-if="selectedAlert.comments && selectedAlert.comments.length > 0"
604 + class="mb-4 max-h-64 overflow-y-auto rounded-lg bg-gray-50 p-4"
605 + >
606 + <div
607 + v-for="comment in selectedAlert.comments"
608 + :key="comment.id"
609 + class="mb-3 border-b border-gray-200 pb-3 last:mb-0 last:border-b-0"
610 + >
611 + <div class="mb-2 flex items-start justify-between">
612 + <span class="text-sm font-medium text-gray-900">{{ comment.user_name }}</span>
613 + <span class="text-xs text-gray-500">{{ formatDate(comment.created_at) }}</span>
614 + </div>
615 + <p class="text-sm whitespace-pre-wrap text-gray-700">{{ comment.comment }}</p>
616 + </div>
617 + </div>
618 +
619 + <!-- No Comments Message -->
620 + <div v-else class="mb-4 rounded-lg bg-gray-50 p-4 text-center">
621 + <p class="text-sm text-gray-500">No comments yet</p>
622 + </div>
623 +
624 + <!-- Add Comment Form -->
625 + <div class="rounded-lg border bg-white p-4">
626 + <label class="mb-2 block text-sm font-medium text-gray-700">Add Comment</label>
627 + <textarea
628 + v-model="newComment"
629 + placeholder="Enter your comment..."
630 + rows="3"
631 + class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
632 + ></textarea>
633 + <div class="mt-3 flex justify-end">
634 + <button
635 + @click="addComment"
636 + :disabled="!newComment.trim() || isAddingComment"
637 + class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
638 + >
639 + <svg
640 + v-if="isAddingComment"
641 + class="mr-2 -ml-1 h-4 w-4 animate-spin text-white"
642 + xmlns="http://www.w3.org/2000/svg"
643 + fill="none"
644 + viewBox="0 0 24 24"
645 + >
646 + <circle
647 + class="opacity-25"
648 + cx="12"
649 + cy="12"
650 + r="10"
651 + stroke="currentColor"
652 + stroke-width="4"
653 + ></circle>
654 + <path
655 + class="opacity-75"
656 + fill="currentColor"
657 + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
658 + ></path>
659 + </svg>
660 + {{ isAddingComment ? "Adding..." : "Add Comment" }}
661 + </button>
662 + </div>
663 + </div>
664 + </div>
665 + </div>
666 + </div>
667 + </div>
668 + </div>
669 </template>
670
671 <script setup lang="ts">
561 -import { ref, onMounted, computed } from 'vue'
562 -import { useRouter } from 'vue-router'
563 -import AlertsAPI, { type Alert, type AlertsResponse } from '@/api/alerts'
672 +import { ref, onMounted, computed } from "vue"
673 +import { useRouter } from "vue-router"
674 +import { usePortalSettingsStore } from "@/stores/portalSettings"
675 +import AlertsAPI, { type Alert, type AlertsResponse } from "@/api/alerts"
676
677 const router = useRouter()
678 +const portalSettingsStore = usePortalSettingsStore()
679
680 // Reactive data
681 const alerts = ref<Alert[]>([])
682 const stats = ref({
570 - total: 0,
571 - open: 0,
572 - in_progress: 0,
573 - closed: 0
683 + total: 0,
684 + open: 0,
685 + in_progress: 0,
686 + closed: 0
687 })
688 const loading = ref(false)
689 const error = ref<string | null>(null)
690 +const showLogo = ref(true)
691 const selectedAlert = ref<Alert | null>(null)
692 const updatingStatus = ref<number | null>(null)
693
694 // Comment management
581 -const newComment = ref('')
695 +const newComment = ref("")
696 const isAddingComment = ref(false)
697
698 // Pagination
@@ -587,151 +701,153 @@ const pageSize = ref(25)
701
702 // Filters
703 const filters = ref({
590 - status: '',
591 - source: '',
592 - asset: ''
704 + status: "",
705 + source: "",
706 + asset: ""
707 })
708
709 // Computed properties
710 +const portalTitle = computed(() => portalSettingsStore.portalTitle || "Customer Portal")
711 +const portalLogo = computed(() => portalSettingsStore.portalLogo)
712 +
713 const totalPages = computed(() => Math.ceil(stats.value.total / pageSize.value))
714
715 const availableSources = computed(() => {
599 - const sources = new Set(alerts.value.map(alert => alert.source))
600 - return Array.from(sources).sort()
716 + const sources = new Set(alerts.value.map(alert => alert.source))
717 + return Array.from(sources).sort()
718 })
719
720 const availableAssets = computed(() => {
604 - const assets = new Set(alerts.value.map(alert => alert.asset_name))
605 - return Array.from(assets).sort()
721 + const assets = new Set(alerts.value.map(alert => alert.asset_name))
722 + return Array.from(assets).sort()
723 })
724
725 // Methods
726 const goBack = () => {
610 - router.push('/')
727 + router.push("/")
728 }
729
730 const loadAlerts = async () => {
614 - loading.value = true
615 - error.value = null
616 -
617 - try {
618 - let response: AlertsResponse
619 -
620 - if (filters.value.status) {
621 - response = await AlertsAPI.getAlertsByStatus(filters.value.status as any)
622 - } else if (filters.value.source) {
623 - response = await AlertsAPI.getAlertsBySource(filters.value.source)
624 - } else if (filters.value.asset) {
625 - response = await AlertsAPI.getAlertsByAsset(filters.value.asset)
626 - } else {
627 - response = await AlertsAPI.getAlerts(currentPage.value, pageSize.value)
628 - }
629 -
630 - alerts.value = response.alerts
631 - stats.value = {
632 - total: response.total,
633 - open: response.open,
634 - in_progress: response.in_progress,
635 - closed: response.closed
636 - }
637 - } catch (err: any) {
638 - error.value = err.response?.data?.detail || err.message || 'Failed to load alerts'
639 - console.error('Error loading alerts:', err)
640 - } finally {
641 - loading.value = false
642 - }
731 + loading.value = true
732 + error.value = null
733 +
734 + try {
735 + let response: AlertsResponse
736 +
737 + if (filters.value.status) {
738 + response = await AlertsAPI.getAlertsByStatus(filters.value.status as any)
739 + } else if (filters.value.source) {
740 + response = await AlertsAPI.getAlertsBySource(filters.value.source)
741 + } else if (filters.value.asset) {
742 + response = await AlertsAPI.getAlertsByAsset(filters.value.asset)
743 + } else {
744 + response = await AlertsAPI.getAlerts(currentPage.value, pageSize.value)
745 + }
746 +
747 + alerts.value = response.alerts
748 + stats.value = {
749 + total: response.total,
750 + open: response.open,
751 + in_progress: response.in_progress,
752 + closed: response.closed
753 + }
754 + } catch (err: any) {
755 + error.value = err.response?.data?.detail || err.message || "Failed to load alerts"
756 + console.error("Error loading alerts:", err)
757 + } finally {
758 + loading.value = false
759 + }
760 }
761
762 const refreshAlerts = () => {
646 - loadAlerts()
763 + loadAlerts()
764 }
765
766 const applyFilters = () => {
650 - currentPage.value = 1
651 - loadAlerts()
767 + currentPage.value = 1
768 + loadAlerts()
769 }
770
771 const updateAlertStatus = async (alertId: number, newStatus: string) => {
655 - updatingStatus.value = alertId
656 -
657 - try {
658 - await AlertsAPI.updateAlertStatus(alertId, newStatus as any)
659 -
660 - // Update the local alert status
661 - const alert = alerts.value.find(a => a.id === alertId)
662 - if (alert) {
663 - alert.status = newStatus as any
664 - }
665 -
666 - // Refresh stats
667 - await loadAlerts()
668 - } catch (err: any) {
669 - error.value = err.response?.data?.detail || err.message || 'Failed to update alert status'
670 - console.error('Error updating alert status:', err)
671 - } finally {
672 - updatingStatus.value = null
673 - }
772 + updatingStatus.value = alertId
773 +
774 + try {
775 + await AlertsAPI.updateAlertStatus(alertId, newStatus as any)
776 +
777 + // Update the local alert status
778 + const alert = alerts.value.find(a => a.id === alertId)
779 + if (alert) {
780 + alert.status = newStatus as any
781 + }
782 +
783 + // Refresh stats
784 + await loadAlerts()
785 + } catch (err: any) {
786 + error.value = err.response?.data?.detail || err.message || "Failed to update alert status"
787 + console.error("Error updating alert status:", err)
788 + } finally {
789 + updatingStatus.value = null
790 + }
791 }
792
793 const viewAlert = (alert: Alert) => {
677 - selectedAlert.value = alert
794 + selectedAlert.value = alert
795 }
796
797 const closeModal = () => {
681 - selectedAlert.value = null
682 - newComment.value = '' // Clear comment when closing modal
798 + selectedAlert.value = null
799 + newComment.value = "" // Clear comment when closing modal
800 }
801
802 const addComment = async () => {
686 - if (!selectedAlert.value || !newComment.value.trim()) return
687 -
688 - isAddingComment.value = true
689 - try {
690 - // Make API call to add comment
691 - const response = await AlertsAPI.addComment({
692 - alert_id: selectedAlert.value.id,
693 - comment: newComment.value.trim(),
694 - user_name: 'Customer User' // This should come from auth context later
695 - })
696 -
697 - // Add the new comment to the local array
698 - if (!selectedAlert.value.comments) {
699 - selectedAlert.value.comments = []
700 - }
701 - selectedAlert.value.comments.push(response.comment)
702 -
703 - // Clear the input
704 - newComment.value = ''
705 -
706 - } catch (err) {
707 - console.error('Failed to add comment:', err)
708 - // Handle error - maybe show a toast notification
709 - error.value = 'Failed to add comment. Please try again.'
710 - } finally {
711 - isAddingComment.value = false
712 - }
803 + if (!selectedAlert.value || !newComment.value.trim()) return
804 +
805 + isAddingComment.value = true
806 + try {
807 + // Make API call to add comment
808 + const response = await AlertsAPI.addComment({
809 + alert_id: selectedAlert.value.id,
810 + comment: newComment.value.trim(),
811 + user_name: "Customer User" // This should come from auth context later
812 + })
813 +
814 + // Add the new comment to the local array
815 + if (!selectedAlert.value.comments) {
816 + selectedAlert.value.comments = []
817 + }
818 + selectedAlert.value.comments.push(response.comment)
819 +
820 + // Clear the input
821 + newComment.value = ""
822 + } catch (err) {
823 + console.error("Failed to add comment:", err)
824 + // Handle error - maybe show a toast notification
825 + error.value = "Failed to add comment. Please try again."
826 + } finally {
827 + isAddingComment.value = false
828 + }
829 }
830
831 const formatDate = (dateString: string) => {
716 - return new Date(dateString).toLocaleString()
832 + return new Date(dateString).toLocaleString()
833 }
834
835 const previousPage = () => {
720 - if (currentPage.value > 1) {
721 - currentPage.value--
722 - loadAlerts()
723 - }
836 + if (currentPage.value > 1) {
837 + currentPage.value--
838 + loadAlerts()
839 + }
840 }
841
842 const nextPage = () => {
727 - if (currentPage.value < totalPages.value) {
728 - currentPage.value++
729 - loadAlerts()
730 - }
843 + if (currentPage.value < totalPages.value) {
844 + currentPage.value++
845 + loadAlerts()
846 + }
847 }
848
849 // Lifecycle
850 onMounted(() => {
735 - loadAlerts()
851 + loadAlerts()
852 })
853 </script>
customer_portal/src/views/CasesPage.vue
+1051 -832
@@ -1,692 +1,904 @@
1 <template>
2 - <div class="min-h-screen bg-gray-50">
3 - <!-- Header -->
4 - <header class="bg-white shadow-sm border-b">
5 - <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
6 - <div class="flex justify-between h-16">
7 - <div class="flex items-center">
8 - <img
9 - class="h-8 w-auto mr-3"
10 - src="/logo.svg"
11 - alt="SOCFortress Logo"
12 - />
13 - <h1 class="text-xl font-semibold text-gray-900">Customer Portal</h1>
14 - <nav class="ml-8 flex space-x-8">
15 - <router-link
16 - to="/"
17 - class="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
18 - >
19 - Overview
20 - </router-link>
21 - <router-link
22 - to="/alerts"
23 - class="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
24 - >
25 - Alerts
26 - </router-link>
27 - <router-link
28 - to="/cases"
29 - class="text-indigo-600 border-b-2 border-indigo-600 px-3 py-2 rounded-md text-sm font-medium"
30 - >
31 - Cases
32 - </router-link>
33 - <router-link
34 - to="/agents"
35 - class="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
36 - >
37 - Agents
38 - </router-link>
39 - </nav>
40 - </div>
41 - <div class="flex items-center space-x-4">
42 - <button
43 - @click="refreshCases"
44 - :disabled="loading"
45 - class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
46 - >
47 - <svg class="w-4 h-4 mr-2" :class="{ 'animate-spin': loading }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
48 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
49 - </svg>
50 - Refresh
51 - </button>
52 - </div>
53 - </div>
54 - </div>
55 - </header>
56 -
57 - <!-- Stats Cards -->
58 - <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
59 - <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
60 - <div class="bg-white overflow-hidden shadow rounded-lg">
61 - <div class="p-5">
62 - <div class="flex items-center">
63 - <div class="flex-shrink-0">
64 - <div class="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
65 - <svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
66 - <path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"></path>
67 - <path fill-rule="evenodd" d="M4 5a2 2 0 012-2v1a1 1 0 102 0V3a2 2 0 012 2v6a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 2a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z" clip-rule="evenodd"></path>
68 - </svg>
69 - </div>
70 - </div>
71 - <div class="ml-5 w-0 flex-1">
72 - <dl>
73 - <dt class="text-sm font-medium text-gray-500 truncate">Total Cases</dt>
74 - <dd class="text-lg font-medium text-gray-900">{{ cases.length }}</dd>
75 - </dl>
76 - </div>
77 - </div>
78 - </div>
79 - </div>
80 -
81 - <div class="bg-white overflow-hidden shadow rounded-lg">
82 - <div class="p-5">
83 - <div class="flex items-center">
84 - <div class="flex-shrink-0">
85 - <div class="w-8 h-8 bg-red-500 rounded-md flex items-center justify-center">
86 - <svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
87 - <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
88 - </svg>
89 - </div>
90 - </div>
91 - <div class="ml-5 w-0 flex-1">
92 - <dl>
93 - <dt class="text-sm font-medium text-gray-500 truncate">Open</dt>
94 - <dd class="text-lg font-medium text-gray-900">{{ openCases }}</dd>
95 - </dl>
96 - </div>
97 - </div>
98 - </div>
99 - </div>
100 -
101 - <div class="bg-white overflow-hidden shadow rounded-lg">
102 - <div class="p-5">
103 - <div class="flex items-center">
104 - <div class="flex-shrink-0">
105 - <div class="w-8 h-8 bg-yellow-500 rounded-md flex items-center justify-center">
106 - <svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
107 - <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"></path>
108 - </svg>
109 - </div>
110 - </div>
111 - <div class="ml-5 w-0 flex-1">
112 - <dl>
113 - <dt class="text-sm font-medium text-gray-500 truncate">In Progress</dt>
114 - <dd class="text-lg font-medium text-gray-900">{{ inProgressCases }}</dd>
115 - </dl>
116 - </div>
117 - </div>
118 - </div>
119 - </div>
120 -
121 - <div class="bg-white overflow-hidden shadow rounded-lg">
122 - <div class="p-5">
123 - <div class="flex items-center">
124 - <div class="flex-shrink-0">
125 - <div class="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
126 - <svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
127 - <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
128 - </svg>
129 - </div>
130 - </div>
131 - <div class="ml-5 w-0 flex-1">
132 - <dl>
133 - <dt class="text-sm font-medium text-gray-500 truncate">Closed</dt>
134 - <dd class="text-lg font-medium text-gray-900">{{ closedCases }}</dd>
135 - </dl>
136 - </div>
137 - </div>
138 - </div>
139 - </div>
140 - </div>
141 -
142 - <!-- Filters -->
143 - <div class="bg-white shadow rounded-lg mb-6">
144 - <div class="px-4 py-5 sm:p-6">
145 - <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
146 - <div>
147 - <label for="status-filter" class="block text-sm font-medium text-gray-700">Status</label>
148 - <select
149 - id="status-filter"
150 - v-model="filters.status"
151 - @change="applyFilters"
152 - class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
153 - >
154 - <option value="">All Statuses</option>
155 - <option value="open">Open</option>
156 - <option value="in_progress">In Progress</option>
157 - <option value="closed">Closed</option>
158 - </select>
159 - </div>
160 - <div>
161 - <label for="assigned-to-filter" class="block text-sm font-medium text-gray-700">Assigned To</label>
162 - <select
163 - id="assigned-to-filter"
164 - v-model="filters.assignedTo"
165 - @change="applyFilters"
166 - class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
167 - >
168 - <option value="">All Assignees</option>
169 - <option v-for="assignee in availableAssignees" :key="assignee" :value="assignee">{{ assignee }}</option>
170 - </select>
171 - </div>
172 - </div>
173 - </div>
174 - </div>
175 -
176 - <!-- Cases List -->
177 - <div class="bg-white shadow overflow-hidden sm:rounded-md">
178 - <div v-if="loading" class="px-4 py-5 sm:p-6 text-center">
179 - <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600 mx-auto"></div>
180 - <p class="mt-2 text-sm text-gray-500">Loading cases...</p>
181 - </div>
182 -
183 - <div v-else-if="error" class="px-4 py-5 sm:p-6 text-center">
184 - <div class="text-red-500 mb-2">
185 - <svg class="w-8 h-8 mx-auto" fill="currentColor" viewBox="0 0 20 20">
186 - <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
187 - </svg>
188 - </div>
189 - <p class="text-sm text-red-600">{{ error }}</p>
190 - <button
191 - @click="loadCases"
192 - class="mt-2 inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
193 - >
194 - Try Again
195 - </button>
196 - </div>
197 -
198 - <ul v-else-if="filteredCases.length > 0" role="list" class="divide-y divide-gray-200">
199 - <li v-for="case_ in filteredCases" :key="case_.id" class="px-4 py-4 sm:px-6 hover:bg-gray-50">
200 - <div class="flex items-center justify-between">
201 - <div class="flex items-center">
202 - <div class="flex-shrink-0">
203 - <span
204 - class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
205 - :class="{
206 - 'bg-red-100 text-red-800': case_.case_status?.toLowerCase() === 'open',
207 - 'bg-yellow-100 text-yellow-800': case_.case_status?.toLowerCase() === 'in_progress',
208 - 'bg-green-100 text-green-800': case_.case_status?.toLowerCase() === 'closed'
209 - }"
210 - >
211 - {{ case_.case_status?.replace('_', ' ').toUpperCase() }}
212 - </span>
213 - </div>
214 - <div class="ml-4">
215 - <div class="text-sm font-medium text-gray-900">
216 - {{ case_.case_name }}
217 - </div>
218 - <div class="text-sm text-gray-500">
219 - Case #{{ case_.id }}
220 - <span v-if="case_.assigned_to"> | Assigned to: {{ case_.assigned_to }}</span>
221 - </div>
222 - <div class="text-xs text-gray-400">
223 - Created: {{ formatDate(case_.case_creation_time) }}
224 - </div>
225 - </div>
226 - </div>
227 - <div class="flex items-center space-x-2">
228 - <select
229 - :value="case_.case_status"
230 - @change="updateCaseStatus(case_.id, ($event.target as HTMLSelectElement).value)"
231 - class="text-sm border-gray-300 rounded-md focus:ring-indigo-500 focus:border-indigo-500"
232 - :disabled="updatingStatus === case_.id"
233 - >
234 - <option value="open">Open</option>
235 - <option value="in_progress">In Progress</option>
236 - <option value="closed">Closed</option>
237 - </select>
238 - <button
239 - @click="viewCase(case_)"
240 - class="inline-flex items-center px-3 py-1 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
241 - >
242 - View Details
243 - </button>
244 - </div>
245 - </div>
246 - <div v-if="case_.case_description" class="mt-2 text-sm text-gray-600">
247 - {{ case_.case_description }}
248 - </div>
249 - <div v-if="case_.alert_ids && case_.alert_ids.length > 0" class="mt-2">
250 - <span class="text-xs text-gray-500">Linked Alerts: {{ case_.alert_ids.length }}</span>
251 - </div>
252 - </li>
253 - </ul>
254 -
255 - <div v-else class="px-4 py-5 sm:p-6 text-center">
256 - <svg class="w-12 h-12 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
257 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
258 - </svg>
259 - <h3 class="mt-2 text-sm font-medium text-gray-900">No cases found</h3>
260 - <p class="mt-1 text-sm text-gray-500">No security cases match your current filters.</p>
261 - </div>
262 - </div>
263 - </div>
264 -
265 - <!-- Case Details Modal -->
266 - <div v-if="selectedCase" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50" @click="closeModal">
267 - <div class="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white" @click.stop>
268 - <div class="flex justify-between items-center mb-4">
269 - <h3 class="text-lg font-medium text-gray-900">Case Details</h3>
270 - <button @click="closeModal" class="text-gray-400 hover:text-gray-600">
271 - <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
272 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
273 - </svg>
274 - </button>
275 - </div>
276 -
277 - <div class="space-y-4">
278 - <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
279 - <div>
280 - <label class="block text-sm font-medium text-gray-700">Case Name</label>
281 - <p class="mt-1 text-sm text-gray-900">{{ selectedCase.case_name }}</p>
282 - </div>
283 - <div>
284 - <label class="block text-sm font-medium text-gray-700">Status</label>
285 - <span
286 - class="mt-1 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
287 - :class="{
288 - 'bg-red-100 text-red-800': selectedCase.case_status?.toLowerCase() === 'open',
289 - 'bg-yellow-100 text-yellow-800': selectedCase.case_status?.toLowerCase() === 'in_progress',
290 - 'bg-green-100 text-green-800': selectedCase.case_status?.toLowerCase() === 'closed'
291 - }"
292 - >
293 - {{ selectedCase.case_status?.replace('_', ' ').toUpperCase() }}
294 - </span>
295 - </div>
296 - <div>
297 - <label class="block text-sm font-medium text-gray-700">Case ID</label>
298 - <p class="mt-1 text-sm text-gray-900">#{{ selectedCase.id }}</p>
299 - </div>
300 - <div>
301 - <label class="block text-sm font-medium text-gray-700">Customer</label>
302 - <p class="mt-1 text-sm text-gray-900">{{ selectedCase.customer_code }}</p>
303 - </div>
304 - <div>
305 - <label class="block text-sm font-medium text-gray-700">Assigned To</label>
306 - <p class="mt-1 text-sm text-gray-900">{{ selectedCase.assigned_to || 'Unassigned' }}</p>
307 - </div>
308 - <div>
309 - <label class="block text-sm font-medium text-gray-700">Created</label>
310 - <p class="mt-1 text-sm text-gray-900">{{ formatDate(selectedCase.case_creation_time) }}</p>
311 - </div>
312 - </div>
313 -
314 - <div v-if="selectedCase.case_description">
315 - <label class="block text-sm font-medium text-gray-700">Description</label>
316 - <p class="mt-1 text-sm text-gray-900 whitespace-pre-wrap">{{ selectedCase.case_description }}</p>
317 - </div>
318 -
319 - <div v-if="selectedCase.alert_ids && selectedCase.alert_ids.length > 0">
320 - <label class="block text-sm font-medium text-gray-700">Linked Alerts</label>
321 - <div class="mt-1 flex flex-wrap gap-2">
322 - <span
323 - v-for="alertId in selectedCase.alert_ids"
324 - :key="alertId"
325 - class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
326 - >
327 - Alert #{{ alertId }}
328 - </span>
329 - </div>
330 - </div>
331 -
332 - <div v-if="selectedCase.alerts && selectedCase.alerts.length > 0">
333 - <div class="flex items-center justify-between mb-2">
334 - <label class="block text-sm font-medium text-gray-700">Alert Details</label>
335 - <span class="text-xs text-gray-500">Click alerts to view details</span>
336 - </div>
337 - <div class="mt-1 space-y-2">
338 - <div
339 - v-for="alert in selectedCase.alerts"
340 - :key="alert.id"
341 - class="p-3 bg-gray-50 rounded-md border hover:bg-gray-100 cursor-pointer transition-colors"
342 - @click="viewAlert(alert.id)"
343 - >
344 - <div class="flex justify-between items-start">
345 - <div class="flex-1">
346 - <div class="flex items-center space-x-2">
347 - <p class="text-sm font-medium text-gray-900">{{ alert.alert_name }}</p>
348 - <svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
349 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-2M7 7l10 10M17 7v4h-4"></path>
350 - </svg>
351 - </div>
352 - <p class="text-xs text-gray-500">Asset: {{ alert.asset_name }}</p>
353 - </div>
354 - <span
355 - class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
356 - :class="{
357 - 'bg-red-100 text-red-800': alert.status === 'open',
358 - 'bg-yellow-100 text-yellow-800': alert.status === 'in_progress',
359 - 'bg-green-100 text-green-800': alert.status === 'closed'
360 - }"
361 - >
362 - {{ alert.status.replace('_', ' ').toUpperCase() }}
363 - </span>
364 - </div>
365 - </div>
366 - </div>
367 - </div>
368 -
369 - <!-- Case Files Section -->
370 - <div>
371 - <div class="flex items-center justify-between mb-2">
372 - <label class="block text-sm font-medium text-gray-700">
373 - Case Files
374 - <span v-if="caseFiles.length > 0" class="text-gray-500 font-normal">
375 - ({{ caseFiles.length }})
376 - </span>
377 - </label>
378 - <div class="flex items-center space-x-2">
379 - <button
380 - @click="openUploadForm"
381 - class="inline-flex items-center px-2 py-1 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
382 - >
383 - <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
384 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
385 - </svg>
386 - Upload File
387 - </button>
388 - <button
389 - v-if="!loadingFiles"
390 - @click="loadCaseFiles(selectedCase.id)"
391 - class="text-xs text-indigo-600 hover:text-indigo-500 focus:outline-none"
392 - >
393 - <svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
394 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
395 - </svg>
396 - Refresh
397 - </button>
398 - </div>
399 - </div>
400 -
401 - <!-- Upload Form -->
402 - <div v-if="showUploadForm" class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
403 - <div class="flex items-center justify-between mb-3">
404 - <h4 class="text-sm font-medium text-blue-900">Upload File to Case</h4>
405 - <button
406 - @click="closeUploadForm"
407 - class="text-blue-400 hover:text-blue-600"
408 - >
409 - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
410 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
411 - </svg>
412 - </button>
413 - </div>
414 -
415 - <div class="space-y-3">
416 - <div>
417 - <input
418 - ref="fileInput"
419 - type="file"
420 - @change="handleFileSelect"
421 - class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100 focus:outline-none"
422 - />
423 - </div>
424 -
425 - <div v-if="selectedFile" class="text-sm text-gray-600">
426 - Selected: {{ selectedFile.name }} ({{ CaseDataStoreAPI.formatFileSize(selectedFile.size) }})
427 - </div>
428 -
429 - <!-- Error message for upload -->
430 - <div v-if="error && showUploadForm" class="text-sm text-red-600 bg-red-50 border border-red-200 rounded-md p-2">
431 - {{ error }}
432 - </div>
433 -
434 - <div class="flex justify-end space-x-2">
435 - <button
436 - @click="closeUploadForm"
437 - type="button"
438 - class="px-3 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
439 - >
440 - Cancel
441 - </button>
442 - <button
443 - @click="uploadFile"
444 - :disabled="!selectedFile || uploadingFile"
445 - class="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
446 - >
447 - <svg v-if="uploadingFile" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
448 - <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
449 - <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
450 - </svg>
451 - {{ uploadingFile ? 'Uploading...' : 'Upload File' }}
452 - </button>
453 - </div>
454 - </div>
455 - </div>
456 -
457 - <!-- Loading Files -->
458 - <div v-if="loadingFiles" class="bg-gray-50 rounded-lg p-4 text-center">
459 - <div class="animate-spin rounded-full h-6 w-6 border-b-2 border-indigo-600 mx-auto"></div>
460 - <p class="mt-2 text-sm text-gray-500">Loading files...</p>
461 - </div>
462 -
463 - <!-- Files List -->
464 - <div v-else-if="caseFiles.length > 0" class="bg-gray-50 rounded-lg p-4 max-h-64 overflow-y-auto">
465 - <div v-for="file in caseFiles" :key="file.id" class="border-b border-gray-200 pb-3 mb-3 last:border-b-0 last:mb-0">
466 - <div class="flex justify-between items-start">
467 - <div class="flex-1 min-w-0">
468 - <div class="flex items-center space-x-2">
469 - <svg class="w-4 h-4 text-gray-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
470 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
471 - </svg>
472 - <p class="text-sm font-medium text-gray-900 truncate">{{ file.file_name }}</p>
473 - </div>
474 - <div class="mt-1 flex items-center space-x-4 text-xs text-gray-500">
475 - <span>{{ CaseDataStoreAPI.formatFileSize(file.file_size) }}</span>
476 - <span v-if="file.content_type">{{ file.content_type }}</span>
477 - <span>{{ formatDate(file.upload_time) }}</span>
478 - </div>
479 - </div>
480 - <button
481 - @click="downloadFile(selectedCase.id, file.file_name)"
482 - :disabled="downloadingFile === file.file_name"
483 - class="ml-3 inline-flex items-center px-2 py-1 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
484 - >
485 - <svg v-if="downloadingFile === file.file_name" class="animate-spin -ml-1 mr-1 h-3 w-3 text-indigo-700" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
486 - <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
487 - <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
488 - </svg>
489 - <svg v-else class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
490 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
491 - </svg>
492 - {{ downloadingFile === file.file_name ? 'Downloading...' : 'Download' }}
493 - </button>
494 - </div>
495 - </div>
496 - </div>
497 -
498 - <!-- No Files Message -->
499 - <div v-else class="bg-gray-50 rounded-lg p-4 text-center">
500 - <svg class="w-8 h-8 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
501 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
502 - </svg>
503 - <p class="mt-2 text-sm text-gray-500">No files available for this case</p>
504 - </div>
505 - </div>
506 - </div>
507 - </div>
508 - </div>
509 -
510 - <!-- Alert Details Modal -->
511 - <div v-if="selectedAlert" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50" @click="closeAlertModal">
512 - <div class="relative top-10 mx-auto p-5 border w-11/12 md:w-4/5 lg:w-3/4 shadow-lg rounded-md bg-white max-h-screen overflow-y-auto" @click.stop>
513 - <div class="flex justify-between items-center mb-4">
514 - <h3 class="text-lg font-medium text-gray-900">Alert Details</h3>
515 - <button @click="closeAlertModal" class="text-gray-400 hover:text-gray-600">
516 - <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
517 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
518 - </svg>
519 - </button>
520 - </div>
521 -
522 - <div class="space-y-6">
523 - <!-- Basic Alert Information -->
524 - <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
525 - <div>
526 - <label class="block text-sm font-medium text-gray-700">Alert Name</label>
527 - <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.alert_name }}</p>
528 - </div>
529 - <div>
530 - <label class="block text-sm font-medium text-gray-700">Status</label>
531 - <span
532 - class="mt-1 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
533 - :class="{
534 - 'bg-red-100 text-red-800': selectedAlert.status === 'OPEN',
535 - 'bg-yellow-100 text-yellow-800': selectedAlert.status === 'IN_PROGRESS',
536 - 'bg-green-100 text-green-800': selectedAlert.status === 'CLOSED'
537 - }"
538 - >
539 - {{ selectedAlert.status.replace('_', ' ') }}
540 - </span>
541 - </div>
542 - <div>
543 - <label class="block text-sm font-medium text-gray-700">Source</label>
544 - <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.source }}</p>
545 - </div>
546 - <div>
547 - <label class="block text-sm font-medium text-gray-700">Customer</label>
548 - <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.customer_code }}</p>
549 - </div>
550 - <div>
551 - <label class="block text-sm font-medium text-gray-700">Created</label>
552 - <p class="mt-1 text-sm text-gray-900">{{ formatDate(selectedAlert.alert_creation_time) }}</p>
553 - </div>
554 - <div v-if="selectedAlert.assigned_to">
555 - <label class="block text-sm font-medium text-gray-700">Assigned To</label>
556 - <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.assigned_to }}</p>
557 - </div>
558 - </div>
559 -
560 - <div v-if="selectedAlert.alert_description">
561 - <label class="block text-sm font-medium text-gray-700">Description</label>
562 - <p class="mt-1 text-sm text-gray-900 whitespace-pre-wrap">{{ selectedAlert.alert_description }}</p>
563 - </div>
564 -
565 - <!-- Assets Section -->
566 - <div v-if="selectedAlert.assets && selectedAlert.assets.length > 0">
567 - <label class="block text-sm font-medium text-gray-700 mb-2">Assets</label>
568 - <div class="bg-gray-50 rounded-lg p-4">
569 - <div v-for="asset in selectedAlert.assets" :key="asset.id" class="border-b border-gray-200 pb-3 mb-3 last:border-b-0 last:mb-0">
570 - <div class="grid grid-cols-1 md:grid-cols-3 gap-2 text-sm">
571 - <div>
572 - <span class="font-medium">Asset Name:</span> {{ asset.asset_name }}
573 - </div>
574 - <div>
575 - <span class="font-medium">Agent ID:</span> {{ asset.agent_id }}
576 - </div>
577 - <div v-if="asset.velociraptor_id">
578 - <span class="font-medium">Velociraptor ID:</span> {{ asset.velociraptor_id }}
579 - </div>
580 - <div>
581 - <span class="font-medium">Index:</span> {{ asset.index_name }}
582 - </div>
583 - <div>
584 - <span class="font-medium">Index ID:</span> {{ asset.index_id.substring(0, 20) }}...
585 - </div>
586 - </div>
587 - </div>
588 - </div>
589 - </div>
590 -
591 - <!-- Tags Section -->
592 - <div v-if="selectedAlert.tags && selectedAlert.tags.length > 0">
593 - <label class="block text-sm font-medium text-gray-700">Tags</label>
594 - <div class="mt-1 flex flex-wrap gap-2">
595 - <span
596 - v-for="tag in selectedAlert.tags"
597 - :key="tag.id"
598 - class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
599 - >
600 - {{ tag.tag }}
601 - </span>
602 - </div>
603 - </div>
604 -
605 - <!-- Linked Cases Section -->
606 - <div v-if="selectedAlert.linked_cases && selectedAlert.linked_cases.length > 0">
607 - <label class="block text-sm font-medium text-gray-700 mb-2">Linked Cases</label>
608 - <div class="bg-gray-50 rounded-lg p-4">
609 - <div v-for="linkedCase in selectedAlert.linked_cases" :key="linkedCase.id" class="border-b border-gray-200 pb-3 mb-3 last:border-b-0 last:mb-0">
610 - <div class="flex justify-between items-start">
611 - <div class="flex-1">
612 - <h4 class="text-sm font-medium text-gray-900">{{ linkedCase.case_name }}</h4>
613 - <p class="text-xs text-gray-600 mt-1">{{ linkedCase.case_description }}</p>
614 - <div class="flex items-center space-x-4 mt-2 text-xs text-gray-500">
615 - <span>Case #{{ linkedCase.id }}</span>
616 - <span>Created: {{ formatDate(linkedCase.case_creation_time) }}</span>
617 - <span v-if="linkedCase.assigned_to">Assigned to: {{ linkedCase.assigned_to }}</span>
618 - </div>
619 - </div>
620 - <span
621 - class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
622 - :class="{
623 - 'bg-red-100 text-red-800': linkedCase.case_status === 'OPEN',
624 - 'bg-yellow-100 text-yellow-800': linkedCase.case_status === 'IN_PROGRESS',
625 - 'bg-green-100 text-green-800': linkedCase.case_status === 'CLOSED'
626 - }"
627 - >
628 - {{ linkedCase.case_status.replace('_', ' ') }}
629 - </span>
630 - </div>
631 - </div>
632 - </div>
633 - </div>
634 -
635 - <!-- IoCs Section -->
636 - <div v-if="selectedAlert.iocs && selectedAlert.iocs.length > 0">
637 - <label class="block text-sm font-medium text-gray-700 mb-2">Indicators of Compromise (IoCs)</label>
638 - <div class="bg-gray-50 rounded-lg p-4">
639 - <div v-for="ioc in selectedAlert.iocs" :key="ioc.id" class="border-b border-gray-200 pb-3 mb-3 last:border-b-0 last:mb-0">
640 - <div class="grid grid-cols-1 md:grid-cols-3 gap-2 text-sm">
641 - <div>
642 - <span class="font-medium">Value:</span>
643 - <code class="bg-gray-100 px-1 rounded text-xs">{{ ioc.ioc_value }}</code>
644 - </div>
645 - <div>
646 - <span class="font-medium">Type:</span> {{ ioc.ioc_type }}
647 - </div>
648 - <div>
649 - <span class="font-medium">Description:</span> {{ ioc.ioc_description }}
650 - </div>
651 - </div>
652 - </div>
653 - </div>
654 - </div>
655 -
656 - <!-- Comments Section -->
657 - <div v-if="selectedAlert.comments && selectedAlert.comments.length > 0">
658 - <label class="block text-sm font-medium text-gray-700 mb-2">
659 - Comments ({{ selectedAlert.comments.length }})
660 - </label>
661 - <div class="bg-gray-50 rounded-lg p-4 max-h-64 overflow-y-auto">
662 - <div v-for="comment in selectedAlert.comments" :key="comment.id" class="border-b border-gray-200 pb-3 mb-3 last:border-b-0 last:mb-0">
663 - <div class="flex justify-between items-start mb-2">
664 - <span class="text-sm font-medium text-gray-900">{{ comment.user_name }}</span>
665 - <span class="text-xs text-gray-500">{{ formatDate(comment.created_at) }}</span>
666 - </div>
667 - <p class="text-sm text-gray-700 whitespace-pre-wrap">{{ comment.comment }}</p>
668 - </div>
669 - </div>
670 - </div>
671 - </div>
672 - </div>
673 - </div>
674 - </div>
2 + <div class="min-h-screen bg-gray-50">
3 + <!-- Header -->
4 + <header class="border-b bg-white shadow-sm">
5 + <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
6 + <div class="flex h-16 justify-between">
7 + <div class="flex items-center">
8 + <div class="mr-3 min-h-8">
9 + <img
10 + v-if="portalLogo && showLogo"
11 + class="h-8 w-auto"
12 + :src="portalLogo"
13 + :alt="portalTitle"
14 + @error="showLogo = false"
15 + />
16 + </div>
17 + <h1 class="text-xl font-semibold text-gray-900">{{ portalTitle }}</h1>
18 + <nav class="ml-8 flex space-x-8">
19 + <router-link
20 + to="/"
21 + class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-900"
22 + >
23 + Overview
24 + </router-link>
25 + <router-link
26 + to="/alerts"
27 + class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-900"
28 + >
29 + Alerts
30 + </router-link>
31 + <router-link
32 + to="/cases"
33 + class="rounded-md border-b-2 border-indigo-600 px-3 py-2 text-sm font-medium text-indigo-600"
34 + >
35 + Cases
36 + </router-link>
37 + <router-link
38 + to="/agents"
39 + class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-900"
40 + >
41 + Agents
42 + </router-link>
43 + </nav>
44 + </div>
45 + <div class="flex items-center space-x-4">
46 + <button
47 + @click="refreshCases"
48 + :disabled="loading"
49 + class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm leading-4 font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:opacity-50"
50 + >
51 + <svg
52 + class="mr-2 h-4 w-4"
53 + :class="{ 'animate-spin': loading }"
54 + fill="none"
55 + stroke="currentColor"
56 + viewBox="0 0 24 24"
57 + >
58 + <path
59 + stroke-linecap="round"
60 + stroke-linejoin="round"
61 + stroke-width="2"
62 + d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
63 + ></path>
64 + </svg>
65 + Refresh
66 + </button>
67 + </div>
68 + </div>
69 + </div>
70 + </header>
71 +
72 + <!-- Stats Cards -->
73 + <div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
74 + <div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-4">
75 + <div class="overflow-hidden rounded-lg bg-white shadow">
76 + <div class="p-5">
77 + <div class="flex items-center">
78 + <div class="flex-shrink-0">
79 + <div class="flex h-8 w-8 items-center justify-center rounded-md bg-blue-500">
80 + <svg class="h-5 w-5 text-white" fill="currentColor" viewBox="0 0 20 20">
81 + <path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"></path>
82 + <path
83 + fill-rule="evenodd"
84 + d="M4 5a2 2 0 012-2v1a1 1 0 102 0V3a2 2 0 012 2v6a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 2a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z"
85 + clip-rule="evenodd"
86 + ></path>
87 + </svg>
88 + </div>
89 + </div>
90 + <div class="ml-5 w-0 flex-1">
91 + <dl>
92 + <dt class="truncate text-sm font-medium text-gray-500">Total Cases</dt>
93 + <dd class="text-lg font-medium text-gray-900">{{ cases.length }}</dd>
94 + </dl>
95 + </div>
96 + </div>
97 + </div>
98 + </div>
99 +
100 + <div class="overflow-hidden rounded-lg bg-white shadow">
101 + <div class="p-5">
102 + <div class="flex items-center">
103 + <div class="flex-shrink-0">
104 + <div class="flex h-8 w-8 items-center justify-center rounded-md bg-red-500">
105 + <svg class="h-5 w-5 text-white" fill="currentColor" viewBox="0 0 20 20">
106 + <path
107 + fill-rule="evenodd"
108 + d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
109 + clip-rule="evenodd"
110 + ></path>
111 + </svg>
112 + </div>
113 + </div>
114 + <div class="ml-5 w-0 flex-1">
115 + <dl>
116 + <dt class="truncate text-sm font-medium text-gray-500">Open</dt>
117 + <dd class="text-lg font-medium text-gray-900">{{ openCases }}</dd>
118 + </dl>
119 + </div>
120 + </div>
121 + </div>
122 + </div>
123 +
124 + <div class="overflow-hidden rounded-lg bg-white shadow">
125 + <div class="p-5">
126 + <div class="flex items-center">
127 + <div class="flex-shrink-0">
128 + <div class="flex h-8 w-8 items-center justify-center rounded-md bg-yellow-500">
129 + <svg class="h-5 w-5 text-white" fill="currentColor" viewBox="0 0 20 20">
130 + <path
131 + fill-rule="evenodd"
132 + d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"
133 + clip-rule="evenodd"
134 + ></path>
135 + </svg>
136 + </div>
137 + </div>
138 + <div class="ml-5 w-0 flex-1">
139 + <dl>
140 + <dt class="truncate text-sm font-medium text-gray-500">In Progress</dt>
141 + <dd class="text-lg font-medium text-gray-900">{{ inProgressCases }}</dd>
142 + </dl>
143 + </div>
144 + </div>
145 + </div>
146 + </div>
147 +
148 + <div class="overflow-hidden rounded-lg bg-white shadow">
149 + <div class="p-5">
150 + <div class="flex items-center">
151 + <div class="flex-shrink-0">
152 + <div class="flex h-8 w-8 items-center justify-center rounded-md bg-green-500">
153 + <svg class="h-5 w-5 text-white" fill="currentColor" viewBox="0 0 20 20">
154 + <path
155 + fill-rule="evenodd"
156 + d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
157 + clip-rule="evenodd"
158 + ></path>
159 + </svg>
160 + </div>
161 + </div>
162 + <div class="ml-5 w-0 flex-1">
163 + <dl>
164 + <dt class="truncate text-sm font-medium text-gray-500">Closed</dt>
165 + <dd class="text-lg font-medium text-gray-900">{{ closedCases }}</dd>
166 + </dl>
167 + </div>
168 + </div>
169 + </div>
170 + </div>
171 + </div>
172 +
173 + <!-- Filters -->
174 + <div class="mb-6 rounded-lg bg-white shadow">
175 + <div class="px-4 py-5 sm:p-6">
176 + <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
177 + <div>
178 + <label for="status-filter" class="block text-sm font-medium text-gray-700">Status</label>
179 + <select
180 + id="status-filter"
181 + v-model="filters.status"
182 + @change="applyFilters"
183 + class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
184 + >
185 + <option value="">All Statuses</option>
186 + <option value="open">Open</option>
187 + <option value="in_progress">In Progress</option>
188 + <option value="closed">Closed</option>
189 + </select>
190 + </div>
191 + <div>
192 + <label for="assigned-to-filter" class="block text-sm font-medium text-gray-700">
193 + Assigned To
194 + </label>
195 + <select
196 + id="assigned-to-filter"
197 + v-model="filters.assignedTo"
198 + @change="applyFilters"
199 + class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
200 + >
201 + <option value="">All Assignees</option>
202 + <option v-for="assignee in availableAssignees" :key="assignee" :value="assignee">
203 + {{ assignee }}
204 + </option>
205 + </select>
206 + </div>
207 + </div>
208 + </div>
209 + </div>
210 +
211 + <!-- Cases List -->
212 + <div class="overflow-hidden bg-white shadow sm:rounded-md">
213 + <div v-if="loading" class="px-4 py-5 text-center sm:p-6">
214 + <div class="mx-auto h-8 w-8 animate-spin rounded-full border-b-2 border-indigo-600"></div>
215 + <p class="mt-2 text-sm text-gray-500">Loading cases...</p>
216 + </div>
217 +
218 + <div v-else-if="error" class="px-4 py-5 text-center sm:p-6">
219 + <div class="mb-2 text-red-500">
220 + <svg class="mx-auto h-8 w-8" fill="currentColor" viewBox="0 0 20 20">
221 + <path
222 + fill-rule="evenodd"
223 + d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
224 + clip-rule="evenodd"
225 + ></path>
226 + </svg>
227 + </div>
228 + <p class="text-sm text-red-600">{{ error }}</p>
229 + <button
230 + @click="loadCases"
231 + class="mt-2 inline-flex items-center rounded-md border border-transparent bg-indigo-100 px-3 py-2 text-sm leading-4 font-medium text-indigo-700 hover:bg-indigo-200 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none"
232 + >
233 + Try Again
234 + </button>
235 + </div>
236 +
237 + <ul v-else-if="filteredCases.length > 0" role="list" class="divide-y divide-gray-200">
238 + <li v-for="case_ in filteredCases" :key="case_.id" class="px-4 py-4 hover:bg-gray-50 sm:px-6">
239 + <div class="flex items-center justify-between">
240 + <div class="flex items-center">
241 + <div class="flex-shrink-0">
242 + <span
243 + class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
244 + :class="{
245 + 'bg-red-100 text-red-800': case_.case_status?.toLowerCase() === 'open',
246 + 'bg-yellow-100 text-yellow-800':
247 + case_.case_status?.toLowerCase() === 'in_progress',
248 + 'bg-green-100 text-green-800': case_.case_status?.toLowerCase() === 'closed'
249 + }"
250 + >
251 + {{ case_.case_status?.replace("_", " ").toUpperCase() }}
252 + </span>
253 + </div>
254 + <div class="ml-4">
255 + <div class="text-sm font-medium text-gray-900">
256 + {{ case_.case_name }}
257 + </div>
258 + <div class="text-sm text-gray-500">
259 + Case #{{ case_.id }}
260 + <span v-if="case_.assigned_to">| Assigned to: {{ case_.assigned_to }}</span>
261 + </div>
262 + <div class="text-xs text-gray-400">
263 + Created: {{ formatDate(case_.case_creation_time) }}
264 + </div>
265 + </div>
266 + </div>
267 + <div class="flex items-center space-x-2">
268 + <select
269 + :value="case_.case_status"
270 + @change="updateCaseStatus(case_.id, ($event.target as HTMLSelectElement).value)"
271 + class="rounded-md border-gray-300 text-sm focus:border-indigo-500 focus:ring-indigo-500"
272 + :disabled="updatingStatus === case_.id"
273 + >
274 + <option value="open">Open</option>
275 + <option value="in_progress">In Progress</option>
276 + <option value="closed">Closed</option>
277 + </select>
278 + <button
279 + @click="viewCase(case_)"
280 + class="inline-flex items-center rounded border border-transparent bg-indigo-100 px-3 py-1 text-xs font-medium text-indigo-700 hover:bg-indigo-200 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none"
281 + >
282 + View Details
283 + </button>
284 + </div>
285 + </div>
286 + <div v-if="case_.case_description" class="mt-2 text-sm text-gray-600">
287 + {{ case_.case_description }}
288 + </div>
289 + <div v-if="case_.alert_ids && case_.alert_ids.length > 0" class="mt-2">
290 + <span class="text-xs text-gray-500">Linked Alerts: {{ case_.alert_ids.length }}</span>
291 + </div>
292 + </li>
293 + </ul>
294 +
295 + <div v-else class="px-4 py-5 text-center sm:p-6">
296 + <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
297 + <path
298 + stroke-linecap="round"
299 + stroke-linejoin="round"
300 + stroke-width="2"
301 + d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
302 + ></path>
303 + </svg>
304 + <h3 class="mt-2 text-sm font-medium text-gray-900">No cases found</h3>
305 + <p class="mt-1 text-sm text-gray-500">No security cases match your current filters.</p>
306 + </div>
307 + </div>
308 + </div>
309 +
310 + <!-- Case Details Modal -->
311 + <div
312 + v-if="selectedCase"
313 + class="bg-opacity-50 fixed inset-0 z-50 h-full w-full overflow-y-auto bg-gray-600"
314 + @click="closeModal"
315 + >
316 + <div
317 + class="relative top-20 mx-auto w-11/12 rounded-md border bg-white p-5 shadow-lg md:w-3/4 lg:w-1/2"
318 + @click.stop
319 + >
320 + <div class="mb-4 flex items-center justify-between">
321 + <h3 class="text-lg font-medium text-gray-900">Case Details</h3>
322 + <button @click="closeModal" class="text-gray-400 hover:text-gray-600">
323 + <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
324 + <path
325 + stroke-linecap="round"
326 + stroke-linejoin="round"
327 + stroke-width="2"
328 + d="M6 18L18 6M6 6l12 12"
329 + ></path>
330 + </svg>
331 + </button>
332 + </div>
333 +
334 + <div class="space-y-4">
335 + <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
336 + <div>
337 + <label class="block text-sm font-medium text-gray-700">Case Name</label>
338 + <p class="mt-1 text-sm text-gray-900">{{ selectedCase.case_name }}</p>
339 + </div>
340 + <div>
341 + <label class="block text-sm font-medium text-gray-700">Status</label>
342 + <span
343 + class="mt-1 inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
344 + :class="{
345 + 'bg-red-100 text-red-800': selectedCase.case_status?.toLowerCase() === 'open',
346 + 'bg-yellow-100 text-yellow-800':
347 + selectedCase.case_status?.toLowerCase() === 'in_progress',
348 + 'bg-green-100 text-green-800': selectedCase.case_status?.toLowerCase() === 'closed'
349 + }"
350 + >
351 + {{ selectedCase.case_status?.replace("_", " ").toUpperCase() }}
352 + </span>
353 + </div>
354 + <div>
355 + <label class="block text-sm font-medium text-gray-700">Case ID</label>
356 + <p class="mt-1 text-sm text-gray-900">#{{ selectedCase.id }}</p>
357 + </div>
358 + <div>
359 + <label class="block text-sm font-medium text-gray-700">Customer</label>
360 + <p class="mt-1 text-sm text-gray-900">{{ selectedCase.customer_code }}</p>
361 + </div>
362 + <div>
363 + <label class="block text-sm font-medium text-gray-700">Assigned To</label>
364 + <p class="mt-1 text-sm text-gray-900">{{ selectedCase.assigned_to || "Unassigned" }}</p>
365 + </div>
366 + <div>
367 + <label class="block text-sm font-medium text-gray-700">Created</label>
368 + <p class="mt-1 text-sm text-gray-900">{{ formatDate(selectedCase.case_creation_time) }}</p>
369 + </div>
370 + </div>
371 +
372 + <div v-if="selectedCase.case_description">
373 + <label class="block text-sm font-medium text-gray-700">Description</label>
374 + <p class="mt-1 text-sm whitespace-pre-wrap text-gray-900">
375 + {{ selectedCase.case_description }}
376 + </p>
377 + </div>
378 +
379 + <div v-if="selectedCase.alert_ids && selectedCase.alert_ids.length > 0">
380 + <label class="block text-sm font-medium text-gray-700">Linked Alerts</label>
381 + <div class="mt-1 flex flex-wrap gap-2">
382 + <span
383 + v-for="alertId in selectedCase.alert_ids"
384 + :key="alertId"
385 + class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800"
386 + >
387 + Alert #{{ alertId }}
388 + </span>
389 + </div>
390 + </div>
391 +
392 + <div v-if="selectedCase.alerts && selectedCase.alerts.length > 0">
393 + <div class="mb-2 flex items-center justify-between">
394 + <label class="block text-sm font-medium text-gray-700">Alert Details</label>
395 + <span class="text-xs text-gray-500">Click alerts to view details</span>
396 + </div>
397 + <div class="mt-1 space-y-2">
398 + <div
399 + v-for="alert in selectedCase.alerts"
400 + :key="alert.id"
401 + class="cursor-pointer rounded-md border bg-gray-50 p-3 transition-colors hover:bg-gray-100"
402 + @click="viewAlert(alert.id)"
403 + >
404 + <div class="flex items-start justify-between">
405 + <div class="flex-1">
406 + <div class="flex items-center space-x-2">
407 + <p class="text-sm font-medium text-gray-900">{{ alert.alert_name }}</p>
408 + <svg
409 + class="h-4 w-4 text-gray-400"
410 + fill="none"
411 + stroke="currentColor"
412 + viewBox="0 0 24 24"
413 + >
414 + <path
415 + stroke-linecap="round"
416 + stroke-linejoin="round"
417 + stroke-width="2"
418 + d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-2M7 7l10 10M17 7v4h-4"
419 + ></path>
420 + </svg>
421 + </div>
422 + <p class="text-xs text-gray-500">Asset: {{ alert.asset_name }}</p>
423 + </div>
424 + <span
425 + class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium"
426 + :class="{
427 + 'bg-red-100 text-red-800': alert.status === 'OPEN',
428 + 'bg-yellow-100 text-yellow-800': alert.status === 'IN_PROGRESS',
429 + 'bg-green-100 text-green-800': alert.status === 'CLOSED'
430 + }"
431 + >
432 + {{ alert.status.replace("_", " ").toUpperCase() }}
433 + </span>
434 + </div>
435 + </div>
436 + </div>
437 + </div>
438 +
439 + <!-- Case Files Section -->
440 + <div>
441 + <div class="mb-2 flex items-center justify-between">
442 + <label class="block text-sm font-medium text-gray-700">
443 + Case Files
444 + <span v-if="caseFiles.length > 0" class="font-normal text-gray-500">
445 + ({{ caseFiles.length }})
446 + </span>
447 + </label>
448 + <div class="flex items-center space-x-2">
449 + <button
450 + @click="openUploadForm"
451 + class="inline-flex items-center rounded border border-transparent bg-indigo-100 px-2 py-1 text-xs font-medium text-indigo-700 hover:bg-indigo-200 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none"
452 + >
453 + <svg class="mr-1 h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
454 + <path
455 + stroke-linecap="round"
456 + stroke-linejoin="round"
457 + stroke-width="2"
458 + d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
459 + ></path>
460 + </svg>
461 + Upload File
462 + </button>
463 + <button
464 + v-if="!loadingFiles"
465 + @click="loadCaseFiles(selectedCase.id)"
466 + class="text-xs text-indigo-600 hover:text-indigo-500 focus:outline-none"
467 + >
468 + <svg
469 + class="mr-1 inline h-4 w-4"
470 + fill="none"
471 + stroke="currentColor"
472 + viewBox="0 0 24 24"
473 + >
474 + <path
475 + stroke-linecap="round"
476 + stroke-linejoin="round"
477 + stroke-width="2"
478 + d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
479 + ></path>
480 + </svg>
481 + Refresh
482 + </button>
483 + </div>
484 + </div>
485 +
486 + <!-- Upload Form -->
487 + <div v-if="showUploadForm" class="mb-4 rounded-lg border border-blue-200 bg-blue-50 p-4">
488 + <div class="mb-3 flex items-center justify-between">
489 + <h4 class="text-sm font-medium text-blue-900">Upload File to Case</h4>
490 + <button @click="closeUploadForm" class="text-blue-400 hover:text-blue-600">
491 + <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
492 + <path
493 + stroke-linecap="round"
494 + stroke-linejoin="round"
495 + stroke-width="2"
496 + d="M6 18L18 6M6 6l12 12"
497 + ></path>
498 + </svg>
499 + </button>
500 + </div>
501 +
502 + <div class="space-y-3">
503 + <div>
504 + <input
505 + ref="fileInput"
506 + type="file"
507 + @change="handleFileSelect"
508 + class="block w-full text-sm text-gray-500 file:mr-4 file:rounded-full file:border-0 file:bg-indigo-50 file:px-4 file:py-2 file:text-sm file:font-semibold file:text-indigo-700 hover:file:bg-indigo-100 focus:outline-none"
509 + />
510 + </div>
511 +
512 + <div v-if="selectedFile" class="text-sm text-gray-600">
513 + Selected: {{ selectedFile.name }} ({{
514 + CaseDataStoreAPI.formatFileSize(selectedFile.size)
515 + }})
516 + </div>
517 +
518 + <!-- Error message for upload -->
519 + <div
520 + v-if="error && showUploadForm"
521 + class="rounded-md border border-red-200 bg-red-50 p-2 text-sm text-red-600"
522 + >
523 + {{ error }}
524 + </div>
525 +
526 + <div class="flex justify-end space-x-2">
527 + <button
528 + @click="closeUploadForm"
529 + type="button"
530 + class="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none"
531 + >
532 + Cancel
533 + </button>
534 + <button
535 + @click="uploadFile"
536 + :disabled="!selectedFile || uploadingFile"
537 + class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
538 + >
539 + <svg
540 + v-if="uploadingFile"
541 + class="mr-2 -ml-1 h-4 w-4 animate-spin text-white"
542 + xmlns="http://www.w3.org/2000/svg"
543 + fill="none"
544 + viewBox="0 0 24 24"
545 + >
546 + <circle
547 + class="opacity-25"
548 + cx="12"
549 + cy="12"
550 + r="10"
551 + stroke="currentColor"
552 + stroke-width="4"
553 + ></circle>
554 + <path
555 + class="opacity-75"
556 + fill="currentColor"
557 + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
558 + ></path>
559 + </svg>
560 + {{ uploadingFile ? "Uploading..." : "Upload File" }}
561 + </button>
562 + </div>
563 + </div>
564 + </div>
565 +
566 + <!-- Loading Files -->
567 + <div v-if="loadingFiles" class="rounded-lg bg-gray-50 p-4 text-center">
568 + <div class="mx-auto h-6 w-6 animate-spin rounded-full border-b-2 border-indigo-600"></div>
569 + <p class="mt-2 text-sm text-gray-500">Loading files...</p>
570 + </div>
571 +
572 + <!-- Files List -->
573 + <div
574 + v-else-if="caseFiles.length > 0"
575 + class="max-h-64 overflow-y-auto rounded-lg bg-gray-50 p-4"
576 + >
577 + <div
578 + v-for="file in caseFiles"
579 + :key="file.id"
580 + class="mb-3 border-b border-gray-200 pb-3 last:mb-0 last:border-b-0"
581 + >
582 + <div class="flex items-start justify-between">
583 + <div class="min-w-0 flex-1">
584 + <div class="flex items-center space-x-2">
585 + <svg
586 + class="h-4 w-4 flex-shrink-0 text-gray-500"
587 + fill="none"
588 + stroke="currentColor"
589 + viewBox="0 0 24 24"
590 + >
591 + <path
592 + stroke-linecap="round"
593 + stroke-linejoin="round"
594 + stroke-width="2"
595 + d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
596 + ></path>
597 + </svg>
598 + <p class="truncate text-sm font-medium text-gray-900">
599 + {{ file.file_name }}
600 + </p>
601 + </div>
602 + <div class="mt-1 flex items-center space-x-4 text-xs text-gray-500">
603 + <span>{{ CaseDataStoreAPI.formatFileSize(file.file_size) }}</span>
604 + <span v-if="file.content_type">{{ file.content_type }}</span>
605 + <span>{{ formatDate(file.upload_time) }}</span>
606 + </div>
607 + </div>
608 + <button
609 + @click="downloadFile(selectedCase.id, file.file_name)"
610 + :disabled="downloadingFile === file.file_name"
611 + class="ml-3 inline-flex items-center rounded border border-transparent bg-indigo-100 px-2 py-1 text-xs font-medium text-indigo-700 hover:bg-indigo-200 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
612 + >
613 + <svg
614 + v-if="downloadingFile === file.file_name"
615 + class="mr-1 -ml-1 h-3 w-3 animate-spin text-indigo-700"
616 + xmlns="http://www.w3.org/2000/svg"
617 + fill="none"
618 + viewBox="0 0 24 24"
619 + >
620 + <circle
621 + class="opacity-25"
622 + cx="12"
623 + cy="12"
624 + r="10"
625 + stroke="currentColor"
626 + stroke-width="4"
627 + ></circle>
628 + <path
629 + class="opacity-75"
630 + fill="currentColor"
631 + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
632 + ></path>
633 + </svg>
634 + <svg
635 + v-else
636 + class="mr-1 h-3 w-3"
637 + fill="none"
638 + stroke="currentColor"
639 + viewBox="0 0 24 24"
640 + >
641 + <path
642 + stroke-linecap="round"
643 + stroke-linejoin="round"
644 + stroke-width="2"
645 + d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
646 + ></path>
647 + </svg>
648 + {{ downloadingFile === file.file_name ? "Downloading..." : "Download" }}
649 + </button>
650 + </div>
651 + </div>
652 + </div>
653 +
654 + <!-- No Files Message -->
655 + <div v-else class="rounded-lg bg-gray-50 p-4 text-center">
656 + <svg
657 + class="mx-auto h-8 w-8 text-gray-400"
658 + fill="none"
659 + stroke="currentColor"
660 + viewBox="0 0 24 24"
661 + >
662 + <path
663 + stroke-linecap="round"
664 + stroke-linejoin="round"
665 + stroke-width="2"
666 + d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
667 + ></path>
668 + </svg>
669 + <p class="mt-2 text-sm text-gray-500">No files available for this case</p>
670 + </div>
671 + </div>
672 + </div>
673 + </div>
674 + </div>
675 +
676 + <!-- Alert Details Modal -->
677 + <div
678 + v-if="selectedAlert"
679 + class="bg-opacity-50 fixed inset-0 z-50 h-full w-full overflow-y-auto bg-gray-600"
680 + @click="closeAlertModal"
681 + >
682 + <div
683 + class="relative top-10 mx-auto max-h-screen w-11/12 overflow-y-auto rounded-md border bg-white p-5 shadow-lg md:w-4/5 lg:w-3/4"
684 + @click.stop
685 + >
686 + <div class="mb-4 flex items-center justify-between">
687 + <h3 class="text-lg font-medium text-gray-900">Alert Details</h3>
688 + <button @click="closeAlertModal" class="text-gray-400 hover:text-gray-600">
689 + <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
690 + <path
691 + stroke-linecap="round"
692 + stroke-linejoin="round"
693 + stroke-width="2"
694 + d="M6 18L18 6M6 6l12 12"
695 + ></path>
696 + </svg>
697 + </button>
698 + </div>
699 +
700 + <div class="space-y-6">
701 + <!-- Basic Alert Information -->
702 + <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
703 + <div>
704 + <label class="block text-sm font-medium text-gray-700">Alert Name</label>
705 + <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.alert_name }}</p>
706 + </div>
707 + <div>
708 + <label class="block text-sm font-medium text-gray-700">Status</label>
709 + <span
710 + class="mt-1 inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
711 + :class="{
712 + 'bg-red-100 text-red-800': selectedAlert.status === 'OPEN',
713 + 'bg-yellow-100 text-yellow-800': selectedAlert.status === 'IN_PROGRESS',
714 + 'bg-green-100 text-green-800': selectedAlert.status === 'CLOSED'
715 + }"
716 + >
717 + {{ selectedAlert.status.replace("_", " ") }}
718 + </span>
719 + </div>
720 + <div>
721 + <label class="block text-sm font-medium text-gray-700">Source</label>
722 + <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.source }}</p>
723 + </div>
724 + <div>
725 + <label class="block text-sm font-medium text-gray-700">Customer</label>
726 + <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.customer_code }}</p>
727 + </div>
728 + <div>
729 + <label class="block text-sm font-medium text-gray-700">Created</label>
730 + <p class="mt-1 text-sm text-gray-900">
731 + {{ formatDate(selectedAlert.alert_creation_time) }}
732 + </p>
733 + </div>
734 + <div v-if="selectedAlert.assigned_to">
735 + <label class="block text-sm font-medium text-gray-700">Assigned To</label>
736 + <p class="mt-1 text-sm text-gray-900">{{ selectedAlert.assigned_to }}</p>
737 + </div>
738 + </div>
739 +
740 + <div v-if="selectedAlert.alert_description">
741 + <label class="block text-sm font-medium text-gray-700">Description</label>
742 + <p class="mt-1 text-sm whitespace-pre-wrap text-gray-900">
743 + {{ selectedAlert.alert_description }}
744 + </p>
745 + </div>
746 +
747 + <!-- Assets Section -->
748 + <div v-if="selectedAlert.assets && selectedAlert.assets.length > 0">
749 + <label class="mb-2 block text-sm font-medium text-gray-700">Assets</label>
750 + <div class="rounded-lg bg-gray-50 p-4">
751 + <div
752 + v-for="asset in selectedAlert.assets"
753 + :key="asset.id"
754 + class="mb-3 border-b border-gray-200 pb-3 last:mb-0 last:border-b-0"
755 + >
756 + <div class="grid grid-cols-1 gap-2 text-sm md:grid-cols-3">
757 + <div>
758 + <span class="font-medium">Asset Name:</span>
759 + {{ asset.asset_name }}
760 + </div>
761 + <div>
762 + <span class="font-medium">Agent ID:</span>
763 + {{ asset.agent_id }}
764 + </div>
765 + <div v-if="asset.velociraptor_id">
766 + <span class="font-medium">Velociraptor ID:</span>
767 + {{ asset.velociraptor_id }}
768 + </div>
769 + <div>
770 + <span class="font-medium">Index:</span>
771 + {{ asset.index_name }}
772 + </div>
773 + <div>
774 + <span class="font-medium">Index ID:</span>
775 + {{ asset.index_id.substring(0, 20) }}...
776 + </div>
777 + </div>
778 + </div>
779 + </div>
780 + </div>
781 +
782 + <!-- Tags Section -->
783 + <div v-if="selectedAlert.tags && selectedAlert.tags.length > 0">
784 + <label class="block text-sm font-medium text-gray-700">Tags</label>
785 + <div class="mt-1 flex flex-wrap gap-2">
786 + <span
787 + v-for="tag in selectedAlert.tags"
788 + :key="tag.id"
789 + class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800"
790 + >
791 + {{ tag.tag }}
792 + </span>
793 + </div>
794 + </div>
795 +
796 + <!-- Linked Cases Section -->
797 + <div v-if="selectedAlert.linked_cases && selectedAlert.linked_cases.length > 0">
798 + <label class="mb-2 block text-sm font-medium text-gray-700">Linked Cases</label>
799 + <div class="rounded-lg bg-gray-50 p-4">
800 + <div
801 + v-for="linkedCase in selectedAlert.linked_cases"
802 + :key="linkedCase.id"
803 + class="mb-3 border-b border-gray-200 pb-3 last:mb-0 last:border-b-0"
804 + >
805 + <div class="flex items-start justify-between">
806 + <div class="flex-1">
807 + <h4 class="text-sm font-medium text-gray-900">{{ linkedCase.case_name }}</h4>
808 + <p class="mt-1 text-xs text-gray-600">{{ linkedCase.case_description }}</p>
809 + <div class="mt-2 flex items-center space-x-4 text-xs text-gray-500">
810 + <span>Case #{{ linkedCase.id }}</span>
811 + <span>Created: {{ formatDate(linkedCase.case_creation_time) }}</span>
812 + <span v-if="linkedCase.assigned_to">
813 + Assigned to: {{ linkedCase.assigned_to }}
814 + </span>
815 + </div>
816 + </div>
817 + <span
818 + class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium"
819 + :class="{
820 + 'bg-red-100 text-red-800': linkedCase.case_status === 'OPEN',
821 + 'bg-yellow-100 text-yellow-800': linkedCase.case_status === 'IN_PROGRESS',
822 + 'bg-green-100 text-green-800': linkedCase.case_status === 'CLOSED'
823 + }"
824 + >
825 + {{ linkedCase.case_status.replace("_", " ") }}
826 + </span>
827 + </div>
828 + </div>
829 + </div>
830 + </div>
831 +
832 + <!-- IoCs Section -->
833 + <div v-if="selectedAlert.iocs && selectedAlert.iocs.length > 0">
834 + <label class="mb-2 block text-sm font-medium text-gray-700">
835 + Indicators of Compromise (IoCs)
836 + </label>
837 + <div class="rounded-lg bg-gray-50 p-4">
838 + <div
839 + v-for="ioc in selectedAlert.iocs"
840 + :key="ioc.id"
841 + class="mb-3 border-b border-gray-200 pb-3 last:mb-0 last:border-b-0"
842 + >
843 + <div class="grid grid-cols-1 gap-2 text-sm md:grid-cols-3">
844 + <div>
845 + <span class="font-medium">Value:</span>
846 + <code class="rounded bg-gray-100 px-1 text-xs">{{ ioc.ioc_value }}</code>
847 + </div>
848 + <div>
849 + <span class="font-medium">Type:</span>
850 + {{ ioc.ioc_type }}
851 + </div>
852 + <div>
853 + <span class="font-medium">Description:</span>
854 + {{ ioc.ioc_description }}
855 + </div>
856 + </div>
857 + </div>
858 + </div>
859 + </div>
860 +
861 + <!-- Comments Section -->
862 + <div v-if="selectedAlert.comments && selectedAlert.comments.length > 0">
863 + <label class="mb-2 block text-sm font-medium text-gray-700">
864 + Comments ({{ selectedAlert.comments.length }})
865 + </label>
866 + <div class="max-h-64 overflow-y-auto rounded-lg bg-gray-50 p-4">
867 + <div
868 + v-for="comment in selectedAlert.comments"
869 + :key="comment.id"
870 + class="mb-3 border-b border-gray-200 pb-3 last:mb-0 last:border-b-0"
871 + >
872 + <div class="mb-2 flex items-start justify-between">
873 + <span class="text-sm font-medium text-gray-900">{{ comment.user_name }}</span>
874 + <span class="text-xs text-gray-500">{{ formatDate(comment.created_at) }}</span>
875 + </div>
876 + <p class="text-sm whitespace-pre-wrap text-gray-700">{{ comment.comment }}</p>
877 + </div>
878 + </div>
879 + </div>
880 + </div>
881 + </div>
882 + </div>
883 + </div>
884 </template>
885
886 <script setup lang="ts">
678 -import { ref, onMounted, computed } from 'vue'
679 -import { useRouter } from 'vue-router'
680 -import CasesAPI, { type Case, type CasesResponse } from '@/api/cases'
681 -import CaseDataStoreAPI, { type CaseDataStoreFile } from '@/api/caseDataStore'
682 -import AlertsAPI, { type Alert } from '@/api/alerts'
887 +import { ref, onMounted, computed } from "vue"
888 +import { useRouter } from "vue-router"
889 +import { usePortalSettingsStore } from "@/stores/portalSettings"
890 +import CasesAPI, { type Case, type CasesResponse } from "@/api/cases"
891 +import CaseDataStoreAPI, { type CaseDataStoreFile } from "@/api/caseDataStore"
892 +import AlertsAPI, { type Alert } from "@/api/alerts"
893
894 const router = useRouter()
895 +const portalSettingsStore = usePortalSettingsStore()
896
897 // Reactive data
898 const cases = ref<Case[]>([])
899 const loading = ref(false)
900 const error = ref<string | null>(null)
901 +const showLogo = ref(true)
902 const selectedCase = ref<Case | null>(null)
903 const selectedAlert = ref<Alert | null>(null)
904 const updatingStatus = ref<number | null>(null)
@@ -704,223 +916,230 @@ const fileInput = ref<HTMLInputElement | null>(null)
916
917 // Filters
918 const filters = ref({
707 - status: '',
708 - assignedTo: ''
919 + status: "",
920 + assignedTo: ""
921 })
922
923 // Computed properties
712 -const openCases = computed(() => cases.value.filter(c => c.case_status?.toLowerCase() === 'open').length)
713 -const inProgressCases = computed(() => cases.value.filter(c => c.case_status?.toLowerCase() === 'in_progress').length)
714 -const closedCases = computed(() => cases.value.filter(c => c.case_status?.toLowerCase() === 'closed').length)
924 +const portalTitle = computed(() => portalSettingsStore.portalTitle || "Customer Portal")
925 +const portalLogo = computed(() => portalSettingsStore.portalLogo)
926 +
927 +const openCases = computed(() => cases.value.filter(c => c.case_status?.toLowerCase() === "open").length)
928 +const inProgressCases = computed(() => cases.value.filter(c => c.case_status?.toLowerCase() === "in_progress").length)
929 +const closedCases = computed(() => cases.value.filter(c => c.case_status?.toLowerCase() === "closed").length)
930
931 const availableAssignees = computed(() => {
717 - const assignees = new Set(cases.value.map(c => c.assigned_to).filter((assignee): assignee is string => assignee !== null))
718 - return Array.from(assignees).sort()
932 + const assignees = new Set(
933 + cases.value.map(c => c.assigned_to).filter((assignee): assignee is string => assignee !== null)
934 + )
935 + return Array.from(assignees).sort()
936 })
937
938 const filteredCases = computed(() => {
722 - let filtered = cases.value
939 + let filtered = cases.value
940
724 - if (filters.value.status) {
725 - filtered = filtered.filter(c => c.case_status?.toLowerCase() === filters.value.status.toLowerCase())
726 - }
941 + if (filters.value.status) {
942 + filtered = filtered.filter(c => c.case_status?.toLowerCase() === filters.value.status.toLowerCase())
943 + }
944
728 - if (filters.value.assignedTo) {
729 - filtered = filtered.filter(c => c.assigned_to === filters.value.assignedTo)
730 - }
945 + if (filters.value.assignedTo) {
946 + filtered = filtered.filter(c => c.assigned_to === filters.value.assignedTo)
947 + }
948
732 - return filtered.sort((a, b) => new Date(b.case_creation_time).getTime() - new Date(a.case_creation_time).getTime())
949 + return filtered.sort((a, b) => new Date(b.case_creation_time).getTime() - new Date(a.case_creation_time).getTime())
950 })
951
952 // Methods
953 const goBack = () => {
737 - router.push('/')
954 + router.push("/")
955 }
956
957 const loadCases = async () => {
741 - loading.value = true
742 - error.value = null
743 -
744 - try {
745 - let response: CasesResponse
746 -
747 - if (filters.value.status) {
748 - // Convert lowercase filter to uppercase for backend API
749 - const backendStatus = filters.value.status.toUpperCase() as any
750 - response = await CasesAPI.getCasesByStatus(backendStatus)
751 - } else if (filters.value.assignedTo) {
752 - response = await CasesAPI.getCasesByAssignedTo(filters.value.assignedTo)
753 - } else {
754 - response = await CasesAPI.getCases()
755 - }
756 -
757 - cases.value = response.cases
758 - console.log('Loaded cases:', response.cases)
759 - console.log('Case statuses:', response.cases.map(c => c.case_status))
760 - } catch (err: any) {
761 - error.value = err.response?.data?.detail || err.message || 'Failed to load cases'
762 - console.error('Error loading cases:', err)
763 - } finally {
764 - loading.value = false
765 - }
958 + loading.value = true
959 + error.value = null
960 +
961 + try {
962 + let response: CasesResponse
963 +
964 + if (filters.value.status) {
965 + // Convert lowercase filter to uppercase for backend API
966 + const backendStatus = filters.value.status.toUpperCase() as any
967 + response = await CasesAPI.getCasesByStatus(backendStatus)
968 + } else if (filters.value.assignedTo) {
969 + response = await CasesAPI.getCasesByAssignedTo(filters.value.assignedTo)
970 + } else {
971 + response = await CasesAPI.getCases()
972 + }
973 +
974 + cases.value = response.cases
975 + console.log("Loaded cases:", response.cases)
976 + console.log(
977 + "Case statuses:",
978 + response.cases.map(c => c.case_status)
979 + )
980 + } catch (err: any) {
981 + error.value = err.response?.data?.detail || err.message || "Failed to load cases"
982 + console.error("Error loading cases:", err)
983 + } finally {
984 + loading.value = false
985 + }
986 }
987
988 const refreshCases = () => {
769 - loadCases()
989 + loadCases()
990 }
991
992 const applyFilters = () => {
773 - // Since we use computed filteredCases, we don't need to reload from API for local filtering
774 - // But if we want to filter on the server side, we can call loadCases()
775 - loadCases()
993 + // Since we use computed filteredCases, we don't need to reload from API for local filtering
994 + // But if we want to filter on the server side, we can call loadCases()
995 + loadCases()
996 }
997
998 const updateCaseStatus = async (caseId: number, newStatus: string) => {
779 - updatingStatus.value = caseId
780 -
781 - try {
782 - await CasesAPI.updateCaseStatus(caseId, newStatus as any)
783 -
784 - // Update the local case status
785 - const case_ = cases.value.find(c => c.id === caseId)
786 - if (case_) {
787 - case_.case_status = newStatus as any
788 - }
789 - } catch (err: any) {
790 - error.value = err.response?.data?.detail || err.message || 'Failed to update case status'
791 - console.error('Error updating case status:', err)
792 - } finally {
793 - updatingStatus.value = null
794 - }
999 + updatingStatus.value = caseId
1000 +
1001 + try {
1002 + await CasesAPI.updateCaseStatus(caseId, newStatus as any)
1003 +
1004 + // Update the local case status
1005 + const case_ = cases.value.find(c => c.id === caseId)
1006 + if (case_) {
1007 + case_.case_status = newStatus as any
1008 + }
1009 + } catch (err: any) {
1010 + error.value = err.response?.data?.detail || err.message || "Failed to update case status"
1011 + console.error("Error updating case status:", err)
1012 + } finally {
1013 + updatingStatus.value = null
1014 + }
1015 }
1016
1017 const viewCase = (case_: Case) => {
798 - selectedCase.value = case_
799 - loadCaseFiles(case_.id)
1018 + selectedCase.value = case_
1019 + loadCaseFiles(case_.id)
1020 }
1021
1022 const loadCaseFiles = async (caseId: number) => {
803 - loadingFiles.value = true
804 - try {
805 - const response = await CaseDataStoreAPI.getCaseFiles(caseId)
806 - caseFiles.value = response.case_data_store
807 - } catch (err: any) {
808 - console.error('Error loading case files:', err)
809 - caseFiles.value = []
810 - } finally {
811 - loadingFiles.value = false
812 - }
1023 + loadingFiles.value = true
1024 + try {
1025 + const response = await CaseDataStoreAPI.getCaseFiles(caseId)
1026 + caseFiles.value = response.case_data_store
1027 + } catch (err: any) {
1028 + console.error("Error loading case files:", err)
1029 + caseFiles.value = []
1030 + } finally {
1031 + loadingFiles.value = false
1032 + }
1033 }
1034
1035 const downloadFile = async (caseId: number, fileName: string) => {
816 - downloadingFile.value = fileName
817 - try {
818 - const blob = await CaseDataStoreAPI.downloadCaseFile(caseId, fileName)
819 - CaseDataStoreAPI.downloadFileBlob(blob, fileName)
820 - } catch (err: any) {
821 - console.error('Error downloading file:', err)
822 - error.value = err.response?.data?.detail || err.message || 'Failed to download file'
823 - } finally {
824 - downloadingFile.value = null
825 - }
1036 + downloadingFile.value = fileName
1037 + try {
1038 + const blob = await CaseDataStoreAPI.downloadCaseFile(caseId, fileName)
1039 + CaseDataStoreAPI.downloadFileBlob(blob, fileName)
1040 + } catch (err: any) {
1041 + console.error("Error downloading file:", err)
1042 + error.value = err.response?.data?.detail || err.message || "Failed to download file"
1043 + } finally {
1044 + downloadingFile.value = null
1045 + }
1046 }
1047
1048 const openUploadForm = () => {
829 - showUploadForm.value = true
830 - selectedFile.value = null
831 - error.value = null
1049 + showUploadForm.value = true
1050 + selectedFile.value = null
1051 + error.value = null
1052 }
1053
1054 const closeUploadForm = () => {
835 - showUploadForm.value = false
836 - selectedFile.value = null
837 - if (fileInput.value) {
838 - fileInput.value.value = ''
839 - }
1055 + showUploadForm.value = false
1056 + selectedFile.value = null
1057 + if (fileInput.value) {
1058 + fileInput.value.value = ""
1059 + }
1060 }
1061
1062 const handleFileSelect = (event: Event) => {
843 - const target = event.target as HTMLInputElement
844 - if (target.files && target.files.length > 0) {
845 - const file = target.files[0]
846 - // Check file size (e.g., limit to 50MB)
847 - const maxSize = 50 * 1024 * 1024 // 50MB in bytes
848 - if (file.size > maxSize) {
849 - error.value = 'File size too large. Maximum size is 50MB.'
850 - selectedFile.value = null
851 - return
852 - }
853 - selectedFile.value = file
854 - error.value = null // Clear any previous errors
855 - }
1063 + const target = event.target as HTMLInputElement
1064 + if (target.files && target.files.length > 0) {
1065 + const file = target.files[0]
1066 + // Check file size (e.g., limit to 50MB)
1067 + const maxSize = 50 * 1024 * 1024 // 50MB in bytes
1068 + if (file.size > maxSize) {
1069 + error.value = "File size too large. Maximum size is 50MB."
1070 + selectedFile.value = null
1071 + return
1072 + }
1073 + selectedFile.value = file
1074 + error.value = null // Clear any previous errors
1075 + }
1076 }
1077
1078 const uploadFile = async () => {
859 - if (!selectedFile.value || !selectedCase.value) return
860 -
861 - uploadingFile.value = true
862 - error.value = null
863 -
864 - try {
865 - await CaseDataStoreAPI.uploadCaseFile(selectedCase.value.id, selectedFile.value)
866 -
867 - // Refresh the files list
868 - await loadCaseFiles(selectedCase.value.id)
869 -
870 - // Close the upload form
871 - closeUploadForm()
872 -
873 - // You could add a success message here if you have a toast/notification system
874 - console.log('File uploaded successfully!')
875 -
876 - } catch (err: any) {
877 - console.error('Error uploading file:', err)
878 -
879 - // Extract more specific error messages
880 - let errorMessage = 'Failed to upload file'
881 - if (err.response?.data?.detail) {
882 - if (typeof err.response.data.detail === 'string') {
883 - errorMessage = err.response.data.detail
884 - } else if (Array.isArray(err.response.data.detail)) {
885 - errorMessage = err.response.data.detail.map((e: any) => e.msg || e.message || e).join(', ')
886 - }
887 - } else if (err.message) {
888 - errorMessage = err.message
889 - }
890 -
891 - error.value = errorMessage
892 - } finally {
893 - uploadingFile.value = false
894 - }
1079 + if (!selectedFile.value || !selectedCase.value) return
1080 +
1081 + uploadingFile.value = true
1082 + error.value = null
1083 +
1084 + try {
1085 + await CaseDataStoreAPI.uploadCaseFile(selectedCase.value.id, selectedFile.value)
1086 +
1087 + // Refresh the files list
1088 + await loadCaseFiles(selectedCase.value.id)
1089 +
1090 + // Close the upload form
1091 + closeUploadForm()
1092 +
1093 + // You could add a success message here if you have a toast/notification system
1094 + console.log("File uploaded successfully!")
1095 + } catch (err: any) {
1096 + console.error("Error uploading file:", err)
1097 +
1098 + // Extract more specific error messages
1099 + let errorMessage = "Failed to upload file"
1100 + if (err.response?.data?.detail) {
1101 + if (typeof err.response.data.detail === "string") {
1102 + errorMessage = err.response.data.detail
1103 + } else if (Array.isArray(err.response.data.detail)) {
1104 + errorMessage = err.response.data.detail.map((e: any) => e.msg || e.message || e).join(", ")
1105 + }
1106 + } else if (err.message) {
1107 + errorMessage = err.message
1108 + }
1109 +
1110 + error.value = errorMessage
1111 + } finally {
1112 + uploadingFile.value = false
1113 + }
1114 }
1115
1116 const viewAlert = async (alertId: number) => {
898 - try {
899 - const response = await AlertsAPI.getAlert(alertId)
900 - selectedAlert.value = response.alerts[0] // AlertResponse contains alerts array
901 - } catch (err: any) {
902 - console.error('Error loading alert details:', err)
903 - error.value = err.response?.data?.detail || err.message || 'Failed to load alert details'
904 - }
1117 + try {
1118 + const response = await AlertsAPI.getAlert(alertId)
1119 + selectedAlert.value = response.alerts[0] // AlertResponse contains alerts array
1120 + } catch (err: any) {
1121 + console.error("Error loading alert details:", err)
1122 + error.value = err.response?.data?.detail || err.message || "Failed to load alert details"
1123 + }
1124 }
1125
1126 const closeAlertModal = () => {
908 - selectedAlert.value = null
1127 + selectedAlert.value = null
1128 }
1129
1130 const closeModal = () => {
912 - selectedCase.value = null
913 - caseFiles.value = []
914 - closeUploadForm()
915 - closeAlertModal()
1131 + selectedCase.value = null
1132 + caseFiles.value = []
1133 + closeUploadForm()
1134 + closeAlertModal()
1135 }
1136
1137 const formatDate = (dateString: string) => {
919 - return new Date(dateString).toLocaleString()
1138 + return new Date(dateString).toLocaleString()
1139 }
1140
1141 // Lifecycle
1142 onMounted(() => {
924 - loadCases()
1143 + loadCases()
1144 })
1145 </script>
customer_portal/src/views/OverviewPage.vue
+681 -541
@@ -1,593 +1,733 @@
1 <template>
2 - <div class="min-h-screen bg-gray-50">
3 - <!-- Header -->
4 - <header class="bg-white shadow-sm border-b">
5 - <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
6 - <div class="flex justify-between h-16">
7 - <div class="flex items-center">
8 - <img
9 - class="h-8 w-auto mr-3"
10 - src="/logo.svg"
11 - alt="SOCFortress Logo"
12 - />
13 - <h1 class="text-xl font-semibold text-gray-900">Customer Portal</h1>
14 - <nav class="ml-8 flex space-x-8">
15 - <router-link
16 - to="/"
17 - class="text-indigo-600 border-b-2 border-indigo-600 px-3 py-2 rounded-md text-sm font-medium"
18 - >
19 - Overview
20 - </router-link>
21 - <router-link
22 - to="/alerts"
23 - class="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
24 - >
25 - Alerts
26 - </router-link>
27 - <router-link
28 - to="/cases"
29 - class="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
30 - >
31 - Cases
32 - </router-link>
33 - <router-link
34 - to="/agents"
35 - class="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
36 - >
37 - Agents
38 - </router-link>
39 - </nav>
40 - </div>
41 - <div class="flex items-center space-x-4">
42 - <div class="text-sm text-gray-700">
43 - Welcome, <span class="font-medium">{{ username }}</span>
44 - </div>
45 - <button
46 - @click="logout"
47 - class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors"
48 - >
49 - Logout
50 - </button>
51 - </div>
52 - </div>
53 - </div>
54 - </header>
55 -
56 - <!-- Main Content -->
57 - <main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
58 - <div class="px-4 py-6 sm:px-0">
59 - <!-- Welcome Section -->
60 - <div class="mb-8">
61 - <h2 class="text-2xl font-bold text-gray-900 mb-2">Security Overview</h2>
62 - <p class="text-gray-600">Monitor your organization's security posture and recent activity</p>
63 - </div>
64 -
65 - <!-- Loading State -->
66 - <div v-if="loading" class="flex justify-center items-center py-12">
67 - <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
68 - <span class="ml-3 text-gray-600">Loading dashboard...</span>
69 - </div>
70 -
71 - <!-- Error State -->
72 - <div v-else-if="error" class="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
73 - <div class="flex">
74 - <div class="flex-shrink-0">
75 - <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
76 - <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
77 - </svg>
78 - </div>
79 - <div class="ml-3">
80 - <h3 class="text-sm font-medium text-red-800">Error Loading Dashboard</h3>
81 - <div class="mt-2 text-sm text-red-700">{{ error }}</div>
82 - <div class="mt-3">
83 - <button
84 - @click="refreshData"
85 - class="bg-red-100 hover:bg-red-200 text-red-800 px-3 py-1 rounded text-sm font-medium"
86 - >
87 - Try Again
88 - </button>
89 - </div>
90 - </div>
91 - </div>
92 - </div>
93 -
94 - <!-- Dashboard Content -->
95 - <div v-else>
96 - <!-- Key Metrics Cards -->
97 - <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6 mb-8">
98 - <!-- Total Alerts -->
99 - <div class="bg-white overflow-hidden shadow-sm rounded-lg">
100 - <div class="p-6">
101 - <div class="flex items-center">
102 - <div class="flex-shrink-0">
103 - <div class="w-8 h-8 bg-red-500 rounded-md flex items-center justify-center">
104 - <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
105 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
106 - </svg>
107 - </div>
108 - </div>
109 - <div class="ml-5 w-0 flex-1">
110 - <dl>
111 - <dt class="text-sm font-medium text-gray-500 truncate">Total Alerts</dt>
112 - <dd class="flex items-baseline">
113 - <div class="text-2xl font-semibold text-gray-900">{{ stats.totalAlerts }}</div>
114 - <div class="ml-2 flex items-baseline text-sm font-semibold" :class="alertTrendClass">
115 - {{ stats.alertTrend }}
116 - </div>
117 - </dd>
118 - </dl>
119 - </div>
120 - </div>
121 - </div>
122 - </div>
123 -
124 - <!-- Critical Alerts -->
125 - <div class="bg-white overflow-hidden shadow-sm rounded-lg">
126 - <div class="p-6">
127 - <div class="flex items-center">
128 - <div class="flex-shrink-0">
129 - <div class="w-8 h-8 bg-orange-500 rounded-md flex items-center justify-center">
130 - <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
131 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
132 - </svg>
133 - </div>
134 - </div>
135 - <div class="ml-5 w-0 flex-1">
136 - <dl>
137 - <dt class="text-sm font-medium text-gray-500 truncate">Critical Alerts</dt>
138 - <dd class="text-2xl font-semibold text-gray-900">{{ stats.criticalAlerts }}</dd>
139 - </dl>
140 - </div>
141 - </div>
142 - </div>
143 - </div>
144 -
145 - <!-- Open Cases -->
146 - <div class="bg-white overflow-hidden shadow-sm rounded-lg">
147 - <div class="p-6">
148 - <div class="flex items-center">
149 - <div class="flex-shrink-0">
150 - <div class="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
151 - <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
152 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
153 - </svg>
154 - </div>
155 - </div>
156 - <div class="ml-5 w-0 flex-1">
157 - <dl>
158 - <dt class="text-sm font-medium text-gray-500 truncate">Open Cases</dt>
159 - <dd class="text-2xl font-semibold text-gray-900">{{ stats.openCases }}</dd>
160 - </dl>
161 - </div>
162 - </div>
163 - </div>
164 - </div>
165 -
166 - <!-- Security Score -->
167 - <div class="bg-white overflow-hidden shadow-sm rounded-lg">
168 - <div class="p-6">
169 - <div class="flex items-center">
170 - <div class="flex-shrink-0">
171 - <div class="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
172 - <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
173 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
174 - </svg>
175 - </div>
176 - </div>
177 - <div class="ml-5 w-0 flex-1">
178 - <dl>
179 - <dt class="text-sm font-medium text-gray-500 truncate">Security Score</dt>
180 - <dd class="flex items-baseline">
181 - <div class="text-2xl font-semibold text-gray-900">{{ stats.securityScore }}%</div>
182 - <div class="ml-2 flex items-baseline text-sm font-semibold text-green-600">
183 - +{{ stats.scoreImprovement }}%
184 - </div>
185 - </dd>
186 - </dl>
187 - </div>
188 - </div>
189 - </div>
190 - </div>
191 -
192 - <!-- Total Agents -->
193 - <div class="bg-white overflow-hidden shadow-sm rounded-lg">
194 - <div class="p-6">
195 - <div class="flex items-center">
196 - <div class="flex-shrink-0">
197 - <div class="w-8 h-8 bg-purple-500 rounded-md flex items-center justify-center">
198 - <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
199 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"></path>
200 - </svg>
201 - </div>
202 - </div>
203 - <div class="ml-5 w-0 flex-1">
204 - <dl>
205 - <dt class="text-sm font-medium text-gray-500 truncate">Total Agents</dt>
206 - <dd class="text-2xl font-semibold text-gray-900">{{ stats.totalAgents }}</dd>
207 - </dl>
208 - </div>
209 - </div>
210 - </div>
211 - </div>
212 - </div>
213 -
214 - <!-- Recent Activity and Charts Section -->
215 - <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
216 - <!-- Recent Alerts -->
217 - <div class="bg-white shadow-sm rounded-lg">
218 - <div class="px-6 py-4 border-b border-gray-200">
219 - <h3 class="text-lg font-medium text-gray-900">Recent Alerts</h3>
220 - </div>
221 - <div class="p-6">
222 - <div v-if="recentAlerts.length === 0" class="text-center text-gray-500 py-8">
223 - No recent alerts
224 - </div>
225 - <div v-else class="space-y-4">
226 - <div
227 - v-for="alert in recentAlerts"
228 - :key="alert.id"
229 - class="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50"
230 - >
231 - <div
232 - class="w-3 h-3 rounded-full mt-2"
233 - :class="{
234 - 'bg-red-500': alert.severity === 'high',
235 - 'bg-yellow-500': alert.severity === 'medium',
236 - 'bg-blue-500': alert.severity === 'low'
237 - }"
238 - ></div>
239 - <div class="flex-1 min-w-0">
240 - <p class="text-sm font-medium text-gray-900 truncate">
241 - {{ alert.name }}
242 - </p>
243 - <p class="text-sm text-gray-500 truncate">
244 - {{ alert.description }}
245 - </p>
246 - <p class="text-xs text-gray-400 mt-1">
247 - {{ formatTimeAgo(alert.created_at) }}
248 - </p>
249 - </div>
250 - <span
251 - class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
252 - :class="{
253 - 'bg-red-100 text-red-800': alert.severity === 'high',
254 - 'bg-yellow-100 text-yellow-800': alert.severity === 'medium',
255 - 'bg-blue-100 text-blue-800': alert.severity === 'low'
256 - }"
257 - >
258 - {{ alert.severity }}
259 - </span>
260 - </div>
261 - </div>
262 - <div class="mt-6 text-center">
263 - <button
264 - @click="goToAlerts"
265 - class="text-indigo-600 hover:text-indigo-500 font-medium text-sm"
266 - >
267 - View all alerts →
268 - </button>
269 - </div>
270 - </div>
271 - </div>
272 -
273 - <!-- Recent Cases -->
274 - <div class="bg-white shadow-sm rounded-lg">
275 - <div class="px-6 py-4 border-b border-gray-200">
276 - <h3 class="text-lg font-medium text-gray-900">Recent Cases</h3>
277 - </div>
278 - <div class="p-6">
279 - <div v-if="recentCases.length === 0" class="text-center text-gray-500 py-8">
280 - No recent cases
281 - </div>
282 - <div v-else class="space-y-4">
283 - <div
284 - v-for="case_ in recentCases"
285 - :key="case_.id"
286 - class="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50"
287 - >
288 - <div
289 - class="w-3 h-3 rounded-full mt-2"
290 - :class="{
291 - 'bg-red-500': case_.status === 'open',
292 - 'bg-yellow-500': case_.status === 'in_progress',
293 - 'bg-green-500': case_.status === 'closed'
294 - }"
295 - ></div>
296 - <div class="flex-1 min-w-0">
297 - <p class="text-sm font-medium text-gray-900 truncate">
298 - {{ case_.name }}
299 - </p>
300 - <p class="text-sm text-gray-500 truncate">
301 - {{ case_.description }}
302 - </p>
303 - <p class="text-xs text-gray-400 mt-1">
304 - {{ formatTimeAgo(case_.created_at) }}
305 - <span v-if="case_.assigned_to"> • Assigned to {{ case_.assigned_to }}</span>
306 - </p>
307 - </div>
308 - <span
309 - class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
310 - :class="{
311 - 'bg-red-100 text-red-800': case_.status === 'open',
312 - 'bg-yellow-100 text-yellow-800': case_.status === 'in_progress',
313 - 'bg-green-100 text-green-800': case_.status === 'closed'
314 - }"
315 - >
316 - {{ case_.status }}
317 - </span>
318 - </div>
319 - </div>
320 - <div class="mt-6 text-center">
321 - <button
322 - @click="goToCases"
323 - class="text-indigo-600 hover:text-indigo-500 font-medium text-sm"
324 - >
325 - View all cases →
326 - </button>
327 - </div>
328 - </div>
329 - </div>
330 - </div>
331 -
332 - <!-- Quick Actions -->
333 - <div class="mt-8 bg-white shadow-sm rounded-lg">
334 - <div class="px-6 py-4 border-b border-gray-200">
335 - <h3 class="text-lg font-medium text-gray-900">Quick Actions</h3>
336 - </div>
337 - <div class="p-6">
338 - <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
339 - <button
340 - @click="goToAlerts"
341 - class="flex items-center justify-center px-4 py-3 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
342 - >
343 - <svg class="w-5 h-5 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
344 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
345 - </svg>
346 - View Alerts
347 - </button>
348 - <button
349 - @click="goToCases"
350 - class="flex items-center justify-center px-4 py-3 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
351 - >
352 - <svg class="w-5 h-5 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
353 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
354 - </svg>
355 - View Cases
356 - </button>
357 - <button
358 - @click="goToAgents"
359 - class="flex items-center justify-center px-4 py-3 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
360 - >
361 - <svg class="w-5 h-5 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
362 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"></path>
363 - </svg>
364 - View Agents
365 - </button>
366 - <button
367 - @click="refreshData"
368 - class="flex items-center justify-center px-4 py-3 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
369 - >
370 - <svg class="w-5 h-5 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
371 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
372 - </svg>
373 - Refresh Data
374 - </button>
375 - <button
376 - class="flex items-center justify-center px-4 py-3 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
377 - disabled
378 - >
379 - <svg class="w-5 h-5 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
380 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 00-2-2z"></path>
381 - </svg>
382 - Reports (Soon)
383 - </button>
384 - </div>
385 - </div>
386 - </div>
387 - </div>
388 - </div>
389 - </main>
390 - </div>
2 + <div class="min-h-screen bg-gray-50">
3 + <!-- Header -->
4 + <header class="border-b bg-white shadow-sm">
5 + <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
6 + <div class="flex h-16 justify-between">
7 + <div class="flex items-center">
8 + <div class="mr-3 min-h-8">
9 + <img
10 + v-if="portalLogo && showLogo"
11 + class="h-8 w-auto"
12 + :src="portalLogo"
13 + :alt="portalTitle"
14 + @error="showLogo = false"
15 + />
16 + </div>
17 + <h1 class="text-xl font-semibold text-gray-900">{{ portalTitle }}</h1>
18 + <nav class="ml-8 flex space-x-8">
19 + <router-link
20 + to="/"
21 + class="rounded-md border-b-2 border-indigo-600 px-3 py-2 text-sm font-medium text-indigo-600"
22 + >
23 + Overview
24 + </router-link>
25 + <router-link
26 + to="/alerts"
27 + class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-900"
28 + >
29 + Alerts
30 + </router-link>
31 + <router-link
32 + to="/cases"
33 + class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-900"
34 + >
35 + Cases
36 + </router-link>
37 + <router-link
38 + to="/agents"
39 + class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-900"
40 + >
41 + Agents
42 + </router-link>
43 + </nav>
44 + </div>
45 + <div class="flex items-center space-x-4">
46 + <div class="text-sm text-gray-700">
47 + Welcome,
48 + <span class="font-medium">{{ username }}</span>
49 + </div>
50 + <button
51 + @click="logout"
52 + class="rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700"
53 + >
54 + Logout
55 + </button>
56 + </div>
57 + </div>
58 + </div>
59 + </header>
60 +
61 + <!-- Main Content -->
62 + <main class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
63 + <div class="px-4 py-6 sm:px-0">
64 + <!-- Welcome Section -->
65 + <div class="mb-8">
66 + <h2 class="mb-2 text-2xl font-bold text-gray-900">Security Overview</h2>
67 + <p class="text-gray-600">Monitor your organization's security posture and recent activity</p>
68 + </div>
69 +
70 + <!-- Loading State -->
71 + <div v-if="loading" class="flex items-center justify-center py-12">
72 + <div class="h-12 w-12 animate-spin rounded-full border-b-2 border-indigo-600"></div>
73 + <span class="ml-3 text-gray-600">Loading dashboard...</span>
74 + </div>
75 +
76 + <!-- Error State -->
77 + <div v-else-if="error" class="mb-6 rounded-lg border border-red-200 bg-red-50 p-4">
78 + <div class="flex">
79 + <div class="flex-shrink-0">
80 + <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
81 + <path
82 + fill-rule="evenodd"
83 + d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
84 + clip-rule="evenodd"
85 + />
86 + </svg>
87 + </div>
88 + <div class="ml-3">
89 + <h3 class="text-sm font-medium text-red-800">Error Loading Dashboard</h3>
90 + <div class="mt-2 text-sm text-red-700">{{ error }}</div>
91 + <div class="mt-3">
92 + <button
93 + @click="refreshData"
94 + class="rounded bg-red-100 px-3 py-1 text-sm font-medium text-red-800 hover:bg-red-200"
95 + >
96 + Try Again
97 + </button>
98 + </div>
99 + </div>
100 + </div>
101 + </div>
102 +
103 + <!-- Dashboard Content -->
104 + <div v-else>
105 + <!-- Key Metrics Cards -->
106 + <div class="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-5">
107 + <!-- Total Alerts -->
108 + <div class="overflow-hidden rounded-lg bg-white shadow-sm">
109 + <div class="p-6">
110 + <div class="flex items-center">
111 + <div class="flex-shrink-0">
112 + <div class="flex h-8 w-8 items-center justify-center rounded-md bg-red-500">
113 + <svg
114 + class="h-5 w-5 text-white"
115 + fill="none"
116 + stroke="currentColor"
117 + viewBox="0 0 24 24"
118 + >
119 + <path
120 + stroke-linecap="round"
121 + stroke-linejoin="round"
122 + stroke-width="2"
123 + d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
124 + ></path>
125 + </svg>
126 + </div>
127 + </div>
128 + <div class="ml-5 w-0 flex-1">
129 + <dl>
130 + <dt class="truncate text-sm font-medium text-gray-500">Total Alerts</dt>
131 + <dd class="flex items-baseline">
132 + <div class="text-2xl font-semibold text-gray-900">
133 + {{ stats.totalAlerts }}
134 + </div>
135 + <div
136 + class="ml-2 flex items-baseline text-sm font-semibold"
137 + :class="alertTrendClass"
138 + >
139 + {{ stats.alertTrend }}
140 + </div>
141 + </dd>
142 + </dl>
143 + </div>
144 + </div>
145 + </div>
146 + </div>
147 +
148 + <!-- Critical Alerts -->
149 + <div class="overflow-hidden rounded-lg bg-white shadow-sm">
150 + <div class="p-6">
151 + <div class="flex items-center">
152 + <div class="flex-shrink-0">
153 + <div class="flex h-8 w-8 items-center justify-center rounded-md bg-orange-500">
154 + <svg
155 + class="h-5 w-5 text-white"
156 + fill="none"
157 + stroke="currentColor"
158 + viewBox="0 0 24 24"
159 + >
160 + <path
161 + stroke-linecap="round"
162 + stroke-linejoin="round"
163 + stroke-width="2"
164 + d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
165 + ></path>
166 + </svg>
167 + </div>
168 + </div>
169 + <div class="ml-5 w-0 flex-1">
170 + <dl>
171 + <dt class="truncate text-sm font-medium text-gray-500">Critical Alerts</dt>
172 + <dd class="text-2xl font-semibold text-gray-900">
173 + {{ stats.criticalAlerts }}
174 + </dd>
175 + </dl>
176 + </div>
177 + </div>
178 + </div>
179 + </div>
180 +
181 + <!-- Open Cases -->
182 + <div class="overflow-hidden rounded-lg bg-white shadow-sm">
183 + <div class="p-6">
184 + <div class="flex items-center">
185 + <div class="flex-shrink-0">
186 + <div class="flex h-8 w-8 items-center justify-center rounded-md bg-blue-500">
187 + <svg
188 + class="h-5 w-5 text-white"
189 + fill="none"
190 + stroke="currentColor"
191 + viewBox="0 0 24 24"
192 + >
193 + <path
194 + stroke-linecap="round"
195 + stroke-linejoin="round"
196 + stroke-width="2"
197 + d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
198 + ></path>
199 + </svg>
200 + </div>
201 + </div>
202 + <div class="ml-5 w-0 flex-1">
203 + <dl>
204 + <dt class="truncate text-sm font-medium text-gray-500">Open Cases</dt>
205 + <dd class="text-2xl font-semibold text-gray-900">{{ stats.openCases }}</dd>
206 + </dl>
207 + </div>
208 + </div>
209 + </div>
210 + </div>
211 +
212 + <!-- Security Score -->
213 + <div class="overflow-hidden rounded-lg bg-white shadow-sm">
214 + <div class="p-6">
215 + <div class="flex items-center">
216 + <div class="flex-shrink-0">
217 + <div class="flex h-8 w-8 items-center justify-center rounded-md bg-green-500">
218 + <svg
219 + class="h-5 w-5 text-white"
220 + fill="none"
221 + stroke="currentColor"
222 + viewBox="0 0 24 24"
223 + >
224 + <path
225 + stroke-linecap="round"
226 + stroke-linejoin="round"
227 + stroke-width="2"
228 + d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
229 + ></path>
230 + </svg>
231 + </div>
232 + </div>
233 + <div class="ml-5 w-0 flex-1">
234 + <dl>
235 + <dt class="truncate text-sm font-medium text-gray-500">Security Score</dt>
236 + <dd class="flex items-baseline">
237 + <div class="text-2xl font-semibold text-gray-900">
238 + {{ stats.securityScore }}%
239 + </div>
240 + <div
241 + class="ml-2 flex items-baseline text-sm font-semibold text-green-600"
242 + >
243 + +{{ stats.scoreImprovement }}%
244 + </div>
245 + </dd>
246 + </dl>
247 + </div>
248 + </div>
249 + </div>
250 + </div>
251 +
252 + <!-- Total Agents -->
253 + <div class="overflow-hidden rounded-lg bg-white shadow-sm">
254 + <div class="p-6">
255 + <div class="flex items-center">
256 + <div class="flex-shrink-0">
257 + <div class="flex h-8 w-8 items-center justify-center rounded-md bg-purple-500">
258 + <svg
259 + class="h-5 w-5 text-white"
260 + fill="none"
261 + stroke="currentColor"
262 + viewBox="0 0 24 24"
263 + >
264 + <path
265 + stroke-linecap="round"
266 + stroke-linejoin="round"
267 + stroke-width="2"
268 + d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
269 + ></path>
270 + </svg>
271 + </div>
272 + </div>
273 + <div class="ml-5 w-0 flex-1">
274 + <dl>
275 + <dt class="truncate text-sm font-medium text-gray-500">Total Agents</dt>
276 + <dd class="text-2xl font-semibold text-gray-900">
277 + {{ stats.totalAgents }}
278 + </dd>
279 + </dl>
280 + </div>
281 + </div>
282 + </div>
283 + </div>
284 + </div>
285 +
286 + <!-- Recent Activity and Charts Section -->
287 + <div class="grid grid-cols-1 gap-8 lg:grid-cols-2">
288 + <!-- Recent Alerts -->
289 + <div class="rounded-lg bg-white shadow-sm">
290 + <div class="border-b border-gray-200 px-6 py-4">
291 + <h3 class="text-lg font-medium text-gray-900">Recent Alerts</h3>
292 + </div>
293 + <div class="p-6">
294 + <div v-if="recentAlerts.length === 0" class="py-8 text-center text-gray-500">
295 + No recent alerts
296 + </div>
297 + <div v-else class="space-y-4">
298 + <div
299 + v-for="alert in recentAlerts"
300 + :key="alert.id"
301 + class="flex items-start space-x-3 rounded-lg p-3 hover:bg-gray-50"
302 + >
303 + <div
304 + class="mt-2 h-3 w-3 rounded-full"
305 + :class="{
306 + 'bg-red-500': alert.severity === 'high',
307 + 'bg-yellow-500': alert.severity === 'medium',
308 + 'bg-blue-500': alert.severity === 'low'
309 + }"
310 + ></div>
311 + <div class="min-w-0 flex-1">
312 + <p class="truncate text-sm font-medium text-gray-900">
313 + {{ alert.name }}
314 + </p>
315 + <p class="truncate text-sm text-gray-500">
316 + {{ alert.description }}
317 + </p>
318 + <p class="mt-1 text-xs text-gray-400">
319 + {{ formatTimeAgo(alert.created_at) }}
320 + </p>
321 + </div>
322 + <span
323 + class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
324 + :class="{
325 + 'bg-red-100 text-red-800': alert.severity === 'high',
326 + 'bg-yellow-100 text-yellow-800': alert.severity === 'medium',
327 + 'bg-blue-100 text-blue-800': alert.severity === 'low'
328 + }"
329 + >
330 + {{ alert.severity }}
331 + </span>
332 + </div>
333 + </div>
334 + <div class="mt-6 text-center">
335 + <button
336 + @click="goToAlerts"
337 + class="text-sm font-medium text-indigo-600 hover:text-indigo-500"
338 + >
339 + View all alerts →
340 + </button>
341 + </div>
342 + </div>
343 + </div>
344 +
345 + <!-- Recent Cases -->
346 + <div class="rounded-lg bg-white shadow-sm">
347 + <div class="border-b border-gray-200 px-6 py-4">
348 + <h3 class="text-lg font-medium text-gray-900">Recent Cases</h3>
349 + </div>
350 + <div class="p-6">
351 + <div v-if="recentCases.length === 0" class="py-8 text-center text-gray-500">
352 + No recent cases
353 + </div>
354 + <div v-else class="space-y-4">
355 + <div
356 + v-for="case_ in recentCases"
357 + :key="case_.id"
358 + class="flex items-start space-x-3 rounded-lg p-3 hover:bg-gray-50"
359 + >
360 + <div
361 + class="mt-2 h-3 w-3 rounded-full"
362 + :class="{
363 + 'bg-red-500': case_.status === 'open',
364 + 'bg-yellow-500': case_.status === 'in_progress',
365 + 'bg-green-500': case_.status === 'closed'
366 + }"
367 + ></div>
368 + <div class="min-w-0 flex-1">
369 + <p class="truncate text-sm font-medium text-gray-900">
370 + {{ case_.name }}
371 + </p>
372 + <p class="truncate text-sm text-gray-500">
373 + {{ case_.description }}
374 + </p>
375 + <p class="mt-1 text-xs text-gray-400">
376 + {{ formatTimeAgo(case_.created_at) }}
377 + <span v-if="case_.assigned_to">
378 + • Assigned to {{ case_.assigned_to }}
379 + </span>
380 + </p>
381 + </div>
382 + <span
383 + class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
384 + :class="{
385 + 'bg-red-100 text-red-800': case_.status === 'open',
386 + 'bg-yellow-100 text-yellow-800': case_.status === 'in_progress',
387 + 'bg-green-100 text-green-800': case_.status === 'closed'
388 + }"
389 + >
390 + {{ case_.status }}
391 + </span>
392 + </div>
393 + </div>
394 + <div class="mt-6 text-center">
395 + <button
396 + @click="goToCases"
397 + class="text-sm font-medium text-indigo-600 hover:text-indigo-500"
398 + >
399 + View all cases →
400 + </button>
401 + </div>
402 + </div>
403 + </div>
404 + </div>
405 +
406 + <!-- Quick Actions -->
407 + <div class="mt-8 rounded-lg bg-white shadow-sm">
408 + <div class="border-b border-gray-200 px-6 py-4">
409 + <h3 class="text-lg font-medium text-gray-900">Quick Actions</h3>
410 + </div>
411 + <div class="p-6">
412 + <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5">
413 + <button
414 + @click="goToAlerts"
415 + class="flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none"
416 + >
417 + <svg
418 + class="mr-2 h-5 w-5 text-gray-400"
419 + fill="none"
420 + stroke="currentColor"
421 + viewBox="0 0 24 24"
422 + >
423 + <path
424 + stroke-linecap="round"
425 + stroke-linejoin="round"
426 + stroke-width="2"
427 + d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
428 + ></path>
429 + </svg>
430 + View Alerts
431 + </button>
432 + <button
433 + @click="goToCases"
434 + class="flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none"
435 + >
436 + <svg
437 + class="mr-2 h-5 w-5 text-gray-400"
438 + fill="none"
439 + stroke="currentColor"
440 + viewBox="0 0 24 24"
441 + >
442 + <path
443 + stroke-linecap="round"
444 + stroke-linejoin="round"
445 + stroke-width="2"
446 + d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
447 + ></path>
448 + </svg>
449 + View Cases
450 + </button>
451 + <button
452 + @click="goToAgents"
453 + class="flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none"
454 + >
455 + <svg
456 + class="mr-2 h-5 w-5 text-gray-400"
457 + fill="none"
458 + stroke="currentColor"
459 + viewBox="0 0 24 24"
460 + >
461 + <path
462 + stroke-linecap="round"
463 + stroke-linejoin="round"
464 + stroke-width="2"
465 + d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
466 + ></path>
467 + </svg>
468 + View Agents
469 + </button>
470 + <button
471 + @click="refreshData"
472 + class="flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none"
473 + >
474 + <svg
475 + class="mr-2 h-5 w-5 text-gray-400"
476 + fill="none"
477 + stroke="currentColor"
478 + viewBox="0 0 24 24"
479 + >
480 + <path
481 + stroke-linecap="round"
482 + stroke-linejoin="round"
483 + stroke-width="2"
484 + d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
485 + ></path>
486 + </svg>
487 + Refresh Data
488 + </button>
489 + <button
490 + class="flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none"
491 + disabled
492 + >
493 + <svg
494 + class="mr-2 h-5 w-5 text-gray-400"
495 + fill="none"
496 + stroke="currentColor"
497 + viewBox="0 0 24 24"
498 + >
499 + <path
500 + stroke-linecap="round"
501 + stroke-linejoin="round"
502 + stroke-width="2"
503 + d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 00-2-2z"
504 + ></path>
505 + </svg>
506 + Reports (Soon)
507 + </button>
508 + </div>
509 + </div>
510 + </div>
511 + </div>
512 + </div>
513 + </main>
514 + </div>
515 </template>
516
517 <script setup lang="ts">
394 -import { ref, onMounted, computed } from 'vue'
395 -import { useRouter } from 'vue-router'
396 -import AlertsAPI, { type Alert } from '@/api/alerts'
397 -import CasesAPI, { type Case } from '@/api/cases'
398 -import AgentsAPI from '@/api/agents'
518 +import { ref, onMounted, computed } from "vue"
519 +import { useRouter } from "vue-router"
520 +import { usePortalSettingsStore } from "@/stores/portalSettings"
521 +import AlertsAPI, { type Alert } from "@/api/alerts"
522 +import CasesAPI, { type Case } from "@/api/cases"
523 +import AgentsAPI from "@/api/agents"
524
525 interface Stats {
401 - totalAlerts: number
402 - criticalAlerts: number
403 - openCases: number
404 - totalAgents: number
405 - securityScore: number
406 - alertTrend: string
407 - scoreImprovement: number
526 + totalAlerts: number
527 + criticalAlerts: number
528 + openCases: number
529 + totalAgents: number
530 + securityScore: number
531 + alertTrend: string
532 + scoreImprovement: number
533 }
534
535 interface DashboardAlert {
411 - id: number
412 - name: string
413 - description: string
414 - severity: string
415 - created_at: string
536 + id: number
537 + name: string
538 + description: string
539 + severity: string
540 + created_at: string
541 }
542
543 interface DashboardCase {
419 - id: number
420 - name: string
421 - description: string
422 - status: string
423 - created_at: string
424 - assigned_to?: string | null
544 + id: number
545 + name: string
546 + description: string
547 + status: string
548 + created_at: string
549 + assigned_to?: string | null
550 }
551
552 const router = useRouter()
553 +const portalSettingsStore = usePortalSettingsStore()
554
555 const loading = ref(true)
430 -const error = ref('')
556 +const error = ref("")
557 +const showLogo = ref(true)
558 const stats = ref<Stats>({
432 - totalAlerts: 0,
433 - criticalAlerts: 0,
434 - openCases: 0,
435 - totalAgents: 0,
436 - securityScore: 0,
437 - alertTrend: '+0',
438 - scoreImprovement: 0
559 + totalAlerts: 0,
560 + criticalAlerts: 0,
561 + openCases: 0,
562 + totalAgents: 0,
563 + securityScore: 0,
564 + alertTrend: "+0",
565 + scoreImprovement: 0
566 })
567 const recentAlerts = ref<DashboardAlert[]>([])
568 const recentCases = ref<DashboardCase[]>([])
569
570 const username = computed(() => {
444 - try {
445 - const user = JSON.parse(localStorage.getItem('customer-portal-user') || '{}')
446 - return user.username || 'User'
447 - } catch {
448 - return 'User'
449 - }
571 + try {
572 + const user = JSON.parse(localStorage.getItem("customer-portal-user") || "{}")
573 + return user.username || "User"
574 + } catch {
575 + return "User"
576 + }
577 })
578
579 +const portalTitle = computed(() => portalSettingsStore.portalTitle || "Customer Portal")
580 +const portalLogo = computed(() => portalSettingsStore.portalLogo)
581 +
582 const alertTrendClass = computed(() => {
453 - if (stats.value.alertTrend.startsWith('+')) {
454 - return 'text-red-600'
455 - } else if (stats.value.alertTrend.startsWith('-')) {
456 - return 'text-green-600'
457 - }
458 - return 'text-gray-600'
583 + if (stats.value.alertTrend.startsWith("+")) {
584 + return "text-red-600"
585 + } else if (stats.value.alertTrend.startsWith("-")) {
586 + return "text-green-600"
587 + }
588 + return "text-gray-600"
589 })
590
591 const formatTimeAgo = (dateString: string) => {
462 - if (!dateString) return 'Unknown'
463 -
464 - try {
465 - const date = new Date(dateString)
466 - const now = new Date()
467 - const diffInMs = now.getTime() - date.getTime()
468 - const diffInHours = diffInMs / (1000 * 60 * 60)
469 -
470 - if (diffInHours < 1) {
471 - const diffInMinutes = Math.floor(diffInMs / (1000 * 60))
472 - return `${diffInMinutes} minutes ago`
473 - } else if (diffInHours < 24) {
474 - return `${Math.floor(diffInHours)} hours ago`
475 - } else {
476 - const diffInDays = Math.floor(diffInHours / 24)
477 - return `${diffInDays} days ago`
478 - }
479 - } catch {
480 - return 'Unknown'
481 - }
592 + if (!dateString) return "Unknown"
593 +
594 + try {
595 + const date = new Date(dateString)
596 + const now = new Date()
597 + const diffInMs = now.getTime() - date.getTime()
598 + const diffInHours = diffInMs / (1000 * 60 * 60)
599 +
600 + if (diffInHours < 1) {
601 + const diffInMinutes = Math.floor(diffInMs / (1000 * 60))
602 + return `${diffInMinutes} minutes ago`
603 + } else if (diffInHours < 24) {
604 + return `${Math.floor(diffInHours)} hours ago`
605 + } else {
606 + const diffInDays = Math.floor(diffInHours / 24)
607 + return `${diffInDays} days ago`
608 + }
609 + } catch {
610 + return "Unknown"
611 + }
612 }
613
614 const fetchDashboardData = async () => {
485 - loading.value = true
486 - error.value = ''
487 -
488 - try {
489 - // Fetch alerts, cases, and agents data using our API services
490 - const [alertsResponse, casesResponse, agentsResponse] = await Promise.all([
491 - AlertsAPI.getAlerts(1, 50).catch(() => ({ alerts: [], total: 0, open: 0, in_progress: 0, closed: 0, success: false, message: 'Failed to load alerts' })),
492 - CasesAPI.getCases().catch(() => ({ cases: [], success: false, message: 'Failed to load cases' })),
493 - AgentsAPI.getAgents().catch(() => ({ agents: [], success: false, message: 'Failed to load agents' }))
494 - ])
495 -
496 - const alerts = alertsResponse.alerts || []
497 - const cases = casesResponse.cases || []
498 - const agents = agentsResponse.agents || []
499 -
500 - // Calculate stats from real data
501 - const openAlerts = alertsResponse.open || 0
502 - const inProgressAlerts = alertsResponse.in_progress || 0
503 -
504 - const openCases = cases.filter((case_: Case) =>
505 - case_.case_status === 'open' || case_.case_status === 'in_progress'
506 - ).length
507 -
508 - // Calculate security score based on actual data
509 - const totalActiveIssues = openAlerts + inProgressAlerts + openCases
510 - const securityScore = Math.max(60, 100 - (totalActiveIssues * 2))
511 -
512 - stats.value = {
513 - totalAlerts: alertsResponse.total || 0,
514 - criticalAlerts: openAlerts + inProgressAlerts,
515 - openCases,
516 - totalAgents: agents.length,
517 - securityScore: Math.min(100, securityScore),
518 - alertTrend: openAlerts > 0 ? `+${openAlerts}` : '0',
519 - scoreImprovement: Math.floor(Math.random() * 5) + 1
520 - }
521 -
522 - // Get recent alerts (last 5, sorted by creation time)
523 - recentAlerts.value = alerts
524 - .map((alert: Alert) => ({
525 - id: alert.id,
526 - name: alert.alert_name || 'Unnamed Alert',
527 - description: alert.alert_description || 'No description available',
528 - severity: alert.status === 'open' ? 'high' : alert.status === 'in_progress' ? 'medium' : 'low',
529 - created_at: alert.alert_creation_time || new Date().toISOString()
530 - }))
531 - .sort((a: DashboardAlert, b: DashboardAlert) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
532 - .slice(0, 5)
533 -
534 - // Get recent cases (last 5, sorted by creation time)
535 - recentCases.value = cases
536 - .map((case_: Case) => ({
537 - id: case_.id,
538 - name: case_.case_name || 'Unnamed Case',
539 - description: case_.case_description || 'No description available',
540 - status: case_.case_status || 'open',
541 - created_at: case_.case_creation_time || new Date().toISOString(),
542 - assigned_to: case_.assigned_to || undefined
543 - }))
544 - .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
545 - .slice(0, 5)
546 -
547 - } catch (err: any) {
548 - console.error('Failed to fetch dashboard data:', err)
549 - error.value = err.response?.data?.detail || err.message || 'Failed to load dashboard data'
550 -
551 - // Set default/mock data if API fails
552 - stats.value = {
553 - totalAlerts: 0,
554 - criticalAlerts: 0,
555 - openCases: 0,
556 - totalAgents: 0,
557 - securityScore: 85,
558 - alertTrend: '0',
559 - scoreImprovement: 2
560 - }
561 - recentAlerts.value = []
562 - recentCases.value = []
563 - } finally {
564 - loading.value = false
565 - }
615 + loading.value = true
616 + error.value = ""
617 +
618 + try {
619 + // Fetch alerts, cases, and agents data using our API services
620 + const [alertsResponse, casesResponse, agentsResponse] = await Promise.all([
621 + AlertsAPI.getAlerts(1, 50).catch(() => ({
622 + alerts: [],
623 + total: 0,
624 + open: 0,
625 + in_progress: 0,
626 + closed: 0,
627 + success: false,
628 + message: "Failed to load alerts"
629 + })),
630 + CasesAPI.getCases().catch(() => ({ cases: [], success: false, message: "Failed to load cases" })),
631 + AgentsAPI.getAgents().catch(() => ({ agents: [], success: false, message: "Failed to load agents" }))
632 + ])
633 +
634 + const alerts = alertsResponse.alerts || []
635 + const cases = casesResponse.cases || []
636 + const agents = agentsResponse.agents || []
637 +
638 + // Calculate stats from real data
639 + const openAlerts = alertsResponse.open || 0
640 + const inProgressAlerts = alertsResponse.in_progress || 0
641 +
642 + const openCases = cases.filter(
643 + (case_: Case) => case_.case_status === "OPEN" || case_.case_status === "IN_PROGRESS"
644 + ).length
645 +
646 + // Calculate security score based on actual data
647 + const totalActiveIssues = openAlerts + inProgressAlerts + openCases
648 + const securityScore = Math.max(60, 100 - totalActiveIssues * 2)
649 +
650 + stats.value = {
651 + totalAlerts: alertsResponse.total || 0,
652 + criticalAlerts: openAlerts + inProgressAlerts,
653 + openCases,
654 + totalAgents: agents.length,
655 + securityScore: Math.min(100, securityScore),
656 + alertTrend: openAlerts > 0 ? `+${openAlerts}` : "0",
657 + scoreImprovement: Math.floor(Math.random() * 5) + 1
658 + }
659 +
660 + // Get recent alerts (last 5, sorted by creation time)
661 + recentAlerts.value = alerts
662 + .map((alert: Alert) => ({
663 + id: alert.id,
664 + name: alert.alert_name || "Unnamed Alert",
665 + description: alert.alert_description || "No description available",
666 + severity: alert.status === "OPEN" ? "high" : alert.status === "IN_PROGRESS" ? "medium" : "low",
667 + created_at: alert.alert_creation_time || new Date().toISOString()
668 + }))
669 + .sort(
670 + (a: DashboardAlert, b: DashboardAlert) =>
671 + new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
672 + )
673 + .slice(0, 5)
674 +
675 + // Get recent cases (last 5, sorted by creation time)
676 + recentCases.value = cases
677 + .map((case_: Case) => ({
678 + id: case_.id,
679 + name: case_.case_name || "Unnamed Case",
680 + description: case_.case_description || "No description available",
681 + status: case_.case_status || "open",
682 + created_at: case_.case_creation_time || new Date().toISOString(),
683 + assigned_to: case_.assigned_to || undefined
684 + }))
685 + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
686 + .slice(0, 5)
687 + } catch (err: any) {
688 + console.error("Failed to fetch dashboard data:", err)
689 + error.value = err.response?.data?.detail || err.message || "Failed to load dashboard data"
690 +
691 + // Set default/mock data if API fails
692 + stats.value = {
693 + totalAlerts: 0,
694 + criticalAlerts: 0,
695 + openCases: 0,
696 + totalAgents: 0,
697 + securityScore: 85,
698 + alertTrend: "0",
699 + scoreImprovement: 2
700 + }
701 + recentAlerts.value = []
702 + recentCases.value = []
703 + } finally {
704 + loading.value = false
705 + }
706 }
707
708 const refreshData = () => {
569 - fetchDashboardData()
709 + fetchDashboardData()
710 }
711
712 const goToAlerts = () => {
573 - router.push('/alerts')
713 + router.push("/alerts")
714 }
715
716 const goToCases = () => {
577 - router.push('/cases')
717 + router.push("/cases")
718 }
719
720 const goToAgents = () => {
581 - router.push('/agents')
721 + router.push("/agents")
722 }
723
724 const logout = () => {
585 - localStorage.removeItem('customer-portal-auth-token')
586 - localStorage.removeItem('customer-portal-user')
587 - router.push('/login')
725 + localStorage.removeItem("customer-portal-auth-token")
726 + localStorage.removeItem("customer-portal-user")
727 + router.push("/login")
728 }
729
730 onMounted(() => {
591 - fetchDashboardData()
731 + fetchDashboardData()
732 })
733 </script>
docker-compose.yml
+9
@@ -24,6 +24,15 @@ services:
24 - "443:443"
25 restart: always
26
27 + # ! Optional Customer Portal Service ! #
28 + # copilot-customer-portal:
29 + # image: ghcr.io/socfortress/copilot-customer-portal:latest
30 + # environment:
31 + # - SERVER_HOST=${SERVER_HOST:-localhost} # Set the domain name of your server
32 + # ports:
33 + # - "8080:80"
34 + # restart: always
35 +
36 copilot-mysql:
37 image: mysql:8.0.38-debian
38 environment:
frontend/.editorconfig new
+13
@@ -0,0 +1,13 @@
1 +# EditorConfig is awesome: http://EditorConfig.org
2 +
3 +# top-most EditorConfig file
4 +root = true
5 +
6 +# Unix-style newlines with a newline ending every file
7 +[*]
8 +indent_style = tab
9 +indent_size = 4
10 +trim_trailing_whitespace = true
11 +insert_final_newline = true
12 +charset = utf-8
13 +end_of_line = lf
frontend/package.json
+38 -38
@@ -3,7 +3,7 @@
3 "type": "module",
4 "version": "1.0.0",
5 "private": true,
6 - "packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd",
6 + "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a",
7 "engines": {
8 "node": ">=18.0.0"
9 },
@@ -40,46 +40,46 @@
40 "@codemirror/commands": "^6.10.0",
41 "@codemirror/lang-javascript": "^6.2.4",
42 "@codemirror/lang-xml": "^6.1.0",
43 - "@codemirror/lint": "^6.9.1",
43 + "@codemirror/lint": "^6.9.2",
44 "@codemirror/theme-one-dark": "^6.1.3",
45 - "@codemirror/view": "^6.38.6",
45 + "@codemirror/view": "^6.38.8",
46 "@f3ve/vue-markdown-it": "^0.2.3",
47 "@fontsource/jetbrains-mono": "^5.2.8",
48 "@fontsource/lexend": "^5.2.11",
49 "@fontsource/public-sans": "^5.2.7",
50 - "@shikijs/markdown-it": "^3.14.0",
50 + "@shikijs/markdown-it": "^3.17.0",
51 "@singulio/app-auth-search": "^0.0.3",
52 "@types/codemirror": "^5.60.17",
53 - "@vueuse/core": "^14.0.0",
53 + "@vueuse/core": "^14.1.0",
54 "@vueuse/motion": "^3.0.3",
55 - "axios": "^1.13.1",
55 + "axios": "^1.13.2",
56 "bytes": "^3.1.2",
57 "codemirror": "~6.0.2",
58 "colord": "^2.9.3",
59 - "dayjs": "^1.11.18",
59 + "dayjs": "^1.11.19",
60 "detect-touch-device": "^1.1.6",
61 "echarts": "^6.0.0",
62 - "fast-xml-parser": "^5.3.0",
62 + "fast-xml-parser": "^5.3.2",
63 "file-saver": "^2.0.5",
64 "html-entities": "^2.6.0",
65 - "jose": "^6.1.0",
65 + "jose": "^6.1.2",
66 "js-md5": "^0.8.3",
67 "lodash": "^4.17.21",
68 "mitt": "^3.0.1",
69 - "naive-ui": "^2.43.1",
69 + "naive-ui": "^2.43.2",
70 "nanoid": "^5.1.6",
71 "password-validator": "^5.3.0",
72 - "pinia": "^3.0.3",
73 - "pinia-plugin-persistedstate": "^4.5.0",
72 + "pinia": "^3.0.4",
73 + "pinia-plugin-persistedstate": "^4.7.1",
74 "secure-ls": "^2.0.0",
75 - "shiki": "^3.14.0",
75 + "shiki": "^3.17.0",
76 "thememirror": "^2.0.1",
77 - "validator": "^13.15.20",
78 - "vue": "^3.5.22",
77 + "validator": "^13.15.23",
78 + "vue": "^3.5.25",
79 "vue-advanced-cropper": "^2.8.9",
80 "vue-codemirror": "^6.1.1",
81 "vue-highlight-words": "^3.0.1",
82 - "vue-i18n": "^11.1.12",
82 + "vue-i18n": "^11.2.2",
83 "vue-router": "^4.6.3",
84 "vue-sjv": "^0.0.6",
85 "vue3-apexcharts": "^1.10.0",
@@ -94,45 +94,45 @@
94 "vueuc": "^0.4.64"
95 },
96 "devDependencies": {
97 - "@antfu/eslint-config": "~6.1.0",
97 + "@antfu/eslint-config": "~6.2.0",
98 "@clack/prompts": "^0.11.0",
99 "@iconify/vue": "^5.0.0",
100 - "@tailwindcss/vite": "^4.1.16",
101 - "@tsconfig/node20": "^20.1.6",
100 + "@tailwindcss/vite": "^4.1.17",
101 + "@tsconfig/node20": "^20.1.8",
102 "@types/bytes": "^3.1.5",
103 "@types/file-saver": "^2.0.7",
104 "@types/fs-extra": "^11.0.4",
105 "@types/jsdom": "^27.0.0",
106 - "@types/lodash": "^4.17.20",
106 + "@types/lodash": "^4.17.21",
107 "@types/markdown-it": "^14.1.2",
108 - "@types/node": "^24.9.2",
109 - "@types/validator": "^13.15.4",
110 - "@vitejs/plugin-vue": "^6.0.1",
111 - "@vitejs/plugin-vue-jsx": "^5.1.1",
108 + "@types/node": "^24.10.1",
109 + "@types/validator": "^13.15.10",
110 + "@vitejs/plugin-vue": "^6.0.2",
111 + "@vitejs/plugin-vue-jsx": "^5.1.2",
112 "@vue/test-utils": "^2.4.6",
113 "@vue/tsconfig": "~0.7.0",
114 - "cypress": "^15.5.0",
114 + "cypress": "^15.7.0",
115 "depcheck": "^1.4.7",
116 - "eslint": "~9.38.0",
116 + "eslint": "~9.39.1",
117 "flourite": "^1.3.0",
118 "fs-extra": "^11.3.2",
119 - "jsdom": "^27.0.1",
119 + "jsdom": "^27.2.0",
120 "npm-run-all2": "^8.0.4",
121 - "prettier": "^3.6.2",
121 + "prettier": "^3.7.3",
122 "prettier-plugin-tailwindcss": "^0.7.1",
123 - "sass": "^1.93.2",
124 - "start-server-and-test": "^2.1.2",
125 - "tailwindcss": "^4.1.16",
126 - "taze": "^19.9.0",
127 - "type-fest": "^5.1.0",
123 + "sass": "^1.94.2",
124 + "start-server-and-test": "^2.1.3",
125 + "tailwindcss": "^4.1.17",
126 + "taze": "^19.9.2",
127 + "type-fest": "^5.2.0",
128 "typescript": "~5.9.3",
129 - "vite": "^7.1.12",
129 + "vite": "^7.2.4",
130 "vite-bundle-visualizer": "^1.2.1",
131 "vite-plugin-inspect": "^11.3.3",
132 - "vite-plugin-vue-devtools": "^8.0.3",
132 + "vite-plugin-vue-devtools": "^8.0.5",
133 "vite-svg-loader": "^5.1.0",
134 - "vitest": "^4.0.5",
135 - "vue-tsc": "^3.1.2"
134 + "vitest": "^4.0.14",
135 + "vue-tsc": "^3.1.5"
136 },
137 "pnpm": {
138 "onlyBuiltDependencies": [
@@ -143,4 +143,4 @@
143 "unrs-resolver"
144 ]
145 }
146 -}
146 +}
\ No newline at end of file
frontend/pnpm-lock.yaml
+792 -714
@@ -21,17 +21,17 @@ importers:
21 specifier: ^6.1.0
22 version: 6.1.0
23 '@codemirror/lint':
24 - specifier: ^6.9.1
25 - version: 6.9.1
24 + specifier: ^6.9.2
25 + version: 6.9.2
26 '@codemirror/theme-one-dark':
27 specifier: ^6.1.3
28 version: 6.1.3
29 '@codemirror/view':
30 - specifier: ^6.38.6
31 - version: 6.38.6
30 + specifier: ^6.38.8
31 + version: 6.38.8
32 '@f3ve/vue-markdown-it':
33 specifier: ^0.2.3
34 - version: 0.2.3(vue@3.5.22(typescript@5.9.3))
34 + version: 0.2.3(vue@3.5.25(typescript@5.9.3))
35 '@fontsource/jetbrains-mono':
36 specifier: ^5.2.8
37 version: 5.2.8
@@ -42,8 +42,8 @@ importers:
42 specifier: ^5.2.7
43 version: 5.2.7
44 '@shikijs/markdown-it':
45 - specifier: ^3.14.0
46 - version: 3.14.0
45 + specifier: ^3.17.0
46 + version: 3.17.0
47 '@singulio/app-auth-search':
48 specifier: ^0.0.3
49 version: 0.0.3
@@ -51,14 +51,14 @@ importers:
51 specifier: ^5.60.17
52 version: 5.60.17
53 '@vueuse/core':
54 - specifier: ^14.0.0
55 - version: 14.0.0(vue@3.5.22(typescript@5.9.3))
54 + specifier: ^14.1.0
55 + version: 14.1.0(vue@3.5.25(typescript@5.9.3))
56 '@vueuse/motion':
57 specifier: ^3.0.3
58 - version: 3.0.3(vue@3.5.22(typescript@5.9.3))
58 + version: 3.0.3(vue@3.5.25(typescript@5.9.3))
59 axios:
60 - specifier: ^1.13.1
61 - version: 1.13.1(debug@4.4.3)
60 + specifier: ^1.13.2
61 + version: 1.13.2(debug@4.4.3)
62 bytes:
63 specifier: ^3.1.2
64 version: 3.1.2
@@ -69,7 +69,7 @@ importers:
69 specifier: ^2.9.3
70 version: 2.9.3
71 dayjs:
72 - specifier: ^1.11.18
72 + specifier: ^1.11.19
73 version: 1.11.19
74 detect-touch-device:
75 specifier: ^1.1.6
@@ -78,8 +78,8 @@ importers:
78 specifier: ^6.0.0
79 version: 6.0.0
80 fast-xml-parser:
81 - specifier: ^5.3.0
82 - version: 5.3.0
81 + specifier: ^5.3.2
82 + version: 5.3.2
83 file-saver:
84 specifier: ^2.0.5
85 version: 2.0.5
@@ -87,8 +87,8 @@ importers:
87 specifier: ^2.6.0
88 version: 2.6.0
89 jose:
90 - specifier: ^6.1.0
91 - version: 6.1.0
90 + specifier: ^6.1.2
91 + version: 6.1.2
92 js-md5:
93 specifier: ^0.8.3
94 version: 0.8.3
@@ -99,8 +99,8 @@ importers:
99 specifier: ^3.0.1
100 version: 3.0.1
101 naive-ui:
102 - specifier: ^2.43.1
103 - version: 2.43.1(vue@3.5.22(typescript@5.9.3))
102 + specifier: ^2.43.2
103 + version: 2.43.2(vue@3.5.25(typescript@5.9.3))
104 nanoid:
105 specifier: ^5.1.6
106 version: 5.1.6
@@ -108,75 +108,75 @@ importers:
108 specifier: ^5.3.0
109 version: 5.3.0
110 pinia:
111 - specifier: ^3.0.3
112 - version: 3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))
111 + specifier: ^3.0.4
112 + version: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
113 pinia-plugin-persistedstate:
114 - specifier: ^4.5.0
115 - version: 4.5.0(@nuxt/kit@3.20.0)(pinia@3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)))
114 + specifier: ^4.7.1
115 + version: 4.7.1(@nuxt/kit@3.20.0)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)))
116 secure-ls:
117 specifier: ^2.0.0
118 version: 2.0.0
119 shiki:
120 - specifier: ^3.14.0
121 - version: 3.14.0
120 + specifier: ^3.17.0
121 + version: 3.17.0
122 thememirror:
123 specifier: ^2.0.1
124 - version: 2.0.1(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)
124 + version: 2.0.1(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)
125 validator:
126 - specifier: ^13.15.20
127 - version: 13.15.20
126 + specifier: ^13.15.23
127 + version: 13.15.23
128 vue:
129 - specifier: ^3.5.22
130 - version: 3.5.22(typescript@5.9.3)
129 + specifier: ^3.5.25
130 + version: 3.5.25(typescript@5.9.3)
131 vue-advanced-cropper:
132 specifier: ^2.8.9
133 - version: 2.8.9(vue@3.5.22(typescript@5.9.3))
133 + version: 2.8.9(vue@3.5.25(typescript@5.9.3))
134 vue-codemirror:
135 specifier: ^6.1.1
136 - version: 6.1.1(codemirror@6.0.2)(vue@3.5.22(typescript@5.9.3))
136 + version: 6.1.1(codemirror@6.0.2)(vue@3.5.25(typescript@5.9.3))
137 vue-highlight-words:
138 specifier: ^3.0.1
139 - version: 3.0.1(vue@3.5.22(typescript@5.9.3))
139 + version: 3.0.1(vue@3.5.25(typescript@5.9.3))
140 vue-i18n:
141 - specifier: ^11.1.12
142 - version: 11.1.12(vue@3.5.22(typescript@5.9.3))
141 + specifier: ^11.2.2
142 + version: 11.2.2(vue@3.5.25(typescript@5.9.3))
143 vue-router:
144 specifier: ^4.6.3
145 - version: 4.6.3(vue@3.5.22(typescript@5.9.3))
145 + version: 4.6.3(vue@3.5.25(typescript@5.9.3))
146 vue-sjv:
147 specifier: ^0.0.6
148 - version: 0.0.6(vue@3.5.22(typescript@5.9.3))
148 + version: 0.0.6(vue@3.5.25(typescript@5.9.3))
149 vue3-apexcharts:
150 specifier: ^1.10.0
151 - version: 1.10.0(apexcharts@5.3.5)(vue@3.5.22(typescript@5.9.3))
151 + version: 1.10.0(apexcharts@5.3.5)(vue@3.5.25(typescript@5.9.3))
152 vue3-marquee:
153 specifier: ^4.2.2
154 - version: 4.2.2(vue@3.5.22(typescript@5.9.3))
154 + version: 4.2.2(vue@3.5.25(typescript@5.9.3))
155 vuedraggable:
156 specifier: ^4.1.0
157 - version: 4.1.0(vue@3.5.22(typescript@5.9.3))
157 + version: 4.1.0(vue@3.5.25(typescript@5.9.3))
158 xmllint:
159 specifier: ^0.1.1
160 version: 0.1.1
161 xmllint-wasm:
162 specifier: ^5.1.0
163 - version: 5.1.0(@types/node@24.9.2)
163 + version: 5.1.0(@types/node@24.10.1)
164 devDependencies:
165 '@antfu/eslint-config':
166 - specifier: ~6.1.0
167 - version: 6.1.0(@vue/compiler-sfc@3.5.22)(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.5(@types/debug@4.1.12)(@types/node@24.9.2)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))
166 + specifier: ~6.2.0
167 + version: 6.2.0(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.14(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))
168 '@clack/prompts':
169 specifier: ^0.11.0
170 version: 0.11.0
171 '@iconify/vue':
172 specifier: ^5.0.0
173 - version: 5.0.0(vue@3.5.22(typescript@5.9.3))
173 + version: 5.0.0(vue@3.5.25(typescript@5.9.3))
174 '@tailwindcss/vite':
175 - specifier: ^4.1.16
176 - version: 4.1.16(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))
175 + specifier: ^4.1.17
176 + version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))
177 '@tsconfig/node20':
178 - specifier: ^20.1.6
179 - version: 20.1.6
178 + specifier: ^20.1.8
179 + version: 20.1.8
180 '@types/bytes':
181 specifier: ^3.1.5
182 version: 3.1.5
@@ -190,38 +190,38 @@ importers:
190 specifier: ^27.0.0
191 version: 27.0.0
192 '@types/lodash':
193 - specifier: ^4.17.20
194 - version: 4.17.20
193 + specifier: ^4.17.21
194 + version: 4.17.21
195 '@types/markdown-it':
196 specifier: ^14.1.2
197 version: 14.1.2
198 '@types/node':
199 - specifier: ^24.9.2
200 - version: 24.9.2
199 + specifier: ^24.10.1
200 + version: 24.10.1
201 '@types/validator':
202 - specifier: ^13.15.4
203 - version: 13.15.4
202 + specifier: ^13.15.10
203 + version: 13.15.10
204 '@vitejs/plugin-vue':
205 - specifier: ^6.0.1
206 - version: 6.0.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
205 + specifier: ^6.0.2
206 + version: 6.0.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))
207 '@vitejs/plugin-vue-jsx':
208 - specifier: ^5.1.1
209 - version: 5.1.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
208 + specifier: ^5.1.2
209 + version: 5.1.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))
210 '@vue/test-utils':
211 specifier: ^2.4.6
212 version: 2.4.6
213 '@vue/tsconfig':
214 specifier: ~0.7.0
215 - version: 0.7.0(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))
215 + version: 0.7.0(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
216 cypress:
217 - specifier: ^15.5.0
218 - version: 15.5.0
217 + specifier: ^15.7.0
218 + version: 15.7.0
219 depcheck:
220 specifier: ^1.4.7
221 version: 1.4.7
222 eslint:
223 - specifier: ~9.38.0
224 - version: 9.38.0(jiti@2.6.1)
223 + specifier: ~9.39.1
224 + version: 9.39.1(jiti@2.6.1)
225 flourite:
226 specifier: ^1.3.0
227 version: 1.3.0
@@ -229,56 +229,56 @@ importers:
229 specifier: ^11.3.2
230 version: 11.3.2
231 jsdom:
232 - specifier: ^27.0.1
233 - version: 27.1.0
232 + specifier: ^27.2.0
233 + version: 27.2.0
234 npm-run-all2:
235 specifier: ^8.0.4
236 version: 8.0.4
237 prettier:
238 - specifier: ^3.6.2
239 - version: 3.6.2
238 + specifier: ^3.7.3
239 + version: 3.7.3
240 prettier-plugin-tailwindcss:
241 specifier: ^0.7.1
242 - version: 0.7.1(prettier@3.6.2)
242 + version: 0.7.1(prettier@3.7.3)
243 sass:
244 - specifier: ^1.93.2
245 - version: 1.93.2
244 + specifier: ^1.94.2
245 + version: 1.94.2
246 start-server-and-test:
247 - specifier: ^2.1.2
248 - version: 2.1.2
247 + specifier: ^2.1.3
248 + version: 2.1.3
249 tailwindcss:
250 - specifier: ^4.1.16
251 - version: 4.1.16
250 + specifier: ^4.1.17
251 + version: 4.1.17
252 taze:
253 - specifier: ^19.9.0
254 - version: 19.9.0
253 + specifier: ^19.9.2
254 + version: 19.9.2
255 type-fest:
256 - specifier: ^5.1.0
257 - version: 5.1.0
256 + specifier: ^5.2.0
257 + version: 5.2.0
258 typescript:
259 specifier: ~5.9.3
260 version: 5.9.3
261 vite:
262 - specifier: ^7.1.12
263 - version: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)
262 + specifier: ^7.2.4
263 + version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
264 vite-bundle-visualizer:
265 specifier: ^1.2.1
266 version: 1.2.1(rollup@4.52.5)
267 vite-plugin-inspect:
268 specifier: ^11.3.3
269 - version: 11.3.3(@nuxt/kit@3.20.0)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))
269 + version: 11.3.3(@nuxt/kit@3.20.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))
270 vite-plugin-vue-devtools:
271 - specifier: ^8.0.3
272 - version: 8.0.3(@nuxt/kit@3.20.0)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
271 + specifier: ^8.0.5
272 + version: 8.0.5(@nuxt/kit@3.20.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))
273 vite-svg-loader:
274 specifier: ^5.1.0
275 - version: 5.1.0(vue@3.5.22(typescript@5.9.3))
275 + version: 5.1.0(vue@3.5.25(typescript@5.9.3))
276 vitest:
277 - specifier: ^4.0.5
278 - version: 4.0.5(@types/debug@4.1.12)(@types/node@24.9.2)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)
277 + specifier: ^4.0.14
278 + version: 4.0.14(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
279 vue-tsc:
280 - specifier: ^3.1.2
281 - version: 3.1.2(typescript@5.9.3)
280 + specifier: ^3.1.5
281 + version: 3.1.5(typescript@5.9.3)
282 optionalDependencies:
283 '@rollup/rollup-linux-x64-gnu':
284 specifier: ^4.46.2
@@ -288,12 +288,12 @@ importers:
288 version: 0.3.11
289 vueuc:
290 specifier: ^0.4.64
291 - version: 0.4.65(vue@3.5.22(typescript@5.9.3))
291 + version: 0.4.65(vue@3.5.25(typescript@5.9.3))
292
293 packages:
294
295 - '@acemir/cssom@0.9.19':
296 - resolution: {integrity: sha512-Pp2gAQXPZ2o7lt4j0IMwNRXqQ3pagxtDj5wctL5U2Lz4oV0ocDNlkgx4DpxfyKav4S/bePuI+SMqcBSUHLy9kg==}
295 + '@acemir/cssom@0.9.24':
296 + resolution: {integrity: sha512-5YjgMmAiT2rjJZU7XK1SNI7iqTy92DpaYVgG6x63FxkJ11UpYfLndHJATtinWJClAXiOlW9XWaUyAQf8pMrQPg==}
297
298 '@ajoelp/json-to-formdata@1.5.0':
299 resolution: {integrity: sha512-nrlfeTSL0X0dtx5r2KpzPiqLSIQquiiJjUKsQAKzWaCmO2QoYZCyb5ENZwF3YoffKronOCJr25mxaD8JRJmK8w==}
@@ -354,8 +354,8 @@ packages:
354 resolution: {integrity: sha512-gkLNpU+b1pCIwk1hKTJz2NWQPT8gsfGhQasnZ5QVv4jd79fKRL/1ikd86P0AzuIQs9tbbhlMwxsSTyJmlq502w==}
355 engines: {node: '>= 14.0.0'}
356
357 - '@antfu/eslint-config@6.1.0':
358 - resolution: {integrity: sha512-m/L9TGvtG3r4tkfq5BY6THz7pk0g6yuJwwA0SkLEDHJJpt0upuABhs8v3SU8yaPtCGUxq8k2QTLMZ3WPg4vSdw==}
357 + '@antfu/eslint-config@6.2.0':
358 + resolution: {integrity: sha512-ksasd+mJk441HHodwPh3Nhmwo9jSHUmgQyfTxMQM05U7SjDS0fy2KnXnPx0Vhc/CqKiJnx8wGpQCCJibyASX9Q==}
359 hasBin: true
360 peerDependencies:
361 '@eslint-react/eslint-plugin': ^2.0.1
@@ -420,8 +420,8 @@ packages:
420 '@asamuzakjp/css-color@4.0.5':
421 resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==}
422
423 - '@asamuzakjp/dom-selector@6.7.3':
424 - resolution: {integrity: sha512-kiGFeY+Hxf5KbPpjRLf+ffWbkos1aGo8MBfd91oxS3O57RgU3XhZrt/6UzoVF9VMpWbC3v87SRc9jxGrc9qHtQ==}
423 + '@asamuzakjp/dom-selector@6.7.4':
424 + resolution: {integrity: sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==}
425
426 '@asamuzakjp/nwsapi@2.3.9':
427 resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
@@ -587,8 +587,8 @@ packages:
587 '@codemirror/language@6.11.3':
588 resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==}
589
590 - '@codemirror/lint@6.9.1':
591 - resolution: {integrity: sha512-te7To1EQHePBQQzasDKWmK2xKINIXpk+xAiSYr9ZN+VB4KaT+/Hi2PEkeErTk5BV3PTz1TLyQL4MtJfPkKZ9sw==}
590 + '@codemirror/lint@6.9.2':
591 + resolution: {integrity: sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==}
592
593 '@codemirror/search@6.5.11':
594 resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==}
@@ -599,8 +599,8 @@ packages:
599 '@codemirror/theme-one-dark@6.1.3':
600 resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==}
601
602 - '@codemirror/view@6.38.6':
603 - resolution: {integrity: sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==}
602 + '@codemirror/view@6.38.8':
603 + resolution: {integrity: sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==}
604
605 '@css-render/plugin-bem@0.15.14':
606 resolution: {integrity: sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==}
@@ -855,10 +855,6 @@ packages:
855 resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
856 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
857
858 - '@eslint/core@0.15.2':
859 - resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==}
860 - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
861 -
858 '@eslint/core@0.16.0':
859 resolution: {integrity: sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==}
860 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -871,8 +867,8 @@ packages:
867 resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
868 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
869
874 - '@eslint/js@9.38.0':
875 - resolution: {integrity: sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==}
870 + '@eslint/js@9.39.1':
871 + resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==}
872 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
873
874 '@eslint/markdown@7.5.0':
@@ -883,10 +879,6 @@ packages:
879 resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
880 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
881
886 - '@eslint/plugin-kit@0.3.5':
887 - resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==}
888 - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
889 -
882 '@eslint/plugin-kit@0.4.1':
883 resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
884 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -949,16 +941,16 @@ packages:
941 peerDependencies:
942 vue: '>=3'
943
952 - '@intlify/core-base@11.1.12':
953 - resolution: {integrity: sha512-whh0trqRsSqVLNEUCwU59pyJZYpU8AmSWl8M3Jz2Mv5ESPP6kFh4juas2NpZ1iCvy7GlNRffUD1xr84gceimjg==}
944 + '@intlify/core-base@11.2.2':
945 + resolution: {integrity: sha512-0mCTBOLKIqFUP3BzwuFW23hYEl9g/wby6uY//AC5hTgQfTsM2srCYF2/hYGp+a5DZ/HIFIgKkLJMzXTt30r0JQ==}
946 engines: {node: '>= 16'}
947
956 - '@intlify/message-compiler@11.1.12':
957 - resolution: {integrity: sha512-Fv9iQSJoJaXl4ZGkOCN1LDM3trzze0AS2zRz2EHLiwenwL6t0Ki9KySYlyr27yVOj5aVz0e55JePO+kELIvfdQ==}
948 + '@intlify/message-compiler@11.2.2':
949 + resolution: {integrity: sha512-XS2p8Ff5JxWsKhgfld4/MRQzZRQ85drMMPhb7Co6Be4ZOgqJX1DzcZt0IFgGTycgqL8rkYNwgnD443Q+TapOoA==}
950 engines: {node: '>= 16'}
951
960 - '@intlify/shared@11.1.12':
961 - resolution: {integrity: sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A==}
952 + '@intlify/shared@11.2.2':
953 + resolution: {integrity: sha512-OtCmyFpSXxNu/oET/aN6HtPCbZ01btXVd0f3w00YsHOb13Kverk1jzA2k47pAekM55qbUw421fvPF1yxZ+gicw==}
954 engines: {node: '>= 16'}
955
956 '@isaacs/cliui@8.0.2':
@@ -1117,11 +1109,11 @@ packages:
1109 '@quansync/fs@0.1.5':
1110 resolution: {integrity: sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA==}
1111
1120 - '@rolldown/pluginutils@1.0.0-beta.29':
1121 - resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==}
1112 + '@rolldown/pluginutils@1.0.0-beta.50':
1113 + resolution: {integrity: sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==}
1114
1123 - '@rolldown/pluginutils@1.0.0-beta.45':
1124 - resolution: {integrity: sha512-Le9ulGCrD8ggInzWw/k2J8QcbPz7eGIOWqfJ2L+1R0Opm7n6J37s2hiDWlh6LJN0Lk9L5sUzMvRHKW7UxBZsQA==}
1115 + '@rolldown/pluginutils@1.0.0-beta.52':
1116 + resolution: {integrity: sha512-/L0htLJZbaZFL1g9OHOblTxbCYIGefErJjtYOwgl9ZqNx27P3L0SDfjhhHIss32gu5NWgnxuT2a2Hnnv6QGHKA==}
1117
1118 '@rollup/rollup-android-arm-eabi@4.52.5':
1119 resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==}
@@ -1233,31 +1225,31 @@ packages:
1225 cpu: [x64]
1226 os: [win32]
1227
1236 - '@shikijs/core@3.14.0':
1237 - resolution: {integrity: sha512-qRSeuP5vlYHCNUIrpEBQFO7vSkR7jn7Kv+5X3FO/zBKVDGQbcnlScD3XhkrHi/R8Ltz0kEjvFR9Szp/XMRbFMw==}
1228 + '@shikijs/core@3.17.0':
1229 + resolution: {integrity: sha512-/HjeOnbc62C+n33QFNFrAhUlIADKwfuoS50Ht0pxujxP4QjZAlFp5Q+OkDo531SCTzivx5T18khwyBdKoPdkuw==}
1230
1239 - '@shikijs/engine-javascript@3.14.0':
1240 - resolution: {integrity: sha512-3v1kAXI2TsWQuwv86cREH/+FK9Pjw3dorVEykzQDhwrZj0lwsHYlfyARaKmn6vr5Gasf8aeVpb8JkzeWspxOLQ==}
1231 + '@shikijs/engine-javascript@3.17.0':
1232 + resolution: {integrity: sha512-WwF99xdP8KfuDrIbT4wxyypfhoIxMeeOCp1AiuvzzZ6JT5B3vIuoclL8xOuuydA6LBeeNXUF/XV5zlwwex1jlA==}
1233
1242 - '@shikijs/engine-oniguruma@3.14.0':
1243 - resolution: {integrity: sha512-TNcYTYMbJyy+ZjzWtt0bG5y4YyMIWC2nyePz+CFMWqm+HnZZyy9SWMgo8Z6KBJVIZnx8XUXS8U2afO6Y0g1Oug==}
1234 + '@shikijs/engine-oniguruma@3.17.0':
1235 + resolution: {integrity: sha512-flSbHZAiOZDNTrEbULY8DLWavu/TyVu/E7RChpLB4WvKX4iHMfj80C6Hi3TjIWaQtHOW0KC6kzMcuB5TO1hZ8Q==}
1236
1245 - '@shikijs/langs@3.14.0':
1246 - resolution: {integrity: sha512-DIB2EQY7yPX1/ZH7lMcwrK5pl+ZkP/xoSpUzg9YC8R+evRCCiSQ7yyrvEyBsMnfZq4eBzLzBlugMyTAf13+pzg==}
1237 + '@shikijs/langs@3.17.0':
1238 + resolution: {integrity: sha512-icmur2n5Ojb+HAiQu6NEcIIJ8oWDFGGEpiqSCe43539Sabpx7Y829WR3QuUW2zjTM4l6V8Sazgb3rrHO2orEAw==}
1239
1248 - '@shikijs/markdown-it@3.14.0':
1249 - resolution: {integrity: sha512-VzlpeD/PsKXcQxvSxYPbR32o5T7L8+eCx0hdTymA9J3i7kFlv2GD59Lth2/XoRIF4aN28FIuHdb1C0I7Zg1gbw==}
1240 + '@shikijs/markdown-it@3.17.0':
1241 + resolution: {integrity: sha512-2DYbo4hi79UkloTHZJsqKooHjLbMQew8yLIcM7P9tuThL52HKIP+Gy9WY9WzkCmcdOufEkUcnjoBjoxrx1rMhA==}
1242 peerDependencies:
1243 markdown-it-async: ^2.2.0
1244 peerDependenciesMeta:
1245 markdown-it-async:
1246 optional: true
1247
1256 - '@shikijs/themes@3.14.0':
1257 - resolution: {integrity: sha512-fAo/OnfWckNmv4uBoUu6dSlkcBc+SA1xzj5oUSaz5z3KqHtEbUypg/9xxgJARtM6+7RVm0Q6Xnty41xA1ma1IA==}
1248 + '@shikijs/themes@3.17.0':
1249 + resolution: {integrity: sha512-/xEizMHLBmMHwtx4JuOkRf3zwhWD2bmG5BRr0IPjpcWpaq4C3mYEuTk/USAEglN0qPrTwEHwKVpSu/y2jhferA==}
1250
1259 - '@shikijs/types@3.14.0':
1260 - resolution: {integrity: sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ==}
1251 + '@shikijs/types@3.17.0':
1252 + resolution: {integrity: sha512-wjLVfutYWVUnxAjsWEob98xgyaGv0dTEnMZDruU5mRjVN7szcGOfgO+997W2yR6odp+1PtSBNeSITRRTfUzK/g==}
1253
1254 '@shikijs/vscode-textmate@10.0.2':
1255 resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
@@ -1303,65 +1295,65 @@ packages:
1295 peerDependencies:
1296 '@svgdotjs/svg.js': ^3.2.4
1297
1306 - '@tailwindcss/node@4.1.16':
1307 - resolution: {integrity: sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==}
1298 + '@tailwindcss/node@4.1.17':
1299 + resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==}
1300
1309 - '@tailwindcss/oxide-android-arm64@4.1.16':
1310 - resolution: {integrity: sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==}
1301 + '@tailwindcss/oxide-android-arm64@4.1.17':
1302 + resolution: {integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==}
1303 engines: {node: '>= 10'}
1304 cpu: [arm64]
1305 os: [android]
1306
1315 - '@tailwindcss/oxide-darwin-arm64@4.1.16':
1316 - resolution: {integrity: sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==}
1307 + '@tailwindcss/oxide-darwin-arm64@4.1.17':
1308 + resolution: {integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==}
1309 engines: {node: '>= 10'}
1310 cpu: [arm64]
1311 os: [darwin]
1312
1321 - '@tailwindcss/oxide-darwin-x64@4.1.16':
1322 - resolution: {integrity: sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==}
1313 + '@tailwindcss/oxide-darwin-x64@4.1.17':
1314 + resolution: {integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==}
1315 engines: {node: '>= 10'}
1316 cpu: [x64]
1317 os: [darwin]
1318
1327 - '@tailwindcss/oxide-freebsd-x64@4.1.16':
1328 - resolution: {integrity: sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==}
1319 + '@tailwindcss/oxide-freebsd-x64@4.1.17':
1320 + resolution: {integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==}
1321 engines: {node: '>= 10'}
1322 cpu: [x64]
1323 os: [freebsd]
1324
1333 - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16':
1334 - resolution: {integrity: sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==}
1325 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17':
1326 + resolution: {integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==}
1327 engines: {node: '>= 10'}
1328 cpu: [arm]
1329 os: [linux]
1330
1339 - '@tailwindcss/oxide-linux-arm64-gnu@4.1.16':
1340 - resolution: {integrity: sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==}
1331 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17':
1332 + resolution: {integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==}
1333 engines: {node: '>= 10'}
1334 cpu: [arm64]
1335 os: [linux]
1336
1345 - '@tailwindcss/oxide-linux-arm64-musl@4.1.16':
1346 - resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==}
1337 + '@tailwindcss/oxide-linux-arm64-musl@4.1.17':
1338 + resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==}
1339 engines: {node: '>= 10'}
1340 cpu: [arm64]
1341 os: [linux]
1342
1351 - '@tailwindcss/oxide-linux-x64-gnu@4.1.16':
1352 - resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==}
1343 + '@tailwindcss/oxide-linux-x64-gnu@4.1.17':
1344 + resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==}
1345 engines: {node: '>= 10'}
1346 cpu: [x64]
1347 os: [linux]
1348
1357 - '@tailwindcss/oxide-linux-x64-musl@4.1.16':
1358 - resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==}
1349 + '@tailwindcss/oxide-linux-x64-musl@4.1.17':
1350 + resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==}
1351 engines: {node: '>= 10'}
1352 cpu: [x64]
1353 os: [linux]
1354
1363 - '@tailwindcss/oxide-wasm32-wasi@4.1.16':
1364 - resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==}
1355 + '@tailwindcss/oxide-wasm32-wasi@4.1.17':
1356 + resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==}
1357 engines: {node: '>=14.0.0'}
1358 cpu: [wasm32]
1359 bundledDependencies:
@@ -1372,24 +1364,24 @@ packages:
1364 - '@emnapi/wasi-threads'
1365 - tslib
1366
1375 - '@tailwindcss/oxide-win32-arm64-msvc@4.1.16':
1376 - resolution: {integrity: sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==}
1367 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17':
1368 + resolution: {integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==}
1369 engines: {node: '>= 10'}
1370 cpu: [arm64]
1371 os: [win32]
1372
1381 - '@tailwindcss/oxide-win32-x64-msvc@4.1.16':
1382 - resolution: {integrity: sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==}
1373 + '@tailwindcss/oxide-win32-x64-msvc@4.1.17':
1374 + resolution: {integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==}
1375 engines: {node: '>= 10'}
1376 cpu: [x64]
1377 os: [win32]
1378
1387 - '@tailwindcss/oxide@4.1.16':
1388 - resolution: {integrity: sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==}
1379 + '@tailwindcss/oxide@4.1.17':
1380 + resolution: {integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==}
1381 engines: {node: '>= 10'}
1382
1391 - '@tailwindcss/vite@4.1.16':
1392 - resolution: {integrity: sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg==}
1383 + '@tailwindcss/vite@4.1.17':
1384 + resolution: {integrity: sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==}
1385 peerDependencies:
1386 vite: ^5.2.0 || ^6 || ^7
1387
@@ -1397,8 +1389,8 @@ packages:
1389 resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
1390 engines: {node: '>=10.13.0'}
1391
1400 - '@tsconfig/node20@20.1.6':
1401 - resolution: {integrity: sha512-sz+Hqx9zwZDpZIV871WSbUzSqNIsXzghZydypnfgzPKLltVJfkINfUeTct31n/tTSa9ZE1ZOfKdRre1uHHquYQ==}
1392 + '@tsconfig/node20@20.1.8':
1393 + resolution: {integrity: sha512-Em+IdPfByIzWRRpqWL4Z7ArLHZGxmc36BxE3jCz9nBFSm+5aLaPMZyjwu4yetvyKXeogWcxik4L1jB5JTWfw7A==}
1394
1395 '@types/bytes@3.1.5':
1396 resolution: {integrity: sha512-VgZkrJckypj85YxEsEavcMmmSOIzkUHqWmM4CCyia5dc54YwsXzJ5uT4fYxBQNEXx+oF1krlhgCbvfubXqZYsQ==}
@@ -1445,8 +1437,8 @@ packages:
1437 '@types/lodash-es@4.17.12':
1438 resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
1439
1448 - '@types/lodash@4.17.20':
1449 - resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
1440 + '@types/lodash@4.17.21':
1441 + resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==}
1442
1443 '@types/markdown-it@14.1.2':
1444 resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
@@ -1463,8 +1455,8 @@ packages:
1455 '@types/ms@2.1.0':
1456 resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
1457
1466 - '@types/node@24.9.2':
1467 - resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==}
1458 + '@types/node@24.10.1':
1459 + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
1460
1461 '@types/parse-json@4.0.2':
1462 resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
@@ -1487,8 +1479,8 @@ packages:
1479 '@types/unist@3.0.3':
1480 resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
1481
1490 - '@types/validator@13.15.4':
1491 - resolution: {integrity: sha512-LSFfpSnJJY9wbC0LQxgvfb+ynbHftFo0tMsFOl/J4wexLnYMmDSPaj2ZyDv3TkfL1UePxPrxOWJfbiRS8mQv7A==}
1482 + '@types/validator@13.15.10':
1483 + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
1484
1485 '@types/web-bluetooth@0.0.21':
1486 resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
@@ -1558,15 +1550,15 @@ packages:
1550 '@ungap/structured-clone@1.3.0':
1551 resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
1552
1561 - '@vitejs/plugin-vue-jsx@5.1.1':
1562 - resolution: {integrity: sha512-uQkfxzlF8SGHJJVH966lFTdjM/lGcwJGzwAHpVqAPDD/QcsqoUGa+q31ox1BrUfi+FLP2ChVp7uLXE3DkHyDdQ==}
1553 + '@vitejs/plugin-vue-jsx@5.1.2':
1554 + resolution: {integrity: sha512-3a2BOryRjG/Iih87x87YXz5c8nw27eSlHytvSKYfp8ZIsp5+FgFQoKeA7k2PnqWpjJrv6AoVTMnvmuKUXb771A==}
1555 engines: {node: ^20.19.0 || >=22.12.0}
1556 peerDependencies:
1557 vite: ^5.0.0 || ^6.0.0 || ^7.0.0
1558 vue: ^3.0.0
1559
1568 - '@vitejs/plugin-vue@6.0.1':
1569 - resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==}
1560 + '@vitejs/plugin-vue@6.0.2':
1561 + resolution: {integrity: sha512-iHmwV3QcVGGvSC1BG5bZ4z6iwa1SOpAPWmnjOErd4Ske+lZua5K9TtAVdx0gMBClJ28DViCbSmZitjWZsWO3LA==}
1562 engines: {node: ^20.19.0 || >=22.12.0}
1563 peerDependencies:
1564 vite: ^5.0.0 || ^6.0.0 || ^7.0.0
@@ -1585,11 +1577,11 @@ packages:
1577 vitest:
1578 optional: true
1579
1588 - '@vitest/expect@4.0.5':
1589 - resolution: {integrity: sha512-DJctLVlKoddvP/G389oGmKWNG6GD9frm2FPXARziU80Rjo7SIYxQzb2YFzmQ4fVD3Q5utUYY8nUmWrqsuIlIXQ==}
1580 + '@vitest/expect@4.0.14':
1581 + resolution: {integrity: sha512-RHk63V3zvRiYOWAV0rGEBRO820ce17hz7cI2kDmEdfQsBjT2luEKB5tCOc91u1oSQoUOZkSv3ZyzkdkSLD7lKw==}
1582
1591 - '@vitest/mocker@4.0.5':
1592 - resolution: {integrity: sha512-iYHIy72LfbK+mL5W8zXROp6oOcJKXWeKcNjcPPsqoa18qIEDrhB6/Z08o0wRajTd6SSSDNw8NCSIHVNOMpz0mw==}
1583 + '@vitest/mocker@4.0.14':
1584 + resolution: {integrity: sha512-RzS5NujlCzeRPF1MK7MXLiEFpkIXeMdQ+rN3Kk3tDI9j0mtbr7Nmuq67tpkOJQpgyClbOltCXMjLZicJHsH5Cg==}
1585 peerDependencies:
1586 msw: ^2.4.9
1587 vite: ^6.0.0 || ^7.0.0-0
@@ -1599,20 +1591,20 @@ packages:
1591 vite:
1592 optional: true
1593
1602 - '@vitest/pretty-format@4.0.5':
1603 - resolution: {integrity: sha512-t1T/sSdsYyNc5AZl0EMeD0jW9cpJe2cODP0R++ZQe1kTkpgrwEfxGFR/yCG4w8ZybizbXRTHU7lE8sTDD/QsGw==}
1594 + '@vitest/pretty-format@4.0.14':
1595 + resolution: {integrity: sha512-SOYPgujB6TITcJxgd3wmsLl+wZv+fy3av2PpiPpsWPZ6J1ySUYfScfpIt2Yv56ShJXR2MOA6q2KjKHN4EpdyRQ==}
1596
1605 - '@vitest/runner@4.0.5':
1606 - resolution: {integrity: sha512-CQVVe+YEeKSiFBD5gBAmRDQglm4PnMBYzeTmt06t5iWtsUN9StQeeKhYCea/oaqBYilf8sARG6fSctUcEL/UmQ==}
1597 + '@vitest/runner@4.0.14':
1598 + resolution: {integrity: sha512-BsAIk3FAqxICqREbX8SetIteT8PiaUL/tgJjmhxJhCsigmzzH8xeadtp7LRnTpCVzvf0ib9BgAfKJHuhNllKLw==}
1599
1608 - '@vitest/snapshot@4.0.5':
1609 - resolution: {integrity: sha512-jfmSAeR6xYNEvcD+/RxFGA1bzpqHtkVhgxo2cxXia+Q3xX7m6GpZij07rz+WyQcA/xEGn4eIS1OItkMyWsGBmQ==}
1600 + '@vitest/snapshot@4.0.14':
1601 + resolution: {integrity: sha512-aQVBfT1PMzDSA16Y3Fp45a0q8nKexx6N5Amw3MX55BeTeZpoC08fGqEZqVmPcqN0ueZsuUQ9rriPMhZ3Mu19Ag==}
1602
1611 - '@vitest/spy@4.0.5':
1612 - resolution: {integrity: sha512-TUmVQpAQign7r8+EnZsgTF3vY9BdGofTUge1rGNbnHn2IN3FChiQoT9lrPz7A7AVUZJU2LAZXl4v66HhsNMhoA==}
1603 + '@vitest/spy@4.0.14':
1604 + resolution: {integrity: sha512-JmAZT1UtZooO0tpY3GRyiC/8W7dCs05UOq9rfsUUgEZEdq+DuHLmWhPsrTt0TiW7WYeL/hXpaE07AZ2RCk44hg==}
1605
1614 - '@vitest/utils@4.0.5':
1615 - resolution: {integrity: sha512-V5RndUgCB5/AfNvK9zxGCrRs99IrPYtMTIdUzJMMFs9nrmE5JXExIEfjVtUteyTRiLfCm+dCRMHf/Uu7Mm8/dg==}
1606 + '@vitest/utils@4.0.14':
1607 + resolution: {integrity: sha512-hLqXZKAWNg8pI+SQXyXxWCTOpA3MvsqcbVeNgSi8x/CSN2wi26dSzn1wrOhmCmFjEvN9p8/kLFRHa6PI8jHazw==}
1608
1609 '@volar/language-core@2.4.23':
1610 resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==}
@@ -1626,6 +1618,9 @@ packages:
1618 '@vue/babel-helper-vue-transform-on@1.5.0':
1619 resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==}
1620
1621 + '@vue/babel-helper-vue-transform-on@2.0.1':
1622 + resolution: {integrity: sha512-uZ66EaFbnnZSYqYEyplWvn46GhZ1KuYSThdT68p+am7MgBNbQ3hphTL9L+xSIsWkdktwhPYLwPgVWqo96jDdRA==}
1623 +
1624 '@vue/babel-plugin-jsx@1.5.0':
1625 resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==}
1626 peerDependencies:
@@ -1634,71 +1629,99 @@ packages:
1629 '@babel/core':
1630 optional: true
1631
1632 + '@vue/babel-plugin-jsx@2.0.1':
1633 + resolution: {integrity: sha512-a8CaLQjD/s4PVdhrLD/zT574ZNPnZBOY+IhdtKWRB4HRZ0I2tXBi5ne7d9eCfaYwp5gU5+4KIyFTV1W1YL9xZA==}
1634 + peerDependencies:
1635 + '@babel/core': ^7.0.0-0
1636 + peerDependenciesMeta:
1637 + '@babel/core':
1638 + optional: true
1639 +
1640 '@vue/babel-plugin-resolve-type@1.5.0':
1641 resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==}
1642 peerDependencies:
1643 '@babel/core': ^7.0.0-0
1644
1645 + '@vue/babel-plugin-resolve-type@2.0.1':
1646 + resolution: {integrity: sha512-ybwgIuRGRRBhOU37GImDoWQoz+TlSqap65qVI6iwg/J7FfLTLmMf97TS7xQH9I7Qtr/gp161kYVdhr1ZMraSYQ==}
1647 + peerDependencies:
1648 + '@babel/core': ^7.0.0-0
1649 +
1650 '@vue/compiler-core@3.5.22':
1651 resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==}
1652
1653 + '@vue/compiler-core@3.5.25':
1654 + resolution: {integrity: sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==}
1655 +
1656 '@vue/compiler-dom@3.5.22':
1657 resolution: {integrity: sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==}
1658
1659 + '@vue/compiler-dom@3.5.25':
1660 + resolution: {integrity: sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==}
1661 +
1662 '@vue/compiler-sfc@3.5.22':
1663 resolution: {integrity: sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==}
1664
1665 + '@vue/compiler-sfc@3.5.25':
1666 + resolution: {integrity: sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==}
1667 +
1668 '@vue/compiler-ssr@3.5.22':
1669 resolution: {integrity: sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==}
1670
1671 + '@vue/compiler-ssr@3.5.25':
1672 + resolution: {integrity: sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==}
1673 +
1674 '@vue/devtools-api@6.6.4':
1675 resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
1676
1677 '@vue/devtools-api@7.7.7':
1678 resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==}
1679
1660 - '@vue/devtools-core@8.0.3':
1661 - resolution: {integrity: sha512-gCEQN7aMmeaigEWJQ2Z2o3g7/CMqGTPvNS1U3n/kzpLoAZ1hkAHNgi4ml/POn/9uqGILBk65GGOUdrraHXRj5Q==}
1680 + '@vue/devtools-core@8.0.5':
1681 + resolution: {integrity: sha512-dpCw8nl0GDBuiL9SaY0mtDxoGIEmU38w+TQiYEPOLhW03VDC0lfNMYXS/qhl4I0YlysGp04NLY4UNn6xgD0VIQ==}
1682 peerDependencies:
1683 vue: ^3.0.0
1684
1685 '@vue/devtools-kit@7.7.7':
1686 resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==}
1687
1668 - '@vue/devtools-kit@8.0.3':
1669 - resolution: {integrity: sha512-UF4YUOVGdfzXLCv5pMg2DxocB8dvXz278fpgEE+nJ/DRALQGAva7sj9ton0VWZ9hmXw+SV8yKMrxP2MpMhq9Wg==}
1688 + '@vue/devtools-kit@8.0.5':
1689 + resolution: {integrity: sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==}
1690
1691 '@vue/devtools-shared@7.7.7':
1692 resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==}
1693
1674 - '@vue/devtools-shared@8.0.3':
1675 - resolution: {integrity: sha512-s/QNll7TlpbADFZrPVsaUNPCOF8NvQgtgmmB7Tip6pLf/HcOvBTly0lfLQ0Eylu9FQ4OqBhFpLyBgwykiSf8zw==}
1694 + '@vue/devtools-shared@8.0.5':
1695 + resolution: {integrity: sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==}
1696
1677 - '@vue/language-core@3.1.2':
1678 - resolution: {integrity: sha512-PyFDCqpdfYUT+oMLqcc61oHfJlC6yjhybaefwQjRdkchIihToOEpJ2Wu/Ebq2yrnJdd1EsaAvZaXVAqcxtnDxQ==}
1697 + '@vue/language-core@3.1.5':
1698 + resolution: {integrity: sha512-FMcqyzWN+sYBeqRMWPGT2QY0mUasZMVIuHvmb5NT3eeqPrbHBYtCP8JWEUCDCgM+Zr62uuWY/qoeBrPrzfa78w==}
1699 peerDependencies:
1700 typescript: '*'
1701 peerDependenciesMeta:
1702 typescript:
1703 optional: true
1704
1685 - '@vue/reactivity@3.5.22':
1686 - resolution: {integrity: sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==}
1705 + '@vue/reactivity@3.5.25':
1706 + resolution: {integrity: sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==}
1707
1688 - '@vue/runtime-core@3.5.22':
1689 - resolution: {integrity: sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==}
1708 + '@vue/runtime-core@3.5.25':
1709 + resolution: {integrity: sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==}
1710
1691 - '@vue/runtime-dom@3.5.22':
1692 - resolution: {integrity: sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==}
1711 + '@vue/runtime-dom@3.5.25':
1712 + resolution: {integrity: sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==}
1713
1694 - '@vue/server-renderer@3.5.22':
1695 - resolution: {integrity: sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==}
1714 + '@vue/server-renderer@3.5.25':
1715 + resolution: {integrity: sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==}
1716 peerDependencies:
1697 - vue: 3.5.22
1717 + vue: 3.5.25
1718
1719 '@vue/shared@3.5.22':
1720 resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==}
1721
1722 + '@vue/shared@3.5.25':
1723 + resolution: {integrity: sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==}
1724 +
1725 '@vue/test-utils@2.4.6':
1726 resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==}
1727
@@ -1718,16 +1741,16 @@ packages:
1741 peerDependencies:
1742 vue: ^3.5.0
1743
1721 - '@vueuse/core@14.0.0':
1722 - resolution: {integrity: sha512-d6tKRWkZE8IQElX2aHBxXOMD478fHIYV+Dzm2y9Ag122ICBpNKtGICiXKOhWU3L1kKdttDD9dCMS4bGP3jhCTQ==}
1744 + '@vueuse/core@14.1.0':
1745 + resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==}
1746 peerDependencies:
1747 vue: ^3.5.0
1748
1749 '@vueuse/metadata@13.9.0':
1750 resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==}
1751
1729 - '@vueuse/metadata@14.0.0':
1730 - resolution: {integrity: sha512-6yoGqbJcMldVCevkFiHDBTB1V5Hq+G/haPlGIuaFZHpXC0HADB0EN1ryQAAceiW+ryS3niUwvdFbGiqHqBrfVA==}
1752 + '@vueuse/metadata@14.1.0':
1753 + resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==}
1754
1755 '@vueuse/motion@3.0.3':
1756 resolution: {integrity: sha512-4B+ITsxCI9cojikvrpaJcLXyq0spj3sdlzXjzesWdMRd99hhtFI6OJ/1JsqwtF73YooLe0hUn/xDR6qCtmn5GQ==}
@@ -1739,8 +1762,8 @@ packages:
1762 peerDependencies:
1763 vue: ^3.5.0
1764
1742 - '@vueuse/shared@14.0.0':
1743 - resolution: {integrity: sha512-mTCA0uczBgurRlwVaQHfG0Ja7UdGe4g9mwffiJmvLiTtp1G4AQyIjej6si/k8c8pUwTfVpNufck+23gXptPAkw==}
1765 + '@vueuse/shared@14.1.0':
1766 + resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==}
1767 peerDependencies:
1768 vue: ^3.5.0
1769
@@ -1869,8 +1892,8 @@ packages:
1892 aws4@1.13.2:
1893 resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==}
1894
1872 - axios@1.13.1:
1873 - resolution: {integrity: sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==}
1895 + axios@1.13.2:
1896 + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
1897
1898 balanced-match@1.0.2:
1899 resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -1977,8 +2000,8 @@ packages:
2000 ccount@2.0.1:
2001 resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
2002
1980 - chai@6.2.0:
1981 - resolution: {integrity: sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==}
2003 + chai@6.2.1:
2004 + resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==}
2005 engines: {node: '>=18'}
2006
2007 chalk@4.1.2:
@@ -2163,8 +2186,8 @@ packages:
2186 resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
2187 engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
2188
2166 - cssstyle@5.3.2:
2167 - resolution: {integrity: sha512-zDMqXh8Vs1CdRYZQ2M633m/SFgcjlu8RB8b/1h82i+6vpArF507NSYIWJHGlJaTWoS+imcnctmEz43txhbVkOw==}
2189 + cssstyle@5.3.3:
2190 + resolution: {integrity: sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==}
2191 engines: {node: '>=20'}
2192
2193 csstype@3.0.11:
@@ -2173,8 +2196,8 @@ packages:
2196 csstype@3.1.3:
2197 resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
2198
2176 - cypress@15.5.0:
2177 - resolution: {integrity: sha512-7jXBsh5hTfjxr9QQONC2IbdTj0nxSyU8x4eiarMZBzXzCj3pedKviUx8JnLcE4vL8e0TsOzp70WSLRORjEssRA==}
2199 + cypress@15.7.0:
2200 + resolution: {integrity: sha512-1C81zKxnQckYm2XGi37rPV4rN0bzUoWhydhKdOyshJn5gJKszEx5as9VLSZI0jp0ye49QxmnbU4TtMpcD+OmGQ==}
2201 engines: {node: ^20.1.0 || ^22.0.0 || >=24.0.0}
2202 hasBin: true
2203
@@ -2191,8 +2214,8 @@ packages:
2214 peerDependencies:
2215 date-fns: ^3.0.0 || ^4.0.0
2216
2194 - date-fns@3.6.0:
2195 - resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
2217 + date-fns@4.1.0:
2218 + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
2219
2220 dayjs@1.11.19:
2221 resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
@@ -2226,9 +2249,6 @@ packages:
2249 deep-is@0.1.4:
2250 resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
2251
2229 - deep-pick-omit@1.2.1:
2230 - resolution: {integrity: sha512-2J6Kc/m3irCeqVG42T+SaUMesaK7oGWaedGnQQK/+O0gYc+2SP5bKh/KKTE7d7SJ+GCA9UUE1GRzh6oDe0EnGw==}
2231 -
2252 default-browser-id@5.0.0:
2253 resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==}
2254 engines: {node: '>=18'}
@@ -2519,11 +2539,11 @@ packages:
2539 peerDependencies:
2540 eslint: '>=6.0.0'
2541
2522 - eslint-plugin-unicorn@61.0.2:
2523 - resolution: {integrity: sha512-zLihukvneYT7f74GNbVJXfWIiNQmkc/a9vYBTE4qPkQZswolWNdu+Wsp9sIXno1JOzdn6OUwLPd19ekXVkahRA==}
2542 + eslint-plugin-unicorn@62.0.0:
2543 + resolution: {integrity: sha512-HIlIkGLkvf29YEiS/ImuDZQbP12gWyx5i3C6XrRxMvVdqMroCI9qoVYCoIl17ChN+U89pn9sVwLxhIWj5nEc7g==}
2544 engines: {node: ^20.10.0 || >=21.0.0}
2545 peerDependencies:
2526 - eslint: '>=9.29.0'
2546 + eslint: '>=9.38.0'
2547
2548 eslint-plugin-unused-imports@4.3.0:
2549 resolution: {integrity: sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==}
@@ -2572,8 +2592,8 @@ packages:
2592 resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
2593 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
2594
2575 - eslint@9.38.0:
2576 - resolution: {integrity: sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==}
2595 + eslint@9.39.1:
2596 + resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==}
2597 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
2598 hasBin: true
2599 peerDependencies:
@@ -2674,8 +2694,8 @@ packages:
2694 fast-levenshtein@2.0.6:
2695 resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
2696
2677 - fast-xml-parser@5.3.0:
2678 - resolution: {integrity: sha512-gkWGshjYcQCF+6qtlrqBqELqNqnt4CxruY6UVAWWnqb3DQ6qaNFEIKqzYep1XzHLM/QtrHVCxyPOtTk4LTQ7Aw==}
2697 + fast-xml-parser@5.3.2:
2698 + resolution: {integrity: sha512-n8v8b6p4Z1sMgqRmqLJm3awW4NX7NkaKPfb3uJIBTSH7Pdvufi3PQ3/lJLQrvxcMYl7JI2jnDO90siPEpD8JBA==}
2699 hasBin: true
2700
2701 fastq@1.19.1:
@@ -3091,8 +3111,8 @@ packages:
3111 resolution: {integrity: sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA==}
3112 engines: {node: '>= 20'}
3113
3094 - jose@6.1.0:
3095 - resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==}
3114 + jose@6.1.2:
3115 + resolution: {integrity: sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ==}
3116
3117 js-beautify@1.15.4:
3118 resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==}
@@ -3132,8 +3152,8 @@ packages:
3152 resolution: {integrity: sha512-+LexoTRyYui5iOhJGn13N9ZazL23nAHGkXsa1p/C8yeq79WRfLBag6ZZ0FQG2aRoc9yfo59JT9EYCQonOkHKkQ==}
3153 engines: {node: '>=20.0.0'}
3154
3135 - jsdom@27.1.0:
3136 - resolution: {integrity: sha512-Pcfm3eZ+eO4JdZCXthW9tCDT3nF4K+9dmeZ+5X39n+Kqz0DDIABRP5CAEOHRFZk8RGuC2efksTJxrjp8EXCunQ==}
3155 + jsdom@27.2.0:
3156 + resolution: {integrity: sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==}
3157 engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
3158 peerDependencies:
3159 canvas: ^3.0.0
@@ -3141,11 +3161,6 @@ packages:
3161 canvas:
3162 optional: true
3163
3144 - jsesc@3.0.2:
3145 - resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==}
3146 - engines: {node: '>=6'}
3147 - hasBin: true
3148 -
3164 jsesc@3.1.0:
3165 resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
3166 engines: {node: '>=6'}
@@ -3568,8 +3583,8 @@ packages:
3583 resolution: {integrity: sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==}
3584 engines: {node: '>=10'}
3585
3571 - naive-ui@2.43.1:
3572 - resolution: {integrity: sha512-w52W0mOhdOGt4uucFSZmP0DI44PCsFyuxeLSs9aoUThfIuxms90MYjv46Qrr7xprjyJRw5RU6vYpCx4o9ind3A==}
3586 + naive-ui@2.43.2:
3587 + resolution: {integrity: sha512-YlLMnGrwGTOc+zMj90sG3ubaH5/7czsgLgGcjTLA981IUaz8r6t4WIujNt8r9PNr+dqv6XNEr0vxkARgPPjfBQ==}
3588 peerDependencies:
3589 vue: ^3.0.0
3590
@@ -3632,8 +3647,11 @@ packages:
3647 resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
3648 engines: {node: '>= 0.4'}
3649
3635 - ofetch@1.5.0:
3636 - resolution: {integrity: sha512-A7llJ7eZyziA5xq9//3ZurA8OhFqtS99K5/V1sLBJ5j137CM/OAjlbA/TEJXBuOWwOfLqih+oH5U3ran4za1FQ==}
3650 + obug@2.1.1:
3651 + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
3652 +
3653 + ofetch@1.5.1:
3654 + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==}
3655
3656 ohash@2.0.11:
3657 resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
@@ -3652,8 +3670,8 @@ packages:
3670 oniguruma-parser@0.12.1:
3671 resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==}
3672
3655 - oniguruma-to-es@4.3.3:
3656 - resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==}
3673 + oniguruma-to-es@4.3.4:
3674 + resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==}
3675
3676 open@10.2.0:
3677 resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
@@ -3780,8 +3798,8 @@ packages:
3798 resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
3799 engines: {node: '>=0.10.0'}
3800
3783 - pinia-plugin-persistedstate@4.5.0:
3784 - resolution: {integrity: sha512-QTkP1xJVyCdr2I2p3AKUZM84/e+IS+HktRxKGAIuDzkyaKKV48mQcYkJFVVDuvTxlI5j6X3oZObpqoVB8JnWpw==}
3801 + pinia-plugin-persistedstate@4.7.1:
3802 + resolution: {integrity: sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ==}
3803 peerDependencies:
3804 '@nuxt/kit': '>=3.0.0'
3805 '@pinia/nuxt': '>=0.10.0'
@@ -3794,11 +3812,11 @@ packages:
3812 pinia:
3813 optional: true
3814
3797 - pinia@3.0.3:
3798 - resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==}
3815 + pinia@3.0.4:
3816 + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==}
3817 peerDependencies:
3800 - typescript: '>=4.4.4'
3801 - vue: ^2.7.0 || ^3.5.11
3818 + typescript: '>=4.5.0'
3819 + vue: ^3.5.11
3820 peerDependenciesMeta:
3821 typescript:
3822 optional: true
@@ -3889,8 +3907,8 @@ packages:
3907 prettier-plugin-svelte:
3908 optional: true
3909
3892 - prettier@3.6.2:
3893 - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
3910 + prettier@3.7.3:
3911 + resolution: {integrity: sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==}
3912 engines: {node: '>=14'}
3913 hasBin: true
3914
@@ -3976,8 +3994,8 @@ packages:
3994 resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==}
3995 hasBin: true
3996
3979 - regjsparser@0.12.0:
3980 - resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==}
3997 + regjsparser@0.13.0:
3998 + resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==}
3999 hasBin: true
4000
4001 request-progress@3.0.0:
@@ -4067,8 +4085,8 @@ packages:
4085 safer-buffer@2.1.2:
4086 resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
4087
4070 - sass@1.93.2:
4071 - resolution: {integrity: sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==}
4088 + sass@1.94.2:
4089 + resolution: {integrity: sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==}
4090 engines: {node: '>=14.0.0'}
4091 hasBin: true
4092
@@ -4114,8 +4132,8 @@ packages:
4132 resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
4133 engines: {node: '>= 0.4'}
4134
4117 - shiki@3.14.0:
4118 - resolution: {integrity: sha512-J0yvpLI7LSig3Z3acIuDLouV5UCKQqu8qOArwMx+/yPVC3WRMgrP67beaG8F+j4xfEWE0eVC4GeBCIXeOPra1g==}
4135 + shiki@3.17.0:
4136 + resolution: {integrity: sha512-lUZfWsyW7czITYTdo/Tb6ZM4VfyXlzmKYBQBjTz+pBzPPkP08RgIt00Ls1Z50Cl3SfwJsue6WbJeF3UgqLVI9Q==}
4137
4138 side-channel-list@1.0.0:
4139 resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
@@ -4199,8 +4217,8 @@ packages:
4217 stackback@0.0.2:
4218 resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
4219
4202 - start-server-and-test@2.1.2:
4203 - resolution: {integrity: sha512-OIjfo3G6QV9Sh6IlMqj58oZwVhPVuU/l6uVACG7YNE9kAfDvcYoPThtb0NNT3tZMMC3wOYbXnC15yiCSNFkdRg==}
4220 + start-server-and-test@2.1.3:
4221 + resolution: {integrity: sha512-k4EcbNjeg0odaDkAMlIeDVDByqX9PIgL4tivgP2tES6Zd8o+4pTq/HgbWCyA3VHIoZopB+wGnNPKYGGSByNriQ==}
4222 engines: {node: '>=16'}
4223 hasBin: true
4224
@@ -4288,15 +4306,15 @@ packages:
4306 resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
4307 engines: {node: '>=20'}
4308
4291 - tailwindcss@4.1.16:
4292 - resolution: {integrity: sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==}
4309 + tailwindcss@4.1.17:
4310 + resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==}
4311
4312 tapable@2.3.0:
4313 resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
4314 engines: {node: '>=6'}
4315
4298 - taze@19.9.0:
4299 - resolution: {integrity: sha512-tai3JKoCGHMPIHtuGHwT6z9vJaVgQ/Oj9MmvU05vah063Le9r7ONIv7ig6G2lrMNd87iWgfYARbSOYg4zaGJjQ==}
4316 + taze@19.9.2:
4317 + resolution: {integrity: sha512-If8bq7lSckIMPfXV+C9jjEfdsQnRryh/foKfpX/ah6zI0TrQfUGWSGCaaD32Bqy5/KGRmLZie3EwMSr3Au21XQ==}
4318 hasBin: true
4319
4320 thememirror@2.0.1:
@@ -4321,6 +4339,10 @@ packages:
4339 tinyexec@1.0.1:
4340 resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==}
4341
4342 + tinyexec@1.0.2:
4343 + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
4344 + engines: {node: '>=18'}
4345 +
4346 tinyglobby@0.2.15:
4347 resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
4348 engines: {node: '>=12.0.0'}
@@ -4423,8 +4445,8 @@ packages:
4445 resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
4446 engines: {node: '>=8'}
4447
4426 - type-fest@5.1.0:
4427 - resolution: {integrity: sha512-wQ531tuWvB6oK+pchHIu5lHe5f5wpSCqB8Kf4dWQRbOYc9HTge7JL0G4Qd44bh6QuJCccIzL3bugb8GI0MwHrg==}
4448 + type-fest@5.2.0:
4449 + resolution: {integrity: sha512-xxCJm+Bckc6kQBknN7i9fnP/xobQRsRQxR01CztFkp/h++yfVxUUcmMgfR2HttJx/dpWjS9ubVuyspJv24Q9DA==}
4450 engines: {node: '>=20'}
4451
4452 typescript@5.9.3:
@@ -4438,8 +4460,11 @@ packages:
4460 ufo@1.6.1:
4461 resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
4462
4441 - unconfig@7.3.3:
4442 - resolution: {integrity: sha512-QCkQoOnJF8L107gxfHL0uavn7WD9b3dpBcFX6HtfQYmjw2YzWxGuFQ0N0J6tE9oguCBJn9KOvfqYDCMPHIZrBA==}
4463 + unconfig-core@7.4.1:
4464 + resolution: {integrity: sha512-Bp/bPZjV2Vl/fofoA2OYLSnw1Z0MOhCX7zHnVCYrazpfZvseBbGhwcNQMxsg185Mqh7VZQqK3C8hFG/Dyng+yA==}
4465 +
4466 + unconfig@7.4.1:
4467 + resolution: {integrity: sha512-uyQ7LElcGizrOGZyIq9KU+xkuEjcRf9IpmDTkCSYv5mEeZzrXSj6rb51C0L+WTedsmAoVxW9WKrLWhSwebIM9Q==}
4468
4469 unctx@2.4.1:
4470 resolution: {integrity: sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg==}
@@ -4498,8 +4523,8 @@ packages:
4523 resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
4524 hasBin: true
4525
4501 - validator@13.15.20:
4502 - resolution: {integrity: sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==}
4526 + validator@13.15.23:
4527 + resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==}
4528 engines: {node: '>= 0.10'}
4529
4530 vdirs@0.1.8:
@@ -4542,8 +4567,8 @@ packages:
4567 '@nuxt/kit':
4568 optional: true
4569
4545 - vite-plugin-vue-devtools@8.0.3:
4546 - resolution: {integrity: sha512-yIi3u31xUi28HcLlTpV0BvSLQHgZ2dA8Zqa59kWfIeMdHqbsunt6TCjq4wCNfOcGSju+E7qyHyI09EjRRFMbuQ==}
4570 + vite-plugin-vue-devtools@8.0.5:
4571 + resolution: {integrity: sha512-p619BlKFOqQXJ6uDWS1vUPQzuJOD6xJTfftj57JXBGoBD/yeQCowR7pnWcr/FEX4/HVkFbreI6w2uuGBmQOh6A==}
4572 engines: {node: '>=v14.21.3'}
4573 peerDependencies:
4574 vite: ^6.0.0 || ^7.0.0-0
@@ -4558,8 +4583,8 @@ packages:
4583 peerDependencies:
4584 vue: '>=3.2.13'
4585
4561 - vite@7.1.12:
4562 - resolution: {integrity: sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==}
4586 + vite@7.2.4:
4587 + resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==}
4588 engines: {node: ^20.19.0 || >=22.12.0}
4589 hasBin: true
4590 peerDependencies:
@@ -4598,24 +4623,24 @@ packages:
4623 yaml:
4624 optional: true
4625
4601 - vitest@4.0.5:
4602 - resolution: {integrity: sha512-4H+J28MI5oeYgGg3h5BFSkQ1g/2GKK1IR8oorH3a6EQQbb7CwjbnyBjH4PGxw9/6vpwAPNzaeUMp4Js4WJmdXQ==}
4626 + vitest@4.0.14:
4627 + resolution: {integrity: sha512-d9B2J9Cm9dN9+6nxMnnNJKJCtcyKfnHj15N6YNJfaFHRLua/d3sRKU9RuKmO9mB0XdFtUizlxfz/VPbd3OxGhw==}
4628 engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
4629 hasBin: true
4630 peerDependencies:
4631 '@edge-runtime/vm': '*'
4607 - '@types/debug': ^4.1.12
4632 + '@opentelemetry/api': ^1.9.0
4633 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
4609 - '@vitest/browser-playwright': 4.0.5
4610 - '@vitest/browser-preview': 4.0.5
4611 - '@vitest/browser-webdriverio': 4.0.5
4612 - '@vitest/ui': 4.0.5
4634 + '@vitest/browser-playwright': 4.0.14
4635 + '@vitest/browser-preview': 4.0.14
4636 + '@vitest/browser-webdriverio': 4.0.14
4637 + '@vitest/ui': 4.0.14
4638 happy-dom: '*'
4639 jsdom: '*'
4640 peerDependenciesMeta:
4641 '@edge-runtime/vm':
4642 optional: true
4618 - '@types/debug':
4643 + '@opentelemetry/api':
4644 optional: true
4645 '@types/node':
4646 optional: true
@@ -4666,8 +4691,8 @@ packages:
4691 peerDependencies:
4692 vue: ^3.0.0
4693
4669 - vue-i18n@11.1.12:
4670 - resolution: {integrity: sha512-BnstPj3KLHLrsqbVU2UOrPmr0+Mv11bsUZG0PyCOzsawCivk8W00GMXHeVUWIDOgNaScCuZah47CZFE+Wnl8mw==}
4694 + vue-i18n@11.2.2:
4695 + resolution: {integrity: sha512-ULIKZyRluUPRCZmihVgUvpq8hJTtOqnbGZuv4Lz+byEKZq4mU0g92og414l6f/4ju+L5mORsiUuEPYrAuX2NJg==}
4696 engines: {node: '>= 16'}
4697 peerDependencies:
4698 vue: ^3.0.0
@@ -4682,8 +4707,8 @@ packages:
4707 peerDependencies:
4708 vue: ^3.3.4
4709
4685 - vue-tsc@3.1.2:
4686 - resolution: {integrity: sha512-3fd4DY0rFczs5f+VB3OhcLU83V6+3Puj2yLBe0Ak65k7ERk+STVNKaOAi0EBo6Lc15UiJB6LzU6Mxy4+h/pKew==}
4710 + vue-tsc@3.1.5:
4711 + resolution: {integrity: sha512-L/G9IUjOWhBU0yun89rv8fKqmKC+T0HfhrFjlIml71WpfBv9eb4E9Bev8FMbyueBIU9vxQqbd+oOsVcDa5amGw==}
4712 hasBin: true
4713 peerDependencies:
4714 typescript: '>=5.0.0'
@@ -4700,8 +4725,8 @@ packages:
4725 peerDependencies:
4726 vue: ^3.2
4727
4703 - vue@3.5.22:
4704 - resolution: {integrity: sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==}
4728 + vue@3.5.25:
4729 + resolution: {integrity: sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==}
4730 peerDependencies:
4731 typescript: '*'
4732 peerDependenciesMeta:
@@ -4725,9 +4750,9 @@ packages:
4750 resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
4751 engines: {node: '>=18'}
4752
4728 - wait-on@8.0.5:
4729 - resolution: {integrity: sha512-J3WlS0txVHkhLRb2FsmRg3dkMTCV1+M6Xra3Ho7HzZDHpE7DCOnoSoCJsZotrmW3uRMhvIJGSKUKrh/MeF4iag==}
4730 - engines: {node: '>=12.0.0'}
4753 + wait-on@9.0.3:
4754 + resolution: {integrity: sha512-13zBnyYvFDW1rBvWiJ6Av3ymAaq8EDQuvxZnPIw3g04UqGi4TyoIJABmfJ6zrvKo9yeFQExNkOk7idQbDJcuKA==}
4755 + engines: {node: '>=20.0.0'}
4756 hasBin: true
4757
4758 webidl-conversions@8.0.0:
@@ -4874,7 +4899,7 @@ packages:
4899
4900 snapshots:
4901
4877 - '@acemir/cssom@0.9.19': {}
4902 + '@acemir/cssom@0.9.24': {}
4903
4904 '@ajoelp/json-to-formdata@1.5.0':
4905 dependencies:
@@ -4964,44 +4989,44 @@ snapshots:
4989 dependencies:
4990 '@algolia/client-common': 5.42.0
4991
4967 - '@antfu/eslint-config@6.1.0(@vue/compiler-sfc@3.5.22)(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.5(@types/debug@4.1.12)(@types/node@24.9.2)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))':
4992 + '@antfu/eslint-config@6.2.0(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.14(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))':
4993 dependencies:
4994 '@antfu/install-pkg': 1.1.0
4995 '@clack/prompts': 0.11.0
4971 - '@eslint-community/eslint-plugin-eslint-comments': 4.5.0(eslint@9.38.0(jiti@2.6.1))
4996 + '@eslint-community/eslint-plugin-eslint-comments': 4.5.0(eslint@9.39.1(jiti@2.6.1))
4997 '@eslint/markdown': 7.5.0
4973 - '@stylistic/eslint-plugin': 5.5.0(eslint@9.38.0(jiti@2.6.1))
4974 - '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
4975 - '@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
4976 - '@vitest/eslint-plugin': 1.4.0(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.5(@types/debug@4.1.12)(@types/node@24.9.2)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))
4998 + '@stylistic/eslint-plugin': 5.5.0(eslint@9.39.1(jiti@2.6.1))
4999 + '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
5000 + '@typescript-eslint/parser': 8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
5001 + '@vitest/eslint-plugin': 1.4.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.14(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))
5002 ansis: 4.2.0
5003 cac: 6.7.14
4979 - eslint: 9.38.0(jiti@2.6.1)
4980 - eslint-config-flat-gitignore: 2.1.0(eslint@9.38.0(jiti@2.6.1))
5004 + eslint: 9.39.1(jiti@2.6.1)
5005 + eslint-config-flat-gitignore: 2.1.0(eslint@9.39.1(jiti@2.6.1))
5006 eslint-flat-config-utils: 2.1.4
4982 - eslint-merge-processors: 2.0.0(eslint@9.38.0(jiti@2.6.1))
4983 - eslint-plugin-antfu: 3.1.1(eslint@9.38.0(jiti@2.6.1))
4984 - eslint-plugin-command: 3.3.1(eslint@9.38.0(jiti@2.6.1))
4985 - eslint-plugin-import-lite: 0.3.0(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
4986 - eslint-plugin-jsdoc: 61.1.11(eslint@9.38.0(jiti@2.6.1))
4987 - eslint-plugin-jsonc: 2.21.0(eslint@9.38.0(jiti@2.6.1))
4988 - eslint-plugin-n: 17.23.1(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
5007 + eslint-merge-processors: 2.0.0(eslint@9.39.1(jiti@2.6.1))
5008 + eslint-plugin-antfu: 3.1.1(eslint@9.39.1(jiti@2.6.1))
5009 + eslint-plugin-command: 3.3.1(eslint@9.39.1(jiti@2.6.1))
5010 + eslint-plugin-import-lite: 0.3.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
5011 + eslint-plugin-jsdoc: 61.1.11(eslint@9.39.1(jiti@2.6.1))
5012 + eslint-plugin-jsonc: 2.21.0(eslint@9.39.1(jiti@2.6.1))
5013 + eslint-plugin-n: 17.23.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
5014 eslint-plugin-no-only-tests: 3.3.0
4990 - eslint-plugin-perfectionist: 4.15.1(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
4991 - eslint-plugin-pnpm: 1.3.0(eslint@9.38.0(jiti@2.6.1))
4992 - eslint-plugin-regexp: 2.10.0(eslint@9.38.0(jiti@2.6.1))
4993 - eslint-plugin-toml: 0.12.0(eslint@9.38.0(jiti@2.6.1))
4994 - eslint-plugin-unicorn: 61.0.2(eslint@9.38.0(jiti@2.6.1))
4995 - eslint-plugin-unused-imports: 4.3.0(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))
4996 - eslint-plugin-vue: 10.5.1(@stylistic/eslint-plugin@5.5.0(eslint@9.38.0(jiti@2.6.1)))(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.38.0(jiti@2.6.1)))
4997 - eslint-plugin-yml: 1.19.0(eslint@9.38.0(jiti@2.6.1))
4998 - eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.22)(eslint@9.38.0(jiti@2.6.1))
5015 + eslint-plugin-perfectionist: 4.15.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
5016 + eslint-plugin-pnpm: 1.3.0(eslint@9.39.1(jiti@2.6.1))
5017 + eslint-plugin-regexp: 2.10.0(eslint@9.39.1(jiti@2.6.1))
5018 + eslint-plugin-toml: 0.12.0(eslint@9.39.1(jiti@2.6.1))
5019 + eslint-plugin-unicorn: 62.0.0(eslint@9.39.1(jiti@2.6.1))
5020 + eslint-plugin-unused-imports: 4.3.0(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))
5021 + eslint-plugin-vue: 10.5.1(@stylistic/eslint-plugin@5.5.0(eslint@9.39.1(jiti@2.6.1)))(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1)))
5022 + eslint-plugin-yml: 1.19.0(eslint@9.39.1(jiti@2.6.1))
5023 + eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1))
5024 globals: 16.4.0
5025 jsonc-eslint-parser: 2.4.1
5026 local-pkg: 1.1.2
5027 parse-gitignore: 2.0.0
5028 toml-eslint-parser: 0.10.0
5004 - vue-eslint-parser: 10.2.0(eslint@9.38.0(jiti@2.6.1))
5029 + vue-eslint-parser: 10.2.0(eslint@9.39.1(jiti@2.6.1))
5030 yaml-eslint-parser: 1.3.0
5031 transitivePeerDependencies:
5032 - '@eslint/json'
@@ -5020,7 +5045,7 @@ snapshots:
5045 ansis: 4.2.0
5046 fzf: 0.5.2
5047 package-manager-detector: 1.5.0
5023 - tinyexec: 1.0.1
5048 + tinyexec: 1.0.2
5049 tinyglobby: 0.2.15
5050
5051 '@asamuzakjp/css-color@4.0.5':
@@ -5031,7 +5056,7 @@ snapshots:
5056 '@csstools/css-tokenizer': 3.0.4
5057 lru-cache: 11.2.2
5058
5034 - '@asamuzakjp/dom-selector@6.7.3':
5059 + '@asamuzakjp/dom-selector@6.7.4':
5060 dependencies:
5061 '@asamuzakjp/nwsapi': 2.3.9
5062 bidi-js: 1.0.3
@@ -5247,23 +5272,23 @@ snapshots:
5272 dependencies:
5273 '@codemirror/language': 6.11.3
5274 '@codemirror/state': 6.5.2
5250 - '@codemirror/view': 6.38.6
5275 + '@codemirror/view': 6.38.8
5276 '@lezer/common': 1.3.0
5277
5278 '@codemirror/commands@6.10.0':
5279 dependencies:
5280 '@codemirror/language': 6.11.3
5281 '@codemirror/state': 6.5.2
5257 - '@codemirror/view': 6.38.6
5282 + '@codemirror/view': 6.38.8
5283 '@lezer/common': 1.3.0
5284
5285 '@codemirror/lang-javascript@6.2.4':
5286 dependencies:
5287 '@codemirror/autocomplete': 6.19.1
5288 '@codemirror/language': 6.11.3
5264 - '@codemirror/lint': 6.9.1
5289 + '@codemirror/lint': 6.9.2
5290 '@codemirror/state': 6.5.2
5266 - '@codemirror/view': 6.38.6
5291 + '@codemirror/view': 6.38.8
5292 '@lezer/common': 1.3.0
5293 '@lezer/javascript': 1.5.4
5294
@@ -5272,29 +5297,29 @@ snapshots:
5297 '@codemirror/autocomplete': 6.19.1
5298 '@codemirror/language': 6.11.3
5299 '@codemirror/state': 6.5.2
5275 - '@codemirror/view': 6.38.6
5300 + '@codemirror/view': 6.38.8
5301 '@lezer/common': 1.3.0
5302 '@lezer/xml': 1.0.6
5303
5304 '@codemirror/language@6.11.3':
5305 dependencies:
5306 '@codemirror/state': 6.5.2
5282 - '@codemirror/view': 6.38.6
5307 + '@codemirror/view': 6.38.8
5308 '@lezer/common': 1.3.0
5309 '@lezer/highlight': 1.2.3
5310 '@lezer/lr': 1.4.2
5311 style-mod: 4.1.3
5312
5288 - '@codemirror/lint@6.9.1':
5313 + '@codemirror/lint@6.9.2':
5314 dependencies:
5315 '@codemirror/state': 6.5.2
5291 - '@codemirror/view': 6.38.6
5316 + '@codemirror/view': 6.38.8
5317 crelt: 1.0.6
5318
5319 '@codemirror/search@6.5.11':
5320 dependencies:
5321 '@codemirror/state': 6.5.2
5297 - '@codemirror/view': 6.38.6
5322 + '@codemirror/view': 6.38.8
5323 crelt: 1.0.6
5324
5325 '@codemirror/state@6.5.2':
@@ -5305,10 +5330,10 @@ snapshots:
5330 dependencies:
5331 '@codemirror/language': 6.11.3
5332 '@codemirror/state': 6.5.2
5308 - '@codemirror/view': 6.38.6
5333 + '@codemirror/view': 6.38.8
5334 '@lezer/highlight': 1.2.3
5335
5311 - '@codemirror/view@6.38.6':
5336 + '@codemirror/view@6.38.8':
5337 dependencies:
5338 '@codemirror/state': 6.5.2
5339 crelt: 1.0.6
@@ -5319,9 +5344,9 @@ snapshots:
5344 dependencies:
5345 css-render: 0.15.14
5346
5322 - '@css-render/vue3-ssr@0.15.14(vue@3.5.22(typescript@5.9.3))':
5347 + '@css-render/vue3-ssr@0.15.14(vue@3.5.25(typescript@5.9.3))':
5348 dependencies:
5324 - vue: 3.5.22(typescript@5.9.3)
5349 + vue: 3.5.25(typescript@5.9.3)
5350
5351 '@csstools/color-helpers@5.1.0': {}
5352
@@ -5471,24 +5496,24 @@ snapshots:
5496 '@esbuild/win32-x64@0.25.11':
5497 optional: true
5498
5474 - '@eslint-community/eslint-plugin-eslint-comments@4.5.0(eslint@9.38.0(jiti@2.6.1))':
5499 + '@eslint-community/eslint-plugin-eslint-comments@4.5.0(eslint@9.39.1(jiti@2.6.1))':
5500 dependencies:
5501 escape-string-regexp: 4.0.0
5477 - eslint: 9.38.0(jiti@2.6.1)
5502 + eslint: 9.39.1(jiti@2.6.1)
5503 ignore: 5.3.2
5504
5480 - '@eslint-community/eslint-utils@4.9.0(eslint@9.38.0(jiti@2.6.1))':
5505 + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))':
5506 dependencies:
5482 - eslint: 9.38.0(jiti@2.6.1)
5507 + eslint: 9.39.1(jiti@2.6.1)
5508 eslint-visitor-keys: 3.4.3
5509
5510 '@eslint-community/regexpp@4.12.2': {}
5511
5487 - '@eslint/compat@1.4.1(eslint@9.38.0(jiti@2.6.1))':
5512 + '@eslint/compat@1.4.1(eslint@9.39.1(jiti@2.6.1))':
5513 dependencies:
5514 '@eslint/core': 0.17.0
5515 optionalDependencies:
5491 - eslint: 9.38.0(jiti@2.6.1)
5516 + eslint: 9.39.1(jiti@2.6.1)
5517
5518 '@eslint/config-array@0.21.1':
5519 dependencies:
@@ -5502,10 +5527,6 @@ snapshots:
5527 dependencies:
5528 '@eslint/core': 0.17.0
5529
5505 - '@eslint/core@0.15.2':
5506 - dependencies:
5507 - '@types/json-schema': 7.0.15
5508 -
5530 '@eslint/core@0.16.0':
5531 dependencies:
5532 '@types/json-schema': 7.0.15
@@ -5528,7 +5549,7 @@ snapshots:
5549 transitivePeerDependencies:
5550 - supports-color
5551
5531 - '@eslint/js@9.38.0': {}
5552 + '@eslint/js@9.39.1': {}
5553
5554 '@eslint/markdown@7.5.0':
5555 dependencies:
@@ -5546,20 +5567,15 @@ snapshots:
5567
5568 '@eslint/object-schema@2.1.7': {}
5569
5549 - '@eslint/plugin-kit@0.3.5':
5550 - dependencies:
5551 - '@eslint/core': 0.15.2
5552 - levn: 0.4.1
5553 -
5570 '@eslint/plugin-kit@0.4.1':
5571 dependencies:
5572 '@eslint/core': 0.17.0
5573 levn: 0.4.1
5574
5559 - '@f3ve/vue-markdown-it@0.2.3(vue@3.5.22(typescript@5.9.3))':
5575 + '@f3ve/vue-markdown-it@0.2.3(vue@3.5.25(typescript@5.9.3))':
5576 dependencies:
5577 markdown-it: 14.1.0
5562 - vue: 3.5.22(typescript@5.9.3)
5578 + vue: 3.5.25(typescript@5.9.3)
5579
5580 '@fontsource/jetbrains-mono@5.2.8': {}
5581
@@ -5596,22 +5612,22 @@ snapshots:
5612
5613 '@iconify/types@2.0.0': {}
5614
5599 - '@iconify/vue@5.0.0(vue@3.5.22(typescript@5.9.3))':
5615 + '@iconify/vue@5.0.0(vue@3.5.25(typescript@5.9.3))':
5616 dependencies:
5617 '@iconify/types': 2.0.0
5602 - vue: 3.5.22(typescript@5.9.3)
5618 + vue: 3.5.25(typescript@5.9.3)
5619
5604 - '@intlify/core-base@11.1.12':
5620 + '@intlify/core-base@11.2.2':
5621 dependencies:
5606 - '@intlify/message-compiler': 11.1.12
5607 - '@intlify/shared': 11.1.12
5622 + '@intlify/message-compiler': 11.2.2
5623 + '@intlify/shared': 11.2.2
5624
5609 - '@intlify/message-compiler@11.1.12':
5625 + '@intlify/message-compiler@11.2.2':
5626 dependencies:
5611 - '@intlify/shared': 11.1.12
5627 + '@intlify/shared': 11.2.2
5628 source-map-js: 1.2.1
5629
5614 - '@intlify/shared@11.1.12': {}
5630 + '@intlify/shared@11.2.2': {}
5631
5632 '@isaacs/cliui@8.0.2':
5633 dependencies:
@@ -5780,9 +5796,9 @@ snapshots:
5796 dependencies:
5797 quansync: 0.2.11
5798
5783 - '@rolldown/pluginutils@1.0.0-beta.29': {}
5799 + '@rolldown/pluginutils@1.0.0-beta.50': {}
5800
5785 - '@rolldown/pluginutils@1.0.0-beta.45': {}
5801 + '@rolldown/pluginutils@1.0.0-beta.52': {}
5802
5803 '@rollup/rollup-android-arm-eabi@4.52.5':
5804 optional: true
@@ -5850,38 +5866,38 @@ snapshots:
5866 '@rollup/rollup-win32-x64-msvc@4.52.5':
5867 optional: true
5868
5853 - '@shikijs/core@3.14.0':
5869 + '@shikijs/core@3.17.0':
5870 dependencies:
5855 - '@shikijs/types': 3.14.0
5871 + '@shikijs/types': 3.17.0
5872 '@shikijs/vscode-textmate': 10.0.2
5873 '@types/hast': 3.0.4
5874 hast-util-to-html: 9.0.5
5875
5860 - '@shikijs/engine-javascript@3.14.0':
5876 + '@shikijs/engine-javascript@3.17.0':
5877 dependencies:
5862 - '@shikijs/types': 3.14.0
5878 + '@shikijs/types': 3.17.0
5879 '@shikijs/vscode-textmate': 10.0.2
5864 - oniguruma-to-es: 4.3.3
5880 + oniguruma-to-es: 4.3.4
5881
5866 - '@shikijs/engine-oniguruma@3.14.0':
5882 + '@shikijs/engine-oniguruma@3.17.0':
5883 dependencies:
5868 - '@shikijs/types': 3.14.0
5884 + '@shikijs/types': 3.17.0
5885 '@shikijs/vscode-textmate': 10.0.2
5886
5871 - '@shikijs/langs@3.14.0':
5887 + '@shikijs/langs@3.17.0':
5888 dependencies:
5873 - '@shikijs/types': 3.14.0
5889 + '@shikijs/types': 3.17.0
5890
5875 - '@shikijs/markdown-it@3.14.0':
5891 + '@shikijs/markdown-it@3.17.0':
5892 dependencies:
5893 markdown-it: 14.1.0
5878 - shiki: 3.14.0
5894 + shiki: 3.17.0
5895
5880 - '@shikijs/themes@3.14.0':
5896 + '@shikijs/themes@3.17.0':
5897 dependencies:
5882 - '@shikijs/types': 3.14.0
5898 + '@shikijs/types': 3.17.0
5899
5884 - '@shikijs/types@3.14.0':
5900 + '@shikijs/types@3.17.0':
5901 dependencies:
5902 '@shikijs/vscode-textmate': 10.0.2
5903 '@types/hast': 3.0.4
@@ -5896,11 +5912,11 @@ snapshots:
5912
5913 '@standard-schema/spec@1.0.0': {}
5914
5899 - '@stylistic/eslint-plugin@5.5.0(eslint@9.38.0(jiti@2.6.1))':
5915 + '@stylistic/eslint-plugin@5.5.0(eslint@9.39.1(jiti@2.6.1))':
5916 dependencies:
5901 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1))
5917 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
5918 '@typescript-eslint/types': 8.46.2
5903 - eslint: 9.38.0(jiti@2.6.1)
5919 + eslint: 9.39.1(jiti@2.6.1)
5920 eslint-visitor-keys: 4.2.1
5921 espree: 10.4.0
5922 estraverse: 5.3.0
@@ -5925,7 +5941,7 @@ snapshots:
5941 dependencies:
5942 '@svgdotjs/svg.js': 3.2.5
5943
5928 - '@tailwindcss/node@4.1.16':
5944 + '@tailwindcss/node@4.1.17':
5945 dependencies:
5946 '@jridgewell/remapping': 2.3.5
5947 enhanced-resolve: 5.18.3
@@ -5933,69 +5949,69 @@ snapshots:
5949 lightningcss: 1.30.2
5950 magic-string: 0.30.21
5951 source-map-js: 1.2.1
5936 - tailwindcss: 4.1.16
5952 + tailwindcss: 4.1.17
5953
5938 - '@tailwindcss/oxide-android-arm64@4.1.16':
5954 + '@tailwindcss/oxide-android-arm64@4.1.17':
5955 optional: true
5956
5941 - '@tailwindcss/oxide-darwin-arm64@4.1.16':
5957 + '@tailwindcss/oxide-darwin-arm64@4.1.17':
5958 optional: true
5959
5944 - '@tailwindcss/oxide-darwin-x64@4.1.16':
5960 + '@tailwindcss/oxide-darwin-x64@4.1.17':
5961 optional: true
5962
5947 - '@tailwindcss/oxide-freebsd-x64@4.1.16':
5963 + '@tailwindcss/oxide-freebsd-x64@4.1.17':
5964 optional: true
5965
5950 - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16':
5966 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17':
5967 optional: true
5968
5953 - '@tailwindcss/oxide-linux-arm64-gnu@4.1.16':
5969 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17':
5970 optional: true
5971
5956 - '@tailwindcss/oxide-linux-arm64-musl@4.1.16':
5972 + '@tailwindcss/oxide-linux-arm64-musl@4.1.17':
5973 optional: true
5974
5959 - '@tailwindcss/oxide-linux-x64-gnu@4.1.16':
5975 + '@tailwindcss/oxide-linux-x64-gnu@4.1.17':
5976 optional: true
5977
5962 - '@tailwindcss/oxide-linux-x64-musl@4.1.16':
5978 + '@tailwindcss/oxide-linux-x64-musl@4.1.17':
5979 optional: true
5980
5965 - '@tailwindcss/oxide-wasm32-wasi@4.1.16':
5981 + '@tailwindcss/oxide-wasm32-wasi@4.1.17':
5982 optional: true
5983
5968 - '@tailwindcss/oxide-win32-arm64-msvc@4.1.16':
5984 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17':
5985 optional: true
5986
5971 - '@tailwindcss/oxide-win32-x64-msvc@4.1.16':
5987 + '@tailwindcss/oxide-win32-x64-msvc@4.1.17':
5988 optional: true
5989
5974 - '@tailwindcss/oxide@4.1.16':
5990 + '@tailwindcss/oxide@4.1.17':
5991 optionalDependencies:
5976 - '@tailwindcss/oxide-android-arm64': 4.1.16
5977 - '@tailwindcss/oxide-darwin-arm64': 4.1.16
5978 - '@tailwindcss/oxide-darwin-x64': 4.1.16
5979 - '@tailwindcss/oxide-freebsd-x64': 4.1.16
5980 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.16
5981 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.16
5982 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.16
5983 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.16
5984 - '@tailwindcss/oxide-linux-x64-musl': 4.1.16
5985 - '@tailwindcss/oxide-wasm32-wasi': 4.1.16
5986 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.16
5987 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.16
5988 -
5989 - '@tailwindcss/vite@4.1.16(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))':
5990 - dependencies:
5991 - '@tailwindcss/node': 4.1.16
5992 - '@tailwindcss/oxide': 4.1.16
5993 - tailwindcss: 4.1.16
5994 - vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)
5992 + '@tailwindcss/oxide-android-arm64': 4.1.17
5993 + '@tailwindcss/oxide-darwin-arm64': 4.1.17
5994 + '@tailwindcss/oxide-darwin-x64': 4.1.17
5995 + '@tailwindcss/oxide-freebsd-x64': 4.1.17
5996 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.17
5997 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.17
5998 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.17
5999 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.17
6000 + '@tailwindcss/oxide-linux-x64-musl': 4.1.17
6001 + '@tailwindcss/oxide-wasm32-wasi': 4.1.17
6002 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17
6003 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.17
6004 +
6005 + '@tailwindcss/vite@4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))':
6006 + dependencies:
6007 + '@tailwindcss/node': 4.1.17
6008 + '@tailwindcss/oxide': 4.1.17
6009 + tailwindcss: 4.1.17
6010 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
6011
6012 '@trysound/sax@0.2.0': {}
6013
5998 - '@tsconfig/node20@20.1.6': {}
6014 + '@tsconfig/node20@20.1.8': {}
6015
6016 '@types/bytes@3.1.5': {}
6017
@@ -6021,7 +6037,7 @@ snapshots:
6037 '@types/fs-extra@11.0.4':
6038 dependencies:
6039 '@types/jsonfile': 6.1.4
6024 - '@types/node': 24.9.2
6040 + '@types/node': 24.10.1
6041
6042 '@types/hast@3.0.4':
6043 dependencies:
@@ -6029,7 +6045,7 @@ snapshots:
6045
6046 '@types/jsdom@27.0.0':
6047 dependencies:
6032 - '@types/node': 24.9.2
6048 + '@types/node': 24.10.1
6049 '@types/tough-cookie': 4.0.5
6050 parse5: 7.3.0
6051
@@ -6037,7 +6053,7 @@ snapshots:
6053
6054 '@types/jsonfile@6.1.4':
6055 dependencies:
6040 - '@types/node': 24.9.2
6056 + '@types/node': 24.10.1
6057
6058 '@types/katex@0.16.7': {}
6059
@@ -6045,9 +6061,9 @@ snapshots:
6061
6062 '@types/lodash-es@4.17.12':
6063 dependencies:
6048 - '@types/lodash': 4.17.20
6064 + '@types/lodash': 4.17.21
6065
6050 - '@types/lodash@4.17.20': {}
6066 + '@types/lodash@4.17.21': {}
6067
6068 '@types/markdown-it@14.1.2':
6069 dependencies:
@@ -6064,7 +6080,7 @@ snapshots:
6080
6081 '@types/ms@2.1.0': {}
6082
6067 - '@types/node@24.9.2':
6083 + '@types/node@24.10.1':
6084 dependencies:
6085 undici-types: 7.16.0
6086
@@ -6084,24 +6100,24 @@ snapshots:
6100
6101 '@types/unist@3.0.3': {}
6102
6087 - '@types/validator@13.15.4': {}
6103 + '@types/validator@13.15.10': {}
6104
6105 '@types/web-bluetooth@0.0.21': {}
6106
6107 '@types/yauzl@2.10.3':
6108 dependencies:
6093 - '@types/node': 24.9.2
6109 + '@types/node': 24.10.1
6110 optional: true
6111
6096 - '@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)':
6112 + '@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
6113 dependencies:
6114 '@eslint-community/regexpp': 4.12.2
6099 - '@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
6115 + '@typescript-eslint/parser': 8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
6116 '@typescript-eslint/scope-manager': 8.46.2
6101 - '@typescript-eslint/type-utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
6102 - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
6117 + '@typescript-eslint/type-utils': 8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
6118 + '@typescript-eslint/utils': 8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
6119 '@typescript-eslint/visitor-keys': 8.46.2
6104 - eslint: 9.38.0(jiti@2.6.1)
6120 + eslint: 9.39.1(jiti@2.6.1)
6121 graphemer: 1.4.0
6122 ignore: 7.0.5
6123 natural-compare: 1.4.0
@@ -6110,14 +6126,14 @@ snapshots:
6126 transitivePeerDependencies:
6127 - supports-color
6128
6113 - '@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)':
6129 + '@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
6130 dependencies:
6131 '@typescript-eslint/scope-manager': 8.46.2
6132 '@typescript-eslint/types': 8.46.2
6133 '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
6134 '@typescript-eslint/visitor-keys': 8.46.2
6135 debug: 4.4.3(supports-color@8.1.1)
6120 - eslint: 9.38.0(jiti@2.6.1)
6136 + eslint: 9.39.1(jiti@2.6.1)
6137 typescript: 5.9.3
6138 transitivePeerDependencies:
6139 - supports-color
@@ -6140,13 +6156,13 @@ snapshots:
6156 dependencies:
6157 typescript: 5.9.3
6158
6143 - '@typescript-eslint/type-utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)':
6159 + '@typescript-eslint/type-utils@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
6160 dependencies:
6161 '@typescript-eslint/types': 8.46.2
6162 '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
6147 - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
6163 + '@typescript-eslint/utils': 8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
6164 debug: 4.4.3(supports-color@8.1.1)
6149 - eslint: 9.38.0(jiti@2.6.1)
6165 + eslint: 9.39.1(jiti@2.6.1)
6166 ts-api-utils: 2.1.0(typescript@5.9.3)
6167 typescript: 5.9.3
6168 transitivePeerDependencies:
@@ -6170,13 +6186,13 @@ snapshots:
6186 transitivePeerDependencies:
6187 - supports-color
6188
6173 - '@typescript-eslint/utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)':
6189 + '@typescript-eslint/utils@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
6190 dependencies:
6175 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1))
6191 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
6192 '@typescript-eslint/scope-manager': 8.46.2
6193 '@typescript-eslint/types': 8.46.2
6194 '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
6179 - eslint: 9.38.0(jiti@2.6.1)
6195 + eslint: 9.39.1(jiti@2.6.1)
6196 typescript: 5.9.3
6197 transitivePeerDependencies:
6198 - supports-color
@@ -6188,72 +6204,72 @@ snapshots:
6204
6205 '@ungap/structured-clone@1.3.0': {}
6206
6191 - '@vitejs/plugin-vue-jsx@5.1.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))':
6207 + '@vitejs/plugin-vue-jsx@5.1.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))':
6208 dependencies:
6209 '@babel/core': 7.28.5
6210 '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5)
6211 '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5)
6196 - '@rolldown/pluginutils': 1.0.0-beta.45
6197 - '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.5)
6198 - vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)
6199 - vue: 3.5.22(typescript@5.9.3)
6212 + '@rolldown/pluginutils': 1.0.0-beta.52
6213 + '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.28.5)
6214 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
6215 + vue: 3.5.25(typescript@5.9.3)
6216 transitivePeerDependencies:
6217 - supports-color
6218
6203 - '@vitejs/plugin-vue@6.0.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))':
6219 + '@vitejs/plugin-vue@6.0.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))':
6220 dependencies:
6205 - '@rolldown/pluginutils': 1.0.0-beta.29
6206 - vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)
6207 - vue: 3.5.22(typescript@5.9.3)
6221 + '@rolldown/pluginutils': 1.0.0-beta.50
6222 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
6223 + vue: 3.5.25(typescript@5.9.3)
6224
6209 - '@vitest/eslint-plugin@1.4.0(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.5(@types/debug@4.1.12)(@types/node@24.9.2)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))':
6225 + '@vitest/eslint-plugin@1.4.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.14(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))':
6226 dependencies:
6227 '@typescript-eslint/scope-manager': 8.46.2
6212 - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
6213 - eslint: 9.38.0(jiti@2.6.1)
6228 + '@typescript-eslint/utils': 8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
6229 + eslint: 9.39.1(jiti@2.6.1)
6230 optionalDependencies:
6231 typescript: 5.9.3
6216 - vitest: 4.0.5(@types/debug@4.1.12)(@types/node@24.9.2)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)
6232 + vitest: 4.0.14(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
6233 transitivePeerDependencies:
6234 - supports-color
6235
6220 - '@vitest/expect@4.0.5':
6236 + '@vitest/expect@4.0.14':
6237 dependencies:
6238 '@standard-schema/spec': 1.0.0
6239 '@types/chai': 5.2.3
6224 - '@vitest/spy': 4.0.5
6225 - '@vitest/utils': 4.0.5
6226 - chai: 6.2.0
6240 + '@vitest/spy': 4.0.14
6241 + '@vitest/utils': 4.0.14
6242 + chai: 6.2.1
6243 tinyrainbow: 3.0.3
6244
6229 - '@vitest/mocker@4.0.5(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))':
6245 + '@vitest/mocker@4.0.14(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))':
6246 dependencies:
6231 - '@vitest/spy': 4.0.5
6247 + '@vitest/spy': 4.0.14
6248 estree-walker: 3.0.3
6249 magic-string: 0.30.21
6250 optionalDependencies:
6235 - vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)
6251 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
6252
6237 - '@vitest/pretty-format@4.0.5':
6253 + '@vitest/pretty-format@4.0.14':
6254 dependencies:
6255 tinyrainbow: 3.0.3
6256
6241 - '@vitest/runner@4.0.5':
6257 + '@vitest/runner@4.0.14':
6258 dependencies:
6243 - '@vitest/utils': 4.0.5
6259 + '@vitest/utils': 4.0.14
6260 pathe: 2.0.3
6261
6246 - '@vitest/snapshot@4.0.5':
6262 + '@vitest/snapshot@4.0.14':
6263 dependencies:
6248 - '@vitest/pretty-format': 4.0.5
6264 + '@vitest/pretty-format': 4.0.14
6265 magic-string: 0.30.21
6266 pathe: 2.0.3
6267
6252 - '@vitest/spy@4.0.5': {}
6268 + '@vitest/spy@4.0.14': {}
6269
6254 - '@vitest/utils@4.0.5':
6270 + '@vitest/utils@4.0.14':
6271 dependencies:
6256 - '@vitest/pretty-format': 4.0.5
6272 + '@vitest/pretty-format': 4.0.14
6273 tinyrainbow: 3.0.3
6274
6275 '@volar/language-core@2.4.23':
@@ -6270,6 +6286,8 @@ snapshots:
6286
6287 '@vue/babel-helper-vue-transform-on@1.5.0': {}
6288
6289 + '@vue/babel-helper-vue-transform-on@2.0.1': {}
6290 +
6291 '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.28.5)':
6292 dependencies:
6293 '@babel/helper-module-imports': 7.27.1
@@ -6286,6 +6304,22 @@ snapshots:
6304 transitivePeerDependencies:
6305 - supports-color
6306
6307 + '@vue/babel-plugin-jsx@2.0.1(@babel/core@7.28.5)':
6308 + dependencies:
6309 + '@babel/helper-module-imports': 7.27.1
6310 + '@babel/helper-plugin-utils': 7.27.1
6311 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5)
6312 + '@babel/template': 7.27.2
6313 + '@babel/traverse': 7.28.5
6314 + '@babel/types': 7.28.5
6315 + '@vue/babel-helper-vue-transform-on': 2.0.1
6316 + '@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.28.5)
6317 + '@vue/shared': 3.5.22
6318 + optionalDependencies:
6319 + '@babel/core': 7.28.5
6320 + transitivePeerDependencies:
6321 + - supports-color
6322 +
6323 '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.28.5)':
6324 dependencies:
6325 '@babel/code-frame': 7.27.1
@@ -6297,6 +6331,17 @@ snapshots:
6331 transitivePeerDependencies:
6332 - supports-color
6333
6334 + '@vue/babel-plugin-resolve-type@2.0.1(@babel/core@7.28.5)':
6335 + dependencies:
6336 + '@babel/code-frame': 7.27.1
6337 + '@babel/core': 7.28.5
6338 + '@babel/helper-module-imports': 7.27.1
6339 + '@babel/helper-plugin-utils': 7.27.1
6340 + '@babel/parser': 7.28.5
6341 + '@vue/compiler-sfc': 3.5.22
6342 + transitivePeerDependencies:
6343 + - supports-color
6344 +
6345 '@vue/compiler-core@3.5.22':
6346 dependencies:
6347 '@babel/parser': 7.28.5
@@ -6305,11 +6350,24 @@ snapshots:
6350 estree-walker: 2.0.2
6351 source-map-js: 1.2.1
6352
6353 + '@vue/compiler-core@3.5.25':
6354 + dependencies:
6355 + '@babel/parser': 7.28.5
6356 + '@vue/shared': 3.5.25
6357 + entities: 4.5.0
6358 + estree-walker: 2.0.2
6359 + source-map-js: 1.2.1
6360 +
6361 '@vue/compiler-dom@3.5.22':
6362 dependencies:
6363 '@vue/compiler-core': 3.5.22
6364 '@vue/shared': 3.5.22
6365
6366 + '@vue/compiler-dom@3.5.25':
6367 + dependencies:
6368 + '@vue/compiler-core': 3.5.25
6369 + '@vue/shared': 3.5.25
6370 +
6371 '@vue/compiler-sfc@3.5.22':
6372 dependencies:
6373 '@babel/parser': 7.28.5
@@ -6322,26 +6380,43 @@ snapshots:
6380 postcss: 8.5.6
6381 source-map-js: 1.2.1
6382
6383 + '@vue/compiler-sfc@3.5.25':
6384 + dependencies:
6385 + '@babel/parser': 7.28.5
6386 + '@vue/compiler-core': 3.5.25
6387 + '@vue/compiler-dom': 3.5.25
6388 + '@vue/compiler-ssr': 3.5.25
6389 + '@vue/shared': 3.5.25
6390 + estree-walker: 2.0.2
6391 + magic-string: 0.30.21
6392 + postcss: 8.5.6
6393 + source-map-js: 1.2.1
6394 +
6395 '@vue/compiler-ssr@3.5.22':
6396 dependencies:
6397 '@vue/compiler-dom': 3.5.22
6398 '@vue/shared': 3.5.22
6399
6400 + '@vue/compiler-ssr@3.5.25':
6401 + dependencies:
6402 + '@vue/compiler-dom': 3.5.25
6403 + '@vue/shared': 3.5.25
6404 +
6405 '@vue/devtools-api@6.6.4': {}
6406
6407 '@vue/devtools-api@7.7.7':
6408 dependencies:
6409 '@vue/devtools-kit': 7.7.7
6410
6336 - '@vue/devtools-core@8.0.3(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))':
6411 + '@vue/devtools-core@8.0.5(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))':
6412 dependencies:
6338 - '@vue/devtools-kit': 8.0.3
6339 - '@vue/devtools-shared': 8.0.3
6413 + '@vue/devtools-kit': 8.0.5
6414 + '@vue/devtools-shared': 8.0.5
6415 mitt: 3.0.1
6416 nanoid: 5.1.6
6417 pathe: 2.0.3
6343 - vite-hot-client: 2.1.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))
6344 - vue: 3.5.22(typescript@5.9.3)
6418 + vite-hot-client: 2.1.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))
6419 + vue: 3.5.25(typescript@5.9.3)
6420 transitivePeerDependencies:
6421 - vite
6422
@@ -6355,9 +6430,9 @@ snapshots:
6430 speakingurl: 14.0.1
6431 superjson: 2.2.5
6432
6358 - '@vue/devtools-kit@8.0.3':
6433 + '@vue/devtools-kit@8.0.5':
6434 dependencies:
6360 - '@vue/devtools-shared': 8.0.3
6435 + '@vue/devtools-shared': 8.0.5
6436 birpc: 2.6.1
6437 hookable: 5.5.3
6438 mitt: 3.0.1
@@ -6369,11 +6444,11 @@ snapshots:
6444 dependencies:
6445 rfdc: 1.4.1
6446
6372 - '@vue/devtools-shared@8.0.3':
6447 + '@vue/devtools-shared@8.0.5':
6448 dependencies:
6449 rfdc: 1.4.1
6450
6376 - '@vue/language-core@3.1.2(typescript@5.9.3)':
6451 + '@vue/language-core@3.1.5(typescript@5.9.3)':
6452 dependencies:
6453 '@volar/language-core': 2.4.23
6454 '@vue/compiler-dom': 3.5.22
@@ -6385,79 +6460,81 @@ snapshots:
6460 optionalDependencies:
6461 typescript: 5.9.3
6462
6388 - '@vue/reactivity@3.5.22':
6463 + '@vue/reactivity@3.5.25':
6464 dependencies:
6390 - '@vue/shared': 3.5.22
6465 + '@vue/shared': 3.5.25
6466
6392 - '@vue/runtime-core@3.5.22':
6467 + '@vue/runtime-core@3.5.25':
6468 dependencies:
6394 - '@vue/reactivity': 3.5.22
6395 - '@vue/shared': 3.5.22
6469 + '@vue/reactivity': 3.5.25
6470 + '@vue/shared': 3.5.25
6471
6397 - '@vue/runtime-dom@3.5.22':
6472 + '@vue/runtime-dom@3.5.25':
6473 dependencies:
6399 - '@vue/reactivity': 3.5.22
6400 - '@vue/runtime-core': 3.5.22
6401 - '@vue/shared': 3.5.22
6474 + '@vue/reactivity': 3.5.25
6475 + '@vue/runtime-core': 3.5.25
6476 + '@vue/shared': 3.5.25
6477 csstype: 3.1.3
6478
6404 - '@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3))':
6479 + '@vue/server-renderer@3.5.25(vue@3.5.25(typescript@5.9.3))':
6480 dependencies:
6406 - '@vue/compiler-ssr': 3.5.22
6407 - '@vue/shared': 3.5.22
6408 - vue: 3.5.22(typescript@5.9.3)
6481 + '@vue/compiler-ssr': 3.5.25
6482 + '@vue/shared': 3.5.25
6483 + vue: 3.5.25(typescript@5.9.3)
6484
6485 '@vue/shared@3.5.22': {}
6486
6487 + '@vue/shared@3.5.25': {}
6488 +
6489 '@vue/test-utils@2.4.6':
6490 dependencies:
6491 js-beautify: 1.15.4
6492 vue-component-type-helpers: 2.2.12
6493
6417 - '@vue/tsconfig@0.7.0(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))':
6494 + '@vue/tsconfig@0.7.0(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))':
6495 optionalDependencies:
6496 typescript: 5.9.3
6420 - vue: 3.5.22(typescript@5.9.3)
6497 + vue: 3.5.25(typescript@5.9.3)
6498
6422 - '@vueuse/core@13.9.0(vue@3.5.22(typescript@5.9.3))':
6499 + '@vueuse/core@13.9.0(vue@3.5.25(typescript@5.9.3))':
6500 dependencies:
6501 '@types/web-bluetooth': 0.0.21
6502 '@vueuse/metadata': 13.9.0
6426 - '@vueuse/shared': 13.9.0(vue@3.5.22(typescript@5.9.3))
6427 - vue: 3.5.22(typescript@5.9.3)
6503 + '@vueuse/shared': 13.9.0(vue@3.5.25(typescript@5.9.3))
6504 + vue: 3.5.25(typescript@5.9.3)
6505
6429 - '@vueuse/core@14.0.0(vue@3.5.22(typescript@5.9.3))':
6506 + '@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3))':
6507 dependencies:
6508 '@types/web-bluetooth': 0.0.21
6432 - '@vueuse/metadata': 14.0.0
6433 - '@vueuse/shared': 14.0.0(vue@3.5.22(typescript@5.9.3))
6434 - vue: 3.5.22(typescript@5.9.3)
6509 + '@vueuse/metadata': 14.1.0
6510 + '@vueuse/shared': 14.1.0(vue@3.5.25(typescript@5.9.3))
6511 + vue: 3.5.25(typescript@5.9.3)
6512
6513 '@vueuse/metadata@13.9.0': {}
6514
6438 - '@vueuse/metadata@14.0.0': {}
6515 + '@vueuse/metadata@14.1.0': {}
6516
6440 - '@vueuse/motion@3.0.3(vue@3.5.22(typescript@5.9.3))':
6517 + '@vueuse/motion@3.0.3(vue@3.5.25(typescript@5.9.3))':
6518 dependencies:
6442 - '@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.9.3))
6443 - '@vueuse/shared': 13.9.0(vue@3.5.22(typescript@5.9.3))
6519 + '@vueuse/core': 13.9.0(vue@3.5.25(typescript@5.9.3))
6520 + '@vueuse/shared': 13.9.0(vue@3.5.25(typescript@5.9.3))
6521 defu: 6.1.4
6522 framesync: 6.1.2
6523 popmotion: 11.0.5
6524 style-value-types: 5.1.2
6448 - vue: 3.5.22(typescript@5.9.3)
6525 + vue: 3.5.25(typescript@5.9.3)
6526 optionalDependencies:
6527 '@nuxt/kit': 3.20.0
6528 transitivePeerDependencies:
6529 - magicast
6530
6454 - '@vueuse/shared@13.9.0(vue@3.5.22(typescript@5.9.3))':
6531 + '@vueuse/shared@13.9.0(vue@3.5.25(typescript@5.9.3))':
6532 dependencies:
6456 - vue: 3.5.22(typescript@5.9.3)
6533 + vue: 3.5.25(typescript@5.9.3)
6534
6458 - '@vueuse/shared@14.0.0(vue@3.5.22(typescript@5.9.3))':
6535 + '@vueuse/shared@14.1.0(vue@3.5.25(typescript@5.9.3))':
6536 dependencies:
6460 - vue: 3.5.22(typescript@5.9.3)
6537 + vue: 3.5.25(typescript@5.9.3)
6538
6539 '@yr/monotone-cubic-spline@1.0.3': {}
6540
@@ -6567,7 +6644,7 @@ snapshots:
6644
6645 aws4@1.13.2: {}
6646
6570 - axios@1.13.1(debug@4.4.3):
6647 + axios@1.13.2(debug@4.4.3):
6648 dependencies:
6649 follow-redirects: 1.15.11(debug@4.4.3)
6650 form-data: 4.0.4
@@ -6675,7 +6752,7 @@ snapshots:
6752
6753 ccount@2.0.1: {}
6754
6678 - chai@6.2.0: {}
6755 + chai@6.2.1: {}
6756
6757 chalk@4.1.2:
6758 dependencies:
@@ -6743,10 +6820,10 @@ snapshots:
6820 '@codemirror/autocomplete': 6.19.1
6821 '@codemirror/commands': 6.10.0
6822 '@codemirror/language': 6.11.3
6746 - '@codemirror/lint': 6.9.1
6823 + '@codemirror/lint': 6.9.2
6824 '@codemirror/search': 6.5.11
6825 '@codemirror/state': 6.5.2
6749 - '@codemirror/view': 6.38.6
6826 + '@codemirror/view': 6.38.8
6827
6828 color-convert@2.0.1:
6829 dependencies:
@@ -6857,7 +6934,7 @@ snapshots:
6934 dependencies:
6935 css-tree: 2.2.1
6936
6860 - cssstyle@5.3.2:
6937 + cssstyle@5.3.3:
6938 dependencies:
6939 '@asamuzakjp/css-color': 4.0.5
6940 '@csstools/css-syntax-patches-for-csstree': 1.0.15
@@ -6867,7 +6944,7 @@ snapshots:
6944
6945 csstype@3.1.3: {}
6946
6870 - cypress@15.5.0:
6947 + cypress@15.7.0:
6948 dependencies:
6949 '@cypress/request': 3.0.9
6950 '@cypress/xvfb': 1.2.4(supports-color@8.1.1)
@@ -6905,7 +6982,6 @@ snapshots:
6982 process: 0.11.10
6983 proxy-from-env: 1.0.0
6984 request-progress: 3.0.0
6908 - semver: 7.7.3
6985 supports-color: 8.1.1
6986 systeminformation: 5.27.7
6987 tmp: 0.2.5
@@ -6922,11 +6998,11 @@ snapshots:
6998 whatwg-mimetype: 4.0.0
6999 whatwg-url: 15.1.0
7000
6925 - date-fns-tz@3.2.0(date-fns@3.6.0):
7001 + date-fns-tz@3.2.0(date-fns@4.1.0):
7002 dependencies:
6927 - date-fns: 3.6.0
7003 + date-fns: 4.1.0
7004
6929 - date-fns@3.6.0: {}
7005 + date-fns@4.1.0: {}
7006
7007 dayjs@1.11.19: {}
7008
@@ -6952,8 +7028,6 @@ snapshots:
7028
7029 deep-is@0.1.4: {}
7030
6955 - deep-pick-omit@1.2.1: {}
6956 -
7031 default-browser-id@5.0.0: {}
7032
7033 default-browser@5.2.1:
@@ -7157,60 +7231,60 @@ snapshots:
7231
7232 escape-string-regexp@5.0.0: {}
7233
7160 - eslint-compat-utils@0.5.1(eslint@9.38.0(jiti@2.6.1)):
7234 + eslint-compat-utils@0.5.1(eslint@9.39.1(jiti@2.6.1)):
7235 dependencies:
7162 - eslint: 9.38.0(jiti@2.6.1)
7236 + eslint: 9.39.1(jiti@2.6.1)
7237 semver: 7.7.3
7238
7165 - eslint-compat-utils@0.6.5(eslint@9.38.0(jiti@2.6.1)):
7239 + eslint-compat-utils@0.6.5(eslint@9.39.1(jiti@2.6.1)):
7240 dependencies:
7167 - eslint: 9.38.0(jiti@2.6.1)
7241 + eslint: 9.39.1(jiti@2.6.1)
7242 semver: 7.7.3
7243
7170 - eslint-config-flat-gitignore@2.1.0(eslint@9.38.0(jiti@2.6.1)):
7244 + eslint-config-flat-gitignore@2.1.0(eslint@9.39.1(jiti@2.6.1)):
7245 dependencies:
7172 - '@eslint/compat': 1.4.1(eslint@9.38.0(jiti@2.6.1))
7173 - eslint: 9.38.0(jiti@2.6.1)
7246 + '@eslint/compat': 1.4.1(eslint@9.39.1(jiti@2.6.1))
7247 + eslint: 9.39.1(jiti@2.6.1)
7248
7249 eslint-flat-config-utils@2.1.4:
7250 dependencies:
7251 pathe: 2.0.3
7252
7179 - eslint-json-compat-utils@0.2.1(eslint@9.38.0(jiti@2.6.1))(jsonc-eslint-parser@2.4.1):
7253 + eslint-json-compat-utils@0.2.1(eslint@9.39.1(jiti@2.6.1))(jsonc-eslint-parser@2.4.1):
7254 dependencies:
7181 - eslint: 9.38.0(jiti@2.6.1)
7255 + eslint: 9.39.1(jiti@2.6.1)
7256 esquery: 1.6.0
7257 jsonc-eslint-parser: 2.4.1
7258
7185 - eslint-merge-processors@2.0.0(eslint@9.38.0(jiti@2.6.1)):
7259 + eslint-merge-processors@2.0.0(eslint@9.39.1(jiti@2.6.1)):
7260 dependencies:
7187 - eslint: 9.38.0(jiti@2.6.1)
7261 + eslint: 9.39.1(jiti@2.6.1)
7262
7189 - eslint-plugin-antfu@3.1.1(eslint@9.38.0(jiti@2.6.1)):
7263 + eslint-plugin-antfu@3.1.1(eslint@9.39.1(jiti@2.6.1)):
7264 dependencies:
7191 - eslint: 9.38.0(jiti@2.6.1)
7265 + eslint: 9.39.1(jiti@2.6.1)
7266
7193 - eslint-plugin-command@3.3.1(eslint@9.38.0(jiti@2.6.1)):
7267 + eslint-plugin-command@3.3.1(eslint@9.39.1(jiti@2.6.1)):
7268 dependencies:
7269 '@es-joy/jsdoccomment': 0.50.2
7196 - eslint: 9.38.0(jiti@2.6.1)
7270 + eslint: 9.39.1(jiti@2.6.1)
7271
7198 - eslint-plugin-es-x@7.8.0(eslint@9.38.0(jiti@2.6.1)):
7272 + eslint-plugin-es-x@7.8.0(eslint@9.39.1(jiti@2.6.1)):
7273 dependencies:
7200 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1))
7274 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
7275 '@eslint-community/regexpp': 4.12.2
7202 - eslint: 9.38.0(jiti@2.6.1)
7203 - eslint-compat-utils: 0.5.1(eslint@9.38.0(jiti@2.6.1))
7276 + eslint: 9.39.1(jiti@2.6.1)
7277 + eslint-compat-utils: 0.5.1(eslint@9.39.1(jiti@2.6.1))
7278
7205 - eslint-plugin-import-lite@0.3.0(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3):
7279 + eslint-plugin-import-lite@0.3.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
7280 dependencies:
7207 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1))
7281 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
7282 '@typescript-eslint/types': 8.46.2
7209 - eslint: 9.38.0(jiti@2.6.1)
7283 + eslint: 9.39.1(jiti@2.6.1)
7284 optionalDependencies:
7285 typescript: 5.9.3
7286
7213 - eslint-plugin-jsdoc@61.1.11(eslint@9.38.0(jiti@2.6.1)):
7287 + eslint-plugin-jsdoc@61.1.11(eslint@9.39.1(jiti@2.6.1)):
7288 dependencies:
7289 '@es-joy/jsdoccomment': 0.76.0
7290 '@es-joy/resolve.exports': 1.2.0
@@ -7218,7 +7292,7 @@ snapshots:
7292 comment-parser: 1.4.1
7293 debug: 4.4.3(supports-color@8.1.1)
7294 escape-string-regexp: 4.0.0
7221 - eslint: 9.38.0(jiti@2.6.1)
7295 + eslint: 9.39.1(jiti@2.6.1)
7296 espree: 10.4.0
7297 esquery: 1.6.0
7298 html-entities: 2.6.0
@@ -7230,13 +7304,13 @@ snapshots:
7304 transitivePeerDependencies:
7305 - supports-color
7306
7233 - eslint-plugin-jsonc@2.21.0(eslint@9.38.0(jiti@2.6.1)):
7307 + eslint-plugin-jsonc@2.21.0(eslint@9.39.1(jiti@2.6.1)):
7308 dependencies:
7235 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1))
7309 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
7310 diff-sequences: 27.5.1
7237 - eslint: 9.38.0(jiti@2.6.1)
7238 - eslint-compat-utils: 0.6.5(eslint@9.38.0(jiti@2.6.1))
7239 - eslint-json-compat-utils: 0.2.1(eslint@9.38.0(jiti@2.6.1))(jsonc-eslint-parser@2.4.1)
7311 + eslint: 9.39.1(jiti@2.6.1)
7312 + eslint-compat-utils: 0.6.5(eslint@9.39.1(jiti@2.6.1))
7313 + eslint-json-compat-utils: 0.2.1(eslint@9.39.1(jiti@2.6.1))(jsonc-eslint-parser@2.4.1)
7314 espree: 10.4.0
7315 graphemer: 1.4.0
7316 jsonc-eslint-parser: 2.4.1
@@ -7245,12 +7319,12 @@ snapshots:
7319 transitivePeerDependencies:
7320 - '@eslint/json'
7321
7248 - eslint-plugin-n@17.23.1(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3):
7322 + eslint-plugin-n@17.23.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
7323 dependencies:
7250 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1))
7324 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
7325 enhanced-resolve: 5.18.3
7252 - eslint: 9.38.0(jiti@2.6.1)
7253 - eslint-plugin-es-x: 7.8.0(eslint@9.38.0(jiti@2.6.1))
7326 + eslint: 9.39.1(jiti@2.6.1)
7327 + eslint-plugin-es-x: 7.8.0(eslint@9.39.1(jiti@2.6.1))
7328 get-tsconfig: 4.13.0
7329 globals: 15.15.0
7330 globrex: 0.1.2
@@ -7262,57 +7336,57 @@ snapshots:
7336
7337 eslint-plugin-no-only-tests@3.3.0: {}
7338
7265 - eslint-plugin-perfectionist@4.15.1(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3):
7339 + eslint-plugin-perfectionist@4.15.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
7340 dependencies:
7341 '@typescript-eslint/types': 8.46.2
7268 - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
7269 - eslint: 9.38.0(jiti@2.6.1)
7342 + '@typescript-eslint/utils': 8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
7343 + eslint: 9.39.1(jiti@2.6.1)
7344 natural-orderby: 5.0.0
7345 transitivePeerDependencies:
7346 - supports-color
7347 - typescript
7348
7275 - eslint-plugin-pnpm@1.3.0(eslint@9.38.0(jiti@2.6.1)):
7349 + eslint-plugin-pnpm@1.3.0(eslint@9.39.1(jiti@2.6.1)):
7350 dependencies:
7351 empathic: 2.0.0
7278 - eslint: 9.38.0(jiti@2.6.1)
7352 + eslint: 9.39.1(jiti@2.6.1)
7353 jsonc-eslint-parser: 2.4.1
7354 pathe: 2.0.3
7355 pnpm-workspace-yaml: 1.3.0
7356 tinyglobby: 0.2.15
7357 yaml-eslint-parser: 1.3.0
7358
7285 - eslint-plugin-regexp@2.10.0(eslint@9.38.0(jiti@2.6.1)):
7359 + eslint-plugin-regexp@2.10.0(eslint@9.39.1(jiti@2.6.1)):
7360 dependencies:
7287 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1))
7361 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
7362 '@eslint-community/regexpp': 4.12.2
7363 comment-parser: 1.4.1
7290 - eslint: 9.38.0(jiti@2.6.1)
7364 + eslint: 9.39.1(jiti@2.6.1)
7365 jsdoc-type-pratt-parser: 4.8.0
7366 refa: 0.12.1
7367 regexp-ast-analysis: 0.7.1
7368 scslre: 0.3.0
7369
7296 - eslint-plugin-toml@0.12.0(eslint@9.38.0(jiti@2.6.1)):
7370 + eslint-plugin-toml@0.12.0(eslint@9.39.1(jiti@2.6.1)):
7371 dependencies:
7372 debug: 4.4.3(supports-color@8.1.1)
7299 - eslint: 9.38.0(jiti@2.6.1)
7300 - eslint-compat-utils: 0.6.5(eslint@9.38.0(jiti@2.6.1))
7373 + eslint: 9.39.1(jiti@2.6.1)
7374 + eslint-compat-utils: 0.6.5(eslint@9.39.1(jiti@2.6.1))
7375 lodash: 4.17.21
7376 toml-eslint-parser: 0.10.0
7377 transitivePeerDependencies:
7378 - supports-color
7379
7306 - eslint-plugin-unicorn@61.0.2(eslint@9.38.0(jiti@2.6.1)):
7380 + eslint-plugin-unicorn@62.0.0(eslint@9.39.1(jiti@2.6.1)):
7381 dependencies:
7382 '@babel/helper-validator-identifier': 7.28.5
7309 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1))
7310 - '@eslint/plugin-kit': 0.3.5
7383 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
7384 + '@eslint/plugin-kit': 0.4.1
7385 change-case: 5.4.4
7386 ci-info: 4.3.1
7387 clean-regexp: 1.0.0
7388 core-js-compat: 3.46.0
7315 - eslint: 9.38.0(jiti@2.6.1)
7389 + eslint: 9.39.1(jiti@2.6.1)
7390 esquery: 1.6.0
7391 find-up-simple: 1.0.1
7392 globals: 16.4.0
@@ -7321,46 +7395,46 @@ snapshots:
7395 jsesc: 3.1.0
7396 pluralize: 8.0.0
7397 regexp-tree: 0.1.27
7324 - regjsparser: 0.12.0
7398 + regjsparser: 0.13.0
7399 semver: 7.7.3
7400 strip-indent: 4.1.1
7401
7328 - eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1)):
7402 + eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)):
7403 dependencies:
7330 - eslint: 9.38.0(jiti@2.6.1)
7404 + eslint: 9.39.1(jiti@2.6.1)
7405 optionalDependencies:
7332 - '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
7406 + '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
7407
7334 - eslint-plugin-vue@10.5.1(@stylistic/eslint-plugin@5.5.0(eslint@9.38.0(jiti@2.6.1)))(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.38.0(jiti@2.6.1))):
7408 + eslint-plugin-vue@10.5.1(@stylistic/eslint-plugin@5.5.0(eslint@9.39.1(jiti@2.6.1)))(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1))):
7409 dependencies:
7336 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1))
7337 - eslint: 9.38.0(jiti@2.6.1)
7410 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
7411 + eslint: 9.39.1(jiti@2.6.1)
7412 natural-compare: 1.4.0
7413 nth-check: 2.1.1
7414 postcss-selector-parser: 6.1.2
7415 semver: 7.7.3
7342 - vue-eslint-parser: 10.2.0(eslint@9.38.0(jiti@2.6.1))
7416 + vue-eslint-parser: 10.2.0(eslint@9.39.1(jiti@2.6.1))
7417 xml-name-validator: 4.0.0
7418 optionalDependencies:
7345 - '@stylistic/eslint-plugin': 5.5.0(eslint@9.38.0(jiti@2.6.1))
7346 - '@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
7419 + '@stylistic/eslint-plugin': 5.5.0(eslint@9.39.1(jiti@2.6.1))
7420 + '@typescript-eslint/parser': 8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
7421
7348 - eslint-plugin-yml@1.19.0(eslint@9.38.0(jiti@2.6.1)):
7422 + eslint-plugin-yml@1.19.0(eslint@9.39.1(jiti@2.6.1)):
7423 dependencies:
7424 debug: 4.4.3(supports-color@8.1.1)
7425 diff-sequences: 27.5.1
7426 escape-string-regexp: 4.0.0
7353 - eslint: 9.38.0(jiti@2.6.1)
7354 - eslint-compat-utils: 0.6.5(eslint@9.38.0(jiti@2.6.1))
7427 + eslint: 9.39.1(jiti@2.6.1)
7428 + eslint-compat-utils: 0.6.5(eslint@9.39.1(jiti@2.6.1))
7429 natural-compare: 1.4.0
7430 yaml-eslint-parser: 1.3.0
7431 transitivePeerDependencies:
7432 - supports-color
7433
7360 - eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.22)(eslint@9.38.0(jiti@2.6.1)):
7434 + eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1)):
7435 dependencies:
7362 - '@vue/compiler-sfc': 3.5.22
7363 - eslint: 9.38.0(jiti@2.6.1)
7436 + '@vue/compiler-sfc': 3.5.25
7437 + eslint: 9.39.1(jiti@2.6.1)
7438
7439 eslint-scope@8.4.0:
7440 dependencies:
@@ -7371,15 +7445,15 @@ snapshots:
7445
7446 eslint-visitor-keys@4.2.1: {}
7447
7374 - eslint@9.38.0(jiti@2.6.1):
7448 + eslint@9.39.1(jiti@2.6.1):
7449 dependencies:
7376 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1))
7450 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
7451 '@eslint-community/regexpp': 4.12.2
7452 '@eslint/config-array': 0.21.1
7453 '@eslint/config-helpers': 0.4.2
7380 - '@eslint/core': 0.16.0
7454 + '@eslint/core': 0.17.0
7455 '@eslint/eslintrc': 3.3.1
7382 - '@eslint/js': 9.38.0
7456 + '@eslint/js': 9.39.1
7457 '@eslint/plugin-kit': 0.4.1
7458 '@humanfs/node': 0.16.7
7459 '@humanwhocodes/module-importer': 1.0.1
@@ -7522,7 +7596,7 @@ snapshots:
7596
7597 fast-levenshtein@2.0.6: {}
7598
7525 - fast-xml-parser@5.3.0:
7599 + fast-xml-parser@5.3.2:
7600 dependencies:
7601 strnum: 2.1.1
7602
@@ -7917,7 +7991,7 @@ snapshots:
7991 '@hapi/topo': 6.0.2
7992 '@standard-schema/spec': 1.0.0
7993
7920 - jose@6.1.0: {}
7994 + jose@6.1.2: {}
7995
7996 js-beautify@1.15.4:
7997 dependencies:
@@ -7950,11 +8024,11 @@ snapshots:
8024
8025 jsdoc-type-pratt-parser@6.10.0: {}
8026
7953 - jsdom@27.1.0:
8027 + jsdom@27.2.0:
8028 dependencies:
7955 - '@acemir/cssom': 0.9.19
7956 - '@asamuzakjp/dom-selector': 6.7.3
7957 - cssstyle: 5.3.2
8029 + '@acemir/cssom': 0.9.24
8030 + '@asamuzakjp/dom-selector': 6.7.4
8031 + cssstyle: 5.3.3
8032 data-urls: 6.0.0
8033 decimal.js: 10.6.0
8034 html-encoding-sniffer: 4.0.0
@@ -7977,8 +8051,6 @@ snapshots:
8051 - supports-color
8052 - utf-8-validate
8053
7980 - jsesc@3.0.2: {}
7981 -
8054 jsesc@3.1.0: {}
8055
8056 json-buffer@3.0.1: {}
@@ -8560,28 +8632,28 @@ snapshots:
8632 arrify: 2.0.1
8633 minimatch: 3.1.2
8634
8563 - naive-ui@2.43.1(vue@3.5.22(typescript@5.9.3)):
8635 + naive-ui@2.43.2(vue@3.5.25(typescript@5.9.3)):
8636 dependencies:
8637 '@css-render/plugin-bem': 0.15.14(css-render@0.15.14)
8566 - '@css-render/vue3-ssr': 0.15.14(vue@3.5.22(typescript@5.9.3))
8638 + '@css-render/vue3-ssr': 0.15.14(vue@3.5.25(typescript@5.9.3))
8639 '@types/katex': 0.16.7
8568 - '@types/lodash': 4.17.20
8640 + '@types/lodash': 4.17.21
8641 '@types/lodash-es': 4.17.12
8642 async-validator: 4.2.5
8643 css-render: 0.15.14
8644 csstype: 3.1.3
8573 - date-fns: 3.6.0
8574 - date-fns-tz: 3.2.0(date-fns@3.6.0)
8645 + date-fns: 4.1.0
8646 + date-fns-tz: 3.2.0(date-fns@4.1.0)
8647 evtd: 0.2.4
8648 highlight.js: 11.11.1
8649 lodash: 4.17.21
8650 lodash-es: 4.17.21
8651 seemly: 0.3.10
8652 treemate: 0.3.11
8581 - vdirs: 0.1.8(vue@3.5.22(typescript@5.9.3))
8582 - vooks: 0.2.12(vue@3.5.22(typescript@5.9.3))
8583 - vue: 3.5.22(typescript@5.9.3)
8584 - vueuc: 0.4.65(vue@3.5.22(typescript@5.9.3))
8653 + vdirs: 0.1.8(vue@3.5.25(typescript@5.9.3))
8654 + vooks: 0.2.12(vue@3.5.25(typescript@5.9.3))
8655 + vue: 3.5.25(typescript@5.9.3)
8656 + vueuc: 0.4.65(vue@3.5.25(typescript@5.9.3))
8657
8658 nanoid@3.3.11: {}
8659
@@ -8636,7 +8708,9 @@ snapshots:
8708
8709 object-inspect@1.13.4: {}
8710
8639 - ofetch@1.5.0:
8711 + obug@2.1.1: {}
8712 +
8713 + ofetch@1.5.1:
8714 dependencies:
8715 destr: 2.0.5
8716 node-fetch-native: 1.6.7
@@ -8658,7 +8732,7 @@ snapshots:
8732
8733 oniguruma-parser@0.12.1: {}
8734
8661 - oniguruma-to-es@4.3.3:
8735 + oniguruma-to-es@4.3.4:
8736 dependencies:
8737 oniguruma-parser: 0.12.1
8738 regex: 6.0.1
@@ -8774,19 +8848,17 @@ snapshots:
8848
8849 pify@2.3.0: {}
8850
8777 - pinia-plugin-persistedstate@4.5.0(@nuxt/kit@3.20.0)(pinia@3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))):
8851 + pinia-plugin-persistedstate@4.7.1(@nuxt/kit@3.20.0)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))):
8852 dependencies:
8779 - deep-pick-omit: 1.2.1
8853 defu: 6.1.4
8781 - destr: 2.0.5
8854 optionalDependencies:
8855 '@nuxt/kit': 3.20.0
8784 - pinia: 3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))
8856 + pinia: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
8857
8786 - pinia@3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)):
8858 + pinia@3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)):
8859 dependencies:
8860 '@vue/devtools-api': 7.7.7
8789 - vue: 3.5.22(typescript@5.9.3)
8861 + vue: 3.5.25(typescript@5.9.3)
8862 optionalDependencies:
8863 typescript: 5.9.3
8864
@@ -8832,11 +8904,11 @@ snapshots:
8904
8905 prelude-ls@1.2.1: {}
8906
8835 - prettier-plugin-tailwindcss@0.7.1(prettier@3.6.2):
8907 + prettier-plugin-tailwindcss@0.7.1(prettier@3.7.3):
8908 dependencies:
8837 - prettier: 3.6.2
8909 + prettier: 3.7.3
8910
8839 - prettier@3.6.2: {}
8911 + prettier@3.7.3: {}
8912
8913 pretty-bytes@5.6.0: {}
8914
@@ -8909,9 +8981,9 @@ snapshots:
8981
8982 regexp-tree@0.1.27: {}
8983
8912 - regjsparser@0.12.0:
8984 + regjsparser@0.13.0:
8985 dependencies:
8914 - jsesc: 3.0.2
8986 + jsesc: 3.1.0
8987
8988 request-progress@3.0.0:
8989 dependencies:
@@ -9007,7 +9079,7 @@ snapshots:
9079
9080 safer-buffer@2.1.2: {}
9081
9010 - sass@1.93.2:
9082 + sass@1.94.2:
9083 dependencies:
9084 chokidar: 4.0.3
9085 immutable: 5.1.4
@@ -9049,14 +9121,14 @@ snapshots:
9121
9122 shell-quote@1.8.3: {}
9123
9052 - shiki@3.14.0:
9124 + shiki@3.17.0:
9125 dependencies:
9054 - '@shikijs/core': 3.14.0
9055 - '@shikijs/engine-javascript': 3.14.0
9056 - '@shikijs/engine-oniguruma': 3.14.0
9057 - '@shikijs/langs': 3.14.0
9058 - '@shikijs/themes': 3.14.0
9059 - '@shikijs/types': 3.14.0
9126 + '@shikijs/core': 3.17.0
9127 + '@shikijs/engine-javascript': 3.17.0
9128 + '@shikijs/engine-oniguruma': 3.17.0
9129 + '@shikijs/langs': 3.17.0
9130 + '@shikijs/themes': 3.17.0
9131 + '@shikijs/types': 3.17.0
9132 '@shikijs/vscode-textmate': 10.0.2
9133 '@types/hast': 3.0.4
9134
@@ -9153,7 +9225,7 @@ snapshots:
9225
9226 stackback@0.0.2: {}
9227
9156 - start-server-and-test@2.1.2:
9228 + start-server-and-test@2.1.3:
9229 dependencies:
9230 arg: 5.0.2
9231 bluebird: 3.7.2
@@ -9162,7 +9234,7 @@ snapshots:
9234 execa: 5.1.1
9235 lazy-ass: 1.6.0
9236 ps-tree: 1.2.0
9165 - wait-on: 8.0.5(debug@4.4.3)
9237 + wait-on: 9.0.3(debug@4.4.3)
9238 transitivePeerDependencies:
9239 - supports-color
9240
@@ -9246,30 +9318,30 @@ snapshots:
9318
9319 tagged-tag@1.0.0: {}
9320
9249 - tailwindcss@4.1.16: {}
9321 + tailwindcss@4.1.17: {}
9322
9323 tapable@2.3.0: {}
9324
9253 - taze@19.9.0:
9325 + taze@19.9.2:
9326 dependencies:
9327 '@antfu/ni': 27.0.1
9328 cac: 6.7.14
9329 find-up-simple: 1.0.1
9258 - ofetch: 1.5.0
9330 + ofetch: 1.5.1
9331 package-manager-detector: 1.5.0
9332 pathe: 2.0.3
9333 pnpm-workspace-yaml: 1.3.0
9334 restore-cursor: 5.1.0
9263 - tinyexec: 1.0.1
9335 + tinyexec: 1.0.2
9336 tinyglobby: 0.2.15
9265 - unconfig: 7.3.3
9337 + unconfig: 7.4.1
9338 yaml: 2.8.1
9339
9268 - thememirror@2.0.1(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6):
9340 + thememirror@2.0.1(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8):
9341 dependencies:
9342 '@codemirror/language': 6.11.3
9343 '@codemirror/state': 6.5.2
9272 - '@codemirror/view': 6.38.6
9344 + '@codemirror/view': 6.38.8
9345
9346 throttleit@1.0.1: {}
9347
@@ -9281,6 +9353,8 @@ snapshots:
9353
9354 tinyexec@1.0.1: {}
9355
9356 + tinyexec@1.0.2: {}
9357 +
9358 tinyglobby@0.2.15:
9359 dependencies:
9360 fdir: 6.5.0(picomatch@4.0.3)
@@ -9364,7 +9438,7 @@ snapshots:
9438
9439 type-fest@0.8.1: {}
9440
9367 - type-fest@5.1.0:
9441 + type-fest@5.2.0:
9442 dependencies:
9443 tagged-tag: 1.0.0
9444
@@ -9374,12 +9448,18 @@ snapshots:
9448
9449 ufo@1.6.1: {}
9450
9377 - unconfig@7.3.3:
9451 + unconfig-core@7.4.1:
9452 + dependencies:
9453 + '@quansync/fs': 0.1.5
9454 + quansync: 0.2.11
9455 +
9456 + unconfig@7.4.1:
9457 dependencies:
9458 '@quansync/fs': 0.1.5
9459 defu: 6.1.4
9460 jiti: 2.6.1
9461 quansync: 0.2.11
9462 + unconfig-core: 7.4.1
9463
9464 unctx@2.4.1:
9465 dependencies:
@@ -9454,12 +9534,12 @@ snapshots:
9534
9535 uuid@8.3.2: {}
9536
9457 - validator@13.15.20: {}
9537 + validator@13.15.23: {}
9538
9459 - vdirs@0.1.8(vue@3.5.22(typescript@5.9.3)):
9539 + vdirs@0.1.8(vue@3.5.25(typescript@5.9.3)):
9540 dependencies:
9541 evtd: 0.2.4
9462 - vue: 3.5.22(typescript@5.9.3)
9542 + vue: 3.5.25(typescript@5.9.3)
9543
9544 verror@1.10.0:
9545 dependencies:
@@ -9488,17 +9568,17 @@ snapshots:
9568 - rollup
9569 - supports-color
9570
9491 - vite-dev-rpc@1.1.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)):
9571 + vite-dev-rpc@1.1.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)):
9572 dependencies:
9573 birpc: 2.6.1
9494 - vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)
9495 - vite-hot-client: 2.1.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))
9574 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
9575 + vite-hot-client: 2.1.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))
9576
9497 - vite-hot-client@2.1.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)):
9577 + vite-hot-client@2.1.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)):
9578 dependencies:
9499 - vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)
9579 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
9580
9501 - vite-plugin-inspect@11.3.3(@nuxt/kit@3.20.0)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)):
9581 + vite-plugin-inspect@11.3.3(@nuxt/kit@3.20.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)):
9582 dependencies:
9583 ansis: 4.2.0
9584 debug: 4.4.3(supports-color@8.1.1)
@@ -9508,28 +9588,28 @@ snapshots:
9588 perfect-debounce: 2.0.0
9589 sirv: 3.0.2
9590 unplugin-utils: 0.3.1
9511 - vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)
9512 - vite-dev-rpc: 1.1.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))
9591 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
9592 + vite-dev-rpc: 1.1.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))
9593 optionalDependencies:
9594 '@nuxt/kit': 3.20.0
9595 transitivePeerDependencies:
9596 - supports-color
9597
9518 - vite-plugin-vue-devtools@8.0.3(@nuxt/kit@3.20.0)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)):
9598 + vite-plugin-vue-devtools@8.0.5(@nuxt/kit@3.20.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3)):
9599 dependencies:
9520 - '@vue/devtools-core': 8.0.3(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
9521 - '@vue/devtools-kit': 8.0.3
9522 - '@vue/devtools-shared': 8.0.3
9600 + '@vue/devtools-core': 8.0.5(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))
9601 + '@vue/devtools-kit': 8.0.5
9602 + '@vue/devtools-shared': 8.0.5
9603 sirv: 3.0.2
9524 - vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)
9525 - vite-plugin-inspect: 11.3.3(@nuxt/kit@3.20.0)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))
9526 - vite-plugin-vue-inspector: 5.3.2(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))
9604 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
9605 + vite-plugin-inspect: 11.3.3(@nuxt/kit@3.20.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))
9606 + vite-plugin-vue-inspector: 5.3.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))
9607 transitivePeerDependencies:
9608 - '@nuxt/kit'
9609 - supports-color
9610 - vue
9611
9532 - vite-plugin-vue-inspector@5.3.2(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)):
9612 + vite-plugin-vue-inspector@5.3.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)):
9613 dependencies:
9614 '@babel/core': 7.28.5
9615 '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.5)
@@ -9540,16 +9620,16 @@ snapshots:
9620 '@vue/compiler-dom': 3.5.22
9621 kolorist: 1.8.0
9622 magic-string: 0.30.21
9543 - vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)
9623 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
9624 transitivePeerDependencies:
9625 - supports-color
9626
9547 - vite-svg-loader@5.1.0(vue@3.5.22(typescript@5.9.3)):
9627 + vite-svg-loader@5.1.0(vue@3.5.25(typescript@5.9.3)):
9628 dependencies:
9629 svgo: 3.3.2
9550 - vue: 3.5.22(typescript@5.9.3)
9630 + vue: 3.5.25(typescript@5.9.3)
9631
9552 - vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1):
9632 + vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1):
9633 dependencies:
9634 esbuild: 0.25.11
9635 fdir: 6.5.0(picomatch@4.0.3)
@@ -9558,26 +9638,26 @@ snapshots:
9638 rollup: 4.52.5
9639 tinyglobby: 0.2.15
9640 optionalDependencies:
9561 - '@types/node': 24.9.2
9641 + '@types/node': 24.10.1
9642 fsevents: 2.3.3
9643 jiti: 2.6.1
9644 lightningcss: 1.30.2
9565 - sass: 1.93.2
9645 + sass: 1.94.2
9646 yaml: 2.8.1
9647
9568 - vitest@4.0.5(@types/debug@4.1.12)(@types/node@24.9.2)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1):
9648 + vitest@4.0.14(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1):
9649 dependencies:
9570 - '@vitest/expect': 4.0.5
9571 - '@vitest/mocker': 4.0.5(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))
9572 - '@vitest/pretty-format': 4.0.5
9573 - '@vitest/runner': 4.0.5
9574 - '@vitest/snapshot': 4.0.5
9575 - '@vitest/spy': 4.0.5
9576 - '@vitest/utils': 4.0.5
9577 - debug: 4.4.3(supports-color@8.1.1)
9650 + '@vitest/expect': 4.0.14
9651 + '@vitest/mocker': 4.0.14(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1))
9652 + '@vitest/pretty-format': 4.0.14
9653 + '@vitest/runner': 4.0.14
9654 + '@vitest/snapshot': 4.0.14
9655 + '@vitest/spy': 4.0.14
9656 + '@vitest/utils': 4.0.14
9657 es-module-lexer: 1.7.0
9658 expect-type: 1.2.2
9659 magic-string: 0.30.21
9660 + obug: 2.1.1
9661 pathe: 2.0.3
9662 picomatch: 4.0.3
9663 std-env: 3.10.0
@@ -9585,12 +9665,11 @@ snapshots:
9665 tinyexec: 0.3.2
9666 tinyglobby: 0.2.15
9667 tinyrainbow: 3.0.3
9588 - vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)
9668 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(yaml@2.8.1)
9669 why-is-node-running: 2.3.0
9670 optionalDependencies:
9591 - '@types/debug': 4.1.12
9592 - '@types/node': 24.9.2
9593 - jsdom: 27.1.0
9671 + '@types/node': 24.10.1
9672 + jsdom: 27.2.0
9673 transitivePeerDependencies:
9674 - jiti
9675 - less
@@ -9600,40 +9679,39 @@ snapshots:
9679 - sass-embedded
9680 - stylus
9681 - sugarss
9603 - - supports-color
9682 - terser
9683 - tsx
9684 - yaml
9685
9608 - vooks@0.2.12(vue@3.5.22(typescript@5.9.3)):
9686 + vooks@0.2.12(vue@3.5.25(typescript@5.9.3)):
9687 dependencies:
9688 evtd: 0.2.4
9611 - vue: 3.5.22(typescript@5.9.3)
9689 + vue: 3.5.25(typescript@5.9.3)
9690
9691 vscode-uri@3.1.0: {}
9692
9615 - vue-advanced-cropper@2.8.9(vue@3.5.22(typescript@5.9.3)):
9693 + vue-advanced-cropper@2.8.9(vue@3.5.25(typescript@5.9.3)):
9694 dependencies:
9695 classnames: 2.5.1
9696 debounce: 1.2.1
9697 easy-bem: 1.1.1
9620 - vue: 3.5.22(typescript@5.9.3)
9698 + vue: 3.5.25(typescript@5.9.3)
9699
9622 - vue-codemirror@6.1.1(codemirror@6.0.2)(vue@3.5.22(typescript@5.9.3)):
9700 + vue-codemirror@6.1.1(codemirror@6.0.2)(vue@3.5.25(typescript@5.9.3)):
9701 dependencies:
9702 '@codemirror/commands': 6.10.0
9703 '@codemirror/language': 6.11.3
9704 '@codemirror/state': 6.5.2
9627 - '@codemirror/view': 6.38.6
9705 + '@codemirror/view': 6.38.8
9706 codemirror: 6.0.2
9629 - vue: 3.5.22(typescript@5.9.3)
9707 + vue: 3.5.25(typescript@5.9.3)
9708
9709 vue-component-type-helpers@2.2.12: {}
9710
9633 - vue-eslint-parser@10.2.0(eslint@9.38.0(jiti@2.6.1)):
9711 + vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1)):
9712 dependencies:
9713 debug: 4.4.3(supports-color@8.1.1)
9636 - eslint: 9.38.0(jiti@2.6.1)
9714 + eslint: 9.39.1(jiti@2.6.1)
9715 eslint-scope: 8.4.0
9716 eslint-visitor-keys: 4.2.1
9717 espree: 10.4.0
@@ -9642,67 +9720,67 @@ snapshots:
9720 transitivePeerDependencies:
9721 - supports-color
9722
9645 - vue-highlight-words@3.0.1(vue@3.5.22(typescript@5.9.3)):
9723 + vue-highlight-words@3.0.1(vue@3.5.25(typescript@5.9.3)):
9724 dependencies:
9725 highlight-words-core: 1.2.3
9648 - vue: 3.5.22(typescript@5.9.3)
9726 + vue: 3.5.25(typescript@5.9.3)
9727
9650 - vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)):
9728 + vue-i18n@11.2.2(vue@3.5.25(typescript@5.9.3)):
9729 dependencies:
9652 - '@intlify/core-base': 11.1.12
9653 - '@intlify/shared': 11.1.12
9730 + '@intlify/core-base': 11.2.2
9731 + '@intlify/shared': 11.2.2
9732 '@vue/devtools-api': 6.6.4
9655 - vue: 3.5.22(typescript@5.9.3)
9733 + vue: 3.5.25(typescript@5.9.3)
9734
9657 - vue-router@4.6.3(vue@3.5.22(typescript@5.9.3)):
9735 + vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)):
9736 dependencies:
9737 '@vue/devtools-api': 6.6.4
9660 - vue: 3.5.22(typescript@5.9.3)
9738 + vue: 3.5.25(typescript@5.9.3)
9739
9662 - vue-sjv@0.0.6(vue@3.5.22(typescript@5.9.3)):
9740 + vue-sjv@0.0.6(vue@3.5.25(typescript@5.9.3)):
9741 dependencies:
9664 - vue: 3.5.22(typescript@5.9.3)
9742 + vue: 3.5.25(typescript@5.9.3)
9743
9666 - vue-tsc@3.1.2(typescript@5.9.3):
9744 + vue-tsc@3.1.5(typescript@5.9.3):
9745 dependencies:
9746 '@volar/typescript': 2.4.23
9669 - '@vue/language-core': 3.1.2(typescript@5.9.3)
9747 + '@vue/language-core': 3.1.5(typescript@5.9.3)
9748 typescript: 5.9.3
9749
9672 - vue3-apexcharts@1.10.0(apexcharts@5.3.5)(vue@3.5.22(typescript@5.9.3)):
9750 + vue3-apexcharts@1.10.0(apexcharts@5.3.5)(vue@3.5.25(typescript@5.9.3)):
9751 dependencies:
9752 apexcharts: 5.3.5
9675 - vue: 3.5.22(typescript@5.9.3)
9753 + vue: 3.5.25(typescript@5.9.3)
9754
9677 - vue3-marquee@4.2.2(vue@3.5.22(typescript@5.9.3)):
9755 + vue3-marquee@4.2.2(vue@3.5.25(typescript@5.9.3)):
9756 dependencies:
9679 - vue: 3.5.22(typescript@5.9.3)
9757 + vue: 3.5.25(typescript@5.9.3)
9758
9681 - vue@3.5.22(typescript@5.9.3):
9759 + vue@3.5.25(typescript@5.9.3):
9760 dependencies:
9683 - '@vue/compiler-dom': 3.5.22
9684 - '@vue/compiler-sfc': 3.5.22
9685 - '@vue/runtime-dom': 3.5.22
9686 - '@vue/server-renderer': 3.5.22(vue@3.5.22(typescript@5.9.3))
9687 - '@vue/shared': 3.5.22
9761 + '@vue/compiler-dom': 3.5.25
9762 + '@vue/compiler-sfc': 3.5.25
9763 + '@vue/runtime-dom': 3.5.25
9764 + '@vue/server-renderer': 3.5.25(vue@3.5.25(typescript@5.9.3))
9765 + '@vue/shared': 3.5.25
9766 optionalDependencies:
9767 typescript: 5.9.3
9768
9691 - vuedraggable@4.1.0(vue@3.5.22(typescript@5.9.3)):
9769 + vuedraggable@4.1.0(vue@3.5.25(typescript@5.9.3)):
9770 dependencies:
9771 sortablejs: 1.14.0
9694 - vue: 3.5.22(typescript@5.9.3)
9772 + vue: 3.5.25(typescript@5.9.3)
9773
9696 - vueuc@0.4.65(vue@3.5.22(typescript@5.9.3)):
9774 + vueuc@0.4.65(vue@3.5.25(typescript@5.9.3)):
9775 dependencies:
9698 - '@css-render/vue3-ssr': 0.15.14(vue@3.5.22(typescript@5.9.3))
9776 + '@css-render/vue3-ssr': 0.15.14(vue@3.5.25(typescript@5.9.3))
9777 '@juggle/resize-observer': 3.4.0
9778 css-render: 0.15.14
9779 evtd: 0.2.4
9780 seemly: 0.3.10
9703 - vdirs: 0.1.8(vue@3.5.22(typescript@5.9.3))
9704 - vooks: 0.2.12(vue@3.5.22(typescript@5.9.3))
9705 - vue: 3.5.22(typescript@5.9.3)
9781 + vdirs: 0.1.8(vue@3.5.25(typescript@5.9.3))
9782 + vooks: 0.2.12(vue@3.5.25(typescript@5.9.3))
9783 + vue: 3.5.25(typescript@5.9.3)
9784
9785 w3c-keyname@2.2.8: {}
9786
@@ -9710,9 +9788,9 @@ snapshots:
9788 dependencies:
9789 xml-name-validator: 5.0.0
9790
9713 - wait-on@8.0.5(debug@4.4.3):
9791 + wait-on@9.0.3(debug@4.4.3):
9792 dependencies:
9715 - axios: 1.13.1(debug@4.4.3)
9793 + axios: 1.13.2(debug@4.4.3)
9794 joi: 18.0.1
9795 lodash: 4.17.21
9796 minimist: 1.2.8
@@ -9787,9 +9865,9 @@ snapshots:
9865
9866 xmlchars@2.2.0: {}
9867
9790 - xmllint-wasm@5.1.0(@types/node@24.9.2):
9868 + xmllint-wasm@5.1.0(@types/node@24.10.1):
9869 dependencies:
9792 - '@types/node': 24.9.2
9870 + '@types/node': 24.10.1
9871
9872 xmllint@0.1.1: {}
9873
frontend/src/api/endpoints/agents.ts
+51
@@ -1,5 +1,6 @@
1 import type {
2 Agent,
3 + AgentArtifactData,
4 AgentSca,
5 AgentVulnerabilities,
6 OutdatedVelociraptorAgents,
@@ -80,6 +81,56 @@ export default {
81 upgradeWazuhAgent(agentId: string) {
82 return HttpClient.post<FlaskBaseResponse>(`/agents/${agentId}/wazuh/upgrade`)
83 },
84 + // Agent Data Store / Artifact Collection Methods
85 + /**
86 + * List all artifact files for a specific agent
87 + * @param agentId - The agent ID
88 + * @param flowId - Optional flow ID to filter artifacts
89 + * @param signal - Optional AbortSignal for request cancellation
90 + */
91 + listAgentArtifacts(agentId: string, flowId?: string, signal?: AbortSignal) {
92 + const params = flowId ? { flow_id: flowId } : {}
93 + return HttpClient.get<
94 + FlaskBaseResponse & {
95 + data: AgentArtifactData[]
96 + total: number
97 + }
98 + >(`/agent_data_store/agent/${agentId}/artifacts`, signal ? { signal, params } : { params })
99 + },
100 +
101 + /**
102 + * Get details of a specific artifact file
103 + * @param agentId - The agent ID
104 + * @param artifactId - The artifact ID
105 + * @param signal - Optional AbortSignal for request cancellation
106 + */
107 + getAgentArtifactDetails(agentId: string, artifactId: number, signal?: AbortSignal) {
108 + return HttpClient.get<FlaskBaseResponse & { data: AgentArtifactData | null }>(
109 + `/agent_data_store/agent/${agentId}/artifacts/${artifactId}`,
110 + signal ? { signal } : {}
111 + )
112 + },
113 +
114 + /**
115 + * Download a specific artifact file
116 + * @param agentId - The agent ID
117 + * @param artifactId - The artifact ID
118 + * @returns Promise with Blob response
119 + */
120 + downloadAgentArtifact(agentId: string, artifactId: number) {
121 + return HttpClient.get<Blob>(`/agent_data_store/agent/${agentId}/artifacts/${artifactId}/download`, {
122 + responseType: "blob"
123 + })
124 + },
125 +
126 + /**
127 + * Delete a specific artifact file
128 + * @param agentId - The agent ID
129 + * @param artifactId - The artifact ID
130 + */
131 + deleteAgentArtifact(agentId: string, artifactId: number) {
132 + return HttpClient.delete<FlaskBaseResponse>(`/agent_data_store/agent/${agentId}/artifacts/${artifactId}`)
133 + },
134
135 // IGNORE AT THE MOMENT !
136 /** @deprecated */
frontend/src/api/endpoints/artifacts.ts
+9
@@ -2,6 +2,7 @@ import type {
2 Artifact,
3 CollectResult,
4 CommandResult,
5 + FileCollection,
6 MatchingParameter,
7 QuarantineResult,
8 Recommendation
@@ -46,6 +47,11 @@ export interface ArtifactRecommendationRequest {
47 prompt: string | object
48 }
49
50 +export interface FileCollectionByAgentRequest {
51 + file: string
52 + root_disk: string
53 +}
54 +
55 export default {
56 getAll(filters?: ArtifactsQuery) {
57 let url = "/artifacts"
@@ -83,5 +89,8 @@ export default {
89 total_matches: number
90 }
91 >(`/artifacts/artifact/${artifactName}/parameters/${parameterPrefix}`)
92 + },
93 + collectFileByAgentId(agentId: string, payload: FileCollectionByAgentRequest) {
94 + return HttpClient.post<FlaskBaseResponse & FileCollection>(`/artifacts/collect/file/agent/${agentId}`, payload)
95 }
96 }
frontend/src/api/endpoints/customerPortal.ts new
+21
@@ -0,0 +1,21 @@
1 +import type { CustomerPortalSettings } from "@/types/customerPortal.d"
2 +import type { FlaskBaseResponse } from "@/types/flask.d"
3 +import { HttpClient } from "../httpClient"
4 +
5 +export interface CustomerPortalSettingsPayload {
6 + title: string | null
7 + logo_base64: string | null
8 + logo_mime_type: string | null
9 +}
10 +
11 +export default {
12 + getSettings() {
13 + return HttpClient.get<FlaskBaseResponse & { settings: CustomerPortalSettings }>(`/customer_portal/settings`)
14 + },
15 + setSettings(payload: CustomerPortalSettingsPayload) {
16 + return HttpClient.post<FlaskBaseResponse & { settings: CustomerPortalSettings }>(
17 + `/customer_portal/settings`,
18 + payload
19 + )
20 + }
21 +}
frontend/src/api/index.ts
+3 -1
@@ -8,6 +8,7 @@ import cloudSecurityAssessment from "./endpoints/cloudSecurityAssessment"
8 import connectors from "./endpoints/connectors"
9 import copilotAction from "./endpoints/copilotAction"
10 import copilotMCP from "./endpoints/copilotMCP"
11 +import customerPortal from "./endpoints/customerPortal"
12 import customers from "./endpoints/customers"
13 import flow from "./endpoints/flow"
14 import graylog from "./endpoints/graylog"
@@ -69,5 +70,6 @@ export default {
70 wazuh,
71 portainer,
72 shuffle,
72 - copilotMCP
73 + copilotMCP,
74 + customerPortal
75 }
frontend/src/app-layouts/common/Navbar/items.tsx
+15
@@ -18,6 +18,7 @@ const CustomersIcon = "carbon:user-multiple"
18 const ExternalServicesIcon = "carbon:ibm-cloud-direct-link-2-dedicated"
19 const ReportCreationIcon = "carbon:report-data"
20 const SchedulerIcon = "material-symbols:autoplay"
21 +const CustomerPortalIcon = "streamline-ultimate:coding-apps-website-apps-browser"
22
23 export default function getItems(): MenuMixedOption[] {
24 return [
@@ -490,6 +491,20 @@ export default function getItems(): MenuMixedOption[] {
491 ),
492 key: "Scheduler",
493 icon: renderIcon(SchedulerIcon)
494 + },
495 + {
496 + label: () =>
497 + h(
498 + RouterLink,
499 + {
500 + to: {
501 + name: "CustomerPortal"
502 + }
503 + },
504 + { default: () => "Customer Portal" }
505 + ),
506 + key: "CustomerPortal",
507 + icon: renderIcon(CustomerPortalIcon)
508 }
509 ]
510 }
frontend/src/components/activeResponse/ActiveResponseActions.vue
+1 -1
@@ -2,7 +2,7 @@
2 <div class="active-response-actions flex justify-end gap-2">
3 <n-button type="success" secondary :size :loading="loadingInvoke" @click="showInvokeForm = true">
4 <template #icon>
5 - <Icon :name="InvokeIcon"></Icon>
5 + <Icon :name="InvokeIcon" />
6 </template>
7 Invoke Action
8 </n-button>
frontend/src/components/activeResponse/ActiveResponseItem.vue
+1 -1
@@ -10,7 +10,7 @@
10 <template #headerExtra>
11 <n-button size="small" @click.stop="showDetails = true">
12 <template #icon>
13 - <Icon :name="InfoIcon"></Icon>
13 + <Icon :name="InfoIcon" />
14 </template>
15 </n-button>
16 </template>
frontend/src/components/activeResponse/ActiveResponseWizard.vue
+5 -5
@@ -17,19 +17,19 @@
17 <div v-if="current === 1" class="flex flex-col gap-2 px-7">
18 <CardEntity embedded hoverable clickable @click.stop="setOs('linux')">
19 <div class="flex items-center gap-3">
20 - <Icon :size="18" :name="iconFromOs('linux')"></Icon>
20 + <Icon :size="18" :name="iconFromOs('linux')" />
21 <span>LINUX</span>
22 </div>
23 </CardEntity>
24 <CardEntity embedded hoverable clickable @click.stop="setOs('windows')">
25 <div class="flex items-center gap-3">
26 - <Icon :size="18" :name="iconFromOs('windows')"></Icon>
26 + <Icon :size="18" :name="iconFromOs('windows')" />
27 <span>WINDOWS</span>
28 </div>
29 </CardEntity>
30 <CardEntity embedded hoverable clickable @click.stop="setOs('macos')">
31 <div class="flex items-center gap-3">
32 - <Icon :size="18" :name="iconFromOs('macos')"></Icon>
32 + <Icon :size="18" :name="iconFromOs('macos')" />
33 <span>MACOS</span>
34 </div>
35 </CardEntity>
@@ -71,7 +71,7 @@
71 <template #additionalActions>
72 <n-button :disabled="loadingActiveResponseInvoke" @click.stop="prev()">
73 <template #icon>
74 - <Icon :name="ArrowLeftIcon"></Icon>
74 + <Icon :name="ArrowLeftIcon" />
75 </template>
76 Prev
77 </n-button>
@@ -86,7 +86,7 @@
86 <div class="flex gap-4">
87 <n-button v-if="isPrevStepEnabled" @click.stop="prev()">
88 <template #icon>
89 - <Icon :name="ArrowLeftIcon"></Icon>
89 + <Icon :name="ArrowLeftIcon" />
90 </template>
91 Prev
92 </n-button>
frontend/src/components/activeResponse/ActiveResponseWizardButton.vue
+1 -1
@@ -1,7 +1,7 @@
1 <template>
2 <n-button :size="size" :type="type" :loading="loading" @click="showInvokeWizard = true">
3 <template #icon>
4 - <Icon :name="InvokeIcon"></Icon>
4 + <Icon :name="InvokeIcon" />
5 </template>
6 Active Response
7 </n-button>
frontend/src/components/agents/AgentCard.vue
+3 -3
@@ -22,7 +22,7 @@
22 @click.stop="toggleCritical(agent.agent_id, agent.critical_asset)"
23 >
24 <template #icon>
25 - <Icon :name="StarIcon"></Icon>
25 + <Icon :name="StarIcon" />
26 </template>
27 </n-button>
28 </template>
@@ -32,7 +32,7 @@
32 <n-tooltip>
33 Quarantined
34 <template #trigger>
35 - <Icon :name="QuarantinedIcon" :size="18"></Icon>
35 + <Icon :name="QuarantinedIcon" :size="18" />
36 </template>
37 </n-tooltip>
38 </div>
@@ -55,7 +55,7 @@
55 <template #trigger>
56 <n-button quaternary circle type="error" @click.stop="handleDelete">
57 <template #icon>
58 - <Icon :name="DeleteIcon"></Icon>
58 + <Icon :name="DeleteIcon" />
59 </template>
60 </n-button>
61 </template>
frontend/src/components/agents/agentFlow/AgentFlowItem.vue
+3 -3
@@ -13,7 +13,7 @@
13 <span>
14 {{ formatDate(flow.start_time, dFormats.datetimesec) }}
15 </span>
16 - <Icon :name="TimeIcon" :size="16"></Icon>
16 + <Icon :name="TimeIcon" :size="16" />
17 </div>
18 </template>
19 <div class="flex flex-col px-1 py-2">
@@ -57,13 +57,13 @@
57 </Badge>
58 <Badge :type="flow.dirty ? 'active' : 'muted'">
59 <template #iconRight>
60 - <Icon :name="flow.dirty ? EnabledIcon : DisabledIcon" :size="14"></Icon>
60 + <Icon :name="flow.dirty ? EnabledIcon : DisabledIcon" :size="14" />
61 </template>
62 <template #label>Dirty</template>
63 </Badge>
64 <Badge :type="flow.user_notified ? 'active' : 'muted'">
65 <template #iconRight>
66 - <Icon :name="flow.user_notified ? EnabledIcon : DisabledIcon" :size="14"></Icon>
66 + <Icon :name="flow.user_notified ? EnabledIcon : DisabledIcon" :size="14" />
67 </template>
68 <template #label>User notified</template>
69 </Badge>
frontend/src/components/agents/dataStore/AgentDataStoreTab.vue new
+253
@@ -0,0 +1,253 @@
1 +<template>
2 + <div class="agent-data-store-tab">
3 + <div class="flex gap-4">
4 + <div class="filters-section" style="min-width: 280px; max-width: 320px">
5 + <n-card size="small" title="Filters" :segmented="{ content: true }">
6 + <div class="flex flex-col gap-3">
7 + <n-input v-model:value="textFilter" placeholder="Search artifacts..." clearable size="small">
8 + <template #prefix>
9 + <Icon :name="SearchIcon" :size="16" />
10 + </template>
11 + </n-input>
12 +
13 + <n-select
14 + v-model:value="statusFilter"
15 + :options="statusOptions"
16 + placeholder="Filter by status"
17 + size="small"
18 + clearable
19 + />
20 +
21 + <n-button type="primary" secondary size="small" :loading="loading" @click="getArtifacts()">
22 + <template #icon>
23 + <Icon :name="RefreshIcon" />
24 + </template>
25 + Refresh
26 + </n-button>
27 +
28 + <n-divider class="my-2!" />
29 +
30 + <div class="flex flex-col gap-2 text-sm">
31 + <div class="flex items-center justify-between">
32 + <span class="text-secondary-color">Total:</span>
33 + <span class="font-mono">{{ artifacts.length }}</span>
34 + </div>
35 + <div class="flex items-center justify-between">
36 + <span class="text-secondary-color">Filtered:</span>
37 + <span class="font-mono">{{ artifactsFiltered.length }}</span>
38 + </div>
39 + </div>
40 + </div>
41 + </n-card>
42 + </div>
43 +
44 + <div class="artifacts-section flex-1">
45 + <n-spin :show="loading">
46 + <n-scrollbar style="max-height: 600px">
47 + <div class="flex flex-col gap-3 pr-2">
48 + <template v-if="artifactsFiltered.length">
49 + <ArtifactCard
50 + v-for="artifact in itemsPaginated"
51 + :key="artifact.id"
52 + :artifact
53 + show-actions
54 + hoverable
55 + @download="downloadArtifact(artifact)"
56 + @delete="deleteArtifact(artifact)"
57 + @details="showArtifactDetails(artifact)"
58 + />
59 + </template>
60 + <template v-else>
61 + <n-empty v-if="!loading" description="No artifacts found" class="h-48 justify-center" />
62 + </template>
63 + </div>
64 + </n-scrollbar>
65 +
66 + <div v-if="artifactsFiltered.length > pageSize" class="mt-4 flex justify-end">
67 + <n-pagination
68 + v-model:page="page"
69 + :page-size="pageSize"
70 + :page-slot="5"
71 + :item-count="artifactsFiltered.length"
72 + size="small"
73 + />
74 + </div>
75 + </n-spin>
76 + </div>
77 + </div>
78 +
79 + <!-- Artifact Details Modal -->
80 + <n-modal
81 + v-model:show="showDetailsModal"
82 + preset="card"
83 + title="Artifact Details"
84 + :style="{ width: '800px' }"
85 + :segmented="{ content: true }"
86 + >
87 + <ArtifactDetails v-if="selectedArtifact" :artifact="selectedArtifact" />
88 + </n-modal>
89 + </div>
90 +</template>
91 +
92 +<script setup lang="ts">
93 +import type { SelectOption } from "naive-ui"
94 +import type { Agent, AgentArtifactData } from "@/types/agents.d"
95 +import { refDebounced } from "@vueuse/core"
96 +import { saveAs } from "file-saver"
97 +import {
98 + NButton,
99 + NCard,
100 + NDivider,
101 + NEmpty,
102 + NInput,
103 + NModal,
104 + NPagination,
105 + NScrollbar,
106 + NSelect,
107 + NSpin,
108 + useDialog,
109 + useMessage
110 +} from "naive-ui"
111 +import { computed, onMounted, ref } from "vue"
112 +import Api from "@/api"
113 +import Icon from "@/components/common/Icon.vue"
114 +import ArtifactCard from "./ArtifactCard.vue"
115 +import ArtifactDetails from "./ArtifactDetails.vue"
116 +
117 +// TODO: join AgentDataStoreTab + AgentDataStoreTabCompact
118 +
119 +const props = defineProps<{
120 + agent: Agent
121 +}>()
122 +
123 +const message = useMessage()
124 +const dialog = useDialog()
125 +
126 +const SearchIcon = "carbon:search"
127 +const RefreshIcon = "carbon:renew"
128 +
129 +const loading = ref(false)
130 +const artifacts = ref<AgentArtifactData[]>([])
131 +const textFilter = ref<string | null>(null)
132 +const textFilterDebounced = refDebounced<string | null>(textFilter, 300)
133 +const statusFilter = ref<string | undefined>(undefined)
134 +const page = ref(1)
135 +const pageSize = ref(20)
136 +const showDetailsModal = ref(false)
137 +const selectedArtifact = ref<AgentArtifactData | null>(null)
138 +
139 +const statusOptions: SelectOption[] = [
140 + { label: "All", value: undefined },
141 + { label: "Completed", value: "completed" },
142 + { label: "Failed", value: "failed" },
143 + { label: "Processing", value: "processing" }
144 +]
145 +
146 +const artifactsFiltered = computed(() => {
147 + return artifacts.value.filter(artifact => {
148 + // Text filter
149 + const matchesText = (artifact.artifact_name + artifact.flow_id + artifact.file_name)
150 + .toString()
151 + .toLowerCase()
152 + .includes((textFilterDebounced.value || "").toString().toLowerCase())
153 +
154 + // Status filter
155 + const matchesStatus = !statusFilter.value || artifact.status === statusFilter.value
156 +
157 + return matchesText && matchesStatus
158 + })
159 +})
160 +
161 +const itemsPaginated = computed(() => {
162 + const from = (page.value - 1) * pageSize.value
163 + const to = page.value * pageSize.value
164 +
165 + return artifactsFiltered.value.slice(from, to)
166 +})
167 +
168 +function getArtifacts() {
169 + loading.value = true
170 +
171 + Api.agents
172 + .listAgentArtifacts(props.agent.agent_id)
173 + .then(res => {
174 + if (res.data.success) {
175 + artifacts.value = res.data.data || []
176 + } else {
177 + message.error(res.data?.message || "An error occurred. Please try again later.")
178 + }
179 + })
180 + .catch(err => {
181 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
182 + })
183 + .finally(() => {
184 + loading.value = false
185 + })
186 +}
187 +
188 +function downloadArtifact(artifact: AgentArtifactData) {
189 + message.loading(`Downloading ${artifact.file_name}...`)
190 +
191 + Api.agents
192 + .downloadAgentArtifact(props.agent.agent_id, artifact.id)
193 + .then(res => {
194 + saveAs(res.data, artifact.file_name)
195 +
196 + message.success(`Downloaded ${artifact.file_name}`)
197 + })
198 + .catch(err => {
199 + message.error(err.response?.data?.message || "Failed to download artifact")
200 + })
201 +}
202 +
203 +function deleteArtifact(artifact: AgentArtifactData) {
204 + dialog.warning({
205 + title: "Delete Artifact",
206 + content: `Are you sure you want to delete "${artifact.file_name}"? This action cannot be undone.`,
207 + positiveText: "Delete",
208 + negativeText: "Cancel",
209 + onPositiveClick: () => {
210 + Api.agents
211 + .deleteAgentArtifact(props.agent.agent_id, artifact.id)
212 + .then(res => {
213 + if (res.data.success) {
214 + message.success("Artifact deleted successfully")
215 + getArtifacts()
216 + } else {
217 + message.error(res.data?.message || "Failed to delete artifact")
218 + }
219 + })
220 + .catch(err => {
221 + message.error(err.response?.data?.message || "Failed to delete artifact")
222 + })
223 + }
224 + })
225 +}
226 +
227 +function showArtifactDetails(artifact: AgentArtifactData) {
228 + selectedArtifact.value = artifact
229 + showDetailsModal.value = true
230 +}
231 +
232 +onMounted(() => {
233 + getArtifacts()
234 +})
235 +</script>
236 +
237 +<style lang="scss" scoped>
238 +// TODO: remove style
239 +
240 +.agent-data-store-tab {
241 + .filters-section {
242 + :deep() {
243 + .n-card__content {
244 + padding: 16px;
245 + }
246 + }
247 + }
248 +
249 + .artifacts-section {
250 + min-height: 300px;
251 + }
252 +}
253 +</style>
frontend/src/components/agents/dataStore/AgentDataStoreTabCompact.vue new
+250
@@ -0,0 +1,250 @@
1 +<template>
2 + <div class="agent-data-store-tab-compact">
3 + <div class="flex flex-col gap-3">
4 + <div class="filters-bar flex flex-wrap items-center gap-2">
5 + <n-input
6 + v-model:value="textFilter"
7 + placeholder="Search artifacts..."
8 + clearable
9 + size="small"
10 + style="max-width: 250px"
11 + >
12 + <template #prefix>
13 + <Icon :name="SearchIcon" :size="14" />
14 + </template>
15 + </n-input>
16 +
17 + <n-select
18 + v-model:value="statusFilter"
19 + :options="statusOptions"
20 + placeholder="Status"
21 + size="small"
22 + clearable
23 + style="max-width: 150px"
24 + />
25 +
26 + <n-button type="primary" secondary size="small" :loading="loading" @click="getArtifacts()">
27 + <template #icon>
28 + <Icon :name="RefreshIcon" />
29 + </template>
30 + </n-button>
31 +
32 + <div class="flex-1"></div>
33 +
34 + <div class="text-secondary-color flex items-center gap-3 text-xs">
35 + <span>
36 + Total:
37 + <strong class="font-mono">{{ artifacts.length }}</strong>
38 + </span>
39 + <span>
40 + Filtered:
41 + <strong class="font-mono">{{ artifactsFiltered.length }}</strong>
42 + </span>
43 + </div>
44 + </div>
45 +
46 + <n-spin :show="loading">
47 + <div class="artifacts-list">
48 + <n-scrollbar style="max-height: 400px">
49 + <div class="flex flex-col gap-2 pr-2">
50 + <template v-if="artifactsFiltered.length">
51 + <ArtifactCardCompact
52 + v-for="artifact in itemsPaginated"
53 + :key="artifact.id"
54 + :artifact
55 + show-actions
56 + @download="downloadArtifact(artifact)"
57 + @delete="deleteArtifact(artifact)"
58 + @details="showArtifactDetails(artifact)"
59 + />
60 + </template>
61 + <template v-else>
62 + <n-empty
63 + v-if="!loading"
64 + description="No artifacts found"
65 + class="h-32 justify-center"
66 + size="small"
67 + />
68 + </template>
69 + </div>
70 + </n-scrollbar>
71 +
72 + <div v-if="artifactsFiltered.length > pageSize" class="mt-3 flex justify-end">
73 + <n-pagination
74 + v-model:page="page"
75 + :page-size="pageSize"
76 + :page-slot="5"
77 + :item-count="artifactsFiltered.length"
78 + size="small"
79 + />
80 + </div>
81 + </div>
82 + </n-spin>
83 + </div>
84 +
85 + <!-- Artifact Details Modal -->
86 + <n-modal
87 + v-model:show="showDetailsModal"
88 + preset="card"
89 + title="Artifact Details"
90 + :style="{ width: '700px' }"
91 + :segmented="{ content: true }"
92 + >
93 + <ArtifactDetails v-if="selectedArtifact" :artifact="selectedArtifact" />
94 + </n-modal>
95 + </div>
96 +</template>
97 +
98 +<script setup lang="ts">
99 +import type { SelectOption } from "naive-ui"
100 +import type { AgentArtifactData } from "@/types/agents.d"
101 +import { refDebounced } from "@vueuse/core"
102 +import { saveAs } from "file-saver"
103 +import {
104 + NButton,
105 + NEmpty,
106 + NInput,
107 + NModal,
108 + NPagination,
109 + NScrollbar,
110 + NSelect,
111 + NSpin,
112 + useDialog,
113 + useMessage
114 +} from "naive-ui"
115 +import { computed, onMounted, ref } from "vue"
116 +import Api from "@/api"
117 +import Icon from "@/components/common/Icon.vue"
118 +import ArtifactCardCompact from "./ArtifactCardCompact.vue"
119 +import ArtifactDetails from "./ArtifactDetails.vue"
120 +
121 +const props = defineProps<{
122 + agentId: string
123 +}>()
124 +
125 +const message = useMessage()
126 +const dialog = useDialog()
127 +
128 +const SearchIcon = "carbon:search"
129 +const RefreshIcon = "carbon:renew"
130 +
131 +const loading = ref(false)
132 +const artifacts = ref<AgentArtifactData[]>([])
133 +const textFilter = ref<string | null>(null)
134 +const textFilterDebounced = refDebounced<string | null>(textFilter, 300)
135 +const statusFilter = ref<string | null>(null)
136 +const page = ref(1)
137 +const pageSize = ref(10)
138 +const showDetailsModal = ref(false)
139 +const selectedArtifact = ref<AgentArtifactData | null>(null)
140 +
141 +const statusOptions: SelectOption[] = [
142 + { label: "All", value: undefined },
143 + { label: "Completed", value: "completed" },
144 + { label: "Failed", value: "failed" },
145 + { label: "Processing", value: "processing" }
146 +]
147 +
148 +const artifactsFiltered = computed(() => {
149 + return artifacts.value.filter(artifact => {
150 + const matchesText = (artifact.artifact_name + artifact.flow_id + artifact.file_name)
151 + .toString()
152 + .toLowerCase()
153 + .includes((textFilterDebounced.value || "").toString().toLowerCase())
154 +
155 + const matchesStatus = !statusFilter.value || artifact.status === statusFilter.value
156 +
157 + return matchesText && matchesStatus
158 + })
159 +})
160 +
161 +const itemsPaginated = computed(() => {
162 + const from = (page.value - 1) * pageSize.value
163 + const to = page.value * pageSize.value
164 +
165 + return artifactsFiltered.value.slice(from, to)
166 +})
167 +
168 +function getArtifacts() {
169 + loading.value = true
170 +
171 + Api.agents
172 + .listAgentArtifacts(props.agentId)
173 + .then(res => {
174 + if (res.data.success) {
175 + artifacts.value = res.data.data || []
176 + } else {
177 + message.error(res.data?.message || "An error occurred. Please try again later.")
178 + }
179 + })
180 + .catch(err => {
181 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
182 + })
183 + .finally(() => {
184 + loading.value = false
185 + })
186 +}
187 +
188 +function downloadArtifact(artifact: AgentArtifactData) {
189 + message.loading(`Downloading ${artifact.file_name}...`)
190 +
191 + Api.agents
192 + .downloadAgentArtifact(props.agentId, artifact.id)
193 + .then(res => {
194 + saveAs(res.data, artifact.file_name)
195 +
196 + message.success(`Downloaded ${artifact.file_name}`)
197 + })
198 + .catch(err => {
199 + message.error(err.response?.data?.message || "Failed to download artifact")
200 + })
201 +}
202 +
203 +function deleteArtifact(artifact: AgentArtifactData) {
204 + dialog.warning({
205 + title: "Delete Artifact",
206 + content: `Are you sure you want to delete "${artifact.file_name}"? This action cannot be undone.`,
207 + positiveText: "Delete",
208 + negativeText: "Cancel",
209 + onPositiveClick: () => {
210 + Api.agents
211 + .deleteAgentArtifact(props.agentId, artifact.id)
212 + .then(res => {
213 + if (res.data.success) {
214 + message.success("Artifact deleted successfully")
215 + getArtifacts()
216 + } else {
217 + message.error(res.data?.message || "Failed to delete artifact")
218 + }
219 + })
220 + .catch(err => {
221 + message.error(err.response?.data?.message || "Failed to delete artifact")
222 + })
223 + }
224 + })
225 +}
226 +
227 +function showArtifactDetails(artifact: AgentArtifactData) {
228 + selectedArtifact.value = artifact
229 + showDetailsModal.value = true
230 +}
231 +
232 +onMounted(() => {
233 + getArtifacts()
234 +})
235 +</script>
236 +
237 +<style lang="scss" scoped>
238 +// TODO: remove style
239 +
240 +.agent-data-store-tab-compact {
241 + .filters-bar {
242 + padding-bottom: 8px;
243 + border-bottom: 1px solid var(--border-color);
244 + }
245 +
246 + .artifacts-list {
247 + min-height: 200px;
248 + }
249 +}
250 +</style>
frontend/src/components/agents/dataStore/ArtifactCard.vue new
+138
@@ -0,0 +1,138 @@
1 +<template>
2 + <n-card
3 + class="artifact-card"
4 + :class="{ 'cursor-pointer': hoverable, clickable }"
5 + content-style="padding: 16px"
6 + @click="emit('click', artifact)"
7 + >
8 + <div class="flex flex-col gap-3">
9 + <div class="flex items-start justify-between gap-4">
10 + <div class="flex grow flex-col gap-1">
11 + <div class="flex items-center gap-2">
12 + <Icon :name="FileIcon" :size="18" class="text-primary-color" />
13 + <span class="text-primary-color font-bold">{{ artifact.artifact_name }}</span>
14 + </div>
15 + <div class="text-secondary-color flex items-center gap-2 text-sm">
16 + <span class="font-mono">{{ artifact.file_name }}</span>
17 + </div>
18 + </div>
19 + <n-badge :value="artifact.status" :type="statusType" />
20 + </div>
21 +
22 + <n-divider class="my-1!" />
23 +
24 + <div class="flex flex-col gap-2 text-sm">
25 + <div class="flex items-center justify-between">
26 + <span class="text-secondary-color">Flow ID:</span>
27 + <code class="font-mono text-xs">{{ artifact.flow_id }}</code>
28 + </div>
29 + <div class="flex items-center justify-between">
30 + <span class="text-secondary-color">File Size:</span>
31 + <span>{{ fileSize }}</span>
32 + </div>
33 + <div class="flex items-center justify-between">
34 + <span class="text-secondary-color">Collected:</span>
35 + <span>{{ formatDate(artifact.collection_time, dFormats.datetime) }}</span>
36 + </div>
37 + <div v-if="artifact.customer_code" class="flex items-center justify-between">
38 + <span class="text-secondary-color">Customer:</span>
39 + <span>{{ artifact.customer_code }}</span>
40 + </div>
41 + </div>
42 +
43 + <div v-if="showActions" class="flex gap-2">
44 + <n-button size="small" secondary type="info" @click.stop="emit('details', artifact)">
45 + <template #icon>
46 + <Icon :name="InfoIcon" />
47 + </template>
48 + Details
49 + </n-button>
50 + <n-button size="small" secondary type="primary" @click.stop="emit('download', artifact)">
51 + <template #icon>
52 + <Icon :name="DownloadIcon" />
53 + </template>
54 + Download
55 + </n-button>
56 + <n-button size="small" secondary type="error" @click.stop="emit('delete', artifact)">
57 + <template #icon>
58 + <Icon :name="DeleteIcon" />
59 + </template>
60 + Delete
61 + </n-button>
62 + </div>
63 + </div>
64 + </n-card>
65 +</template>
66 +
67 +<script setup lang="ts">
68 +import type { BadgeProps } from "naive-ui"
69 +import type { AgentArtifactData } from "@/types/agents.d"
70 +import bytes from "bytes"
71 +import { NBadge, NButton, NCard, NDivider } from "naive-ui"
72 +import { computed } from "vue"
73 +import Icon from "@/components/common/Icon.vue"
74 +import { useSettingsStore } from "@/stores/settings"
75 +import { formatDate } from "@/utils"
76 +
77 +// TODO: join ArtifactCard + ArtifactCardCompact
78 +
79 +const {
80 + artifact,
81 + showActions = false,
82 + hoverable = false,
83 + clickable = false
84 +} = defineProps<{
85 + artifact: AgentArtifactData
86 + showActions?: boolean
87 + hoverable?: boolean
88 + clickable?: boolean
89 +}>()
90 +
91 +const emit = defineEmits<{
92 + (e: "click", artifact: AgentArtifactData): void
93 + (e: "download", artifact: AgentArtifactData): void
94 + (e: "delete", artifact: AgentArtifactData): void
95 + (e: "details", artifact: AgentArtifactData): void
96 +}>()
97 +
98 +const dFormats = useSettingsStore().dateFormat
99 +
100 +const FileIcon = "lsicon:file-zip-outline"
101 +const DownloadIcon = "carbon:download"
102 +const DeleteIcon = "carbon:trash-can"
103 +const InfoIcon = "carbon:information"
104 +
105 +const STATUS_TYPE_MAP: Record<string, BadgeProps["type"]> = {
106 + completed: "success",
107 + failed: "error",
108 + processing: "warning",
109 + pending: "info"
110 +} as const
111 +
112 +const fileSize = computed(() => bytes(artifact.file_size))
113 +
114 +const statusType = computed<BadgeProps["type"]>(() => {
115 + const normalizedStatus = artifact.status.toLowerCase()
116 + return STATUS_TYPE_MAP[normalizedStatus] ?? "default"
117 +})
118 +</script>
119 +
120 +<style lang="scss" scoped>
121 +// TODO: remove style
122 +
123 +.artifact-card {
124 + border-radius: var(--border-radius);
125 + overflow: hidden;
126 + transition: all 0.2s var(--bezier-ease);
127 +
128 + &.hoverable {
129 + &:hover {
130 + box-shadow: 0 0 0 1px var(--primary-color);
131 + }
132 + }
133 +
134 + &.clickable {
135 + cursor: pointer;
136 + }
137 +}
138 +</style>
frontend/src/components/agents/dataStore/ArtifactCardCompact.vue new
+95
@@ -0,0 +1,95 @@
1 +<template>
2 + <n-card size="small" :bordered="true" hoverable class="artifact-card-compact">
3 + <div class="flex items-start justify-between gap-3">
4 + <div class="min-w-0 flex-1">
5 + <div class="mb-1 flex items-center gap-2">
6 + <Icon :name="FileIcon" :size="16" class="text-primary-color shrink-0" />
7 + <span class="truncate text-sm font-semibold">{{ artifact.artifact_name }}</span>
8 + <n-tag :type="statusType" size="small" round>
9 + {{ artifact.status }}
10 + </n-tag>
11 + </div>
12 + <div class="text-secondary-color mb-2 text-xs">
13 + <code class="text-xs">{{ artifact.file_name }}</code>
14 + </div>
15 + <div class="text-secondary-color flex flex-wrap gap-x-3 gap-y-1 text-xs">
16 + <span>Size: {{ fileSize }}</span>
17 + <span>{{ formatDate(artifact.collection_time, dFormats.datetime) }}</span>
18 + </div>
19 + </div>
20 +
21 + <div v-if="showActions" class="flex shrink-0 flex-col gap-1">
22 + <n-button size="tiny" secondary type="info" @click.stop="emit('details', artifact)">
23 + <template #icon>
24 + <Icon :name="InfoIcon" :size="14" />
25 + </template>
26 + </n-button>
27 + <n-button size="tiny" secondary type="primary" @click.stop="emit('download', artifact)">
28 + <template #icon>
29 + <Icon :name="DownloadIcon" :size="14" />
30 + </template>
31 + </n-button>
32 + <n-button size="tiny" secondary type="error" @click.stop="emit('delete', artifact)">
33 + <template #icon>
34 + <Icon :name="DeleteIcon" :size="14" />
35 + </template>
36 + </n-button>
37 + </div>
38 + </div>
39 + </n-card>
40 +</template>
41 +
42 +<script setup lang="ts">
43 +import type { TagProps } from "naive-ui"
44 +import type { AgentArtifactData } from "@/types/agents.d"
45 +import bytes from "bytes"
46 +import { NButton, NCard, NTag } from "naive-ui"
47 +import { computed } from "vue"
48 +import Icon from "@/components/common/Icon.vue"
49 +import { useSettingsStore } from "@/stores/settings"
50 +import { formatDate } from "@/utils"
51 +
52 +const { artifact, showActions = false } = defineProps<{
53 + artifact: AgentArtifactData
54 + showActions?: boolean
55 +}>()
56 +
57 +const emit = defineEmits<{
58 + (e: "download", artifact: AgentArtifactData): void
59 + (e: "delete", artifact: AgentArtifactData): void
60 + (e: "details", artifact: AgentArtifactData): void
61 +}>()
62 +
63 +const dFormats = useSettingsStore().dateFormat
64 +
65 +const FileIcon = "lsicon:file-zip-outline"
66 +const DownloadIcon = "carbon:download"
67 +const DeleteIcon = "carbon:trash-can"
68 +const InfoIcon = "carbon:information"
69 +
70 +const STATUS_TYPE_MAP: Record<string, TagProps["type"]> = {
71 + completed: "success",
72 + failed: "error",
73 + processing: "warning",
74 + pending: "info"
75 +} as const
76 +
77 +const fileSize = computed(() => bytes(artifact.file_size))
78 +
79 +const statusType = computed<TagProps["type"]>(() => {
80 + const normalizedStatus = artifact.status.toLowerCase()
81 + return STATUS_TYPE_MAP[normalizedStatus] ?? "default"
82 +})
83 +</script>
84 +
85 +<style lang="scss" scoped>
86 +// TODO: remove style
87 +
88 +.artifact-card-compact {
89 + transition: all 0.2s var(--bezier-ease);
90 +
91 + &:hover {
92 + border-color: var(--primary-color);
93 + }
94 +}
95 +</style>
frontend/src/components/agents/dataStore/ArtifactDetails.vue new
+152
@@ -0,0 +1,152 @@
1 +<template>
2 + <div class="artifact-details flex flex-col gap-4">
3 + <n-descriptions :column="1" bordered size="small">
4 + <n-descriptions-item v-for="field in visibleFields" :key="field.key" :label="field.label">
5 + <component :is="field.component" v-bind="field.props">
6 + {{ field.value }}
7 + </component>
8 + </n-descriptions-item>
9 + </n-descriptions>
10 + </div>
11 +</template>
12 +
13 +<script setup lang="ts">
14 +import type { BadgeProps } from "naive-ui"
15 +import type { AgentArtifactData } from "@/types/agents.d"
16 +import bytes from "bytes"
17 +import { NBadge, NDescriptions, NDescriptionsItem } from "naive-ui"
18 +import { computed } from "vue"
19 +import { useSettingsStore } from "@/stores/settings"
20 +import { formatDate } from "@/utils"
21 +
22 +const { artifact } = defineProps<{ artifact: AgentArtifactData }>()
23 +
24 +const dFormats = useSettingsStore().dateFormat
25 +
26 +const STATUS_TYPE_MAP: Record<string, BadgeProps["type"]> = {
27 + completed: "success",
28 + failed: "error",
29 + processing: "warning",
30 + pending: "info"
31 +} as const
32 +
33 +const fileSize = computed(() => bytes(artifact.file_size))
34 +
35 +const statusType = computed<BadgeProps["type"]>(() => {
36 + const normalizedStatus = artifact.status.toLowerCase()
37 + return STATUS_TYPE_MAP[normalizedStatus] ?? "default"
38 +})
39 +
40 +const fields = computed(() => [
41 + {
42 + key: "id",
43 + label: "ID",
44 + value: artifact.id,
45 + component: "code"
46 + },
47 + {
48 + key: "agent_id",
49 + label: "Agent ID",
50 + value: artifact.agent_id,
51 + component: "code"
52 + },
53 + {
54 + key: "velociraptor_id",
55 + label: "Velociraptor ID",
56 + value: artifact.velociraptor_id,
57 + component: "code"
58 + },
59 + {
60 + key: "customer_code",
61 + label: "Customer Code",
62 + value: artifact.customer_code || "",
63 + condition: !!artifact.customer_code
64 + },
65 + {
66 + key: "artifact_name",
67 + label: "Artifact Name",
68 + value: artifact.artifact_name
69 + },
70 + {
71 + key: "flow_id",
72 + label: "Flow ID",
73 + value: artifact.flow_id,
74 + component: "code"
75 + },
76 + {
77 + key: "file_name",
78 + label: "File Name",
79 + value: artifact.file_name
80 + },
81 + {
82 + key: "file_size",
83 + label: "File Size",
84 + value: fileSize.value
85 + },
86 + {
87 + key: "content_type",
88 + label: "Content Type",
89 + value: artifact.content_type
90 + },
91 + {
92 + key: "file_hash",
93 + label: "File Hash",
94 + value: artifact.file_hash,
95 + component: "code",
96 + props: { class: "text-xs" }
97 + },
98 + {
99 + key: "collection_time",
100 + label: "Collection Time",
101 + value: formatDate(artifact.collection_time, dFormats.datetime)
102 + },
103 + {
104 + key: "status",
105 + label: "Status",
106 + value: artifact.status,
107 + component: NBadge,
108 + props: { value: artifact.status, type: statusType.value }
109 + },
110 + {
111 + key: "bucket_name",
112 + label: "Bucket",
113 + value: artifact.bucket_name
114 + },
115 + {
116 + key: "object_key",
117 + label: "Object Key",
118 + value: artifact.object_key,
119 + component: "code",
120 + props: { class: "text-xs" }
121 + },
122 + {
123 + key: "uploaded_by",
124 + label: "Uploaded By",
125 + value: artifact.uploaded_by || "",
126 + condition: !!artifact.uploaded_by
127 + },
128 + {
129 + key: "notes",
130 + label: "Notes",
131 + value: artifact.notes || "",
132 + condition: !!artifact.notes
133 + }
134 +])
135 +
136 +const visibleFields = computed(() => fields.value.filter(field => field.condition !== false))
137 +</script>
138 +
139 +<style lang="scss" scoped>
140 +// TODO: remove style
141 +.artifact-details {
142 + :deep() {
143 + code {
144 + font-family: var(--font-family-mono);
145 + font-size: 12px;
146 + padding: 2px 4px;
147 + background-color: var(--bg-secondary-color);
148 + border-radius: 3px;
149 + }
150 + }
151 +}
152 +</style>
frontend/src/components/agents/fileCollection/FileCollectionForm.vue new
+225
@@ -0,0 +1,225 @@
1 +<template>
2 + <div class="file-collection-form">
3 + <n-form ref="formRef" :model="formData" :rules="formRules">
4 + <n-form-item label="File Path" path="file_path">
5 + <n-input
6 + v-model:value="formData.file_path"
7 + placeholder="e.g., Users\Administrator\Downloads\file.txt or /home/user/file.txt"
8 + :disabled="loading"
9 + clearable
10 + >
11 + <template #prefix>
12 + <Icon :name="FileIcon" :size="16" />
13 + </template>
14 + </n-input>
15 + </n-form-item>
16 +
17 + <n-form-item label="Root Disk" path="root_disk">
18 + <n-input
19 + v-model:value="formData.root_disk"
20 + placeholder="e.g., C: (Windows) or / (Linux)"
21 + :disabled="loading"
22 + clearable
23 + >
24 + <template #prefix>
25 + <Icon :name="DiskIcon" :size="16" />
26 + </template>
27 + </n-input>
28 + </n-form-item>
29 +
30 + <n-form-item>
31 + <div class="flex items-center gap-3">
32 + <n-button
33 + type="primary"
34 + :loading="loading"
35 + :disabled="!formData.file_path || !formData.root_disk"
36 + @click="handleSubmit"
37 + >
38 + <template #icon>
39 + <Icon :name="CollectIcon" />
40 + </template>
41 + Collect File
42 + </n-button>
43 +
44 + <n-button
45 + v-if="formData.file_path || formData.root_disk"
46 + secondary
47 + :disabled="loading"
48 + @click="handleReset"
49 + >
50 + Reset
51 + </n-button>
52 + </div>
53 + </n-form-item>
54 + </n-form>
55 +
56 + <n-divider v-if="result" class="my-4!" />
57 +
58 + <n-alert
59 + v-if="result"
60 + :type="result.success ? 'success' : 'error'"
61 + :title="result.success ? 'Collection Started' : 'Collection Failed'"
62 + closable
63 + @close="result = null"
64 + >
65 + <template v-if="result.success">
66 + <div class="flex flex-col gap-2">
67 + <div v-if="result.flow_id">
68 + <strong>Flow ID:</strong>
69 + <code class="ml-2">{{ result.flow_id }}</code>
70 + </div>
71 + <div v-if="result.session_id">
72 + <strong>Session ID:</strong>
73 + <code class="ml-2">{{ result.session_id }}</code>
74 + </div>
75 + <div class="text-secondary-color mt-2 text-sm">
76 + {{ result.message }}
77 + </div>
78 + </div>
79 + </template>
80 + <template v-else>
81 + {{ result.message }}
82 + </template>
83 + </n-alert>
84 + </div>
85 +</template>
86 +
87 +<script setup lang="ts">
88 +import type { FormInst, FormRules } from "naive-ui"
89 +import type { FileCollection } from "@/types/artifacts.d"
90 +import { NAlert, NButton, NDivider, NForm, NFormItem, NInput, useMessage } from "naive-ui"
91 +import { ref } from "vue"
92 +import Api from "@/api"
93 +import Icon from "@/components/common/Icon.vue"
94 +
95 +interface FileCollectionResult extends FileCollection {
96 + success: boolean
97 + message: string
98 +}
99 +
100 +const props = defineProps<{
101 + agentId: string
102 +}>()
103 +
104 +const emit = defineEmits<{
105 + (e: "success", result: FileCollectionResult): void
106 + (e: "error", error: string): void
107 +}>()
108 +
109 +const FileIcon = "carbon:document"
110 +const DiskIcon = "carbon:storage-pool"
111 +const CollectIcon = "carbon:download"
112 +
113 +const message = useMessage()
114 +const formRef = ref<FormInst | null>(null)
115 +const loading = ref(false)
116 +
117 +const formData = ref({
118 + file_path: "",
119 + root_disk: ""
120 +})
121 +
122 +const result = ref<FileCollectionResult | null>(null)
123 +
124 +const formRules: FormRules = {
125 + file_path: [
126 + {
127 + required: true,
128 + message: "File path is required",
129 + trigger: ["blur", "input"]
130 + }
131 + ],
132 + root_disk: [
133 + {
134 + required: true,
135 + message: "Root disk is required",
136 + trigger: ["blur", "input"]
137 + }
138 + ]
139 +}
140 +
141 +async function handleSubmit() {
142 + if (!formRef.value) return
143 +
144 + try {
145 + await formRef.value.validate()
146 + } catch {
147 + message.warning("You must fill in the required fields correctly.")
148 + return false
149 + }
150 +
151 + loading.value = true
152 + result.value = null
153 +
154 + Api.artifacts
155 + .collectFileByAgentId(props.agentId, {
156 + file: formData.value.file_path,
157 + root_disk: formData.value.root_disk
158 + })
159 + .then(res => {
160 + if (res.data.success) {
161 + result.value = {
162 + success: true,
163 + message: res.data.message || "File collection started successfully",
164 + flow_id: res.data.flow_id,
165 + session_id: res.data.session_id
166 + }
167 + message.success("File collection started successfully")
168 + emit("success", result.value)
169 + } else {
170 + result.value = {
171 + success: false,
172 + message: res.data?.message || "Failed to start file collection"
173 + }
174 + message.error(result.value.message)
175 + emit("error", result.value.message)
176 + }
177 + })
178 + .catch(err => {
179 + const errorMessage = err.response?.data?.message || "An error occurred during file collection"
180 + result.value = {
181 + success: false,
182 + message: errorMessage
183 + }
184 + message.error(errorMessage)
185 + emit("error", errorMessage)
186 + })
187 + .finally(() => {
188 + loading.value = false
189 + })
190 +}
191 +
192 +function handleReset() {
193 + formData.value = {
194 + file_path: "",
195 + root_disk: ""
196 + }
197 + result.value = null
198 + formRef.value?.restoreValidation()
199 +}
200 +
201 +// Expose methods for parent components
202 +defineExpose({
203 + reset: handleReset
204 +})
205 +</script>
206 +
207 +<style lang="scss" scoped>
208 +// TODO: remove style
209 +
210 +.file-collection-form {
211 + max-width: 600px;
212 +
213 + :deep(.n-form-item) {
214 + margin-bottom: 20px;
215 + }
216 +
217 + code {
218 + font-family: var(--font-family-mono);
219 + font-size: 12px;
220 + padding: 2px 6px;
221 + background-color: var(--bg-secondary-color);
222 + border-radius: 3px;
223 + }
224 +}
225 +</style>
frontend/src/components/agents/sca/ScaResultItem.vue
+1 -1
@@ -5,7 +5,7 @@
5 <template #headerExtra>
6 <n-button size="small" @click.stop="showDetails = true">
7 <template #icon>
8 - <Icon :name="DetailsIcon"></Icon>
8 + <Icon :name="DetailsIcon" />
9 </template>
10 Details
11 </n-button>
frontend/src/components/agents/sca/ScaResults.vue
+2 -2
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
@@ -46,7 +46,7 @@
46 <div class="bg-default rounded-lg">
47 <n-button size="small">
48 <template #icon>
49 - <Icon :name="FilterIcon"></Icon>
49 + <Icon :name="FilterIcon" />
50 </template>
51 </n-button>
52 </div>
frontend/src/components/agents/sca/ScaTable.vue
+3 -3
@@ -21,7 +21,7 @@
21 <template #trigger>
22 <n-button size="small" @click="showScaDetails(item)">
23 <template #icon>
24 - <Icon :name="InfoIcon"></Icon>
24 + <Icon :name="InfoIcon" />
25 </template>
26 </n-button>
27 </template>
@@ -32,7 +32,7 @@
32 <template #trigger>
33 <n-button size="small" :loading="item.downloading" @click="scaDownload(item)">
34 <template #icon>
35 - <Icon :name="DownloadIcon"></Icon>
35 + <Icon :name="DownloadIcon" />
36 </template>
37 </n-button>
38 </template>
@@ -90,7 +90,7 @@
90 :title="selectedSca?.policy_id || ''"
91 :bordered="false"
92 segmented
93 - content-class="!p-0"
93 + content-class="p-0!"
94 >
95 <ScaItem v-if="selectedSca" :sca="selectedSca" :agent></ScaItem>
96 </n-modal>
frontend/src/components/agents/vulnerabilities/VulnerabilityCard.vue
+6 -6
@@ -9,14 +9,14 @@
9 </div>
10 <div class="property">
11 <div v-if="hideTooltip">
12 - <Icon :name="ClockIcon"></Icon>
12 + <Icon :name="ClockIcon" />
13 <span>{{ detectionTime }}</span>
14 </div>
15 <n-tooltip v-else placement="top-start">
16 <span>Detection time</span>
17 <template #trigger>
18 <div>
19 - <Icon :name="ClockIcon"></Icon>
19 + <Icon :name="ClockIcon" />
20 <span>{{ detectionTime }}</span>
21 </div>
22 </template>
@@ -24,14 +24,14 @@
24 </div>
25 <div class="property">
26 <div v-if="hideTooltip">
27 - <Icon :name="CounterIcon"></Icon>
27 + <Icon :name="CounterIcon" />
28 <span>{{ vulnerability.cve }}</span>
29 </div>
30 <n-tooltip v-else placement="top-start">
31 <span>{{ `CVSS2: ${vulnerability.cvss2_score} - CVSS3: ${vulnerability.cvss3_score}` }}</span>
32 <template #trigger>
33 <div>
34 - <Icon :name="CounterIcon"></Icon>
34 + <Icon :name="CounterIcon" />
35 <span>{{ vulnerability.cve }}</span>
36 </div>
37 </template>
@@ -39,14 +39,14 @@
39 </div>
40 <div class="property">
41 <div v-if="hideTooltip">
42 - <Icon :name="VulnerabilityIcon"></Icon>
42 + <Icon :name="VulnerabilityIcon" />
43 <span>{{ vulnerability.name }}</span>
44 </div>
45 <n-tooltip v-else placement="top-start">
46 <span>{{ `Version: ${vulnerability.version}` }}</span>
47 <template #trigger>
48 <div>
49 - <Icon :name="VulnerabilityIcon"></Icon>
49 + <Icon :name="VulnerabilityIcon" />
50 <span>{{ vulnerability.name }}</span>
51 </div>
52 </template>
frontend/src/components/alerts/AlertActions.vue
+10 -8
@@ -2,7 +2,7 @@
2 <div class="flex flex-wrap justify-end gap-2">
3 <n-button v-if="socAlertFieldValue" type="success" secondary :size="size" @click.stop="gotoSocAlertUrl()">
4 <template #icon>
5 - <Icon :name="ViewIcon"></Icon>
5 + <Icon :name="ViewIcon" />
6 </template>
7 View SOC Alert
8 </n-button>
@@ -15,13 +15,13 @@
15 @click.stop="createAlert()"
16 >
17 <template #icon>
18 - <Icon :name="DangerIcon"></Icon>
18 + <Icon :name="DangerIcon" />
19 </template>
20 Create SOC Alert
21 </n-button>
22 <n-button v-if="alertAskMessage" type="success" secondary :size="size" @click.stop="showSocResponse = true">
23 <template #icon>
24 - <Icon :name="ViewIcon"></Icon>
24 + <Icon :name="ViewIcon" />
25 </template>
26 View SOCFortress Response
27 </n-button>
@@ -34,7 +34,7 @@
34 @click.stop="askSOCFortress()"
35 >
36 <template #icon>
37 - <Icon :name="AskIcon"></Icon>
37 + <Icon :name="AskIcon" />
38 </template>
39 Ask SOCFortress
40 </n-button>
@@ -46,7 +46,7 @@
46 @click.stop="wazuhManagerRuleExclude()"
47 >
48 <template #icon>
49 - <Icon :name="RulesIcon"></Icon>
49 + <Icon :name="RulesIcon"/>
50 </template>
51 Exclude Rule in Wazuh
52 </n-button> -->
@@ -77,7 +77,7 @@
77 :style="{ maxWidth: 'min(800px, 90vw)', overflow: 'hidden' }"
78 title="Recommended exclusion for a Wazuh Rule"
79 :bordered="false"
80 - content-class="!p-0"
80 + content-class="p-0!"
81 segmented
82 >
83 <AlertWazuhRules v-if="wazuhRuleData" :data="wazuhRuleData" />
@@ -112,7 +112,7 @@ const emit = defineEmits<{
112 const DangerIcon = "majesticons:exclamation-line"
113 const AskIcon = "majesticons:question-mark-circle-line"
114 const ViewIcon = "iconoir:eye-solid"
115 -const RulesIcon = "carbon:rule-cancelled"
115 +// const RulesIcon = "carbon:rule-cancelled"
116
117 const router = useRouter()
118 const message = useMessage()
@@ -131,7 +131,7 @@ const wazuhRuleData = ref<WazuhRuleExclude | null>(null)
131 const socAlertFieldValue = computed(() => (socAlertField === "alert_id" ? alertId.value : alertUrl.value))
132
133 const isAskVisible = computed(() => alert._source?.rule_group3 === "sigma" && !alertAskMessage.value)
134 -const isWazuhRulesVisible = computed(() => alert._source)
134 +// const _isWazuhRulesVisible = computed(() => alert._source)
135
136 watch(loading, val => {
137 if (val) {
@@ -188,6 +188,7 @@ function askSOCFortress() {
188 })
189 }
190
191 +/*
192 function wazuhManagerRuleExclude() {
193 if (wazuhRuleData.value) {
194 showWazuhRuleExclude.value = true
@@ -216,6 +217,7 @@ function wazuhManagerRuleExclude() {
217 loadingWazuhRuleExclude.value = false
218 })
219 }
220 +*/
221
222 function createAlert() {
223 loadingSocAlert.value = true
frontend/src/components/alerts/AlertsFilters.vue
+2 -2
@@ -17,7 +17,7 @@
17 <template #action>
18 <n-button size="tiny" quaternary class="!w-full" @click="clearFieldsHistory()">
19 <template #icon>
20 - <Icon :name="ClearIcon"></Icon>
20 + <Icon :name="ClearIcon" />
21 </template>
22 Clear history
23 </n-button>
@@ -47,7 +47,7 @@
47 <div class="flex justify-end">
48 <n-button strong secondary type="primary" @click="emit('search')">
49 <template #icon>
50 - <Icon :name="SearchIcon"></Icon>
50 + <Icon :name="SearchIcon" />
51 </template>
52 Search
53 </n-button>
frontend/src/components/alerts/AlertsGraylogFilters.vue
+1 -1
@@ -12,7 +12,7 @@
12 <div class="flex justify-end">
13 <n-button strong secondary type="primary" @click="search()">
14 <template #icon>
15 - <Icon :name="SearchIcon"></Icon>
15 + <Icon :name="SearchIcon" />
16 </template>
17 Search
18 </n-button>
frontend/src/components/alerts/AlertsGraylogList.vue
+2 -2
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
@@ -27,7 +27,7 @@
27 <div class="actions flex items-center gap-2">
28 <n-button size="small" @click="showFiltersDrawer = true">
29 <template #icon>
30 - <Icon :name="FilterIcon" :size="15"></Icon>
30 + <Icon :name="FilterIcon" :size="15" />
31 </template>
32 Filters
33 </n-button>
frontend/src/components/alerts/AlertsList.vue
+3 -3
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
@@ -27,13 +27,13 @@
27 <div class="actions flex items-center gap-2">
28 <n-button v-if="!isFilterPreselected" size="small" @click="showStatsDrawer = true">
29 <template #icon>
30 - <Icon :name="StatsIcon" :size="14"></Icon>
30 + <Icon :name="StatsIcon" :size="14" />
31 </template>
32 Stats
33 </n-button>
34 <n-button size="small" @click="showFiltersDrawer = true">
35 <template #icon>
36 - <Icon :name="FilterIcon" :size="15"></Icon>
36 + <Icon :name="FilterIcon" :size="15" />
37 </template>
38 Filters
39 </n-button>
frontend/src/components/alerts/AlertsSummary.vue
+1 -1
@@ -38,7 +38,7 @@
38 <div v-if="alertsSummary.alerts.length > 3" class="load-more" @click="showAllAlerts = true">
39 <n-button size="small" text class="!w-full">
40 <template #icon>
41 - <Icon :name="ExpandIcon"></Icon>
41 + <Icon :name="ExpandIcon" />
42 </template>
43 See all alerts
44 </n-button>
frontend/src/components/artifacts/ArtifactsCollect.vue
+1 -1
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
frontend/src/components/artifacts/ArtifactsCommand.vue
+2 -2
@@ -56,7 +56,7 @@
56 <template #trigger>
57 <Badge type="splitted" color="primary" hint-cursor>
58 <template #iconLeft>
59 - <Icon :name="TimeIcon"></Icon>
59 + <Icon :name="TimeIcon" />
60 </template>
61 <template #value>
62 <span class="flex">
@@ -74,7 +74,7 @@
74
75 <Badge v-if="diffTime" type="splitted" color="primary">
76 <template #iconLeft>
77 - <Icon :name="StopWatchIcon" :size="15"></Icon>
77 + <Icon :name="StopWatchIcon" :size="15" />
78 </template>
79 <template #value>
80 {{ diffTime }}
frontend/src/components/artifacts/ArtifactsList.vue
+2 -2
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
@@ -40,7 +40,7 @@
40 >
41 <n-button v-show="!isFilterPreselected" size="small" @click="showFilters = true">
42 <template #icon>
43 - <Icon :name="FilterIcon"></Icon>
43 + <Icon :name="FilterIcon" />
44 </template>
45 </n-button>
46 </n-badge>
frontend/src/components/artifacts/ArtifactsQuarantine.vue
+1 -1
@@ -53,7 +53,7 @@
53 :disabled="!areFiltersValid"
54 @click="getData()"
55 >
56 - <Icon :name="SubmitIcon"></Icon>
56 + <Icon :name="SubmitIcon" />
57 </n-button>
58 </n-input-group>
59 </div>
frontend/src/components/auth/SignUp.vue
+5 -5
@@ -45,7 +45,7 @@
45 @click="wizardCurrent = 2"
46 >
47 <template #icon>
48 - <Icon :name="ArrowRightIcon"></Icon>
48 + <Icon :name="ArrowRightIcon" />
49 </template>
50 Next
51 </n-button>
@@ -87,14 +87,14 @@
87 >
88 <n-button type="primary" @click="openCropper()" size="small">
89 <template #icon>
90 - <Icon :name="ImageIcon"></Icon>
90 + <Icon :name="ImageIcon"/>
91 </template>
92 {{ model.propic ? "Edit" : "Add" }} Photo
93 </n-button>
94 </ImageCropper>
95 <n-button @click="model.propic = ''" v-if="model.propic" size="small">
96 <template #icon>
97 - <Icon :name="RemoveImageIcon"></Icon>
97 + <Icon :name="RemoveImageIcon"/>
98 </template>
99 Remove Photo
100 </n-button>
@@ -130,7 +130,7 @@
130 <div class="flex items-center justify-between gap-3">
131 <n-button size="large" @click="wizardCurrent = 1">
132 <template #icon>
133 - <Icon :name="ArrowLeftIcon"></Icon>
133 + <Icon :name="ArrowLeftIcon" />
134 </template>
135 Back
136 </n-button>
@@ -144,7 +144,7 @@
144 @click="signUp"
145 >
146 <template #icon>
147 - <Icon :name="UserAddIcon"></Icon>
147 + <Icon :name="UserAddIcon" />
148 </template>
149 Create account
150 </n-button>
frontend/src/components/cloudSecurityAssessment/AvailableReportsList.vue
+2 -2
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
@@ -23,7 +23,7 @@
23 <div class="actions flex items-center gap-2">
24 <n-button size="small" type="primary" @click="showForm = true">
25 <template #icon>
26 - <Icon :name="NewReportIcon" :size="15"></Icon>
26 + <Icon :name="NewReportIcon" :size="15" />
27 </template>
28 Create new Report
29 </n-button>
frontend/src/components/cloudSecurityAssessment/CloudSecurityAssessmentButton.vue
+1 -1
@@ -1,7 +1,7 @@
1 <template>
2 <n-button :size="size" :type="type" @click="gotoPage()">
3 <template #icon>
4 - <Icon :name="CloudIcon"></Icon>
4 + <Icon :name="CloudIcon" />
5 </template>
6 Cloud Security Assessment
7 </n-button>
frontend/src/components/cloudSecurityAssessment/FormTypes/GcpTypeForm.vue
+1 -1
@@ -5,7 +5,7 @@
5 <n-upload v-model:file-list="fileList" :max="1" accept="application/json, .json, .JSON">
6 <n-upload-dragger>
7 <div>
8 - <Icon :name="UploadIcon" :size="28" :depth="3"></Icon>
8 + <Icon :name="UploadIcon" :size="28" :depth="3" />
9 </div>
10 <div class="font-semibold">Click or drag a file to this area to upload</div>
11 <p class="mt-2">Only .json files are accepted</p>
frontend/src/components/common/PaginationIndeterminate.vue
+2 -2
@@ -9,10 +9,10 @@
9 :disabled="disabled"
10 >
11 <template #minus-icon>
12 - <Icon :name="ArrowBackIcon"></Icon>
12 + <Icon :name="ArrowBackIcon" />
13 </template>
14 <template #add-icon>
15 - <Icon :name="ArrowForwardIcon"></Icon>
15 + <Icon :name="ArrowForwardIcon" />
16 </template>
17 </n-input-number>
18 <n-select
frontend/src/components/common/XMLEditor.vue
+1 -1
@@ -6,7 +6,7 @@
6 indent-with-tab
7 :tab-size="4"
8 :extensions
9 - style="height: 100%"
9 + :style="{ height: '100%' }"
10 @ready="handleReady"
11 />
12 </template>
frontend/src/components/common/cards/CardStats.vue
+1 -1
@@ -8,7 +8,7 @@
8 <div class="info flex grow flex-col overflow-hidden">
9 <div class="title flex items-center gap-2">
10 {{ title }}
11 - <Icon v-if="hovered" :name="ArrowRightIcon" :size="12"></Icon>
11 + <Icon v-if="hovered" :name="ArrowRightIcon" :size="12" />
12 </div>
13 <div v-if="value" class="value mt-1">
14 {{ value }}
frontend/src/components/common/cards/CardStatsBars.vue
+1 -1
@@ -4,7 +4,7 @@
4 <div class="card-header flex items-center justify-between gap-4">
5 <div class="title flex grow items-center gap-2">
6 <span class="truncate">{{ title }}</span>
7 - <Icon v-if="hovered" :name="ArrowRightIcon" :size="12"></Icon>
7 + <Icon v-if="hovered" :name="ArrowRightIcon" :size="12" />
8 </div>
9 <div class="icon">
10 <slot name="icon"></slot>
frontend/src/components/common/cards/CardStatsIcon.vue
+1 -1
@@ -4,7 +4,7 @@
4 <Icon v-if="$slots.default" :size="iconFinalSize">
5 <slot></slot>
6 </Icon>
7 - <Icon v-else :size="iconFinalSize" :name="iconName"></Icon>
7 + <Icon v-else :size="iconFinalSize" :name="iconName" />
8 </div>
9 </template>
10
frontend/src/components/common/cards/CardStatsMulti.vue
+1 -1
@@ -4,7 +4,7 @@
4 <div class="card-header flex items-center justify-between gap-4">
5 <div class="title flex grow items-center gap-2">
6 <span class="truncate">{{ title }}</span>
7 - <Icon v-if="hovered" :name="ArrowRightIcon" :size="12"></Icon>
7 + <Icon v-if="hovered" :name="ArrowRightIcon" :size="12" />
8 </div>
9 <div class="icon">
10 <slot name="icon"></slot>
frontend/src/components/connectors/ConfigForm/FormTypes/FileType.vue
+1 -1
@@ -11,7 +11,7 @@
11 >
12 <n-upload-dragger>
13 <div>
14 - <Icon :name="UploadIcon" :size="48" :depth="3"></Icon>
14 + <Icon :name="UploadIcon" :size="48" :depth="3" />
15 </div>
16 <h4>Click or drag a file to this area to upload</h4>
17 <p class="mt-2">Limit 1 file .YAML, new file will cover the old file</p>
frontend/src/components/connectors/ConnectorItem.vue
+2 -2
@@ -56,7 +56,7 @@
56 @click="verify(connector)"
57 >
58 <template #icon>
59 - <Icon :name="VerifyIcon"></Icon>
59 + <Icon :name="VerifyIcon" />
60 </template>
61 Verify
62 </n-button>
@@ -68,7 +68,7 @@
68 @click="openConfigDialog()"
69 >
70 <template #icon>
71 - <Icon :name="DetailsIcon"></Icon>
71 + <Icon :name="DetailsIcon" />
72 </template>
73
74 {{ !connector.connector_configured ? "Configure" : "Update" }}
frontend/src/components/copilotAction/ActionCard.vue
+1 -1
@@ -48,7 +48,7 @@
48 <template #footerExtra>
49 <n-button size="small" type="primary" secondary @click.stop="showInvokeModal = true">
50 <template #icon>
51 - <Icon :name="PlayIcon"></Icon>
51 + <Icon :name="PlayIcon" />
52 </template>
53 Invoke
54 </n-button>
frontend/src/components/copilotAction/List.vue
+2 -2
@@ -16,7 +16,7 @@
16 <div class="bg-default rounded-lg">
17 <n-button size="small" class="cursor-help!">
18 <template #icon>
19 - <Icon :name="InfoIcon"></Icon>
19 + <Icon :name="InfoIcon" />
20 </template>
21 </n-button>
22 </div>
@@ -48,7 +48,7 @@
48 clearable
49 >
50 <template #prefix>
51 - <Icon :name="SearchIcon"></Icon>
51 + <Icon :name="SearchIcon" />
52 </template>
53 </n-input>
54 </div>
frontend/src/components/customerPortal/Form.vue new
+168
@@ -0,0 +1,168 @@
1 +<template>
2 + <n-spin :show="loading" content-class="flex grow flex-col">
3 + <div class="flex flex-col gap-10">
4 + <n-form-item label="Title" path="title" :show-feedback="false">
5 + <n-input v-model:value="model.title" clearable />
6 + </n-form-item>
7 +
8 + <div class="flex gap-3">
9 + <div v-if="model.logo" class="relative">
10 + <div
11 + class="absolute inset-0 flex cursor-pointer items-center justify-center bg-black/10 opacity-0 transition-opacity duration-300 hover:opacity-100"
12 + @click="model.logo = null"
13 + >
14 + <Icon :name="RemoveIcon" :size="30" class="text-secondary drop-shadow-md/90" />
15 + </div>
16 +
17 + <img :src="model.logo" width="66" height="66" class="object-cover" />
18 + </div>
19 + <n-form-item label="Logo" path="logo" :show-feedback="false">
20 + <ImageCropper v-slot="{ openCropper }" placeholder="Select a Logo" @crop="setCroppedImage">
21 + <n-button @click="openCropper()">
22 + <template #icon>
23 + <Icon :name="EditIcon" />
24 + </template>
25 + Edit Logo Image
26 + </n-button>
27 + </ImageCropper>
28 + </n-form-item>
29 + </div>
30 +
31 + <div>
32 + <n-button type="primary" :loading="loading" @click="save()">
33 + <template #icon>
34 + <Icon :name="SaveIcon" />
35 + </template>
36 + Save Changes
37 + </n-button>
38 + </div>
39 + </div>
40 + </n-spin>
41 +</template>
42 +
43 +<script setup lang="ts">
44 +import type { CustomerPortalSettingsPayload } from "@/api/endpoints/customerPortal"
45 +import type { ImageCropperResult } from "@/components/common/ImageCropper.vue"
46 +import type { CustomerPortalSettings } from "@/types/customerPortal"
47 +import _split from "lodash/split"
48 +import { NButton, NFormItem, NInput, NSpin, useMessage } from "naive-ui"
49 +import { onBeforeMount, ref, watch } from "vue"
50 +import Api from "@/api"
51 +import Icon from "@/components/common/Icon.vue"
52 +import ImageCropper from "@/components/common/ImageCropper.vue"
53 +
54 +export interface SettingsModel {
55 + title: string | null
56 + logo: string | null
57 +}
58 +
59 +const emit = defineEmits<{
60 + (e: "update", value: SettingsModel): void
61 + (e: "success"): void
62 +}>()
63 +
64 +const SaveIcon = "carbon:save"
65 +const EditIcon = "uil:image-edit"
66 +const RemoveIcon = "carbon:trash-can"
67 +const message = useMessage()
68 +const loading = ref(false)
69 +const settings = ref<CustomerPortalSettings | null>(null)
70 +const model = ref<SettingsModel>(getDefaultModel())
71 +
72 +function setCroppedImage(result: ImageCropperResult) {
73 + const canvas = result.canvas as HTMLCanvasElement
74 + model.value.logo = canvas.toDataURL()
75 +}
76 +
77 +function getDefaultModel(entity?: CustomerPortalSettings): SettingsModel {
78 + return {
79 + title: entity?.title || "",
80 + logo:
81 + entity?.logo_base64 && entity?.logo_mime_type
82 + ? `data:${entity.logo_mime_type};base64,${entity.logo_base64}`
83 + : null
84 + }
85 +}
86 +
87 +function getLogoMeta(logo?: string | null): { base64: string | null; mime_type: string | null } {
88 + // Parse data URL format: data:image/png;base64,iVBORw0KG...
89 + const parts = _split(logo, ",")
90 + if (!logo || parts.length !== 2) {
91 + return {
92 + base64: null,
93 + mime_type: null
94 + }
95 + }
96 +
97 + const base64 = parts[1]
98 + const mimeMatch = parts[0].match(/data:([^;]+);base64/)
99 + const mime_type = mimeMatch ? mimeMatch[1] : null
100 +
101 + return {
102 + base64,
103 + mime_type
104 + }
105 +}
106 +
107 +function save() {
108 + loading.value = true
109 +
110 + const payload: CustomerPortalSettingsPayload = {
111 + title: model.value.title || null,
112 + logo_base64: getLogoMeta(model.value.logo).base64,
113 + logo_mime_type: getLogoMeta(model.value.logo).mime_type
114 + }
115 +
116 + Api.customerPortal
117 + .setSettings(payload)
118 + .then(res => {
119 + if (res.data.success) {
120 + message.success(res.data?.message || "Customer Portal settings updated successfully")
121 + // Reload the data to show updated values
122 + emit("success")
123 + } else {
124 + message.warning(res.data?.message || "Failed to update metadata")
125 + }
126 + })
127 + .catch(err => {
128 + const errorMsg = err.response?.data?.message || "An error occurred while updating metadata"
129 + message.error(errorMsg)
130 + })
131 + .finally(() => {
132 + loading.value = false
133 + })
134 +}
135 +
136 +function getSettings() {
137 + loading.value = true
138 +
139 + Api.customerPortal
140 + .getSettings()
141 + .then(res => {
142 + if (res.data.success) {
143 + settings.value = res.data.settings
144 + model.value = getDefaultModel(settings.value)
145 + } else {
146 + message.warning(res.data?.message || "An error occurred. Please try again later.")
147 + }
148 + })
149 + .catch(err => {
150 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
151 + })
152 + .finally(() => {
153 + loading.value = false
154 + })
155 +}
156 +
157 +watch(
158 + model,
159 + val => {
160 + emit("update", val)
161 + },
162 + { immediate: true, deep: true }
163 +)
164 +
165 +onBeforeMount(() => {
166 + getSettings()
167 +})
168 +</script>
frontend/src/components/customerPortal/Preview.vue new
+53
@@ -0,0 +1,53 @@
1 +<template>
2 + <div class="w-full max-w-md space-y-8">
3 + <!-- Logo and Title -->
4 + <div class="text-center">
5 + <div v-if="logo" class="mb-6">
6 + <img class="mx-auto h-12 w-auto" :src="logo" :alt="`${title} Logo`" />
7 + </div>
8 + <h2 class="mb-2 text-4xl font-bold text-gray-900">{{ title || "CoPilot" }}</h2>
9 + <p class="text-lg text-gray-600">Access your security dashboard and reports</p>
10 + </div>
11 + <!-- Login Form -->
12 + <div class="mt-8 rounded-lg bg-white px-6 py-8 shadow-lg">
13 + <form class="space-y-6">
14 + <!-- v-if -->
15 + <div>
16 + <label for="username" class="mb-2 block text-sm font-medium text-gray-700">Username</label>
17 + <input
18 + id="username"
19 + type="text"
20 + disabled
21 + autocomplete="username"
22 + class="block w-full rounded-md border border-gray-300 px-3 py-3 text-base placeholder-gray-400 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
23 + placeholder="Enter your username"
24 + />
25 + </div>
26 + <div>
27 + <label for="password" class="mb-2 block text-sm font-medium text-gray-700">Password</label>
28 + <input
29 + id="password"
30 + type="password"
31 + disabled
32 + autocomplete="current-password"
33 + class="block w-full rounded-md border border-gray-300 px-3 py-3 text-base placeholder-gray-400 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
34 + placeholder="Enter your password"
35 + />
36 + </div>
37 + <div>
38 + <button
39 + type="submit"
40 + disabled
41 + class="group relative flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-3 text-base font-medium text-white transition-colors duration-200 hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
42 + >
43 + <span>Sign in</span>
44 + </button>
45 + </div>
46 + </form>
47 + </div>
48 + </div>
49 +</template>
50 +
51 +<script setup lang="ts">
52 +const { title, logo } = defineProps<{ title?: string | null; logo?: string | null }>()
53 +</script>
frontend/src/components/customerPortal/Settings.vue new
+21
@@ -0,0 +1,21 @@
1 +<template>
2 + <div class="flex flex-col-reverse gap-6 lg:flex-row">
3 + <div class="border-primary shrink-0 overflow-hidden rounded-lg border-2 bg-white p-4 pt-12 lg:w-120 lg:p-8">
4 + <Preview class="mx-auto" :title="previewData?.title" :logo="previewData?.logo" />
5 + </div>
6 +
7 + <n-card class="grow">
8 + <SettingsForm @update="previewData = $event" />
9 + </n-card>
10 + </div>
11 +</template>
12 +
13 +<script setup lang="ts">
14 +import type { SettingsModel } from "./Form.vue"
15 +import { NCard } from "naive-ui"
16 +import { ref } from "vue"
17 +import SettingsForm from "./Form.vue"
18 +import Preview from "./Preview.vue"
19 +
20 +const previewData = ref<SettingsModel | null>(null)
21 +</script>
frontend/src/components/customers/CustomerCreationButton.vue
+1 -1
@@ -30,7 +30,7 @@
30 @click="showAddCustomer = true"
31 >
32 <template #icon>
33 - <Icon :name="AddUserIcon" :size="14"></Icon>
33 + <Icon :name="AddUserIcon" :size="14" />
34 </template>
35 <div class="flex items-center gap-2">
36 <span>Add Customer</span>
frontend/src/components/customers/CustomerInfo.vue
+2 -2
@@ -11,13 +11,13 @@
11 <div class="flex items-center justify-between gap-4 px-7 pt-2">
12 <n-button size="small" :disabled="loadingDelete" @click="editing = true">
13 <template #icon>
14 - <Icon :name="EditIcon" :size="14"></Icon>
14 + <Icon :name="EditIcon" :size="14" />
15 </template>
16 Edit
17 </n-button>
18 <n-button size="small" type="error" ghost :loading="loadingDelete" @click="handleDelete">
19 <template #icon>
20 - <Icon :name="DeleteIcon" :size="15"></Icon>
20 + <Icon :name="DeleteIcon" :size="15" />
21 </template>
22 Delete Customer
23 </n-button>
frontend/src/components/customers/CustomerItem.vue
+9 -9
@@ -26,7 +26,7 @@
26 <div class="flex flex-wrap items-center gap-3">
27 <Badge type="splitted" color="primary">
28 <template #iconLeft>
29 - <Icon :name="UserTypeIcon" :size="14"></Icon>
29 + <Icon :name="UserTypeIcon" :size="14" />
30 </template>
31 <template #label>Type</template>
32 <template #value>
@@ -38,7 +38,7 @@
38 <template #trigger>
39 <Badge type="splitted" color="primary" :class="{ 'cursor-help': addressLabel !== '-' }">
40 <template #iconLeft>
41 - <Icon :name="LocationIcon" :size="13"></Icon>
41 + <Icon :name="LocationIcon" :size="13" />
42 </template>
43 <template #value>
44 <div class="flex flex-wrap items-center gap-2">
@@ -48,7 +48,7 @@
48 :name="InfoIcon"
49 :size="13"
50 class="!opacity-80"
51 - ></Icon>
51 + />
52 </div>
53 </template>
54 </Badge>
@@ -84,7 +84,7 @@
84
85 <Badge type="splitted" color="primary">
86 <template #iconLeft>
87 - <Icon :name="PhoneIcon" :size="13"></Icon>
87 + <Icon :name="PhoneIcon" :size="13" />
88 </template>
89 <template #value>
90 {{ customerInfo?.phone || "-" }}
@@ -93,7 +93,7 @@
93
94 <Badge v-if="customerInfo?.parent_customer_code" type="splitted" color="primary">
95 <template #iconLeft>
96 - <Icon :name="ParentIcon" :size="13"></Icon>
96 + <Icon :name="ParentIcon" :size="13" />
97 </template>
98 <template #label>Parent</template>
99 <template #value>
@@ -106,7 +106,7 @@
106 <template v-if="!hideCardActions" #footerExtra>
107 <n-button size="small" @click.stop="showDetails = true">
108 <template #icon>
109 - <Icon :name="DetailsIcon"></Icon>
109 + <Icon :name="DetailsIcon" />
110 </template>
111 Details
112 </n-button>
@@ -116,7 +116,7 @@
116 <n-modal
117 v-model:show="showDetails"
118 preset="card"
119 - content-class="!p-0"
119 + content-class="p-0!"
120 :style="{ maxWidth: 'min(1100px, 90vw)', minHeight: 'min(600px, 90vh)', overflow: 'hidden' }"
121 :title="customerInfo?.customer_name"
122 :bordered="false"
@@ -188,7 +188,7 @@
188 class="hover:text-primary relative top-1 cursor-pointer pl-6"
189 @click="selectedTabsGroup = 'customer'"
190 >
191 - <Icon :name="ArrowIcon" :size="20"></Icon>
191 + <Icon :name="ArrowIcon" :size="20" />
192 </div>
193 </template>
194 <n-tab-pane name="Agents" tab="Agents" display-directive="show:lazy">
@@ -226,7 +226,7 @@
226 class="hover:text-primary relative top-1 cursor-pointer pl-6"
227 @click="selectedTabsGroup = 'customer'"
228 >
229 - <Icon :name="ArrowIcon" :size="20"></Icon>
229 + <Icon :name="ArrowIcon" :size="20" />
230 </div>
231 </template>
232 <n-tab-pane name="Wazuh Worker" tab="Wazuh Worker" display-directive="show:lazy">
frontend/src/components/customers/CustomerWazuhWorker.vue
+5 -5
@@ -29,13 +29,13 @@
29 <div class="flex flex-wrap items-center gap-3">
30 <Badge type="splitted" color="primary">
31 <template #iconLeft>
32 - <Icon :name="UserIcon" :size="14"></Icon>
32 + <Icon :name="UserIcon" :size="14" />
33 </template>
34 <template #value>{{ portainerStack.CreatedBy }}</template>
35 </Badge>
36 <Badge type="splitted" color="primary">
37 <template #iconLeft>
38 - <Icon :name="DateIcon" :size="14"></Icon>
38 + <Icon :name="DateIcon" :size="14" />
39 </template>
40 <template #value>
41 {{ formatDate(portainerStack.CreationDate, dFormats.datetimesec) }}
@@ -47,7 +47,7 @@
47 <div class="flex flex-wrap items-center justify-end gap-3">
48 <n-button secondary size="small" @click.stop="showResourceControl = true">
49 <template #icon>
50 - <Icon :name="ViewIcon"></Icon>
50 + <Icon :name="ViewIcon" />
51 </template>
52 ResourceControl
53 </n-button>
@@ -60,7 +60,7 @@
60 @click.stop="stop()"
61 >
62 <template #icon>
63 - <Icon :name="StopIcon"></Icon>
63 + <Icon :name="StopIcon" />
64 </template>
65 Stop Worker
66 </n-button>
@@ -73,7 +73,7 @@
73 @click.stop="start()"
74 >
75 <template #icon>
76 - <Icon :name="StartIcon"></Icon>
76 + <Icon :name="StartIcon" />
77 </template>
78 Start Worker
79 </n-button>
frontend/src/components/customers/healthcheck/CustomerHealthcheckItem.vue
+2 -2
@@ -40,13 +40,13 @@
40 <template #trigger>
41 <Badge type="splitted" hint-cursor>
42 <template #iconLeft>
43 - <Icon :name="AgentIcon" :size="13" class="!opacity-80"></Icon>
43 + <Icon :name="AgentIcon" :size="13" class="!opacity-80" />
44 </template>
45 <template #label>Agent</template>
46 <template #value>
47 <div class="flex items-center gap-2">
48 {{ healthData.hostname }}
49 - <Icon :name="InfoIcon" :size="14"></Icon>
49 + <Icon :name="InfoIcon" :size="14" />
50 </div>
51 </template>
52 </Badge>
frontend/src/components/customers/healthcheck/CustomerHealthcheckList.vue
+2 -2
@@ -17,7 +17,7 @@
17 <div class="flex min-h-52 flex-col gap-6">
18 <div v-if="healthyList.length" class="flex flex-col gap-2 [&:not(:last-child)]:mb-5">
19 <div class="text-primary mb-2 flex items-center gap-2">
20 - <Icon :name="CheckIcon" :size="16"></Icon>
20 + <Icon :name="CheckIcon" :size="16" />
21 Healthy
22 <code>{{ healthyList.length }}</code>
23 </div>
@@ -33,7 +33,7 @@
33 </div>
34 <div v-if="unhealthyList.length" class="flex flex-col gap-2 [&:not(:last-child)]:mb-5">
35 <div class="text-warning mb-2 flex items-center gap-2">
36 - <Icon :name="AlertIcon" :size="16"></Icon>
36 + <Icon :name="AlertIcon" :size="16" />
37 Unhealthy
38 <code>{{ unhealthyList.length }}</code>
39 </div>
frontend/src/components/customers/integrations/CustomerIntegrationActions.vue
+2 -2
@@ -2,14 +2,14 @@
2 <div class="contents">
3 <n-button v-if="isDeployEnabled" :loading="loading" type="success" :size secondary @click.stop="provision()">
4 <template #icon>
5 - <Icon :name="DeployIcon"></Icon>
5 + <Icon :name="DeployIcon" />
6 </template>
7 Deploy
8 </n-button>
9
10 <n-button v-if="!hideDeleteButton" :size type="error" ghost :loading="loadingDelete" @click.stop="handleDelete">
11 <template #icon>
12 - <Icon :name="DeleteIcon" :size="15"></Icon>
12 + <Icon :name="DeleteIcon" :size="15" />
13 </template>
14 Delete
15 </n-button>
frontend/src/components/customers/integrations/CustomerIntegrationDetails.vue
+4 -4
@@ -16,14 +16,14 @@
16 <div class="flex items-center justify-end gap-3">
17 <n-button :loading="updating" :disabled="deleting" secondary @click.stop="switchMode('edit')">
18 <template #icon>
19 - <Icon :name="EditIcon"></Icon>
19 + <Icon :name="EditIcon" />
20 </template>
21 Edit
22 </n-button>
23
24 <n-button type="error" :loading="deleting" secondary @click.stop="handleDelete">
25 <template #icon>
26 - <Icon :name="DeleteIcon"></Icon>
26 + <Icon :name="DeleteIcon" />
27 </template>
28 Delete
29 </n-button>
@@ -46,7 +46,7 @@
46 <div class="flex items-center justify-between gap-3">
47 <n-button secondary @click="switchMode('view')">
48 <template #icon>
49 - <Icon :name="BackIcon"></Icon>
49 + <Icon :name="BackIcon" />
50 </template>
51 Back
52 </n-button>
@@ -56,7 +56,7 @@
56
57 <n-button :loading="updating" type="success" :disabled="!isValid" @click="validate()">
58 <template #icon>
59 - <Icon :name="UpdateIcon"></Icon>
59 + <Icon :name="UpdateIcon" />
60 </template>
61 Submit
62 </n-button>
frontend/src/components/customers/integrations/CustomerIntegrationForm.vue
+3 -3
@@ -7,7 +7,7 @@
7 <n-step title="Choose Integration" />
8 <n-step title="Set Auth Keys">
9 <template #icon>
10 - <Icon v-if="!isAuthKeysStepEnabled" :name="SkipIcon"></Icon>
10 + <Icon v-if="!isAuthKeysStepEnabled" :name="SkipIcon" />
11 </template>
12 </n-step>
13 </n-steps>
@@ -55,13 +55,13 @@
55 <div class="flex gap-4">
56 <n-button v-if="isPrevStepEnabled" @click="prev()">
57 <template #icon>
58 - <Icon :name="ArrowLeftIcon"></Icon>
58 + <Icon :name="ArrowLeftIcon" />
59 </template>
60 Prev
61 </n-button>
62 <n-button v-if="isNextStepShown" :disabled="!isNextStepEnabled" icon-placement="right" @click="next()">
63 <template #icon>
64 - <Icon :name="ArrowRightIcon"></Icon>
64 + <Icon :name="ArrowRightIcon" />
65 </template>
66 Next
67 </n-button>
frontend/src/components/customers/integrations/CustomerIntegrationItem.vue
+2 -2
@@ -8,7 +8,7 @@
8 <template v-if="integration.deployed" #footerMain>
9 <Badge type="active">
10 <template #iconLeft>
11 - <Icon :name="DeployIcon" :size="13"></Icon>
11 + <Icon :name="DeployIcon" :size="13" />
12 </template>
13 <template #value>Deployed</template>
14 </Badge>
@@ -17,7 +17,7 @@
17 <div class="flex flex-wrap gap-3">
18 <n-button size="small" @click.stop="showDetails = true">
19 <template #icon>
20 - <Icon :name="DetailsIcon"></Icon>
20 + <Icon :name="DetailsIcon" />
21 </template>
22 Details
23 </n-button>
frontend/src/components/customers/integrations/CustomerIntegrations.vue
+1 -1
@@ -14,7 +14,7 @@
14 <div class="flex items-center justify-between gap-4 px-7 pt-2">
15 <n-button size="small" type="primary" @click="openForm()">
16 <template #icon>
17 - <Icon :name="AddIcon" :size="14"></Icon>
17 + <Icon :name="AddIcon" :size="14" />
18 </template>
19 Add Integration
20 </n-button>
frontend/src/components/customers/metadata/CustomerIntegrationMetaButton.vue
+1 -1
@@ -2,7 +2,7 @@
2 <div>
3 <n-button :size @click.stop="showDetails = true">
4 <template #icon>
5 - <Icon :name="MetaIcon"></Icon>
5 + <Icon :name="MetaIcon" />
6 </template>
7 Meta Details
8 </n-button>
frontend/src/components/customers/metadata/CustomerIntegrationMetaDetails.vue
+3 -3
@@ -8,7 +8,7 @@
8 <template #extraActions>
9 <n-button secondary @click="setViewMode()">
10 <template #icon>
11 - <Icon :name="BackIcon"></Icon>
11 + <Icon :name="BackIcon" />
12 </template>
13 Back
14 </n-button>
@@ -38,13 +38,13 @@
38 <div class="flex gap-3">
39 <n-button secondary :loading @click="loadMetaData()">
40 <template #icon>
41 - <Icon :name="RefreshIcon"></Icon>
41 + <Icon :name="RefreshIcon" />
42 </template>
43 Refresh
44 </n-button>
45 <n-button secondary type="primary" @click="setEditMode()">
46 <template #icon>
47 - <Icon :name="EditIcon"></Icon>
47 + <Icon :name="EditIcon" />
48 </template>
49 Edit
50 </n-button>
frontend/src/components/customers/metadata/CustomerIntegrationMetaForm.vue
+1 -1
@@ -34,7 +34,7 @@
34 </div>
35 <n-button type="primary" :loading="loading" @click="save()">
36 <template #icon>
37 - <Icon :name="SaveIcon"></Icon>
37 + <Icon :name="SaveIcon" />
38 </template>
39 Save Changes
40 </n-button>
frontend/src/components/customers/networkConnectors/CustomerNetworkConnectorActions.vue
+4 -4
@@ -9,7 +9,7 @@
9 @click.stop="decommissionNetworkConnector()"
10 >
11 <template #icon>
12 - <Icon :name="DecommissionIcon"></Icon>
12 + <Icon :name="DecommissionIcon" />
13 </template>
14 Decommission
15 </n-button>
@@ -23,7 +23,7 @@
23 @click.stop="showFortinetForm = true"
24 >
25 <template #icon>
26 - <Icon :name="DeployIcon"></Icon>
26 + <Icon :name="DeployIcon" />
27 </template>
28 Deploy
29 </n-button>
@@ -37,7 +37,7 @@
37 @click.stop="handleDelete"
38 >
39 <template #icon>
40 - <Icon :name="DeleteIcon" :size="15"></Icon>
40 + <Icon :name="DeleteIcon" :size="15" />
41 </template>
42 Delete
43 </n-button>
@@ -65,7 +65,7 @@
65 @click.stop="fortinetProvision()"
66 >
67 <template #icon>
68 - <Icon :name="DeployIcon"></Icon>
68 + <Icon :name="DeployIcon" />
69 </template>
70 Deploy
71 </n-button>
frontend/src/components/customers/networkConnectors/CustomerNetworkConnectorForm.vue
+3 -3
@@ -7,7 +7,7 @@
7 <n-step title="Choose Network Connector" />
8 <n-step title="Set Auth Keys">
9 <template #icon>
10 - <Icon v-if="!isAuthKeysStepEnabled" :name="SkipIcon"></Icon>
10 + <Icon v-if="!isAuthKeysStepEnabled" :name="SkipIcon" />
11 </template>
12 </n-step>
13 </n-steps>
@@ -55,13 +55,13 @@
55 <div class="flex gap-4">
56 <n-button v-if="isPrevStepEnabled" @click="prev()">
57 <template #icon>
58 - <Icon :name="ArrowLeftIcon"></Icon>
58 + <Icon :name="ArrowLeftIcon" />
59 </template>
60 Prev
61 </n-button>
62 <n-button v-if="isNextStepShown" :disabled="!isNextStepEnabled" icon-placement="right" @click="next()">
63 <template #icon>
64 - <Icon :name="ArrowRightIcon"></Icon>
64 + <Icon :name="ArrowRightIcon" />
65 </template>
66 Next
67 </n-button>
frontend/src/components/customers/networkConnectors/CustomerNetworkConnectorItem.vue
+2 -2
@@ -8,7 +8,7 @@
8 <template v-if="networkConnector.deployed" #footerMain>
9 <Badge type="active">
10 <template #iconLeft>
11 - <Icon :name="DeployIcon" :size="13"></Icon>
11 + <Icon :name="DeployIcon" :size="13" />
12 </template>
13 <template #value>Deployed</template>
14 </Badge>
@@ -17,7 +17,7 @@
17 <div class="flex flex-wrap gap-3">
18 <n-button size="small" @click.stop="showDetails = true">
19 <template #icon>
20 - <Icon :name="InfoIcon"></Icon>
20 + <Icon :name="InfoIcon" />
21 </template>
22 Details
23 </n-button>
frontend/src/components/customers/networkConnectors/CustomerNetworkConnectors.vue
+1 -1
@@ -14,7 +14,7 @@
14 <div class="flex items-center justify-between gap-4 px-7 pt-2">
15 <n-button size="small" type="primary" @click="openForm()">
16 <template #icon>
17 - <Icon :name="AddIcon" :size="14"></Icon>
17 + <Icon :name="AddIcon" :size="14" />
18 </template>
19 Add Network Connector
20 </n-button>
frontend/src/components/customers/notifications/CustomerNotificationsWorkflows.vue
+1 -1
@@ -33,7 +33,7 @@
33
34 <n-button size="small" type="primary" @click="openForm()">
35 <template #icon>
36 - <Icon :name="AddIcon" :size="14"></Icon>
36 + <Icon :name="AddIcon" :size="14" />
37 </template>
38 Create a Notification
39 </n-button>
frontend/src/components/customers/notifications/CustomerNotificationsWorkflowsItem.vue
+2 -7
@@ -7,13 +7,8 @@
7 <span :class="{ 'text-default': incidentNotification.enabled }">
8 {{ incidentNotification.enabled ? "Enabled" : "Disabled" }}
9 </span>
10 - <Icon
11 - v-if="incidentNotification.enabled"
12 - :name="EnabledIcon"
13 - :size="14"
14 - class="text-success"
15 - ></Icon>
16 - <Icon v-else :name="DisabledIcon" :size="14" class="text-secondary"></Icon>
10 + <Icon v-if="incidentNotification.enabled" :name="EnabledIcon" :size="14" class="text-success" />
11 + <Icon v-else :name="DisabledIcon" :size="14" class="text-secondary" />
12 </div>
13 </template>
14 <template #default>
frontend/src/components/customers/provision/CustomerDefaultSettingsButton.vue
+1 -1
@@ -1,7 +1,7 @@
1 <template>
2 <n-button size="small" secondary :loading="loading" @click="showForm = true">
3 <template #icon>
4 - <Icon :name="SettingsIcon" :size="14"></Icon>
4 + <Icon :name="SettingsIcon" :size="14" />
5 </template>
6 Default Settings
7 </n-button>
frontend/src/components/customers/provision/CustomerProvision.vue
+2 -2
@@ -16,7 +16,7 @@
16 <div v-if="customerMeta" class="flex items-center justify-end gap-4 px-7 pt-2">
17 <n-button size="small" type="error" ghost :loading="loadingDelete" @click="handleDelete">
18 <template #icon>
19 - <Icon :name="DeleteIcon" :size="15"></Icon>
19 + <Icon :name="DeleteIcon" :size="15" />
20 </template>
21 Decommission
22 </n-button>
@@ -24,7 +24,7 @@
24 <div v-else class="flex items-center justify-between gap-4 px-7 pt-2">
25 <n-button size="small" type="primary" @click="editing = true">
26 <template #icon>
27 - <Icon :name="AddIcon" :size="14"></Icon>
27 + <Icon :name="AddIcon" :size="14" />
28 </template>
29 Create Provision
30 </n-button>
frontend/src/components/customers/provision/CustomerProvisionWizard.vue
+4 -4
@@ -10,12 +10,12 @@
10 <n-step title="Subscription" />
11 <n-step title="Infrastructure">
12 <template #icon>
13 - <Icon v-if="!isInfrastructureEnabled" :name="SkipIcon"></Icon>
13 + <Icon v-if="!isInfrastructureEnabled" :name="SkipIcon" />
14 </template>
15 </n-step>
16 <n-step title="Wazuh Worker" :status="!isWazuhStepEnabled ? 'wait' : undefined">
17 <template #icon>
18 - <Icon v-if="!isWazuhStepEnabled" :name="SkipIcon"></Icon>
18 + <Icon v-if="!isWazuhStepEnabled" :name="SkipIcon" />
19 </template>
20 </n-step>
21 </n-steps>
@@ -248,13 +248,13 @@
248 <div class="flex gap-4">
249 <n-button v-if="isPrevStepEnabled" @click="prev()">
250 <template #icon>
251 - <Icon :name="ArrowLeftIcon"></Icon>
251 + <Icon :name="ArrowLeftIcon" />
252 </template>
253 Prev
254 </n-button>
255 <n-button v-if="isNextStepEnabled" icon-placement="right" @click="next()">
256 <template #icon>
257 - <Icon :name="ArrowRightIcon"></Icon>
257 + <Icon :name="ArrowRightIcon" />
258 </template>
259 Next
260 </n-button>
frontend/src/components/graylog/Alerts/Item.vue
+2 -2
@@ -5,7 +5,7 @@
5 <template #trigger>
6 <div class="hover:text-primary flex cursor-help items-center gap-2">
7 <span>#{{ alertsEvent.event.id }}</span>
8 - <Icon :name="InfoIcon" :size="16"></Icon>
8 + <Icon :name="InfoIcon" :size="16" />
9 </div>
10 </template>
11 <div class="flex flex-col gap-1">
@@ -54,7 +54,7 @@
54 <template #trigger>
55 <div class="hover:text-primary flex cursor-help items-center gap-2">
56 <span>{{ formatDateTime(alertsEvent.event.timestamp) }}</span>
57 - <Icon :name="TimeIcon" :size="16"></Icon>
57 + <Icon :name="TimeIcon" :size="16" />
58 </div>
59 </template>
60 <div class="flex flex-col px-1 py-2">
frontend/src/components/graylog/Alerts/List.vue
+2 -2
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
@@ -39,7 +39,7 @@
39 <div class="bg-default rounded-lg">
40 <n-button size="small">
41 <template #icon>
42 - <Icon :name="FilterIcon"></Icon>
42 + <Icon :name="FilterIcon" />
43 </template>
44 </n-button>
45 </div>
frontend/src/components/graylog/Events/List.vue
+1 -1
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
frontend/src/components/graylog/Inputs/Item.vue
+6 -6
@@ -3,7 +3,7 @@
3 <CardEntity hoverable :embedded>
4 <template #headerMain>
5 <div class="flex items-center gap-2">
6 - <Icon :name="UserIcon" :size="14"></Icon>
6 + <Icon :name="UserIcon" :size="14" />
7 {{ input.creator_user_id }}
8 </div>
9 </template>
@@ -20,7 +20,7 @@
20 <div class="flex flex-wrap items-center gap-3">
21 <Badge :type="input.global ? 'active' : 'muted'">
22 <template #iconRight>
23 - <Icon :name="input.global ? GlobalIcon : DisabledIcon" :size="14"></Icon>
23 + <Icon :name="input.global ? GlobalIcon : DisabledIcon" :size="14" />
24 </template>
25 <template #label>Global</template>
26 </Badge>
@@ -28,7 +28,7 @@
28 <template #trigger>
29 <Badge :type="isRunning ? 'active' : 'muted'" :hint-cursor="isRunning">
30 <template #iconRight>
31 - <Icon :name="isRunning ? TimeIcon : DisabledIcon" :size="14"></Icon>
31 + <Icon :name="isRunning ? TimeIcon : DisabledIcon" :size="14" />
32 </template>
33 <template #label>Running</template>
34 </Badge>
@@ -41,19 +41,19 @@
41 <div class="flex flex-wrap items-center justify-end gap-3">
42 <n-button size="small" @click.stop="showDetails = true">
43 <template #icon>
44 - <Icon :name="InfoIcon"></Icon>
44 + <Icon :name="InfoIcon" />
45 </template>
46 Details
47 </n-button>
48 <n-button v-if="isRunning" :loading="loading" type="warning" size="small" secondary @click="stop()">
49 <template #icon>
50 - <Icon :name="StopIcon"></Icon>
50 + <Icon :name="StopIcon" />
51 </template>
52 Stop input
53 </n-button>
54 <n-button v-else :loading="loading" type="success" secondary size="small" @click="start()">
55 <template #icon>
56 - <Icon :name="StartIcon"></Icon>
56 + <Icon :name="StartIcon" />
57 </template>
58 Start input
59 </n-button>
frontend/src/components/graylog/Inputs/List.vue
+1 -1
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
frontend/src/components/graylog/Messages/List.vue
+1 -1
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
frontend/src/components/graylog/Metrics/UncommittedEntries.vue
+2 -2
@@ -3,7 +3,7 @@
3 <div class="uncommitted-entries">
4 <div class="chart grow">
5 <div class="label flex items-center gap-3">
6 - <Icon v-if="isWarning" :name="DangerIcon"></Icon>
6 + <Icon v-if="isWarning" :name="DangerIcon" />
7 <span>Uncommitted Journal Entries</span>
8 </div>
9 <apexchart height="70" :options="options" :series="series"></apexchart>
@@ -15,7 +15,7 @@
15 <div class="footer">
16 <n-button type="primary" ghost icon-placement="right" @click="gotoGraylogManagement('messages')">
17 <template #icon>
18 - <Icon :name="LinkIcon" :size="14"></Icon>
18 + <Icon :name="LinkIcon" :size="14" />
19 </template>
20 See messages
21 </n-button>
frontend/src/components/graylog/MonitoringAlerts/CustomAlertButton.vue
+1 -1
@@ -1,7 +1,7 @@
1 <template>
2 <n-button size="small" type="primary" secondary :loading="loading" @click="showForm = true">
3 <div class="flex items-center gap-2">
4 - <Icon :name="DangerIcon" :size="18"></Icon>
4 + <Icon :name="DangerIcon" :size="18" />
5 <span class="xs:block hidden">Custom Alert</span>
6 </div>
7 </n-button>
frontend/src/components/graylog/MonitoringAlerts/CustomAlertForm.vue
+2 -2
@@ -96,7 +96,7 @@
96 <n-form-item size="small">
97 <n-button type="error" secondary @click="removeCustomFiled(cf.key)">
98 <template #icon>
99 - <Icon :name="RemoveIcon" :size="16"></Icon>
99 + <Icon :name="RemoveIcon" :size="16" />
100 </template>
101 </n-button>
102 </n-form-item>
@@ -105,7 +105,7 @@
105 <div class="mt-3">
106 <n-button @click="addCustomFiled()">
107 <template #icon>
108 - <Icon :name="AddIcon"></Icon>
108 + <Icon :name="AddIcon" />
109 </template>
110 Add Custom Field
111 </n-button>
frontend/src/components/graylog/MonitoringAlerts/Item.vue
+2 -2
@@ -6,7 +6,7 @@
6 <template #footerMain>
7 <Badge :type="isEnabled ? 'active' : 'muted'">
8 <template #iconRight>
9 - <Icon :name="isEnabled ? EnabledIcon : DisabledIcon" :size="13"></Icon>
9 + <Icon :name="isEnabled ? EnabledIcon : DisabledIcon" :size="13" />
10 </template>
11 <template #label>
12 <span class="whitespace-nowrap">
@@ -25,7 +25,7 @@
25 @click="openFormDialog()"
26 >
27 <template #icon>
28 - <Icon :name="EnableIcon"></Icon>
28 + <Icon :name="EnableIcon" />
29 </template>
30 Enable
31 </n-button>
frontend/src/components/graylog/MonitoringAlerts/List.vue
+1 -1
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
frontend/src/components/graylog/Pipelines/PipeDetails.vue
+1 -1
@@ -26,7 +26,7 @@
26 <template #trigger>
27 <n-button size="tiny">
28 <template #icon>
29 - <Icon :name="RulesIcon" :size="18"></Icon>
29 + <Icon :name="RulesIcon" :size="18" />
30 </template>
31 Rules
32 <span class="text-secondary ml-2 font-mono">{{ stage.rules.length }}</span>
frontend/src/components/graylog/Pipelines/PipeList.vue
+1 -1
@@ -16,7 +16,7 @@
16 <template #header-extra>
17 <n-button size="small" @click.stop="openModal(pipe)">
18 <template #icon>
19 - <Icon :name="InfoIcon"></Icon>
19 + <Icon :name="InfoIcon" />
20 </template>
21 </n-button>
22 </template>
frontend/src/components/graylog/Pipelines/PipeTitle.vue
+1 -1
@@ -2,7 +2,7 @@
2 <div class="flex items-center gap-1 leading-[1.1]" :class="{ 'text-warning': isWarning }">
3 <n-tooltip v-if="isWarning" placement="top-start" trigger="hover">
4 <template #trigger>
5 - <Icon :name="DangerIcon" :size="18"></Icon>
5 + <Icon :name="DangerIcon" :size="18" />
6 </template>
7 Open Info dialog to see errors
8 </n-tooltip>
frontend/src/components/graylog/Pipelines/Rule.vue
+1 -1
@@ -8,7 +8,7 @@
8 <div class="hover:text-primary flex cursor-help items-center gap-2">
9 {{ formatDateTime(rule.modified_at) }}
10
11 - <Icon :name="TimeIcon" :size="16"></Icon>
11 + <Icon :name="TimeIcon" :size="16" />
12 </div>
13 </template>
14 <div class="flex flex-col px-1 py-2">
frontend/src/components/graylog/Pipelines/RulesSmallList.vue
+1 -1
@@ -3,7 +3,7 @@
3 <n-button v-for="rule of rules" :key="rule.id" quaternary size="tiny" @click="emit('click', rule.id)">
4 <div class="btn-wrap flex items-center">
5 <span class="spacer">
6 - <Icon :name="ViewIcon" :size="16"></Icon>
6 + <Icon :name="ViewIcon" :size="16" />
7 </span>
8 <span class="title grow">
9 {{ rule.title }}
frontend/src/components/graylog/Streams/Item.vue
+7 -7
@@ -3,7 +3,7 @@
3 <CardEntity hoverable :highlighted="stream.is_default">
4 <template #headerMain>
5 <div class="flex items-center gap-2">
6 - <Icon :name="UserIcon" :size="14"></Icon>
6 + <Icon :name="UserIcon" :size="14" />
7 {{ stream.creator_user_id }}
8 </div>
9 </template>
@@ -21,19 +21,19 @@
21 <div class="flex flex-wrap items-center gap-3">
22 <Badge :type="stream.disabled ? 'muted' : 'active'">
23 <template #iconRight>
24 - <Icon :name="stream.disabled ? DisabledIcon : EnabledIcon" :size="14"></Icon>
24 + <Icon :name="stream.disabled ? DisabledIcon : EnabledIcon" :size="14" />
25 </template>
26 <template #label>Enabled</template>
27 </Badge>
28 <Badge :type="stream.is_default ? 'active' : 'muted'">
29 <template #iconRight>
30 - <Icon :name="stream.is_default ? EnabledIcon : DisabledIcon" :size="14"></Icon>
30 + <Icon :name="stream.is_default ? EnabledIcon : DisabledIcon" :size="14" />
31 </template>
32 <template #label>Default</template>
33 </Badge>
34 <Badge :type="stream.is_editable ? 'active' : 'muted'">
35 <template #iconRight>
36 - <Icon :name="stream.is_editable ? EnabledIcon : DisabledIcon" :size="14"></Icon>
36 + <Icon :name="stream.is_editable ? EnabledIcon : DisabledIcon" :size="14" />
37 </template>
38 <template #label>Editable</template>
39 </Badge>
@@ -43,7 +43,7 @@
43 <div class="flex flex-wrap items-center justify-end gap-3">
44 <n-button size="small" @click.stop="showDetails = true">
45 <template #icon>
46 - <Icon :name="InfoIcon"></Icon>
46 + <Icon :name="InfoIcon" />
47 </template>
48 Details
49 </n-button>
@@ -56,7 +56,7 @@
56 @click="stop()"
57 >
58 <template #icon>
59 - <Icon :name="StopIcon"></Icon>
59 + <Icon :name="StopIcon" />
60 </template>
61 Stop stream
62 </n-button>
@@ -69,7 +69,7 @@
69 @click="start()"
70 >
71 <template #icon>
72 - <Icon :name="StartIcon"></Icon>
72 + <Icon :name="StartIcon" />
73 </template>
74 Start stream
75 </n-button>
frontend/src/components/graylog/Streams/List.vue
+2 -2
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
@@ -38,7 +38,7 @@
38 <div class="bg-default rounded-lg">
39 <n-button size="small">
40 <template #icon>
41 - <Icon :name="FilterIcon"></Icon>
41 + <Icon :name="FilterIcon" />
42 </template>
43 </n-button>
44 </div>
frontend/src/components/healthcheck/HealthcheckList.vue
+1 -1
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
frontend/src/components/incidentManagement/alerts/AlertAsset.vue
+19
@@ -151,6 +151,19 @@
151 <AlertDetailTimeline :asset />
152 </div>
153 </n-tab-pane>
154 + <n-tab-pane name="File Collection" tab="File Collection" display-directive="show:lazy">
155 + <div class="p-7 pt-2">
156 + <FileCollectionForm v-if="asset.agent_id" :agent-id="asset.agent_id" />
157 +
158 + <n-empty v-else description="No agent associated with this asset" class="h-40" />
159 + </div>
160 + </n-tab-pane>
161 + <n-tab-pane name="Data Store" tab="Data Store" display-directive="show:lazy">
162 + <div class="p-7 pt-2">
163 + <AgentDataStoreTabCompact v-if="asset.agent_id" :agent-id="asset.agent_id" />
164 + <n-empty v-else description="No agent associated with this asset" class="h-40" />
165 + </div>
166 + </n-tab-pane>
167 </n-tabs>
168 </n-modal>
169 </div>
@@ -185,6 +198,12 @@ const ThreatIntelProcessEvaluationProvider = defineAsyncComponent(
198 const ArtifactsCollect = defineAsyncComponent(() => import("@/components/artifacts/ArtifactsCollect.vue"))
199 const CodeSource = defineAsyncComponent(() => import("@/components/common/CodeSource.vue"))
200 const LicenseFeatureCheck = defineAsyncComponent(() => import("@/components/license/LicenseFeatureCheck.vue"))
201 +const AgentDataStoreTabCompact = defineAsyncComponent(
202 + () => import("@/components/agents/dataStore/AgentDataStoreTabCompact.vue")
203 +)
204 +const FileCollectionForm = defineAsyncComponent(
205 + () => import("@/components/agents/fileCollection/FileCollectionForm.vue")
206 +)
207
208 const ViewIcon = "iconoir:eye-solid"
209 const LinkIcon = "carbon:launch"
frontend/src/components/incidentManagement/alerts/AlertComment.vue
+4 -4
@@ -33,7 +33,7 @@
33 <template v-if="mode === 'view'">
34 <n-button size="tiny" secondary :disabled="canceling" @click="editComment()">
35 <template #icon>
36 - <Icon :name="EditIcon" :size="12"></Icon>
36 + <Icon :name="EditIcon" :size="12" />
37 </template>
38 <span>Edit</span>
39 </n-button>
@@ -41,7 +41,7 @@
41 <template #trigger>
42 <n-button size="tiny" secondary type="error" :loading="canceling">
43 <template #icon>
44 - <Icon :name="DeleteIcon" :size="12"></Icon>
44 + <Icon :name="DeleteIcon" :size="12" />
45 </template>
46 <span>Delete</span>
47 </n-button>
@@ -52,7 +52,7 @@
52 <template v-if="mode === 'edit'">
53 <n-button size="tiny" secondary :disabled="saving" @click="setMode('view')">
54 <template #icon>
55 - <Icon :name="ArrowLeftIcon" :size="12"></Icon>
55 + <Icon :name="ArrowLeftIcon" :size="12" />
56 </template>
57 <span>Cancel</span>
58 </n-button>
@@ -66,7 +66,7 @@
66 @click="updateAlertComment()"
67 >
68 <template #icon>
69 - <Icon :name="SaveIcon" :size="13"></Icon>
69 + <Icon :name="SaveIcon" :size="13" />
70 </template>
71 <span>Save</span>
72 </n-button>
frontend/src/components/incidentManagement/alerts/AlertItem.vue
+2 -2
@@ -20,7 +20,7 @@
20 @click="compact ? undefined : openDetails()"
21 >
22 <span>#{{ alert.id }} - {{ alert.source }}</span>
23 - <Icon v-if="!compact" :name="InfoIcon" :size="16"></Icon>
23 + <Icon v-if="!compact" :name="InfoIcon" :size="16" />
24 </div>
25 </div>
26 </template>
@@ -40,7 +40,7 @@
40 <span v-if="alert.alert_creation_time">
41 {{ formatDate(alert.alert_creation_time, dFormats.datetime) }}
42 </span>
43 - <Icon :name="TimeIcon" :size="16"></Icon>
43 + <Icon :name="TimeIcon" :size="16" />
44 </div>
45 </template>
46 <div class="flex flex-col px-1 py-2">
frontend/src/components/incidentManagement/alerts/AlertsList.vue
+2 -2
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
@@ -100,7 +100,7 @@
100 <n-badge v-if="showFilters" :show="filtered" dot type="success" :offset="[-4, 0]">
101 <n-button size="small" secondary @click="showFiltersView = !showFiltersView">
102 <template #icon>
103 - <Icon :name="FilterIcon"></Icon>
103 + <Icon :name="FilterIcon" />
104 </template>
105 </n-button>
106 </n-badge>
frontend/src/components/incidentManagement/cases/CaseComment.vue
+4 -4
@@ -33,7 +33,7 @@
33 <template v-if="mode === 'view'">
34 <n-button size="tiny" secondary :disabled="canceling" @click="editComment()">
35 <template #icon>
36 - <Icon :name="EditIcon" :size="12"></Icon>
36 + <Icon :name="EditIcon" :size="12" />
37 </template>
38 <span>Edit</span>
39 </n-button>
@@ -41,7 +41,7 @@
41 <template #trigger>
42 <n-button size="tiny" secondary type="error" :loading="canceling">
43 <template #icon>
44 - <Icon :name="DeleteIcon" :size="12"></Icon>
44 + <Icon :name="DeleteIcon" :size="12" />
45 </template>
46 <span>Delete</span>
47 </n-button>
@@ -52,7 +52,7 @@
52 <template v-if="mode === 'edit'">
53 <n-button size="tiny" secondary :disabled="saving" @click="setMode('view')">
54 <template #icon>
55 - <Icon :name="ArrowLeftIcon" :size="12"></Icon>
55 + <Icon :name="ArrowLeftIcon" :size="12" />
56 </template>
57 <span>Cancel</span>
58 </n-button>
@@ -66,7 +66,7 @@
66 @click="updateCaseComment()"
67 >
68 <template #icon>
69 - <Icon :name="SaveIcon" :size="13"></Icon>
69 + <Icon :name="SaveIcon" :size="13" />
70 </template>
71 <span>Save</span>
72 </n-button>
frontend/src/components/incidentManagement/cases/CaseCreationButton.vue
+1 -1
@@ -1,7 +1,7 @@
1 <template>
2 <n-button :size type="primary" :loading="loading" @click="showForm = true">
3 <template v-if="showIcon" #icon>
4 - <Icon :name="NewCaseIcon" :size="14"></Icon>
4 + <Icon :name="NewCaseIcon" :size="14" />
5 </template>
6 <span>Create Case</span>
7 </n-button>
frontend/src/components/incidentManagement/cases/CaseDataStore.vue
+1 -1
@@ -15,7 +15,7 @@
15 <n-upload v-model:file-list="fileList" :max="1" :disabled="uploading">
16 <n-upload-dragger>
17 <div>
18 - <Icon :name="UploadIcon" :size="28" :depth="3"></Icon>
18 + <Icon :name="UploadIcon" :size="28" :depth="3" />
19 </div>
20 <div class="font-semibold">Click or drag a file to this area to upload</div>
21 </n-upload-dragger>
frontend/src/components/incidentManagement/cases/CaseItem.vue
+1 -1
@@ -15,7 +15,7 @@
15 @click="compact ? undefined : openDetails()"
16 >
17 <span>#{{ caseEntity.id }}</span>
18 - <Icon v-if="!compact" :name="InfoIcon" :size="16"></Icon>
18 + <Icon v-if="!compact" :name="InfoIcon" :size="16" />
19 </div>
20 </template>
21 <template v-if="caseEntity?.case_creation_time" #headerExtra>
frontend/src/components/incidentManagement/cases/CaseReportTemplateManager.vue
+1 -1
@@ -33,7 +33,7 @@
33 >
34 <n-upload-dragger>
35 <div>
36 - <Icon :name="UploadIcon" :size="28" :depth="3"></Icon>
36 + <Icon :name="UploadIcon" :size="28" :depth="3" />
37 </div>
38 <div class="font-semibold">Click or drag a file to this area to upload</div>
39 <p class="mt-2">
frontend/src/components/incidentManagement/cases/CasesList.vue
+3 -3
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
@@ -41,7 +41,7 @@
41 <div class="bg-default rounded-lg">
42 <n-button size="small" class="!cursor-pointer">
43 <template #icon>
44 - <Icon :name="MenuIcon"></Icon>
44 + <Icon :name="MenuIcon" />
45 </template>
46 </n-button>
47 </div>
@@ -76,7 +76,7 @@
76 <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
77 <n-button size="small" @click="showFilters = true">
78 <template #icon>
79 - <Icon :name="FilterIcon"></Icon>
79 + <Icon :name="FilterIcon" />
80 </template>
81 </n-button>
82 </n-badge>
frontend/src/components/incidentManagement/exclusionRules/ExclusionRuleItem.vue
+3 -3
@@ -61,7 +61,7 @@
61
62 <n-button size="small" @click.stop="openDetails()">
63 <template #icon>
64 - <Icon :name="DetailsIcon"></Icon>
64 + <Icon :name="DetailsIcon" />
65 </template>
66 Details
67 </n-button>
@@ -96,13 +96,13 @@
96 <div v-if="!editing" class="flex items-center justify-end gap-4">
97 <n-button text type="error" ghost :loading="loadingDelete" @click="handleDelete">
98 <template #icon>
99 - <Icon :name="DeleteIcon" :size="15"></Icon>
99 + <Icon :name="DeleteIcon" :size="15" />
100 </template>
101 Delete
102 </n-button>
103 <n-button :disabled="loadingDelete" @click="editing = true">
104 <template #icon>
105 - <Icon :name="EditIcon" :size="14"></Icon>
105 + <Icon :name="EditIcon" :size="14" />
106 </template>
107 Edit
108 </n-button>
frontend/src/components/incidentManagement/exclusionRules/ExclusionRulesList.vue
+2 -2
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
@@ -37,7 +37,7 @@
37 <n-badge :show="filters.enabledOnly" dot type="success" :offset="[-3, 4]">
38 <n-button size="small">
39 <template #icon>
40 - <Icon :name="FilterIcon"></Icon>
40 + <Icon :name="FilterIcon" />
41 </template>
42 </n-button>
43 </n-badge>
frontend/src/components/incidentManagement/exclusionRules/NewExclusionRuleButton.vue
+1 -1
@@ -2,7 +2,7 @@
2 <div>
3 <n-button size="small" type="primary" @click="showForm = true">
4 <template #icon>
5 - <Icon :name="NewNewExclusionRuleIcon" :size="16"></Icon>
5 + <Icon :name="NewNewExclusionRuleIcon" :size="16" />
6 </template>
7 {{ hideButtonExtendedLabel ? "Create" : " Create Exclusion Rule" }}
8 </n-button>
frontend/src/components/incidentManagement/sources/ConfiguredSourcesList.vue
+1 -1
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
frontend/src/components/incidentManagement/sources/NewConfiguredSourceButton.vue
+1 -1
@@ -2,7 +2,7 @@
2 <div>
3 <n-button size="small" type="primary" @click="showWizard = true">
4 <template #icon>
5 - <Icon :name="NewSourceConfigurationIcon" :size="15"></Icon>
5 + <Icon :name="NewSourceConfigurationIcon" :size="15" />
6 </template>
7 Create Source Configuration
8 </n-button>
frontend/src/components/incidentManagement/sources/SourceConfigurationDetails.vue
+2 -2
@@ -5,7 +5,7 @@
5 <div v-if="!editing && !loading" class="mt-4 flex items-center justify-end gap-2">
6 <n-button @click="setEditMode()">
7 <template #icon>
8 - <Icon :name="EditIcon" :size="16"></Icon>
8 + <Icon :name="EditIcon" :size="16" />
9 </template>
10 Edit
11 </n-button>
@@ -22,7 +22,7 @@
22 <template #additionalActions>
23 <n-button @click="setViewMode()">
24 <template #icon>
25 - <Icon :name="ArrowIcon" :size="16"></Icon>
25 + <Icon :name="ArrowIcon" :size="16" />
26 </template>
27 Cancel
28 </n-button>
frontend/src/components/incidentManagement/sources/SourceConfigurationWizard.vue
+2 -2
@@ -34,7 +34,7 @@
34 @click="standardMode()"
35 >
36 <template #icon>
37 - <Icon :name="ArrowRightIcon"></Icon>
37 + <Icon :name="ArrowRightIcon" />
38 </template>
39 Next
40 </n-button>
@@ -57,7 +57,7 @@
57 <template #additionalActions>
58 <n-button v-if="isPrevStepEnabled" :disabled="submitting" @click="prev()">
59 <template #icon>
60 - <Icon :name="ArrowLeftIcon"></Icon>
60 + <Icon :name="ArrowLeftIcon" />
61 </template>
62 Prev
63 </n-button>
frontend/src/components/indices/IndexCard.vue
+1 -1
@@ -48,7 +48,7 @@
48 <template #trigger>
49 <n-button quaternary circle type="error" @click.stop="handleDelete">
50 <template #icon>
51 - <Icon :name="DeleteIcon"></Icon>
51 + <Icon :name="DeleteIcon" />
52 </template>
53 </n-button>
54 </template>
frontend/src/components/indices/IndexIcon.vue
+3 -3
@@ -1,8 +1,8 @@
1 <template>
2 <span class="index-icon" :class="[`health-${health}`, { color }]">
3 - <Icon v-if="health === IndexHealth.GREEN" :name="ShieldIcon" :size="18"></Icon>
4 - <Icon v-if="health === IndexHealth.YELLOW" :name="WarningIcon" :size="18"></Icon>
5 - <Icon v-if="health === IndexHealth.RED" :name="DangerIcon" :size="18"></Icon>
3 + <Icon v-if="health === IndexHealth.GREEN" :name="ShieldIcon" :size="18" />
4 + <Icon v-if="health === IndexHealth.YELLOW" :name="WarningIcon" :size="18" />
5 + <Icon v-if="health === IndexHealth.RED" :name="DangerIcon" :size="18" />
6 </span>
7 </template>
8
frontend/src/components/indices/UnhealthyIndices.vue
+1 -1
@@ -23,7 +23,7 @@
23 </template>
24 <n-empty v-else description="No Unhealthy Indices found">
25 <template #icon>
26 - <Icon :name="ShieldIcon"></Icon>
26 + <Icon :name="ShieldIcon" />
27 </template>
28 <template #extra>Great, all indices are healthy!</template>
29 </n-empty>
frontend/src/components/license/LicenseCheckoutResponse.vue
+2 -2
@@ -15,13 +15,13 @@
15 </h4>
16 </template>
17 <template v-if="type === 'error'">
18 - <Icon :name="ErrorIcon" class="text-error" :size="100"></Icon>
18 + <Icon :name="ErrorIcon" class="text-error" :size="100" />
19 <h1 class="text-center">Checkout canceled</h1>
20 </template>
21 <div>
22 <n-button @click="gotoLicense()">
23 <template #icon>
24 - <Icon :name="LicenseIcon"></Icon>
24 + <Icon :name="LicenseIcon" />
25 </template>
26 View license
27 </n-button>
frontend/src/components/license/LicenseCheckoutWizard.vue
+4 -4
@@ -2,7 +2,7 @@
2 <n-spin :show="loading" class="min-h-48">
3 <n-empty v-if="errorMessage" :description="errorMessage" class="h-48 justify-center">
4 <template #icon>
5 - <Icon :name="WarningIcon"></Icon>
5 + <Icon :name="WarningIcon" />
6 </template>
7 </n-empty>
8 <template v-else>
@@ -25,7 +25,7 @@
25 class="h-48 justify-center"
26 >
27 <template #icon>
28 - <Icon :name="CheckIcon"></Icon>
28 + <Icon :name="CheckIcon" />
29 </template>
30 </n-empty>
31 </template>
@@ -53,7 +53,7 @@
53 <div class="flex justify-end gap-4">
54 <n-button quaternary @click="selectedSubscription = null">
55 <template #icon>
56 - <Icon :name="ArrowLeftIcon"></Icon>
56 + <Icon :name="ArrowLeftIcon" />
57 </template>
58 Back
59 </n-button>
@@ -64,7 +64,7 @@
64 @click="createCheckoutSession()"
65 >
66 <template #icon>
67 - <Icon :name="CartIcon"></Icon>
67 + <Icon :name="CartIcon" />
68 </template>
69 Checkout
70 </n-button>
frontend/src/components/license/LicenseDetails.vue
+6 -6
@@ -4,7 +4,7 @@
4 <CardKV v-if="!hideKey">
5 <template #key>
6 <span class="flex items-center gap-3">
7 - <Icon :name="KeyIcon" :size="14"></Icon>
7 + <Icon :name="KeyIcon" :size="14" />
8 <span>Key</span>
9 </span>
10 </template>
@@ -15,7 +15,7 @@
15 <CardKV v-if="!hideFeatures">
16 <template #key>
17 <span class="flex items-center gap-3">
18 - <Icon :name="FeaturesIcon" :size="14"></Icon>
18 + <Icon :name="FeaturesIcon" :size="14" />
19 <span>Features</span>
20 </span>
21 </template>
@@ -24,7 +24,7 @@
24 <CardKV v-for="feature of features" :key="feature">
25 <template #value>
26 <span class="flex items-center gap-3">
27 - <Icon :name="CheckIcon" :size="14" class="text-primary"></Icon>
27 + <Icon :name="CheckIcon" :size="14" class="text-primary" />
28 <span>{{ feature }}</span>
29 </span>
30 </template>
@@ -36,7 +36,7 @@
36 <CardKV>
37 <template #key>
38 <span class="flex items-center gap-3">
39 - <Icon :name="ExpiresIcon" :size="14"></Icon>
39 + <Icon :name="ExpiresIcon" :size="14" />
40 <span>Expires</span>
41 </span>
42 </template>
@@ -48,7 +48,7 @@
48 <CardKV>
49 <template #key>
50 <span class="flex items-center gap-3">
51 - <Icon :name="CustomerIcon" :size="14"></Icon>
51 + <Icon :name="CustomerIcon" :size="14" />
52 <span>Customer</span>
53 </span>
54 </template>
@@ -79,7 +79,7 @@
79 <CardKV v-if="dockerCompose">
80 <template #key>
81 <span class="flex items-center gap-3">
82 - <Icon :name="ConfigIcon" :size="14"></Icon>
82 + <Icon :name="ConfigIcon" :size="14" />
83 <span>Docker Configuration</span>
84 </span>
85 </template>
frontend/src/components/license/LicenseFeatureCheck.vue
+3 -3
@@ -16,7 +16,7 @@
16 <div class="flex justify-end">
17 <n-button @click="gotoLicense()">
18 <template #icon>
19 - <Icon :name="LicenseIcon"></Icon>
19 + <Icon :name="LicenseIcon" />
20 </template>
21 View license
22 </n-button>
@@ -40,7 +40,7 @@
40 <div class="flex justify-end">
41 <n-button @click="gotoLicense()">
42 <template #icon>
43 - <Icon :name="LicenseIcon"></Icon>
43 + <Icon :name="LicenseIcon" />
44 </template>
45 View license
46 </n-button>
@@ -64,7 +64,7 @@
64 <div class="flex justify-end">
65 <n-button @click="gotoLicense()">
66 <template #icon>
67 - <Icon :name="LicenseIcon"></Icon>
67 + <Icon :name="LicenseIcon" />
68 </template>
69 View license
70 </n-button>
frontend/src/components/license/LicenseFeatures.vue
+4 -4
@@ -9,7 +9,7 @@
9 </h3>
10 <n-popover v-if="features.length" class="max-w-80" trigger="hover">
11 <template #trigger>
12 - <Icon :name="InfoIcon" :size="20" class="cursor-help"></Icon>
12 + <Icon :name="InfoIcon" :size="20" class="cursor-help" />
13 </template>
14 To unsubscribe, click on the feature you wish to remove, and then click on the "Unsubscribe"
15 button.
@@ -31,7 +31,7 @@
31 </template>
32 <n-empty v-else description="No features unlocked" class="h-48 justify-center">
33 <template #icon>
34 - <Icon :name="NoFeaturesIcon"></Icon>
34 + <Icon :name="NoFeaturesIcon" />
35 </template>
36 </n-empty>
37 </div>
@@ -41,7 +41,7 @@
41 <div v-if="license" class="cta-section">
42 <n-button type="primary" class="!w-full" size="large" @click="openCheckout()">
43 <template #icon>
44 - <Icon :name="ExtendIcon"></Icon>
44 + <Icon :name="ExtendIcon" />
45 </template>
46 Add feature
47 </n-button>
@@ -55,7 +55,7 @@
55 <span class="cursor-pointer" @click="showLicenseDetails = true">
56 Your license:
57 <strong>{{ license }}</strong>
58 - <Icon :name="InfoIcon" :size="14" class="relative top-0.5 ml-1"></Icon>
58 + <Icon :name="InfoIcon" :size="14" class="relative top-0.5 ml-1" />
59 </span>
60 </template>
61 <template v-else>
frontend/src/components/license/LicenseLoadForm.vue
+1 -1
@@ -4,7 +4,7 @@
4 <div class="flex justify-end">
5 <n-button type="success" :loading="loadingReplace" :disabled="!licenseKey" @click="replaceLicense()">
6 <template #icon>
7 - <Icon :name="LicenseIcon"></Icon>
7 + <Icon :name="LicenseIcon" />
8 </template>
9 Load License
10 </n-button>
frontend/src/components/license/SubscriptionCard.vue
+2 -2
@@ -38,7 +38,7 @@
38 <template v-if="selectable" #footerExtra>
39 <n-button size="small" @click.stop="showDetails = true">
40 <template #icon>
41 - <Icon :name="InfoIcon"></Icon>
41 + <Icon :name="InfoIcon" />
42 </template>
43 Details
44 </n-button>
@@ -69,7 +69,7 @@
69 <template #trigger>
70 <n-button text size="small" class="opacity-50">
71 <template #icon>
72 - <Icon :name="DeleteIcon" :size="16"></Icon>
72 + <Icon :name="DeleteIcon" :size="16" />
73 </template>
74 Unsubscribe
75 </n-button>
frontend/src/components/license/deprecated/LicenseCheckout.vue
+2 -2
@@ -3,7 +3,7 @@
3 <div class="flex flex-col gap-1">
4 <p class="flex items-center gap-4">
5 <span>license</span>
6 - <Icon v-if="loadingLicense" :name="LoadingIcon"></Icon>
6 + <Icon v-if="loadingLicense" :name="LoadingIcon" />
7 </p>
8
9 <h3 v-if="!loadingLicense">
@@ -14,7 +14,7 @@
14 <div v-if="!loadingLicense" class="actions-box mr-2 flex gap-2">
15 <n-button type="primary" @click="openCheckout()">
16 <template #icon>
17 - <Icon :name="licenseKey ? ExtendIcon : LicenseIcon"></Icon>
17 + <Icon :name="licenseKey ? ExtendIcon : LicenseIcon" />
18 </template>
19 {{ licenseKey ? "Extend license" : "Create new license" }}
20 </n-button>
frontend/src/components/license/deprecated/LicenseEditor.vue
+7 -7
@@ -3,7 +3,7 @@
3 <div class="license-box" :class="{ loading: loadingLicense && !licenseKey }">
4 <p v-if="loadingLicense || licenseKey" class="flex items-center gap-4">
5 <span>your license:</span>
6 - <Icon v-if="loadingLicense" :name="LoadingIcon"></Icon>
6 + <Icon v-if="loadingLicense" :name="LoadingIcon" />
7 </p>
8 <div v-if="!loadingLicense && !licenseKey">
9 <p v-if="!creationEnabled" class="flex items-center gap-4">no license found</p>
@@ -17,7 +17,7 @@
17 @click="enableCreation()"
18 >
19 <template #icon>
20 - <Icon :name="LicenseIcon"></Icon>
20 + <Icon :name="LicenseIcon" />
21 </template>
22 Create new license
23 </n-button>
@@ -29,7 +29,7 @@
29 <div class="actions-box flex gap-2">
30 <n-button secondary :disabled="replaceEnabled" size="small" @click="enableReplace()">
31 <template #icon>
32 - <Icon :name="EditIcon"></Icon>
32 + <Icon :name="EditIcon" />
33 </template>
34 Edit
35 </n-button>
@@ -41,7 +41,7 @@
41 @click="enableExtend()"
42 >
43 <template #icon>
44 - <Icon :name="ExtendIcon"></Icon>
44 + <Icon :name="ExtendIcon" />
45 </template>
46 Extend
47 </n-button>
@@ -54,7 +54,7 @@
54 <n-button secondary :disabled="loadingReplace" @click="resetLicense()">Reset</n-button>
55 <n-button type="success" :loading="loadingReplace" :disabled="!licenseKeyModel" @click="replaceLicense()">
56 <template #icon>
57 - <Icon :name="EditIcon"></Icon>
57 + <Icon :name="EditIcon" />
58 </template>
59 Replace
60 </n-button>
@@ -69,7 +69,7 @@
69 <n-button secondary :disabled="loadingExtend" @click="resetPeriod()">Reset</n-button>
70 <n-button type="success" :loading="loadingExtend" :disabled="!period" @click="extendLicense()">
71 <template #icon>
72 - <Icon :name="ExtendIcon"></Icon>
72 + <Icon :name="ExtendIcon" />
73 </template>
74 Extend
75 </n-button>
@@ -102,7 +102,7 @@
102 @click="validateCreation()"
103 >
104 <template #icon>
105 - <Icon :name="LicenseIcon"></Icon>
105 + <Icon :name="LicenseIcon" />
106 </template>
107 Create License
108 </n-button>
frontend/src/components/logs/LogItem.vue
+2 -2
@@ -33,7 +33,7 @@
33 <div class="flex flex-wrap items-center gap-3">
34 <Badge type="splitted" :color="eventTypeLower === LogEventType.ERROR ? 'danger' : 'primary'">
35 <template #iconLeft>
36 - <Icon :name="eventTypeLower === LogEventType.ERROR ? ErrorIcon : InfoIcon" :size="14"></Icon>
36 + <Icon :name="eventTypeLower === LogEventType.ERROR ? ErrorIcon : InfoIcon" :size="14" />
37 </template>
38 <template #label>Type</template>
39 <template #value>
@@ -42,7 +42,7 @@
42 </Badge>
43 <Badge v-if="log.user_id" type="splitted" color="primary">
44 <template #iconLeft>
45 - <Icon :name="UserIcon" :size="14"></Icon>
45 + <Icon :name="UserIcon" :size="14" />
46 </template>
47 <template #value>
48 <span class="flex items-center gap-2">
frontend/src/components/logs/LogsList.vue
+3 -3
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
@@ -30,7 +30,7 @@
30
31 <n-button size="small" type="error" ghost :loading="loadingPurge" @click="showPurgeConfirm = true">
32 <div class="flex items-center gap-2">
33 - <Icon :name="TrashIcon" :size="16"></Icon>
33 + <Icon :name="TrashIcon" :size="16" />
34 <span class="xs:block hidden">Purge</span>
35 </div>
36 </n-button>
@@ -50,7 +50,7 @@
50 <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
51 <n-button size="small" @click="showFilters = true">
52 <template #icon>
53 - <Icon :name="FilterIcon"></Icon>
53 + <Icon :name="FilterIcon" />
54 </template>
55 </n-button>
56 </n-badge>
frontend/src/components/mitre/AtomicTests/List.vue
+1 -1
@@ -8,7 +8,7 @@
8 <div class="bg-default rounded-lg">
9 <n-button size="small" class="!cursor-help">
10 <template #icon>
11 - <Icon :name="InfoIcon"></Icon>
11 + <Icon :name="InfoIcon" />
12 </template>
13 </n-button>
14 </div>
frontend/src/components/mitre/AttackSimulator/SimulatorWizard.vue
+3 -3
@@ -117,20 +117,20 @@
117 <div class="flex items-center gap-4">
118 <n-button v-if="isPrevStepEnabled" :disabled="loading" @click="prev()">
119 <template #icon>
120 - <Icon :name="ArrowLeftIcon"></Icon>
120 + <Icon :name="ArrowLeftIcon" />
121 </template>
122 Prev
123 </n-button>
124 <n-button v-if="isNextStepEnabled" icon-placement="right" :disabled="loading" @click="next()">
125 <template #icon>
126 - <Icon :name="ArrowRightIcon"></Icon>
126 + <Icon :name="ArrowRightIcon" />
127 </template>
128 Next
129 </n-button>
130 </div>
131 <n-button v-if="isSubmitEnabled" type="primary" :disabled="!isSubmitValid" :loading @click="submit()">
132 <template #icon>
133 - <Icon :name="AttackIcon"></Icon>
133 + <Icon :name="AttackIcon" />
134 </template>
135 Simulate attack
136 </n-button>
frontend/src/components/mitre/TechniqueEvents/List.vue
+1 -1
@@ -10,7 +10,7 @@
10 <div class="bg-default rounded-lg">
11 <n-button size="small" class="!cursor-help">
12 <template #icon>
13 - <Icon :name="InfoIcon"></Icon>
13 + <Icon :name="InfoIcon" />
14 </template>
15 </n-button>
16 </div>
frontend/src/components/monitoringAlerts/Item.vue
+2 -2
@@ -8,14 +8,14 @@
8 <div class="flex flex-wrap items-center gap-3">
9 <Badge type="active" class="cursor-pointer" @click.stop="gotoIndex(alert.alert_index)">
10 <template #iconRight>
11 - <Icon :name="LinkIcon" :size="14"></Icon>
11 + <Icon :name="LinkIcon" :size="14" />
12 </template>
13 <template #label>Index / {{ alert.alert_index }}</template>
14 </Badge>
15
16 <Badge type="active" class="cursor-pointer" @click.stop="gotoCustomer({ code: alert.customer_code })">
17 <template #iconRight>
18 - <Icon :name="LinkIcon" :size="14"></Icon>
18 + <Icon :name="LinkIcon" :size="14" />
19 </template>
20 <template #label>Customer #{{ alert.customer_code }}</template>
21 </Badge>
frontend/src/components/monitoringAlerts/ItemActions.vue
+2 -2
@@ -2,13 +2,13 @@
2 <div class="contents">
3 <n-button :loading="loadingInvoke" type="success" secondary :size="size" @click.stop="invoke()">
4 <template #icon>
5 - <Icon :name="InvokeIcon"></Icon>
5 + <Icon :name="InvokeIcon" />
6 </template>
7 Invoke
8 </n-button>
9 <n-button :loading="loadingDelete" :size="size" type="error" secondary @click.stop="handleDelete()">
10 <template #icon>
11 - <Icon :name="DeleteIcon"></Icon>
11 + <Icon :name="DeleteIcon" />
12 </template>
13 Delete
14 </n-button>
frontend/src/components/monitoringAlerts/List.vue
+2 -2
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
@@ -29,7 +29,7 @@
29 @click="handlePurge()"
30 >
31 <div class="flex items-center gap-2">
32 - <Icon :name="TrashIcon" :size="16"></Icon>
32 + <Icon :name="TrashIcon" :size="16" />
33 <span class="xs:block hidden">Purge</span>
34 </div>
35 </n-button>
frontend/src/components/reportCreation/Panels.vue
+7 -7
@@ -34,14 +34,14 @@
34 <template #header>
35 <div class="left-box flex justify-end">
36 <div class="pan-area">
37 - <Icon :name="PanIcon" :size="20"></Icon>
37 + <Icon :name="PanIcon" :size="20" />
38 </div>
39 <div class="delete-box">
40 <n-tooltip trigger="hover">
41 <template #trigger>
42 <n-button text type="error" @click="removeRow(row)">
43 <template #icon>
44 - <Icon :name="CloseIcon"></Icon>
44 + <Icon :name="CloseIcon" />
45 </template>
46 </n-button>
47 </template>
@@ -53,7 +53,7 @@
53 <template #trigger>
54 <n-button text>
55 <template #icon>
56 - <Icon :name="RowSettingsIcon" :size="13"></Icon>
56 + <Icon :name="RowSettingsIcon" :size="13" />
57 </template>
58 </n-button>
59 </template>
@@ -89,7 +89,7 @@
89 @click="removePanel(row, panel)"
90 >
91 <template #icon>
92 - <Icon :name="CloseIcon"></Icon>
92 + <Icon :name="CloseIcon" />
93 </template>
94 </n-button>
95 </template>
@@ -118,7 +118,7 @@
118 @click="addRow()"
119 >
120 <template #icon>
121 - <Icon :name="AddIcon"></Icon>
121 + <Icon :name="AddIcon" />
122 </template>
123 <span>Add row</span>
124 </n-button>
@@ -126,13 +126,13 @@
126 <div v-if="panelsReady" class="flex items-center gap-2">
127 <n-button type="success" :loading="loading" @click="print()">
128 <template #icon>
129 - <Icon :name="PrintIcon"></Icon>
129 + <Icon :name="PrintIcon" />
130 </template>
131 Print Report
132 </n-button>
133 <n-button type="success" :loading="loading" @click="openSettings()">
134 <template #icon>
135 - <Icon :name="SettingsIcon"></Icon>
135 + <Icon :name="SettingsIcon" />
136 </template>
137 </n-button>
138 </div>
frontend/src/components/reportCreation/PrintSettings.vue
+1 -1
@@ -7,7 +7,7 @@
7 <ImageCropper v-slot="{ openCropper }" placeholder="Select a Logo" @crop="setCroppedImage">
8 <n-button @click="openCropper()">
9 <template #icon>
10 - <Icon :name="EditIcon"></Icon>
10 + <Icon :name="EditIcon" />
11 </template>
12 Edit Logo Image
13 </n-button>
frontend/src/components/sca/List.vue
+1 -1
@@ -25,7 +25,7 @@
25 <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
26 <n-button size="small" secondary @click="showFiltersView = !showFiltersView">
27 <template #icon>
28 - <Icon :name="FilterIcon"></Icon>
28 + <Icon :name="FilterIcon" />
29 </template>
30 </n-button>
31 </n-badge>
frontend/src/components/scheduler/Item.vue
+1 -1
@@ -6,7 +6,7 @@
6 <template #trigger>
7 <div class="flex items-center gap-2">
8 {{ formatDate(job.last_success, dFormats.datetimesec) }}
9 - <Icon :name="TimeIcon"></Icon>
9 + <Icon :name="TimeIcon" />
10 </div>
11 </template>
12 Last success time
frontend/src/components/scheduler/JobActions.vue
+3 -3
@@ -10,7 +10,7 @@
10 @click="toggleState()"
11 >
12 <template #icon>
13 - <Icon :name="job.enabled ? PauseIcon : StartIcon"></Icon>
13 + <Icon :name="job.enabled ? PauseIcon : StartIcon" />
14 </template>
15 {{ job.enabled ? "Pause" : "Start" }}
16 </n-button>
@@ -18,13 +18,13 @@
18 <div class="flex items-center gap-3">
19 <n-button :size type="success" secondary :loading="loadingRun" @click="run()">
20 <template #icon>
21 - <Icon :name="RunIcon"></Icon>
21 + <Icon :name="RunIcon" />
22 </template>
23 Run once
24 </n-button>
25 <n-button :size secondary :loading="loadingUpdate" @click="showForm = true">
26 <template #icon>
27 - <Icon :name="UpdatedIcon"></Icon>
27 + <Icon :name="UpdatedIcon" />
28 </template>
29 </n-button>
30 </div>
frontend/src/components/scheduler/NextJobTimeTooltip.vue
+1 -1
@@ -1,7 +1,7 @@
1 <template>
2 <n-tooltip placement="top-end" @update:show="getNextRun()">
3 <template #trigger>
4 - <Icon :name="NextIcon"></Icon>
4 + <Icon :name="NextIcon" />
5 </template>
6 <template #header>Next run time:</template>
7 <div>
frontend/src/components/services/Item.vue
+1 -1
@@ -28,7 +28,7 @@
28 <template #footerExtra>
29 <n-button size="small" @click.stop="showDetails = true">
30 <template #icon>
31 - <Icon :name="InfoIcon"></Icon>
31 + <Icon :name="InfoIcon" />
32 </template>
33 Details
34 </n-button>
frontend/src/components/sigma/QueriesList.vue
+4 -4
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
@@ -32,14 +32,14 @@
32
33 <n-button size="small" type="primary" secondary strong @click="showActionsView = !showActionsView">
34 <div class="flex items-center gap-2">
35 - <Icon :name="ToolsIcon" :size="16"></Icon>
35 + <Icon :name="ToolsIcon" :size="16" />
36 <span class="xs:block hidden">Actions</span>
37 <Icon
38 class="xs:!block !hidden transition-transform"
39 :class="{ '!rotate-90': showActionsView }"
40 :name="ChevronIcon"
41 :size="16"
42 - ></Icon>
42 + />
43 </div>
44 </n-button>
45 </div>
@@ -58,7 +58,7 @@
58 <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
59 <n-button size="small" @click="showFilters = true">
60 <template #icon>
61 - <Icon :name="FilterIcon"></Icon>
61 + <Icon :name="FilterIcon" />
62 </template>
63 </n-button>
64 </n-badge>
frontend/src/components/sigma/QueryItem.vue
+2 -2
@@ -5,8 +5,8 @@
5 <template #headerExtra>
6 <div class="flex items-center gap-2">
7 <span>{{ query.active ? "Active" : "Inactive" }}</span>
8 - <Icon v-if="query.active" :name="EnabledIcon" :size="14" class="text-success"></Icon>
9 - <Icon v-else :name="DisabledIcon" :size="14" class="text-secondary"></Icon>
8 + <Icon v-if="query.active" :name="EnabledIcon" :size="14" class="text-success" />
9 + <Icon v-else :name="DisabledIcon" :size="14" class="text-secondary" />
10 </div>
11 </template>
12 <template #default>
frontend/src/components/sigma/actionsProviders/QueryUploadFile.vue
+1 -1
@@ -13,7 +13,7 @@
13 >
14 <n-upload-dragger>
15 <div>
16 - <Icon :name="UploadIcon" :size="28" :depth="3"></Icon>
16 + <Icon :name="UploadIcon" :size="28" :depth="3" />
17 </div>
18 <div class="font-semibold">Click or drag a file to this area to upload</div>
19 <p class="mt-2">Only .yaml files are accepted</p>
frontend/src/components/soc/SocAlerts/SocAlertAssets/SocAlertAssetsItem.vue
+3 -3
@@ -33,7 +33,7 @@
33 <div class="flex flex-wrap items-center gap-3">
34 <Badge v-if="asset.date_added" type="splitted" color="primary">
35 <template #iconLeft>
36 - <Icon :name="ClockIcon" :size="14"></Icon>
36 + <Icon :name="ClockIcon" :size="14" />
37 </template>
38 <template #label>Added</template>
39 <template #value>
@@ -42,7 +42,7 @@
42 </Badge>
43 <Badge v-if="asset.date_update" type="splitted" color="primary">
44 <template #iconLeft>
45 - <Icon :name="ClockIcon" :size="14"></Icon>
45 + <Icon :name="ClockIcon" :size="14" />
46 </template>
47 <template #label>Updated</template>
48 <template #value>
@@ -51,7 +51,7 @@
51 </Badge>
52 <Badge type="active" class="cursor-pointer" @click.stop="gotoAgent(asset.asset_tags)">
53 <template #iconRight>
54 - <Icon :name="LinkIcon" :size="14"></Icon>
54 + <Icon :name="LinkIcon" :size="14" />
55 </template>
56 <template #label>Agent: {{ asset.asset_tags }}</template>
57 </Badge>
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItem.vue
+2 -2
@@ -49,7 +49,7 @@
49 >
50 {{ showBadges ? "Less info" : "More info" }}
51 <span class="flex items-center transition-transform" :class="{ 'rotate-90': showBadges }">
52 - <Icon :name="ChevronIcon" :size="14"></Icon>
52 + <Icon :name="ChevronIcon" :size="14" />
53 </span>
54 </div>
55 <n-collapse-transition :show="!showBadgesToggle || showBadges">
@@ -73,7 +73,7 @@
73 <n-collapse>
74 <template #arrow>
75 <div class="mx-5 flex">
76 - <Icon :name="ChevronIcon"></Icon>
76 + <Icon :name="ChevronIcon" />
77 </div>
78 </template>
79 <n-collapse-item>
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItemActions.vue
+3 -3
@@ -2,7 +2,7 @@
2 <div class="contents">
3 <n-button v-if="existCase" type="success" secondary :size="size" @click.stop="openSocCase()">
4 <template #icon>
5 - <Icon :name="ViewIcon"></Icon>
5 + <Icon :name="ViewIcon" />
6 </template>
7 View SOC Case
8 </n-button>
@@ -15,7 +15,7 @@
15 @click.stop="createCase()"
16 >
17 <template #icon>
18 - <Icon :name="DangerIcon"></Icon>
18 + <Icon :name="DangerIcon" />
19 </template>
20 Create SOC Case
21 </n-button>
@@ -28,7 +28,7 @@
28 @click.stop="handleDelete()"
29 >
30 <template #icon>
31 - <Icon :name="DeleteIcon"></Icon>
31 + <Icon :name="DeleteIcon" />
32 </template>
33 Delete
34 </n-button>
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItemBadges.vue
+6 -6
@@ -4,7 +4,7 @@
4 <template #trigger>
5 <Badge type="splitted" color="primary" hint-cursor>
6 <template #iconLeft>
7 - <Icon :name="StatusIcon" :size="14"></Icon>
7 + <Icon :name="StatusIcon" :size="14" />
8 </template>
9 <template #label>Status</template>
10 <template #value>
@@ -16,7 +16,7 @@
16 </n-tooltip>
17 <Badge type="splitted" :color="alert.severity?.severity_id === 5 ? 'danger' : 'primary'">
18 <template #iconLeft>
19 - <Icon :name="SeverityIcon" :size="13"></Icon>
19 + <Icon :name="SeverityIcon" :size="13" />
20 </template>
21 <template #label>Severity</template>
22 <template #value>
@@ -25,7 +25,7 @@
25 </Badge>
26 <Badge type="splitted" color="primary" class="!hidden @2xl:!flex">
27 <template #iconLeft>
28 - <Icon :name="SourceIcon" :size="13"></Icon>
28 + <Icon :name="SourceIcon" :size="13" />
29 </template>
30 <template #label>Source</template>
31 <template #value>
@@ -34,7 +34,7 @@
34 </Badge>
35 <Badge type="splitted" color="primary" class="!hidden @2xl:!flex">
36 <template #iconLeft>
37 - <Icon :name="CustomerIcon" :size="13"></Icon>
37 + <Icon :name="CustomerIcon" :size="13" />
38 </template>
39 <template #label>Customer</template>
40 <template #value>
@@ -57,7 +57,7 @@
57 <Badge type="active" class="cursor-pointer">
58 <template #iconLeft>
59 <n-spin :size="16" :show="loading">
60 - <Icon :name="OwnerIcon" :size="16"></Icon>
60 + <Icon :name="OwnerIcon" :size="16" />
61 </n-spin>
62 </template>
63 <template #label>Owner</template>
@@ -76,7 +76,7 @@
76 rel="nofollow noopener noreferrer"
77 >
78 <template #iconRight>
79 - <Icon :name="LinkIcon" :size="14"></Icon>
79 + <Icon :name="LinkIcon" :size="14" />
80 </template>
81 <template #label>Source link</template>
82 </Badge>
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItemBookmarkToggler.vue
+1 -1
@@ -5,7 +5,7 @@
5 class="toggler-bookmark"
6 :class="{ active: isBookmark }"
7 @click.stop="toggleBookmark()"
8 - ></Icon>
8 + />
9 </template>
10
11 <script setup lang="ts">
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItemDetails.vue
+2 -2
@@ -32,7 +32,7 @@
32 <div class="grid gap-2 px-7 pt-4">
33 <Badge type="active" style="max-width: 145px" class="cursor-pointer" @click="gotoSocUsers(ownerId)">
34 <template #iconRight>
35 - <Icon :name="LinkIcon" :size="14"></Icon>
35 + <Icon :name="LinkIcon" :size="14" />
36 </template>
37 <template #label>Go to users page</template>
38 </Badge>
@@ -49,7 +49,7 @@
49 >
50 <div class="text-primary flex cursor-pointer items-center gap-2">
51 <n-spin :size="16" :show="loading">
52 - <Icon :name="EditIcon" :size="16"></Icon>
52 + <Icon :name="EditIcon" :size="16" />
53 </n-spin>
54 <span>{{ ownerName || "Assign a user" }}</span>
55 </div>
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItemTime.vue
+1 -1
@@ -5,7 +5,7 @@
5 <span>
6 {{ formatDate(alert.alert_source_event_time) }}
7 </span>
8 - <Icon v-if="!hideTimeline" :name="TimeIcon" :size="16"></Icon>
8 + <Icon v-if="!hideTimeline" :name="TimeIcon" :size="16" />
9 </div>
10 </template>
11 <div class="flex flex-col px-1 py-2">
frontend/src/components/soc/SocAlerts/SocAlertsFullList.vue
+2 -2
@@ -30,7 +30,7 @@
30 <template #resize-trigger>
31 <div class="split-trigger">
32 <div class="split-trigger-icon">
33 - <Icon :name="SplitIcon"></Icon>
33 + <Icon :name="SplitIcon" />
34 </div>
35 </div>
36 </template>
@@ -48,7 +48,7 @@
48 <template #header>
49 <n-button size="small" @click="showBookmarkedDrawer = true">
50 <template #icon>
51 - <Icon :name="StarIcon" :size="14"></Icon>
51 + <Icon :name="StarIcon" :size="14" />
52 </template>
53 </n-button>
54 </template>
frontend/src/components/soc/SocAlerts/SocAlertsList.vue
+3 -3
@@ -17,7 +17,7 @@
17 @click="handleDelete()"
18 >
19 <div class="flex items-center gap-2">
20 - <Icon :name="TrashIcon" :size="16"></Icon>
20 + <Icon :name="TrashIcon" :size="16" />
21 <span class="flex items-center gap-2">
22 <span v-if="!compactMode">Delete Alerts:</span>
23 <span class="font-mono">
@@ -37,7 +37,7 @@
37 <div v-for="alertId of checkedList" :key="alertId" class="w-full">
38 <n-button size="small" class="!w-full !justify-start" @click="clearChecked(alertId)">
39 <template #icon>
40 - <Icon :name="CloseIcon" :size="18"></Icon>
40 + <Icon :name="CloseIcon" :size="18" />
41 </template>
42 <span class="font-mono">#{{ alertId }}</span>
43 </n-button>
@@ -46,7 +46,7 @@
46 </n-popover>
47 <n-button v-else size="small" type="error" ghost :loading="loadingPurge" @click="handlePurge()">
48 <div class="flex items-center gap-2">
49 - <Icon :name="TrashIcon" :size="16"></Icon>
49 + <Icon :name="TrashIcon" :size="16" />
50 <span class="xs:block hidden">Purge</span>
51 </div>
52 </n-button>
frontend/src/components/soc/SocCases/SocCaseAssetLink.vue
+1 -1
@@ -33,7 +33,7 @@
33 <n-collapse @item-header-click="showSocCase($event.expanded)">
34 <template #arrow>
35 <div class="mx-5 flex">
36 - <Icon :name="ChevronIcon"></Icon>
36 + <Icon :name="ChevronIcon" />
37 </div>
38 </template>
39 <n-collapse-item>
frontend/src/components/soc/SocCases/SocCaseItem.vue
+6 -6
@@ -16,7 +16,7 @@
16 <span>
17 {{ formatDate(caseOpenDate) }}
18 </span>
19 - <Icon :name="TimeIcon" :size="16"></Icon>
19 + <Icon :name="TimeIcon" :size="16" />
20 </div>
21 </template>
22 <div class="flex flex-col px-1 py-2">
@@ -47,7 +47,7 @@
47 <div class="flex flex-wrap items-center gap-3">
48 <Badge type="splitted" :color="baseInfo.state_name === StateName.Open ? 'warning' : 'primary'">
49 <template #iconLeft>
50 - <Icon :name="StatusIcon" :size="14"></Icon>
50 + <Icon :name="StatusIcon" :size="14" />
51 </template>
52 <template #label>State</template>
53 <template #value>
@@ -56,7 +56,7 @@
56 </Badge>
57 <Badge type="splitted" color="primary">
58 <template #iconLeft>
59 - <Icon :name="OwnerIcon" :size="16"></Icon>
59 + <Icon :name="OwnerIcon" :size="16" />
60 </template>
61 <template #label>Owner</template>
62 <template #value>
@@ -65,7 +65,7 @@
65 </Badge>
66 <Badge type="splitted" color="primary">
67 <template #iconLeft>
68 - <Icon :name="CustomerIcon" :size="13"></Icon>
68 + <Icon :name="CustomerIcon" :size="13" />
69 </template>
70 <template #label>Client</template>
71 <template #value>
@@ -79,7 +79,7 @@
79 @click.stop="openSocAlert()"
80 >
81 <template #iconRight>
82 - <Icon :name="LinkIcon" :size="14"></Icon>
82 + <Icon :name="LinkIcon" :size="14" />
83 </template>
84 <template #label>Alert #{{ baseInfo.case_soc_id }}</template>
85 </Badge>
@@ -205,7 +205,7 @@
205 <n-collapse v-model:expanded-names="noteFormVisible" display-directive="show">
206 <template #arrow>
207 <div class="mx-4 flex">
208 - <Icon :name="AddIcon"></Icon>
208 + <Icon :name="AddIcon" />
209 </div>
210 </template>
211 <n-collapse-item name="1">
frontend/src/components/soc/SocCases/SocCaseItemActions.vue
+3 -3
@@ -9,19 +9,19 @@
9 @click.stop="reopenCase()"
10 >
11 <template #icon>
12 - <Icon :name="OpenIcon"></Icon>
12 + <Icon :name="OpenIcon" />
13 </template>
14 Reopen
15 </n-button>
16 <n-button v-else :loading="loadingCaseClose" type="success" secondary :size="size" @click.stop="closeCase()">
17 <template #icon>
18 - <Icon :name="CloseIcon"></Icon>
18 + <Icon :name="CloseIcon" />
19 </template>
20 Close
21 </n-button>
22 <n-button :loading="loadingCaseDelete" :size="size" type="error" secondary @click.stop="handleDelete()">
23 <template #icon>
24 - <Icon :name="DeleteIcon"></Icon>
24 + <Icon :name="DeleteIcon" />
25 </template>
26 Delete
27 </n-button>
frontend/src/components/soc/SocCases/SocCaseNote.vue
+1 -1
@@ -9,7 +9,7 @@
9 <span>
10 {{ formatDateTime(note.note_details.note_creationdate) }}
11 </span>
12 - <Icon :name="TimeIcon" :size="16"></Icon>
12 + <Icon :name="TimeIcon" :size="16" />
13 </div>
14 </template>
15 <div class="flex flex-col px-1 py-2">
frontend/src/components/soc/SocCases/SocCasesList.vue
+3 -3
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
@@ -29,7 +29,7 @@
29 @click="handlePurge()"
30 >
31 <div class="flex items-center gap-2">
32 - <Icon :name="TrashIcon" :size="16"></Icon>
32 + <Icon :name="TrashIcon" :size="16" />
33 <span class="xs:block hidden">Purge</span>
34 </div>
35 </n-button>
@@ -49,7 +49,7 @@
49 <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
50 <n-button size="small" @click="showFilters = true">
51 <template #icon>
52 - <Icon :name="FilterIcon"></Icon>
52 + <Icon :name="FilterIcon" />
53 </template>
54 </n-button>
55 </n-badge>
frontend/src/components/soc/SocUsers/SocUsersList.vue
+1 -1
@@ -23,7 +23,7 @@
23 <span>#{{ user.user_id }}</span>
24 <n-tooltip trigger="hover">
25 <template #trigger>
26 - <Icon :name="InfoIcon" :size="16" class="cursor-help"></Icon>
26 + <Icon :name="InfoIcon" :size="16" class="cursor-help" />
27 </template>
28 {{ user.user_uuid }}
29 </n-tooltip>
frontend/src/components/stackProvisioning/StackProvisioningButton.vue
+1 -1
@@ -1,7 +1,7 @@
1 <template>
2 <n-button :size="size" :type="type" @click="showForm = true">
3 <template #icon>
4 - <Icon :name="PackIcon"></Icon>
4 + <Icon :name="PackIcon" />
5 </template>
6 Stack Provisioning
7 </n-button>
frontend/src/components/stackProvisioning/StackProvisioningItem.vue
+1 -1
@@ -15,7 +15,7 @@
15 @click="provision(contentPack.name)"
16 >
17 <template #icon>
18 - <Icon :name="DeployIcon"></Icon>
18 + <Icon :name="DeployIcon" />
19 </template>
20 Deploy
21 </n-button>
frontend/src/components/threatIntel/ThreatIntelButton.vue
+1 -1
@@ -1,7 +1,7 @@
1 <template>
2 <n-button :size="size" :type="type" @click="showThreatIntelDrawer = true">
3 <template #icon>
4 - <Icon :name="ThreatIcon"></Icon>
4 + <Icon :name="ThreatIcon" />
5 </template>
6 Threat Intel
7 </n-button>
frontend/src/components/users/AssignCustomer.vue
+1 -1
@@ -1,7 +1,7 @@
1 <template>
2 <n-button quaternary class="!w-full !justify-start" @click="showModal = true">
3 <template #icon>
4 - <Icon :name="CustomerIcon" :size="14"></Icon>
4 + <Icon :name="CustomerIcon" :size="14" />
5 </template>
6 Assign Customer
7 </n-button>
frontend/src/components/users/AssignRole.vue
+1 -1
@@ -1,7 +1,7 @@
1 <template>
2 <n-button quaternary class="!w-full !justify-start" @click="showModal = true">
3 <template #icon>
4 - <Icon :name="RoleIcon" :size="14"></Icon>
4 + <Icon :name="RoleIcon" :size="14" />
5 </template>
6 Assign Role
7 </n-button>
frontend/src/components/users/ChangePassword.vue
+1 -1
@@ -1,7 +1,7 @@
1 <template>
2 <n-button :size :type quaternary class="!w-full !justify-start" @click="showFormDrawer = true">
3 <template #icon>
4 - <Icon :name="PasswordIcon" :size="14"></Icon>
4 + <Icon :name="PasswordIcon" :size="14" />
5 </template>
6 Change Password
7 </n-button>
frontend/src/components/users/DeleteUser.vue
+1 -1
@@ -1,7 +1,7 @@
1 <template>
2 <n-button :size :type quaternary class="!w-full !justify-start" :loading @click="handleDelete()">
3 <template #icon>
4 - <Icon :name="DeleteIcon" :size="14"></Icon>
4 + <Icon :name="DeleteIcon" :size="14" />
5 </template>
6 Delete User
7 </n-button>
frontend/src/components/users/UsersList.vue
+2 -2
@@ -8,7 +8,7 @@
8 <div>
9 <n-button size="small" type="primary" @click="showForm = true">
10 <template #icon>
11 - <Icon :name="UserAddIcon"></Icon>
11 + <Icon :name="UserAddIcon" />
12 </template>
13 Add User
14 </n-button>
@@ -56,7 +56,7 @@
56 >
57 <n-button text>
58 <template #icon>
59 - <Icon :name="DropdownIcon" :size="24"></Icon>
59 + <Icon :name="DropdownIcon" :size="24" />
60 </template>
61 </n-button>
62 </n-dropdown>
frontend/src/components/vulnerabilities/List.vue
+1 -1
@@ -19,7 +19,7 @@
19 <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
20 <n-button size="small" secondary @click="showFiltersView = !showFiltersView">
21 <template #icon>
22 - <Icon :name="FilterIcon"></Icon>
22 + <Icon :name="FilterIcon" />
23 </template>
24 </n-button>
25 </n-badge>
frontend/src/components/webVulnerabilityAssessment/ReportsList.vue
+2 -2
@@ -7,7 +7,7 @@
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="!cursor-help">
9 <template #icon>
10 - <Icon :name="InfoIcon"></Icon>
10 + <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
@@ -23,7 +23,7 @@
23 <div class="actions flex items-center gap-2">
24 <n-button size="small" type="primary" @click="showForm = true">
25 <template #icon>
26 - <Icon :name="NewReportIcon" :size="15"></Icon>
26 + <Icon :name="NewReportIcon" :size="15" />
27 </template>
28 Scan Host
29 </n-button>
frontend/src/components/webVulnerabilityAssessment/WebVulnerabilityAssessmentButton.vue
+1 -1
@@ -1,7 +1,7 @@
1 <template>
2 <n-button :size="size" :type="type" @click="gotoPage()">
3 <template #icon>
4 - <Icon :name="VulnerabilityIcon" :size="16"></Icon>
4 + <Icon :name="VulnerabilityIcon" :size="16" />
5 </template>
6 Web Vulnerability Assessment
7 </n-button>
frontend/src/main.ts
+3
@@ -20,3 +20,6 @@ app.use(i18n)
20 app.use(router)
21
22 app.mount("#app")
23 +
24 +// TODO: Rename all files to file-case. For example: activeResponse.ts -> active-response.ts
25 +// TODO: use .ts files instead of .d.ts files
frontend/src/router/index.ts
+6
@@ -296,6 +296,12 @@ const router = createRouter({
296 component: () => import("@/views/WebVulnerabilityAssessment.vue"),
297 meta: { title: "Web Vuln. Assess.", auth: true, roles: RouteRole.All }
298 },
299 + {
300 + path: "/customer-portal",
301 + name: "CustomerPortal",
302 + component: () => import("@/views/CustomerPortal.vue"),
303 + meta: { title: "Customer Portal", auth: true, roles: RouteRole.All }
304 + },
305 {
306 path: "/license",
307 meta: {
frontend/src/types/agents.d.ts
+19
@@ -102,3 +102,22 @@ export interface ScaPolicyResultRule {
102 type: "command" | "directory" | "file" | "numeric" | string
103 rule: string
104 }
105 +
106 +export interface AgentArtifactData {
107 + id: number
108 + agent_id: string
109 + velociraptor_id: string
110 + customer_code: string | null
111 + artifact_name: string
112 + flow_id: string
113 + bucket_name: string
114 + object_key: string
115 + file_name: string
116 + content_type: string
117 + file_size: number
118 + file_hash: string
119 + collection_time: string
120 + uploaded_by: number | null
121 + notes: string | null
122 + status: string
123 +}
frontend/src/types/artifacts.d.ts
+5
@@ -31,3 +31,8 @@ export interface MatchingParameter {
31 type: string
32 default?: string
33 }
34 +
35 +export interface FileCollection {
36 + flow_id?: string
37 + session_id?: string
38 +}
frontend/src/types/customerPortal.d.ts new
+7
@@ -0,0 +1,7 @@
1 +export interface CustomerPortalSettings {
2 + id: number
3 + title: string
4 + logo_base64: string
5 + logo_mime_type: string
6 + updated_at: Date
7 +}
frontend/src/types/customers.d.ts
+1
@@ -13,6 +13,7 @@ export interface Customer {
13 postal_code: string
14 country: string
15 customer_type: string
16 + // TODO: consider removing it (logo_file)
17 logo_file: string
18 }
19
frontend/src/types/graylog/inputs.d.ts
+1 -2
@@ -77,5 +77,4 @@ export interface StaticFields {
77 }
78
79 export interface InputExtended
80 - extends ConfiguredInput,
81 - Pick<RunningInput, "state" | "started_at" | "detailed_message"> {}
80 + extends ConfiguredInput, Pick<RunningInput, "state" | "started_at" | "detailed_message"> {}
frontend/src/views/CustomerPortal.vue new
+9
@@ -0,0 +1,9 @@
1 +<template>
2 + <div class="page">
3 + <Settings />
4 + </div>
5 +</template>
6 +
7 +<script setup lang="ts">
8 +import Settings from "@/components/customerPortal/Settings.vue"
9 +</script>
frontend/src/views/Overview.vue
+1 -1
@@ -13,7 +13,7 @@
13 <div class="mobile-box block lg:hidden">
14 <n-button size="small" type="primary" @click="showQuickActions = true">
15 <template #icon>
16 - <Icon :name="QuickActionsIcon"></Icon>
16 + <Icon :name="QuickActionsIcon" />
17 </template>
18 Quick Actions
19 </n-button>
frontend/src/views/ReportCreation.vue
+1 -1
@@ -22,7 +22,7 @@
22 <div v-if="licenseResponse" class="over-layer mobile-layer">
23 <n-alert>
24 <template #icon>
25 - <Icon :name="AlertIcon" :size="18"></Icon>
25 + <Icon :name="AlertIcon" :size="18" />
26 </template>
27 This function is available only for desktop devices
28 </n-alert>
frontend/src/views/agents/DetectionRules.vue
+1 -1
@@ -26,7 +26,7 @@
26 <template #trigger>
27 <n-button secondary :loading="loadingManager" size="small" @click="reloadManager()">
28 <template #icon>
29 - <Icon :name="RefreshIcon"></Icon>
29 + <Icon :name="RefreshIcon" />
30 </template>
31 </n-button>
32 </template>
frontend/src/views/agents/Groups.vue
+1 -1
@@ -26,7 +26,7 @@
26 <template #trigger>
27 <n-button secondary :loading="loadingRefresh" size="small" @click="refreshGroups()">
28 <template #icon>
29 - <Icon :name="RefreshIcon"></Icon>
29 + <Icon :name="RefreshIcon" />
30 </template>
31 </n-button>
32 </template>
frontend/src/views/agents/Overview.vue
+17 -3
@@ -2,7 +2,7 @@
2 <div class="page">
3 <div class="agent-toolbar">
4 <div class="back-btn" @click="gotoAgent()">
5 - <Icon :name="ArrowIcon" :size="16"></Icon>
5 + <Icon :name="ArrowIcon" :size="16" />
6 <span>Agents list</span>
7 </div>
8 <div v-if="agent" class="delete-btn" @click.stop="handleDelete">Delete Agent</div>
@@ -26,7 +26,7 @@
26 @click.stop="toggleCritical(agent.agent_id, agent.critical_asset)"
27 >
28 <template #icon>
29 - <Icon :name="StarIcon"></Icon>
29 + <Icon :name="StarIcon" />
30 </template>
31 </n-button>
32 </template>
@@ -40,7 +40,7 @@
40 <n-tag v-if="isOnline" type="success" round :bordered="false">ONLINE</n-tag>
41 <n-tag v-if="isQuarantined" type="warning" round :bordered="false">
42 <template #icon>
43 - <Icon :name="QuarantinedIcon"></Icon>
43 + <Icon :name="QuarantinedIcon" />
44 </template>
45 <span>QUARANTINED</span>
46 </n-tag>
@@ -134,6 +134,16 @@
134 <n-tab-pane name="active-response" tab="Active Response" display-directive="show:lazy">
135 <ActiveResponseAgent v-if="agent" :agent="agent" embedded />
136 </n-tab-pane>
137 + <n-tab-pane name="file-collection" tab="File Collection" display-directive="show:lazy">
138 + <div class="section">
139 + <FileCollectionForm v-if="agent" :agent-id="agent.agent_id" />
140 + </div>
141 + </n-tab-pane>
142 + <n-tab-pane name="data-store" tab="Data Store" display-directive="show:lazy">
143 + <div class="section">
144 + <AgentDataStoreTab v-if="agent" :agent="agent" />
145 + </div>
146 + </n-tab-pane>
147 </n-tabs>
148 </n-spin>
149 </n-card>
@@ -167,6 +177,10 @@ const ArtifactsCollect = defineAsyncComponent(() => import("@/components/artifac
177 const ArtifactsCommand = defineAsyncComponent(() => import("@/components/artifacts/ArtifactsCommand.vue"))
178 const ArtifactsQuarantine = defineAsyncComponent(() => import("@/components/artifacts/ArtifactsQuarantine.vue"))
179 const ActiveResponseAgent = defineAsyncComponent(() => import("@/components/activeResponse/ActiveResponseAgent.vue"))
180 +const AgentDataStoreTab = defineAsyncComponent(() => import("@/components/agents/dataStore/AgentDataStoreTab.vue"))
181 +const FileCollectionForm = defineAsyncComponent(
182 + () => import("@/components/agents/fileCollection/FileCollectionForm.vue")
183 +)
184
185 const StarIcon = "carbon:star"
186 const QuarantinedIcon = "ph:seal-warning-light"
frontend/src/views/graylog/Metrics.vue
+3 -3
@@ -4,7 +4,7 @@
4 <div class="info flex items-center gap-3">
5 <n-button size="small" type="primary" secondary :loading="loading" @click="getData()">
6 <template #icon>
7 - <Icon :name="UpdatedIcon" :size="15"></Icon>
7 + <Icon :name="UpdatedIcon" :size="15" />
8 </template>
9 </n-button>
10 <span>Last check:</span>
@@ -14,13 +14,13 @@
14 <div class="toolbar flex items-center gap-3">
15 <n-button v-if="!isRunning" size="small" type="primary" class="!w-24" @click="start()">
16 <template #icon>
17 - <Icon :name="StartIcon"></Icon>
17 + <Icon :name="StartIcon" />
18 </template>
19 Start
20 </n-button>
21 <n-button v-if="isRunning" size="small" type="error" ghost class="!w-24" @click="stop()">
22 <template #icon>
23 - <Icon :name="StopIcon"></Icon>
23 + <Icon :name="StopIcon" />
24 </template>
25 Stop
26 </n-button>
frontend/src/views/graylog/Pipelines.vue
+1 -1
@@ -3,7 +3,7 @@
3 <div class="mb-4">
4 <n-button secondary type="primary" @click="showRulesDrawer = true">
5 <template #icon>
6 - <Icon :name="RulesIcon" :size="22"></Icon>
6 + <Icon :name="RulesIcon" :size="22" />
7 </template>
8 View All Rules
9 </n-button>
package-lock.json deleted
-6
@@ -1,6 +0,0 @@
1 -{
2 - "name": "CoPilot",
3 - "lockfileVersion": 3,
4 - "requires": true,
5 - "packages": {}
6 -}