diff --git a/.github/workflows/cd-cloud.yml b/.github/workflows/cd-cloud.yml
index 90a09dab0..4d84b841b 100644
--- a/.github/workflows/cd-cloud.yml
+++ b/.github/workflows/cd-cloud.yml
@@ -9,7 +9,7 @@ on:
jobs:
build:
name: Build, push, and deploy
- runs-on: ubuntu-latest
+ runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index 199cbe617..b875e314b 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -19,7 +19,7 @@ on:
jobs:
build:
name: Build, push, and deploy
- runs-on: ubuntu-latest
+ runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
packages: write
@@ -113,7 +113,7 @@ jobs:
push: true
tags: ${{ steps.compute.outputs.ghcr_tags }}
cache-from: type=gha
- cache-to: type=gha,mode=max
+ cache-to: type=gha,mode=max,ignore-error=true
- name: Build and push to Docker Hub
if: github.repository == 'umami-software/umami'
@@ -124,4 +124,4 @@ jobs:
push: true
tags: ${{ steps.compute.outputs.docker_tags }}
cache-from: type=gha
- cache-to: type=gha,mode=max
\ No newline at end of file
+ cache-to: type=gha,mode=max,ignore-error=true
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b97dac128..b05776cd9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,20 +9,19 @@ env:
jobs:
build:
if: github.repository == 'umami-software/umami'
- runs-on: ubuntu-latest
+ runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
- version: 10
+ version: 10.15.1
run_install: false
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm"
- - run: npm install --global pnpm
- run: pnpm install
- run: pnpm test
- run: pnpm build
diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml
index f16040149..ec497b70a 100644
--- a/.github/workflows/stale-issues.yml
+++ b/.github/workflows/stale-issues.yml
@@ -5,7 +5,7 @@ on:
jobs:
stale:
- runs-on: ubuntu-latest
+ runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
issues: write
pull-requests: write
diff --git a/.gitignore b/.gitignore
index 5c98531ed..7ac75d6c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,8 @@ package-lock.json
# testing
/coverage
+/playwright-report
+/test-results
# next.js
next-env.d.ts
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..623e689e1
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,46 @@
+# Contributing to Umami
+
+Thanks for your interest in contributing to Umami! This document outlines the process for contributing code.
+
+## Branching
+
+Umami uses the following long-lived branches:
+
+- `master` — stable, released code. **Do not open PRs against `master`.**
+- `dev` — active development. **All pull requests should target `dev`.**
+
+Feature branches and fixes are merged into `dev`, and `dev` is periodically merged into `master` for releases.
+
+## Submitting a Pull Request
+
+1. Fork the repository and create your branch from `dev`:
+ ```bash
+ git checkout dev
+ git pull origin dev
+ git checkout -b my-feature
+ ```
+2. Make your changes. Keep PRs focused — one logical change per PR.
+3. Ensure the project builds and lints cleanly:
+ ```bash
+ pnpm install
+ pnpm build
+ pnpm lint
+ ```
+4. Push your branch and open a pull request **against the `dev` branch**.
+5. Fill in the PR description with what changed and why. Link any related issues.
+
+PRs opened against `master` will be asked to retarget `dev`.
+
+## Reporting Issues
+
+- Search [existing issues](https://github.com/umami-software/umami/issues) before opening a new one.
+- For bugs, include reproduction steps, expected vs. actual behavior, and your environment (Umami version, database, browser).
+- For feature requests, describe the use case before the proposed solution.
+
+## Development Setup
+
+See the [README](./README.md) for instructions on installing dependencies, configuring the database, and running Umami locally.
+
+## License
+
+By contributing, you agree that your contributions will be licensed under the [MIT License](./LICENSE).
diff --git a/Dockerfile b/Dockerfile
index ac9fd8de5..1a1a1ac95 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,12 +1,19 @@
ARG NODE_IMAGE_VERSION="22-alpine"
+ARG PNPM_VERSION="10.15.1"
+# Keep in sync with the prisma/@prisma/* versions in package.json
+ARG PRISMA_VERSION="7.8.0"
# Install dependencies only when needed
FROM node:${NODE_IMAGE_VERSION} AS deps
+
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm
+
+RUN printf 'strictDepBuilds: false\n' > pnpm-workspace.yaml
+
RUN pnpm install --frozen-lockfile
# Rebuild the source code only when needed
@@ -28,8 +35,8 @@ RUN npm run build-docker
FROM node:${NODE_IMAGE_VERSION} AS runner
WORKDIR /app
-ARG PRISMA_VERSION="7.3.0"
ARG NODE_OPTIONS
+ARG PRISMA_VERSION
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
@@ -37,12 +44,20 @@ ENV NODE_OPTIONS=$NODE_OPTIONS
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
+# Bootstrap pnpm with the bundled npm, then remove npm in the same layer so the
+# vulnerable packages vendored inside the npm CLI are not shipped in the final
+# image. pnpm is the only package manager needed at build and runtime.
RUN set -x \
- && apk add --no-cache curl \
- && npm install -g pnpm
+ && apk add --no-cache curl libc6-compat \
+ && npm install -g pnpm \
+ && rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx
+
+RUN echo {} > package.json
+
+RUN printf "allowBuilds:\n '@prisma/engines': true\n prisma: false\nverifyDepsBeforeRun: false\n" > pnpm-workspace.yaml
# Script dependencies
-RUN pnpm --allow-build='@prisma/engines' add npm-run-all dotenv chalk semver \
+RUN pnpm add npm-run-all dotenv chalk semver \
prisma@${PRISMA_VERSION} \
@prisma/client@${PRISMA_VERSION} \
@prisma/adapter-pg@${PRISMA_VERSION}
@@ -65,4 +80,4 @@ EXPOSE 3000
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
-CMD ["pnpm", "start-docker"]
\ No newline at end of file
+CMD ["sh", "scripts/start-docker.sh"]
diff --git a/README.md b/README.md
index cd365a68d..474c75b3f 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
-
+
---
@@ -46,6 +46,10 @@ Create an `.env` file with the following:
DATABASE_URL=connection-url
```
+Optional: set `API_URL` to change the base URL used by internal UI API calls.
+Relative paths are served under `BASE_PATH`; absolute URLs are proxied through the local `/api` route.
+For example, `API_URL=/internal-api` or `API_URL=https://api.example.com/api`.
+
The connection URL format:
```bash
diff --git a/cypress.config.ts b/cypress.config.ts
deleted file mode 100644
index c1100467d..000000000
--- a/cypress.config.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { defineConfig } from 'cypress';
-
-export default defineConfig({
- e2e: {
- baseUrl: 'http://localhost:3000',
- },
- // default username / password on init
- env: {
- umami_user: 'admin',
- umami_password: 'umami',
- umami_user_id: '41e2b680-648e-4b09-bcd7-3e2b10c06264',
- },
-});
diff --git a/cypress/docker-compose.yml b/cypress/docker-compose.yml
deleted file mode 100644
index 01a47bd8c..000000000
--- a/cypress/docker-compose.yml
+++ /dev/null
@@ -1,52 +0,0 @@
----
-version: '3'
-services:
- umami:
- build: ../
- #image: ghcr.io/umami-software/umami:postgresql-latest
- ports:
- - '3000:3000'
- environment:
- DATABASE_URL: postgresql://umami:umami@db:5432/umami
- DATABASE_TYPE: postgresql
- APP_SECRET: replace-me-with-a-random-string
- depends_on:
- db:
- condition: service_healthy
- restart: always
- healthcheck:
- test: ['CMD-SHELL', 'curl http://localhost:3000/api/heartbeat']
- interval: 5s
- timeout: 5s
- retries: 5
- db:
- image: postgres:15-alpine
- environment:
- POSTGRES_DB: umami
- POSTGRES_USER: umami
- POSTGRES_PASSWORD: umami
- volumes:
- - umami-db-data:/var/lib/postgresql/data
- restart: always
- healthcheck:
- test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}']
- interval: 5s
- timeout: 5s
- retries: 5
- cypress:
- image: 'cypress/included:13.6.0'
- depends_on:
- - umami
- - db
- environment:
- - CYPRESS_baseUrl=http://umami:3000
- - CYPRESS_umami_user=admin
- - CYPRESS_umami_password=umami
- volumes:
- - ./tsconfig.json:/tsconfig.json
- - ../cypress.config.ts:/cypress.config.ts
- - ./:/cypress
- - ../node_modules/:/node_modules
- - ../src/lib/crypto.ts:/src/lib/crypto.ts
-volumes:
- umami-db-data:
diff --git a/cypress/e2e/api-team.cy.ts b/cypress/e2e/api-team.cy.ts
deleted file mode 100644
index 22811130f..000000000
--- a/cypress/e2e/api-team.cy.ts
+++ /dev/null
@@ -1,209 +0,0 @@
-describe('Team API tests', () => {
- Cypress.session.clearAllSavedSessions();
-
- let teamId;
- let userId;
-
- before(() => {
- cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
- cy.fixture('users').then(data => {
- const userCreate = data.userCreate;
- cy.request({
- method: 'POST',
- url: '/api/users',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- body: userCreate,
- }).then(response => {
- userId = response.body.id;
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('username', 'cypress1');
- expect(response.body).to.have.property('role', 'user');
- });
- });
- });
-
- it('Creates a team.', () => {
- cy.fixture('teams').then(data => {
- const teamCreate = data.teamCreate;
- cy.request({
- method: 'POST',
- url: '/api/teams',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- body: teamCreate,
- }).then(response => {
- teamId = response.body[0].id;
- expect(response.status).to.eq(200);
- expect(response.body[0]).to.have.property('name', 'cypress');
- expect(response.body[1]).to.have.property('role', 'team-owner');
- });
- });
- });
-
- it('Gets a teams by ID.', () => {
- cy.request({
- method: 'GET',
- url: `/api/teams/${teamId}`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('id', teamId);
- });
- });
-
- it('Updates a team.', () => {
- cy.fixture('teams').then(data => {
- const teamUpdate = data.teamUpdate;
- cy.request({
- method: 'POST',
- url: `/api/teams/${teamId}`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- body: teamUpdate,
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('id', teamId);
- expect(response.body).to.have.property('name', 'cypressUpdate');
- });
- });
- });
-
- it('Get all users that belong to a team.', () => {
- cy.request({
- method: 'GET',
- url: `/api/teams/${teamId}/users`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body.data[0]).to.have.property('id');
- expect(response.body.data[0]).to.have.property('teamId');
- expect(response.body.data[0]).to.have.property('userId');
- expect(response.body.data[0]).to.have.property('user');
- });
- });
-
- it('Get a user belonging to a team.', () => {
- cy.request({
- method: 'GET',
- url: `/api/teams/${teamId}/users/${Cypress.env('umami_user_id')}`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('teamId');
- expect(response.body).to.have.property('userId');
- expect(response.body).to.have.property('role');
- });
- });
-
- it('Get all websites belonging to a team.', () => {
- cy.request({
- method: 'GET',
- url: `/api/teams/${teamId}/websites`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('data');
- });
- });
-
- it('Add a user to a team.', () => {
- cy.request({
- method: 'POST',
- url: `/api/teams/${teamId}/users`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- body: {
- userId,
- role: 'team-member',
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('userId', userId);
- expect(response.body).to.have.property('role', 'team-member');
- });
- });
-
- it(`Update a user's role on a team.`, () => {
- cy.request({
- method: 'POST',
- url: `/api/teams/${teamId}/users/${userId}`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- body: {
- role: 'team-view-only',
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('userId', userId);
- expect(response.body).to.have.property('role', 'team-view-only');
- });
- });
-
- it(`Remove a user from a team.`, () => {
- cy.request({
- method: 'DELETE',
- url: `/api/teams/${teamId}/users/${userId}`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- });
- });
-
- it('Deletes a team.', () => {
- cy.request({
- method: 'DELETE',
- url: `/api/teams/${teamId}`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('ok', true);
- });
- });
-
- // it('Gets all teams that belong to a user.', () => {
- // cy.request({
- // method: 'GET',
- // url: `/api/users/${userId}/teams`,
- // headers: {
- // 'Content-Type': 'application/json',
- // Authorization: Cypress.env('authorization'),
- // },
- // }).then(response => {
- // expect(response.status).to.eq(200);
- // expect(response.body).to.have.property('data');
- // });
- // });
-
- after(() => {
- cy.deleteUser(userId);
- });
-});
diff --git a/cypress/e2e/api-user.cy.ts b/cypress/e2e/api-user.cy.ts
deleted file mode 100644
index 696cd21e4..000000000
--- a/cypress/e2e/api-user.cy.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-describe('User API tests', () => {
- Cypress.session.clearAllSavedSessions();
-
- before(() => {
- cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
- });
-
- let userId;
-
- it('Creates a user.', () => {
- cy.fixture('users').then(data => {
- const userCreate = data.userCreate;
- cy.request({
- method: 'POST',
- url: '/api/users',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- body: userCreate,
- }).then(response => {
- userId = response.body.id;
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('username', 'cypress1');
- expect(response.body).to.have.property('role', 'user');
- });
- });
- });
-
- it('Returns all users. Admin access is required.', () => {
- cy.request({
- method: 'GET',
- url: '/api/admin/users',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body.data[0]).to.have.property('id');
- expect(response.body.data[0]).to.have.property('username');
- expect(response.body.data[0]).to.have.property('password');
- expect(response.body.data[0]).to.have.property('role');
- });
- });
-
- it('Updates a user.', () => {
- cy.fixture('users').then(data => {
- const userUpdate = data.userUpdate;
- cy.request({
- method: 'POST',
- url: `/api/users/${userId}`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- body: userUpdate,
- }).then(response => {
- userId = response.body.id;
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('id', userId);
- expect(response.body).to.have.property('username', 'cypress1');
- expect(response.body).to.have.property('role', 'view-only');
- });
- });
- });
-
- it('Gets a user by ID.', () => {
- cy.request({
- method: 'GET',
- url: `/api/users/${userId}`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('id', userId);
- expect(response.body).to.have.property('username', 'cypress1');
- expect(response.body).to.have.property('role', 'view-only');
- });
- });
-
- it('Deletes a user.', () => {
- cy.request({
- method: 'DELETE',
- url: `/api/users/${userId}`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('ok', true);
- });
- });
-
- it('Gets all websites that belong to a user.', () => {
- cy.request({
- method: 'GET',
- url: `/api/users/${userId}/websites`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('data');
- });
- });
-
- it('Gets all teams that belong to a user.', () => {
- cy.request({
- method: 'GET',
- url: `/api/users/${userId}/teams`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('data');
- });
- });
-});
diff --git a/cypress/e2e/api-website.cy.ts b/cypress/e2e/api-website.cy.ts
deleted file mode 100644
index cd336bda0..000000000
--- a/cypress/e2e/api-website.cy.ts
+++ /dev/null
@@ -1,198 +0,0 @@
-import { uuid } from '../../src/lib/crypto';
-
-describe('Website API tests', () => {
- Cypress.session.clearAllSavedSessions();
-
- let websiteId;
- let teamId;
-
- before(() => {
- cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
- cy.fixture('teams').then(data => {
- const teamCreate = data.teamCreate;
- cy.request({
- method: 'POST',
- url: '/api/teams',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- body: teamCreate,
- }).then(response => {
- teamId = response.body[0].id;
- expect(response.status).to.eq(200);
- expect(response.body[0]).to.have.property('name', 'cypress');
- expect(response.body[1]).to.have.property('role', 'team-owner');
- });
- });
- });
-
- it('Creates a website for user.', () => {
- cy.fixture('websites').then(data => {
- const websiteCreate = data.websiteCreate;
- cy.request({
- method: 'POST',
- url: '/api/websites',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- body: websiteCreate,
- }).then(response => {
- websiteId = response.body.id;
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('name', 'Cypress Website');
- expect(response.body).to.have.property('domain', 'cypress.com');
- });
- });
- });
-
- it('Creates a website for team.', () => {
- cy.request({
- method: 'POST',
- url: '/api/websites',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- body: {
- name: 'Team Website',
- domain: 'teamwebsite.com',
- teamId: teamId,
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('name', 'Team Website');
- expect(response.body).to.have.property('domain', 'teamwebsite.com');
- });
- });
-
- it('Creates a website with a fixed ID.', () => {
- cy.fixture('websites').then(data => {
- const websiteCreate = data.websiteCreate;
- const fixedId = uuid();
- cy.request({
- method: 'POST',
- url: '/api/websites',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- body: { ...websiteCreate, id: fixedId },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('id', fixedId);
- expect(response.body).to.have.property('name', 'Cypress Website');
- expect(response.body).to.have.property('domain', 'cypress.com');
-
- // cleanup
- cy.request({
- method: 'DELETE',
- url: `/api/websites/${fixedId}`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- });
- });
- });
- });
-
- it('Returns all tracked websites.', () => {
- cy.request({
- method: 'GET',
- url: '/api/websites',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body.data[0]).to.have.property('id');
- expect(response.body.data[0]).to.have.property('name');
- expect(response.body.data[0]).to.have.property('domain');
- });
- });
-
- it('Gets a website by ID.', () => {
- cy.request({
- method: 'GET',
- url: `/api/websites/${websiteId}`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('name', 'Cypress Website');
- expect(response.body).to.have.property('domain', 'cypress.com');
- });
- });
-
- it('Updates a website.', () => {
- cy.fixture('websites').then(data => {
- const websiteUpdate = data.websiteUpdate;
- cy.request({
- method: 'POST',
- url: `/api/websites/${websiteId}`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- body: websiteUpdate,
- }).then(response => {
- websiteId = response.body.id;
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('name', 'Cypress Website Updated');
- expect(response.body).to.have.property('domain', 'cypressupdated.com');
- });
- });
- });
-
- it('Updates a website with only shareId.', () => {
- cy.request({
- method: 'POST',
- url: `/api/websites/${websiteId}`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- body: { shareId: 'ABCDEF' },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('shareId', 'ABCDEF');
- });
- });
-
- it('Resets a website by removing all data related to the website.', () => {
- cy.request({
- method: 'POST',
- url: `/api/websites/${websiteId}/reset`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('ok', true);
- });
- });
-
- it('Deletes a website.', () => {
- cy.request({
- method: 'DELETE',
- url: `/api/websites/${websiteId}`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- expect(response.body).to.have.property('ok', true);
- });
- });
-
- after(() => {
- cy.deleteTeam(teamId);
- });
-});
diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts
deleted file mode 100644
index 507b1b580..000000000
--- a/cypress/e2e/login.cy.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-describe('Login tests', () => {
- beforeEach(() => {
- cy.visit('/login');
- });
-
- it(
- 'logs user in with correct credentials and logs user out',
- {
- defaultCommandTimeout: 10000,
- },
- () => {
- cy.getDataTest('input-username').find('input').as('inputUsername').click();
- cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 });
- cy.get('@inputUsername').click();
- cy.getDataTest('input-password')
- .find('input')
- .type(Cypress.env('umami_password'), { delay: 0 });
- cy.getDataTest('button-submit').click();
- cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
- cy.logout();
- },
- );
-
- it('login with blank inputs or incorrect credentials', () => {
- cy.getDataTest('button-submit').click();
- cy.contains(/Required/i).should('be.visible');
-
- cy.getDataTest('input-username').find('input').as('inputUsername');
- cy.get('@inputUsername').click();
- cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 });
- cy.get('@inputUsername').click();
- cy.getDataTest('input-password').find('input').type('wrongpassword', { delay: 0 });
- cy.getDataTest('button-submit').click();
- cy.contains(/Incorrect username and\/or password./i).should('be.visible');
- });
-});
diff --git a/cypress/e2e/user.cy.ts b/cypress/e2e/user.cy.ts
deleted file mode 100644
index 24df30539..000000000
--- a/cypress/e2e/user.cy.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-describe('User tests', () => {
- Cypress.session.clearAllSavedSessions();
-
- beforeEach(() => {
- cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
- cy.visit('/settings/users');
- });
-
- it('Add a User', () => {
- // add user
- cy.contains(/Create user/i).should('be.visible');
- cy.getDataTest('button-create-user').click();
- cy.getDataTest('input-username').find('input').as('inputName').click();
- cy.get('@inputName').type('Test-user', { delay: 0 });
- cy.getDataTest('input-password').find('input').as('inputPassword').click();
- cy.get('@inputPassword').type('testPasswordCypress', { delay: 0 });
- cy.getDataTest('dropdown-role').click();
- cy.getDataTest('dropdown-item-user').click();
- cy.getDataTest('button-submit').click();
- cy.get('td[label="Username"]').should('contain.text', 'Test-user');
- cy.get('td[label="Role"]').should('contain.text', 'User');
- });
-
- it('Edit a User role and password', () => {
- // edit user
- cy.get('table tbody tr')
- .contains('td', /Test-user/i)
- .parent()
- .within(() => {
- cy.getDataTest('link-button-edit').click(); // Clicks the button inside the row
- });
- cy.getDataTest('input-password').find('input').as('inputPassword').click();
- cy.get('@inputPassword').type('newPassword', { delay: 0 });
- cy.getDataTest('dropdown-role').click();
- cy.getDataTest('dropdown-item-viewOnly').click();
- cy.getDataTest('button-submit').click();
-
- cy.visit('/settings/users');
- cy.get('table tbody tr')
- .contains('td', /Test-user/i)
- .parent()
- .should('contain.text', 'View only');
-
- cy.logout();
- cy.url().should('eq', Cypress.config().baseUrl + '/login');
- cy.getDataTest('input-username').find('input').as('inputUsername').click();
- cy.get('@inputUsername').type('Test-user', { delay: 0 });
- cy.get('@inputUsername').click();
- cy.getDataTest('input-password').find('input').type('newPassword', { delay: 0 });
- cy.getDataTest('button-submit').click();
- cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
- });
-
- it('Delete a user', () => {
- // delete user
- cy.get('table tbody tr')
- .contains('td', /Test-user/i)
- .parent()
- .within(() => {
- cy.getDataTest('button-delete').click(); // Clicks the button inside the row
- });
- cy.contains(/Are you sure you want to delete Test-user?/i).should('be.visible');
- cy.getDataTest('button-confirm').click();
- });
-});
diff --git a/cypress/e2e/website.cy.ts b/cypress/e2e/website.cy.ts
deleted file mode 100644
index 2dcd60273..000000000
--- a/cypress/e2e/website.cy.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-describe('Website tests', () => {
- Cypress.session.clearAllSavedSessions();
-
- beforeEach(() => {
- cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
- });
-
- it('Add a website', () => {
- // add website
- cy.visit('/settings/websites');
- cy.getDataTest('button-website-add').click();
- cy.contains(/Add website/i).should('be.visible');
- cy.getDataTest('input-name').find('input').as('inputUsername').click();
- cy.getDataTest('input-name').find('input').type('Add test', { delay: 0 });
- cy.getDataTest('input-domain').find('input').click();
- cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 0 });
- cy.getDataTest('button-submit').click();
- cy.get('td[label="Name"]').should('contain.text', 'Add test');
- cy.get('td[label="Domain"]').should('contain.text', 'addtest.com');
-
- // clean-up data
- cy.getDataTest('link-button-edit').first().click();
- cy.contains(/Details/i).should('be.visible');
- cy.getDataTest('text-field-websiteId')
- .find('input')
- .then($input => {
- const websiteId = $input[0].value;
- cy.deleteWebsite(websiteId);
- });
- cy.visit('/settings/websites');
- cy.contains(/Add test/i).should('not.exist');
- });
-
- it('Edit a website', () => {
- // prep data
- cy.addWebsite('Update test', 'updatetest.com');
- cy.visit('/settings/websites');
-
- // edit website
- cy.getDataTest('link-button-edit').first().click();
- cy.contains(/Details/i).should('be.visible');
- cy.getDataTest('input-name').find('input').click();
- cy.getDataTest('input-name').find('input').clear();
- cy.getDataTest('input-name').find('input').type('Updated website', { delay: 0 });
- cy.getDataTest('input-domain').find('input').click();
- cy.getDataTest('input-domain').find('input').clear();
- cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 0 });
- cy.getDataTest('button-submit').click({ force: true });
- cy.getDataTest('input-name').find('input').should('have.value', 'Updated website');
- cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com');
-
- // verify tracking script
- cy.get('div')
- .contains(/Tracking code/i)
- .click();
- cy.get('textarea').should('contain.text', Cypress.config().baseUrl + '/script.js');
-
- // clean-up data
- cy.get('div')
- .contains(/Details/i)
- .click();
- cy.contains(/Details/i).should('be.visible');
- cy.getDataTest('text-field-websiteId')
- .find('input')
- .then($input => {
- const websiteId = $input[0].value;
- cy.deleteWebsite(websiteId);
- });
- cy.visit('/settings/websites');
- cy.contains(/Add test/i).should('not.exist');
- });
-
- it('Delete a website', () => {
- // prep data
- cy.addWebsite('Delete test', 'deletetest.com');
- cy.visit('/settings/websites');
-
- // delete website
- cy.getDataTest('link-button-edit').first().click();
- cy.contains(/Data/i).should('be.visible');
- cy.get('div').contains(/Data/i).click();
- cy.contains(/All website data will be deleted./i).should('be.visible');
- cy.getDataTest('button-delete').click();
- cy.contains(/Type DELETE in the box below to confirm./i).should('be.visible');
- cy.get('input[name="confirm"').type('DELETE');
- cy.get('button[type="submit"]').click();
- cy.contains(/Delete test/i).should('not.exist');
- });
-});
diff --git a/cypress/fixtures/teams.json b/cypress/fixtures/teams.json
deleted file mode 100644
index 4c001e655..000000000
--- a/cypress/fixtures/teams.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "teamCreate": {
- "name": "cypress"
- },
- "teamUpdate": {
- "name": "cypressUpdate"
- }
-}
diff --git a/cypress/fixtures/users.json b/cypress/fixtures/users.json
deleted file mode 100644
index 235f0d637..000000000
--- a/cypress/fixtures/users.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "userCreate": {
- "username": "cypress1",
- "password": "password",
- "role": "user"
- },
- "userUpdate": {
- "username": "cypress1",
- "role": "view-only"
- }
-}
diff --git a/cypress/fixtures/websites.json b/cypress/fixtures/websites.json
deleted file mode 100644
index 0b817889b..000000000
--- a/cypress/fixtures/websites.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "websiteCreate": {
- "name": "Cypress Website",
- "domain": "cypress.com"
- },
- "websiteUpdate": {
- "name": "Cypress Website Updated",
- "domain": "cypressupdated.com"
- }
-}
diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts
deleted file mode 100644
index a95035ba0..000000000
--- a/cypress/support/e2e.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-///
-import { uuid } from '../../src/lib/crypto';
-
-Cypress.Commands.add('getDataTest', (value: string) => {
- return cy.get(`[data-test=${value}]`);
-});
-
-Cypress.Commands.add('logout', () => {
- cy.getDataTest('button-profile').click();
- cy.getDataTest('item-logout').click();
- cy.url().should('eq', Cypress.config().baseUrl + '/login');
-});
-
-Cypress.Commands.add('login', (username: string, password: string) => {
- cy.session([username, password], () => {
- cy.request({
- method: 'POST',
- url: '/api/auth/login',
- body: {
- username,
- password,
- },
- })
- .then(response => {
- Cypress.env('authorization', `bearer ${response.body.token}`);
- window.localStorage.setItem('umami.auth', JSON.stringify(response.body.token));
- })
- .its('status')
- .should('eq', 200);
- });
-});
-
-Cypress.Commands.add('addWebsite', (name: string, domain: string) => {
- cy.request({
- method: 'POST',
- url: '/api/websites',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- body: {
- id: uuid(),
- createdBy: '41e2b680-648e-4b09-bcd7-3e2b10c06264',
- name: name,
- domain: domain,
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- });
-});
-
-Cypress.Commands.add('deleteWebsite', (websiteId: string) => {
- cy.request({
- method: 'DELETE',
- url: `/api/websites/${websiteId}`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- });
-});
-
-Cypress.Commands.add('addUser', (username: string, password: string, role: string) => {
- cy.request({
- method: 'POST',
- url: '/api/users',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- body: {
- username: username,
- password: password,
- role: role,
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- });
-});
-
-Cypress.Commands.add('deleteUser', (userId: string) => {
- cy.request({
- method: 'DELETE',
- url: `/api/users/${userId}`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- });
-});
-
-Cypress.Commands.add('addTeam', (name: string) => {
- cy.request({
- method: 'POST',
- url: '/api/teams',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- body: {
- name: name,
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- });
-});
-
-Cypress.Commands.add('deleteTeam', (teamId: string) => {
- cy.request({
- method: 'DELETE',
- url: `/api/teams/${teamId}`,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: Cypress.env('authorization'),
- },
- }).then(response => {
- expect(response.status).to.eq(200);
- });
-});
diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts
deleted file mode 100644
index b63026905..000000000
--- a/cypress/support/index.d.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-///
-/* global JQuery */
-
-declare namespace Cypress {
- interface Chainable {
- /**
- * Custom command to select DOM element by data-test attribute.
- * @example cy.getDataTest('greeting')
- */
- getDataTest(value: string): Chainable>;
- /**
- * Custom command to logout through UI.
- * @example cy.logout()
- */
- logout(): Chainable>;
- /**
- * Custom command to login user into the app.
- * @example cy.login('admin', 'password)
- */
- login(username: string, password: string): Chainable>;
- /**
- * Custom command to create a website
- * @example cy.addWebsite('test', 'test.com')
- */
- addWebsite(name: string, domain: string): Chainable>;
- /**
- * Custom command to delete a website
- * @example cy.deleteWebsite('02d89813-7a72-41e1-87f0-8d668f85008b')
- */
- deleteWebsite(websiteId: string): Chainable>;
- /**
- * Custom command to create a website
- * @example cy.deleteWebsite('02d89813-7a72-41e1-87f0-8d668f85008b')
- */
- /**
- * Custom command to create a user
- * @example cy.addUser('cypress', 'password', 'User')
- */
- addUser(username: string, password: string, role: string): Chainable>;
- /**
- * Custom command to delete a user
- * @example cy.deleteUser('02d89813-7a72-41e1-87f0-8d668f85008b')
- */
- deleteUser(userId: string): Chainable>;
- /**
- * Custom command to create a team
- * @example cy.addTeam('cypressTeam')
- */
- addTeam(name: string): Chainable>;
- /**
- * Custom command to create a website
- * @example cy.deleteTeam('02d89813-7a72-41e1-87f0-8d668f85008b')
- */
- deleteTeam(teamId: string): Chainable>;
- }
-}
diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json
deleted file mode 100644
index 48c3e14e4..000000000
--- a/cypress/tsconfig.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "compilerOptions": {
- "target": "es5",
- "lib": ["es5", "dom"],
- "types": ["cypress", "node"]
- },
- "include": ["**/*.ts", "../cypress.config.ts"]
-}
diff --git a/db/clickhouse/migrations/11_add_event_data_pivot.sql b/db/clickhouse/migrations/11_add_event_session_data_pivot.sql
similarity index 78%
rename from db/clickhouse/migrations/11_add_event_data_pivot.sql
rename to db/clickhouse/migrations/11_add_event_session_data_pivot.sql
index 15ba1a7e8..708826786 100644
--- a/db/clickhouse/migrations/11_add_event_data_pivot.sql
+++ b/db/clickhouse/migrations/11_add_event_session_data_pivot.sql
@@ -35,7 +35,7 @@ AS SELECT
FROM umami.event_data
GROUP BY website_id, session_id, event_id, event_name, url_path, created_at;
--- Backfill existing data
+-- Backfill existing event data
INSERT INTO umami.event_data_pivot
SELECT
website_id,
@@ -54,3 +54,22 @@ SELECT
groupArrayState(data_type)
FROM umami.event_data
GROUP BY website_id, session_id, event_id, event_name, url_path, created_at;
+
+ALTER TABLE umami.session_data
+MODIFY SETTING deduplicate_merge_projection_mode = 'drop';
+
+ALTER TABLE umami.session_data
+ADD PROJECTION session_data_property_filter_projection (
+ SELECT *
+ ORDER BY (
+ website_id,
+ data_key,
+ data_type,
+ string_value,
+ number_value,
+ date_value,
+ session_id
+ )
+);
+
+ALTER TABLE umami.session_data MATERIALIZE PROJECTION session_data_property_filter_projection;
diff --git a/db/clickhouse/migrations/12_add_heatmap.sql b/db/clickhouse/migrations/12_add_heatmap.sql
new file mode 100644
index 000000000..f6bb316c4
--- /dev/null
+++ b/db/clickhouse/migrations/12_add_heatmap.sql
@@ -0,0 +1,24 @@
+-- Create heatmap_event
+CREATE TABLE umami.heatmap_event
+(
+ heatmap_event_id UUID,
+ website_id UUID,
+ session_id UUID,
+ visit_id UUID,
+ url_path String,
+ event_type UInt8,
+ x Nullable(Int32),
+ y Nullable(Int32),
+ page_x Nullable(Int32),
+ page_y Nullable(Int32),
+ page_w Nullable(Int32),
+ viewport_w Nullable(Int32),
+ viewport_h Nullable(Int32),
+ page_h Nullable(Int32),
+ scroll_pct Nullable(UInt8),
+ created_at DateTime('UTC')
+)
+ENGINE = MergeTree
+ PARTITION BY toYYYYMM(created_at)
+ ORDER BY (website_id, url_path, event_type, created_at)
+ SETTINGS index_granularity = 8192;
\ No newline at end of file
diff --git a/db/clickhouse/migrations/13_add_session_link.sql b/db/clickhouse/migrations/13_add_session_link.sql
new file mode 100644
index 000000000..2455c05e7
--- /dev/null
+++ b/db/clickhouse/migrations/13_add_session_link.sql
@@ -0,0 +1,12 @@
+-- Create session_link
+CREATE TABLE umami.session_link
+(
+ website_id UUID,
+ session_id UUID,
+ distinct_id String,
+ created_at DateTime('UTC'),
+ INDEX idx_session_id session_id TYPE bloom_filter GRANULARITY 1
+)
+ENGINE = ReplacingMergeTree
+ ORDER BY (website_id, distinct_id, session_id)
+ SETTINGS index_granularity = 8192;
diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql
index 0540c97b2..1030538c1 100644
--- a/db/clickhouse/schema.sql
+++ b/db/clickhouse/schema.sql
@@ -90,6 +90,25 @@ ENGINE = ReplacingMergeTree
ORDER BY (website_id, session_id, data_key)
SETTINGS index_granularity = 8192;
+ALTER TABLE umami.session_data
+MODIFY SETTING deduplicate_merge_projection_mode = 'drop';
+
+ALTER TABLE umami.session_data
+ADD PROJECTION session_data_property_filter_projection (
+ SELECT *
+ ORDER BY (
+ website_id,
+ data_key,
+ data_type,
+ string_value,
+ number_value,
+ date_value,
+ session_id
+ )
+);
+
+ALTER TABLE umami.session_data MATERIALIZE PROJECTION session_data_property_filter_projection;
+
-- stats hourly
CREATE TABLE umami.website_event_stats_hourly
(
@@ -344,3 +363,77 @@ AS SELECT
groupArrayState(data_type) AS property_types
FROM umami.event_data
GROUP BY website_id, session_id, event_id, event_name, url_path, created_at;
+
+-- Create session_data_pivot
+CREATE TABLE IF NOT EXISTS umami.session_data_pivot
+(
+ website_id UUID,
+ session_id UUID,
+ distinct_id String,
+ created_year_month UInt32,
+ created_at AggregateFunction(max, DateTime('UTC')),
+ property_keys AggregateFunction(groupArray, String),
+ property_values AggregateFunction(groupArray, String),
+ property_types AggregateFunction(groupArray, UInt32)
+)
+ENGINE = AggregatingMergeTree()
+PARTITION BY created_year_month
+ORDER BY (website_id, session_id, distinct_id)
+SETTINGS index_granularity = 8192;
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS umami.session_data_pivot_mv
+TO umami.session_data_pivot
+AS SELECT
+ website_id,
+ session_id,
+ ifNull(distinct_id, '') AS distinct_id,
+ toYYYYMM(max(session_data.created_at)) AS created_year_month,
+ maxState(session_data.created_at) AS created_at,
+ groupArrayState(data_key) AS property_keys,
+ groupArrayState(multiIf(
+ data_type IN (1, 3, 5), ifNull(string_value, ''),
+ data_type = 2, toString(ifNull(number_value, 0)),
+ data_type = 4, toString(ifNull(date_value, toDateTime(0))),
+ ''
+ )) AS property_values,
+ groupArrayState(data_type) AS property_types
+FROM umami.session_data
+GROUP BY website_id, session_id, distinct_id;
+
+-- Create heatmap_event
+CREATE TABLE umami.heatmap_event
+(
+ heatmap_event_id UUID,
+ website_id UUID,
+ session_id UUID,
+ visit_id UUID,
+ url_path String,
+ event_type UInt8,
+ x Nullable(Int32),
+ y Nullable(Int32),
+ page_x Nullable(Int32),
+ page_y Nullable(Int32),
+ page_w Nullable(Int32),
+ viewport_w Nullable(Int32),
+ viewport_h Nullable(Int32),
+ page_h Nullable(Int32),
+ scroll_pct Nullable(UInt8),
+ created_at DateTime('UTC')
+)
+ENGINE = MergeTree
+ PARTITION BY toYYYYMM(created_at)
+ ORDER BY (website_id, url_path, event_type, created_at)
+ SETTINGS index_granularity = 8192;
+
+-- Create session_link
+CREATE TABLE umami.session_link
+(
+ website_id UUID,
+ session_id UUID,
+ distinct_id String,
+ created_at DateTime('UTC'),
+ INDEX idx_session_id session_id TYPE bloom_filter GRANULARITY 1
+)
+ENGINE = ReplacingMergeTree
+ ORDER BY (website_id, distinct_id, session_id)
+ SETTINGS index_granularity = 8192;
diff --git a/jest.config.ts b/jest.config.ts
deleted file mode 100644
index d06ac09a6..000000000
--- a/jest.config.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export default {
- roots: ['./src'],
- testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'],
- transform: {
- '^.+\\.(ts|tsx)$': 'ts-jest',
- },
- moduleNameMapper: {
- '^@/(.*)$': '/src/$1',
- },
-};
diff --git a/next.config.ts b/next.config.ts
index ab7fbf484..922d128bd 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -8,6 +8,7 @@ const TRACKER_SCRIPT = '/script.js';
const isProd = process.env.NODE_ENV === 'production';
+const apiUrl = process.env.API_URL || '';
const basePath = process.env.BASE_PATH || '';
const cloudMode = process.env.CLOUD_MODE || '';
const cloudUrl = process.env.CLOUD_URL || '';
@@ -22,12 +23,32 @@ const trackerScriptURL = process.env.TRACKER_SCRIPT_URL || '';
const selfTrack = process.env.UMAMI_SELF_TRACK || '';
const selfRecord = process.env.UMAMI_SELF_RECORD || '';
+function getUrlOrigin(url: string) {
+ try {
+ return new URL(url).origin;
+ } catch {
+ return '';
+ }
+}
+
+function isRelativeUrl(url: string) {
+ return Boolean(url && !/^https?:\/\//i.test(url));
+}
+
+function normalizePath(url: string) {
+ return `/${url.replace(/^\/+|\/+$/g, '')}`;
+}
+
+const apiUrlOrigin = getUrlOrigin(apiUrl);
+const connectSrc = ["'self'", 'https:', apiUrlOrigin].filter(Boolean).join(' ');
+
const contentSecurityPolicy = `
default-src 'self';
- img-src 'self' https: data:;
+ img-src 'self' https: data: blob:;
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
- connect-src 'self' https:;
+ connect-src ${connectSrc};
+ frame-src 'self' http: https:;
frame-ancestors 'self' ${frameAncestors};
`;
@@ -122,6 +143,22 @@ if (collectApiEndpoint) {
});
}
+if (isRelativeUrl(apiUrl)) {
+ const normalizedApiUrl = normalizePath(apiUrl);
+
+ if (normalizedApiUrl !== '/' && normalizedApiUrl !== '/api') {
+ headers.push({
+ source: `${normalizedApiUrl}/:path*`,
+ headers: apiHeaders,
+ });
+
+ rewrites.push({
+ source: `${normalizedApiUrl}/:path*`,
+ destination: '/api/:path*',
+ });
+ }
+}
+
const redirects = [
{
source: '/teams/:id/dashboard/edit',
@@ -187,6 +224,7 @@ if (isProd && cloudMode) {
export default withNextIntl({
reactStrictMode: false,
env: {
+ apiUrl,
basePath,
cloudMode,
cloudUrl,
diff --git a/package.json b/package.json
index 2261b776e..5549f03e1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "umami",
- "version": "3.1.0",
+ "version": "3.2.0",
"description": "A modern, privacy-focused alternative to Google Analytics.",
"author": "Umami Software, Inc. ",
"license": "MIT",
@@ -21,7 +21,10 @@
"build-app": "next build --turbo",
"build-icons": "svgr ./src/assets --out-dir src/components/svg --typescript",
"build-components": "node scripts/bump-components.js && tsup",
- "build-tracker": "rollup -c rollup.tracker.config.js",
+ "build-tracker": "npm-run-all check-tracker build-tracker-script build-tracker-types",
+ "build-tracker-script": "rollup -c rollup.tracker.config.js",
+ "build-tracker-types": "tsc -p tsconfig.tracker.types.json && biome format --write src/tracker/index.d.ts",
+ "check-tracker": "tsc -p tsconfig.tracker.json --noEmit",
"build-recorder": "rollup -c rollup.recorder.config.js",
"build-prisma-client": "node scripts/build-prisma-client.js",
"build-lang": "npm-run-all download-country-names download-language-names",
@@ -39,9 +42,11 @@
"download-language-names": "node scripts/download-language-names.js",
"change-password": "node scripts/change-password.js",
"postbuild": "node scripts/postbuild.js",
- "test": "jest",
- "cypress-open": "cypress open cypress run",
- "cypress-run": "cypress run cypress run",
+ "pretest": "pnpm build-db-client",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "test:e2e": "playwright test",
+ "test:e2e:ui": "playwright test --ui",
"seed-data": "tsx scripts/seed-data.ts",
"lint": "biome lint .",
"format": "biome format --write .",
@@ -51,16 +56,17 @@
".next/cache"
],
"dependencies": {
- "@clickhouse/client": "^1.18.2",
+ "@clickhouse/client": "^1.22.0",
"@date-fns/utc": "^2.1.1",
"@dicebear/collection": "^9.4.2",
"@dicebear/core": "^9.4.2",
"@hello-pangea/dnd": "^18.0.1",
- "@prisma/adapter-pg": "^7.7.0",
- "@prisma/client": "^7.7.0",
+ "@playwright/test": "^1.61.1",
+ "@prisma/adapter-pg": "^7.8.0",
+ "@prisma/client": "^7.8.0",
"@prisma/extension-read-replicas": "^0.5.0",
"@svgr/cli": "^8.1.0",
- "@tanstack/react-query": "^5.99.0",
+ "@tanstack/react-query": "^5.101.1",
"@umami/react-zen": "^0.245.0",
"bcryptjs": "^3.0.2",
"chalk": "^5.6.2",
@@ -70,27 +76,27 @@
"colord": "^2.9.2",
"cors": "^2.8.6",
"cross-spawn": "^7.0.3",
- "date-fns": "^4.1.0",
+ "date-fns": "^4.4.0",
"date-fns-tz": "^3.2.0",
"debug": "^4.4.3",
"del": "^8.0.1",
"detect-browser": "^5.2.0",
"dotenv": "^17.4.2",
- "esbuild": "^0.28.0",
- "immer": "^11.1.4",
- "ipaddr.js": "^2.3.0",
+ "esbuild": "^0.28.1",
+ "immer": "^11.1.8",
+ "ipaddr.js": "^2.4.0",
"is-ci": "^4.1.0",
"is-docker": "^4.0.0",
"is-localhost-ip": "^3.0.1",
- "isbot": "^5.1.39",
+ "isbot": "^5.1.44",
"jsonwebtoken": "^9.0.3",
"jszip": "^3.10.1",
"kafkajs": "^2.1.0",
- "lucide-react": "^1.8.0",
- "maxmind": "^5.0.6",
- "motion": "^12.38.0",
- "next": "16.2.4",
- "next-intl": "4.9.1",
+ "lucide-react": "^1.21.0",
+ "maxmind": "^5.0.5",
+ "motion": "^12.41.0",
+ "next": "16.2.9",
+ "next-intl": "4.13.0",
"node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5",
"otplib": "^13.4.0",
@@ -103,26 +109,36 @@
"react-dom": "^19.2.5",
"react-error-boundary": "^6.1.1",
"react-resizable-panels": "^4.10.0",
+ "papaparse": "^5.5.4",
+ "pg": "^8.22.0",
+ "prisma": "^7.8.0",
+ "prop-types": "^15.8.1",
+ "punycode": "^2.3.1",
+ "react": "^19.2.7",
+ "react-aria-components": "^1.19.0",
+ "react-dom": "^19.2.7",
+ "react-error-boundary": "^6.1.2",
+ "react-resizable-panels": "^4.11.2",
"react-simple-maps": "^3.0.0",
"react-use-measure": "^2.0.4",
"react-window": "^2.2.7",
"redis": "^5.12.1",
"request-ip": "^3.3.0",
- "rrweb": "2.0.0-alpha.4",
- "rrweb-player": "1.0.0-alpha.4",
- "semver": "^7.7.4",
+ "rrweb": "^2.0.1",
+ "rrweb-player": "2.0.0-alpha.20",
+ "semver": "^7.8.5",
"serialize-error": "^13.0.1",
- "thenby": "^1.4.0",
- "ua-parser-js": "^2.0.9",
- "uuid": "^13.0.0",
- "zod": "^4.3.6",
- "zustand": "^5.0.12"
+ "thenby": "^1.4.1",
+ "ua-parser-js": "^2.0.10",
+ "uuid": "^14.0.1",
+ "zod": "^4.4.3",
+ "zustand": "^5.0.14"
},
"devDependencies": {
- "@biomejs/biome": "^2.4.12",
- "@netlify/plugin-nextjs": "^5.15.9",
+ "@biomejs/biome": "^2.5.1",
+ "@netlify/plugin-nextjs": "^5.15.12",
"@rollup/plugin-alias": "^6.0.0",
- "@rollup/plugin-commonjs": "^29.0.2",
+ "@rollup/plugin-commonjs": "^29.0.3",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.3",
@@ -132,30 +148,35 @@
"@types/node": "^25.6.0",
"@types/qrcode": "^1.5.6",
"@types/react": "^19.2.14",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/node": "^26.0.1",
+ "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.2",
"babel-plugin-react-compiler": "19.1.0-rc.2",
"cross-env": "^10.1.0",
- "cypress": "^15.14.0",
"dotenv-cli": "^11.0.0",
- "jest": "^30.3.0",
- "postcss": "^8.5.10",
+ "jsdom": "^29.1.1",
+ "msw": "^2.14.6",
+ "postcss": "^8.5.15",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-import": "^16.1.1",
- "postcss-preset-env": "11.2.1",
+ "postcss-preset-env": "11.3.1",
"prompts": "2.4.2",
- "rollup": "^4.60.1",
+ "rollup": "^4.62.2",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-delete": "^3.0.2",
"rollup-plugin-dts": "^6.4.1",
"rollup-plugin-node-externals": "^9.0.1",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-postcss": "^4.0.2",
- "tar": "^7.5.13",
- "ts-jest": "^29.4.9",
+ "tar": "^7.5.16",
"ts-morph": "^28.0.0",
"ts-node": "^10.9.1",
"tsup": "^8.5.0",
- "tsx": "^4.19.0",
- "typescript": "^6.0.3"
+ "tsx": "^4.22.4",
+ "typescript": "^6.0.3",
+ "vitest": "^4.1.9"
}
}
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 000000000..04636abb9
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,33 @@
+///
+import { defineConfig, devices } from '@playwright/test';
+
+const port = process.env.PORT ?? '3000';
+const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://localhost:${port}`;
+
+export default defineConfig({
+ testDir: './tests/e2e',
+ fullyParallel: false,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: process.env.CI ? 1 : undefined,
+ reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
+ use: {
+ baseURL,
+ testIdAttribute: 'data-test',
+ trace: 'on-first-retry',
+ },
+ webServer: process.env.PLAYWRIGHT_SKIP_WEB_SERVER
+ ? undefined
+ : {
+ command: process.env.PLAYWRIGHT_WEB_SERVER_COMMAND ?? 'pnpm dev',
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ timeout: 120_000,
+ },
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index afe27b77d..e71239a53 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@clickhouse/client':
- specifier: ^1.18.2
- version: 1.18.2
+ specifier: ^1.22.0
+ version: 1.22.0
'@date-fns/utc':
specifier: ^2.1.1
version: 2.1.1
@@ -22,25 +22,28 @@ importers:
version: 9.4.2
'@hello-pangea/dnd':
specifier: ^18.0.1
- version: 18.0.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ version: 18.0.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@playwright/test':
+ specifier: ^1.61.1
+ version: 1.61.1
'@prisma/adapter-pg':
- specifier: ^7.7.0
- version: 7.7.0
+ specifier: ^7.8.0
+ version: 7.8.0
'@prisma/client':
- specifier: ^7.7.0
- version: 7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3))(typescript@6.0.3)
+ specifier: ^7.8.0
+ version: 7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(typescript@6.0.3)
'@prisma/extension-read-replicas':
specifier: ^0.5.0
- version: 0.5.0(@prisma/client@7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3))(typescript@6.0.3))
+ version: 0.5.0(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(typescript@6.0.3))
'@svgr/cli':
specifier: ^8.1.0
version: 8.1.0(typescript@6.0.3)
'@tanstack/react-query':
- specifier: ^5.99.0
- version: 5.99.0(react@19.2.5)
+ specifier: ^5.101.1
+ version: 5.101.1(react@19.2.7)
'@umami/react-zen':
specifier: ^0.245.0
- version: 0.245.0(@types/react@19.2.14)(immer@11.1.4)(react-aria-components@1.17.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.1.18)(use-sync-external-store@1.6.0(react@19.2.5))
+ version: 0.245.0(@types/react@19.2.17)(immer@11.1.8)(react-aria-components@1.19.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tailwindcss@4.1.18)(use-sync-external-store@1.6.0(react@19.2.7))
bcryptjs:
specifier: ^3.0.2
version: 3.0.3
@@ -52,7 +55,7 @@ importers:
version: 4.5.1
chartjs-adapter-date-fns:
specifier: ^3.0.0
- version: 3.0.0(chart.js@4.5.1)(date-fns@4.1.0)
+ version: 3.0.0(chart.js@4.5.1)(date-fns@4.4.0)
classnames:
specifier: ^2.3.1
version: 2.5.1
@@ -66,14 +69,14 @@ importers:
specifier: ^7.0.3
version: 7.0.6
date-fns:
- specifier: ^4.1.0
- version: 4.1.0
+ specifier: ^4.4.0
+ version: 4.4.0
date-fns-tz:
specifier: ^3.2.0
- version: 3.2.0(date-fns@4.1.0)
+ version: 3.2.0(date-fns@4.4.0)
debug:
specifier: ^4.4.3
- version: 4.4.3(supports-color@8.1.1)
+ version: 4.4.3
del:
specifier: ^8.0.1
version: 8.0.1
@@ -84,14 +87,14 @@ importers:
specifier: ^17.4.2
version: 17.4.2
esbuild:
- specifier: ^0.28.0
- version: 0.28.0
+ specifier: ^0.28.1
+ version: 0.28.1
immer:
- specifier: ^11.1.4
- version: 11.1.4
+ specifier: ^11.1.8
+ version: 11.1.8
ipaddr.js:
- specifier: ^2.3.0
- version: 2.3.0
+ specifier: ^2.4.0
+ version: 2.4.0
is-ci:
specifier: ^4.1.0
version: 4.1.0
@@ -102,8 +105,8 @@ importers:
specifier: ^3.0.1
version: 3.0.1
isbot:
- specifier: ^5.1.39
- version: 5.1.39
+ specifier: ^5.1.44
+ version: 5.1.44
jsonwebtoken:
specifier: ^9.0.3
version: 9.0.3
@@ -114,20 +117,20 @@ importers:
specifier: ^2.1.0
version: 2.2.4
lucide-react:
- specifier: ^1.8.0
- version: 1.8.0(react@19.2.5)
+ specifier: ^1.21.0
+ version: 1.21.0(react@19.2.7)
maxmind:
- specifier: ^5.0.6
+ specifier: ^5.0.5
version: 5.0.6
motion:
- specifier: ^12.38.0
- version: 12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ specifier: ^12.41.0
+ version: 12.41.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
next:
- specifier: 16.2.4
- version: 16.2.4(@babel/core@7.29.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ specifier: 16.2.9
+ version: 16.2.9(@babel/core@7.29.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
next-intl:
- specifier: 4.9.1
- version: 4.9.1(@swc/helpers@0.5.21)(next@16.2.4(@babel/core@7.29.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.3)
+ specifier: 4.13.0
+ version: 4.13.0(@swc/helpers@0.5.23)(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
node-fetch:
specifier: ^3.2.8
version: 3.3.2
@@ -138,41 +141,44 @@ importers:
specifier: ^13.4.0
version: 13.4.0
papaparse:
- specifier: ^5.5.3
- version: 5.5.3
+ specifier: ^5.5.4
+ version: 5.5.4
pg:
- specifier: ^8.20.0
- version: 8.20.0
+ specifier: ^8.22.0
+ version: 8.22.0
prisma:
- specifier: ^7.7.0
- version: 7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3)
- pure-rand:
- specifier: ^8.4.0
- version: 8.4.0
- qrcode:
- specifier: ^1.5.4
- version: 1.5.4
+ specifier: ^7.8.0
+ version: 7.8.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
+ prop-types:
+ specifier: ^15.8.1
+ version: 15.8.1
+ punycode:
+ specifier: ^2.3.1
+ version: 2.3.1
react:
- specifier: ^19.2.5
- version: 19.2.5
+ specifier: ^19.2.7
+ version: 19.2.7
+ react-aria-components:
+ specifier: ^1.19.0
+ version: 1.19.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react-dom:
- specifier: ^19.2.5
- version: 19.2.5(react@19.2.5)
+ specifier: ^19.2.7
+ version: 19.2.7(react@19.2.7)
react-error-boundary:
- specifier: ^6.1.1
- version: 6.1.1(react@19.2.5)
+ specifier: ^6.1.2
+ version: 6.1.2(react@19.2.7)
react-resizable-panels:
- specifier: ^4.10.0
- version: 4.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ specifier: ^4.11.2
+ version: 4.11.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react-simple-maps:
specifier: ^3.0.0
- version: 3.0.0(prop-types@15.8.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ version: 3.0.0(prop-types@15.8.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react-use-measure:
specifier: ^2.0.4
- version: 2.1.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ version: 2.1.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react-window:
specifier: ^2.2.7
- version: 2.2.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ version: 2.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
redis:
specifier: ^5.12.1
version: 5.12.1
@@ -180,63 +186,69 @@ importers:
specifier: ^3.3.0
version: 3.3.0
rrweb:
- specifier: 2.0.0-alpha.4
- version: 2.0.0-alpha.4
+ specifier: ^2.0.1
+ version: 2.0.1
rrweb-player:
- specifier: 1.0.0-alpha.4
- version: 1.0.0-alpha.4
+ specifier: 2.0.0-alpha.20
+ version: 2.0.0-alpha.20
semver:
- specifier: ^7.7.4
- version: 7.7.4
+ specifier: ^7.8.5
+ version: 7.8.5
serialize-error:
specifier: ^13.0.1
version: 13.0.1
thenby:
- specifier: ^1.4.0
- version: 1.4.0
+ specifier: ^1.4.1
+ version: 1.4.1
ua-parser-js:
- specifier: ^2.0.9
- version: 2.0.9
+ specifier: ^2.0.10
+ version: 2.0.10
uuid:
- specifier: ^13.0.0
- version: 13.0.0
+ specifier: ^14.0.1
+ version: 14.0.1
zod:
- specifier: ^4.3.6
- version: 4.3.6
+ specifier: ^4.4.3
+ version: 4.4.3
zustand:
- specifier: ^5.0.12
- version: 5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))
+ specifier: ^5.0.14
+ version: 5.0.14(@types/react@19.2.17)(immer@11.1.8)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7))
devDependencies:
'@biomejs/biome':
- specifier: ^2.4.12
- version: 2.4.12
+ specifier: ^2.5.1
+ version: 2.5.1
'@netlify/plugin-nextjs':
- specifier: ^5.15.9
- version: 5.15.9
+ specifier: ^5.15.12
+ version: 5.15.12
'@rollup/plugin-alias':
specifier: ^6.0.0
- version: 6.0.0(rollup@4.60.1)
+ version: 6.0.0(rollup@4.62.2)
'@rollup/plugin-commonjs':
- specifier: ^29.0.2
- version: 29.0.2(rollup@4.60.1)
+ specifier: ^29.0.3
+ version: 29.0.3(rollup@4.62.2)
'@rollup/plugin-json':
specifier: ^6.0.0
- version: 6.1.0(rollup@4.60.1)
+ version: 6.1.0(rollup@4.62.2)
'@rollup/plugin-node-resolve':
specifier: ^16.0.3
- version: 16.0.3(rollup@4.60.1)
+ version: 16.0.3(rollup@4.62.2)
'@rollup/plugin-replace':
specifier: ^6.0.3
- version: 6.0.3(rollup@4.60.1)
+ version: 6.0.3(rollup@4.62.2)
'@rollup/plugin-terser':
specifier: ^1.0.0
- version: 1.0.0(rollup@4.60.1)
+ version: 1.0.0(rollup@4.62.2)
'@rollup/plugin-typescript':
specifier: ^12.3.0
- version: 12.3.0(rollup@4.60.1)(tslib@2.8.1)(typescript@6.0.3)
- '@types/jest':
- specifier: ^30.0.0
- version: 30.0.0
+ version: 12.3.0(rollup@4.62.2)(tslib@2.8.1)(typescript@6.0.3)
+ '@testing-library/jest-dom':
+ specifier: ^6.9.1
+ version: 6.9.1
+ '@testing-library/react':
+ specifier: ^16.3.2
+ version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@testing-library/user-event':
+ specifier: ^14.6.1
+ version: 14.6.1(@testing-library/dom@10.4.1)
'@types/node':
specifier: ^25.6.0
version: 25.6.0
@@ -244,90 +256,108 @@ importers:
specifier: ^1.5.6
version: 1.5.6
'@types/react':
- specifier: ^19.2.14
- version: 19.2.14
+ specifier: ^19.2.17
+ version: 19.2.17
'@types/react-dom':
specifier: ^19.2.2
- version: 19.2.3(@types/react@19.2.14)
+ version: 19.2.3(@types/react@19.2.17)
babel-plugin-react-compiler:
specifier: 19.1.0-rc.2
version: 19.1.0-rc.2
cross-env:
specifier: ^10.1.0
version: 10.1.0
- cypress:
- specifier: ^15.14.0
- version: 15.14.0
dotenv-cli:
specifier: ^11.0.0
version: 11.0.0
- jest:
- specifier: ^30.3.0
- version: 30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3))
+ jsdom:
+ specifier: ^29.1.1
+ version: 29.1.1
+ msw:
+ specifier: ^2.14.6
+ version: 2.14.6(@types/node@26.0.1)(typescript@6.0.3)
postcss:
- specifier: ^8.5.10
- version: 8.5.10
+ specifier: ^8.5.15
+ version: 8.5.15
postcss-flexbugs-fixes:
specifier: ^5.0.2
- version: 5.0.2(postcss@8.5.10)
+ version: 5.0.2(postcss@8.5.15)
postcss-import:
specifier: ^16.1.1
- version: 16.1.1(postcss@8.5.10)
+ version: 16.1.1(postcss@8.5.15)
postcss-preset-env:
- specifier: 11.2.1
- version: 11.2.1(postcss@8.5.10)
+ specifier: 11.3.1
+ version: 11.3.1(postcss@8.5.15)
prompts:
specifier: 2.4.2
version: 2.4.2
rollup:
- specifier: ^4.60.1
- version: 4.60.1
+ specifier: ^4.62.2
+ version: 4.62.2
rollup-plugin-copy:
specifier: ^3.4.0
version: 3.5.0
rollup-plugin-delete:
specifier: ^3.0.2
- version: 3.0.2(rollup@4.60.1)
+ version: 3.0.2(rollup@4.62.2)
rollup-plugin-dts:
specifier: ^6.4.1
- version: 6.4.1(rollup@4.60.1)(typescript@6.0.3)
+ version: 6.4.1(rollup@4.62.2)(typescript@6.0.3)
rollup-plugin-node-externals:
specifier: ^9.0.1
- version: 9.0.1(rollup@4.60.1)
+ version: 9.0.1(rollup@4.62.2)(vite@8.0.11(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.22.4))
rollup-plugin-peer-deps-external:
specifier: ^2.2.4
- version: 2.2.4(rollup@4.60.1)
+ version: 2.2.4(rollup@4.62.2)
rollup-plugin-postcss:
specifier: ^4.0.2
- version: 4.0.2(postcss@8.5.10)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3))
+ version: 4.0.2(postcss@8.5.15)(ts-node@10.9.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(@types/node@26.0.1)(typescript@6.0.3))
tar:
- specifier: ^7.5.13
- version: 7.5.13
- ts-jest:
- specifier: ^29.4.9
- version: 29.4.9(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(esbuild@0.28.0)(jest-util@30.3.0)(jest@30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)))(typescript@6.0.3)
+ specifier: ^7.5.16
+ version: 7.5.16
ts-morph:
specifier: ^28.0.0
version: 28.0.0
ts-node:
specifier: ^10.9.1
- version: 10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)
+ version: 10.9.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(@types/node@26.0.1)(typescript@6.0.3)
tsup:
specifier: ^8.5.0
- version: 8.5.1(@swc/core@1.15.26(@swc/helpers@0.5.21))(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.1)
+ version: 8.5.1(@swc/core@1.15.43(@swc/helpers@0.5.23))(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@6.0.3)
tsx:
- specifier: ^4.19.0
- version: 4.21.0
+ specifier: ^4.22.4
+ version: 4.22.4
typescript:
specifier: ^6.0.3
version: 6.0.3
+ vitest:
+ specifier: ^4.1.9
+ version: 4.1.9(@types/node@26.0.1)(jsdom@29.1.1)(msw@2.14.6(@types/node@26.0.1)(typescript@6.0.3))(vite@8.0.11(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.22.4))
packages:
+ '@adobe/css-tools@4.4.4':
+ resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==}
+
'@ampproject/remapping@2.3.0':
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
+ '@asamuzakjp/css-color@5.1.11':
+ resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ '@asamuzakjp/dom-selector@7.1.1':
+ resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ '@asamuzakjp/generational-cache@1.0.1':
+ resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ '@asamuzakjp/nwsapi@2.3.9':
+ resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
+
'@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
@@ -336,12 +366,16 @@ packages:
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'}
+ '@babel/code-frame@7.29.7':
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/compat-data@7.28.0':
resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==}
engines: {node: '>=6.9.0'}
- '@babel/compat-data@7.29.0':
- resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
+ '@babel/compat-data@7.29.7':
+ resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
engines: {node: '>=6.9.0'}
'@babel/core@7.28.3':
@@ -356,28 +390,32 @@ packages:
resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==}
engines: {node: '>=6.9.0'}
- '@babel/generator@7.29.1':
- resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
+ '@babel/generator@7.29.7':
+ resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-compilation-targets@7.27.2':
resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
engines: {node: '>=6.9.0'}
- '@babel/helper-compilation-targets@7.28.6':
- resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
+ '@babel/helper-compilation-targets@7.29.7':
+ resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
engines: {node: '>=6.9.0'}
'@babel/helper-globals@7.28.0':
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-globals@7.29.7':
+ resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-module-imports@7.27.1':
resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
engines: {node: '>=6.9.0'}
- '@babel/helper-module-imports@7.28.6':
- resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
+ '@babel/helper-module-imports@7.29.7':
+ resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-transforms@7.28.3':
@@ -386,20 +424,20 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0
- '@babel/helper-module-transforms@7.28.6':
- resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
+ '@babel/helper-module-transforms@7.29.7':
+ resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
- '@babel/helper-plugin-utils@7.28.6':
- resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
- engines: {node: '>=6.9.0'}
-
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-validator-identifier@7.27.1':
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
engines: {node: '>=6.9.0'}
@@ -408,16 +446,24 @@ packages:
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-validator-option@7.27.1':
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-validator-option@7.29.7':
+ resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helpers@7.28.3':
resolution: {integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==}
engines: {node: '>=6.9.0'}
- '@babel/helpers@7.29.2':
- resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==}
+ '@babel/helpers@7.29.7':
+ resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.28.3':
@@ -425,8 +471,8 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
- '@babel/parser@7.29.2':
- resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==}
+ '@babel/parser@7.29.7':
+ resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -525,101 +571,106 @@ packages:
resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
engines: {node: '>=6.9.0'}
+ '@babel/runtime@7.29.7':
+ resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'}
- '@babel/template@7.28.6':
- resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
+ '@babel/template@7.29.7':
+ resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
engines: {node: '>=6.9.0'}
'@babel/traverse@7.28.3':
resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==}
engines: {node: '>=6.9.0'}
- '@babel/traverse@7.29.0':
- resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
+ '@babel/traverse@7.29.7':
+ resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
engines: {node: '>=6.9.0'}
'@babel/types@7.28.2':
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
engines: {node: '>=6.9.0'}
- '@babel/types@7.29.0':
- resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
+ '@babel/types@7.29.7':
+ resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
engines: {node: '>=6.9.0'}
- '@bcoe/v8-coverage@0.2.3':
- resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
-
- '@biomejs/biome@2.4.12':
- resolution: {integrity: sha512-Rro7adQl3NLq/zJCIL98eElXKI8eEiBtoeu5TbXF/U3qbjuSc7Jb5rjUbeHHcquDWeSf3HnGP7XI5qGrlRk/pA==}
+ '@biomejs/biome@2.5.1':
+ resolution: {integrity: sha512-IXWLCxKmae+rI7LOHS1B3EbVisQ6GRAWbhN9msa6KjNCyFWrvKZWR4oUdinaNssrV852OrSHuSPa95h1GPJc7Q==}
engines: {node: '>=14.21.3'}
hasBin: true
- '@biomejs/cli-darwin-arm64@2.4.12':
- resolution: {integrity: sha512-BnMU4Pc3ciEVteVpZ0BK33MLr7X57F5w1dwDLDn+/iy/yTrA4Q/N2yftidFtsA4vrDh0FMXDpacNV/Tl3fbmng==}
+ '@biomejs/cli-darwin-arm64@2.5.1':
+ resolution: {integrity: sha512-npqDzvqv7vFaWRiNN1Te71siRgPaqS9MpqgYCdP/CrUbkJ7ApezaeaKjueKHRN/JH/6lRjJQAHi8acQDCAz22w==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
- '@biomejs/cli-darwin-x64@2.4.12':
- resolution: {integrity: sha512-x9uJ0bI1rJsWICp3VH8w/5PnAVD3A7SqzDpbrfoUQX1QyWrK5jSU4fRLo/wSgGeplCivbxBRKmt5Xq4/nWvq8A==}
+ '@biomejs/cli-darwin-x64@2.5.1':
+ resolution: {integrity: sha512-RgwTqPAM8g2tn1j+b5oRjF/DbSBX8a4gwojtuG9XuhfK7GgomvZ9+T+tqjXiVbjLEeGJOoL6VEk8mvRTVeSybw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
- '@biomejs/cli-linux-arm64-musl@2.4.12':
- resolution: {integrity: sha512-FhfpkAAlKL6kwvcVap0Hgp4AhZmtd3YImg0kK1jd7C/aSoh4SfsB2f++yG1rU0lr8Y5MCFJrcSkmssiL9Xnnig==}
+ '@biomejs/cli-linux-arm64-musl@2.5.1':
+ resolution: {integrity: sha512-WMcvMLgByyTqVxGlq918NBBYliq9FRR9GAQVETHb+VjGVqXCZFfHlZHC1FX4ibuYY/Hg6TJE3rHU0xVrdJXNRw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@biomejs/cli-linux-arm64@2.4.12':
- resolution: {integrity: sha512-tOwuCuZZtKi1jVzbk/5nXmIsziOB6yqN8c9r9QM0EJYPU6DpQWf11uBOSCfFKKM4H3d9ZoarvlgMfbcuD051Pw==}
+ '@biomejs/cli-linux-arm64@2.5.1':
+ resolution: {integrity: sha512-yhV35CzZh38VyMvTEXi3JTjxZBs++oCKK9KG8vB6VI5+uvQvZNR3BFWEKKzuOmx9DJJj7sQpZ4LQJcmbGTs3+Q==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@biomejs/cli-linux-x64-musl@2.4.12':
- resolution: {integrity: sha512-dwTIgZrGutzhkQCuvHynCkyW6hJxUuyZqKKO0YNfaS2GUoRO+tOvxXZqZB6SkWAOdfZTzwaw8IEdUnIkHKHoew==}
+ '@biomejs/cli-linux-x64-musl@2.5.1':
+ resolution: {integrity: sha512-ANTowtlLmPYm5yeMckWY8Xzb9Ix+JJP3tgHR/n6xRj1VWyIzzWtfRfih9hv9VmClwadpBvZduISZIbBsIlYG3A==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [musl]
- '@biomejs/cli-linux-x64@2.4.12':
- resolution: {integrity: sha512-8pFeAnLU9QdW9jCIslB/v82bI0lhBmz2ZAKc8pVMFPO0t0wAHsoEkrUQUbMkIorTRIjbqyNZHA3lEXavsPWYSw==}
+ '@biomejs/cli-linux-x64@2.5.1':
+ resolution: {integrity: sha512-J/7uHSX7NfoYDI7HijAkd8lnQIOrRb2W7j3X+tw4R+N5ExvXGsyXFiGdQcfcxfOmNQmZVSQOCDk757fwpzqQcg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@biomejs/cli-win32-arm64@2.4.12':
- resolution: {integrity: sha512-B0DLnx0vA9ya/3v7XyCaP+/lCpnbWbMOfUFFve+xb5OxyYvdHaS55YsSddr228Y+JAFk58agCuZTsqNiw2a6ig==}
+ '@biomejs/cli-win32-arm64@2.5.1':
+ resolution: {integrity: sha512-zgXnKNgWPC4iPF7Y1lR3STUeCUuZRpD6IiOrC7TZTlh0Lx6FiVUT05myuMQHQ9D+1cc7uyMldi4forE6lp0ivQ==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
- '@biomejs/cli-win32-x64@2.4.12':
- resolution: {integrity: sha512-yMckRzTyZ83hkk8iDFWswqSdU8tvZxspJKnYNh7JZr/zhZNOlzH13k4ecboU6MurKExCe2HUkH75pGI/O2JwGA==}
+ '@biomejs/cli-win32-x64@2.5.1':
+ resolution: {integrity: sha512-6uxpR9hvaglANkZemeSiN/FhYgkGasrEGn267eXIWvjrjJ2LhDlk251IhjVJq6MXzkV2/bcXwLwSroLyPtqRZg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
+ '@bramus/specificity@2.4.2':
+ resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
+ hasBin: true
+
'@clack/core@0.5.0':
resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==}
'@clack/prompts@0.11.0':
resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==}
- '@clickhouse/client-common@1.18.2':
- resolution: {integrity: sha512-J0SG6q9V31ydxonglpj9xhNRsUxCsF71iEZ784yldqMYwsHixj/9xHFDgBDX3DuMiDx/kPDfXnf+pimp08wIBA==}
+ '@clickhouse/client-common@1.22.0':
+ resolution: {integrity: sha512-MQgXRhoYXut6GhRrTJlub42bnPX7+5Vm+5gHNR0zZXU5+EwZKsBgMXiWXPOerAmQd3weGKm8hzoeZJCfU3Cw2w==}
- '@clickhouse/client@1.18.2':
- resolution: {integrity: sha512-fuquQswRSHWM6D079ZeuGqkMOsqtcUPL06UdTnowmoeeYjVrqisfVmvnw8pc3OeKS4kVb91oygb/MfLDiMs0TQ==}
+ '@clickhouse/client@1.22.0':
+ resolution: {integrity: sha512-iQAAM4VT9fO7mYVOkGB/Ul9Xxuf0atKn+GFceZqfE8xFakV8KOAQxR3tfNrXFMlJ8T+Q3gbrpfLFyj7/TbOwyA==}
engines: {node: '>=16'}
'@cspotcode/source-map-support@0.8.1':
@@ -644,6 +695,13 @@ packages:
'@csstools/css-parser-algorithms': ^4.0.0
'@csstools/css-tokenizer': ^4.0.0
+ '@csstools/css-calc@3.2.1':
+ resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^4.0.0
+ '@csstools/css-tokenizer': ^4.0.0
+
'@csstools/css-color-parser@4.1.0':
resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==}
engines: {node: '>=20.19.0'}
@@ -651,12 +709,27 @@ packages:
'@csstools/css-parser-algorithms': ^4.0.0
'@csstools/css-tokenizer': ^4.0.0
+ '@csstools/css-color-parser@4.1.8':
+ resolution: {integrity: sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^4.0.0
+ '@csstools/css-tokenizer': ^4.0.0
+
'@csstools/css-parser-algorithms@4.0.0':
resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
engines: {node: '>=20.19.0'}
peerDependencies:
'@csstools/css-tokenizer': ^4.0.0
+ '@csstools/css-syntax-patches-for-csstree@1.1.3':
+ resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==}
+ peerDependencies:
+ css-tree: ^3.2.1
+ peerDependenciesMeta:
+ css-tree:
+ optional: true
+
'@csstools/css-tokenizer@4.0.0':
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
engines: {node: '>=20.19.0'}
@@ -668,8 +741,8 @@ packages:
'@csstools/css-parser-algorithms': ^4.0.0
'@csstools/css-tokenizer': ^4.0.0
- '@csstools/postcss-alpha-function@2.0.4':
- resolution: {integrity: sha512-fti7+GybzvfMrv5TSU6x8rWtXWOth5nLefT5w5AKJ3F3T0bZoxlRqajF0ZUgTtnytfMd4dQ8n5UiaNmsjFA65A==}
+ '@csstools/postcss-alpha-function@2.0.6':
+ resolution: {integrity: sha512-XaMnJJqqZv4veulLELvM+5caEMcLTsFyqTrkwGKPMF+UbiM7dlQoe4K46EnwfSJIvnm91K1ZXsZSd3OuJ04p9w==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
@@ -680,44 +753,50 @@ packages:
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-color-function-display-p3-linear@2.0.3':
- resolution: {integrity: sha512-u8QNV2TKOxG6cqK4ZrJkpctnxdrwdNTMrkyokmCi+iuLpJegOraA0cqC7HoxF2tHhxjuXc+BxwY/Qd62SwvanQ==}
+ '@csstools/postcss-color-function-display-p3-linear@2.0.5':
+ resolution: {integrity: sha512-YzY5qI0S/CsvqvMSiDn85ZyTCRLdnywxQn+6Fv8AU17aCE/fjcor54OSdVb/HlABBTcBq+d8NlWcLz11Bmo2mQ==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-color-function@5.0.3':
- resolution: {integrity: sha512-BiBukIeQ7rPjx9A//9+qgJugBjX6FY9eWiojbnfIJCPulWrl8J07rCgQbFkloTXena+a6Aw5xa25weU+3MA75A==}
+ '@csstools/postcss-color-function@5.0.5':
+ resolution: {integrity: sha512-s+9fU1+sZazUNk0WyKShlfmTLC0fosxNY5x7DiD637xXbZLX2lyce23QrdRhytP3Ja1G77qUk6cRD37N1gemdQ==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-color-mix-function@4.0.3':
- resolution: {integrity: sha512-M8ju3iqHRXtW1/5HYuOmi9WFR5rGGFgqkPh+kXkv/eG56oYK/WYtTeIwJgdcro7lRwjlo4Ut8xqbV3Iovkwfrw==}
+ '@csstools/postcss-color-mix-function@4.0.5':
+ resolution: {integrity: sha512-eBrrzTKudOlDl2XOJzW/pzHPIkC8tGkcGpNiFO/vmevb08U1huYEINhlxr8iz4OzSqs1GtiJx4d2v5iHFOZjNw==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-color-mix-variadic-function-arguments@2.0.3':
- resolution: {integrity: sha512-tL46UyFjIjz7mDywoPOe/JgOpvMic0rsTUfdMBB1OHrUcCtE8MQpBILzYl/cAOtinJGu+ZQLuDhqTgTBOoeg3g==}
+ '@csstools/postcss-color-mix-variadic-function-arguments@2.0.5':
+ resolution: {integrity: sha512-O4tE1hZXfEAbTP1IC2R857KjPCLNtpsFUqY2dqgycF/3M6GuFyJI20EWwkxVZzlSFvWdIcNppwRf9pxPFn0qnA==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-content-alt-text@3.0.0':
- resolution: {integrity: sha512-OHa+4aCcrJtHpPWB3zptScHwpS1TUbeLR4uO0ntIz0Su/zw9SoWkVu+tDMSySSAsNtNSI3kut4fTliFwIsrHxA==}
+ '@csstools/postcss-container-rule-prelude-list@1.0.1':
+ resolution: {integrity: sha512-c5qlevVGKHU+zDbVoUGSZl1Mw7Vl1gVRKv6cdIYnaoyM+9Ou23Ian0H5Gr2ZF+lsDWovPK03hOSAbkw6HS8aTg==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-contrast-color-function@3.0.3':
- resolution: {integrity: sha512-YcohXq+/hfYeobKirg3oXGivDaaTfOPv568bE3jYQCn9ILpFz+RgyJR/kF7ZWh5560TTlTjeCqF4ZmVsj2zwnw==}
+ '@csstools/postcss-content-alt-text@3.0.1':
+ resolution: {integrity: sha512-mK5lCgzgV/ZC+LgnFy4rAQVMcXR6HsnX3D1+4Q5gshSQsst5TtcvHbxTdzKy1XTv09sNZHJX8CO4CEQF9zA4ug==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-exponential-functions@3.0.2':
- resolution: {integrity: sha512-WDrfdFJXF4M67+wniEGr/5XVzsmn1rt2lL1YAlTfE7x7XDlRstTc5e+HuFoGv6jkiMWTwPsiADJaLwsnGC3UjQ==}
+ '@csstools/postcss-contrast-color-function@3.0.5':
+ resolution: {integrity: sha512-gfdTZ4a5ioL2zM/yN2FqExy6rql+6egkI5sDuK9MvrbfrVJMzB0OjiCkboT5UprU/P0JwfTiIutW1ZSyqK4Icw==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ postcss: ^8.4
+
+ '@csstools/postcss-exponential-functions@3.0.3':
+ resolution: {integrity: sha512-mB/NoeHLBHh0LZiVSrFdRDA/NxSfmg4tSN9117IJH9bdC2BzSTVgc82h3Gu/sdBXay6kDH2sA7fbkTigMiEi2A==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
@@ -734,26 +813,32 @@ packages:
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-gamut-mapping@3.0.3':
- resolution: {integrity: sha512-3v5ZvcVuynhFh5qCJX2LIJ9Iry8/SvxfOEj6vDngNxbH/3OKTZBFLgK+DgLuIbsP1DLA9LLH3Rn7jmRxXgEDLA==}
+ '@csstools/postcss-gamut-mapping@3.0.5':
+ resolution: {integrity: sha512-X6XkKkR9R8KyJey9n1ryEzzfX6WpihPz/JBsyIVvxAlztQcMjMA7I9mMybWVv3ZyRMC+0+H7RlIUe85vZkasNQ==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-gradients-interpolation-method@6.0.3':
- resolution: {integrity: sha512-wrRIaRv1dkq30a8nvYWtSAf41bwCl+sVzLBKGnqeOwk81aSktKN3NattJpkiPyoOtEoFqChisl3WH3Csj/rOsw==}
+ '@csstools/postcss-gradients-interpolation-method@6.0.5':
+ resolution: {integrity: sha512-wXiZI6bLRAGcw7XuzsqqPnTVNrHFkHTkcymK2su+ynJjemfCdpCD9HdG+ICikPqtQ782r6LSZdyC3cDhSQqF3Q==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-hwb-function@5.0.3':
- resolution: {integrity: sha512-bHz0uc/PBg2wJEAlGinUf494nMyuXsVKH/fExc2xGkvL6WHOKlxzx/lkn+2AVCQACtWBLVRCBDgDnkYr4RSC9w==}
+ '@csstools/postcss-hwb-function@5.0.5':
+ resolution: {integrity: sha512-HeJOXAMr1nYHZ7gJT1+6d899X9Y+5qJcpbLJ8WzhujQOIB4oqbzeP3769sd1xl3eH4qbasxtewxr4crs08SEQw==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-ic-unit@5.0.0':
- resolution: {integrity: sha512-/ws5d6c4uKqfM9zIL3ugcGI+3fvZEOOkJHNzAyTAGJIdZ+aSL9BVPNlHGV4QzmL0vqBSCOdU3+rhcMEj3+KzYw==}
+ '@csstools/postcss-ic-unit@5.0.1':
+ resolution: {integrity: sha512-jmsVLXPdMBTlaJAhiEijhIR3qL0j75MrlRfhJEs91DF1Wlt2kpJTDsbpXQpYFzn1nPFHZC/WEf+Mw0I/HXkHzQ==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ postcss: ^8.4
+
+ '@csstools/postcss-image-function@1.0.0':
+ resolution: {integrity: sha512-iuQztV6Cfeuc7NczazfickrzEhALOpxUS0yWgGkmRY1zZ0CKjBBFc/7WWSN9qupfpNAzHY7cPNcJCqUhtr+YMw==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
@@ -770,8 +855,8 @@ packages:
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-light-dark-function@3.0.0':
- resolution: {integrity: sha512-s++V5/hYazeRUCYIn2lsBVzUsxdeC46gtwpgW6lu5U/GlPOS5UTDT14kkEyPgXmFbCvaWLREqV7YTMJq1K3G6w==}
+ '@csstools/postcss-light-dark-function@3.0.1':
+ resolution: {integrity: sha512-tD2MMJmZ6XXCHgDythLHcXQDNi5z7KEEWPe7JeB3vPcw+YMuMabpW5ugRqndhIrui+vduhc0Md7f7yGPCmOErg==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
@@ -806,8 +891,8 @@ packages:
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-media-minmax@3.0.2':
- resolution: {integrity: sha512-+ABxs2ZhJDhy+B9PJg7pgkGq6/d3XPXsWl7+6yZfAk4b2ba6aQ1h2AiTn04XwS6rpMpZEF3tONli/ubfu4y8AQ==}
+ '@csstools/postcss-media-minmax@3.0.3':
+ resolution: {integrity: sha512-ch1tNS+1QayiHTGsyc53zv3AzrSd0zigjbkfLxoeuzzJyn32+P3V7em3u5vLVnqLMzBbEZK//GI13EVTIPRdDA==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
@@ -836,8 +921,8 @@ packages:
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-oklab-function@5.0.3':
- resolution: {integrity: sha512-vTMgJFMwMt9gnPvhKaDnMR7E/h9Nb+rPUv825SY5VUo4PWj+w0OH/N2NqgvjYeubaA3BVckbKDlvADATRpD4Hw==}
+ '@csstools/postcss-oklab-function@5.0.5':
+ resolution: {integrity: sha512-A+Nkzj2ODvQboM5FlqEcp0iqilyVo78f9FMx/3cHrRrEBqCymSXvf8sa1cTY54lJoUVI3Sn9XysgvYaVIAuIYg==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
@@ -848,8 +933,8 @@ packages:
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-progressive-custom-properties@5.0.0':
- resolution: {integrity: sha512-NsJoZ89rxmDrUsITf8QIk5w+lQZQ8Xw5K6cLFG+cfiffsLYHb3zcbOOrHLetGl1WIhjWWQ4Cr8MMrg46Q+oACg==}
+ '@csstools/postcss-progressive-custom-properties@5.1.0':
+ resolution: {integrity: sha512-lt/4yHy2GdKcGVpK4OGhBdSIq+z2PXynSusSRggn/T4y7uFurYAhdHqo/aYM+xI37vNb8rJlEKchqKKvVCXROQ==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
@@ -860,14 +945,14 @@ packages:
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-random-function@3.0.2':
- resolution: {integrity: sha512-iQ3vfX1LIqRXX7P1/ol45EpJ5CTWdQCAfdpTlHlsRPU4jMQeepmeNjQ0F60bj8RWTS1RkJ318fzzq4mUlyZ7hA==}
+ '@csstools/postcss-random-function@3.0.3':
+ resolution: {integrity: sha512-0EScyKxscGonwpi30Hj9DEAr0X8D2eDhOqqayQXE91gIqGli9UT+deLYqoogZLOy5GT+ncqltMqztc/q+0UkhA==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-relative-color-syntax@4.0.3':
- resolution: {integrity: sha512-SZSImz4KufmLi0dRwYivWXlza+7HF84SRApY8R48SyWgn+f0gDvmCn7D2Ie4CED7qU0JJK+YfCUC1HVlaQ10dg==}
+ '@csstools/postcss-relative-color-syntax@4.0.5':
+ resolution: {integrity: sha512-kBzf+LIm824cpjsZPhNtl/2N1KK+TXnxy8Kce4y+pEAQSrxhpX6WDUg54wjdHBGx2UZUXKBnlaUOsc71sSRDvg==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
@@ -878,14 +963,14 @@ packages:
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-sign-functions@2.0.2':
- resolution: {integrity: sha512-vOxkkMCMVnyaj7CW03uKR2R/zhJaCrptsXlm31HgI/dqC1lSIGnmu5W7N68x23XwcSgc8fE/fg0jKj4x1XFH4w==}
+ '@csstools/postcss-sign-functions@2.0.3':
+ resolution: {integrity: sha512-2BCPwlpeQweTC/8S8oQFYhYD5kxYkiroLf3AUJV2kVoKkSZ+4WM4rSwySXlKrqXL8HfCryAwVrJg7B0jr/RnOw==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-stepped-value-functions@5.0.2':
- resolution: {integrity: sha512-4PtqkRoBcMSxZG00gcDv+nq7cxVUua+Yd7TmG16qzJjdolyICHkx1RfhNL5mKSnWOLxUnk/IdxAoWN+KU7E/ng==}
+ '@csstools/postcss-stepped-value-functions@5.0.3':
+ resolution: {integrity: sha512-nXMFQBz5Pi2LLG02iqm2k+scrqwtqJT9ta/gN8S79oBZ23M0E7O3wDJ20//3z5Q6HU5e+K0n+SmmxN6iWtbm6w==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
@@ -908,8 +993,8 @@ packages:
peerDependencies:
postcss: ^8.4
- '@csstools/postcss-trigonometric-functions@5.0.2':
- resolution: {integrity: sha512-hRansZmQk1HH11WGUNlWy8H/DCB9Wy6zDbRcyBfF2UUP+V2fubK+qwmq0q6LIDje5gRzxlKyWhgFYxPy1ohivA==}
+ '@csstools/postcss-trigonometric-functions@5.0.3':
+ resolution: {integrity: sha512-p9LTvLj+DFpl5RHbG/X9QGwg7BoMOBsRBZqsUAKKVvCw7MRCsk1P1llTUR/MW5nyZ4IsjFGDtDwTTj1reJjxvg==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
@@ -938,13 +1023,6 @@ packages:
peerDependencies:
postcss: ^8.4
- '@cypress/request@3.0.10':
- resolution: {integrity: sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==}
- engines: {node: '>= 6'}
-
- '@cypress/xvfb@1.2.4':
- resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==}
-
'@date-fns/utc@2.1.1':
resolution: {integrity: sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA==}
@@ -1164,8 +1242,8 @@ packages:
'@emnapi/runtime@1.10.0':
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
- '@emnapi/runtime@1.9.2':
- resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==}
+ '@emnapi/runtime@1.11.1':
+ resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
'@emnapi/wasi-threads@1.2.1':
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
@@ -1179,8 +1257,8 @@ packages:
cpu: [ppc64]
os: [aix]
- '@esbuild/aix-ppc64@0.28.0':
- resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==}
+ '@esbuild/aix-ppc64@0.28.1':
+ resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
@@ -1191,8 +1269,8 @@ packages:
cpu: [arm64]
os: [android]
- '@esbuild/android-arm64@0.28.0':
- resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==}
+ '@esbuild/android-arm64@0.28.1':
+ resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
@@ -1203,8 +1281,8 @@ packages:
cpu: [arm]
os: [android]
- '@esbuild/android-arm@0.28.0':
- resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==}
+ '@esbuild/android-arm@0.28.1':
+ resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
@@ -1215,8 +1293,8 @@ packages:
cpu: [x64]
os: [android]
- '@esbuild/android-x64@0.28.0':
- resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==}
+ '@esbuild/android-x64@0.28.1':
+ resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
@@ -1227,8 +1305,8 @@ packages:
cpu: [arm64]
os: [darwin]
- '@esbuild/darwin-arm64@0.28.0':
- resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==}
+ '@esbuild/darwin-arm64@0.28.1':
+ resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
@@ -1239,8 +1317,8 @@ packages:
cpu: [x64]
os: [darwin]
- '@esbuild/darwin-x64@0.28.0':
- resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==}
+ '@esbuild/darwin-x64@0.28.1':
+ resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
@@ -1251,8 +1329,8 @@ packages:
cpu: [arm64]
os: [freebsd]
- '@esbuild/freebsd-arm64@0.28.0':
- resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==}
+ '@esbuild/freebsd-arm64@0.28.1':
+ resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
@@ -1263,8 +1341,8 @@ packages:
cpu: [x64]
os: [freebsd]
- '@esbuild/freebsd-x64@0.28.0':
- resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==}
+ '@esbuild/freebsd-x64@0.28.1':
+ resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
@@ -1275,8 +1353,8 @@ packages:
cpu: [arm64]
os: [linux]
- '@esbuild/linux-arm64@0.28.0':
- resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==}
+ '@esbuild/linux-arm64@0.28.1':
+ resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
@@ -1287,8 +1365,8 @@ packages:
cpu: [arm]
os: [linux]
- '@esbuild/linux-arm@0.28.0':
- resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==}
+ '@esbuild/linux-arm@0.28.1':
+ resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
@@ -1299,8 +1377,8 @@ packages:
cpu: [ia32]
os: [linux]
- '@esbuild/linux-ia32@0.28.0':
- resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==}
+ '@esbuild/linux-ia32@0.28.1':
+ resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
@@ -1311,8 +1389,8 @@ packages:
cpu: [loong64]
os: [linux]
- '@esbuild/linux-loong64@0.28.0':
- resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==}
+ '@esbuild/linux-loong64@0.28.1':
+ resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
@@ -1323,8 +1401,8 @@ packages:
cpu: [mips64el]
os: [linux]
- '@esbuild/linux-mips64el@0.28.0':
- resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==}
+ '@esbuild/linux-mips64el@0.28.1':
+ resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
@@ -1335,8 +1413,8 @@ packages:
cpu: [ppc64]
os: [linux]
- '@esbuild/linux-ppc64@0.28.0':
- resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==}
+ '@esbuild/linux-ppc64@0.28.1':
+ resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
@@ -1347,8 +1425,8 @@ packages:
cpu: [riscv64]
os: [linux]
- '@esbuild/linux-riscv64@0.28.0':
- resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==}
+ '@esbuild/linux-riscv64@0.28.1':
+ resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
@@ -1359,8 +1437,8 @@ packages:
cpu: [s390x]
os: [linux]
- '@esbuild/linux-s390x@0.28.0':
- resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==}
+ '@esbuild/linux-s390x@0.28.1':
+ resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
@@ -1371,8 +1449,8 @@ packages:
cpu: [x64]
os: [linux]
- '@esbuild/linux-x64@0.28.0':
- resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==}
+ '@esbuild/linux-x64@0.28.1':
+ resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
@@ -1383,8 +1461,8 @@ packages:
cpu: [arm64]
os: [netbsd]
- '@esbuild/netbsd-arm64@0.28.0':
- resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==}
+ '@esbuild/netbsd-arm64@0.28.1':
+ resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
@@ -1395,8 +1473,8 @@ packages:
cpu: [x64]
os: [netbsd]
- '@esbuild/netbsd-x64@0.28.0':
- resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==}
+ '@esbuild/netbsd-x64@0.28.1':
+ resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
@@ -1407,8 +1485,8 @@ packages:
cpu: [arm64]
os: [openbsd]
- '@esbuild/openbsd-arm64@0.28.0':
- resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==}
+ '@esbuild/openbsd-arm64@0.28.1':
+ resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
@@ -1419,8 +1497,8 @@ packages:
cpu: [x64]
os: [openbsd]
- '@esbuild/openbsd-x64@0.28.0':
- resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==}
+ '@esbuild/openbsd-x64@0.28.1':
+ resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
@@ -1431,8 +1509,8 @@ packages:
cpu: [arm64]
os: [openharmony]
- '@esbuild/openharmony-arm64@0.28.0':
- resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==}
+ '@esbuild/openharmony-arm64@0.28.1':
+ resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
@@ -1443,8 +1521,8 @@ packages:
cpu: [x64]
os: [sunos]
- '@esbuild/sunos-x64@0.28.0':
- resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==}
+ '@esbuild/sunos-x64@0.28.1':
+ resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
@@ -1455,8 +1533,8 @@ packages:
cpu: [arm64]
os: [win32]
- '@esbuild/win32-arm64@0.28.0':
- resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==}
+ '@esbuild/win32-arm64@0.28.1':
+ resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
@@ -1467,8 +1545,8 @@ packages:
cpu: [ia32]
os: [win32]
- '@esbuild/win32-ia32@0.28.0':
- resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==}
+ '@esbuild/win32-ia32@0.28.1':
+ resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
@@ -1479,23 +1557,32 @@ packages:
cpu: [x64]
os: [win32]
- '@esbuild/win32-x64@0.28.0':
- resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==}
+ '@esbuild/win32-x64@0.28.1':
+ resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
- '@formatjs/fast-memoize@3.1.2':
- resolution: {integrity: sha512-vPnriihkfK0lzoQGaXq+qXH23VsYyansRTkTgo2aTG0k1NjLFyZimFVdfj4C9JkSE5dm7CEngcQ5TTc1yAyBfQ==}
+ '@exodus/bytes@1.15.0':
+ resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+ peerDependencies:
+ '@noble/hashes': ^1.8.0 || ^2.0.0
+ peerDependenciesMeta:
+ '@noble/hashes':
+ optional: true
- '@formatjs/icu-messageformat-parser@3.5.4':
- resolution: {integrity: sha512-JVY39ROgLt+pIYngo6piyj4OVfZmXs/2FkC4wLS+ql1Eig/sGJKB7YwDO/5bkJFkfwaFAeIpgEiJc8hiYxNalw==}
+ '@formatjs/fast-memoize@3.1.6':
+ resolution: {integrity: sha512-H5aexk1Le7T9TPmscacZ+1pR6CTa2n1wq+HDVGXhH8TzUlQQpeXzZs91dRtmFHrbeNbjPFPfQujUqm7MHgVoXQ==}
- '@formatjs/icu-skeleton-parser@2.1.4':
- resolution: {integrity: sha512-8bSFZbrlvGX11ywMZxtgkPBt5Q8/etyts7j7j+GWpOVK1g43zwMIH3LZxk43HAtEP7L/jtZ+OZaMiFTOiBj9CA==}
+ '@formatjs/icu-messageformat-parser@3.5.11':
+ resolution: {integrity: sha512-NVsuNsc2dUVG9+4HBJ/srScxtA/18LqGgwtop/tuN/OIBjVl6QA+0KhfZQddDD9sEh2LeVjLFPGVU3ixa3blcA==}
- '@formatjs/intl-localematcher@0.8.3':
- resolution: {integrity: sha512-pHUjWb9NuhnMs8+PxQdzBtZRFJHlGhrURGAbm6Ltwl82BFajeuiIR3jblSa7ia3r62rXe/0YtVpUG3xWr5bFCA==}
+ '@formatjs/icu-skeleton-parser@2.1.10':
+ resolution: {integrity: sha512-XuSva+8ZGawk8VnD5VD6UeH8KarQ/Z022zgjHDoHmlNiAewstXuuzXc0Hk5pGFSdG+nNw5bfJKXqj1ZXHn9yUA==}
+
+ '@formatjs/intl-localematcher@0.8.10':
+ resolution: {integrity: sha512-P/IC3qws3jH+1fEs+o0RIFgXKRaQlFehjS5W0FPAqdo6hgzawLl+eD0q0JjheQ3XtoOe5n8WSYfX06KQZI/QJA==}
'@hello-pangea/dnd@18.0.1':
resolution: {integrity: sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==}
@@ -1662,17 +1749,52 @@ packages:
cpu: [x64]
os: [win32]
+ '@inquirer/ansi@2.0.5':
+ resolution: {integrity: sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
+
+ '@inquirer/confirm@6.0.12':
+ resolution: {integrity: sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/core@11.1.9':
+ resolution: {integrity: sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/figures@2.0.5':
+ resolution: {integrity: sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
+
+ '@inquirer/type@4.0.5':
+ resolution: {integrity: sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
'@internationalized/date@3.11.0':
resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==}
- '@internationalized/date@3.12.1':
- resolution: {integrity: sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==}
+ '@internationalized/date@3.12.2':
+ resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==}
- '@internationalized/number@3.6.6':
- resolution: {integrity: sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==}
+ '@internationalized/number@3.6.7':
+ resolution: {integrity: sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==}
- '@internationalized/string@3.2.8':
- resolution: {integrity: sha512-NdbMQUSfXLYIQol5VyMtinm9pZDciiMfN7RtmSuSB78io1hqwJ0naYfxyW6vgxWBkzWymQa/3uLDlbfmshtCaA==}
+ '@internationalized/string@3.2.9':
+ resolution: {integrity: sha512-kzP/M/mbQxODlmOt4bIQZ2SBVUWUSqMLXooXixnX7noche8WHaQcA+nwFN1K2KCF/cp+LDUhcJsCicwkvhD1pg==}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
@@ -1682,108 +1804,6 @@ packages:
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
- '@istanbuljs/load-nyc-config@1.1.0':
- resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
- engines: {node: '>=8'}
-
- '@istanbuljs/schema@0.1.6':
- resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==}
- engines: {node: '>=8'}
-
- '@jest/console@30.3.0':
- resolution: {integrity: sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/core@30.3.0':
- resolution: {integrity: sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- peerDependencies:
- node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
- peerDependenciesMeta:
- node-notifier:
- optional: true
-
- '@jest/diff-sequences@30.0.1':
- resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/diff-sequences@30.3.0':
- resolution: {integrity: sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/environment@30.3.0':
- resolution: {integrity: sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/expect-utils@30.1.1':
- resolution: {integrity: sha512-5YUHr27fpJ64dnvtu+tt11ewATynrHkGYD+uSFgRr8V2eFJis/vEXgToyLwccIwqBihVfz9jwio+Zr1ab1Zihw==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/expect-utils@30.3.0':
- resolution: {integrity: sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/expect@30.3.0':
- resolution: {integrity: sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/fake-timers@30.3.0':
- resolution: {integrity: sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/get-type@30.1.0':
- resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/globals@30.3.0':
- resolution: {integrity: sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/pattern@30.0.1':
- resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/reporters@30.3.0':
- resolution: {integrity: sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- peerDependencies:
- node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
- peerDependenciesMeta:
- node-notifier:
- optional: true
-
- '@jest/schemas@30.0.5':
- resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/snapshot-utils@30.3.0':
- resolution: {integrity: sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/source-map@30.0.1':
- resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/test-result@30.3.0':
- resolution: {integrity: sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/test-sequencer@30.3.0':
- resolution: {integrity: sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/transform@30.3.0':
- resolution: {integrity: sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/types@30.0.5':
- resolution: {integrity: sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- '@jest/types@30.3.0':
- resolution: {integrity: sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -1812,64 +1832,71 @@ packages:
'@kurkle/color@0.3.4':
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
- '@napi-rs/wasm-runtime@0.2.12':
- resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
+ '@mswjs/interceptors@0.41.8':
+ resolution: {integrity: sha512-pRLMNKTSGRoLq+KnEB/7OY5vijw1XmcheAAOiv6pj7W1FG32kAGqj1C/RK/cqxRGr1Fh+zBi8sDur8kj3EQv6A==}
+ engines: {node: '>=18'}
- '@netlify/plugin-nextjs@5.15.9':
- resolution: {integrity: sha512-4kJvV+wrt4/ncehfaOXfhv1FYKCv9EFjq6/tgXXYSlqPDcbsuTXR0DTz9IvyZ/CpPRadRD/zzIqD9wXgCmA2lg==}
+ '@napi-rs/wasm-runtime@1.1.6':
+ resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1
+ '@emnapi/runtime': ^1.7.1
+
+ '@netlify/plugin-nextjs@5.15.12':
+ resolution: {integrity: sha512-eydPd4gtc/OJOx9IbIJ5CRrC5K3rLxfEQS/S/fRNEAAT0qFvPoQVy23VdK3jRXKFJWXdCRl6ZudWLEZECQPSRQ==}
engines: {node: '>=18.0.0'}
- '@next/env@16.2.4':
- resolution: {integrity: sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==}
+ '@next/env@16.2.9':
+ resolution: {integrity: sha512-ki5VxxXfzD/9TDe13wyeTKIjQTAwBVpnr8KhRDUr8ltMUq1/NBpWNT5tiPoxiGl+PHM4X2ahSOiPk6iAimIzPg==}
- '@next/swc-darwin-arm64@16.2.4':
- resolution: {integrity: sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==}
+ '@next/swc-darwin-arm64@16.2.9':
+ resolution: {integrity: sha512-HkfxNYUCmcct0Xsqib5KxqMSHV4AHJq857BNRchyBDs4YS19aHzVfn1kDuBYKqLLQBjXgnkIsjV2Kd4d2wzYhw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
- '@next/swc-darwin-x64@16.2.4':
- resolution: {integrity: sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==}
+ '@next/swc-darwin-x64@16.2.9':
+ resolution: {integrity: sha512-7IAtK4MeybpqRV9GRABWEhJ62mOS+rzWOzOTFie4cSEtm12xsoOMJRcECoZx3FHPzFAqN/IJtHqWAFOLfl152w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
- '@next/swc-linux-arm64-gnu@16.2.4':
- resolution: {integrity: sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==}
+ '@next/swc-linux-arm64-gnu@16.2.9':
+ resolution: {integrity: sha512-hBD75iWpUtkL9SmQmcRhmLomn9jgkPzCEkbOcLgHymPEKzv+6ONy13RRiIEz/iEObjkS2Jlb5gYS2XGoS3X4rw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@next/swc-linux-arm64-musl@16.2.4':
- resolution: {integrity: sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==}
+ '@next/swc-linux-arm64-musl@16.2.9':
+ resolution: {integrity: sha512-qZTI3pf9SGc/obr8NkQAekBxmp1QK+kVm+VAf3BALLfFAj+1kUhkTxmrWpVos9R/UYIA8AWX2p6cGI5WdwzVUA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@next/swc-linux-x64-gnu@16.2.4':
- resolution: {integrity: sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==}
+ '@next/swc-linux-x64-gnu@16.2.9':
+ resolution: {integrity: sha512-xm0HfRNX+UkH4R3c18ynswjj5o5uEj/7iI9p9omdtTSIsRCzQqkGMA+10nzJ4EHnYC3as65IMhbbl5fWRUWHYg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@next/swc-linux-x64-musl@16.2.4':
- resolution: {integrity: sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==}
+ '@next/swc-linux-x64-musl@16.2.9':
+ resolution: {integrity: sha512-QumimHkGEG6vM3PfEDWKyKen03NcqLOkeKB1EfcPe7VxzmEiCa4jNnMyBn/US5zcd/VE1CI+O8Ovb3lfjVHfGw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
- '@next/swc-win32-arm64-msvc@16.2.4':
- resolution: {integrity: sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==}
+ '@next/swc-win32-arm64-msvc@16.2.9':
+ resolution: {integrity: sha512-hzQpKZvw8rAwI6A2uQh6SacCSvNAXaIkPNsWwzqqfRiIMiXMfH936skDhz1OO6KpvdKkJrgHHtqQOq5PIXOvdQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
- '@next/swc-win32-x64-msvc@16.2.4':
- resolution: {integrity: sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==}
+ '@next/swc-win32-x64-msvc@16.2.9':
+ resolution: {integrity: sha512-qr2VL3Ce5QrwgO2yh1ujSBawrimjVKX8FGF/cOynmdYKJY0BdHpGVNIRK1tqONB10Vkm25Ub1BD2bkjWs4+96w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -2000,18 +2027,19 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
- '@pkgr/core@0.2.9':
- resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
- engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+ '@playwright/test@1.61.1':
+ resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==}
+ engines: {node: '>=18'}
+ hasBin: true
- '@prisma/adapter-pg@7.7.0':
- resolution: {integrity: sha512-q33Ta8sKbgzEpAy0lx45tAq//yMv0qcb+8nj+TCA3P4wiAY+OBFEFk/NDkZncAfHaNJeGo5WJpJdpbL+ijYx8g==}
+ '@prisma/adapter-pg@7.8.0':
+ resolution: {integrity: sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg==}
- '@prisma/client-runtime-utils@7.7.0':
- resolution: {integrity: sha512-BLyd0UpFYOtyJFTHm7jS9vesHW7P83abibodQMiIofqjBKzDHQ1VAsQkdfvXyYDkPlONPfOTz7/rv3x/+CQqvQ==}
+ '@prisma/client-runtime-utils@7.8.0':
+ resolution: {integrity: sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==}
- '@prisma/client@7.7.0':
- resolution: {integrity: sha512-5Ar4OsZpJ54s21sy5oDNNW9gQtd4NuxCaiM7+JDTOU07D6VvlpLjYzAVCMB1+JzokN+08dAVomlx+b7bhJd3ww==}
+ '@prisma/client@7.8.0':
+ resolution: {integrity: sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==}
engines: {node: ^20.19 || ^22.12 || >=24.0}
peerDependencies:
prisma: '*'
@@ -2022,40 +2050,40 @@ packages:
typescript:
optional: true
- '@prisma/config@7.7.0':
- resolution: {integrity: sha512-hmPI3tKLO2aP0Y5vugbjcnA9qqlfJndiT6ds4tw28U5hNHLWg+mHJEWAhjsSPgxjtmxhJ/EDIeIlyh+3Us0OPg==}
+ '@prisma/config@7.8.0':
+ resolution: {integrity: sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==}
'@prisma/debug@7.2.0':
resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==}
- '@prisma/debug@7.7.0':
- resolution: {integrity: sha512-12J62XdqCmpiwJHhHdQxZeY3ckVCWIFmcJP8hg5dPTceeiQ0wiojXGFYTluKqFQfu46fRLgb/rLALZMAx3+dTA==}
+ '@prisma/debug@7.8.0':
+ resolution: {integrity: sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==}
'@prisma/dev@0.24.3':
resolution: {integrity: sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==}
- '@prisma/driver-adapter-utils@7.7.0':
- resolution: {integrity: sha512-gZXREeu6mOk7zXfGFJgh86p7Vhj0sXNKp+4Cg1tWYo7V2dfncP2qxS2BiTmbIIha8xPqItkl0WSw38RuSq1HoQ==}
+ '@prisma/driver-adapter-utils@7.8.0':
+ resolution: {integrity: sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==}
- '@prisma/engines-version@7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711':
- resolution: {integrity: sha512-r51DLcJ8bDRSrBEJF3J4cinoWyGA7rfP2mG6lD90VqIbGNOkbfcLcXalSVjq5Y6brQS3vcjrq4GbyUb1Cb7vkw==}
+ '@prisma/engines-version@7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a':
+ resolution: {integrity: sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==}
- '@prisma/engines@7.7.0':
- resolution: {integrity: sha512-7fmcbT7HHXBq/b+3h/dO1JI3fd8l8q7erf7xP7pRprh58hmSSnG8mg9K3yjW3h9WaHWUwngVFpSxxxivaitQ2w==}
+ '@prisma/engines@7.8.0':
+ resolution: {integrity: sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==}
'@prisma/extension-read-replicas@0.5.0':
resolution: {integrity: sha512-t0kLjqFMte4wwGrZFhya4iFoyoOY4kjlyyE60WXZL66PqP6PcBpW/35eKb/3UcxgjJxcDlaJKfmGokFLOyafiQ==}
peerDependencies:
'@prisma/client': ^7.0.0
- '@prisma/fetch-engine@7.7.0':
- resolution: {integrity: sha512-TfyzveBQoK4xALzsTpVhB/0KG1N8zOK0ap+RnBMkzGUu3f98fnQ4QtXa2wlKPhsO2X8a3N5ugFQgcKNoHGmDfw==}
+ '@prisma/fetch-engine@7.8.0':
+ resolution: {integrity: sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==}
'@prisma/get-platform@7.2.0':
resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==}
- '@prisma/get-platform@7.7.0':
- resolution: {integrity: sha512-MEUNzvKxvYnJ7kgvd6oNRnMmmiGNS9TYLB2weMeIXplnHdL/UWEGnvavYGnN7KLJ2n0iI4dDAyzSkHI3c7AscQ==}
+ '@prisma/get-platform@7.8.0':
+ resolution: {integrity: sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==}
'@prisma/query-plan-executor@7.2.0':
resolution: {integrity: sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==}
@@ -2205,13 +2233,8 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
- '@react-types/shared@3.33.0':
- resolution: {integrity: sha512-xuUpP6MyuPmJtzNOqF5pzFUIHH2YogyOQfUQHag54PRmWB7AbjuGWBUv0l1UDmz6+AbzAYGmDVAzcRDOu2PFpw==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-
- '@react-types/shared@3.34.0':
- resolution: {integrity: sha512-gp6xo/s2lX54AlTjOiqwDnxA7UW79BNvI9dB9pr3LZTzRKCd1ZA+ZbgKw/ReIiWuvvVw/8QFJpnqeeFyLocMcQ==}
+ '@react-types/shared@3.36.0':
+ resolution: {integrity: sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
@@ -2251,6 +2274,104 @@ packages:
peerDependencies:
'@redis/client': ^5.12.1
+ '@rolldown/binding-android-arm64@1.0.0-rc.18':
+ resolution: {integrity: sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.18':
+ resolution: {integrity: sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.0.0-rc.18':
+ resolution: {integrity: sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.18':
+ resolution: {integrity: sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18':
+ resolution: {integrity: sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18':
+ resolution: {integrity: sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18':
+ resolution: {integrity: sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18':
+ resolution: {integrity: sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18':
+ resolution: {integrity: sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18':
+ resolution: {integrity: sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.18':
+ resolution: {integrity: sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.18':
+ resolution: {integrity: sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.18':
+ resolution: {integrity: sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [wasm32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18':
+ resolution: {integrity: sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18':
+ resolution: {integrity: sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.0-rc.18':
+ resolution: {integrity: sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==}
+
'@rollup/plugin-alias@6.0.0':
resolution: {integrity: sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g==}
engines: {node: '>=20.19.0'}
@@ -2260,8 +2381,8 @@ packages:
rollup:
optional: true
- '@rollup/plugin-commonjs@29.0.2':
- resolution: {integrity: sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==}
+ '@rollup/plugin-commonjs@29.0.3':
+ resolution: {integrity: sha512-ZaOxZceP7SOUW7Lqw5IRVweSQYWaeIPnXIGLiB690EBA3FGJTO40EEr2L5yZplJWsgTCogILRSpcAe7+U0Otdg==}
engines: {node: '>=16.0.0 || 14 >= 14.17'}
peerDependencies:
rollup: ^2.68.0||^3.0.0||^4.0.0
@@ -2336,146 +2457,155 @@ packages:
rollup:
optional: true
- '@rollup/rollup-android-arm-eabi@4.60.1':
- resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==}
+ '@rollup/rollup-android-arm-eabi@4.62.2':
+ resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==}
cpu: [arm]
os: [android]
- '@rollup/rollup-android-arm64@4.60.1':
- resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==}
+ '@rollup/rollup-android-arm64@4.62.2':
+ resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==}
cpu: [arm64]
os: [android]
- '@rollup/rollup-darwin-arm64@4.60.1':
- resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==}
+ '@rollup/rollup-darwin-arm64@4.62.2':
+ resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==}
cpu: [arm64]
os: [darwin]
- '@rollup/rollup-darwin-x64@4.60.1':
- resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==}
+ '@rollup/rollup-darwin-x64@4.62.2':
+ resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==}
cpu: [x64]
os: [darwin]
- '@rollup/rollup-freebsd-arm64@4.60.1':
- resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==}
+ '@rollup/rollup-freebsd-arm64@4.62.2':
+ resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==}
cpu: [arm64]
os: [freebsd]
- '@rollup/rollup-freebsd-x64@4.60.1':
- resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==}
+ '@rollup/rollup-freebsd-x64@4.62.2':
+ resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==}
cpu: [x64]
os: [freebsd]
- '@rollup/rollup-linux-arm-gnueabihf@4.60.1':
- resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==}
+ '@rollup/rollup-linux-arm-gnueabihf@4.62.2':
+ resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==}
cpu: [arm]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-arm-musleabihf@4.60.1':
- resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==}
+ '@rollup/rollup-linux-arm-musleabihf@4.62.2':
+ resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==}
cpu: [arm]
os: [linux]
libc: [musl]
- '@rollup/rollup-linux-arm64-gnu@4.60.1':
- resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==}
+ '@rollup/rollup-linux-arm64-gnu@4.62.2':
+ resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-arm64-musl@4.60.1':
- resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==}
+ '@rollup/rollup-linux-arm64-musl@4.62.2':
+ resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@rollup/rollup-linux-loong64-gnu@4.60.1':
- resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==}
+ '@rollup/rollup-linux-loong64-gnu@4.62.2':
+ resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==}
cpu: [loong64]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-loong64-musl@4.60.1':
- resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==}
+ '@rollup/rollup-linux-loong64-musl@4.62.2':
+ resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==}
cpu: [loong64]
os: [linux]
libc: [musl]
- '@rollup/rollup-linux-ppc64-gnu@4.60.1':
- resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==}
+ '@rollup/rollup-linux-ppc64-gnu@4.62.2':
+ resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-ppc64-musl@4.60.1':
- resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==}
+ '@rollup/rollup-linux-ppc64-musl@4.62.2':
+ resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==}
cpu: [ppc64]
os: [linux]
libc: [musl]
- '@rollup/rollup-linux-riscv64-gnu@4.60.1':
- resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==}
+ '@rollup/rollup-linux-riscv64-gnu@4.62.2':
+ resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-riscv64-musl@4.60.1':
- resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==}
+ '@rollup/rollup-linux-riscv64-musl@4.62.2':
+ resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==}
cpu: [riscv64]
os: [linux]
libc: [musl]
- '@rollup/rollup-linux-s390x-gnu@4.60.1':
- resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==}
+ '@rollup/rollup-linux-s390x-gnu@4.62.2':
+ resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-x64-gnu@4.60.1':
- resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==}
+ '@rollup/rollup-linux-x64-gnu@4.62.2':
+ resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-x64-musl@4.60.1':
- resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==}
+ '@rollup/rollup-linux-x64-musl@4.62.2':
+ resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==}
cpu: [x64]
os: [linux]
libc: [musl]
- '@rollup/rollup-openbsd-x64@4.60.1':
- resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==}
+ '@rollup/rollup-openbsd-x64@4.62.2':
+ resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==}
cpu: [x64]
os: [openbsd]
- '@rollup/rollup-openharmony-arm64@4.60.1':
- resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==}
+ '@rollup/rollup-openharmony-arm64@4.62.2':
+ resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==}
cpu: [arm64]
os: [openharmony]
- '@rollup/rollup-win32-arm64-msvc@4.60.1':
- resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==}
+ '@rollup/rollup-win32-arm64-msvc@4.62.2':
+ resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==}
cpu: [arm64]
os: [win32]
- '@rollup/rollup-win32-ia32-msvc@4.60.1':
- resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==}
+ '@rollup/rollup-win32-ia32-msvc@4.62.2':
+ resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==}
cpu: [ia32]
os: [win32]
- '@rollup/rollup-win32-x64-gnu@4.60.1':
- resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==}
+ '@rollup/rollup-win32-x64-gnu@4.62.2':
+ resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==}
cpu: [x64]
os: [win32]
- '@rollup/rollup-win32-x64-msvc@4.60.1':
- resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==}
+ '@rollup/rollup-win32-x64-msvc@4.62.2':
+ resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==}
cpu: [x64]
os: [win32]
- '@rrweb/types@2.0.0-alpha.20':
- resolution: {integrity: sha512-RbnDgKxA/odwB1R4gF7eUUj+rdSrq6ROQJsnMw7MIsGzlbSYvJeZN8YY4XqU0G6sKJvXI6bSzk7w/G94jNwzhw==}
+ '@rrweb/packer@2.0.1':
+ resolution: {integrity: sha512-2U8RkVe+ZK3gTtmKjJKjoh6QL1Q6wIlt3sBTJeCPERLr9e0JIimNrZ1+bog1L30CTVP3ULuDf5Ba7RZO+QNcHg==}
+
+ '@rrweb/replay@2.0.1':
+ resolution: {integrity: sha512-UrZONirb8o4t0urg1aJ8aMSWhNHvJ3a2usuVbPJIm/edyFbwTvvY9sg/rrXccyXOntFhhp73R5nCretODRZcCw==}
+
+ '@rrweb/types@2.0.1':
+ resolution: {integrity: sha512-x8cNaKFxdvS0/5IfKOj1xG8Htnw1fUwqoPqgP61bOaWqZL6t9UtRPpO9ZlMB2z+FiTxg9zlFFL+Da4ZpFuhoiA==}
+
+ '@rrweb/utils@2.0.1':
+ resolution: {integrity: sha512-t+b+4rB23/e4EpoGMdPUr6cKtDsFuOb3LxA9IHE8e3A/Xo0nMUfH0bkwqdpUNa/VGDOdiGeG3bhHjuO9zWX54g==}
'@schummar/icu-type-parser@1.21.5':
resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==}
@@ -2490,12 +2620,6 @@ packages:
resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==}
engines: {node: '>=18'}
- '@sinonjs/commons@3.0.1':
- resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==}
-
- '@sinonjs/fake-timers@15.3.2':
- resolution: {integrity: sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==}
-
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -2584,86 +2708,86 @@ packages:
peerDependencies:
'@svgr/core': '*'
- '@swc/core-darwin-arm64@1.15.26':
- resolution: {integrity: sha512-OmcP96CFsNOwa65tamQayRcfqhNlcQ3YCWOq+0Wb+CAM4uB7kOMrXY41Gj4atthxrGhLQ9pg7Vk26iApb88idA==}
+ '@swc/core-darwin-arm64@1.15.43':
+ resolution: {integrity: sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==}
engines: {node: '>=10'}
cpu: [arm64]
os: [darwin]
- '@swc/core-darwin-x64@1.15.26':
- resolution: {integrity: sha512-liTTTpKSv89ivIxcZ+iU1cRige9Y7JkOjVnJ2Ystzl+DsWNHqt7wLTTgm/u7gEqmmAS2JKryODLQn3q1UtFNPQ==}
+ '@swc/core-darwin-x64@1.15.43':
+ resolution: {integrity: sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==}
engines: {node: '>=10'}
cpu: [x64]
os: [darwin]
- '@swc/core-linux-arm-gnueabihf@1.15.26':
- resolution: {integrity: sha512-Y/g+m3I8CeBof5A3kWWOS6QA2HOIUytF5EeTgfwcAK+GKT/tGe7Xqo5svBtaqflU5od2zzbMTWqkinPXgRWGgA==}
+ '@swc/core-linux-arm-gnueabihf@1.15.43':
+ resolution: {integrity: sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==}
engines: {node: '>=10'}
cpu: [arm]
os: [linux]
- '@swc/core-linux-arm64-gnu@1.15.26':
- resolution: {integrity: sha512-19IvwyPfBN/rz9s7qXhOTQmW0922+pjpRUUvIebu+CMM75nX6YuDzHsGx8hSmn5dS89SNaMCh1lgUuXqm++6jg==}
+ '@swc/core-linux-arm64-gnu@1.15.43':
+ resolution: {integrity: sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@swc/core-linux-arm64-musl@1.15.26':
- resolution: {integrity: sha512-iNlbvTIo425rkKzDLLWFJGnFXr3myETUdIDHcjuiPNZE8b0ogmcAuilC4yEJX7FSHGbnlsoJcCT2xf4b3VJmmQ==}
+ '@swc/core-linux-arm64-musl@1.15.43':
+ resolution: {integrity: sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@swc/core-linux-ppc64-gnu@1.15.26':
- resolution: {integrity: sha512-AuuEOtG+YXKIjIUup4RsxYNklx6XVB3WKWfhxG6hnfPrn7vp89RNOLbbyyprgj6Sk7k9ulwGVTJElEvmBNPSCA==}
+ '@swc/core-linux-ppc64-gnu@1.15.43':
+ resolution: {integrity: sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==}
engines: {node: '>=10'}
cpu: [ppc64]
os: [linux]
libc: [glibc]
- '@swc/core-linux-s390x-gnu@1.15.26':
- resolution: {integrity: sha512-JcMDWQvW1BchUyRg8E0jHiTx7CQYpUr5uDEL1dnPDECrEjBEGG2ynmJ3XX70sWXql0JagqR1t3VpANYFWdUnqA==}
+ '@swc/core-linux-s390x-gnu@1.15.43':
+ resolution: {integrity: sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==}
engines: {node: '>=10'}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@swc/core-linux-x64-gnu@1.15.26':
- resolution: {integrity: sha512-FW7V7Mbpq4+PA7BiAq76LJs8MdNuUSylyuRVfQRkhIyeWadFroZ+KOPgjku8Z/fXzngxBRvsk+PGGB0t8mGcjA==}
+ '@swc/core-linux-x64-gnu@1.15.43':
+ resolution: {integrity: sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@swc/core-linux-x64-musl@1.15.26':
- resolution: {integrity: sha512-w8erqMHsVcdGwUfJxF6LaiTuPoKnyLOcUbhLcxiXrlLt5MLjtlgcIeUY/NWK/oPoyqkgH+/i8pOJnMTxvl83ZQ==}
+ '@swc/core-linux-x64-musl@1.15.43':
+ resolution: {integrity: sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
libc: [musl]
- '@swc/core-win32-arm64-msvc@1.15.26':
- resolution: {integrity: sha512-uDCWCNpUiqkbvPmsuPUTn/P7ag9SqNXD2JT/W3dUu7yZ2krzN+nmmoQ2xRX63/J6RYiHI7aT4jo7Z++lsljlPA==}
+ '@swc/core-win32-arm64-msvc@1.15.43':
+ resolution: {integrity: sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==}
engines: {node: '>=10'}
cpu: [arm64]
os: [win32]
- '@swc/core-win32-ia32-msvc@1.15.26':
- resolution: {integrity: sha512-2k1ax1QmmqLEnpC0uRCw7OXhBfyvdPqERBXupDasjYbChT6ZSO/uha28Bp38cw0viKIG79L27aTDkbkABsMW3w==}
+ '@swc/core-win32-ia32-msvc@1.15.43':
+ resolution: {integrity: sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==}
engines: {node: '>=10'}
cpu: [ia32]
os: [win32]
- '@swc/core-win32-x64-msvc@1.15.26':
- resolution: {integrity: sha512-aUuYecSEGa4SUSdyCWaI/vk8jdseifYnsF1GZQx2+piL8GIuT/5QrVcFfmes4Iwy7FIVXxtzD063z/FfpZ7K7w==}
+ '@swc/core-win32-x64-msvc@1.15.43':
+ resolution: {integrity: sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==}
engines: {node: '>=10'}
cpu: [x64]
os: [win32]
- '@swc/core@1.15.26':
- resolution: {integrity: sha512-tglZGyx8N5PC+x1Nd/JrZxqpqlcZoSuG9gTDKO6AuFToFiVB3uS8HvbKFuO7g3lJzvFf9riAb94xs9HU2UhAHQ==}
+ '@swc/core@1.15.43':
+ resolution: {integrity: sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==}
engines: {node: '>=10'}
peerDependencies:
'@swc/helpers': '>=0.5.17'
@@ -2677,23 +2801,49 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
- '@swc/helpers@0.5.18':
- resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
+ '@swc/helpers@0.5.23':
+ resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
- '@swc/helpers@0.5.21':
- resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==}
+ '@swc/types@0.1.27':
+ resolution: {integrity: sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==}
- '@swc/types@0.1.26':
- resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==}
+ '@tanstack/query-core@5.101.1':
+ resolution: {integrity: sha512-Y6Y92dkXtNqx67m2pMSxUsA3zOCwv862JexZRP8/EPwvKXMPu9m8rv43spiXWzOUIggQ3SQApttALStzhA8B4g==}
- '@tanstack/query-core@5.99.0':
- resolution: {integrity: sha512-3Jv3WQG0BCcH7G+7lf/bP8QyBfJOXeY+T08Rin3GZ1bshvwlbPt7NrDHMEzGdKIOmOzvIQmxjk28YEQX60k7pQ==}
-
- '@tanstack/react-query@5.99.0':
- resolution: {integrity: sha512-OY2bCqPemT1LlqJ8Y2CUau4KELnIhhG9Ol3ZndPbdnB095pRbPo1cHuXTndg8iIwtoHTgwZjyaDnQ0xD0mYwAw==}
+ '@tanstack/react-query@5.101.1':
+ resolution: {integrity: sha512-ZnONUuQKJe1bJMStXUL1s5uKN9FcfC28j5cK+iDZcdSHtUv1wtin1cGc/Oewhf2Oc4eKY7lggtpvT/AbMmhHew==}
peerDependencies:
react: ^18 || ^19
+ '@testing-library/dom@10.4.1':
+ resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
+ engines: {node: '>=18'}
+
+ '@testing-library/jest-dom@6.9.1':
+ resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==}
+ engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
+
+ '@testing-library/react@16.3.2':
+ resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@testing-library/dom': ^10.0.0
+ '@types/react': ^18.0.0 || ^19.0.0
+ '@types/react-dom': ^18.0.0 || ^19.0.0
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@testing-library/user-event@14.6.1':
+ resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==}
+ engines: {node: '>=12', npm: '>=6'}
+ peerDependencies:
+ '@testing-library/dom': '>=7.21.4'
+
'@trysound/sax@0.2.0':
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
@@ -2716,45 +2866,33 @@ packages:
'@tsconfig/svelte@1.0.13':
resolution: {integrity: sha512-5lYJP45Xllo4yE/RUBccBT32eBlRDbqN8r1/MIvQbKxW3aFqaYPCNgm8D5V20X4ShHcwvYWNlKg3liDh1MlBoA==}
- '@tybys/wasm-util@0.10.1':
- resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
+ '@tybys/wasm-util@0.10.3':
+ resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
- '@types/babel__core@7.20.5':
- resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+ '@types/aria-query@5.0.4':
+ resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
- '@types/babel__generator@7.27.0':
- resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
-
- '@types/babel__template@7.4.4':
- resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
-
- '@types/babel__traverse@7.28.0':
- resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
'@types/css-font-loading-module@0.0.7':
resolution: {integrity: sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==}
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
'@types/fs-extra@8.1.5':
resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==}
'@types/glob@7.2.0':
resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
- '@types/istanbul-lib-coverage@2.0.6':
- resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
-
- '@types/istanbul-lib-report@3.0.3':
- resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==}
-
- '@types/istanbul-reports@3.0.4':
- resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==}
-
- '@types/jest@30.0.0':
- resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==}
-
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -2762,8 +2900,8 @@ packages:
resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==}
deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed.
- '@types/node@25.6.0':
- resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
+ '@types/node@26.0.1':
+ resolution: {integrity: sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==}
'@types/pg@8.20.0':
resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==}
@@ -2776,39 +2914,21 @@ packages:
peerDependencies:
'@types/react': ^19.2.0
- '@types/react@19.2.14':
- resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
+ '@types/react@19.2.17':
+ resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==}
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
- '@types/sinonjs__fake-timers@8.1.1':
- resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==}
+ '@types/set-cookie-parser@2.4.10':
+ resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==}
- '@types/sizzle@2.3.10':
- resolution: {integrity: sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==}
-
- '@types/stack-utils@2.0.3':
- resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
-
- '@types/tmp@0.2.6':
- resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==}
+ '@types/statuses@2.0.6':
+ resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==}
'@types/use-sync-external-store@0.0.6':
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
- '@types/yargs-parser@21.0.3':
- resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
-
- '@types/yargs@17.0.33':
- resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==}
-
- '@types/yargs@17.0.35':
- resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==}
-
- '@types/yauzl@2.10.3':
- resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
-
'@umami/react-zen@0.245.0':
resolution: {integrity: sha512-UApl8fDJM6VDaDOc8I67sdClzLpp4e9eJB3ntf6rlUgvdllf5T+hdYiHJ98T/tDFfhZWBKuLhnVDX/qNfjlrSw==}
peerDependencies:
@@ -2816,111 +2936,34 @@ packages:
react-aria-components: ^1.0.0
react-dom: ^18.0.0 || ^19.0.0
- '@ungap/structured-clone@1.3.0':
- resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
+ '@vitest/expect@4.1.9':
+ resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==}
- '@unrs/resolver-binding-android-arm-eabi@1.11.1':
- resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
- cpu: [arm]
- os: [android]
+ '@vitest/mocker@4.1.9':
+ resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
- '@unrs/resolver-binding-android-arm64@1.11.1':
- resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==}
- cpu: [arm64]
- os: [android]
+ '@vitest/pretty-format@4.1.9':
+ resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==}
- '@unrs/resolver-binding-darwin-arm64@1.11.1':
- resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==}
- cpu: [arm64]
- os: [darwin]
+ '@vitest/runner@4.1.9':
+ resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==}
- '@unrs/resolver-binding-darwin-x64@1.11.1':
- resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==}
- cpu: [x64]
- os: [darwin]
+ '@vitest/snapshot@4.1.9':
+ resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==}
- '@unrs/resolver-binding-freebsd-x64@1.11.1':
- resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==}
- cpu: [x64]
- os: [freebsd]
+ '@vitest/spy@4.1.9':
+ resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==}
- '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1':
- resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==}
- cpu: [arm]
- os: [linux]
-
- '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1':
- resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==}
- cpu: [arm]
- os: [linux]
-
- '@unrs/resolver-binding-linux-arm64-gnu@1.11.1':
- resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
- cpu: [arm64]
- os: [linux]
- libc: [glibc]
-
- '@unrs/resolver-binding-linux-arm64-musl@1.11.1':
- resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
- cpu: [arm64]
- os: [linux]
- libc: [musl]
-
- '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
- resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
- cpu: [ppc64]
- os: [linux]
- libc: [glibc]
-
- '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
- resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
- cpu: [riscv64]
- os: [linux]
- libc: [glibc]
-
- '@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
- resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
- cpu: [riscv64]
- os: [linux]
- libc: [musl]
-
- '@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
- resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
- cpu: [s390x]
- os: [linux]
- libc: [glibc]
-
- '@unrs/resolver-binding-linux-x64-gnu@1.11.1':
- resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
- cpu: [x64]
- os: [linux]
- libc: [glibc]
-
- '@unrs/resolver-binding-linux-x64-musl@1.11.1':
- resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
- cpu: [x64]
- os: [linux]
- libc: [musl]
-
- '@unrs/resolver-binding-wasm32-wasi@1.11.1':
- resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
- engines: {node: '>=14.0.0'}
- cpu: [wasm32]
-
- '@unrs/resolver-binding-win32-arm64-msvc@1.11.1':
- resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==}
- cpu: [arm64]
- os: [win32]
-
- '@unrs/resolver-binding-win32-ia32-msvc@1.11.1':
- resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==}
- cpu: [ia32]
- os: [win32]
-
- '@unrs/resolver-binding-win32-x64-msvc@1.11.1':
- resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==}
- cpu: [x64]
- os: [win32]
+ '@vitest/utils@4.1.9':
+ resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==}
'@xstate/fsm@1.6.5':
resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==}
@@ -2939,21 +2982,14 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
- aggregate-error@3.1.0:
- resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
- engines: {node: '>=8'}
+ acorn@8.17.0:
+ resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
ajv@8.18.0:
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
- ansi-colors@4.1.3:
- resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
- engines: {node: '>=6'}
-
- ansi-escapes@4.3.2:
- resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
- engines: {node: '>=8'}
-
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
@@ -2981,19 +3017,9 @@ packages:
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
- anymatch@3.1.3:
- resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
- engines: {node: '>= 8'}
-
- arch@2.2.0:
- resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==}
-
arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
- argparse@1.0.10:
- resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
-
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@@ -3001,6 +3027,13 @@ packages:
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
engines: {node: '>=10'}
+ aria-query@5.3.0:
+ resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
+
+ aria-query@5.3.2:
+ resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
+ engines: {node: '>= 0.4'}
+
array-buffer-byte-length@1.0.2:
resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
engines: {node: '>= 0.4'}
@@ -3013,30 +3046,16 @@ packages:
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
engines: {node: '>= 0.4'}
- asn1@0.2.6:
- resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
-
- assert-plus@1.0.0:
- resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==}
- engines: {node: '>=0.8'}
-
- astral-regex@2.0.0:
- resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
- engines: {node: '>=8'}
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
async-function@1.0.0:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
- asynckit@0.4.0:
- resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
-
- at-least-node@1.0.0:
- resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
- engines: {node: '>= 4.0.0'}
-
- autoprefixer@10.5.0:
- resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==}
+ autoprefixer@10.5.2:
+ resolution: {integrity: sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==}
engines: {node: ^10 || ^12 || >=14}
hasBin: true
peerDependencies:
@@ -3046,44 +3065,13 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
- aws-sign2@0.7.0:
- resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==}
-
aws-ssl-profiles@1.1.2:
resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==}
engines: {node: '>= 6.0.0'}
- aws4@1.13.2:
- resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==}
-
- babel-jest@30.3.0:
- resolution: {integrity: sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- peerDependencies:
- '@babel/core': ^7.11.0 || ^8.0.0-0
-
- babel-plugin-istanbul@7.0.1:
- resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==}
- engines: {node: '>=12'}
-
- babel-plugin-jest-hoist@30.3.0:
- resolution: {integrity: sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
babel-plugin-react-compiler@19.1.0-rc.2:
resolution: {integrity: sha512-kSNA//p5fMO6ypG8EkEVPIqAjwIXm5tMjfD1XRPL/sRjYSbJ6UsvORfaeolNWnZ9n310aM0xJP7peW26BuCVzA==}
- babel-preset-current-node-syntax@1.2.0:
- resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==}
- peerDependencies:
- '@babel/core': ^7.0.0 || ^8.0.0-0
-
- babel-preset-jest@30.3.0:
- resolution: {integrity: sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- peerDependencies:
- '@babel/core': ^7.11.0 || ^8.0.0-beta.1
-
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -3095,17 +3083,11 @@ packages:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
- base64-js@1.5.1:
- resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
-
- baseline-browser-mapping@2.10.19:
- resolution: {integrity: sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==}
+ baseline-browser-mapping@2.10.38:
+ resolution: {integrity: sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==}
engines: {node: '>=6.0.0'}
hasBin: true
- bcrypt-pbkdf@1.0.2:
- resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
-
bcryptjs@3.0.3:
resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==}
hasBin: true
@@ -3114,11 +3096,8 @@ packages:
resolution: {integrity: sha512-7zrmXjAK8u8Z6SOe4R65XObOR5X+Y2I/VVku3t5cPOGQ8/WsBcfFmfnIPiEl5EBMDOzPHRwbiPbMtQBKYdw7RA==}
hasBin: true
- blob-util@2.0.2:
- resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==}
-
- bluebird@3.7.2:
- resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
+ bidi-js@1.0.3:
+ resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
@@ -3126,9 +3105,6 @@ packages:
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
- brace-expansion@1.1.14:
- resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==}
-
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
@@ -3140,45 +3116,27 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
- browserslist@4.25.2:
- resolution: {integrity: sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==}
+ browserslist@4.28.4:
+ resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
- browserslist@4.28.2:
- resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
- engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
- hasBin: true
-
- bs-logger@0.2.6:
- resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==}
- engines: {node: '>= 6'}
-
- bser@2.1.1:
- resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
-
- buffer-crc32@0.2.13:
- resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
-
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
- buffer@5.7.1:
- resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
-
bundle-require@5.1.0:
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
peerDependencies:
esbuild: '>=0.18'
- c12@3.1.0:
- resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==}
+ c12@3.3.4:
+ resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==}
peerDependencies:
- magicast: ^0.3.5
+ magicast: '*'
peerDependenciesMeta:
magicast:
optional: true
@@ -3187,10 +3145,6 @@ packages:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
- cachedir@2.4.0:
- resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==}
- engines: {node: '>=6'}
-
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -3207,10 +3161,6 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
- camelcase@5.3.1:
- resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
- engines: {node: '>=6'}
-
camelcase@6.3.0:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
@@ -3218,14 +3168,12 @@ packages:
caniuse-api@3.0.0:
resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==}
- caniuse-lite@1.0.30001780:
- resolution: {integrity: sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==}
+ caniuse-lite@1.0.30001799:
+ resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==}
- caniuse-lite@1.0.30001788:
- resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==}
-
- caseless@0.12.0:
- resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
+ chai@6.2.2:
+ resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
+ engines: {node: '>=18'}
chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
@@ -3239,10 +3187,6 @@ packages:
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
- char-regex@1.0.2:
- resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
- engines: {node: '>=10'}
-
chart.js@4.5.1:
resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==}
engines: {pnpm: '>=8'}
@@ -3257,45 +3201,24 @@ packages:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
+ chokidar@5.0.0:
+ resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
+ engines: {node: '>= 20.19.0'}
+
chownr@3.0.0:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
- ci-info@4.3.0:
- resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==}
- engines: {node: '>=8'}
-
ci-info@4.4.0:
resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
engines: {node: '>=8'}
- citty@0.1.6:
- resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
-
- citty@0.2.2:
- resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==}
-
- cjs-module-lexer@2.2.0:
- resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==}
-
classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
- clean-stack@2.2.0:
- resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
- engines: {node: '>=6'}
-
- cli-cursor@3.1.0:
- resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
- engines: {node: '>=8'}
-
- cli-table3@0.6.1:
- resolution: {integrity: sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==}
- engines: {node: 10.* || >= 12.*}
-
- cli-truncate@2.1.0:
- resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==}
- engines: {node: '>=8'}
+ cli-width@4.1.0:
+ resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
+ engines: {node: '>= 12'}
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
@@ -3315,16 +3238,9 @@ packages:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
- co@4.6.0:
- resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
- engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
-
code-block-writer@13.0.3:
resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==}
- collect-v8-coverage@1.0.3:
- resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==}
-
color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
@@ -3344,17 +3260,6 @@ packages:
colorette@1.4.0:
resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
- colorette@2.0.20:
- resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
-
- colors@1.4.0:
- resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==}
- engines: {node: '>=0.1.90'}
-
- combined-stream@1.0.8:
- resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
- engines: {node: '>= 0.8'}
-
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@@ -3362,10 +3267,6 @@ packages:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
- commander@6.2.1:
- resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==}
- engines: {node: '>= 6'}
-
commander@7.2.0:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
@@ -3374,10 +3275,6 @@ packages:
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
engines: {node: ^12.20.0 || >=14}
- common-tags@1.8.2:
- resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
- engines: {node: '>=4.0.0'}
-
commondir@1.0.1:
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
@@ -3400,8 +3297,9 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
- core-util-is@1.0.2:
- resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
+ cookie@1.1.1:
+ resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
+ engines: {node: '>=18'}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
@@ -3480,12 +3378,19 @@ packages:
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+ css-tree@3.2.1:
+ resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
css-what@6.2.2:
resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
engines: {node: '>= 6'}
- cssdb@8.8.0:
- resolution: {integrity: sha512-QbLeyz2Bgso1iRlh7IpWk6OKa3lLNGXsujVjDMPl9rOZpxKeiG69icLpbLCFxeURwmcdIfZqQyhlooKJYM4f8Q==}
+ css.escape@1.5.1:
+ resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
+
+ cssdb@8.9.0:
+ resolution: {integrity: sha512-J8jOU/hLjaXcO1LldOLraJSQpfLXRKof0I7mtbRyOy2AAXgqst0x9rlgi2qXeD6d0ou3ZLqcPAMqYVbpCbrxEw==}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
@@ -3521,11 +3426,6 @@ packages:
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
- cypress@15.14.0:
- resolution: {integrity: sha512-AHt9YLKVl6uOFfXsLM3+LSZFwsx36BJRyFv4CjsqcRgr+V9kir0IPVRZUgqZVNudRalx771A1c4yR3DmXvSiBQ==}
- engines: {node: ^20.1.0 || ^22.0.0 || >=24.0.0}
- hasBin: true
-
d3-array@2.12.1:
resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==}
@@ -3561,10 +3461,6 @@ packages:
d3-zoom@2.0.0:
resolution: {integrity: sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw==}
- dashdash@1.14.1:
- resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==}
- engines: {node: '>=0.10'}
-
dashify@2.0.0:
resolution: {integrity: sha512-hpA5C/YrPjucXypHPPc0oJ1l9Hf6wWbiOL7Ik42cxnsUOhWiCB/fylKbKqqJalW9FgkNQCw16YO8uW9Hs0Iy1A==}
engines: {node: '>=4'}
@@ -3573,6 +3469,10 @@ packages:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
+ data-urls@7.0.0:
+ resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
data-view-buffer@1.0.2:
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
engines: {node: '>= 0.4'}
@@ -3590,19 +3490,8 @@ packages:
peerDependencies:
date-fns: ^3.0.0 || ^4.0.0
- date-fns@4.1.0:
- resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
-
- dayjs@1.11.20:
- resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
-
- debug@3.2.7:
- resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
- peerDependencies:
- supports-color: '*'
- peerDependenciesMeta:
- supports-color:
- optional: true
+ date-fns@4.4.0:
+ resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
@@ -3624,6 +3513,8 @@ packages:
peerDependenciesMeta:
babel-plugin-macros:
optional: true
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
deepmerge-ts@7.1.5:
resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==}
@@ -3648,14 +3539,14 @@ packages:
resolution: {integrity: sha512-gPqh0mKTPvaUZGAuHbrBUYKZWBNAeHG7TU3QH5EhVwPMyKvmfJaNXhcD2jTcXsJRRcffuho4vaYweu80dRrMGA==}
engines: {node: '>=18'}
- delayed-stream@1.0.0:
- resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
- engines: {node: '>=0.4.0'}
-
denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
destr@2.0.5:
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
@@ -3669,10 +3560,6 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
- detect-newline@3.1.0:
- resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
- engines: {node: '>=8'}
-
diff@4.0.2:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'}
@@ -3684,6 +3571,12 @@ packages:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
+ dom-accessibility-api@0.5.16:
+ resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
+
+ dom-accessibility-api@0.6.3:
+ resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
+
dom-serializer@1.4.1:
resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==}
@@ -3733,24 +3626,14 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
- ecc-jsbn@0.1.2:
- resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==}
-
ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
effect@3.20.0:
resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==}
- electron-to-chromium@1.5.202:
- resolution: {integrity: sha512-NxbYjRmiHcHXV1Ws3fWUW+SLb62isauajk45LUJ/HgIOkUA7jLZu/X2Iif+X9FBNK8QkF9Zb4Q2mcwXCcY30mg==}
-
- electron-to-chromium@1.5.340:
- resolution: {integrity: sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==}
-
- emittery@0.13.1:
- resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
- engines: {node: '>=12'}
+ electron-to-chromium@1.5.378:
+ resolution: {integrity: sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -3762,13 +3645,6 @@ packages:
resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==}
engines: {node: '>=14'}
- end-of-stream@1.4.5:
- resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
-
- enquirer@2.4.1:
- resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
- engines: {node: '>=8.6'}
-
entities@2.2.0:
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
@@ -3776,6 +3652,10 @@ packages:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
+ entities@8.0.0:
+ resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==}
+ engines: {node: '>=20.19.0'}
+
env-paths@3.0.0:
resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -3795,6 +3675,9 @@ packages:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
+ es-module-lexer@2.1.0:
+ resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
+
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
@@ -3812,8 +3695,8 @@ packages:
engines: {node: '>=18'}
hasBin: true
- esbuild@0.28.0:
- resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==}
+ esbuild@0.28.1:
+ resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
engines: {node: '>=18'}
hasBin: true
@@ -3825,66 +3708,25 @@ packages:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
- escape-string-regexp@2.0.0:
- resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==}
- engines: {node: '>=8'}
-
- esprima@4.0.1:
- resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
- engines: {node: '>=4'}
- hasBin: true
-
estree-walker@0.6.1:
resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==}
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
- eventemitter2@6.4.7:
- resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==}
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
- execa@4.1.0:
- resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==}
- engines: {node: '>=10'}
-
- execa@5.1.1:
- resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
- engines: {node: '>=10'}
-
- executable@4.1.1:
- resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==}
- engines: {node: '>=4'}
-
- exit-x@0.2.2:
- resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==}
- engines: {node: '>= 0.8.0'}
-
- expect@30.1.1:
- resolution: {integrity: sha512-OKe7cdic4qbfWd/CcgwJvvCrNX2KWfuMZee9AfJHL1gTYmvqjBjZG1a2NwfhspBzxzlXwsN75WWpKTYfsJpBxg==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- expect@30.3.0:
- resolution: {integrity: sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+ expect-type@1.3.0:
+ resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
+ engines: {node: '>=12.0.0'}
exsolve@1.0.8:
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
- extend@3.0.2:
- resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
-
- extract-zip@2.0.1:
- resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
- engines: {node: '>= 10.17.0'}
- hasBin: true
-
- extsprintf@1.3.0:
- resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==}
- engines: {'0': node >=0.6.0}
-
fast-check@3.23.2:
resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==}
engines: {node: '>=8.0.0'}
@@ -3896,21 +3738,21 @@ packages:
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'}
- fast-json-stable-stringify@2.1.0:
- resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+ fast-string-truncated-width@3.0.3:
+ resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==}
+
+ fast-string-width@3.0.2:
+ resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
fast-uri@3.1.0:
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
+ fast-wrap-ansi@0.2.0:
+ resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==}
+
fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
- fb-watchman@2.0.2:
- resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
-
- fd-slicer@1.1.0:
- resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
-
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -3927,18 +3769,10 @@ packages:
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
- figures@3.2.0:
- resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
- engines: {node: '>=8'}
-
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
- find-up@4.1.0:
- resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
- engines: {node: '>=8'}
-
fix-dts-default-cjs-exports@1.0.1:
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
@@ -3950,13 +3784,6 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
- forever-agent@0.6.1:
- resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
-
- form-data@4.0.5:
- resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
- engines: {node: '>= 6'}
-
formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
@@ -3964,8 +3791,8 @@ packages:
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
- framer-motion@12.38.0:
- resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==}
+ framer-motion@12.41.0:
+ resolution: {integrity: sha512-OHAMNiCEON1RDBlRGuulsN5AD8ptMjvk5QWfFmYmBLPZ3zFGIJe60kQucQQf4cez1OzQmjYBWDY+dYfISkUdqg==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
@@ -3982,13 +3809,14 @@ packages:
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
engines: {node: '>=6 <7 || >=8'}
- fs-extra@9.1.0:
- resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
- engines: {node: '>=10'}
-
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+ fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -4022,10 +3850,6 @@ packages:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
- get-package-type@0.1.0:
- resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}
- engines: {node: '>=8.0.0'}
-
get-port-please@3.2.0:
resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==}
@@ -4033,26 +3857,12 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
- get-stream@5.2.0:
- resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
- engines: {node: '>=8'}
-
- get-stream@6.0.1:
- resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
- engines: {node: '>=10'}
-
get-symbol-description@1.1.0:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'}
- get-tsconfig@4.13.0:
- resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
-
- getpass@0.1.7:
- resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
-
- giget@2.0.0:
- resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==}
+ giget@3.2.0:
+ resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==}
hasBin: true
glob-parent@5.1.2:
@@ -4073,10 +3883,6 @@ packages:
engines: {node: '>=12'}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
- global-dirs@3.0.1:
- resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==}
- engines: {node: '>=10'}
-
globalthis@1.0.4:
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
engines: {node: '>= 0.4'}
@@ -4102,10 +3908,9 @@ packages:
graphmatch@1.1.1:
resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==}
- handlebars@4.7.9:
- resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==}
- engines: {node: '>=0.4.7'}
- hasBin: true
+ graphql@16.14.0:
+ resolution: {integrity: sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==}
+ engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
has-bigints@1.1.0:
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
@@ -4134,43 +3939,27 @@ packages:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
- hasha@5.2.2:
- resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==}
- engines: {node: '>=8'}
-
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
- hasown@2.0.3:
- resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
- engines: {node: '>= 0.4'}
+ headers-polyfill@5.0.1:
+ resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==}
- hono@4.12.9:
- resolution: {integrity: sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==}
+ hono@4.12.21:
+ resolution: {integrity: sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ==}
engines: {node: '>=16.9.0'}
hosted-git-info@2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
- html-escaper@2.0.2:
- resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
-
- http-signature@1.4.0:
- resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==}
- engines: {node: '>=0.10'}
+ html-encoding-sniffer@6.0.0:
+ resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
http-status-codes@2.3.0:
resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==}
- human-signals@1.1.1:
- resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==}
- engines: {node: '>=8.12.0'}
-
- human-signals@2.1.0:
- resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
- engines: {node: '>=10.17.0'}
-
iconv-lite@0.7.2:
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
engines: {node: '>=0.10.0'}
@@ -4184,11 +3973,8 @@ packages:
peerDependencies:
postcss: ^8.1.0
- icu-minify@4.9.1:
- resolution: {integrity: sha512-6NkfF9GHHFouqnz+wuiLjCWQiyxoEyJ5liUv4Jxxo/8wyhV7MY0L0iTEGDAVEa4aAD58WqTxFMa20S5nyMjwNw==}
-
- ieee754@1.2.1:
- resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+ icu-minify@4.13.0:
+ resolution: {integrity: sha512-SIFMeUHZJjzS5RvIGvybKvWoHjDm9cGVEs2EpJ8PmywOdJLWyblPm7TdPLLoUtkJtwQD7iGhl2WMptZ+N0on+w==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
@@ -4201,8 +3987,8 @@ packages:
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
- immer@11.1.4:
- resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==}
+ immer@11.1.8:
+ resolution: {integrity: sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==}
import-cwd@3.0.0:
resolution: {integrity: sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==}
@@ -4216,15 +4002,6 @@ packages:
resolution: {integrity: sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==}
engines: {node: '>=8'}
- import-local@3.2.0:
- resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==}
- engines: {node: '>=8'}
- hasBin: true
-
- imurmurhash@0.1.4:
- resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
- engines: {node: '>=0.8.19'}
-
indent-string@4.0.0:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
@@ -4236,10 +4013,6 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
- ini@2.0.0:
- resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==}
- engines: {node: '>=10'}
-
internal-slot@1.1.0:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
@@ -4247,11 +4020,11 @@ packages:
internmap@1.0.1:
resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==}
- intl-messageformat@11.2.1:
- resolution: {integrity: sha512-1gAVEUt3wEPvTqML4Fsw9klZV5j0vszQxayP/fi6gUroAc8AUHiNaisBKLWxybL1AdWq1mP07YV1q8v4N92ilQ==}
+ intl-messageformat@11.2.8:
+ resolution: {integrity: sha512-l323RCl3qJDVQ8U9j74ut/hVMdg3VPsOHpVMDvFfz9qiq4dPO5ooVYFNVUzzrpgG39a+RLzcXyJb8VFgIU+tUA==}
- ipaddr.js@2.3.0:
- resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==}
+ ipaddr.js@2.4.0:
+ resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==}
engines: {node: '>= 10'}
is-array-buffer@3.0.5:
@@ -4310,10 +4083,6 @@ packages:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
- is-generator-fn@2.1.0:
- resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==}
- engines: {node: '>=6'}
-
is-generator-function@1.1.0:
resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==}
engines: {node: '>= 0.4'}
@@ -4322,10 +4091,6 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
- is-installed-globally@0.4.0:
- resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==}
- engines: {node: '>=10'}
-
is-localhost-ip@3.0.1:
resolution: {integrity: sha512-OwNA8bw72L/gDVfCrhJ3GFb3AGp+I7t+cvJaQGkiLZgeBAZmAygIduAdhLq56JycuJ68NCICxA2yyIaAtbR9Kg==}
engines: {node: '>=18'}
@@ -4341,6 +4106,9 @@ packages:
resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==}
engines: {node: '>= 0.4'}
+ is-node-process@1.2.0:
+ resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==}
+
is-number-object@1.1.1:
resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==}
engines: {node: '>= 0.4'}
@@ -4353,10 +4121,6 @@ packages:
resolution: {integrity: sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
- is-path-inside@3.0.3:
- resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
- engines: {node: '>=8'}
-
is-path-inside@4.0.0:
resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==}
engines: {node: '>=12'}
@@ -4365,6 +4129,9 @@ packages:
resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==}
engines: {node: '>=0.10.0'}
+ is-potential-custom-element-name@1.0.1:
+ resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+
is-property@1.0.2:
resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
@@ -4386,10 +4153,6 @@ packages:
is-standalone-pwa@0.1.1:
resolution: {integrity: sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==}
- is-stream@2.0.1:
- resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
- engines: {node: '>=8'}
-
is-string@1.1.1:
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
engines: {node: '>= 0.4'}
@@ -4402,13 +4165,6 @@ packages:
resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
engines: {node: '>= 0.4'}
- is-typedarray@1.0.0:
- resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
-
- is-unicode-supported@0.1.0:
- resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
- engines: {node: '>=10'}
-
is-weakmap@2.0.2:
resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
engines: {node: '>= 0.4'}
@@ -4427,189 +4183,18 @@ packages:
isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
- isbot@5.1.39:
- resolution: {integrity: sha512-obH0yYahGXdzNxo+djmHhBYThUKDkz565cxkIlt2L9hXfv1NlaLKoDBHo6KxXsYrIXx2RK3x5vY36CfZcobxEw==}
+ isbot@5.1.44:
+ resolution: {integrity: sha512-PGEHtwMnKbZpeSEXW2Utx+/JWed7dp6DiH0WWg33vGSDA7RUvpUeJSVlLrVkQ1RCpvDOUc/eH9ql7VsdbBZ8pA==}
engines: {node: '>=18'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
- isstream@0.1.2:
- resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==}
-
- istanbul-lib-coverage@3.2.2:
- resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
- engines: {node: '>=8'}
-
- istanbul-lib-instrument@6.0.3:
- resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==}
- engines: {node: '>=10'}
-
- istanbul-lib-report@3.0.1:
- resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
- engines: {node: '>=10'}
-
- istanbul-lib-source-maps@5.0.6:
- resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
- engines: {node: '>=10'}
-
- istanbul-reports@3.2.0:
- resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
- engines: {node: '>=8'}
-
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
- jest-changed-files@30.3.0:
- resolution: {integrity: sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-circus@30.3.0:
- resolution: {integrity: sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-cli@30.3.0:
- resolution: {integrity: sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- hasBin: true
- peerDependencies:
- node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
- peerDependenciesMeta:
- node-notifier:
- optional: true
-
- jest-config@30.3.0:
- resolution: {integrity: sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- peerDependencies:
- '@types/node': '*'
- esbuild-register: '>=3.4.0'
- ts-node: '>=9.0.0'
- peerDependenciesMeta:
- '@types/node':
- optional: true
- esbuild-register:
- optional: true
- ts-node:
- optional: true
-
- jest-diff@30.1.1:
- resolution: {integrity: sha512-LUU2Gx8EhYxpdzTR6BmjL1ifgOAQJQELTHOiPv9KITaKjZvJ9Jmgigx01tuZ49id37LorpGc9dPBPlXTboXScw==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-diff@30.3.0:
- resolution: {integrity: sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-docblock@30.2.0:
- resolution: {integrity: sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-each@30.3.0:
- resolution: {integrity: sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-environment-node@30.3.0:
- resolution: {integrity: sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-haste-map@30.3.0:
- resolution: {integrity: sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-leak-detector@30.3.0:
- resolution: {integrity: sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-matcher-utils@30.1.1:
- resolution: {integrity: sha512-SuH2QVemK48BNTqReti6FtjsMPFsSOD/ZzRxU1TttR7RiRsRSe78d03bb4Cx6D4bQC/80Q8U4VnaaAH9FlbZ9w==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-matcher-utils@30.3.0:
- resolution: {integrity: sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-message-util@30.1.0:
- resolution: {integrity: sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-message-util@30.3.0:
- resolution: {integrity: sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-mock@30.0.5:
- resolution: {integrity: sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-mock@30.3.0:
- resolution: {integrity: sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-pnp-resolver@1.2.3:
- resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==}
- engines: {node: '>=6'}
- peerDependencies:
- jest-resolve: '*'
- peerDependenciesMeta:
- jest-resolve:
- optional: true
-
- jest-regex-util@30.0.1:
- resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-resolve-dependencies@30.3.0:
- resolution: {integrity: sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-resolve@30.3.0:
- resolution: {integrity: sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-runner@30.3.0:
- resolution: {integrity: sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-runtime@30.3.0:
- resolution: {integrity: sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-snapshot@30.3.0:
- resolution: {integrity: sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-util@30.0.5:
- resolution: {integrity: sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-util@30.3.0:
- resolution: {integrity: sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-validate@30.3.0:
- resolution: {integrity: sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-watcher@30.3.0:
- resolution: {integrity: sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest-worker@30.3.0:
- resolution: {integrity: sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- jest@30.3.0:
- resolution: {integrity: sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- hasBin: true
- peerDependencies:
- node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
- peerDependenciesMeta:
- node-notifier:
- optional: true
-
- jiti@2.6.1:
- resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
+ jiti@2.7.0:
+ resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
joycon@3.1.1:
@@ -4619,16 +4204,18 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
- js-yaml@3.14.2:
- resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
- hasBin: true
-
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
- jsbn@0.1.1:
- resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
+ jsdom@29.1.1:
+ resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
+ peerDependencies:
+ canvas: ^3.0.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
@@ -4644,12 +4231,6 @@ packages:
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
- json-schema@0.4.0:
- resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
-
- json-stringify-safe@5.0.1:
- resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
-
json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
@@ -4658,17 +4239,10 @@ packages:
jsonfile@4.0.0:
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
- jsonfile@6.2.0:
- resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
-
jsonwebtoken@9.0.3:
resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==}
engines: {node: '>=12', npm: '>=6'}
- jsprim@2.0.2:
- resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==}
- engines: {'0': node >=0.6.0}
-
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
@@ -4686,13 +4260,83 @@ packages:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
- leven@3.1.0:
- resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
- engines: {node: '>=6'}
-
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
lilconfig@2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
@@ -4704,15 +4348,6 @@ packages:
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
- listr2@3.14.0:
- resolution: {integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==}
- engines: {node: '>=10.0.0'}
- peerDependencies:
- enquirer: '>= 2.3.0 < 3'
- peerDependenciesMeta:
- enquirer:
- optional: true
-
load-json-file@4.0.0:
resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==}
engines: {node: '>=4'}
@@ -4725,10 +4360,6 @@ packages:
resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==}
engines: {node: '>= 12.13.0'}
- locate-path@5.0.0:
- resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
- engines: {node: '>=8'}
-
lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
@@ -4759,17 +4390,6 @@ packages:
lodash.uniq@4.5.0:
resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==}
- lodash@4.18.1:
- resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
-
- log-symbols@4.1.0:
- resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
- engines: {node: '>=10'}
-
- log-update@4.0.0:
- resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==}
- engines: {node: '>=10'}
-
long@5.3.2:
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
@@ -4783,6 +4403,10 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+ lru-cache@11.3.6:
+ resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==}
+ engines: {node: 20 || >=22}
+
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -4795,24 +4419,21 @@ packages:
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
- lucide-react@1.8.0:
- resolution: {integrity: sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==}
+ lucide-react@1.21.0:
+ resolution: {integrity: sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ lz-string@1.5.0:
+ resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
+ hasBin: true
+
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
- make-dir@4.0.0:
- resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
- engines: {node: '>=10'}
-
make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
- makeerror@1.0.12:
- resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
-
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -4830,13 +4451,13 @@ packages:
mdn-data@2.0.30:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
+ mdn-data@2.27.1:
+ resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
+
memorystream@0.3.1:
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
engines: {node: '>= 0.10.0'}
- merge-stream@2.0.0:
- resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
-
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@@ -4845,17 +4466,9 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
- mime-db@1.52.0:
- resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
- engines: {node: '>= 0.6'}
-
- mime-types@2.1.35:
- resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
- engines: {node: '>= 0.6'}
-
- mimic-fn@2.1.0:
- resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
- engines: {node: '>=6'}
+ min-indent@1.0.1:
+ resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
+ engines: {node: '>=4'}
minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
@@ -4864,9 +4477,6 @@ packages:
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
- minimatch@3.1.5:
- resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
-
minimatch@5.1.6:
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
engines: {node: '>=10'}
@@ -4896,14 +4506,14 @@ packages:
resolution: {integrity: sha512-7e87vk0DdWT647wjcfEtWeMtjm+zVGqNohN/aeIymbUfjHQ2T4Sx5kM+1irVDBSloNC3CkGKxswdMoo8yhqTDg==}
engines: {node: '>=10', npm: '>=6'}
- motion-dom@12.38.0:
- resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
+ motion-dom@12.41.0:
+ resolution: {integrity: sha512-Lk3J39fOGg6xNr1KRZsN6usDyBf8aP7MEbUPez1VCughHt79OrP7VGqNrPyFL0riaT7WS8t9DRw1M3BHtM/xKw==}
- motion-utils@12.36.0:
- resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==}
+ motion-utils@12.39.0:
+ resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==}
- motion@12.38.0:
- resolution: {integrity: sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==}
+ motion@12.41.0:
+ resolution: {integrity: sha512-avEDKE22rFPJqDr3Ttk7gMQpeaOmNik60NoJ5T0tj+RBCNvz21D3ArY3l4uitoeQ7eIpDqueWaO3pPYFv8JOVA==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
@@ -4919,6 +4529,20 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+ msw@2.14.6:
+ resolution: {integrity: sha512-ALe+N10S72cyx94cMcy3Zs4HhXCj35sgeAL4c+WTvKi0zWnbd8/h0lcFqv0mb2P+aSgAdD7p9HzvA0DiUPxsyg==}
+ engines: {node: '>=18'}
+ hasBin: true
+ peerDependencies:
+ typescript: '>= 4.8.x'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ mute-stream@3.0.0:
+ resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==}
+ engines: {node: ^20.17.0 || >=22.9.0}
+
mysql2@3.15.3:
resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==}
engines: {node: '>= 8.0'}
@@ -4930,31 +4554,25 @@ packages:
resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==}
engines: {node: '>=8.0.0'}
- nanoid@3.3.11:
- resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ nanoid@3.3.12:
+ resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
- napi-postinstall@0.3.4:
- resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==}
- engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+ nanoid@3.3.15:
+ resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
- natural-compare@1.4.0:
- resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
-
negotiator@1.0.0:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
- neo-async@2.6.2:
- resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
+ next-intl-swc-plugin-extractor@4.13.0:
+ resolution: {integrity: sha512-6S/fJI0KXvLCL8nhBo9P8eGaJPzmwJBTCzX0NaUIj0VyU8U89d//T+vjMLdNIXl5MlLaYH7B9MbAjb8Mvu+tqQ==}
- next-intl-swc-plugin-extractor@4.9.1:
- resolution: {integrity: sha512-8whJJ6oxJz8JqkHarggmmuEDyXgC7nEnaPhZD91CJwEWW4xp0AST3Mw17YxvHyP2vAF3taWfFbs1maD+WWtz3w==}
-
- next-intl@4.9.1:
- resolution: {integrity: sha512-N7ga0CjtYcdxNvaKNIi6eJ2mmatlHK5hp8rt0YO2Omoc1m0gean242/Ukdj6+gJNiReBVcYIjK0HZeNx7CV1ug==}
+ next-intl@4.13.0:
+ resolution: {integrity: sha512-OvNq2v5XLx4EkQOsAhVE9g+6zdb83XHusADCXXtIW4LILYnjEVaeINdr1lkVWKSjzwNUiMSlH5N4K0OQTRiv6A==}
peerDependencies:
next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
@@ -4963,8 +4581,8 @@ packages:
typescript:
optional: true
- next@16.2.4:
- resolution: {integrity: sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==}
+ next@16.2.9:
+ resolution: {integrity: sha512-MEOJiq/UvuezAdqVSceHbqDgZt1kDw2tpGVOlsdIoJsQdbN2JY2hpVG4xnXGkbdJUOEWhnRfiu/O4Hpc9Juwww==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -4998,21 +4616,13 @@ packages:
engines: {node: '>=10.5.0'}
deprecated: Use your platform's native DOMException instead
- node-fetch-native@1.6.7:
- resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
-
node-fetch@3.3.2:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
- node-int64@0.4.0:
- resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
-
- node-releases@2.0.19:
- resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
-
- node-releases@2.0.37:
- resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==}
+ node-releases@2.0.49:
+ resolution: {integrity: sha512-f06bl1D+8ZDkn2oOQQKAh5/otFWqVnM1Q5oerA8Pex7UfT66Tx4IPHIqVVFKqFT3FUtaDstdgkM7yT7JWhqxfw==}
+ engines: {node: '>=18'}
non-error@0.1.0:
resolution: {integrity: sha512-TMB1uHiGsHRGv1uYclfhivcnf0/PdFp2pNqRxXjncaAsjYMoisaQJI+SSZCqRq+VliwRTC8tsMQfmrWjDMhkPQ==}
@@ -5021,10 +4631,6 @@ packages:
normalize-package-data@2.5.0:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
- normalize-path@3.0.0:
- resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
- engines: {node: '>=0.10.0'}
-
normalize-url@6.1.0:
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
engines: {node: '>=10'}
@@ -5034,18 +4640,9 @@ packages:
engines: {node: '>= 4'}
hasBin: true
- npm-run-path@4.0.1:
- resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
- engines: {node: '>=8'}
-
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
- nypm@0.6.5:
- resolution: {integrity: sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==}
- engines: {node: '>=18'}
- hasBin: true
-
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -5062,18 +4659,18 @@ packages:
resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==}
engines: {node: '>= 0.4'}
+ obug@2.1.3:
+ resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
+ engines: {node: '>=12.20.0'}
+
ohash@2.0.11:
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
- onetime@5.1.2:
- resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
- engines: {node: '>=6'}
-
- ospath@1.2.2:
- resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==}
+ outvariant@1.4.3:
+ resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
otplib@13.4.0:
resolution: {integrity: sha512-RUcYcRMCgRWhUE/XabRppXpUwCwaWBNHe5iPXhdvP8wwDGpGpsIf/kxX/ec3zFsOaM1Oq8lEhUqDwk6W7DHkwg==}
@@ -5086,22 +4683,6 @@ packages:
resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
engines: {node: '>=4'}
- p-limit@2.3.0:
- resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
- engines: {node: '>=6'}
-
- p-limit@3.1.0:
- resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
- engines: {node: '>=10'}
-
- p-locate@4.1.0:
- resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
- engines: {node: '>=8'}
-
- p-map@4.0.0:
- resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
- engines: {node: '>=10'}
-
p-map@7.0.4:
resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==}
engines: {node: '>=18'}
@@ -5114,18 +4695,14 @@ packages:
resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
engines: {node: '>=8'}
- p-try@2.2.0:
- resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
- engines: {node: '>=6'}
-
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
- papaparse@5.5.3:
- resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==}
+ papaparse@5.5.4:
+ resolution: {integrity: sha512-SwzWD9gl/ElwYLCI0nUja1mFJzjq2D8ziShfNBa7zCHzkOozeOGDwHWQ+tvCzEZcewecWZ5U7kUopDnG+DFYEQ==}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
@@ -5139,13 +4716,12 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
+ parse5@8.0.1:
+ resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==}
+
path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
- path-exists@4.0.0:
- resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
- engines: {node: '>=8'}
-
path-is-absolute@1.0.1:
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'}
@@ -5165,6 +4741,9 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
+ path-to-regexp@6.3.0:
+ resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
+
path-type@3.0.0:
resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==}
engines: {node: '>=4'}
@@ -5180,39 +4759,33 @@ packages:
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
- pend@1.2.0:
- resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
+ perfect-debounce@2.1.0:
+ resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==}
- perfect-debounce@1.0.0:
- resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
+ pg-cloudflare@1.4.0:
+ resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==}
- performance-now@2.1.0:
- resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
-
- pg-cloudflare@1.3.0:
- resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==}
-
- pg-connection-string@2.12.0:
- resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==}
+ pg-connection-string@2.14.0:
+ resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==}
pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
engines: {node: '>=4.0.0'}
- pg-pool@3.13.0:
- resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==}
+ pg-pool@3.14.0:
+ resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==}
peerDependencies:
pg: '>=8.0'
- pg-protocol@1.13.0:
- resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==}
+ pg-protocol@1.15.0:
+ resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==}
pg-types@2.2.0:
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
engines: {node: '>=4'}
- pg@8.20.0:
- resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==}
+ pg@8.22.0:
+ resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==}
engines: {node: '>= 16.0.0'}
peerDependencies:
pg-native: '>=3.0.1'
@@ -5259,15 +4832,21 @@ packages:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
- pkg-dir@4.2.0:
- resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
- engines: {node: '>=8'}
-
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
- pkg-types@2.3.0:
- resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
+ pkg-types@2.3.1:
+ resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
+
+ playwright-core@1.61.1:
+ resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.61.1:
+ resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==}
+ engines: {node: '>=18'}
+ hasBin: true
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
@@ -5297,8 +4876,8 @@ packages:
peerDependencies:
postcss: ^8.4.6
- postcss-color-functional-notation@8.0.3:
- resolution: {integrity: sha512-MyaFK+3PusD7F2+qlMDP6+zfSgHWP17AtmvHQs44W3+Qbb39VptVDVRJ4Lf7gHSVffW5ekEy/XrsZ0S0t34hrA==}
+ postcss-color-functional-notation@8.0.5:
+ resolution: {integrity: sha512-Cxr97Vtt2VeJCGaex0JNSU5MViqYtjKmJLHKM+jI7d+qIs0J5xgHEVG6Q2bTCaFJ1yjcFz9s9VmWCibuzk3+MA==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
@@ -5375,8 +4954,8 @@ packages:
peerDependencies:
postcss: ^8.2.15
- postcss-double-position-gradients@7.0.0:
- resolution: {integrity: sha512-Msr/dxj8Os7KLJE5Hdhvprwm3K5Zrh1KTY0eFN3ngPKNkej/Usy4BM9JQmqE6CLAkDpHoQVsi4snbL72CPt6qg==}
+ postcss-double-position-gradients@7.0.1:
+ resolution: {integrity: sha512-M69I4EolEGwiYa0KmxKWg4zZp2DxhlNM0Bz12OvHCj930GXDVCvFhdWNGsRscz6BIijN6tFryzSFsy8kMLyD5Q==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
@@ -5421,8 +5000,8 @@ packages:
peerDependencies:
postcss: ^8.0.0
- postcss-lab-function@8.0.3:
- resolution: {integrity: sha512-rUa27RLVXjMn1aDkHEt5dRsK80+bAACPr8w5Ow0BkIlfH6gEk0Mh1I0REkYhtp4UhKFw1HLEk3AzvKBi6BGOqw==}
+ postcss-lab-function@8.0.5:
+ resolution: {integrity: sha512-ohQnYx1LloPkiLQhAjpt/Y9tAGCGOBOUaxgbcmO+1bDTFzUQCTfdpemOVh6oewI4V2K6q7+Vz8d3rP1glvK3uw==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
@@ -5617,8 +5196,8 @@ packages:
peerDependencies:
postcss: ^8.4
- postcss-preset-env@11.2.1:
- resolution: {integrity: sha512-dqL7WR5wg9yP/+6pTHIsIeIpK6XVghJDE4/r4ZSdr5ExrbLiN5x78gly0Xs0MLGbHy2oT3WWNfbxowmnw9BurQ==}
+ postcss-preset-env@11.3.1:
+ resolution: {integrity: sha512-ox2lu2L0fbuKXB0zRcUFCNii7koS9+fNLFqj+WOKaJ4DU/zZsYkFHOmz73lWNTKx8OHDqnV0R7Si98PIbJXLjQ==}
engines: {node: '>=20.19.0'}
peerDependencies:
postcss: ^8.4
@@ -5652,16 +5231,12 @@ packages:
peerDependencies:
postcss: ^8.4
- postcss-selector-parser@6.1.2:
- resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
+ postcss-selector-parser@6.1.4:
+ resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==}
engines: {node: '>=4'}
- postcss-selector-parser@7.1.0:
- resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==}
- engines: {node: '>=4'}
-
- postcss-selector-parser@7.1.1:
- resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
+ postcss-selector-parser@7.1.4:
+ resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==}
engines: {node: '>=4'}
postcss-svgo@5.1.0:
@@ -5683,8 +5258,8 @@ packages:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
- postcss@8.5.10:
- resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==}
+ postcss@8.5.15:
+ resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14}
postgres-array@2.0.0:
@@ -5720,20 +5295,12 @@ packages:
engines: {node: '>=10.13.0'}
hasBin: true
- pretty-bytes@5.6.0:
- resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}
- engines: {node: '>=6'}
+ pretty-format@27.5.1:
+ resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
+ engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
- pretty-format@30.0.5:
- resolution: {integrity: sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- pretty-format@30.3.0:
- resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==}
- engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
-
- prisma@7.7.0:
- resolution: {integrity: sha512-HlgwRBt1uEFB9LStHL4HLYDvoi4BNu1rYA0hPG0zCAEyK9SaZBqp7E5Rjpc3Qh8Lex/ye/svoHZ0OWoFNhWxuQ==}
+ prisma@7.8.0:
+ resolution: {integrity: sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==}
engines: {node: ^20.19 || ^22.12 || >=24.0}
hasBin: true
peerDependencies:
@@ -5748,10 +5315,6 @@ packages:
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
- process@0.11.10:
- resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
- engines: {node: '>= 0.6.0'}
-
promise.series@0.2.0:
resolution: {integrity: sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==}
engines: {node: '>=0.12'}
@@ -5766,11 +5329,9 @@ packages:
proper-lockfile@4.1.2:
resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==}
- proxy-from-env@1.0.0:
- resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==}
-
- pump@3.0.4:
- resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
pure-rand@6.1.0:
resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==}
@@ -5796,28 +5357,28 @@ packages:
raf-schd@4.0.3:
resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==}
- rc9@2.1.2:
- resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==}
+ rc9@3.0.1:
+ resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==}
- react-aria-components@1.17.0:
- resolution: {integrity: sha512-0EyisMgvsFJ2aML3crDYv2tW5vT2Ryf8PGzY/g63JjDdCbLshlwazhS8JNtPF1vkTkungJJ6sVJbKyX+YKSoFA==}
+ react-aria-components@1.19.0:
+ resolution: {integrity: sha512-2smSS5nqJ8cGYMQezuUXveZm7eMyHCqTN6mDpylQBYLYbdF5dxCCuW1DHn1VKLe1DybSfPvX/cZtJlDmvFfn8A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
- react-aria@3.48.0:
- resolution: {integrity: sha512-jQjd4rBEIMqecBaAKYJbVGK6EqIHLa5znVQ7jwFyK5vCyljoj6KhgtiahmcIPsG5vG5vEDLw+ba+bEWn6A2P4w==}
+ react-aria@3.50.0:
+ resolution: {integrity: sha512-S0Os6QZk33fzUAKu1QLT9afoUaCBt1ZNdoiq0n2YMVgKIdNIQS8zxiZ8O9hYE6QyDkHKjD6q39LQZ+qaSAIgjw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
- react-dom@19.2.5:
- resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
+ react-dom@19.2.7:
+ resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==}
peerDependencies:
- react: ^19.2.5
+ react: ^19.2.7
- react-error-boundary@6.1.1:
- resolution: {integrity: sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==}
+ react-error-boundary@6.1.2:
+ resolution: {integrity: sha512-3DpCr5HVdZ0caUjYE/kIHBEJN0mNP3ZCgf16c48uJ5TbWjorKVp+YG8W3XqlJ7vJAVNw6wNIImyPXmFydwmyng==}
peerDependencies:
react: ^18.0.0 || ^19.0.0
@@ -5830,8 +5391,8 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
- react-is@18.3.1:
- resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
+ react-is@17.0.2:
+ resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
react-redux@9.2.0:
resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
@@ -5845,8 +5406,8 @@ packages:
redux:
optional: true
- react-resizable-panels@4.10.0:
- resolution: {integrity: sha512-frjewRQt7TCv/vCH1pJfjZ7RxAhr5pKuqVQtVgzFq/vherxBFOWyC3xMbryx5Ti2wylViGUFc93Etg4rB3E0UA==}
+ react-resizable-panels@4.11.2:
+ resolution: {integrity: sha512-+kfFbDZ8mygc7g0vxOcDzCVGuwiIUOnILqPoUHo6/uP+Mmyx6HzZU+kj1aOPDlktXuobYbr6BtQekvJwHRX4Eg==}
peerDependencies:
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
@@ -5858,8 +5419,8 @@ packages:
react: ^16.8.0 || 17.x || 18.x
react-dom: ^16.8.0 || 17.x || 18.x
- react-stately@3.46.0:
- resolution: {integrity: sha512-OdxhWvHgs2L4OJGIs7hnuTr5WjjMM6enhNEAMRqiekhF8+ITvA2LRwNftOZwcogaoCslGYq5S2VQTQwnm0GbCA==}
+ react-stately@3.48.0:
+ resolution: {integrity: sha512-ImicSAG+lTotAe5izcs1fz49Zk48w7pDusqYg04WaPhCoej8BJ24soMu3iLXIrsi273s4P1gZrYGrqReMfgEEA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
@@ -5878,8 +5439,8 @@ packages:
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
- react@19.2.5:
- resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==}
+ react@19.2.7:
+ resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==}
engines: {node: '>=0.10.0'}
read-cache@1.0.0:
@@ -5896,6 +5457,14 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
+ readdirp@5.0.0:
+ resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
+ engines: {node: '>= 20.19.0'}
+
+ redent@3.0.0:
+ resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
+ engines: {node: '>=8'}
+
redis@5.12.1:
resolution: {integrity: sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g==}
engines: {node: '>= 18.19.0'}
@@ -5917,9 +5486,6 @@ packages:
request-ip@3.3.0:
resolution: {integrity: sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA==}
- request-progress@3.0.0:
- resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==}
-
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
@@ -5943,9 +5509,6 @@ packages:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'}
- resolve-pkg-maps@1.0.0:
- resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
-
resolve@1.22.10:
resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
engines: {node: '>= 0.4'}
@@ -5956,20 +5519,21 @@ packages:
engines: {node: '>= 0.4'}
hasBin: true
- restore-cursor@3.1.0:
- resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
- engines: {node: '>=8'}
-
retry@0.12.0:
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
engines: {node: '>= 4'}
+ rettime@0.11.11:
+ resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==}
+
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
- rfdc@1.4.1:
- resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
+ rolldown@1.0.0-rc.18:
+ resolution: {integrity: sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
rollup-plugin-copy@3.5.0:
resolution: {integrity: sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==}
@@ -6014,29 +5578,26 @@ packages:
rollup-pluginutils@2.8.2:
resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==}
- rollup@4.60.1:
- resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==}
+ rollup@4.62.2:
+ resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
- rrdom@0.1.7:
- resolution: {integrity: sha512-ZLd8f14z9pUy2Hk9y636cNv5Y2BMnNEY99wxzW9tD2BLDfe1xFxtLjB4q/xCBYo6HRe0wofzKzjm4JojmpBfFw==}
+ rrdom@2.0.1:
+ resolution: {integrity: sha512-z8GdQWFmVfa8GpcGkAqfBXseb89z0ciTPSBkID0qr0i6KHCye1MdQ4VoXFf0Ejp3ELe/c7AObu4zsbSbMT+hiw==}
- rrweb-player@1.0.0-alpha.4:
- resolution: {integrity: sha512-Wlmn9GZ5Fdqa37vd3TzsYdLl/JWEvXNUrLCrYpnOwEgmY409HwVIvvA5aIo7k582LoKgdRCsB87N+f0oWAR0Kg==}
+ rrweb-player@2.0.0-alpha.20:
+ resolution: {integrity: sha512-3ZCv1ksUxuIOn3Vn/eWrwWs9Xy+4KVjISD+q26ZLfisZ3hZ0CPgYG3iC22pmmycIeMS2svOfvf7gPh7jExwpUA==}
- rrweb-snapshot@2.0.0-alpha.4:
- resolution: {integrity: sha512-KQ2OtPpXO5jLYqg1OnXS/Hf+EzqnZyP5A+XPqBCjYpj3XIje/Od4gdUwjbFo3cVuWq5Cw5Y1d3/xwgIS7/XpQQ==}
+ rrweb-snapshot@2.0.1:
+ resolution: {integrity: sha512-H8TA29ct2tpYtr5oaB0N+ksRY9bhEErfAYcKbce4+w8lqEUQChpKZd41HpBJLkp04b5QUfWUg4v6r9Okf8r7vA==}
- rrweb@2.0.0-alpha.4:
- resolution: {integrity: sha512-wEHUILbxDPcNwkM3m4qgPgXAiBJyqCbbOHyVoNEVBJzHszWEFYyTbrZqUdeb1EfmTRC2PsumCIkVcomJ/xcOzA==}
+ rrweb@2.0.1:
+ resolution: {integrity: sha512-MQYzOtI7aZft9ffDMT6rmRSAj6hO4W6T7iA4eB9Mtcuiv3M7DbnnxDMrcPW0sQXepgYN46YhfSKe75d/mlUHrQ==}
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
- rxjs@7.8.2:
- resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
-
safe-array-concat@1.1.3:
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
engines: {node: '>=0.4'}
@@ -6061,6 +5622,10 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+ saxes@6.0.0:
+ resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
+ engines: {node: '>=v12.22.7'}
+
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
@@ -6072,8 +5637,8 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
- semver@7.7.4:
- resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
+ semver@7.8.5:
+ resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
engines: {node: '>=10'}
hasBin: true
@@ -6090,6 +5655,8 @@ packages:
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
+ set-cookie-parser@3.1.0:
+ resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==}
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
@@ -6146,6 +5713,9 @@ packages:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'}
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
@@ -6164,14 +5734,6 @@ packages:
resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==}
engines: {node: '>=14.16'}
- slice-ansi@3.0.0:
- resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==}
- engines: {node: '>=8'}
-
- slice-ansi@4.0.0:
- resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
- engines: {node: '>=10'}
-
smob@1.6.1:
resolution: {integrity: sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==}
engines: {node: '>=20.0.0'}
@@ -6183,9 +5745,6 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
- source-map-support@0.5.13:
- resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==}
-
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
@@ -6213,40 +5772,37 @@ packages:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
- sprintf-js@1.0.3:
- resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
-
sqlstring@2.3.3:
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
engines: {node: '>= 0.6'}
- sshpk@1.18.0:
- resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==}
- engines: {node: '>=0.10.0'}
- hasBin: true
-
stable@0.1.8:
resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==}
deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility'
- stack-utils@2.0.6:
- resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
- engines: {node: '>=10'}
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+ statuses@2.0.2:
+ resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
+ engines: {node: '>= 0.8'}
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
+ std-env@4.1.0:
+ resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
+
stop-iteration-iterator@1.1.0:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
+ strict-event-emitter@0.5.1:
+ resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==}
+
string-hash@1.1.3:
resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==}
- string-length@4.0.2:
- resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==}
- engines: {node: '>=10'}
-
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@@ -6286,16 +5842,8 @@ packages:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
- strip-bom@4.0.0:
- resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==}
- engines: {node: '>=8'}
-
- strip-final-newline@2.0.0:
- resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
- engines: {node: '>=6'}
-
- strip-json-comments@3.1.1:
- resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ strip-indent@3.0.0:
+ resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'}
style-inject@0.3.0:
@@ -6333,10 +5881,6 @@ packages:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
- supports-color@8.1.1:
- resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
- engines: {node: '>=10'}
-
supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
@@ -6354,15 +5898,8 @@ packages:
engines: {node: '>=14.0.0'}
hasBin: true
- synckit@0.11.12:
- resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
- engines: {node: ^14.18.0 || >=16.0.0}
-
- systeminformation@5.31.5:
- resolution: {integrity: sha512-5SyLdip4/3alxD4Kh+63bUQTJmu7YMfYQTC+koZy7X73HgNqZSD2P4wOZQWtUncvPvcEmnfIjCoygN4MRoEejQ==}
- engines: {node: '>=8.0.0'}
- os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
- hasBin: true
+ symbol-tree@3.2.4:
+ resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
tagged-tag@1.0.0:
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
@@ -6384,8 +5921,8 @@ packages:
tailwindcss@4.1.18:
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
- tar@7.5.13:
- resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==}
+ tar@7.5.16:
+ resolution: {integrity: sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==}
engines: {node: '>=18'}
terser@5.46.1:
@@ -6399,6 +5936,8 @@ packages:
thenby@1.4.0:
resolution: {integrity: sha512-D+BkEl5MpeTbQ1Rz0io/TLZmwmYUAgvUpS1r2eKUvPicYOnpqzjZE6bVa7rSEYv189zkSM+CVc+hG5X7nVe9LQ==}
+ thenby@1.4.1:
+ resolution: {integrity: sha512-D5a/bO0KdalOE3q8MlrRmSxjbKZHT3MQmXkJP+r97Vw8MMwOZKOwUSEyTtK7eSMj2y0kyAjpYMRMZmmLw1FtNQ==}
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
@@ -6407,12 +5946,6 @@ packages:
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
- throttleit@1.0.1:
- resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==}
-
- through@2.3.8:
- resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
-
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@@ -6420,11 +5953,14 @@ packages:
resolution: {integrity: sha512-xDHxKKS1FdF0Tv2P+QT7IeSEg74K/8cEDzbv3Tv6UyHHUgBOjOiQiBp818MGj66dhurQus/IBcoAbwIKtSGc6Q==}
engines: {node: '>=14'}
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
- tinyexec@1.1.1:
- resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==}
+ tinyexec@1.2.4:
+ resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==}
engines: {node: '>=18'}
tinyglobby@0.2.14:
@@ -6435,20 +5971,21 @@ packages:
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
engines: {node: '>=12.0.0'}
- tldts-core@6.1.86:
- resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
- tldts@6.1.86:
- resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
+ tinyrainbow@3.1.0:
+ resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
+ engines: {node: '>=14.0.0'}
+
+ tldts-core@7.0.30:
+ resolution: {integrity: sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==}
+
+ tldts@7.0.30:
+ resolution: {integrity: sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==}
hasBin: true
- tmp@0.2.5:
- resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
- engines: {node: '>=14.14'}
-
- tmpl@1.0.5:
- resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
-
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -6457,10 +5994,14 @@ packages:
resolution: {integrity: sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==}
hasBin: true
- tough-cookie@5.1.2:
- resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
+ tough-cookie@6.0.1:
+ resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
engines: {node: '>=16'}
+ tr46@6.0.0:
+ resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
+ engines: {node: '>=20'}
+
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
@@ -6468,33 +6009,6 @@ packages:
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
- ts-jest@29.4.9:
- resolution: {integrity: sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==}
- engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0}
- hasBin: true
- peerDependencies:
- '@babel/core': '>=7.0.0-beta.0 <8'
- '@jest/transform': ^29.0.0 || ^30.0.0
- '@jest/types': ^29.0.0 || ^30.0.0
- babel-jest: ^29.0.0 || ^30.0.0
- esbuild: '*'
- jest: ^29.0.0 || ^30.0.0
- jest-util: ^29.0.0 || ^30.0.0
- typescript: '>=4.3 <7'
- peerDependenciesMeta:
- '@babel/core':
- optional: true
- '@jest/transform':
- optional: true
- '@jest/types':
- optional: true
- babel-jest:
- optional: true
- esbuild:
- optional: true
- jest-util:
- optional: true
-
ts-morph@28.0.0:
resolution: {integrity: sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g==}
@@ -6512,9 +6026,6 @@ packages:
'@swc/wasm':
optional: true
- tslib@1.14.1:
- resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
-
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -6537,33 +6048,11 @@ packages:
typescript:
optional: true
- tsx@4.21.0:
- resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
+ tsx@4.22.4:
+ resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==}
engines: {node: '>=18.0.0'}
hasBin: true
- tunnel-agent@0.6.0:
- resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
-
- tweetnacl@0.14.5:
- resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
-
- type-detect@4.0.8:
- resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
- engines: {node: '>=4'}
-
- type-fest@0.21.3:
- resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
- engines: {node: '>=10'}
-
- type-fest@0.8.1:
- resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
- engines: {node: '>=8'}
-
- type-fest@4.41.0:
- resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
- engines: {node: '>=16'}
-
type-fest@5.6.0:
resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==}
engines: {node: '>=20'}
@@ -6592,24 +6081,23 @@ packages:
ua-is-frozen@0.1.2:
resolution: {integrity: sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==}
- ua-parser-js@2.0.9:
- resolution: {integrity: sha512-OsqGhxyo/wGdLSXMSJxuMGN6H4gDnKz6Fb3IBm4bxZFMnyy0sdf6MN96Ie8tC6z/btdO+Bsy8guxlvLdwT076w==}
+ ua-parser-js@2.0.10:
+ resolution: {integrity: sha512-t+3Ktbq0Ies2vaSezfOaWiolH4OigQIO1dk+1xDpOydB1COVPocVYOrEV5rqZ0kFY9XYG1v9LutCyMgYBpABcw==}
hasBin: true
ufo@1.6.1:
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
- uglify-js@3.19.3:
- resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
- engines: {node: '>=0.8.0'}
- hasBin: true
-
unbox-primitive@1.1.0:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
- undici-types@7.19.2:
- resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
+ undici-types@8.3.0:
+ resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
+
+ undici@7.25.0:
+ resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==}
+ engines: {node: '>=20.18.1'}
unicorn-magic@0.3.0:
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
@@ -6619,22 +6107,8 @@ packages:
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
engines: {node: '>= 4.0.0'}
- universalify@2.0.1:
- resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
- engines: {node: '>= 10.0.0'}
-
- unrs-resolver@1.11.1:
- resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==}
-
- untildify@4.0.0:
- resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
- engines: {node: '>=8'}
-
- update-browserslist-db@1.1.3:
- resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
- hasBin: true
- peerDependencies:
- browserslist: '>= 4.21.0'
+ until-async@3.0.2:
+ resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==}
update-browserslist-db@1.2.3:
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
@@ -6642,8 +6116,8 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
- use-intl@4.9.1:
- resolution: {integrity: sha512-iGVV/xFYlhe3btafRlL8RPLD2Jsuet4yqn9DR6LWWbMhULsJnXgLonDkzDmsAIBIwFtk02oJuX/Ox2vwHKF+UQ==}
+ use-intl@4.13.0:
+ resolution: {integrity: sha512-fAFDrWaASxlhXOipcOyb5VDD+YONqj6+8O8EcG/J7RBoOUF3A8YahRWLN+mBxYMrlMQB8N6Voqk5X+YC+HSL0A==}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
@@ -6661,15 +6135,13 @@ packages:
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
+ uuid@14.0.1:
+ resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==}
hasBin: true
v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
- v8-to-istanbul@9.3.0:
- resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
- engines: {node: '>=10.12.0'}
-
valibot@1.2.0:
resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==}
peerDependencies:
@@ -6685,17 +6157,110 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
- verror@1.10.0:
- resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
- engines: {'0': node >=0.6.0}
+ vite@8.0.11:
+ resolution: {integrity: sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.1.18
+ esbuild: ^0.27.0 || ^0.28.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
- walker@1.0.8:
- resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
+ vitest@4.1.9:
+ resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==}
+ engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@opentelemetry/api': ^1.9.0
+ '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
+ '@vitest/browser-playwright': 4.1.9
+ '@vitest/browser-preview': 4.1.9
+ '@vitest/browser-webdriverio': 4.1.9
+ '@vitest/coverage-istanbul': 4.1.9
+ '@vitest/coverage-v8': 4.1.9
+ '@vitest/ui': 4.1.9
+ happy-dom: '*'
+ jsdom: '*'
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@opentelemetry/api':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser-playwright':
+ optional: true
+ '@vitest/browser-preview':
+ optional: true
+ '@vitest/browser-webdriverio':
+ optional: true
+ '@vitest/coverage-istanbul':
+ optional: true
+ '@vitest/coverage-v8':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
+ w3c-xmlserializer@5.0.0:
+ resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
+ engines: {node: '>=18'}
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
+ webidl-conversions@8.0.1:
+ resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
+ engines: {node: '>=20'}
+
+ whatwg-mimetype@5.0.0:
+ resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
+ engines: {node: '>=20'}
+
+ whatwg-url@16.0.1:
+ resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'}
@@ -6724,12 +6289,10 @@ packages:
engines: {node: '>= 8'}
hasBin: true
- wordwrap@1.0.0:
- resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
-
- wrap-ansi@6.2.0:
- resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
+ hasBin: true
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
@@ -6742,9 +6305,12 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
- write-file-atomic@5.0.1:
- resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+ xml-name-validator@5.0.0:
+ resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+ engines: {node: '>=18'}
+
+ xmlchars@2.2.0:
+ resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
@@ -6789,25 +6355,18 @@ packages:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
- yauzl@2.10.0:
- resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
-
yn@3.1.1:
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
engines: {node: '>=6'}
- yocto-queue@0.1.0:
- resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
- engines: {node: '>=10'}
-
zeptomatch@2.1.0:
resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==}
- zod@4.3.6:
- resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
+ zod@4.4.3:
+ resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
- zustand@5.0.12:
- resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==}
+ zustand@5.0.14:
+ resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=18.0.0'
@@ -6826,11 +6385,33 @@ packages:
snapshots:
+ '@adobe/css-tools@4.4.4': {}
+
'@ampproject/remapping@2.3.0':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.30
+ '@asamuzakjp/css-color@5.1.11':
+ dependencies:
+ '@asamuzakjp/generational-cache': 1.0.1
+ '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@asamuzakjp/dom-selector@7.1.1':
+ dependencies:
+ '@asamuzakjp/generational-cache': 1.0.1
+ '@asamuzakjp/nwsapi': 2.3.9
+ bidi-js: 1.0.3
+ css-tree: 3.2.1
+ is-potential-custom-element-name: 1.0.1
+
+ '@asamuzakjp/generational-cache@1.0.1': {}
+
+ '@asamuzakjp/nwsapi@2.3.9': {}
+
'@babel/code-frame@7.27.1':
dependencies:
'@babel/helper-validator-identifier': 7.27.1
@@ -6842,10 +6423,18 @@ snapshots:
'@babel/helper-validator-identifier': 7.28.5
js-tokens: 4.0.0
picocolors: 1.1.1
+ optional: true
+
+ '@babel/code-frame@7.29.7':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.29.7
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
'@babel/compat-data@7.28.0': {}
- '@babel/compat-data@7.29.0': {}
+ '@babel/compat-data@7.29.7':
+ optional: true
'@babel/core@7.28.3':
dependencies:
@@ -6860,7 +6449,7 @@ snapshots:
'@babel/traverse': 7.28.3
'@babel/types': 7.28.2
convert-source-map: 2.0.0
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.4.3
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -6869,23 +6458,24 @@ snapshots:
'@babel/core@7.29.0':
dependencies:
- '@babel/code-frame': 7.29.0
- '@babel/generator': 7.29.1
- '@babel/helper-compilation-targets': 7.28.6
- '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
- '@babel/helpers': 7.29.2
- '@babel/parser': 7.29.2
- '@babel/template': 7.28.6
- '@babel/traverse': 7.29.0
- '@babel/types': 7.29.0
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.7
+ '@babel/helper-compilation-targets': 7.29.7
+ '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.0)
+ '@babel/helpers': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/template': 7.29.7
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.4.3
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
transitivePeerDependencies:
- supports-color
+ optional: true
'@babel/generator@7.28.3':
dependencies:
@@ -6895,32 +6485,37 @@ snapshots:
'@jridgewell/trace-mapping': 0.3.30
jsesc: 3.1.0
- '@babel/generator@7.29.1':
+ '@babel/generator@7.29.7':
dependencies:
- '@babel/parser': 7.29.2
- '@babel/types': 7.29.0
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
+ optional: true
'@babel/helper-compilation-targets@7.27.2':
dependencies:
'@babel/compat-data': 7.28.0
'@babel/helper-validator-option': 7.27.1
- browserslist: 4.25.2
+ browserslist: 4.28.4
lru-cache: 5.1.1
semver: 6.3.1
- '@babel/helper-compilation-targets@7.28.6':
+ '@babel/helper-compilation-targets@7.29.7':
dependencies:
- '@babel/compat-data': 7.29.0
- '@babel/helper-validator-option': 7.27.1
- browserslist: 4.28.2
+ '@babel/compat-data': 7.29.7
+ '@babel/helper-validator-option': 7.29.7
+ browserslist: 4.28.4
lru-cache: 5.1.1
semver: 6.3.1
+ optional: true
'@babel/helper-globals@7.28.0': {}
+ '@babel/helper-globals@7.29.7':
+ optional: true
+
'@babel/helper-module-imports@7.27.1':
dependencies:
'@babel/traverse': 7.28.3
@@ -6928,12 +6523,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@babel/helper-module-imports@7.28.6':
+ '@babel/helper-module-imports@7.29.7':
dependencies:
- '@babel/traverse': 7.29.0
- '@babel/types': 7.29.0
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
transitivePeerDependencies:
- supports-color
+ optional: true
'@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3)':
dependencies:
@@ -6944,59 +6540,49 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
+ '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
- '@babel/helper-module-imports': 7.28.6
- '@babel/helper-validator-identifier': 7.28.5
- '@babel/traverse': 7.29.0
+ '@babel/helper-module-imports': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+ '@babel/traverse': 7.29.7
transitivePeerDependencies:
- supports-color
-
- '@babel/helper-plugin-utils@7.28.6': {}
+ optional: true
'@babel/helper-string-parser@7.27.1': {}
+ '@babel/helper-string-parser@7.29.7':
+ optional: true
+
'@babel/helper-validator-identifier@7.27.1': {}
- '@babel/helper-validator-identifier@7.28.5': {}
+ '@babel/helper-validator-identifier@7.28.5':
+ optional: true
+
+ '@babel/helper-validator-identifier@7.29.7': {}
'@babel/helper-validator-option@7.27.1': {}
+ '@babel/helper-validator-option@7.29.7':
+ optional: true
+
'@babel/helpers@7.28.3':
dependencies:
'@babel/template': 7.27.2
'@babel/types': 7.28.2
- '@babel/helpers@7.29.2':
+ '@babel/helpers@7.29.7':
dependencies:
- '@babel/template': 7.28.6
- '@babel/types': 7.29.0
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.7
+ optional: true
'@babel/parser@7.28.3':
dependencies:
'@babel/types': 7.28.2
- '@babel/parser@7.29.2':
- dependencies:
- '@babel/types': 7.29.0
-
- '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)':
- dependencies:
- '@babel/core': 7.29.0
- '@babel/helper-plugin-utils': 7.28.6
-
- '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)':
- dependencies:
- '@babel/core': 7.29.0
- '@babel/helper-plugin-utils': 7.28.6
-
- '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)':
- dependencies:
- '@babel/core': 7.29.0
- '@babel/helper-plugin-utils': 7.28.6
-
- '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)':
+ '@babel/parser@7.29.7':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
@@ -7065,20 +6651,25 @@ snapshots:
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
+ '@babel/types': 7.29.7
+ optional: true
'@babel/runtime@7.29.2': {}
+ '@babel/runtime@7.29.7': {}
+
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/parser': 7.28.3
'@babel/types': 7.28.2
- '@babel/template@7.28.6':
+ '@babel/template@7.29.7':
dependencies:
- '@babel/code-frame': 7.29.0
- '@babel/parser': 7.29.2
- '@babel/types': 7.29.0
+ '@babel/code-frame': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+ optional: true
'@babel/traverse@7.28.3':
dependencies:
@@ -7088,69 +6679,73 @@ snapshots:
'@babel/parser': 7.28.3
'@babel/template': 7.27.2
'@babel/types': 7.28.2
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.4.3
transitivePeerDependencies:
- supports-color
- '@babel/traverse@7.29.0':
+ '@babel/traverse@7.29.7':
dependencies:
- '@babel/code-frame': 7.29.0
- '@babel/generator': 7.29.1
- '@babel/helper-globals': 7.28.0
- '@babel/parser': 7.29.2
- '@babel/template': 7.28.6
- '@babel/types': 7.29.0
- debug: 4.4.3(supports-color@8.1.1)
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.7
+ '@babel/helper-globals': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.7
+ debug: 4.4.3
transitivePeerDependencies:
- supports-color
+ optional: true
'@babel/types@7.28.2':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
- '@babel/types@7.29.0':
+ '@babel/types@7.29.7':
dependencies:
- '@babel/helper-string-parser': 7.27.1
- '@babel/helper-validator-identifier': 7.28.5
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+ optional: true
- '@bcoe/v8-coverage@0.2.3': {}
-
- '@biomejs/biome@2.4.12':
+ '@biomejs/biome@2.5.1':
optionalDependencies:
- '@biomejs/cli-darwin-arm64': 2.4.12
- '@biomejs/cli-darwin-x64': 2.4.12
- '@biomejs/cli-linux-arm64': 2.4.12
- '@biomejs/cli-linux-arm64-musl': 2.4.12
- '@biomejs/cli-linux-x64': 2.4.12
- '@biomejs/cli-linux-x64-musl': 2.4.12
- '@biomejs/cli-win32-arm64': 2.4.12
- '@biomejs/cli-win32-x64': 2.4.12
+ '@biomejs/cli-darwin-arm64': 2.5.1
+ '@biomejs/cli-darwin-x64': 2.5.1
+ '@biomejs/cli-linux-arm64': 2.5.1
+ '@biomejs/cli-linux-arm64-musl': 2.5.1
+ '@biomejs/cli-linux-x64': 2.5.1
+ '@biomejs/cli-linux-x64-musl': 2.5.1
+ '@biomejs/cli-win32-arm64': 2.5.1
+ '@biomejs/cli-win32-x64': 2.5.1
- '@biomejs/cli-darwin-arm64@2.4.12':
+ '@biomejs/cli-darwin-arm64@2.5.1':
optional: true
- '@biomejs/cli-darwin-x64@2.4.12':
+ '@biomejs/cli-darwin-x64@2.5.1':
optional: true
- '@biomejs/cli-linux-arm64-musl@2.4.12':
+ '@biomejs/cli-linux-arm64-musl@2.5.1':
optional: true
- '@biomejs/cli-linux-arm64@2.4.12':
+ '@biomejs/cli-linux-arm64@2.5.1':
optional: true
- '@biomejs/cli-linux-x64-musl@2.4.12':
+ '@biomejs/cli-linux-x64-musl@2.5.1':
optional: true
- '@biomejs/cli-linux-x64@2.4.12':
+ '@biomejs/cli-linux-x64@2.5.1':
optional: true
- '@biomejs/cli-win32-arm64@2.4.12':
+ '@biomejs/cli-win32-arm64@2.5.1':
optional: true
- '@biomejs/cli-win32-x64@2.4.12':
+ '@biomejs/cli-win32-x64@2.5.1':
optional: true
+ '@bramus/specificity@2.4.2':
+ dependencies:
+ css-tree: 3.2.1
+
'@clack/core@0.5.0':
dependencies:
picocolors: 1.1.1
@@ -7162,11 +6757,11 @@ snapshots:
picocolors: 1.1.1
sisteransi: 1.0.5
- '@clickhouse/client-common@1.18.2': {}
+ '@clickhouse/client-common@1.22.0': {}
- '@clickhouse/client@1.18.2':
+ '@clickhouse/client@1.22.0':
dependencies:
- '@clickhouse/client-common': 1.18.2
+ '@clickhouse/client-common': 1.22.0
'@cspotcode/source-map-support@0.8.1':
dependencies:
@@ -7184,6 +6779,11 @@ snapshots:
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
+ '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
'@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
dependencies:
'@csstools/color-helpers': 6.0.2
@@ -7191,10 +6791,21 @@ snapshots:
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
+ '@csstools/css-color-parser@4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/color-helpers': 6.0.2
+ '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
'@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
dependencies:
'@csstools/css-tokenizer': 4.0.0
+ '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)':
+ optionalDependencies:
+ css-tree: 3.2.1
+
'@csstools/css-tokenizer@4.0.0': {}
'@csstools/media-query-list-parser@5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
@@ -7202,323 +6813,309 @@ snapshots:
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- '@csstools/postcss-alpha-function@2.0.4(postcss@8.5.10)':
+ '@csstools/postcss-alpha-function@2.0.6(postcss@8.5.15)':
dependencies:
- '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10)
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
- '@csstools/postcss-cascade-layers@6.0.0(postcss@8.5.10)':
+ '@csstools/postcss-cascade-layers@6.0.0(postcss@8.5.15)':
dependencies:
- '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1)
- postcss: 8.5.10
- postcss-selector-parser: 7.1.1
+ '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.4)
+ postcss: 8.5.15
+ postcss-selector-parser: 7.1.4
- '@csstools/postcss-color-function-display-p3-linear@2.0.3(postcss@8.5.10)':
+ '@csstools/postcss-color-function-display-p3-linear@2.0.5(postcss@8.5.15)':
dependencies:
- '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10)
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
- '@csstools/postcss-color-function@5.0.3(postcss@8.5.10)':
+ '@csstools/postcss-color-function@5.0.5(postcss@8.5.15)':
dependencies:
- '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10)
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
- '@csstools/postcss-color-mix-function@4.0.3(postcss@8.5.10)':
+ '@csstools/postcss-color-mix-function@4.0.5(postcss@8.5.15)':
dependencies:
- '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10)
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
- '@csstools/postcss-color-mix-variadic-function-arguments@2.0.3(postcss@8.5.10)':
+ '@csstools/postcss-color-mix-variadic-function-arguments@2.0.5(postcss@8.5.15)':
dependencies:
- '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10)
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
- '@csstools/postcss-content-alt-text@3.0.0(postcss@8.5.10)':
+ '@csstools/postcss-container-rule-prelude-list@1.0.1(postcss@8.5.15)':
dependencies:
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10)
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ postcss: 8.5.15
- '@csstools/postcss-contrast-color-function@3.0.3(postcss@8.5.10)':
+ '@csstools/postcss-content-alt-text@3.0.1(postcss@8.5.15)':
dependencies:
- '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10)
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
- '@csstools/postcss-exponential-functions@3.0.2(postcss@8.5.10)':
+ '@csstools/postcss-contrast-color-function@3.0.5(postcss@8.5.15)':
dependencies:
- '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- postcss: 8.5.10
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
- '@csstools/postcss-font-format-keywords@5.0.0(postcss@8.5.10)':
+ '@csstools/postcss-exponential-functions@3.0.3(postcss@8.5.15)':
dependencies:
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+ postcss: 8.5.15
+
+ '@csstools/postcss-font-format-keywords@5.0.0(postcss@8.5.15)':
+ dependencies:
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- '@csstools/postcss-font-width-property@1.0.0(postcss@8.5.10)':
+ '@csstools/postcss-font-width-property@1.0.0(postcss@8.5.15)':
dependencies:
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
- '@csstools/postcss-gamut-mapping@3.0.3(postcss@8.5.10)':
+ '@csstools/postcss-gamut-mapping@3.0.5(postcss@8.5.15)':
dependencies:
- '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- postcss: 8.5.10
+ postcss: 8.5.15
- '@csstools/postcss-gradients-interpolation-method@6.0.3(postcss@8.5.10)':
+ '@csstools/postcss-gradients-interpolation-method@6.0.5(postcss@8.5.15)':
dependencies:
- '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10)
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
- '@csstools/postcss-hwb-function@5.0.3(postcss@8.5.10)':
+ '@csstools/postcss-hwb-function@5.0.5(postcss@8.5.15)':
dependencies:
- '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10)
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
- '@csstools/postcss-ic-unit@5.0.0(postcss@8.5.10)':
+ '@csstools/postcss-ic-unit@5.0.1(postcss@8.5.15)':
dependencies:
- '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10)
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- '@csstools/postcss-initial@3.0.0(postcss@8.5.10)':
- dependencies:
- postcss: 8.5.10
-
- '@csstools/postcss-is-pseudo-class@6.0.0(postcss@8.5.10)':
- dependencies:
- '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1)
- postcss: 8.5.10
- postcss-selector-parser: 7.1.1
-
- '@csstools/postcss-light-dark-function@3.0.0(postcss@8.5.10)':
+ '@csstools/postcss-image-function@1.0.0(postcss@8.5.15)':
dependencies:
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10)
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
- '@csstools/postcss-logical-float-and-clear@4.0.0(postcss@8.5.10)':
+ '@csstools/postcss-initial@3.0.0(postcss@8.5.15)':
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- '@csstools/postcss-logical-overflow@3.0.0(postcss@8.5.10)':
+ '@csstools/postcss-is-pseudo-class@6.0.0(postcss@8.5.15)':
dependencies:
- postcss: 8.5.10
+ '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.4)
+ postcss: 8.5.15
+ postcss-selector-parser: 7.1.4
- '@csstools/postcss-logical-overscroll-behavior@3.0.0(postcss@8.5.10)':
+ '@csstools/postcss-light-dark-function@3.0.1(postcss@8.5.15)':
dependencies:
- postcss: 8.5.10
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
- '@csstools/postcss-logical-resize@4.0.0(postcss@8.5.10)':
+ '@csstools/postcss-logical-float-and-clear@4.0.0(postcss@8.5.15)':
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
+
+ '@csstools/postcss-logical-overflow@3.0.0(postcss@8.5.15)':
+ dependencies:
+ postcss: 8.5.15
+
+ '@csstools/postcss-logical-overscroll-behavior@3.0.0(postcss@8.5.15)':
+ dependencies:
+ postcss: 8.5.15
+
+ '@csstools/postcss-logical-resize@4.0.0(postcss@8.5.15)':
+ dependencies:
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- '@csstools/postcss-logical-viewport-units@4.0.0(postcss@8.5.10)':
+ '@csstools/postcss-logical-viewport-units@4.0.0(postcss@8.5.15)':
dependencies:
'@csstools/css-tokenizer': 4.0.0
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
- '@csstools/postcss-media-minmax@3.0.2(postcss@8.5.10)':
+ '@csstools/postcss-media-minmax@3.0.3(postcss@8.5.15)':
dependencies:
- '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
'@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
- postcss: 8.5.10
+ postcss: 8.5.15
- '@csstools/postcss-media-queries-aspect-ratio-number-values@4.0.0(postcss@8.5.10)':
+ '@csstools/postcss-media-queries-aspect-ratio-number-values@4.0.0(postcss@8.5.15)':
dependencies:
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
'@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
- postcss: 8.5.10
+ postcss: 8.5.15
- '@csstools/postcss-mixins@1.0.0(postcss@8.5.10)':
+ '@csstools/postcss-mixins@1.0.0(postcss@8.5.15)':
dependencies:
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- postcss: 8.5.10
+ postcss: 8.5.15
- '@csstools/postcss-nested-calc@5.0.0(postcss@8.5.10)':
+ '@csstools/postcss-nested-calc@5.0.0(postcss@8.5.15)':
dependencies:
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- '@csstools/postcss-normalize-display-values@5.0.1(postcss@8.5.10)':
+ '@csstools/postcss-normalize-display-values@5.0.1(postcss@8.5.15)':
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- '@csstools/postcss-oklab-function@5.0.3(postcss@8.5.10)':
+ '@csstools/postcss-oklab-function@5.0.5(postcss@8.5.15)':
dependencies:
- '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10)
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
- '@csstools/postcss-position-area-property@2.0.0(postcss@8.5.10)':
+ '@csstools/postcss-position-area-property@2.0.0(postcss@8.5.15)':
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- '@csstools/postcss-progressive-custom-properties@5.0.0(postcss@8.5.10)':
+ '@csstools/postcss-progressive-custom-properties@5.1.0(postcss@8.5.15)':
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- '@csstools/postcss-property-rule-prelude-list@2.0.0(postcss@8.5.10)':
+ '@csstools/postcss-property-rule-prelude-list@2.0.0(postcss@8.5.15)':
dependencies:
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- postcss: 8.5.10
+ postcss: 8.5.15
- '@csstools/postcss-random-function@3.0.2(postcss@8.5.10)':
+ '@csstools/postcss-random-function@3.0.3(postcss@8.5.15)':
dependencies:
- '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- postcss: 8.5.10
+ postcss: 8.5.15
- '@csstools/postcss-relative-color-syntax@4.0.3(postcss@8.5.10)':
+ '@csstools/postcss-relative-color-syntax@4.0.5(postcss@8.5.15)':
dependencies:
- '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10)
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
- '@csstools/postcss-scope-pseudo-class@5.0.0(postcss@8.5.10)':
+ '@csstools/postcss-scope-pseudo-class@5.0.0(postcss@8.5.15)':
dependencies:
- postcss: 8.5.10
- postcss-selector-parser: 7.1.1
+ postcss: 8.5.15
+ postcss-selector-parser: 7.1.4
- '@csstools/postcss-sign-functions@2.0.2(postcss@8.5.10)':
+ '@csstools/postcss-sign-functions@2.0.3(postcss@8.5.15)':
dependencies:
- '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- postcss: 8.5.10
+ postcss: 8.5.15
- '@csstools/postcss-stepped-value-functions@5.0.2(postcss@8.5.10)':
+ '@csstools/postcss-stepped-value-functions@5.0.3(postcss@8.5.15)':
dependencies:
- '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- postcss: 8.5.10
+ postcss: 8.5.15
- '@csstools/postcss-syntax-descriptor-syntax-production@2.0.0(postcss@8.5.10)':
+ '@csstools/postcss-syntax-descriptor-syntax-production@2.0.0(postcss@8.5.15)':
dependencies:
'@csstools/css-tokenizer': 4.0.0
- postcss: 8.5.10
+ postcss: 8.5.15
- '@csstools/postcss-system-ui-font-family@2.0.0(postcss@8.5.10)':
+ '@csstools/postcss-system-ui-font-family@2.0.0(postcss@8.5.15)':
dependencies:
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- postcss: 8.5.10
+ postcss: 8.5.15
- '@csstools/postcss-text-decoration-shorthand@5.0.3(postcss@8.5.10)':
+ '@csstools/postcss-text-decoration-shorthand@5.0.3(postcss@8.5.15)':
dependencies:
'@csstools/color-helpers': 6.0.2
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- '@csstools/postcss-trigonometric-functions@5.0.2(postcss@8.5.10)':
+ '@csstools/postcss-trigonometric-functions@5.0.3(postcss@8.5.15)':
dependencies:
- '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- postcss: 8.5.10
+ postcss: 8.5.15
- '@csstools/postcss-unset-value@5.0.0(postcss@8.5.10)':
+ '@csstools/postcss-unset-value@5.0.0(postcss@8.5.15)':
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- '@csstools/selector-resolve-nested@4.0.0(postcss-selector-parser@7.1.1)':
+ '@csstools/selector-resolve-nested@4.0.0(postcss-selector-parser@7.1.4)':
dependencies:
- postcss-selector-parser: 7.1.1
+ postcss-selector-parser: 7.1.4
- '@csstools/selector-specificity@6.0.0(postcss-selector-parser@7.1.1)':
+ '@csstools/selector-specificity@6.0.0(postcss-selector-parser@7.1.4)':
dependencies:
- postcss-selector-parser: 7.1.1
+ postcss-selector-parser: 7.1.4
- '@csstools/utilities@3.0.0(postcss@8.5.10)':
+ '@csstools/utilities@3.0.0(postcss@8.5.15)':
dependencies:
- postcss: 8.5.10
-
- '@cypress/request@3.0.10':
- dependencies:
- aws-sign2: 0.7.0
- aws4: 1.13.2
- caseless: 0.12.0
- combined-stream: 1.0.8
- extend: 3.0.2
- forever-agent: 0.6.1
- form-data: 4.0.5
- http-signature: 1.4.0
- is-typedarray: 1.0.0
- isstream: 0.1.2
- json-stringify-safe: 5.0.1
- mime-types: 2.1.35
- performance-now: 2.1.0
- qs: 6.14.2
- safe-buffer: 5.2.1
- tough-cookie: 5.1.2
- tunnel-agent: 0.6.0
- uuid: 8.3.2
-
- '@cypress/xvfb@1.2.4(supports-color@8.1.1)':
- dependencies:
- debug: 3.2.7(supports-color@8.1.1)
- lodash.once: 4.1.1
- transitivePeerDependencies:
- - supports-color
+ postcss: 8.5.15
'@date-fns/utc@2.1.1': {}
@@ -7706,7 +7303,7 @@ snapshots:
tslib: 2.8.1
optional: true
- '@emnapi/runtime@1.9.2':
+ '@emnapi/runtime@1.11.1':
dependencies:
tslib: 2.8.1
optional: true
@@ -7721,186 +7318,188 @@ snapshots:
'@esbuild/aix-ppc64@0.27.7':
optional: true
- '@esbuild/aix-ppc64@0.28.0':
+ '@esbuild/aix-ppc64@0.28.1':
optional: true
'@esbuild/android-arm64@0.27.7':
optional: true
- '@esbuild/android-arm64@0.28.0':
+ '@esbuild/android-arm64@0.28.1':
optional: true
'@esbuild/android-arm@0.27.7':
optional: true
- '@esbuild/android-arm@0.28.0':
+ '@esbuild/android-arm@0.28.1':
optional: true
'@esbuild/android-x64@0.27.7':
optional: true
- '@esbuild/android-x64@0.28.0':
+ '@esbuild/android-x64@0.28.1':
optional: true
'@esbuild/darwin-arm64@0.27.7':
optional: true
- '@esbuild/darwin-arm64@0.28.0':
+ '@esbuild/darwin-arm64@0.28.1':
optional: true
'@esbuild/darwin-x64@0.27.7':
optional: true
- '@esbuild/darwin-x64@0.28.0':
+ '@esbuild/darwin-x64@0.28.1':
optional: true
'@esbuild/freebsd-arm64@0.27.7':
optional: true
- '@esbuild/freebsd-arm64@0.28.0':
+ '@esbuild/freebsd-arm64@0.28.1':
optional: true
'@esbuild/freebsd-x64@0.27.7':
optional: true
- '@esbuild/freebsd-x64@0.28.0':
+ '@esbuild/freebsd-x64@0.28.1':
optional: true
'@esbuild/linux-arm64@0.27.7':
optional: true
- '@esbuild/linux-arm64@0.28.0':
+ '@esbuild/linux-arm64@0.28.1':
optional: true
'@esbuild/linux-arm@0.27.7':
optional: true
- '@esbuild/linux-arm@0.28.0':
+ '@esbuild/linux-arm@0.28.1':
optional: true
'@esbuild/linux-ia32@0.27.7':
optional: true
- '@esbuild/linux-ia32@0.28.0':
+ '@esbuild/linux-ia32@0.28.1':
optional: true
'@esbuild/linux-loong64@0.27.7':
optional: true
- '@esbuild/linux-loong64@0.28.0':
+ '@esbuild/linux-loong64@0.28.1':
optional: true
'@esbuild/linux-mips64el@0.27.7':
optional: true
- '@esbuild/linux-mips64el@0.28.0':
+ '@esbuild/linux-mips64el@0.28.1':
optional: true
'@esbuild/linux-ppc64@0.27.7':
optional: true
- '@esbuild/linux-ppc64@0.28.0':
+ '@esbuild/linux-ppc64@0.28.1':
optional: true
'@esbuild/linux-riscv64@0.27.7':
optional: true
- '@esbuild/linux-riscv64@0.28.0':
+ '@esbuild/linux-riscv64@0.28.1':
optional: true
'@esbuild/linux-s390x@0.27.7':
optional: true
- '@esbuild/linux-s390x@0.28.0':
+ '@esbuild/linux-s390x@0.28.1':
optional: true
'@esbuild/linux-x64@0.27.7':
optional: true
- '@esbuild/linux-x64@0.28.0':
+ '@esbuild/linux-x64@0.28.1':
optional: true
'@esbuild/netbsd-arm64@0.27.7':
optional: true
- '@esbuild/netbsd-arm64@0.28.0':
+ '@esbuild/netbsd-arm64@0.28.1':
optional: true
'@esbuild/netbsd-x64@0.27.7':
optional: true
- '@esbuild/netbsd-x64@0.28.0':
+ '@esbuild/netbsd-x64@0.28.1':
optional: true
'@esbuild/openbsd-arm64@0.27.7':
optional: true
- '@esbuild/openbsd-arm64@0.28.0':
+ '@esbuild/openbsd-arm64@0.28.1':
optional: true
'@esbuild/openbsd-x64@0.27.7':
optional: true
- '@esbuild/openbsd-x64@0.28.0':
+ '@esbuild/openbsd-x64@0.28.1':
optional: true
'@esbuild/openharmony-arm64@0.27.7':
optional: true
- '@esbuild/openharmony-arm64@0.28.0':
+ '@esbuild/openharmony-arm64@0.28.1':
optional: true
'@esbuild/sunos-x64@0.27.7':
optional: true
- '@esbuild/sunos-x64@0.28.0':
+ '@esbuild/sunos-x64@0.28.1':
optional: true
'@esbuild/win32-arm64@0.27.7':
optional: true
- '@esbuild/win32-arm64@0.28.0':
+ '@esbuild/win32-arm64@0.28.1':
optional: true
'@esbuild/win32-ia32@0.27.7':
optional: true
- '@esbuild/win32-ia32@0.28.0':
+ '@esbuild/win32-ia32@0.28.1':
optional: true
'@esbuild/win32-x64@0.27.7':
optional: true
- '@esbuild/win32-x64@0.28.0':
+ '@esbuild/win32-x64@0.28.1':
optional: true
- '@formatjs/fast-memoize@3.1.2': {}
+ '@exodus/bytes@1.15.0': {}
- '@formatjs/icu-messageformat-parser@3.5.4':
+ '@formatjs/fast-memoize@3.1.6': {}
+
+ '@formatjs/icu-messageformat-parser@3.5.11':
dependencies:
- '@formatjs/icu-skeleton-parser': 2.1.4
+ '@formatjs/icu-skeleton-parser': 2.1.10
- '@formatjs/icu-skeleton-parser@2.1.4': {}
+ '@formatjs/icu-skeleton-parser@2.1.10': {}
- '@formatjs/intl-localematcher@0.8.3':
+ '@formatjs/intl-localematcher@0.8.10':
dependencies:
- '@formatjs/fast-memoize': 3.1.2
+ '@formatjs/fast-memoize': 3.1.6
- '@hello-pangea/dnd@18.0.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
+ '@hello-pangea/dnd@18.0.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
'@babel/runtime': 7.29.2
css-box-model: 1.2.1
raf-schd: 4.0.3
- react: 19.2.5
- react-dom: 19.2.5(react@19.2.5)
- react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ react-redux: 9.2.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1)
redux: 5.0.1
transitivePeerDependencies:
- '@types/react'
- '@hono/node-server@1.19.11(hono@4.12.9)':
+ '@hono/node-server@1.19.11(hono@4.12.21)':
dependencies:
- hono: 4.12.9
+ hono: 4.12.21
'@img/colour@1.1.0':
optional: true
@@ -7987,7 +7586,7 @@ snapshots:
'@img/sharp-wasm32@0.34.5':
dependencies:
- '@emnapi/runtime': 1.9.2
+ '@emnapi/runtime': 1.11.1
optional: true
'@img/sharp-win32-arm64@0.34.5':
@@ -7999,21 +7598,48 @@ snapshots:
'@img/sharp-win32-x64@0.34.5':
optional: true
+ '@inquirer/ansi@2.0.5': {}
+
+ '@inquirer/confirm@6.0.12(@types/node@26.0.1)':
+ dependencies:
+ '@inquirer/core': 11.1.9(@types/node@26.0.1)
+ '@inquirer/type': 4.0.5(@types/node@26.0.1)
+ optionalDependencies:
+ '@types/node': 26.0.1
+
+ '@inquirer/core@11.1.9(@types/node@26.0.1)':
+ dependencies:
+ '@inquirer/ansi': 2.0.5
+ '@inquirer/figures': 2.0.5
+ '@inquirer/type': 4.0.5(@types/node@26.0.1)
+ cli-width: 4.1.0
+ fast-wrap-ansi: 0.2.0
+ mute-stream: 3.0.0
+ signal-exit: 4.1.0
+ optionalDependencies:
+ '@types/node': 26.0.1
+
+ '@inquirer/figures@2.0.5': {}
+
+ '@inquirer/type@4.0.5(@types/node@26.0.1)':
+ optionalDependencies:
+ '@types/node': 26.0.1
+
'@internationalized/date@3.11.0':
dependencies:
- '@swc/helpers': 0.5.18
+ '@swc/helpers': 0.5.23
- '@internationalized/date@3.12.1':
+ '@internationalized/date@3.12.2':
dependencies:
- '@swc/helpers': 0.5.21
+ '@swc/helpers': 0.5.23
- '@internationalized/number@3.6.6':
+ '@internationalized/number@3.6.7':
dependencies:
- '@swc/helpers': 0.5.21
+ '@swc/helpers': 0.5.23
- '@internationalized/string@3.2.8':
+ '@internationalized/string@3.2.9':
dependencies:
- '@swc/helpers': 0.5.21
+ '@swc/helpers': 0.5.23
'@isaacs/cliui@8.0.2':
dependencies:
@@ -8028,213 +7654,10 @@ snapshots:
dependencies:
minipass: 7.1.2
- '@istanbuljs/load-nyc-config@1.1.0':
- dependencies:
- camelcase: 5.3.1
- find-up: 4.1.0
- get-package-type: 0.1.0
- js-yaml: 3.14.2
- resolve-from: 5.0.0
-
- '@istanbuljs/schema@0.1.6': {}
-
- '@jest/console@30.3.0':
- dependencies:
- '@jest/types': 30.3.0
- '@types/node': 25.6.0
- chalk: 4.1.2
- jest-message-util: 30.3.0
- jest-util: 30.3.0
- slash: 3.0.0
-
- '@jest/core@30.3.0(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3))':
- dependencies:
- '@jest/console': 30.3.0
- '@jest/pattern': 30.0.1
- '@jest/reporters': 30.3.0
- '@jest/test-result': 30.3.0
- '@jest/transform': 30.3.0
- '@jest/types': 30.3.0
- '@types/node': 25.6.0
- ansi-escapes: 4.3.2
- chalk: 4.1.2
- ci-info: 4.4.0
- exit-x: 0.2.2
- graceful-fs: 4.2.11
- jest-changed-files: 30.3.0
- jest-config: 30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3))
- jest-haste-map: 30.3.0
- jest-message-util: 30.3.0
- jest-regex-util: 30.0.1
- jest-resolve: 30.3.0
- jest-resolve-dependencies: 30.3.0
- jest-runner: 30.3.0
- jest-runtime: 30.3.0
- jest-snapshot: 30.3.0
- jest-util: 30.3.0
- jest-validate: 30.3.0
- jest-watcher: 30.3.0
- pretty-format: 30.3.0
- slash: 3.0.0
- transitivePeerDependencies:
- - babel-plugin-macros
- - esbuild-register
- - supports-color
- - ts-node
-
- '@jest/diff-sequences@30.0.1': {}
-
- '@jest/diff-sequences@30.3.0': {}
-
- '@jest/environment@30.3.0':
- dependencies:
- '@jest/fake-timers': 30.3.0
- '@jest/types': 30.3.0
- '@types/node': 25.6.0
- jest-mock: 30.3.0
-
- '@jest/expect-utils@30.1.1':
- dependencies:
- '@jest/get-type': 30.1.0
-
- '@jest/expect-utils@30.3.0':
- dependencies:
- '@jest/get-type': 30.1.0
-
- '@jest/expect@30.3.0':
- dependencies:
- expect: 30.3.0
- jest-snapshot: 30.3.0
- transitivePeerDependencies:
- - supports-color
-
- '@jest/fake-timers@30.3.0':
- dependencies:
- '@jest/types': 30.3.0
- '@sinonjs/fake-timers': 15.3.2
- '@types/node': 25.6.0
- jest-message-util: 30.3.0
- jest-mock: 30.3.0
- jest-util: 30.3.0
-
- '@jest/get-type@30.1.0': {}
-
- '@jest/globals@30.3.0':
- dependencies:
- '@jest/environment': 30.3.0
- '@jest/expect': 30.3.0
- '@jest/types': 30.3.0
- jest-mock: 30.3.0
- transitivePeerDependencies:
- - supports-color
-
- '@jest/pattern@30.0.1':
- dependencies:
- '@types/node': 25.6.0
- jest-regex-util: 30.0.1
-
- '@jest/reporters@30.3.0':
- dependencies:
- '@bcoe/v8-coverage': 0.2.3
- '@jest/console': 30.3.0
- '@jest/test-result': 30.3.0
- '@jest/transform': 30.3.0
- '@jest/types': 30.3.0
- '@jridgewell/trace-mapping': 0.3.31
- '@types/node': 25.6.0
- chalk: 4.1.2
- collect-v8-coverage: 1.0.3
- exit-x: 0.2.2
- glob: 10.5.0
- graceful-fs: 4.2.11
- istanbul-lib-coverage: 3.2.2
- istanbul-lib-instrument: 6.0.3
- istanbul-lib-report: 3.0.1
- istanbul-lib-source-maps: 5.0.6
- istanbul-reports: 3.2.0
- jest-message-util: 30.3.0
- jest-util: 30.3.0
- jest-worker: 30.3.0
- slash: 3.0.0
- string-length: 4.0.2
- v8-to-istanbul: 9.3.0
- transitivePeerDependencies:
- - supports-color
-
- '@jest/schemas@30.0.5':
- dependencies:
- '@sinclair/typebox': 0.34.40
-
- '@jest/snapshot-utils@30.3.0':
- dependencies:
- '@jest/types': 30.3.0
- chalk: 4.1.2
- graceful-fs: 4.2.11
- natural-compare: 1.4.0
-
- '@jest/source-map@30.0.1':
- dependencies:
- '@jridgewell/trace-mapping': 0.3.31
- callsites: 3.1.0
- graceful-fs: 4.2.11
-
- '@jest/test-result@30.3.0':
- dependencies:
- '@jest/console': 30.3.0
- '@jest/types': 30.3.0
- '@types/istanbul-lib-coverage': 2.0.6
- collect-v8-coverage: 1.0.3
-
- '@jest/test-sequencer@30.3.0':
- dependencies:
- '@jest/test-result': 30.3.0
- graceful-fs: 4.2.11
- jest-haste-map: 30.3.0
- slash: 3.0.0
-
- '@jest/transform@30.3.0':
- dependencies:
- '@babel/core': 7.29.0
- '@jest/types': 30.3.0
- '@jridgewell/trace-mapping': 0.3.31
- babel-plugin-istanbul: 7.0.1
- chalk: 4.1.2
- convert-source-map: 2.0.0
- fast-json-stable-stringify: 2.1.0
- graceful-fs: 4.2.11
- jest-haste-map: 30.3.0
- jest-regex-util: 30.0.1
- jest-util: 30.3.0
- pirates: 4.0.7
- slash: 3.0.0
- write-file-atomic: 5.0.1
- transitivePeerDependencies:
- - supports-color
-
- '@jest/types@30.0.5':
- dependencies:
- '@jest/pattern': 30.0.1
- '@jest/schemas': 30.0.5
- '@types/istanbul-lib-coverage': 2.0.6
- '@types/istanbul-reports': 3.0.4
- '@types/node': 25.6.0
- '@types/yargs': 17.0.33
- chalk: 4.1.2
-
- '@jest/types@30.3.0':
- dependencies:
- '@jest/pattern': 30.0.1
- '@jest/schemas': 30.0.5
- '@types/istanbul-lib-coverage': 2.0.6
- '@types/istanbul-reports': 3.0.4
- '@types/node': 25.6.0
- '@types/yargs': 17.0.35
- chalk: 4.1.2
-
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
- '@jridgewell/trace-mapping': 0.3.30
+ '@jridgewell/trace-mapping': 0.3.31
'@jridgewell/remapping@2.3.5':
dependencies:
@@ -8267,39 +7690,48 @@ snapshots:
'@kurkle/color@0.3.4': {}
- '@napi-rs/wasm-runtime@0.2.12':
+ '@mswjs/interceptors@0.41.8':
+ dependencies:
+ '@open-draft/deferred-promise': 2.2.0
+ '@open-draft/logger': 0.3.0
+ '@open-draft/until': 2.1.0
+ is-node-process: 1.2.0
+ outvariant: 1.4.3
+ strict-event-emitter: 0.5.1
+
+ '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
dependencies:
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
- '@tybys/wasm-util': 0.10.1
+ '@tybys/wasm-util': 0.10.3
optional: true
- '@netlify/plugin-nextjs@5.15.9': {}
+ '@netlify/plugin-nextjs@5.15.12': {}
- '@next/env@16.2.4': {}
+ '@next/env@16.2.9': {}
- '@next/swc-darwin-arm64@16.2.4':
+ '@next/swc-darwin-arm64@16.2.9':
optional: true
- '@next/swc-darwin-x64@16.2.4':
+ '@next/swc-darwin-x64@16.2.9':
optional: true
- '@next/swc-linux-arm64-gnu@16.2.4':
+ '@next/swc-linux-arm64-gnu@16.2.9':
optional: true
- '@next/swc-linux-arm64-musl@16.2.4':
+ '@next/swc-linux-arm64-musl@16.2.9':
optional: true
- '@next/swc-linux-x64-gnu@16.2.4':
+ '@next/swc-linux-x64-gnu@16.2.9':
optional: true
- '@next/swc-linux-x64-musl@16.2.4':
+ '@next/swc-linux-x64-musl@16.2.9':
optional: true
- '@next/swc-win32-arm64-msvc@16.2.4':
+ '@next/swc-win32-arm64-msvc@16.2.9':
optional: true
- '@next/swc-win32-x64-msvc@16.2.4':
+ '@next/swc-win32-x64-msvc@16.2.9':
optional: true
'@noble/hashes@2.2.0': {}
@@ -8342,6 +7774,18 @@ snapshots:
'@otplib/uri@13.4.0':
dependencies:
'@otplib/core': 13.4.0
+ '@open-draft/deferred-promise@2.2.0': {}
+
+ '@open-draft/deferred-promise@3.0.0': {}
+
+ '@open-draft/logger@0.3.0':
+ dependencies:
+ is-node-process: 1.2.0
+ outvariant: 1.4.3
+
+ '@open-draft/until@2.1.0': {}
+
+ '@oxc-project/types@0.128.0': {}
'@parcel/watcher-android-arm64@2.5.6':
optional: true
@@ -8406,29 +7850,31 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
- '@pkgr/core@0.2.9': {}
-
- '@prisma/adapter-pg@7.7.0':
+ '@playwright/test@1.61.1':
dependencies:
- '@prisma/driver-adapter-utils': 7.7.0
+ playwright: 1.61.1
+
+ '@prisma/adapter-pg@7.8.0':
+ dependencies:
+ '@prisma/driver-adapter-utils': 7.8.0
'@types/pg': 8.20.0
- pg: 8.20.0
+ pg: 8.22.0
postgres-array: 3.0.4
transitivePeerDependencies:
- pg-native
- '@prisma/client-runtime-utils@7.7.0': {}
+ '@prisma/client-runtime-utils@7.8.0': {}
- '@prisma/client@7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3))(typescript@6.0.3)':
+ '@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(typescript@6.0.3)':
dependencies:
- '@prisma/client-runtime-utils': 7.7.0
+ '@prisma/client-runtime-utils': 7.8.0
optionalDependencies:
- prisma: 7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3)
+ prisma: 7.8.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
typescript: 6.0.3
- '@prisma/config@7.7.0':
+ '@prisma/config@7.8.0':
dependencies:
- c12: 3.1.0
+ c12: 3.3.4
deepmerge-ts: 7.1.5
effect: 3.20.0
empathic: 2.0.0
@@ -8437,20 +7883,20 @@ snapshots:
'@prisma/debug@7.2.0': {}
- '@prisma/debug@7.7.0': {}
+ '@prisma/debug@7.8.0': {}
'@prisma/dev@0.24.3(typescript@6.0.3)':
dependencies:
'@electric-sql/pglite': 0.4.1
'@electric-sql/pglite-socket': 0.1.1(@electric-sql/pglite@0.4.1)
'@electric-sql/pglite-tools': 0.3.1(@electric-sql/pglite@0.4.1)
- '@hono/node-server': 1.19.11(hono@4.12.9)
+ '@hono/node-server': 1.19.11(hono@4.12.21)
'@prisma/get-platform': 7.2.0
'@prisma/query-plan-executor': 7.2.0
'@prisma/streams-local': 0.1.2
foreground-child: 3.3.1
get-port-please: 3.2.0
- hono: 4.12.9
+ hono: 4.12.21
http-status-codes: 2.3.0
pathe: 2.0.3
proper-lockfile: 4.1.2
@@ -8461,36 +7907,36 @@ snapshots:
transitivePeerDependencies:
- typescript
- '@prisma/driver-adapter-utils@7.7.0':
+ '@prisma/driver-adapter-utils@7.8.0':
dependencies:
- '@prisma/debug': 7.7.0
+ '@prisma/debug': 7.8.0
- '@prisma/engines-version@7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711': {}
+ '@prisma/engines-version@7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a': {}
- '@prisma/engines@7.7.0':
+ '@prisma/engines@7.8.0':
dependencies:
- '@prisma/debug': 7.7.0
- '@prisma/engines-version': 7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711
- '@prisma/fetch-engine': 7.7.0
- '@prisma/get-platform': 7.7.0
+ '@prisma/debug': 7.8.0
+ '@prisma/engines-version': 7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a
+ '@prisma/fetch-engine': 7.8.0
+ '@prisma/get-platform': 7.8.0
- '@prisma/extension-read-replicas@0.5.0(@prisma/client@7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3))(typescript@6.0.3))':
+ '@prisma/extension-read-replicas@0.5.0(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(typescript@6.0.3))':
dependencies:
- '@prisma/client': 7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3))(typescript@6.0.3)
+ '@prisma/client': 7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(typescript@6.0.3)
- '@prisma/fetch-engine@7.7.0':
+ '@prisma/fetch-engine@7.8.0':
dependencies:
- '@prisma/debug': 7.7.0
- '@prisma/engines-version': 7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711
- '@prisma/get-platform': 7.7.0
+ '@prisma/debug': 7.8.0
+ '@prisma/engines-version': 7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a
+ '@prisma/get-platform': 7.8.0
'@prisma/get-platform@7.2.0':
dependencies:
'@prisma/debug': 7.2.0
- '@prisma/get-platform@7.7.0':
+ '@prisma/get-platform@7.8.0':
dependencies:
- '@prisma/debug': 7.7.0
+ '@prisma/debug': 7.8.0
'@prisma/query-plan-executor@7.2.0': {}
@@ -8501,156 +7947,152 @@ snapshots:
env-paths: 3.0.0
proper-lockfile: 4.1.2
- '@prisma/studio-core@0.27.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
+ '@prisma/studio-core@0.27.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
- '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- '@types/react': 19.2.14
+ '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@types/react': 19.2.17
chart.js: 4.5.1
- react: 19.2.5
- react-dom: 19.2.5(react@19.2.5)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
transitivePeerDependencies:
- '@types/react-dom'
'@radix-ui/primitive@1.1.3': {}
- '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.5)':
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.17)(react@19.2.7)':
dependencies:
- react: 19.2.5
+ react: 19.2.7
optionalDependencies:
- '@types/react': 19.2.14
+ '@types/react': 19.2.17
- '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
+ '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5)
- react: 19.2.5
- react-dom: 19.2.5(react@19.2.5)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
optionalDependencies:
- '@types/react': 19.2.14
- '@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
- '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.5)':
+ '@radix-ui/react-slot@1.2.3(@types/react@19.2.17)(react@19.2.7)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
- react: 19.2.5
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
optionalDependencies:
- '@types/react': 19.2.14
+ '@types/react': 19.2.17
- '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
+ '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5)
- react: 19.2.5
- react-dom: 19.2.5(react@19.2.5)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
optionalDependencies:
- '@types/react': 19.2.14
- '@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
- '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.5)':
+ '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.17)(react@19.2.7)':
dependencies:
- '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5)
- react: 19.2.5
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
optionalDependencies:
- '@types/react': 19.2.14
+ '@types/react': 19.2.17
- '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.5)':
+ '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.17)(react@19.2.7)':
dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5)
- react: 19.2.5
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
optionalDependencies:
- '@types/react': 19.2.14
+ '@types/react': 19.2.17
- '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.5)':
+ '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.17)(react@19.2.7)':
dependencies:
- react: 19.2.5
+ react: 19.2.7
optionalDependencies:
- '@types/react': 19.2.14
+ '@types/react': 19.2.17
- '@react-aria/focus@3.21.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
+ '@react-aria/focus@3.21.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
- '@react-aria/interactions': 3.27.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- '@react-aria/utils': 3.33.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- '@react-types/shared': 3.33.0(react@19.2.5)
- '@swc/helpers': 0.5.18
+ '@react-aria/interactions': 3.27.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@react-aria/utils': 3.33.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@react-types/shared': 3.36.0(react@19.2.7)
+ '@swc/helpers': 0.5.23
clsx: 2.1.1
- react: 19.2.5
- react-dom: 19.2.5(react@19.2.5)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
- '@react-aria/interactions@3.27.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
+ '@react-aria/interactions@3.27.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
- '@react-aria/ssr': 3.9.10(react@19.2.5)
- '@react-aria/utils': 3.33.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/ssr': 3.9.10(react@19.2.7)
+ '@react-aria/utils': 3.33.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@react-stately/flags': 3.1.2
- '@react-types/shared': 3.33.0(react@19.2.5)
- '@swc/helpers': 0.5.18
- react: 19.2.5
- react-dom: 19.2.5(react@19.2.5)
+ '@react-types/shared': 3.36.0(react@19.2.7)
+ '@swc/helpers': 0.5.23
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
- '@react-aria/ssr@3.9.10(react@19.2.5)':
+ '@react-aria/ssr@3.9.10(react@19.2.7)':
dependencies:
- '@swc/helpers': 0.5.18
- react: 19.2.5
+ '@swc/helpers': 0.5.23
+ react: 19.2.7
- '@react-aria/utils@3.33.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
+ '@react-aria/utils@3.33.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
- '@react-aria/ssr': 3.9.10(react@19.2.5)
+ '@react-aria/ssr': 3.9.10(react@19.2.7)
'@react-stately/flags': 3.1.2
- '@react-stately/utils': 3.11.0(react@19.2.5)
- '@react-types/shared': 3.33.0(react@19.2.5)
- '@swc/helpers': 0.5.18
+ '@react-stately/utils': 3.11.0(react@19.2.7)
+ '@react-types/shared': 3.36.0(react@19.2.7)
+ '@swc/helpers': 0.5.23
clsx: 2.1.1
- react: 19.2.5
- react-dom: 19.2.5(react@19.2.5)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
- '@react-spring/animated@9.7.5(react@19.2.5)':
+ '@react-spring/animated@9.7.5(react@19.2.7)':
dependencies:
- '@react-spring/shared': 9.7.5(react@19.2.5)
+ '@react-spring/shared': 9.7.5(react@19.2.7)
'@react-spring/types': 9.7.5
- react: 19.2.5
+ react: 19.2.7
- '@react-spring/core@9.7.5(react@19.2.5)':
+ '@react-spring/core@9.7.5(react@19.2.7)':
dependencies:
- '@react-spring/animated': 9.7.5(react@19.2.5)
- '@react-spring/shared': 9.7.5(react@19.2.5)
+ '@react-spring/animated': 9.7.5(react@19.2.7)
+ '@react-spring/shared': 9.7.5(react@19.2.7)
'@react-spring/types': 9.7.5
- react: 19.2.5
+ react: 19.2.7
'@react-spring/rafz@9.7.5': {}
- '@react-spring/shared@9.7.5(react@19.2.5)':
+ '@react-spring/shared@9.7.5(react@19.2.7)':
dependencies:
'@react-spring/rafz': 9.7.5
'@react-spring/types': 9.7.5
- react: 19.2.5
+ react: 19.2.7
'@react-spring/types@9.7.5': {}
- '@react-spring/web@9.7.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
+ '@react-spring/web@9.7.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
- '@react-spring/animated': 9.7.5(react@19.2.5)
- '@react-spring/core': 9.7.5(react@19.2.5)
- '@react-spring/shared': 9.7.5(react@19.2.5)
+ '@react-spring/animated': 9.7.5(react@19.2.7)
+ '@react-spring/core': 9.7.5(react@19.2.7)
+ '@react-spring/shared': 9.7.5(react@19.2.7)
'@react-spring/types': 9.7.5
- react: 19.2.5
- react-dom: 19.2.5(react@19.2.5)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
'@react-stately/flags@3.1.2':
dependencies:
- '@swc/helpers': 0.5.18
+ '@swc/helpers': 0.5.23
- '@react-stately/utils@3.11.0(react@19.2.5)':
+ '@react-stately/utils@3.11.0(react@19.2.7)':
dependencies:
- '@swc/helpers': 0.5.18
- react: 19.2.5
+ '@swc/helpers': 0.5.23
+ react: 19.2.7
- '@react-types/shared@3.33.0(react@19.2.5)':
+ '@react-types/shared@3.36.0(react@19.2.7)':
dependencies:
- react: 19.2.5
-
- '@react-types/shared@3.34.0(react@19.2.5)':
- dependencies:
- react: 19.2.5
+ react: 19.2.7
'@redis/bloom@5.12.1(@redis/client@5.12.1)':
dependencies:
@@ -8672,13 +8114,64 @@ snapshots:
dependencies:
'@redis/client': 5.12.1
- '@rollup/plugin-alias@6.0.0(rollup@4.60.1)':
- optionalDependencies:
- rollup: 4.60.1
+ '@rolldown/binding-android-arm64@1.0.0-rc.18':
+ optional: true
- '@rollup/plugin-commonjs@29.0.2(rollup@4.60.1)':
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.18':
dependencies:
- '@rollup/pluginutils': 5.3.0(rollup@4.60.1)
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.0-rc.18': {}
+
+ '@rollup/plugin-alias@6.0.0(rollup@4.62.2)':
+ optionalDependencies:
+ rollup: 4.62.2
+
+ '@rollup/plugin-commonjs@29.0.3(rollup@4.62.2)':
+ dependencies:
+ '@rollup/pluginutils': 5.3.0(rollup@4.62.2)
commondir: 1.0.1
estree-walker: 2.0.2
fdir: 6.5.0(picomatch@4.0.4)
@@ -8686,157 +8179,160 @@ snapshots:
magic-string: 0.30.21
picomatch: 4.0.4
optionalDependencies:
- rollup: 4.60.1
+ rollup: 4.62.2
- '@rollup/plugin-json@6.1.0(rollup@4.60.1)':
+ '@rollup/plugin-json@6.1.0(rollup@4.62.2)':
dependencies:
- '@rollup/pluginutils': 5.2.0(rollup@4.60.1)
+ '@rollup/pluginutils': 5.2.0(rollup@4.62.2)
optionalDependencies:
- rollup: 4.60.1
+ rollup: 4.62.2
- '@rollup/plugin-node-resolve@16.0.3(rollup@4.60.1)':
+ '@rollup/plugin-node-resolve@16.0.3(rollup@4.62.2)':
dependencies:
- '@rollup/pluginutils': 5.3.0(rollup@4.60.1)
+ '@rollup/pluginutils': 5.3.0(rollup@4.62.2)
'@types/resolve': 1.20.2
deepmerge: 4.3.1
is-module: 1.0.0
resolve: 1.22.12
optionalDependencies:
- rollup: 4.60.1
+ rollup: 4.62.2
- '@rollup/plugin-replace@6.0.3(rollup@4.60.1)':
+ '@rollup/plugin-replace@6.0.3(rollup@4.62.2)':
dependencies:
- '@rollup/pluginutils': 5.3.0(rollup@4.60.1)
+ '@rollup/pluginutils': 5.3.0(rollup@4.62.2)
magic-string: 0.30.21
optionalDependencies:
- rollup: 4.60.1
+ rollup: 4.62.2
- '@rollup/plugin-terser@1.0.0(rollup@4.60.1)':
+ '@rollup/plugin-terser@1.0.0(rollup@4.62.2)':
dependencies:
serialize-javascript: 7.0.5
smob: 1.6.1
terser: 5.46.1
optionalDependencies:
- rollup: 4.60.1
+ rollup: 4.62.2
- '@rollup/plugin-typescript@12.3.0(rollup@4.60.1)(tslib@2.8.1)(typescript@6.0.3)':
+ '@rollup/plugin-typescript@12.3.0(rollup@4.62.2)(tslib@2.8.1)(typescript@6.0.3)':
dependencies:
- '@rollup/pluginutils': 5.2.0(rollup@4.60.1)
+ '@rollup/pluginutils': 5.2.0(rollup@4.62.2)
resolve: 1.22.10
typescript: 6.0.3
optionalDependencies:
- rollup: 4.60.1
+ rollup: 4.62.2
tslib: 2.8.1
- '@rollup/pluginutils@5.2.0(rollup@4.60.1)':
+ '@rollup/pluginutils@5.2.0(rollup@4.62.2)':
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 4.0.3
optionalDependencies:
- rollup: 4.60.1
+ rollup: 4.62.2
- '@rollup/pluginutils@5.3.0(rollup@4.60.1)':
+ '@rollup/pluginutils@5.3.0(rollup@4.62.2)':
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 4.0.4
optionalDependencies:
- rollup: 4.60.1
+ rollup: 4.62.2
- '@rollup/rollup-android-arm-eabi@4.60.1':
+ '@rollup/rollup-android-arm-eabi@4.62.2':
optional: true
- '@rollup/rollup-android-arm64@4.60.1':
+ '@rollup/rollup-android-arm64@4.62.2':
optional: true
- '@rollup/rollup-darwin-arm64@4.60.1':
+ '@rollup/rollup-darwin-arm64@4.62.2':
optional: true
- '@rollup/rollup-darwin-x64@4.60.1':
+ '@rollup/rollup-darwin-x64@4.62.2':
optional: true
- '@rollup/rollup-freebsd-arm64@4.60.1':
+ '@rollup/rollup-freebsd-arm64@4.62.2':
optional: true
- '@rollup/rollup-freebsd-x64@4.60.1':
+ '@rollup/rollup-freebsd-x64@4.62.2':
optional: true
- '@rollup/rollup-linux-arm-gnueabihf@4.60.1':
+ '@rollup/rollup-linux-arm-gnueabihf@4.62.2':
optional: true
- '@rollup/rollup-linux-arm-musleabihf@4.60.1':
+ '@rollup/rollup-linux-arm-musleabihf@4.62.2':
optional: true
- '@rollup/rollup-linux-arm64-gnu@4.60.1':
+ '@rollup/rollup-linux-arm64-gnu@4.62.2':
optional: true
- '@rollup/rollup-linux-arm64-musl@4.60.1':
+ '@rollup/rollup-linux-arm64-musl@4.62.2':
optional: true
- '@rollup/rollup-linux-loong64-gnu@4.60.1':
+ '@rollup/rollup-linux-loong64-gnu@4.62.2':
optional: true
- '@rollup/rollup-linux-loong64-musl@4.60.1':
+ '@rollup/rollup-linux-loong64-musl@4.62.2':
optional: true
- '@rollup/rollup-linux-ppc64-gnu@4.60.1':
+ '@rollup/rollup-linux-ppc64-gnu@4.62.2':
optional: true
- '@rollup/rollup-linux-ppc64-musl@4.60.1':
+ '@rollup/rollup-linux-ppc64-musl@4.62.2':
optional: true
- '@rollup/rollup-linux-riscv64-gnu@4.60.1':
+ '@rollup/rollup-linux-riscv64-gnu@4.62.2':
optional: true
- '@rollup/rollup-linux-riscv64-musl@4.60.1':
+ '@rollup/rollup-linux-riscv64-musl@4.62.2':
optional: true
- '@rollup/rollup-linux-s390x-gnu@4.60.1':
+ '@rollup/rollup-linux-s390x-gnu@4.62.2':
optional: true
- '@rollup/rollup-linux-x64-gnu@4.60.1':
+ '@rollup/rollup-linux-x64-gnu@4.62.2':
optional: true
- '@rollup/rollup-linux-x64-musl@4.60.1':
+ '@rollup/rollup-linux-x64-musl@4.62.2':
optional: true
- '@rollup/rollup-openbsd-x64@4.60.1':
+ '@rollup/rollup-openbsd-x64@4.62.2':
optional: true
- '@rollup/rollup-openharmony-arm64@4.60.1':
+ '@rollup/rollup-openharmony-arm64@4.62.2':
optional: true
- '@rollup/rollup-win32-arm64-msvc@4.60.1':
+ '@rollup/rollup-win32-arm64-msvc@4.62.2':
optional: true
- '@rollup/rollup-win32-ia32-msvc@4.60.1':
+ '@rollup/rollup-win32-ia32-msvc@4.62.2':
optional: true
- '@rollup/rollup-win32-x64-gnu@4.60.1':
+ '@rollup/rollup-win32-x64-gnu@4.62.2':
optional: true
- '@rollup/rollup-win32-x64-msvc@4.60.1':
+ '@rollup/rollup-win32-x64-msvc@4.62.2':
optional: true
- '@rrweb/types@2.0.0-alpha.20': {}
+ '@rrweb/packer@2.0.1':
+ dependencies:
+ '@rrweb/types': 2.0.1
+ fflate: 0.4.8
- '@schummar/icu-type-parser@1.21.5': {}
+ '@rrweb/replay@2.0.1':
+ dependencies:
+ '@rrweb/types': 2.0.1
+ rrweb: 2.0.1
'@scure/base@2.2.0': {}
'@sinclair/typebox@0.34.40': {}
+ '@rrweb/types@2.0.1': {}
+
+ '@rrweb/utils@2.0.1': {}
+
+ '@schummar/icu-type-parser@1.21.5': {}
'@sindresorhus/merge-streams@2.3.0': {}
- '@sinonjs/commons@3.0.1':
- dependencies:
- type-detect: 4.0.8
-
- '@sinonjs/fake-timers@15.3.2':
- dependencies:
- '@sinonjs/commons': 3.0.1
-
'@standard-schema/spec@1.1.0': {}
'@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.3)':
@@ -8940,60 +8436,60 @@ snapshots:
transitivePeerDependencies:
- typescript
- '@swc/core-darwin-arm64@1.15.26':
+ '@swc/core-darwin-arm64@1.15.43':
optional: true
- '@swc/core-darwin-x64@1.15.26':
+ '@swc/core-darwin-x64@1.15.43':
optional: true
- '@swc/core-linux-arm-gnueabihf@1.15.26':
+ '@swc/core-linux-arm-gnueabihf@1.15.43':
optional: true
- '@swc/core-linux-arm64-gnu@1.15.26':
+ '@swc/core-linux-arm64-gnu@1.15.43':
optional: true
- '@swc/core-linux-arm64-musl@1.15.26':
+ '@swc/core-linux-arm64-musl@1.15.43':
optional: true
- '@swc/core-linux-ppc64-gnu@1.15.26':
+ '@swc/core-linux-ppc64-gnu@1.15.43':
optional: true
- '@swc/core-linux-s390x-gnu@1.15.26':
+ '@swc/core-linux-s390x-gnu@1.15.43':
optional: true
- '@swc/core-linux-x64-gnu@1.15.26':
+ '@swc/core-linux-x64-gnu@1.15.43':
optional: true
- '@swc/core-linux-x64-musl@1.15.26':
+ '@swc/core-linux-x64-musl@1.15.43':
optional: true
- '@swc/core-win32-arm64-msvc@1.15.26':
+ '@swc/core-win32-arm64-msvc@1.15.43':
optional: true
- '@swc/core-win32-ia32-msvc@1.15.26':
+ '@swc/core-win32-ia32-msvc@1.15.43':
optional: true
- '@swc/core-win32-x64-msvc@1.15.26':
+ '@swc/core-win32-x64-msvc@1.15.43':
optional: true
- '@swc/core@1.15.26(@swc/helpers@0.5.21)':
+ '@swc/core@1.15.43(@swc/helpers@0.5.23)':
dependencies:
'@swc/counter': 0.1.3
- '@swc/types': 0.1.26
+ '@swc/types': 0.1.27
optionalDependencies:
- '@swc/core-darwin-arm64': 1.15.26
- '@swc/core-darwin-x64': 1.15.26
- '@swc/core-linux-arm-gnueabihf': 1.15.26
- '@swc/core-linux-arm64-gnu': 1.15.26
- '@swc/core-linux-arm64-musl': 1.15.26
- '@swc/core-linux-ppc64-gnu': 1.15.26
- '@swc/core-linux-s390x-gnu': 1.15.26
- '@swc/core-linux-x64-gnu': 1.15.26
- '@swc/core-linux-x64-musl': 1.15.26
- '@swc/core-win32-arm64-msvc': 1.15.26
- '@swc/core-win32-ia32-msvc': 1.15.26
- '@swc/core-win32-x64-msvc': 1.15.26
- '@swc/helpers': 0.5.21
+ '@swc/core-darwin-arm64': 1.15.43
+ '@swc/core-darwin-x64': 1.15.43
+ '@swc/core-linux-arm-gnueabihf': 1.15.43
+ '@swc/core-linux-arm64-gnu': 1.15.43
+ '@swc/core-linux-arm64-musl': 1.15.43
+ '@swc/core-linux-ppc64-gnu': 1.15.43
+ '@swc/core-linux-s390x-gnu': 1.15.43
+ '@swc/core-linux-x64-gnu': 1.15.43
+ '@swc/core-linux-x64-musl': 1.15.43
+ '@swc/core-win32-arm64-msvc': 1.15.43
+ '@swc/core-win32-ia32-msvc': 1.15.43
+ '@swc/core-win32-x64-msvc': 1.15.43
+ '@swc/helpers': 0.5.23
'@swc/counter@0.1.3': {}
@@ -9001,24 +8497,54 @@ snapshots:
dependencies:
tslib: 2.8.1
- '@swc/helpers@0.5.18':
+ '@swc/helpers@0.5.23':
dependencies:
tslib: 2.8.1
- '@swc/helpers@0.5.21':
- dependencies:
- tslib: 2.8.1
-
- '@swc/types@0.1.26':
+ '@swc/types@0.1.27':
dependencies:
'@swc/counter': 0.1.3
- '@tanstack/query-core@5.99.0': {}
+ '@tanstack/query-core@5.101.1': {}
- '@tanstack/react-query@5.99.0(react@19.2.5)':
+ '@tanstack/react-query@5.101.1(react@19.2.7)':
dependencies:
- '@tanstack/query-core': 5.99.0
- react: 19.2.5
+ '@tanstack/query-core': 5.101.1
+ react: 19.2.7
+
+ '@testing-library/dom@10.4.1':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/runtime': 7.29.7
+ '@types/aria-query': 5.0.4
+ aria-query: 5.3.0
+ dom-accessibility-api: 0.5.16
+ lz-string: 1.5.0
+ picocolors: 1.1.1
+ pretty-format: 27.5.1
+
+ '@testing-library/jest-dom@6.9.1':
+ dependencies:
+ '@adobe/css-tools': 4.4.4
+ aria-query: 5.3.2
+ css.escape: 1.5.1
+ dom-accessibility-api: 0.6.3
+ picocolors: 1.1.1
+ redent: 3.0.0
+
+ '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ dependencies:
+ '@babel/runtime': 7.29.2
+ '@testing-library/dom': 10.4.1
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+
+ '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
+ dependencies:
+ '@testing-library/dom': 10.4.1
'@trysound/sax@0.2.0': {}
@@ -9038,59 +8564,34 @@ snapshots:
'@tsconfig/svelte@1.0.13': {}
- '@tybys/wasm-util@0.10.1':
+ '@tybys/wasm-util@0.10.3':
dependencies:
tslib: 2.8.1
optional: true
- '@types/babel__core@7.20.5':
- dependencies:
- '@babel/parser': 7.29.2
- '@babel/types': 7.29.0
- '@types/babel__generator': 7.27.0
- '@types/babel__template': 7.4.4
- '@types/babel__traverse': 7.28.0
+ '@types/aria-query@5.0.4': {}
- '@types/babel__generator@7.27.0':
+ '@types/chai@5.2.3':
dependencies:
- '@babel/types': 7.29.0
-
- '@types/babel__template@7.4.4':
- dependencies:
- '@babel/parser': 7.29.2
- '@babel/types': 7.29.0
-
- '@types/babel__traverse@7.28.0':
- dependencies:
- '@babel/types': 7.29.0
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
'@types/css-font-loading-module@0.0.7': {}
+ '@types/deep-eql@4.0.2': {}
+
'@types/estree@1.0.8': {}
+ '@types/estree@1.0.9': {}
+
'@types/fs-extra@8.1.5':
dependencies:
- '@types/node': 25.6.0
+ '@types/node': 26.0.1
'@types/glob@7.2.0':
dependencies:
'@types/minimatch': 6.0.0
- '@types/node': 25.6.0
-
- '@types/istanbul-lib-coverage@2.0.6': {}
-
- '@types/istanbul-lib-report@3.0.3':
- dependencies:
- '@types/istanbul-lib-coverage': 2.0.6
-
- '@types/istanbul-reports@3.0.4':
- dependencies:
- '@types/istanbul-lib-report': 3.0.3
-
- '@types/jest@30.0.0':
- dependencies:
- expect: 30.1.1
- pretty-format: 30.0.5
+ '@types/node': 26.0.1
'@types/json-schema@7.0.15': {}
@@ -9098,14 +8599,14 @@ snapshots:
dependencies:
minimatch: 10.2.5
- '@types/node@25.6.0':
+ '@types/node@26.0.1':
dependencies:
- undici-types: 7.19.2
+ undici-types: 8.3.0
'@types/pg@8.20.0':
dependencies:
- '@types/node': 25.6.0
- pg-protocol: 1.13.0
+ '@types/node': 26.0.1
+ pg-protocol: 1.15.0
pg-types: 2.2.0
'@types/qrcode@1.5.6':
@@ -9113,120 +8614,85 @@ snapshots:
'@types/node': 25.6.0
'@types/react-dom@19.2.3(@types/react@19.2.14)':
+ '@types/react-dom@19.2.3(@types/react@19.2.17)':
dependencies:
- '@types/react': 19.2.14
+ '@types/react': 19.2.17
- '@types/react@19.2.14':
+ '@types/react@19.2.17':
dependencies:
csstype: 3.2.3
'@types/resolve@1.20.2': {}
- '@types/sinonjs__fake-timers@8.1.1': {}
+ '@types/set-cookie-parser@2.4.10':
+ dependencies:
+ '@types/node': 26.0.1
- '@types/sizzle@2.3.10': {}
-
- '@types/stack-utils@2.0.3': {}
-
- '@types/tmp@0.2.6': {}
+ '@types/statuses@2.0.6': {}
'@types/use-sync-external-store@0.0.6': {}
- '@types/yargs-parser@21.0.3': {}
-
- '@types/yargs@17.0.33':
- dependencies:
- '@types/yargs-parser': 21.0.3
-
- '@types/yargs@17.0.35':
- dependencies:
- '@types/yargs-parser': 21.0.3
-
- '@types/yauzl@2.10.3':
- dependencies:
- '@types/node': 25.6.0
- optional: true
-
- '@umami/react-zen@0.245.0(@types/react@19.2.14)(immer@11.1.4)(react-aria-components@1.17.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.1.18)(use-sync-external-store@1.6.0(react@19.2.5))':
+ '@umami/react-zen@0.245.0(@types/react@19.2.17)(immer@11.1.8)(react-aria-components@1.19.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tailwindcss@4.1.18)(use-sync-external-store@1.6.0(react@19.2.7))':
dependencies:
'@internationalized/date': 3.11.0
- '@react-aria/focus': 3.21.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- '@react-spring/web': 9.7.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@react-aria/focus': 3.21.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@react-spring/web': 9.7.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
clsx: 2.1.1
- lucide-react: 0.555.0(react@19.2.5)
- react: 19.2.5
- react-aria-components: 1.17.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- react-dom: 19.2.5(react@19.2.5)
- react-hook-form: 7.71.1(react@19.2.5)
+ lucide-react: 0.555.0(react@19.2.7)
+ react: 19.2.7
+ react-aria-components: 1.19.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ react-dom: 19.2.7(react@19.2.7)
+ react-hook-form: 7.71.1(react@19.2.7)
tailwind-merge: 3.4.0
tailwind-variants: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18)
- zustand: 5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))
+ zustand: 5.0.14(@types/react@19.2.17)(immer@11.1.8)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7))
transitivePeerDependencies:
- '@types/react'
- immer
- tailwindcss
- use-sync-external-store
- '@ungap/structured-clone@1.3.0': {}
-
- '@unrs/resolver-binding-android-arm-eabi@1.11.1':
- optional: true
-
- '@unrs/resolver-binding-android-arm64@1.11.1':
- optional: true
-
- '@unrs/resolver-binding-darwin-arm64@1.11.1':
- optional: true
-
- '@unrs/resolver-binding-darwin-x64@1.11.1':
- optional: true
-
- '@unrs/resolver-binding-freebsd-x64@1.11.1':
- optional: true
-
- '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1':
- optional: true
-
- '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1':
- optional: true
-
- '@unrs/resolver-binding-linux-arm64-gnu@1.11.1':
- optional: true
-
- '@unrs/resolver-binding-linux-arm64-musl@1.11.1':
- optional: true
-
- '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
- optional: true
-
- '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
- optional: true
-
- '@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
- optional: true
-
- '@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
- optional: true
-
- '@unrs/resolver-binding-linux-x64-gnu@1.11.1':
- optional: true
-
- '@unrs/resolver-binding-linux-x64-musl@1.11.1':
- optional: true
-
- '@unrs/resolver-binding-wasm32-wasi@1.11.1':
+ '@vitest/expect@4.1.9':
dependencies:
- '@napi-rs/wasm-runtime': 0.2.12
- optional: true
+ '@standard-schema/spec': 1.1.0
+ '@types/chai': 5.2.3
+ '@vitest/spy': 4.1.9
+ '@vitest/utils': 4.1.9
+ chai: 6.2.2
+ tinyrainbow: 3.1.0
- '@unrs/resolver-binding-win32-arm64-msvc@1.11.1':
- optional: true
+ '@vitest/mocker@4.1.9(msw@2.14.6(@types/node@26.0.1)(typescript@6.0.3))(vite@8.0.11(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.22.4))':
+ dependencies:
+ '@vitest/spy': 4.1.9
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ msw: 2.14.6(@types/node@26.0.1)(typescript@6.0.3)
+ vite: 8.0.11(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.22.4)
- '@unrs/resolver-binding-win32-ia32-msvc@1.11.1':
- optional: true
+ '@vitest/pretty-format@4.1.9':
+ dependencies:
+ tinyrainbow: 3.1.0
- '@unrs/resolver-binding-win32-x64-msvc@1.11.1':
- optional: true
+ '@vitest/runner@4.1.9':
+ dependencies:
+ '@vitest/utils': 4.1.9
+ pathe: 2.0.3
+
+ '@vitest/snapshot@4.1.9':
+ dependencies:
+ '@vitest/pretty-format': 4.1.9
+ '@vitest/utils': 4.1.9
+ magic-string: 0.30.21
+ pathe: 2.0.3
+
+ '@vitest/spy@4.1.9': {}
+
+ '@vitest/utils@4.1.9':
+ dependencies:
+ '@vitest/pretty-format': 4.1.9
+ convert-source-map: 2.0.0
+ tinyrainbow: 3.1.0
'@xstate/fsm@1.6.5': {}
@@ -9238,10 +8704,7 @@ snapshots:
acorn@8.16.0: {}
- aggregate-error@3.1.0:
- dependencies:
- clean-stack: 2.2.0
- indent-string: 4.0.0
+ acorn@8.17.0: {}
ajv@8.18.0:
dependencies:
@@ -9250,12 +8713,6 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
- ansi-colors@4.1.3: {}
-
- ansi-escapes@4.3.2:
- dependencies:
- type-fest: 0.21.3
-
ansi-regex@5.0.1: {}
ansi-regex@6.2.2: {}
@@ -9274,25 +8731,20 @@ snapshots:
any-promise@1.3.0: {}
- anymatch@3.1.3:
- dependencies:
- normalize-path: 3.0.0
- picomatch: 2.3.2
-
- arch@2.2.0: {}
-
arg@4.1.3: {}
- argparse@1.0.10:
- dependencies:
- sprintf-js: 1.0.3
-
argparse@2.0.1: {}
aria-hidden@1.2.6:
dependencies:
tslib: 2.8.1
+ aria-query@5.3.0:
+ dependencies:
+ dequal: 2.0.3
+
+ aria-query@5.3.2: {}
+
array-buffer-byte-length@1.0.2:
dependencies:
call-bound: 1.0.4
@@ -9310,108 +8762,36 @@ snapshots:
get-intrinsic: 1.3.0
is-array-buffer: 3.0.5
- asn1@0.2.6:
- dependencies:
- safer-buffer: 2.1.2
-
- assert-plus@1.0.0: {}
-
- astral-regex@2.0.0: {}
+ assertion-error@2.0.1: {}
async-function@1.0.0: {}
- asynckit@0.4.0: {}
-
- at-least-node@1.0.0: {}
-
- autoprefixer@10.5.0(postcss@8.5.10):
+ autoprefixer@10.5.2(postcss@8.5.15):
dependencies:
- browserslist: 4.28.2
- caniuse-lite: 1.0.30001788
+ browserslist: 4.28.4
+ caniuse-lite: 1.0.30001799
fraction.js: 5.3.4
picocolors: 1.1.1
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.1.0
- aws-sign2@0.7.0: {}
-
aws-ssl-profiles@1.1.2: {}
- aws4@1.13.2: {}
-
- babel-jest@30.3.0(@babel/core@7.29.0):
- dependencies:
- '@babel/core': 7.29.0
- '@jest/transform': 30.3.0
- '@types/babel__core': 7.20.5
- babel-plugin-istanbul: 7.0.1
- babel-preset-jest: 30.3.0(@babel/core@7.29.0)
- chalk: 4.1.2
- graceful-fs: 4.2.11
- slash: 3.0.0
- transitivePeerDependencies:
- - supports-color
-
- babel-plugin-istanbul@7.0.1:
- dependencies:
- '@babel/helper-plugin-utils': 7.28.6
- '@istanbuljs/load-nyc-config': 1.1.0
- '@istanbuljs/schema': 0.1.6
- istanbul-lib-instrument: 6.0.3
- test-exclude: 6.0.0
- transitivePeerDependencies:
- - supports-color
-
- babel-plugin-jest-hoist@30.3.0:
- dependencies:
- '@types/babel__core': 7.20.5
-
babel-plugin-react-compiler@19.1.0-rc.2:
dependencies:
'@babel/types': 7.28.2
- babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0):
- dependencies:
- '@babel/core': 7.29.0
- '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0)
- '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0)
- '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0)
- '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0)
- '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
- '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0)
- '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0)
- '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0)
- '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0)
- '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0)
- '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0)
- '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0)
- '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0)
- '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0)
- '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0)
-
- babel-preset-jest@30.3.0(@babel/core@7.29.0):
- dependencies:
- '@babel/core': 7.29.0
- babel-plugin-jest-hoist: 30.3.0
- babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0)
-
balanced-match@1.0.2: {}
balanced-match@4.0.4: {}
base64-arraybuffer@1.0.2: {}
- base64-js@1.5.1: {}
-
- baseline-browser-mapping@2.10.19: {}
-
- bcrypt-pbkdf@1.0.2:
- dependencies:
- tweetnacl: 0.14.5
+ baseline-browser-mapping@2.10.38: {}
bcryptjs@3.0.3: {}
@@ -9419,9 +8799,9 @@ snapshots:
dependencies:
'@clack/prompts': 0.11.0
- blob-util@2.0.2: {}
-
- bluebird@3.7.2: {}
+ bidi-js@1.0.3:
+ dependencies:
+ require-from-string: 2.0.2
boolbase@1.0.0: {}
@@ -9430,11 +8810,6 @@ snapshots:
balanced-match: 1.0.2
concat-map: 0.0.1
- brace-expansion@1.1.14:
- dependencies:
- balanced-match: 1.0.2
- concat-map: 0.0.1
-
brace-expansion@2.0.2:
dependencies:
balanced-match: 1.0.2
@@ -9447,64 +8822,40 @@ snapshots:
dependencies:
fill-range: 7.1.1
- browserslist@4.25.2:
+ browserslist@4.28.4:
dependencies:
- caniuse-lite: 1.0.30001780
- electron-to-chromium: 1.5.202
- node-releases: 2.0.19
- update-browserslist-db: 1.1.3(browserslist@4.25.2)
-
- browserslist@4.28.2:
- dependencies:
- baseline-browser-mapping: 2.10.19
- caniuse-lite: 1.0.30001788
- electron-to-chromium: 1.5.340
- node-releases: 2.0.37
- update-browserslist-db: 1.2.3(browserslist@4.28.2)
-
- bs-logger@0.2.6:
- dependencies:
- fast-json-stable-stringify: 2.1.0
-
- bser@2.1.1:
- dependencies:
- node-int64: 0.4.0
-
- buffer-crc32@0.2.13: {}
+ baseline-browser-mapping: 2.10.38
+ caniuse-lite: 1.0.30001799
+ electron-to-chromium: 1.5.378
+ node-releases: 2.0.49
+ update-browserslist-db: 1.2.3(browserslist@4.28.4)
buffer-equal-constant-time@1.0.1: {}
buffer-from@1.1.2: {}
- buffer@5.7.1:
- dependencies:
- base64-js: 1.5.1
- ieee754: 1.2.1
-
bundle-require@5.1.0(esbuild@0.27.7):
dependencies:
esbuild: 0.27.7
load-tsconfig: 0.2.5
- c12@3.1.0:
+ c12@3.3.4:
dependencies:
- chokidar: 4.0.3
+ chokidar: 5.0.0
confbox: 0.2.4
defu: 6.1.7
- dotenv: 16.6.1
+ dotenv: 17.4.2
exsolve: 1.0.8
- giget: 2.0.0
- jiti: 2.6.1
+ giget: 3.2.0
+ jiti: 2.7.0
ohash: 2.0.11
pathe: 2.0.3
- perfect-debounce: 1.0.0
- pkg-types: 2.3.0
- rc9: 2.1.2
+ perfect-debounce: 2.1.0
+ pkg-types: 2.3.1
+ rc9: 3.0.1
cac@6.7.14: {}
- cachedir@2.4.0: {}
-
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -9524,22 +8875,18 @@ snapshots:
callsites@3.1.0: {}
- camelcase@5.3.1: {}
-
camelcase@6.3.0: {}
caniuse-api@3.0.0:
dependencies:
- browserslist: 4.25.2
- caniuse-lite: 1.0.30001780
+ browserslist: 4.28.4
+ caniuse-lite: 1.0.30001799
lodash.memoize: 4.1.2
lodash.uniq: 4.5.0
- caniuse-lite@1.0.30001780: {}
+ caniuse-lite@1.0.30001799: {}
- caniuse-lite@1.0.30001788: {}
-
- caseless@0.12.0: {}
+ chai@6.2.2: {}
chalk@2.4.2:
dependencies:
@@ -9554,53 +8901,30 @@ snapshots:
chalk@5.6.2: {}
- char-regex@1.0.2: {}
-
chart.js@4.5.1:
dependencies:
'@kurkle/color': 0.3.4
- chartjs-adapter-date-fns@3.0.0(chart.js@4.5.1)(date-fns@4.1.0):
+ chartjs-adapter-date-fns@3.0.0(chart.js@4.5.1)(date-fns@4.4.0):
dependencies:
chart.js: 4.5.1
- date-fns: 4.1.0
+ date-fns: 4.4.0
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
- chownr@3.0.0: {}
+ chokidar@5.0.0:
+ dependencies:
+ readdirp: 5.0.0
- ci-info@4.3.0: {}
+ chownr@3.0.0: {}
ci-info@4.4.0: {}
- citty@0.1.6:
- dependencies:
- consola: 3.4.2
-
- citty@0.2.2: {}
-
- cjs-module-lexer@2.2.0: {}
-
classnames@2.5.1: {}
- clean-stack@2.2.0: {}
-
- cli-cursor@3.1.0:
- dependencies:
- restore-cursor: 3.1.0
-
- cli-table3@0.6.1:
- dependencies:
- string-width: 4.2.3
- optionalDependencies:
- colors: 1.4.0
-
- cli-truncate@2.1.0:
- dependencies:
- slice-ansi: 3.0.0
- string-width: 4.2.3
+ cli-width@4.1.0: {}
client-only@0.0.1: {}
@@ -9620,12 +8944,8 @@ snapshots:
cluster-key-slot@1.1.2: {}
- co@4.6.0: {}
-
code-block-writer@13.0.3: {}
- collect-v8-coverage@1.0.3: {}
-
color-convert@1.9.3:
dependencies:
color-name: 1.1.3
@@ -9642,27 +8962,14 @@ snapshots:
colorette@1.4.0: {}
- colorette@2.0.20: {}
-
- colors@1.4.0:
- optional: true
-
- combined-stream@1.0.8:
- dependencies:
- delayed-stream: 1.0.0
-
commander@2.20.3: {}
commander@4.1.1: {}
- commander@6.2.1: {}
-
commander@7.2.0: {}
commander@9.5.0: {}
- common-tags@1.8.2: {}
-
commondir@1.0.1: {}
concat-map@0.0.1: {}
@@ -9679,7 +8986,7 @@ snapshots:
convert-source-map@2.0.0: {}
- core-util-is@1.0.2: {}
+ cookie@1.1.1: {}
core-util-is@1.0.3: {}
@@ -9718,29 +9025,29 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
- css-blank-pseudo@8.0.1(postcss@8.5.10):
+ css-blank-pseudo@8.0.1(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
- postcss-selector-parser: 7.1.1
+ postcss: 8.5.15
+ postcss-selector-parser: 7.1.4
css-box-model@1.2.1:
dependencies:
tiny-invariant: 1.3.3
- css-declaration-sorter@6.4.1(postcss@8.5.10):
+ css-declaration-sorter@6.4.1(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- css-has-pseudo@8.0.0(postcss@8.5.10):
+ css-has-pseudo@8.0.0(postcss@8.5.15):
dependencies:
- '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1)
- postcss: 8.5.10
- postcss-selector-parser: 7.1.1
+ '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.4)
+ postcss: 8.5.15
+ postcss-selector-parser: 7.1.4
postcss-value-parser: 4.2.0
- css-prefers-color-scheme@11.0.0(postcss@8.5.10):
+ css-prefers-color-scheme@11.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
css-select@4.3.0:
dependencies:
@@ -9773,54 +9080,61 @@ snapshots:
mdn-data: 2.0.30
source-map-js: 1.2.1
+ css-tree@3.2.1:
+ dependencies:
+ mdn-data: 2.27.1
+ source-map-js: 1.2.1
+
css-what@6.2.2: {}
- cssdb@8.8.0: {}
+ css.escape@1.5.1: {}
+
+ cssdb@8.9.0: {}
cssesc@3.0.0: {}
- cssnano-preset-default@5.2.14(postcss@8.5.10):
+ cssnano-preset-default@5.2.14(postcss@8.5.15):
dependencies:
- css-declaration-sorter: 6.4.1(postcss@8.5.10)
- cssnano-utils: 3.1.0(postcss@8.5.10)
- postcss: 8.5.10
- postcss-calc: 8.2.4(postcss@8.5.10)
- postcss-colormin: 5.3.1(postcss@8.5.10)
- postcss-convert-values: 5.1.3(postcss@8.5.10)
- postcss-discard-comments: 5.1.2(postcss@8.5.10)
- postcss-discard-duplicates: 5.1.0(postcss@8.5.10)
- postcss-discard-empty: 5.1.1(postcss@8.5.10)
- postcss-discard-overridden: 5.1.0(postcss@8.5.10)
- postcss-merge-longhand: 5.1.7(postcss@8.5.10)
- postcss-merge-rules: 5.1.4(postcss@8.5.10)
- postcss-minify-font-values: 5.1.0(postcss@8.5.10)
- postcss-minify-gradients: 5.1.1(postcss@8.5.10)
- postcss-minify-params: 5.1.4(postcss@8.5.10)
- postcss-minify-selectors: 5.2.1(postcss@8.5.10)
- postcss-normalize-charset: 5.1.0(postcss@8.5.10)
- postcss-normalize-display-values: 5.1.0(postcss@8.5.10)
- postcss-normalize-positions: 5.1.1(postcss@8.5.10)
- postcss-normalize-repeat-style: 5.1.1(postcss@8.5.10)
- postcss-normalize-string: 5.1.0(postcss@8.5.10)
- postcss-normalize-timing-functions: 5.1.0(postcss@8.5.10)
- postcss-normalize-unicode: 5.1.1(postcss@8.5.10)
- postcss-normalize-url: 5.1.0(postcss@8.5.10)
- postcss-normalize-whitespace: 5.1.1(postcss@8.5.10)
- postcss-ordered-values: 5.1.3(postcss@8.5.10)
- postcss-reduce-initial: 5.1.2(postcss@8.5.10)
- postcss-reduce-transforms: 5.1.0(postcss@8.5.10)
- postcss-svgo: 5.1.0(postcss@8.5.10)
- postcss-unique-selectors: 5.1.1(postcss@8.5.10)
+ css-declaration-sorter: 6.4.1(postcss@8.5.15)
+ cssnano-utils: 3.1.0(postcss@8.5.15)
+ postcss: 8.5.15
+ postcss-calc: 8.2.4(postcss@8.5.15)
+ postcss-colormin: 5.3.1(postcss@8.5.15)
+ postcss-convert-values: 5.1.3(postcss@8.5.15)
+ postcss-discard-comments: 5.1.2(postcss@8.5.15)
+ postcss-discard-duplicates: 5.1.0(postcss@8.5.15)
+ postcss-discard-empty: 5.1.1(postcss@8.5.15)
+ postcss-discard-overridden: 5.1.0(postcss@8.5.15)
+ postcss-merge-longhand: 5.1.7(postcss@8.5.15)
+ postcss-merge-rules: 5.1.4(postcss@8.5.15)
+ postcss-minify-font-values: 5.1.0(postcss@8.5.15)
+ postcss-minify-gradients: 5.1.1(postcss@8.5.15)
+ postcss-minify-params: 5.1.4(postcss@8.5.15)
+ postcss-minify-selectors: 5.2.1(postcss@8.5.15)
+ postcss-normalize-charset: 5.1.0(postcss@8.5.15)
+ postcss-normalize-display-values: 5.1.0(postcss@8.5.15)
+ postcss-normalize-positions: 5.1.1(postcss@8.5.15)
+ postcss-normalize-repeat-style: 5.1.1(postcss@8.5.15)
+ postcss-normalize-string: 5.1.0(postcss@8.5.15)
+ postcss-normalize-timing-functions: 5.1.0(postcss@8.5.15)
+ postcss-normalize-unicode: 5.1.1(postcss@8.5.15)
+ postcss-normalize-url: 5.1.0(postcss@8.5.15)
+ postcss-normalize-whitespace: 5.1.1(postcss@8.5.15)
+ postcss-ordered-values: 5.1.3(postcss@8.5.15)
+ postcss-reduce-initial: 5.1.2(postcss@8.5.15)
+ postcss-reduce-transforms: 5.1.0(postcss@8.5.15)
+ postcss-svgo: 5.1.0(postcss@8.5.15)
+ postcss-unique-selectors: 5.1.1(postcss@8.5.15)
- cssnano-utils@3.1.0(postcss@8.5.10):
+ cssnano-utils@3.1.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- cssnano@5.1.15(postcss@8.5.10):
+ cssnano@5.1.15(postcss@8.5.15):
dependencies:
- cssnano-preset-default: 5.2.14(postcss@8.5.10)
+ cssnano-preset-default: 5.2.14(postcss@8.5.15)
lilconfig: 2.1.0
- postcss: 8.5.10
+ postcss: 8.5.15
yaml: 1.10.2
csso@4.2.0:
@@ -9833,52 +9147,6 @@ snapshots:
csstype@3.2.3: {}
- cypress@15.14.0:
- dependencies:
- '@cypress/request': 3.0.10
- '@cypress/xvfb': 1.2.4(supports-color@8.1.1)
- '@types/sinonjs__fake-timers': 8.1.1
- '@types/sizzle': 2.3.10
- '@types/tmp': 0.2.6
- arch: 2.2.0
- blob-util: 2.0.2
- bluebird: 3.7.2
- buffer: 5.7.1
- cachedir: 2.4.0
- chalk: 4.1.2
- ci-info: 4.4.0
- cli-cursor: 3.1.0
- cli-table3: 0.6.1
- commander: 6.2.1
- common-tags: 1.8.2
- dayjs: 1.11.20
- debug: 4.4.3(supports-color@8.1.1)
- enquirer: 2.4.1
- eventemitter2: 6.4.7
- execa: 4.1.0
- executable: 4.1.1
- extract-zip: 2.0.1(supports-color@8.1.1)
- figures: 3.2.0
- fs-extra: 9.1.0
- hasha: 5.2.2
- is-installed-globally: 0.4.0
- listr2: 3.14.0(enquirer@2.4.1)
- lodash: 4.18.1
- log-symbols: 4.1.0
- minimist: 1.2.8
- ospath: 1.2.2
- pretty-bytes: 5.6.0
- process: 0.11.10
- proxy-from-env: 1.0.0
- request-progress: 3.0.0
- supports-color: 8.1.1
- systeminformation: 5.31.5
- tmp: 0.2.5
- tree-kill: 1.2.2
- tslib: 1.14.1
- untildify: 4.0.0
- yauzl: 2.10.0
-
d3-array@2.12.1:
dependencies:
internmap: 1.0.1
@@ -9923,14 +9191,17 @@ snapshots:
d3-selection: 2.0.0
d3-transition: 2.0.0(d3-selection@2.0.0)
- dashdash@1.14.1:
- dependencies:
- assert-plus: 1.0.0
-
dashify@2.0.0: {}
data-uri-to-buffer@4.0.1: {}
+ data-urls@7.0.0:
+ dependencies:
+ whatwg-mimetype: 5.0.0
+ whatwg-url: 16.0.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
data-view-buffer@1.0.2:
dependencies:
call-bound: 1.0.4
@@ -9949,29 +9220,20 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.2
- date-fns-tz@3.2.0(date-fns@4.1.0):
+ date-fns-tz@3.2.0(date-fns@4.4.0):
dependencies:
- date-fns: 4.1.0
+ date-fns: 4.4.0
- date-fns@4.1.0: {}
+ date-fns@4.4.0: {}
- dayjs@1.11.20: {}
-
- debug@3.2.7(supports-color@8.1.1):
+ debug@4.4.3:
dependencies:
ms: 2.1.3
- optionalDependencies:
- supports-color: 8.1.1
-
- debug@4.4.3(supports-color@8.1.1):
- dependencies:
- ms: 2.1.3
- optionalDependencies:
- supports-color: 8.1.1
decamelize@1.2.0: {}
dedent@1.7.2: {}
+ decimal.js@10.6.0: {}
deepmerge-ts@7.1.5: {}
@@ -10001,10 +9263,10 @@ snapshots:
presentable-error: 0.0.1
slash: 5.1.0
- delayed-stream@1.0.0: {}
-
denque@2.1.0: {}
+ dequal@2.0.3: {}
+
destr@2.0.5: {}
detect-browser@5.3.0: {}
@@ -10013,8 +9275,6 @@ snapshots:
detect-libc@2.1.2: {}
- detect-newline@3.1.0: {}
-
diff@4.0.2: {}
dijkstrajs@1.0.3: {}
@@ -10023,6 +9283,10 @@ snapshots:
dependencies:
path-type: 4.0.0
+ dom-accessibility-api@0.5.16: {}
+
+ dom-accessibility-api@0.6.3: {}
+
dom-serializer@1.4.1:
dependencies:
domelementtype: 2.3.0
@@ -10085,11 +9349,6 @@ snapshots:
eastasianwidth@0.2.0: {}
- ecc-jsbn@0.1.2:
- dependencies:
- jsbn: 0.1.1
- safer-buffer: 2.1.2
-
ecdsa-sig-formatter@1.0.11:
dependencies:
safe-buffer: 5.2.1
@@ -10099,11 +9358,7 @@ snapshots:
'@standard-schema/spec': 1.1.0
fast-check: 3.23.2
- electron-to-chromium@1.5.202: {}
-
- electron-to-chromium@1.5.340: {}
-
- emittery@0.13.1: {}
+ electron-to-chromium@1.5.378: {}
emoji-regex@8.0.0: {}
@@ -10111,19 +9366,12 @@ snapshots:
empathic@2.0.0: {}
- end-of-stream@1.4.5:
- dependencies:
- once: 1.4.0
-
- enquirer@2.4.1:
- dependencies:
- ansi-colors: 4.1.3
- strip-ansi: 6.0.1
-
entities@2.2.0: {}
entities@4.5.0: {}
+ entities@8.0.0: {}
+
env-paths@3.0.0: {}
error-ex@1.3.2:
@@ -10191,6 +9439,8 @@ snapshots:
es-errors@1.3.0: {}
+ es-module-lexer@2.1.0: {}
+
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
@@ -10237,115 +9487,53 @@ snapshots:
'@esbuild/win32-ia32': 0.27.7
'@esbuild/win32-x64': 0.27.7
- esbuild@0.28.0:
+ esbuild@0.28.1:
optionalDependencies:
- '@esbuild/aix-ppc64': 0.28.0
- '@esbuild/android-arm': 0.28.0
- '@esbuild/android-arm64': 0.28.0
- '@esbuild/android-x64': 0.28.0
- '@esbuild/darwin-arm64': 0.28.0
- '@esbuild/darwin-x64': 0.28.0
- '@esbuild/freebsd-arm64': 0.28.0
- '@esbuild/freebsd-x64': 0.28.0
- '@esbuild/linux-arm': 0.28.0
- '@esbuild/linux-arm64': 0.28.0
- '@esbuild/linux-ia32': 0.28.0
- '@esbuild/linux-loong64': 0.28.0
- '@esbuild/linux-mips64el': 0.28.0
- '@esbuild/linux-ppc64': 0.28.0
- '@esbuild/linux-riscv64': 0.28.0
- '@esbuild/linux-s390x': 0.28.0
- '@esbuild/linux-x64': 0.28.0
- '@esbuild/netbsd-arm64': 0.28.0
- '@esbuild/netbsd-x64': 0.28.0
- '@esbuild/openbsd-arm64': 0.28.0
- '@esbuild/openbsd-x64': 0.28.0
- '@esbuild/openharmony-arm64': 0.28.0
- '@esbuild/sunos-x64': 0.28.0
- '@esbuild/win32-arm64': 0.28.0
- '@esbuild/win32-ia32': 0.28.0
- '@esbuild/win32-x64': 0.28.0
+ '@esbuild/aix-ppc64': 0.28.1
+ '@esbuild/android-arm': 0.28.1
+ '@esbuild/android-arm64': 0.28.1
+ '@esbuild/android-x64': 0.28.1
+ '@esbuild/darwin-arm64': 0.28.1
+ '@esbuild/darwin-x64': 0.28.1
+ '@esbuild/freebsd-arm64': 0.28.1
+ '@esbuild/freebsd-x64': 0.28.1
+ '@esbuild/linux-arm': 0.28.1
+ '@esbuild/linux-arm64': 0.28.1
+ '@esbuild/linux-ia32': 0.28.1
+ '@esbuild/linux-loong64': 0.28.1
+ '@esbuild/linux-mips64el': 0.28.1
+ '@esbuild/linux-ppc64': 0.28.1
+ '@esbuild/linux-riscv64': 0.28.1
+ '@esbuild/linux-s390x': 0.28.1
+ '@esbuild/linux-x64': 0.28.1
+ '@esbuild/netbsd-arm64': 0.28.1
+ '@esbuild/netbsd-x64': 0.28.1
+ '@esbuild/openbsd-arm64': 0.28.1
+ '@esbuild/openbsd-x64': 0.28.1
+ '@esbuild/openharmony-arm64': 0.28.1
+ '@esbuild/sunos-x64': 0.28.1
+ '@esbuild/win32-arm64': 0.28.1
+ '@esbuild/win32-ia32': 0.28.1
+ '@esbuild/win32-x64': 0.28.1
escalade@3.2.0: {}
escape-string-regexp@1.0.5: {}
- escape-string-regexp@2.0.0: {}
-
- esprima@4.0.1: {}
-
estree-walker@0.6.1: {}
estree-walker@2.0.2: {}
- eventemitter2@6.4.7: {}
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
eventemitter3@4.0.7: {}
- execa@4.1.0:
- dependencies:
- cross-spawn: 7.0.6
- get-stream: 5.2.0
- human-signals: 1.1.1
- is-stream: 2.0.1
- merge-stream: 2.0.0
- npm-run-path: 4.0.1
- onetime: 5.1.2
- signal-exit: 3.0.7
- strip-final-newline: 2.0.0
-
- execa@5.1.1:
- dependencies:
- cross-spawn: 7.0.6
- get-stream: 6.0.1
- human-signals: 2.1.0
- is-stream: 2.0.1
- merge-stream: 2.0.0
- npm-run-path: 4.0.1
- onetime: 5.1.2
- signal-exit: 3.0.7
- strip-final-newline: 2.0.0
-
- executable@4.1.1:
- dependencies:
- pify: 2.3.0
-
- exit-x@0.2.2: {}
-
- expect@30.1.1:
- dependencies:
- '@jest/expect-utils': 30.1.1
- '@jest/get-type': 30.1.0
- jest-matcher-utils: 30.1.1
- jest-message-util: 30.1.0
- jest-mock: 30.0.5
- jest-util: 30.0.5
-
- expect@30.3.0:
- dependencies:
- '@jest/expect-utils': 30.3.0
- '@jest/get-type': 30.1.0
- jest-matcher-utils: 30.3.0
- jest-message-util: 30.3.0
- jest-mock: 30.3.0
- jest-util: 30.3.0
+ expect-type@1.3.0: {}
exsolve@1.0.8: {}
- extend@3.0.2: {}
-
- extract-zip@2.0.1(supports-color@8.1.1):
- dependencies:
- debug: 4.4.3(supports-color@8.1.1)
- get-stream: 5.2.0
- yauzl: 2.10.0
- optionalDependencies:
- '@types/yauzl': 2.10.3
- transitivePeerDependencies:
- - supports-color
-
- extsprintf@1.3.0: {}
-
fast-check@3.23.2:
dependencies:
pure-rand: 6.1.0
@@ -10360,22 +9548,22 @@ snapshots:
merge2: 1.4.1
micromatch: 4.0.8
- fast-json-stable-stringify@2.1.0: {}
+ fast-string-truncated-width@3.0.3: {}
+
+ fast-string-width@3.0.2:
+ dependencies:
+ fast-string-truncated-width: 3.0.3
fast-uri@3.1.0: {}
+ fast-wrap-ansi@0.2.0:
+ dependencies:
+ fast-string-width: 3.0.2
+
fastq@1.19.1:
dependencies:
reusify: 1.1.0
- fb-watchman@2.0.2:
- dependencies:
- bser: 2.1.1
-
- fd-slicer@1.1.0:
- dependencies:
- pend: 1.2.0
-
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.4
@@ -10387,24 +9575,15 @@ snapshots:
fflate@0.4.8: {}
- figures@3.2.0:
- dependencies:
- escape-string-regexp: 1.0.5
-
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
- find-up@4.1.0:
- dependencies:
- locate-path: 5.0.0
- path-exists: 4.0.0
-
fix-dts-default-cjs-exports@1.0.1:
dependencies:
magic-string: 0.30.21
mlly: 1.8.0
- rollup: 4.60.1
+ rollup: 4.62.2
for-each@0.3.5:
dependencies:
@@ -10415,30 +9594,20 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
- forever-agent@0.6.1: {}
-
- form-data@4.0.5:
- dependencies:
- asynckit: 0.4.0
- combined-stream: 1.0.8
- es-set-tostringtag: 2.1.0
- hasown: 2.0.3
- mime-types: 2.1.35
-
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
fraction.js@5.3.4: {}
- framer-motion@12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ framer-motion@12.41.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
- motion-dom: 12.38.0
- motion-utils: 12.36.0
+ motion-dom: 12.41.0
+ motion-utils: 12.39.0
tslib: 2.8.1
optionalDependencies:
- react: 19.2.5
- react-dom: 19.2.5(react@19.2.5)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
fs-extra@8.1.0:
dependencies:
@@ -10446,15 +9615,11 @@ snapshots:
jsonfile: 4.0.0
universalify: 0.1.2
- fs-extra@9.1.0:
- dependencies:
- at-least-node: 1.0.0
- graceful-fs: 4.2.11
- jsonfile: 6.2.0
- universalify: 2.0.1
-
fs.realpath@1.0.0: {}
+ fsevents@2.3.2:
+ optional: true
+
fsevents@2.3.3:
optional: true
@@ -10496,8 +9661,6 @@ snapshots:
hasown: 2.0.2
math-intrinsics: 1.1.0
- get-package-type@0.1.0: {}
-
get-port-please@3.2.0: {}
get-proto@1.0.1:
@@ -10505,34 +9668,13 @@ snapshots:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
- get-stream@5.2.0:
- dependencies:
- pump: 3.0.4
-
- get-stream@6.0.1: {}
-
get-symbol-description@1.1.0:
dependencies:
call-bound: 1.0.4
es-errors: 1.3.0
get-intrinsic: 1.3.0
- get-tsconfig@4.13.0:
- dependencies:
- resolve-pkg-maps: 1.0.0
-
- getpass@0.1.7:
- dependencies:
- assert-plus: 1.0.0
-
- giget@2.0.0:
- dependencies:
- citty: 0.1.6
- consola: 3.4.2
- defu: 6.1.7
- node-fetch-native: 1.6.7
- nypm: 0.6.5
- pathe: 2.0.3
+ giget@3.2.0: {}
glob-parent@5.1.2:
dependencies:
@@ -10564,10 +9706,6 @@ snapshots:
minimatch: 5.1.6
once: 1.4.0
- global-dirs@3.0.1:
- dependencies:
- ini: 2.0.0
-
globalthis@1.0.4:
dependencies:
define-properties: 1.2.1
@@ -10601,14 +9739,7 @@ snapshots:
graphmatch@1.1.1: {}
- handlebars@4.7.9:
- dependencies:
- minimist: 1.2.8
- neo-async: 2.6.2
- source-map: 0.6.1
- wordwrap: 1.0.0
- optionalDependencies:
- uglify-js: 3.19.3
+ graphql@16.14.0: {}
has-bigints@1.1.0: {}
@@ -10630,52 +9761,40 @@ snapshots:
dependencies:
has-symbols: 1.1.0
- hasha@5.2.2:
- dependencies:
- is-stream: 2.0.1
- type-fest: 0.8.1
-
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
- hasown@2.0.3:
+ headers-polyfill@5.0.1:
dependencies:
- function-bind: 1.1.2
+ '@types/set-cookie-parser': 2.4.10
+ set-cookie-parser: 3.1.0
- hono@4.12.9: {}
+ hono@4.12.21: {}
hosted-git-info@2.8.9: {}
- html-escaper@2.0.2: {}
-
- http-signature@1.4.0:
+ html-encoding-sniffer@6.0.0:
dependencies:
- assert-plus: 1.0.0
- jsprim: 2.0.2
- sshpk: 1.18.0
+ '@exodus/bytes': 1.15.0
+ transitivePeerDependencies:
+ - '@noble/hashes'
http-status-codes@2.3.0: {}
- human-signals@1.1.1: {}
-
- human-signals@2.1.0: {}
-
iconv-lite@0.7.2:
dependencies:
safer-buffer: 2.1.2
icss-replace-symbols@1.1.0: {}
- icss-utils@5.1.0(postcss@8.5.10):
+ icss-utils@5.1.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- icu-minify@4.9.1:
+ icu-minify@4.13.0:
dependencies:
- '@formatjs/icu-messageformat-parser': 3.5.4
-
- ieee754@1.2.1: {}
+ '@formatjs/icu-messageformat-parser': 3.5.11
ignore@5.3.2: {}
@@ -10683,7 +9802,7 @@ snapshots:
immediate@3.0.6: {}
- immer@11.1.4: {}
+ immer@11.1.8: {}
import-cwd@3.0.0:
dependencies:
@@ -10698,13 +9817,6 @@ snapshots:
dependencies:
resolve-from: 5.0.0
- import-local@3.2.0:
- dependencies:
- pkg-dir: 4.2.0
- resolve-cwd: 3.0.0
-
- imurmurhash@0.1.4: {}
-
indent-string@4.0.0: {}
inflight@1.0.6:
@@ -10714,8 +9826,6 @@ snapshots:
inherits@2.0.4: {}
- ini@2.0.0: {}
-
internal-slot@1.1.0:
dependencies:
es-errors: 1.3.0
@@ -10724,12 +9834,12 @@ snapshots:
internmap@1.0.1: {}
- intl-messageformat@11.2.1:
+ intl-messageformat@11.2.8:
dependencies:
- '@formatjs/fast-memoize': 3.1.2
- '@formatjs/icu-messageformat-parser': 3.5.4
+ '@formatjs/fast-memoize': 3.1.6
+ '@formatjs/icu-messageformat-parser': 3.5.11
- ipaddr.js@2.3.0: {}
+ ipaddr.js@2.4.0: {}
is-array-buffer@3.0.5:
dependencies:
@@ -10787,8 +9897,6 @@ snapshots:
is-fullwidth-code-point@3.0.0: {}
- is-generator-fn@2.1.0: {}
-
is-generator-function@1.1.0:
dependencies:
call-bound: 1.0.4
@@ -10800,11 +9908,6 @@ snapshots:
dependencies:
is-extglob: 2.1.1
- is-installed-globally@0.4.0:
- dependencies:
- global-dirs: 3.0.1
- is-path-inside: 3.0.3
-
is-localhost-ip@3.0.1: {}
is-map@2.0.3: {}
@@ -10813,6 +9916,8 @@ snapshots:
is-negative-zero@2.0.3: {}
+ is-node-process@1.2.0: {}
+
is-number-object@1.1.1:
dependencies:
call-bound: 1.0.4
@@ -10822,12 +9927,12 @@ snapshots:
is-path-cwd@3.0.0: {}
- is-path-inside@3.0.3: {}
-
is-path-inside@4.0.0: {}
is-plain-object@3.0.1: {}
+ is-potential-custom-element-name@1.0.1: {}
+
is-property@1.0.2: {}
is-reference@1.2.1:
@@ -10849,8 +9954,6 @@ snapshots:
is-standalone-pwa@0.1.1: {}
- is-stream@2.0.1: {}
-
is-string@1.1.1:
dependencies:
call-bound: 1.0.4
@@ -10866,10 +9969,6 @@ snapshots:
dependencies:
which-typed-array: 1.1.19
- is-typedarray@1.0.0: {}
-
- is-unicode-supported@0.1.0: {}
-
is-weakmap@2.0.2: {}
is-weakref@1.1.1:
@@ -10885,417 +9984,51 @@ snapshots:
isarray@2.0.5: {}
- isbot@5.1.39: {}
+ isbot@5.1.44: {}
isexe@2.0.0: {}
- isstream@0.1.2: {}
-
- istanbul-lib-coverage@3.2.2: {}
-
- istanbul-lib-instrument@6.0.3:
- dependencies:
- '@babel/core': 7.29.0
- '@babel/parser': 7.29.2
- '@istanbuljs/schema': 0.1.6
- istanbul-lib-coverage: 3.2.2
- semver: 7.7.4
- transitivePeerDependencies:
- - supports-color
-
- istanbul-lib-report@3.0.1:
- dependencies:
- istanbul-lib-coverage: 3.2.2
- make-dir: 4.0.0
- supports-color: 7.2.0
-
- istanbul-lib-source-maps@5.0.6:
- dependencies:
- '@jridgewell/trace-mapping': 0.3.31
- debug: 4.4.3(supports-color@8.1.1)
- istanbul-lib-coverage: 3.2.2
- transitivePeerDependencies:
- - supports-color
-
- istanbul-reports@3.2.0:
- dependencies:
- html-escaper: 2.0.2
- istanbul-lib-report: 3.0.1
-
jackspeak@3.4.3:
dependencies:
'@isaacs/cliui': 8.0.2
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
- jest-changed-files@30.3.0:
- dependencies:
- execa: 5.1.1
- jest-util: 30.3.0
- p-limit: 3.1.0
-
- jest-circus@30.3.0:
- dependencies:
- '@jest/environment': 30.3.0
- '@jest/expect': 30.3.0
- '@jest/test-result': 30.3.0
- '@jest/types': 30.3.0
- '@types/node': 25.6.0
- chalk: 4.1.2
- co: 4.6.0
- dedent: 1.7.2
- is-generator-fn: 2.1.0
- jest-each: 30.3.0
- jest-matcher-utils: 30.3.0
- jest-message-util: 30.3.0
- jest-runtime: 30.3.0
- jest-snapshot: 30.3.0
- jest-util: 30.3.0
- p-limit: 3.1.0
- pretty-format: 30.3.0
- pure-rand: 7.0.1
- slash: 3.0.0
- stack-utils: 2.0.6
- transitivePeerDependencies:
- - babel-plugin-macros
- - supports-color
-
- jest-cli@30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)):
- dependencies:
- '@jest/core': 30.3.0(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3))
- '@jest/test-result': 30.3.0
- '@jest/types': 30.3.0
- chalk: 4.1.2
- exit-x: 0.2.2
- import-local: 3.2.0
- jest-config: 30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3))
- jest-util: 30.3.0
- jest-validate: 30.3.0
- yargs: 17.7.2
- transitivePeerDependencies:
- - '@types/node'
- - babel-plugin-macros
- - esbuild-register
- - supports-color
- - ts-node
-
- jest-config@30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)):
- dependencies:
- '@babel/core': 7.29.0
- '@jest/get-type': 30.1.0
- '@jest/pattern': 30.0.1
- '@jest/test-sequencer': 30.3.0
- '@jest/types': 30.3.0
- babel-jest: 30.3.0(@babel/core@7.29.0)
- chalk: 4.1.2
- ci-info: 4.4.0
- deepmerge: 4.3.1
- glob: 10.5.0
- graceful-fs: 4.2.11
- jest-circus: 30.3.0
- jest-docblock: 30.2.0
- jest-environment-node: 30.3.0
- jest-regex-util: 30.0.1
- jest-resolve: 30.3.0
- jest-runner: 30.3.0
- jest-util: 30.3.0
- jest-validate: 30.3.0
- parse-json: 5.2.0
- pretty-format: 30.3.0
- slash: 3.0.0
- strip-json-comments: 3.1.1
- optionalDependencies:
- '@types/node': 25.6.0
- ts-node: 10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)
- transitivePeerDependencies:
- - babel-plugin-macros
- - supports-color
-
- jest-diff@30.1.1:
- dependencies:
- '@jest/diff-sequences': 30.0.1
- '@jest/get-type': 30.1.0
- chalk: 4.1.2
- pretty-format: 30.0.5
-
- jest-diff@30.3.0:
- dependencies:
- '@jest/diff-sequences': 30.3.0
- '@jest/get-type': 30.1.0
- chalk: 4.1.2
- pretty-format: 30.3.0
-
- jest-docblock@30.2.0:
- dependencies:
- detect-newline: 3.1.0
-
- jest-each@30.3.0:
- dependencies:
- '@jest/get-type': 30.1.0
- '@jest/types': 30.3.0
- chalk: 4.1.2
- jest-util: 30.3.0
- pretty-format: 30.3.0
-
- jest-environment-node@30.3.0:
- dependencies:
- '@jest/environment': 30.3.0
- '@jest/fake-timers': 30.3.0
- '@jest/types': 30.3.0
- '@types/node': 25.6.0
- jest-mock: 30.3.0
- jest-util: 30.3.0
- jest-validate: 30.3.0
-
- jest-haste-map@30.3.0:
- dependencies:
- '@jest/types': 30.3.0
- '@types/node': 25.6.0
- anymatch: 3.1.3
- fb-watchman: 2.0.2
- graceful-fs: 4.2.11
- jest-regex-util: 30.0.1
- jest-util: 30.3.0
- jest-worker: 30.3.0
- picomatch: 4.0.4
- walker: 1.0.8
- optionalDependencies:
- fsevents: 2.3.3
-
- jest-leak-detector@30.3.0:
- dependencies:
- '@jest/get-type': 30.1.0
- pretty-format: 30.3.0
-
- jest-matcher-utils@30.1.1:
- dependencies:
- '@jest/get-type': 30.1.0
- chalk: 4.1.2
- jest-diff: 30.1.1
- pretty-format: 30.0.5
-
- jest-matcher-utils@30.3.0:
- dependencies:
- '@jest/get-type': 30.1.0
- chalk: 4.1.2
- jest-diff: 30.3.0
- pretty-format: 30.3.0
-
- jest-message-util@30.1.0:
- dependencies:
- '@babel/code-frame': 7.27.1
- '@jest/types': 30.0.5
- '@types/stack-utils': 2.0.3
- chalk: 4.1.2
- graceful-fs: 4.2.11
- micromatch: 4.0.8
- pretty-format: 30.0.5
- slash: 3.0.0
- stack-utils: 2.0.6
-
- jest-message-util@30.3.0:
- dependencies:
- '@babel/code-frame': 7.29.0
- '@jest/types': 30.3.0
- '@types/stack-utils': 2.0.3
- chalk: 4.1.2
- graceful-fs: 4.2.11
- picomatch: 4.0.4
- pretty-format: 30.3.0
- slash: 3.0.0
- stack-utils: 2.0.6
-
- jest-mock@30.0.5:
- dependencies:
- '@jest/types': 30.0.5
- '@types/node': 25.6.0
- jest-util: 30.0.5
-
- jest-mock@30.3.0:
- dependencies:
- '@jest/types': 30.3.0
- '@types/node': 25.6.0
- jest-util: 30.3.0
-
- jest-pnp-resolver@1.2.3(jest-resolve@30.3.0):
- optionalDependencies:
- jest-resolve: 30.3.0
-
- jest-regex-util@30.0.1: {}
-
- jest-resolve-dependencies@30.3.0:
- dependencies:
- jest-regex-util: 30.0.1
- jest-snapshot: 30.3.0
- transitivePeerDependencies:
- - supports-color
-
- jest-resolve@30.3.0:
- dependencies:
- chalk: 4.1.2
- graceful-fs: 4.2.11
- jest-haste-map: 30.3.0
- jest-pnp-resolver: 1.2.3(jest-resolve@30.3.0)
- jest-util: 30.3.0
- jest-validate: 30.3.0
- slash: 3.0.0
- unrs-resolver: 1.11.1
-
- jest-runner@30.3.0:
- dependencies:
- '@jest/console': 30.3.0
- '@jest/environment': 30.3.0
- '@jest/test-result': 30.3.0
- '@jest/transform': 30.3.0
- '@jest/types': 30.3.0
- '@types/node': 25.6.0
- chalk: 4.1.2
- emittery: 0.13.1
- exit-x: 0.2.2
- graceful-fs: 4.2.11
- jest-docblock: 30.2.0
- jest-environment-node: 30.3.0
- jest-haste-map: 30.3.0
- jest-leak-detector: 30.3.0
- jest-message-util: 30.3.0
- jest-resolve: 30.3.0
- jest-runtime: 30.3.0
- jest-util: 30.3.0
- jest-watcher: 30.3.0
- jest-worker: 30.3.0
- p-limit: 3.1.0
- source-map-support: 0.5.13
- transitivePeerDependencies:
- - supports-color
-
- jest-runtime@30.3.0:
- dependencies:
- '@jest/environment': 30.3.0
- '@jest/fake-timers': 30.3.0
- '@jest/globals': 30.3.0
- '@jest/source-map': 30.0.1
- '@jest/test-result': 30.3.0
- '@jest/transform': 30.3.0
- '@jest/types': 30.3.0
- '@types/node': 25.6.0
- chalk: 4.1.2
- cjs-module-lexer: 2.2.0
- collect-v8-coverage: 1.0.3
- glob: 10.5.0
- graceful-fs: 4.2.11
- jest-haste-map: 30.3.0
- jest-message-util: 30.3.0
- jest-mock: 30.3.0
- jest-regex-util: 30.0.1
- jest-resolve: 30.3.0
- jest-snapshot: 30.3.0
- jest-util: 30.3.0
- slash: 3.0.0
- strip-bom: 4.0.0
- transitivePeerDependencies:
- - supports-color
-
- jest-snapshot@30.3.0:
- dependencies:
- '@babel/core': 7.29.0
- '@babel/generator': 7.29.1
- '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
- '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
- '@babel/types': 7.29.0
- '@jest/expect-utils': 30.3.0
- '@jest/get-type': 30.1.0
- '@jest/snapshot-utils': 30.3.0
- '@jest/transform': 30.3.0
- '@jest/types': 30.3.0
- babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0)
- chalk: 4.1.2
- expect: 30.3.0
- graceful-fs: 4.2.11
- jest-diff: 30.3.0
- jest-matcher-utils: 30.3.0
- jest-message-util: 30.3.0
- jest-util: 30.3.0
- pretty-format: 30.3.0
- semver: 7.7.4
- synckit: 0.11.12
- transitivePeerDependencies:
- - supports-color
-
- jest-util@30.0.5:
- dependencies:
- '@jest/types': 30.0.5
- '@types/node': 25.6.0
- chalk: 4.1.2
- ci-info: 4.3.0
- graceful-fs: 4.2.11
- picomatch: 4.0.3
-
- jest-util@30.3.0:
- dependencies:
- '@jest/types': 30.3.0
- '@types/node': 25.6.0
- chalk: 4.1.2
- ci-info: 4.4.0
- graceful-fs: 4.2.11
- picomatch: 4.0.4
-
- jest-validate@30.3.0:
- dependencies:
- '@jest/get-type': 30.1.0
- '@jest/types': 30.3.0
- camelcase: 6.3.0
- chalk: 4.1.2
- leven: 3.1.0
- pretty-format: 30.3.0
-
- jest-watcher@30.3.0:
- dependencies:
- '@jest/test-result': 30.3.0
- '@jest/types': 30.3.0
- '@types/node': 25.6.0
- ansi-escapes: 4.3.2
- chalk: 4.1.2
- emittery: 0.13.1
- jest-util: 30.3.0
- string-length: 4.0.2
-
- jest-worker@30.3.0:
- dependencies:
- '@types/node': 25.6.0
- '@ungap/structured-clone': 1.3.0
- jest-util: 30.3.0
- merge-stream: 2.0.0
- supports-color: 8.1.1
-
- jest@30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)):
- dependencies:
- '@jest/core': 30.3.0(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3))
- '@jest/types': 30.3.0
- import-local: 3.2.0
- jest-cli: 30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3))
- transitivePeerDependencies:
- - '@types/node'
- - babel-plugin-macros
- - esbuild-register
- - supports-color
- - ts-node
-
- jiti@2.6.1: {}
+ jiti@2.7.0: {}
joycon@3.1.1: {}
js-tokens@4.0.0: {}
- js-yaml@3.14.2:
- dependencies:
- argparse: 1.0.10
- esprima: 4.0.1
-
js-yaml@4.1.0:
dependencies:
argparse: 2.0.1
- jsbn@0.1.1: {}
+ jsdom@29.1.1:
+ dependencies:
+ '@asamuzakjp/css-color': 5.1.11
+ '@asamuzakjp/dom-selector': 7.1.1
+ '@bramus/specificity': 2.4.2
+ '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1)
+ '@exodus/bytes': 1.15.0
+ css-tree: 3.2.1
+ data-urls: 7.0.0
+ decimal.js: 10.6.0
+ html-encoding-sniffer: 6.0.0
+ is-potential-custom-element-name: 1.0.1
+ lru-cache: 11.3.6
+ parse5: 8.0.1
+ saxes: 6.0.0
+ symbol-tree: 3.2.4
+ tough-cookie: 6.0.1
+ undici: 7.25.0
+ w3c-xmlserializer: 5.0.0
+ webidl-conversions: 8.0.1
+ whatwg-mimetype: 5.0.0
+ whatwg-url: 16.0.1
+ xml-name-validator: 5.0.0
+ transitivePeerDependencies:
+ - '@noble/hashes'
jsesc@3.1.0: {}
@@ -11305,22 +10038,12 @@ snapshots:
json-schema-traverse@1.0.0: {}
- json-schema@0.4.0: {}
-
- json-stringify-safe@5.0.1: {}
-
json5@2.2.3: {}
jsonfile@4.0.0:
optionalDependencies:
graceful-fs: 4.2.11
- jsonfile@6.2.0:
- dependencies:
- universalify: 2.0.1
- optionalDependencies:
- graceful-fs: 4.2.11
-
jsonwebtoken@9.0.3:
dependencies:
jws: 4.0.1
@@ -11332,14 +10055,7 @@ snapshots:
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
- semver: 7.7.4
-
- jsprim@2.0.2:
- dependencies:
- assert-plus: 1.0.0
- extsprintf: 1.3.0
- json-schema: 0.4.0
- verror: 1.10.0
+ semver: 7.8.5
jszip@3.10.1:
dependencies:
@@ -11363,31 +10079,65 @@ snapshots:
kleur@3.0.3: {}
- leven@3.1.0: {}
-
lie@3.3.0:
dependencies:
immediate: 3.0.6
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.32.0
+ lightningcss-darwin-arm64: 1.32.0
+ lightningcss-darwin-x64: 1.32.0
+ lightningcss-freebsd-x64: 1.32.0
+ lightningcss-linux-arm-gnueabihf: 1.32.0
+ lightningcss-linux-arm64-gnu: 1.32.0
+ lightningcss-linux-arm64-musl: 1.32.0
+ lightningcss-linux-x64-gnu: 1.32.0
+ lightningcss-linux-x64-musl: 1.32.0
+ lightningcss-win32-arm64-msvc: 1.32.0
+ lightningcss-win32-x64-msvc: 1.32.0
+
lilconfig@2.1.0: {}
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
- listr2@3.14.0(enquirer@2.4.1):
- dependencies:
- cli-truncate: 2.1.0
- colorette: 2.0.20
- log-update: 4.0.0
- p-map: 4.0.0
- rfdc: 1.4.1
- rxjs: 7.8.2
- through: 2.3.8
- wrap-ansi: 7.0.0
- optionalDependencies:
- enquirer: 2.4.1
-
load-json-file@4.0.0:
dependencies:
graceful-fs: 4.2.11
@@ -11399,10 +10149,6 @@ snapshots:
loader-utils@3.3.1: {}
- locate-path@5.0.0:
- dependencies:
- p-locate: 4.1.0
-
lodash.camelcase@4.3.0: {}
lodash.includes@4.3.0: {}
@@ -11423,20 +10169,6 @@ snapshots:
lodash.uniq@4.5.0: {}
- lodash@4.18.1: {}
-
- log-symbols@4.1.0:
- dependencies:
- chalk: 4.1.2
- is-unicode-supported: 0.1.0
-
- log-update@4.0.0:
- dependencies:
- ansi-escapes: 4.3.2
- cli-cursor: 3.1.0
- slice-ansi: 4.0.0
- wrap-ansi: 6.2.0
-
long@5.3.2: {}
loose-envify@1.4.0:
@@ -11449,6 +10181,8 @@ snapshots:
lru-cache@10.4.3: {}
+ lru-cache@11.3.6: {}
+
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
@@ -11456,27 +10190,22 @@ snapshots:
lru.min@1.1.4: {}
lucide-react@0.555.0(react@19.2.5):
+ lucide-react@0.555.0(react@19.2.7):
dependencies:
- react: 19.2.5
+ react: 19.2.7
- lucide-react@1.8.0(react@19.2.5):
+ lucide-react@1.21.0(react@19.2.7):
dependencies:
- react: 19.2.5
+ react: 19.2.7
+
+ lz-string@1.5.0: {}
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
- make-dir@4.0.0:
- dependencies:
- semver: 7.7.4
-
make-error@1.3.6: {}
- makeerror@1.0.12:
- dependencies:
- tmpl: 1.0.5
-
math-intrinsics@1.1.0: {}
maxmind@5.0.6:
@@ -11490,9 +10219,9 @@ snapshots:
mdn-data@2.0.30: {}
- memorystream@0.3.1: {}
+ mdn-data@2.27.1: {}
- merge-stream@2.0.0: {}
+ memorystream@0.3.1: {}
merge2@1.4.1: {}
@@ -11501,13 +10230,7 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.2
- mime-db@1.52.0: {}
-
- mime-types@2.1.35:
- dependencies:
- mime-db: 1.52.0
-
- mimic-fn@2.1.0: {}
+ min-indent@1.0.1: {}
minimatch@10.2.5:
dependencies:
@@ -11517,10 +10240,6 @@ snapshots:
dependencies:
brace-expansion: 1.1.12
- minimatch@3.1.5:
- dependencies:
- brace-expansion: 1.1.14
-
minimatch@5.1.6:
dependencies:
brace-expansion: 2.0.2
@@ -11548,22 +10267,49 @@ snapshots:
mmdb-lib@3.0.2: {}
- motion-dom@12.38.0:
+ motion-dom@12.41.0:
dependencies:
- motion-utils: 12.36.0
+ motion-utils: 12.39.0
- motion-utils@12.36.0: {}
+ motion-utils@12.39.0: {}
- motion@12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ motion@12.41.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
- framer-motion: 12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ framer-motion: 12.41.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
tslib: 2.8.1
optionalDependencies:
- react: 19.2.5
- react-dom: 19.2.5(react@19.2.5)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
ms@2.1.3: {}
+ msw@2.14.6(@types/node@26.0.1)(typescript@6.0.3):
+ dependencies:
+ '@inquirer/confirm': 6.0.12(@types/node@26.0.1)
+ '@mswjs/interceptors': 0.41.8
+ '@open-draft/deferred-promise': 3.0.0
+ '@types/statuses': 2.0.6
+ cookie: 1.1.1
+ graphql: 16.14.0
+ headers-polyfill: 5.0.1
+ is-node-process: 1.2.0
+ outvariant: 1.4.3
+ path-to-regexp: 6.3.0
+ picocolors: 1.1.1
+ rettime: 0.11.11
+ statuses: 2.0.2
+ strict-event-emitter: 0.5.1
+ tough-cookie: 6.0.1
+ type-fest: 5.6.0
+ until-async: 3.0.2
+ yargs: 17.7.2
+ optionalDependencies:
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - '@types/node'
+
+ mute-stream@3.0.0: {}
+
mysql2@3.15.3:
dependencies:
aws-ssl-profiles: 1.1.2
@@ -11586,54 +10332,51 @@ snapshots:
dependencies:
lru.min: 1.1.4
- nanoid@3.3.11: {}
+ nanoid@3.3.12: {}
- napi-postinstall@0.3.4: {}
-
- natural-compare@1.4.0: {}
+ nanoid@3.3.15: {}
negotiator@1.0.0: {}
- neo-async@2.6.2: {}
+ next-intl-swc-plugin-extractor@4.13.0: {}
- next-intl-swc-plugin-extractor@4.9.1: {}
-
- next-intl@4.9.1(@swc/helpers@0.5.21)(next@16.2.4(@babel/core@7.29.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.3):
+ next-intl@4.13.0(@swc/helpers@0.5.23)(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3):
dependencies:
- '@formatjs/intl-localematcher': 0.8.3
+ '@formatjs/intl-localematcher': 0.8.10
'@parcel/watcher': 2.5.6
- '@swc/core': 1.15.26(@swc/helpers@0.5.21)
- icu-minify: 4.9.1
+ '@swc/core': 1.15.43(@swc/helpers@0.5.23)
+ icu-minify: 4.13.0
negotiator: 1.0.0
- next: 16.2.4(@babel/core@7.29.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- next-intl-swc-plugin-extractor: 4.9.1
+ next: 16.2.9(@babel/core@7.29.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ next-intl-swc-plugin-extractor: 4.13.0
po-parser: 2.1.1
- react: 19.2.5
- use-intl: 4.9.1(react@19.2.5)
+ react: 19.2.7
+ use-intl: 4.13.0(react@19.2.7)
optionalDependencies:
typescript: 6.0.3
transitivePeerDependencies:
- '@swc/helpers'
- next@16.2.4(@babel/core@7.29.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
- '@next/env': 16.2.4
+ '@next/env': 16.2.9
'@swc/helpers': 0.5.15
- baseline-browser-mapping: 2.10.19
- caniuse-lite: 1.0.30001788
+ baseline-browser-mapping: 2.10.38
+ caniuse-lite: 1.0.30001799
postcss: 8.4.31
- react: 19.2.5
- react-dom: 19.2.5(react@19.2.5)
- styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.5)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.7)
optionalDependencies:
- '@next/swc-darwin-arm64': 16.2.4
- '@next/swc-darwin-x64': 16.2.4
- '@next/swc-linux-arm64-gnu': 16.2.4
- '@next/swc-linux-arm64-musl': 16.2.4
- '@next/swc-linux-x64-gnu': 16.2.4
- '@next/swc-linux-x64-musl': 16.2.4
- '@next/swc-win32-arm64-msvc': 16.2.4
- '@next/swc-win32-x64-msvc': 16.2.4
+ '@next/swc-darwin-arm64': 16.2.9
+ '@next/swc-darwin-x64': 16.2.9
+ '@next/swc-linux-arm64-gnu': 16.2.9
+ '@next/swc-linux-arm64-musl': 16.2.9
+ '@next/swc-linux-x64-gnu': 16.2.9
+ '@next/swc-linux-x64-musl': 16.2.9
+ '@next/swc-win32-arm64-msvc': 16.2.9
+ '@next/swc-win32-x64-msvc': 16.2.9
+ '@playwright/test': 1.61.1
babel-plugin-react-compiler: 19.1.0-rc.2
sharp: 0.34.5
transitivePeerDependencies:
@@ -11651,19 +10394,13 @@ snapshots:
node-domexception@1.0.0: {}
- node-fetch-native@1.6.7: {}
-
node-fetch@3.3.2:
dependencies:
data-uri-to-buffer: 4.0.1
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
- node-int64@0.4.0: {}
-
- node-releases@2.0.19: {}
-
- node-releases@2.0.37: {}
+ node-releases@2.0.49: {}
non-error@0.1.0: {}
@@ -11674,8 +10411,6 @@ snapshots:
semver: 5.7.2
validate-npm-package-license: 3.0.4
- normalize-path@3.0.0: {}
-
normalize-url@6.1.0: {}
npm-run-all@4.1.5:
@@ -11690,20 +10425,10 @@ snapshots:
shell-quote: 1.8.3
string.prototype.padend: 3.1.6
- npm-run-path@4.0.1:
- dependencies:
- path-key: 3.1.1
-
nth-check@2.1.1:
dependencies:
boolbase: 1.0.0
- nypm@0.6.5:
- dependencies:
- citty: 0.2.2
- pathe: 2.0.3
- tinyexec: 1.1.1
-
object-assign@4.1.1: {}
object-inspect@1.13.4: {}
@@ -11719,17 +10444,15 @@ snapshots:
has-symbols: 1.1.0
object-keys: 1.1.1
+ obug@2.1.3: {}
+
ohash@2.0.11: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
- onetime@5.1.2:
- dependencies:
- mimic-fn: 2.1.0
-
- ospath@1.2.2: {}
+ outvariant@1.4.3: {}
otplib@13.4.0:
dependencies:
@@ -11748,22 +10471,6 @@ snapshots:
p-finally@1.0.0: {}
- p-limit@2.3.0:
- dependencies:
- p-try: 2.2.0
-
- p-limit@3.1.0:
- dependencies:
- yocto-queue: 0.1.0
-
- p-locate@4.1.0:
- dependencies:
- p-limit: 2.3.0
-
- p-map@4.0.0:
- dependencies:
- aggregate-error: 3.1.0
-
p-map@7.0.4: {}
p-queue@6.6.2:
@@ -11775,13 +10482,11 @@ snapshots:
dependencies:
p-finally: 1.0.0
- p-try@2.2.0: {}
-
package-json-from-dist@1.0.1: {}
pako@1.0.11: {}
- papaparse@5.5.3: {}
+ papaparse@5.5.4: {}
parent-module@1.0.1:
dependencies:
@@ -11799,9 +10504,11 @@ snapshots:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
- path-browserify@1.0.1: {}
+ parse5@8.0.1:
+ dependencies:
+ entities: 8.0.0
- path-exists@4.0.0: {}
+ path-browserify@1.0.1: {}
path-is-absolute@1.0.1: {}
@@ -11816,6 +10523,8 @@ snapshots:
lru-cache: 10.4.3
minipass: 7.1.2
+ path-to-regexp@6.3.0: {}
+
path-type@3.0.0:
dependencies:
pify: 3.0.0
@@ -11826,24 +10535,20 @@ snapshots:
pathe@2.0.3: {}
- pend@1.2.0: {}
+ perfect-debounce@2.1.0: {}
- perfect-debounce@1.0.0: {}
-
- performance-now@2.1.0: {}
-
- pg-cloudflare@1.3.0:
+ pg-cloudflare@1.4.0:
optional: true
- pg-connection-string@2.12.0: {}
+ pg-connection-string@2.14.0: {}
pg-int8@1.0.1: {}
- pg-pool@3.13.0(pg@8.20.0):
+ pg-pool@3.14.0(pg@8.22.0):
dependencies:
- pg: 8.20.0
+ pg: 8.22.0
- pg-protocol@1.13.0: {}
+ pg-protocol@1.15.0: {}
pg-types@2.2.0:
dependencies:
@@ -11853,15 +10558,15 @@ snapshots:
postgres-date: 1.0.7
postgres-interval: 1.2.0
- pg@8.20.0:
+ pg@8.22.0:
dependencies:
- pg-connection-string: 2.12.0
- pg-pool: 3.13.0(pg@8.20.0)
- pg-protocol: 1.13.0
+ pg-connection-string: 2.14.0
+ pg-pool: 3.14.0(pg@8.22.0)
+ pg-protocol: 1.15.0
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
- pg-cloudflare: 1.3.0
+ pg-cloudflare: 1.4.0
pgpass@1.0.5:
dependencies:
@@ -11885,485 +10590,484 @@ snapshots:
pirates@4.0.7: {}
- pkg-dir@4.2.0:
- dependencies:
- find-up: 4.1.0
-
pkg-types@1.3.1:
dependencies:
confbox: 0.1.8
mlly: 1.8.0
pathe: 2.0.3
- pkg-types@2.3.0:
+ pkg-types@2.3.1:
dependencies:
confbox: 0.2.4
exsolve: 1.0.8
pathe: 2.0.3
pngjs@5.0.0: {}
+ playwright-core@1.61.1: {}
+
+ playwright@1.61.1:
+ dependencies:
+ playwright-core: 1.61.1
+ optionalDependencies:
+ fsevents: 2.3.2
po-parser@2.1.1: {}
possible-typed-array-names@1.1.0: {}
- postcss-attribute-case-insensitive@8.0.0(postcss@8.5.10):
+ postcss-attribute-case-insensitive@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
- postcss-selector-parser: 7.1.1
+ postcss: 8.5.15
+ postcss-selector-parser: 7.1.4
- postcss-calc@8.2.4(postcss@8.5.10):
+ postcss-calc@8.2.4(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
- postcss-selector-parser: 6.1.2
+ postcss: 8.5.15
+ postcss-selector-parser: 6.1.4
postcss-value-parser: 4.2.0
- postcss-clamp@4.1.0(postcss@8.5.10):
+ postcss-clamp@4.1.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-color-functional-notation@8.0.3(postcss@8.5.10):
+ postcss-color-functional-notation@8.0.5(postcss@8.5.15):
dependencies:
- '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10)
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
- postcss-color-hex-alpha@11.0.0(postcss@8.5.10):
+ postcss-color-hex-alpha@11.0.0(postcss@8.5.15):
dependencies:
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-color-rebeccapurple@11.0.0(postcss@8.5.10):
+ postcss-color-rebeccapurple@11.0.0(postcss@8.5.15):
dependencies:
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-colormin@5.3.1(postcss@8.5.10):
+ postcss-colormin@5.3.1(postcss@8.5.15):
dependencies:
- browserslist: 4.25.2
+ browserslist: 4.28.4
caniuse-api: 3.0.0
colord: 2.9.3
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-convert-values@5.1.3(postcss@8.5.10):
+ postcss-convert-values@5.1.3(postcss@8.5.15):
dependencies:
- browserslist: 4.25.2
- postcss: 8.5.10
+ browserslist: 4.28.4
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-custom-media@12.0.1(postcss@8.5.10):
+ postcss-custom-media@12.0.1(postcss@8.5.15):
dependencies:
'@csstools/cascade-layer-name-parser': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
'@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
- postcss: 8.5.10
+ postcss: 8.5.15
- postcss-custom-properties@15.0.1(postcss@8.5.10):
+ postcss-custom-properties@15.0.1(postcss@8.5.15):
dependencies:
'@csstools/cascade-layer-name-parser': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-custom-selectors@9.0.1(postcss@8.5.10):
+ postcss-custom-selectors@9.0.1(postcss@8.5.15):
dependencies:
'@csstools/cascade-layer-name-parser': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- postcss: 8.5.10
- postcss-selector-parser: 7.1.1
+ postcss: 8.5.15
+ postcss-selector-parser: 7.1.4
- postcss-dir-pseudo-class@10.0.0(postcss@8.5.10):
+ postcss-dir-pseudo-class@10.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
- postcss-selector-parser: 7.1.1
+ postcss: 8.5.15
+ postcss-selector-parser: 7.1.4
- postcss-discard-comments@5.1.2(postcss@8.5.10):
+ postcss-discard-comments@5.1.2(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- postcss-discard-duplicates@5.1.0(postcss@8.5.10):
+ postcss-discard-duplicates@5.1.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- postcss-discard-empty@5.1.1(postcss@8.5.10):
+ postcss-discard-empty@5.1.1(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- postcss-discard-overridden@5.1.0(postcss@8.5.10):
+ postcss-discard-overridden@5.1.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- postcss-double-position-gradients@7.0.0(postcss@8.5.10):
+ postcss-double-position-gradients@7.0.1(postcss@8.5.15):
dependencies:
- '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10)
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-flexbugs-fixes@5.0.2(postcss@8.5.10):
+ postcss-flexbugs-fixes@5.0.2(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- postcss-focus-visible@11.0.0(postcss@8.5.10):
+ postcss-focus-visible@11.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
- postcss-selector-parser: 7.1.1
+ postcss: 8.5.15
+ postcss-selector-parser: 7.1.4
- postcss-focus-within@10.0.0(postcss@8.5.10):
+ postcss-focus-within@10.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
- postcss-selector-parser: 7.1.1
+ postcss: 8.5.15
+ postcss-selector-parser: 7.1.4
- postcss-font-variant@5.0.0(postcss@8.5.10):
+ postcss-font-variant@5.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- postcss-gap-properties@7.0.0(postcss@8.5.10):
+ postcss-gap-properties@7.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- postcss-image-set-function@8.0.0(postcss@8.5.10):
+ postcss-image-set-function@8.0.0(postcss@8.5.15):
dependencies:
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-import@16.1.1(postcss@8.5.10):
+ postcss-import@16.1.1(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
read-cache: 1.0.0
resolve: 1.22.12
- postcss-lab-function@8.0.3(postcss@8.5.10):
+ postcss-lab-function@8.0.5(postcss@8.5.15):
dependencies:
- '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10)
- '@csstools/utilities': 3.0.0(postcss@8.5.10)
- postcss: 8.5.10
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/utilities': 3.0.0(postcss@8.5.15)
+ postcss: 8.5.15
- postcss-load-config@3.1.4(postcss@8.5.10)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)):
+ postcss-load-config@3.1.4(postcss@8.5.15)(ts-node@10.9.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(@types/node@26.0.1)(typescript@6.0.3)):
dependencies:
lilconfig: 2.1.0
yaml: 1.10.2
optionalDependencies:
- postcss: 8.5.10
- ts-node: 10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)
+ postcss: 8.5.15
+ ts-node: 10.9.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(@types/node@26.0.1)(typescript@6.0.3)
- postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(yaml@2.8.1):
+ postcss-load-config@6.0.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4):
dependencies:
lilconfig: 3.1.3
optionalDependencies:
- jiti: 2.6.1
- postcss: 8.5.10
- tsx: 4.21.0
- yaml: 2.8.1
+ jiti: 2.7.0
+ postcss: 8.5.15
+ tsx: 4.22.4
- postcss-logical@9.0.0(postcss@8.5.10):
+ postcss-logical@9.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-merge-longhand@5.1.7(postcss@8.5.10):
+ postcss-merge-longhand@5.1.7(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- stylehacks: 5.1.1(postcss@8.5.10)
+ stylehacks: 5.1.1(postcss@8.5.15)
- postcss-merge-rules@5.1.4(postcss@8.5.10):
+ postcss-merge-rules@5.1.4(postcss@8.5.15):
dependencies:
- browserslist: 4.25.2
+ browserslist: 4.28.4
caniuse-api: 3.0.0
- cssnano-utils: 3.1.0(postcss@8.5.10)
- postcss: 8.5.10
- postcss-selector-parser: 6.1.2
+ cssnano-utils: 3.1.0(postcss@8.5.15)
+ postcss: 8.5.15
+ postcss-selector-parser: 6.1.4
- postcss-minify-font-values@5.1.0(postcss@8.5.10):
+ postcss-minify-font-values@5.1.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-minify-gradients@5.1.1(postcss@8.5.10):
+ postcss-minify-gradients@5.1.1(postcss@8.5.15):
dependencies:
colord: 2.9.3
- cssnano-utils: 3.1.0(postcss@8.5.10)
- postcss: 8.5.10
+ cssnano-utils: 3.1.0(postcss@8.5.15)
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-minify-params@5.1.4(postcss@8.5.10):
+ postcss-minify-params@5.1.4(postcss@8.5.15):
dependencies:
- browserslist: 4.25.2
- cssnano-utils: 3.1.0(postcss@8.5.10)
- postcss: 8.5.10
+ browserslist: 4.28.4
+ cssnano-utils: 3.1.0(postcss@8.5.15)
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-minify-selectors@5.2.1(postcss@8.5.10):
+ postcss-minify-selectors@5.2.1(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
- postcss-selector-parser: 6.1.2
+ postcss: 8.5.15
+ postcss-selector-parser: 6.1.4
- postcss-modules-extract-imports@3.1.0(postcss@8.5.10):
+ postcss-modules-extract-imports@3.1.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- postcss-modules-local-by-default@4.2.0(postcss@8.5.10):
+ postcss-modules-local-by-default@4.2.0(postcss@8.5.15):
dependencies:
- icss-utils: 5.1.0(postcss@8.5.10)
- postcss: 8.5.10
- postcss-selector-parser: 7.1.0
+ icss-utils: 5.1.0(postcss@8.5.15)
+ postcss: 8.5.15
+ postcss-selector-parser: 7.1.4
postcss-value-parser: 4.2.0
- postcss-modules-scope@3.2.1(postcss@8.5.10):
+ postcss-modules-scope@3.2.1(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
- postcss-selector-parser: 7.1.0
+ postcss: 8.5.15
+ postcss-selector-parser: 7.1.4
- postcss-modules-values@4.0.0(postcss@8.5.10):
+ postcss-modules-values@4.0.0(postcss@8.5.15):
dependencies:
- icss-utils: 5.1.0(postcss@8.5.10)
- postcss: 8.5.10
+ icss-utils: 5.1.0(postcss@8.5.15)
+ postcss: 8.5.15
- postcss-modules@4.3.1(postcss@8.5.10):
+ postcss-modules@4.3.1(postcss@8.5.15):
dependencies:
generic-names: 4.0.0
icss-replace-symbols: 1.1.0
lodash.camelcase: 4.3.0
- postcss: 8.5.10
- postcss-modules-extract-imports: 3.1.0(postcss@8.5.10)
- postcss-modules-local-by-default: 4.2.0(postcss@8.5.10)
- postcss-modules-scope: 3.2.1(postcss@8.5.10)
- postcss-modules-values: 4.0.0(postcss@8.5.10)
+ postcss: 8.5.15
+ postcss-modules-extract-imports: 3.1.0(postcss@8.5.15)
+ postcss-modules-local-by-default: 4.2.0(postcss@8.5.15)
+ postcss-modules-scope: 3.2.1(postcss@8.5.15)
+ postcss-modules-values: 4.0.0(postcss@8.5.15)
string-hash: 1.1.3
- postcss-nesting@14.0.0(postcss@8.5.10):
+ postcss-nesting@14.0.0(postcss@8.5.15):
dependencies:
- '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1)
- '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1)
- postcss: 8.5.10
- postcss-selector-parser: 7.1.1
+ '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.4)
+ '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.4)
+ postcss: 8.5.15
+ postcss-selector-parser: 7.1.4
- postcss-normalize-charset@5.1.0(postcss@8.5.10):
+ postcss-normalize-charset@5.1.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- postcss-normalize-display-values@5.1.0(postcss@8.5.10):
+ postcss-normalize-display-values@5.1.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-normalize-positions@5.1.1(postcss@8.5.10):
+ postcss-normalize-positions@5.1.1(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-normalize-repeat-style@5.1.1(postcss@8.5.10):
+ postcss-normalize-repeat-style@5.1.1(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-normalize-string@5.1.0(postcss@8.5.10):
+ postcss-normalize-string@5.1.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-normalize-timing-functions@5.1.0(postcss@8.5.10):
+ postcss-normalize-timing-functions@5.1.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-normalize-unicode@5.1.1(postcss@8.5.10):
+ postcss-normalize-unicode@5.1.1(postcss@8.5.15):
dependencies:
- browserslist: 4.25.2
- postcss: 8.5.10
+ browserslist: 4.28.4
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-normalize-url@5.1.0(postcss@8.5.10):
+ postcss-normalize-url@5.1.0(postcss@8.5.15):
dependencies:
normalize-url: 6.1.0
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-normalize-whitespace@5.1.1(postcss@8.5.10):
+ postcss-normalize-whitespace@5.1.1(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-opacity-percentage@3.0.0(postcss@8.5.10):
+ postcss-opacity-percentage@3.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- postcss-ordered-values@5.1.3(postcss@8.5.10):
+ postcss-ordered-values@5.1.3(postcss@8.5.15):
dependencies:
- cssnano-utils: 3.1.0(postcss@8.5.10)
- postcss: 8.5.10
+ cssnano-utils: 3.1.0(postcss@8.5.15)
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-overflow-shorthand@7.0.0(postcss@8.5.10):
+ postcss-overflow-shorthand@7.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-page-break@3.0.4(postcss@8.5.10):
+ postcss-page-break@3.0.4(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- postcss-place@11.0.0(postcss@8.5.10):
+ postcss-place@11.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-preset-env@11.2.1(postcss@8.5.10):
+ postcss-preset-env@11.3.1(postcss@8.5.15):
dependencies:
- '@csstools/postcss-alpha-function': 2.0.4(postcss@8.5.10)
- '@csstools/postcss-cascade-layers': 6.0.0(postcss@8.5.10)
- '@csstools/postcss-color-function': 5.0.3(postcss@8.5.10)
- '@csstools/postcss-color-function-display-p3-linear': 2.0.3(postcss@8.5.10)
- '@csstools/postcss-color-mix-function': 4.0.3(postcss@8.5.10)
- '@csstools/postcss-color-mix-variadic-function-arguments': 2.0.3(postcss@8.5.10)
- '@csstools/postcss-content-alt-text': 3.0.0(postcss@8.5.10)
- '@csstools/postcss-contrast-color-function': 3.0.3(postcss@8.5.10)
- '@csstools/postcss-exponential-functions': 3.0.2(postcss@8.5.10)
- '@csstools/postcss-font-format-keywords': 5.0.0(postcss@8.5.10)
- '@csstools/postcss-font-width-property': 1.0.0(postcss@8.5.10)
- '@csstools/postcss-gamut-mapping': 3.0.3(postcss@8.5.10)
- '@csstools/postcss-gradients-interpolation-method': 6.0.3(postcss@8.5.10)
- '@csstools/postcss-hwb-function': 5.0.3(postcss@8.5.10)
- '@csstools/postcss-ic-unit': 5.0.0(postcss@8.5.10)
- '@csstools/postcss-initial': 3.0.0(postcss@8.5.10)
- '@csstools/postcss-is-pseudo-class': 6.0.0(postcss@8.5.10)
- '@csstools/postcss-light-dark-function': 3.0.0(postcss@8.5.10)
- '@csstools/postcss-logical-float-and-clear': 4.0.0(postcss@8.5.10)
- '@csstools/postcss-logical-overflow': 3.0.0(postcss@8.5.10)
- '@csstools/postcss-logical-overscroll-behavior': 3.0.0(postcss@8.5.10)
- '@csstools/postcss-logical-resize': 4.0.0(postcss@8.5.10)
- '@csstools/postcss-logical-viewport-units': 4.0.0(postcss@8.5.10)
- '@csstools/postcss-media-minmax': 3.0.2(postcss@8.5.10)
- '@csstools/postcss-media-queries-aspect-ratio-number-values': 4.0.0(postcss@8.5.10)
- '@csstools/postcss-mixins': 1.0.0(postcss@8.5.10)
- '@csstools/postcss-nested-calc': 5.0.0(postcss@8.5.10)
- '@csstools/postcss-normalize-display-values': 5.0.1(postcss@8.5.10)
- '@csstools/postcss-oklab-function': 5.0.3(postcss@8.5.10)
- '@csstools/postcss-position-area-property': 2.0.0(postcss@8.5.10)
- '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.10)
- '@csstools/postcss-property-rule-prelude-list': 2.0.0(postcss@8.5.10)
- '@csstools/postcss-random-function': 3.0.2(postcss@8.5.10)
- '@csstools/postcss-relative-color-syntax': 4.0.3(postcss@8.5.10)
- '@csstools/postcss-scope-pseudo-class': 5.0.0(postcss@8.5.10)
- '@csstools/postcss-sign-functions': 2.0.2(postcss@8.5.10)
- '@csstools/postcss-stepped-value-functions': 5.0.2(postcss@8.5.10)
- '@csstools/postcss-syntax-descriptor-syntax-production': 2.0.0(postcss@8.5.10)
- '@csstools/postcss-system-ui-font-family': 2.0.0(postcss@8.5.10)
- '@csstools/postcss-text-decoration-shorthand': 5.0.3(postcss@8.5.10)
- '@csstools/postcss-trigonometric-functions': 5.0.2(postcss@8.5.10)
- '@csstools/postcss-unset-value': 5.0.0(postcss@8.5.10)
- autoprefixer: 10.5.0(postcss@8.5.10)
- browserslist: 4.28.2
- css-blank-pseudo: 8.0.1(postcss@8.5.10)
- css-has-pseudo: 8.0.0(postcss@8.5.10)
- css-prefers-color-scheme: 11.0.0(postcss@8.5.10)
- cssdb: 8.8.0
- postcss: 8.5.10
- postcss-attribute-case-insensitive: 8.0.0(postcss@8.5.10)
- postcss-clamp: 4.1.0(postcss@8.5.10)
- postcss-color-functional-notation: 8.0.3(postcss@8.5.10)
- postcss-color-hex-alpha: 11.0.0(postcss@8.5.10)
- postcss-color-rebeccapurple: 11.0.0(postcss@8.5.10)
- postcss-custom-media: 12.0.1(postcss@8.5.10)
- postcss-custom-properties: 15.0.1(postcss@8.5.10)
- postcss-custom-selectors: 9.0.1(postcss@8.5.10)
- postcss-dir-pseudo-class: 10.0.0(postcss@8.5.10)
- postcss-double-position-gradients: 7.0.0(postcss@8.5.10)
- postcss-focus-visible: 11.0.0(postcss@8.5.10)
- postcss-focus-within: 10.0.0(postcss@8.5.10)
- postcss-font-variant: 5.0.0(postcss@8.5.10)
- postcss-gap-properties: 7.0.0(postcss@8.5.10)
- postcss-image-set-function: 8.0.0(postcss@8.5.10)
- postcss-lab-function: 8.0.3(postcss@8.5.10)
- postcss-logical: 9.0.0(postcss@8.5.10)
- postcss-nesting: 14.0.0(postcss@8.5.10)
- postcss-opacity-percentage: 3.0.0(postcss@8.5.10)
- postcss-overflow-shorthand: 7.0.0(postcss@8.5.10)
- postcss-page-break: 3.0.4(postcss@8.5.10)
- postcss-place: 11.0.0(postcss@8.5.10)
- postcss-pseudo-class-any-link: 11.0.0(postcss@8.5.10)
- postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.10)
- postcss-selector-not: 9.0.0(postcss@8.5.10)
+ '@csstools/postcss-alpha-function': 2.0.6(postcss@8.5.15)
+ '@csstools/postcss-cascade-layers': 6.0.0(postcss@8.5.15)
+ '@csstools/postcss-color-function': 5.0.5(postcss@8.5.15)
+ '@csstools/postcss-color-function-display-p3-linear': 2.0.5(postcss@8.5.15)
+ '@csstools/postcss-color-mix-function': 4.0.5(postcss@8.5.15)
+ '@csstools/postcss-color-mix-variadic-function-arguments': 2.0.5(postcss@8.5.15)
+ '@csstools/postcss-container-rule-prelude-list': 1.0.1(postcss@8.5.15)
+ '@csstools/postcss-content-alt-text': 3.0.1(postcss@8.5.15)
+ '@csstools/postcss-contrast-color-function': 3.0.5(postcss@8.5.15)
+ '@csstools/postcss-exponential-functions': 3.0.3(postcss@8.5.15)
+ '@csstools/postcss-font-format-keywords': 5.0.0(postcss@8.5.15)
+ '@csstools/postcss-font-width-property': 1.0.0(postcss@8.5.15)
+ '@csstools/postcss-gamut-mapping': 3.0.5(postcss@8.5.15)
+ '@csstools/postcss-gradients-interpolation-method': 6.0.5(postcss@8.5.15)
+ '@csstools/postcss-hwb-function': 5.0.5(postcss@8.5.15)
+ '@csstools/postcss-ic-unit': 5.0.1(postcss@8.5.15)
+ '@csstools/postcss-image-function': 1.0.0(postcss@8.5.15)
+ '@csstools/postcss-initial': 3.0.0(postcss@8.5.15)
+ '@csstools/postcss-is-pseudo-class': 6.0.0(postcss@8.5.15)
+ '@csstools/postcss-light-dark-function': 3.0.1(postcss@8.5.15)
+ '@csstools/postcss-logical-float-and-clear': 4.0.0(postcss@8.5.15)
+ '@csstools/postcss-logical-overflow': 3.0.0(postcss@8.5.15)
+ '@csstools/postcss-logical-overscroll-behavior': 3.0.0(postcss@8.5.15)
+ '@csstools/postcss-logical-resize': 4.0.0(postcss@8.5.15)
+ '@csstools/postcss-logical-viewport-units': 4.0.0(postcss@8.5.15)
+ '@csstools/postcss-media-minmax': 3.0.3(postcss@8.5.15)
+ '@csstools/postcss-media-queries-aspect-ratio-number-values': 4.0.0(postcss@8.5.15)
+ '@csstools/postcss-mixins': 1.0.0(postcss@8.5.15)
+ '@csstools/postcss-nested-calc': 5.0.0(postcss@8.5.15)
+ '@csstools/postcss-normalize-display-values': 5.0.1(postcss@8.5.15)
+ '@csstools/postcss-oklab-function': 5.0.5(postcss@8.5.15)
+ '@csstools/postcss-position-area-property': 2.0.0(postcss@8.5.15)
+ '@csstools/postcss-progressive-custom-properties': 5.1.0(postcss@8.5.15)
+ '@csstools/postcss-property-rule-prelude-list': 2.0.0(postcss@8.5.15)
+ '@csstools/postcss-random-function': 3.0.3(postcss@8.5.15)
+ '@csstools/postcss-relative-color-syntax': 4.0.5(postcss@8.5.15)
+ '@csstools/postcss-scope-pseudo-class': 5.0.0(postcss@8.5.15)
+ '@csstools/postcss-sign-functions': 2.0.3(postcss@8.5.15)
+ '@csstools/postcss-stepped-value-functions': 5.0.3(postcss@8.5.15)
+ '@csstools/postcss-syntax-descriptor-syntax-production': 2.0.0(postcss@8.5.15)
+ '@csstools/postcss-system-ui-font-family': 2.0.0(postcss@8.5.15)
+ '@csstools/postcss-text-decoration-shorthand': 5.0.3(postcss@8.5.15)
+ '@csstools/postcss-trigonometric-functions': 5.0.3(postcss@8.5.15)
+ '@csstools/postcss-unset-value': 5.0.0(postcss@8.5.15)
+ autoprefixer: 10.5.2(postcss@8.5.15)
+ browserslist: 4.28.4
+ css-blank-pseudo: 8.0.1(postcss@8.5.15)
+ css-has-pseudo: 8.0.0(postcss@8.5.15)
+ css-prefers-color-scheme: 11.0.0(postcss@8.5.15)
+ cssdb: 8.9.0
+ postcss: 8.5.15
+ postcss-attribute-case-insensitive: 8.0.0(postcss@8.5.15)
+ postcss-clamp: 4.1.0(postcss@8.5.15)
+ postcss-color-functional-notation: 8.0.5(postcss@8.5.15)
+ postcss-color-hex-alpha: 11.0.0(postcss@8.5.15)
+ postcss-color-rebeccapurple: 11.0.0(postcss@8.5.15)
+ postcss-custom-media: 12.0.1(postcss@8.5.15)
+ postcss-custom-properties: 15.0.1(postcss@8.5.15)
+ postcss-custom-selectors: 9.0.1(postcss@8.5.15)
+ postcss-dir-pseudo-class: 10.0.0(postcss@8.5.15)
+ postcss-double-position-gradients: 7.0.1(postcss@8.5.15)
+ postcss-focus-visible: 11.0.0(postcss@8.5.15)
+ postcss-focus-within: 10.0.0(postcss@8.5.15)
+ postcss-font-variant: 5.0.0(postcss@8.5.15)
+ postcss-gap-properties: 7.0.0(postcss@8.5.15)
+ postcss-image-set-function: 8.0.0(postcss@8.5.15)
+ postcss-lab-function: 8.0.5(postcss@8.5.15)
+ postcss-logical: 9.0.0(postcss@8.5.15)
+ postcss-nesting: 14.0.0(postcss@8.5.15)
+ postcss-opacity-percentage: 3.0.0(postcss@8.5.15)
+ postcss-overflow-shorthand: 7.0.0(postcss@8.5.15)
+ postcss-page-break: 3.0.4(postcss@8.5.15)
+ postcss-place: 11.0.0(postcss@8.5.15)
+ postcss-pseudo-class-any-link: 11.0.0(postcss@8.5.15)
+ postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.15)
+ postcss-selector-not: 9.0.0(postcss@8.5.15)
- postcss-pseudo-class-any-link@11.0.0(postcss@8.5.10):
+ postcss-pseudo-class-any-link@11.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
- postcss-selector-parser: 7.1.1
+ postcss: 8.5.15
+ postcss-selector-parser: 7.1.4
- postcss-reduce-initial@5.1.2(postcss@8.5.10):
+ postcss-reduce-initial@5.1.2(postcss@8.5.15):
dependencies:
- browserslist: 4.25.2
+ browserslist: 4.28.4
caniuse-api: 3.0.0
- postcss: 8.5.10
+ postcss: 8.5.15
- postcss-reduce-transforms@5.1.0(postcss@8.5.10):
+ postcss-reduce-transforms@5.1.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-replace-overflow-wrap@4.0.0(postcss@8.5.10):
+ postcss-replace-overflow-wrap@4.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
- postcss-selector-not@9.0.0(postcss@8.5.10):
+ postcss-selector-not@9.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
- postcss-selector-parser: 7.1.1
+ postcss: 8.5.15
+ postcss-selector-parser: 7.1.4
- postcss-selector-parser@6.1.2:
+ postcss-selector-parser@6.1.4:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
- postcss-selector-parser@7.1.0:
+ postcss-selector-parser@7.1.4:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
- postcss-selector-parser@7.1.1:
+ postcss-svgo@5.1.0(postcss@8.5.15):
dependencies:
- cssesc: 3.0.0
- util-deprecate: 1.0.2
-
- postcss-svgo@5.1.0(postcss@8.5.10):
- dependencies:
- postcss: 8.5.10
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
svgo: 2.8.0
- postcss-unique-selectors@5.1.1(postcss@8.5.10):
+ postcss-unique-selectors@5.1.1(postcss@8.5.15):
dependencies:
- postcss: 8.5.10
- postcss-selector-parser: 6.1.2
+ postcss: 8.5.15
+ postcss-selector-parser: 6.1.4
postcss-value-parser@4.2.0: {}
postcss@8.4.31:
dependencies:
- nanoid: 3.3.11
+ nanoid: 3.3.15
picocolors: 1.1.1
source-map-js: 1.2.1
- postcss@8.5.10:
+ postcss@8.5.15:
dependencies:
- nanoid: 3.3.11
+ nanoid: 3.3.12
picocolors: 1.1.1
source-map-js: 1.2.1
@@ -12385,26 +11089,18 @@ snapshots:
prettier@2.8.8: {}
- pretty-bytes@5.6.0: {}
-
- pretty-format@30.0.5:
+ pretty-format@27.5.1:
dependencies:
- '@jest/schemas': 30.0.5
+ ansi-regex: 5.0.1
ansi-styles: 5.2.0
- react-is: 18.3.1
+ react-is: 17.0.2
- pretty-format@30.3.0:
+ prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3):
dependencies:
- '@jest/schemas': 30.0.5
- ansi-styles: 5.2.0
- react-is: 18.3.1
-
- prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3):
- dependencies:
- '@prisma/config': 7.7.0
+ '@prisma/config': 7.8.0
'@prisma/dev': 0.24.3(typescript@6.0.3)
- '@prisma/engines': 7.7.0
- '@prisma/studio-core': 0.27.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@prisma/engines': 7.8.0
+ '@prisma/studio-core': 0.27.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
mysql2: 3.15.3
postgres: 3.4.7
optionalDependencies:
@@ -12418,8 +11114,6 @@ snapshots:
process-nextick-args@2.0.1: {}
- process@0.11.10: {}
-
promise.series@0.2.0: {}
prompts@2.4.2:
@@ -12439,12 +11133,7 @@ snapshots:
retry: 0.12.0
signal-exit: 3.0.7
- proxy-from-env@1.0.0: {}
-
- pump@3.0.4:
- dependencies:
- end-of-stream: 1.4.5
- once: 1.4.0
+ punycode@2.3.1: {}
pure-rand@6.1.0: {}
@@ -12466,60 +11155,60 @@ snapshots:
raf-schd@4.0.3: {}
- rc9@2.1.2:
+ rc9@3.0.1:
dependencies:
defu: 6.1.7
destr: 2.0.5
- react-aria-components@1.17.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ react-aria-components@1.19.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
- '@internationalized/date': 3.12.1
- '@react-types/shared': 3.34.0(react@19.2.5)
- '@swc/helpers': 0.5.21
+ '@internationalized/date': 3.12.2
+ '@react-types/shared': 3.36.0(react@19.2.7)
+ '@swc/helpers': 0.5.23
client-only: 0.0.1
- react: 19.2.5
- react-aria: 3.48.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- react-dom: 19.2.5(react@19.2.5)
- react-stately: 3.46.0(react@19.2.5)
+ react: 19.2.7
+ react-aria: 3.50.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ react-dom: 19.2.7(react@19.2.7)
+ react-stately: 3.48.0(react@19.2.7)
- react-aria@3.48.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ react-aria@3.50.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
- '@internationalized/date': 3.12.1
- '@internationalized/number': 3.6.6
- '@internationalized/string': 3.2.8
- '@react-types/shared': 3.34.0(react@19.2.5)
- '@swc/helpers': 0.5.21
+ '@internationalized/date': 3.12.2
+ '@internationalized/number': 3.6.7
+ '@internationalized/string': 3.2.9
+ '@react-types/shared': 3.36.0(react@19.2.7)
+ '@swc/helpers': 0.5.23
aria-hidden: 1.2.6
clsx: 2.1.1
- react: 19.2.5
- react-dom: 19.2.5(react@19.2.5)
- react-stately: 3.46.0(react@19.2.5)
- use-sync-external-store: 1.6.0(react@19.2.5)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ react-stately: 3.48.0(react@19.2.7)
+ use-sync-external-store: 1.6.0(react@19.2.7)
- react-dom@19.2.5(react@19.2.5):
+ react-dom@19.2.7(react@19.2.7):
dependencies:
- react: 19.2.5
+ react: 19.2.7
scheduler: 0.27.0
- react-error-boundary@6.1.1(react@19.2.5):
+ react-error-boundary@6.1.2(react@19.2.7):
dependencies:
- react: 19.2.5
+ react: 19.2.7
- react-hook-form@7.71.1(react@19.2.5):
+ react-hook-form@7.71.1(react@19.2.7):
dependencies:
- react: 19.2.5
+ react: 19.2.7
react-is@16.13.1: {}
- react-is@18.3.1: {}
+ react-is@17.0.2: {}
- react-redux@9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1):
+ react-redux@9.2.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1):
dependencies:
'@types/use-sync-external-store': 0.0.6
- react: 19.2.5
- use-sync-external-store: 1.6.0(react@19.2.5)
+ react: 19.2.7
+ use-sync-external-store: 1.6.0(react@19.2.7)
optionalDependencies:
- '@types/react': 19.2.14
+ '@types/react': 19.2.17
redux: 5.0.1
react-resizable-panels@4.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
@@ -12528,37 +11217,46 @@ snapshots:
react-dom: 19.2.5(react@19.2.5)
react-simple-maps@3.0.0(prop-types@15.8.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ react-resizable-panels@4.11.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+
+ react-simple-maps@3.0.0(prop-types@15.8.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
d3-geo: 2.0.2
d3-selection: 2.0.0
d3-zoom: 2.0.0
prop-types: 15.8.1
- react: 19.2.5
- react-dom: 19.2.5(react@19.2.5)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
topojson-client: 3.1.0
- react-stately@3.46.0(react@19.2.5):
+ react-stately@3.48.0(react@19.2.7):
dependencies:
- '@internationalized/date': 3.12.1
- '@internationalized/number': 3.6.6
- '@internationalized/string': 3.2.8
- '@react-types/shared': 3.34.0(react@19.2.5)
- '@swc/helpers': 0.5.21
- react: 19.2.5
- use-sync-external-store: 1.6.0(react@19.2.5)
+ '@internationalized/date': 3.12.2
+ '@internationalized/number': 3.6.7
+ '@internationalized/string': 3.2.9
+ '@react-types/shared': 3.36.0(react@19.2.7)
+ '@swc/helpers': 0.5.23
+ react: 19.2.7
+ use-sync-external-store: 1.6.0(react@19.2.7)
- react-use-measure@2.1.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ react-use-measure@2.1.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
- react: 19.2.5
+ react: 19.2.7
optionalDependencies:
react-dom: 19.2.5(react@19.2.5)
react-window@2.2.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
- dependencies:
- react: 19.2.5
- react-dom: 19.2.5(react@19.2.5)
+ react-dom: 19.2.7(react@19.2.7)
- react@19.2.5: {}
+ react-window@2.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+
+ react@19.2.7: {}
read-cache@1.0.0:
dependencies:
@@ -12582,6 +11280,13 @@ snapshots:
readdirp@4.1.2: {}
+ readdirp@5.0.0: {}
+
+ redent@3.0.0:
+ dependencies:
+ indent-string: 4.0.0
+ strip-indent: 3.0.0
+
redis@5.12.1:
dependencies:
'@redis/bloom': 5.12.1(@redis/client@5.12.1)
@@ -12619,10 +11324,6 @@ snapshots:
request-ip@3.3.0: {}
- request-progress@3.0.0:
- dependencies:
- throttleit: 1.0.1
-
require-directory@2.1.1: {}
require-from-string@2.0.2: {}
@@ -12637,8 +11338,6 @@ snapshots:
resolve-from@5.0.0: {}
- resolve-pkg-maps@1.0.0: {}
-
resolve@1.22.10:
dependencies:
is-core-module: 2.16.1
@@ -12652,16 +11351,32 @@ snapshots:
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
- restore-cursor@3.1.0:
- dependencies:
- onetime: 5.1.2
- signal-exit: 3.0.7
-
retry@0.12.0: {}
+ rettime@0.11.11: {}
+
reusify@1.1.0: {}
- rfdc@1.4.1: {}
+ rolldown@1.0.0-rc.18:
+ dependencies:
+ '@oxc-project/types': 0.128.0
+ '@rolldown/pluginutils': 1.0.0-rc.18
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.0.0-rc.18
+ '@rolldown/binding-darwin-arm64': 1.0.0-rc.18
+ '@rolldown/binding-darwin-x64': 1.0.0-rc.18
+ '@rolldown/binding-freebsd-x64': 1.0.0-rc.18
+ '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.18
+ '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.18
+ '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.18
+ '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.18
+ '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.18
+ '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.18
+ '@rolldown/binding-linux-x64-musl': 1.0.0-rc.18
+ '@rolldown/binding-openharmony-arm64': 1.0.0-rc.18
+ '@rolldown/binding-wasm32-wasi': 1.0.0-rc.18
+ '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.18
+ '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.18
rollup-plugin-copy@3.5.0:
dependencies:
@@ -12671,41 +11386,42 @@ snapshots:
globby: 10.0.1
is-plain-object: 3.0.1
- rollup-plugin-delete@3.0.2(rollup@4.60.1):
+ rollup-plugin-delete@3.0.2(rollup@4.62.2):
dependencies:
del: 8.0.1
- rollup: 4.60.1
+ rollup: 4.62.2
- rollup-plugin-dts@6.4.1(rollup@4.60.1)(typescript@6.0.3):
+ rollup-plugin-dts@6.4.1(rollup@4.62.2)(typescript@6.0.3):
dependencies:
'@jridgewell/remapping': 2.3.5
'@jridgewell/sourcemap-codec': 1.5.5
convert-source-map: 2.0.0
magic-string: 0.30.21
- rollup: 4.60.1
+ rollup: 4.62.2
typescript: 6.0.3
optionalDependencies:
'@babel/code-frame': 7.29.0
- rollup-plugin-node-externals@9.0.1(rollup@4.60.1):
+ rollup-plugin-node-externals@9.0.1(rollup@4.62.2)(vite@8.0.11(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.22.4)):
optionalDependencies:
- rollup: 4.60.1
+ rollup: 4.62.2
+ vite: 8.0.11(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.22.4)
- rollup-plugin-peer-deps-external@2.2.4(rollup@4.60.1):
+ rollup-plugin-peer-deps-external@2.2.4(rollup@4.62.2):
dependencies:
- rollup: 4.60.1
+ rollup: 4.62.2
- rollup-plugin-postcss@4.0.2(postcss@8.5.10)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)):
+ rollup-plugin-postcss@4.0.2(postcss@8.5.15)(ts-node@10.9.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(@types/node@26.0.1)(typescript@6.0.3)):
dependencies:
chalk: 4.1.2
concat-with-sourcemaps: 1.1.0
- cssnano: 5.1.15(postcss@8.5.10)
+ cssnano: 5.1.15(postcss@8.5.15)
import-cwd: 3.0.0
p-queue: 6.6.2
pify: 5.0.0
- postcss: 8.5.10
- postcss-load-config: 3.1.4(postcss@8.5.10)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3))
- postcss-modules: 4.3.1(postcss@8.5.10)
+ postcss: 8.5.15
+ postcss-load-config: 3.1.4(postcss@8.5.15)(ts-node@10.9.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(@types/node@26.0.1)(typescript@6.0.3))
+ postcss-modules: 4.3.1(postcss@8.5.15)
promise.series: 0.2.0
resolve: 1.22.10
rollup-pluginutils: 2.8.2
@@ -12718,67 +11434,66 @@ snapshots:
dependencies:
estree-walker: 0.6.1
- rollup@4.60.1:
+ rollup@4.62.2:
dependencies:
- '@types/estree': 1.0.8
+ '@types/estree': 1.0.9
optionalDependencies:
- '@rollup/rollup-android-arm-eabi': 4.60.1
- '@rollup/rollup-android-arm64': 4.60.1
- '@rollup/rollup-darwin-arm64': 4.60.1
- '@rollup/rollup-darwin-x64': 4.60.1
- '@rollup/rollup-freebsd-arm64': 4.60.1
- '@rollup/rollup-freebsd-x64': 4.60.1
- '@rollup/rollup-linux-arm-gnueabihf': 4.60.1
- '@rollup/rollup-linux-arm-musleabihf': 4.60.1
- '@rollup/rollup-linux-arm64-gnu': 4.60.1
- '@rollup/rollup-linux-arm64-musl': 4.60.1
- '@rollup/rollup-linux-loong64-gnu': 4.60.1
- '@rollup/rollup-linux-loong64-musl': 4.60.1
- '@rollup/rollup-linux-ppc64-gnu': 4.60.1
- '@rollup/rollup-linux-ppc64-musl': 4.60.1
- '@rollup/rollup-linux-riscv64-gnu': 4.60.1
- '@rollup/rollup-linux-riscv64-musl': 4.60.1
- '@rollup/rollup-linux-s390x-gnu': 4.60.1
- '@rollup/rollup-linux-x64-gnu': 4.60.1
- '@rollup/rollup-linux-x64-musl': 4.60.1
- '@rollup/rollup-openbsd-x64': 4.60.1
- '@rollup/rollup-openharmony-arm64': 4.60.1
- '@rollup/rollup-win32-arm64-msvc': 4.60.1
- '@rollup/rollup-win32-ia32-msvc': 4.60.1
- '@rollup/rollup-win32-x64-gnu': 4.60.1
- '@rollup/rollup-win32-x64-msvc': 4.60.1
+ '@rollup/rollup-android-arm-eabi': 4.62.2
+ '@rollup/rollup-android-arm64': 4.62.2
+ '@rollup/rollup-darwin-arm64': 4.62.2
+ '@rollup/rollup-darwin-x64': 4.62.2
+ '@rollup/rollup-freebsd-arm64': 4.62.2
+ '@rollup/rollup-freebsd-x64': 4.62.2
+ '@rollup/rollup-linux-arm-gnueabihf': 4.62.2
+ '@rollup/rollup-linux-arm-musleabihf': 4.62.2
+ '@rollup/rollup-linux-arm64-gnu': 4.62.2
+ '@rollup/rollup-linux-arm64-musl': 4.62.2
+ '@rollup/rollup-linux-loong64-gnu': 4.62.2
+ '@rollup/rollup-linux-loong64-musl': 4.62.2
+ '@rollup/rollup-linux-ppc64-gnu': 4.62.2
+ '@rollup/rollup-linux-ppc64-musl': 4.62.2
+ '@rollup/rollup-linux-riscv64-gnu': 4.62.2
+ '@rollup/rollup-linux-riscv64-musl': 4.62.2
+ '@rollup/rollup-linux-s390x-gnu': 4.62.2
+ '@rollup/rollup-linux-x64-gnu': 4.62.2
+ '@rollup/rollup-linux-x64-musl': 4.62.2
+ '@rollup/rollup-openbsd-x64': 4.62.2
+ '@rollup/rollup-openharmony-arm64': 4.62.2
+ '@rollup/rollup-win32-arm64-msvc': 4.62.2
+ '@rollup/rollup-win32-ia32-msvc': 4.62.2
+ '@rollup/rollup-win32-x64-gnu': 4.62.2
+ '@rollup/rollup-win32-x64-msvc': 4.62.2
fsevents: 2.3.3
- rrdom@0.1.7:
+ rrdom@2.0.1:
dependencies:
- rrweb-snapshot: 2.0.0-alpha.4
+ rrweb-snapshot: 2.0.1
- rrweb-player@1.0.0-alpha.4:
+ rrweb-player@2.0.0-alpha.20:
dependencies:
+ '@rrweb/packer': 2.0.1
+ '@rrweb/replay': 2.0.1
'@tsconfig/svelte': 1.0.13
- rrweb: 2.0.0-alpha.4
- rrweb-snapshot@2.0.0-alpha.4: {}
-
- rrweb@2.0.0-alpha.4:
+ rrweb-snapshot@2.0.1:
dependencies:
- '@rrweb/types': 2.0.0-alpha.20
+ postcss: 8.5.15
+
+ rrweb@2.0.1:
+ dependencies:
+ '@rrweb/types': 2.0.1
+ '@rrweb/utils': 2.0.1
'@types/css-font-loading-module': 0.0.7
'@xstate/fsm': 1.6.5
base64-arraybuffer: 1.0.2
- fflate: 0.4.8
mitt: 3.0.1
- rrdom: 0.1.7
- rrweb-snapshot: 2.0.0-alpha.4
+ rrdom: 2.0.1
+ rrweb-snapshot: 2.0.1
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
- rxjs@7.8.2:
- dependencies:
- tslib: 2.8.1
-
safe-array-concat@1.1.3:
dependencies:
call-bind: 1.0.8
@@ -12806,13 +11521,17 @@ snapshots:
safer-buffer@2.1.2: {}
+ saxes@6.0.0:
+ dependencies:
+ xmlchars: 2.2.0
+
scheduler@0.27.0: {}
semver@5.7.2: {}
semver@6.3.1: {}
- semver@7.7.4: {}
+ semver@7.8.5: {}
seq-queue@0.0.5: {}
@@ -12824,6 +11543,7 @@ snapshots:
serialize-javascript@7.0.5: {}
set-blocking@2.0.0: {}
+ set-cookie-parser@3.1.0: {}
set-function-length@1.2.2:
dependencies:
@@ -12853,7 +11573,7 @@ snapshots:
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
- semver: 7.7.4
+ semver: 7.8.5
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.5
'@img/sharp-darwin-x64': 0.34.5
@@ -12923,6 +11643,8 @@ snapshots:
side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2
+ siginfo@2.0.0: {}
+
signal-exit@3.0.7: {}
signal-exit@4.1.0: {}
@@ -12933,18 +11655,6 @@ snapshots:
slash@5.1.0: {}
- slice-ansi@3.0.0:
- dependencies:
- ansi-styles: 4.3.0
- astral-regex: 2.0.0
- is-fullwidth-code-point: 3.0.0
-
- slice-ansi@4.0.0:
- dependencies:
- ansi-styles: 4.3.0
- astral-regex: 2.0.0
- is-fullwidth-code-point: 3.0.0
-
smob@1.6.1: {}
snake-case@3.0.4:
@@ -12954,11 +11664,6 @@ snapshots:
source-map-js@1.2.1: {}
- source-map-support@0.5.13:
- dependencies:
- buffer-from: 1.1.2
- source-map: 0.6.1
-
source-map-support@0.5.21:
dependencies:
buffer-from: 1.1.2
@@ -12984,41 +11689,26 @@ snapshots:
split2@4.2.0: {}
- sprintf-js@1.0.3: {}
-
sqlstring@2.3.3: {}
- sshpk@1.18.0:
- dependencies:
- asn1: 0.2.6
- assert-plus: 1.0.0
- bcrypt-pbkdf: 1.0.2
- dashdash: 1.14.1
- ecc-jsbn: 0.1.2
- getpass: 0.1.7
- jsbn: 0.1.1
- safer-buffer: 2.1.2
- tweetnacl: 0.14.5
-
stable@0.1.8: {}
- stack-utils@2.0.6:
- dependencies:
- escape-string-regexp: 2.0.0
+ stackback@0.0.2: {}
+
+ statuses@2.0.2: {}
std-env@3.10.0: {}
+ std-env@4.1.0: {}
+
stop-iteration-iterator@1.1.0:
dependencies:
es-errors: 1.3.0
internal-slot: 1.1.0
- string-hash@1.1.3: {}
+ strict-event-emitter@0.5.1: {}
- string-length@4.0.2:
- dependencies:
- char-regex: 1.0.2
- strip-ansi: 6.0.1
+ string-hash@1.1.3: {}
string-width@4.2.3:
dependencies:
@@ -13076,26 +11766,24 @@ snapshots:
strip-bom@3.0.0: {}
- strip-bom@4.0.0: {}
-
- strip-final-newline@2.0.0: {}
-
- strip-json-comments@3.1.1: {}
+ strip-indent@3.0.0:
+ dependencies:
+ min-indent: 1.0.1
style-inject@0.3.0: {}
- styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.5):
+ styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.7):
dependencies:
client-only: 0.0.1
- react: 19.2.5
+ react: 19.2.7
optionalDependencies:
'@babel/core': 7.29.0
- stylehacks@5.1.1(postcss@8.5.10):
+ stylehacks@5.1.1(postcss@8.5.15):
dependencies:
- browserslist: 4.25.2
- postcss: 8.5.10
- postcss-selector-parser: 6.1.2
+ browserslist: 4.28.4
+ postcss: 8.5.15
+ postcss-selector-parser: 6.1.4
sucrase@3.35.0:
dependencies:
@@ -13115,10 +11803,6 @@ snapshots:
dependencies:
has-flag: 4.0.0
- supports-color@8.1.1:
- dependencies:
- has-flag: 4.0.0
-
supports-preserve-symlinks-flag@1.0.0: {}
svg-parser@2.0.4: {}
@@ -13143,11 +11827,7 @@ snapshots:
csso: 5.0.5
picocolors: 1.1.1
- synckit@0.11.12:
- dependencies:
- '@pkgr/core': 0.2.9
-
- systeminformation@5.31.5: {}
+ symbol-tree@3.2.4: {}
tagged-tag@1.0.0: {}
@@ -13161,7 +11841,7 @@ snapshots:
tailwindcss@4.1.18: {}
- tar@7.5.13:
+ tar@7.5.16:
dependencies:
'@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0
@@ -13172,7 +11852,7 @@ snapshots:
terser@5.46.1:
dependencies:
'@jridgewell/source-map': 0.3.11
- acorn: 8.16.0
+ acorn: 8.17.0
commander: 2.20.3
source-map-support: 0.5.21
@@ -13183,6 +11863,7 @@ snapshots:
minimatch: 3.1.5
thenby@1.4.0: {}
+ thenby@1.4.1: {}
thenify-all@1.6.0:
dependencies:
@@ -13192,17 +11873,15 @@ snapshots:
dependencies:
any-promise: 1.3.0
- throttleit@1.0.1: {}
-
- through@2.3.8: {}
-
tiny-invariant@1.3.3: {}
tiny-lru@13.0.0: {}
+ tinybench@2.9.0: {}
+
tinyexec@0.3.2: {}
- tinyexec@1.1.1: {}
+ tinyexec@1.2.4: {}
tinyglobby@0.2.14:
dependencies:
@@ -13214,15 +11893,18 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
- tldts-core@6.1.86: {}
-
- tldts@6.1.86:
+ tinyglobby@0.2.17:
dependencies:
- tldts-core: 6.1.86
+ fdir: 6.5.0(picomatch@4.0.4)
+ picomatch: 4.0.4
- tmp@0.2.5: {}
+ tinyrainbow@3.1.0: {}
- tmpl@1.0.5: {}
+ tldts-core@7.0.30: {}
+
+ tldts@7.0.30:
+ dependencies:
+ tldts-core: 7.0.30
to-regex-range@5.0.1:
dependencies:
@@ -13232,48 +11914,31 @@ snapshots:
dependencies:
commander: 2.20.3
- tough-cookie@5.1.2:
+ tough-cookie@6.0.1:
dependencies:
- tldts: 6.1.86
+ tldts: 7.0.30
+
+ tr46@6.0.0:
+ dependencies:
+ punycode: 2.3.1
tree-kill@1.2.2: {}
ts-interface-checker@0.1.13: {}
- ts-jest@29.4.9(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(esbuild@0.28.0)(jest-util@30.3.0)(jest@30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3)))(typescript@6.0.3):
- dependencies:
- bs-logger: 0.2.6
- fast-json-stable-stringify: 2.1.0
- handlebars: 4.7.9
- jest: 30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3))
- json5: 2.2.3
- lodash.memoize: 4.1.2
- make-error: 1.3.6
- semver: 7.7.4
- type-fest: 4.41.0
- typescript: 6.0.3
- yargs-parser: 21.1.1
- optionalDependencies:
- '@babel/core': 7.29.0
- '@jest/transform': 30.3.0
- '@jest/types': 30.3.0
- babel-jest: 30.3.0(@babel/core@7.29.0)
- esbuild: 0.28.0
- jest-util: 30.3.0
-
ts-morph@28.0.0:
dependencies:
'@ts-morph/common': 0.29.0
code-block-writer: 13.0.3
- ts-node@10.9.2(@swc/core@1.15.26(@swc/helpers@0.5.21))(@types/node@25.6.0)(typescript@6.0.3):
+ ts-node@10.9.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(@types/node@26.0.1)(typescript@6.0.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
- '@types/node': 25.6.0
+ '@types/node': 26.0.1
acorn: 8.15.0
acorn-walk: 8.3.4
arg: 4.1.3
@@ -13284,34 +11949,32 @@ snapshots:
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
optionalDependencies:
- '@swc/core': 1.15.26(@swc/helpers@0.5.21)
-
- tslib@1.14.1: {}
+ '@swc/core': 1.15.43(@swc/helpers@0.5.23)
tslib@2.8.1: {}
- tsup@8.5.1(@swc/core@1.15.26(@swc/helpers@0.5.21))(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.1):
+ tsup@8.5.1(@swc/core@1.15.43(@swc/helpers@0.5.23))(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@6.0.3):
dependencies:
bundle-require: 5.1.0(esbuild@0.27.7)
cac: 6.7.14
chokidar: 4.0.3
consola: 3.4.2
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.4.3
esbuild: 0.27.7
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
picocolors: 1.1.1
- postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(yaml@2.8.1)
+ postcss-load-config: 6.0.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)
resolve-from: 5.0.0
- rollup: 4.60.1
+ rollup: 4.62.2
source-map: 0.7.6
sucrase: 3.35.0
tinyexec: 0.3.2
tinyglobby: 0.2.14
tree-kill: 1.2.2
optionalDependencies:
- '@swc/core': 1.15.26(@swc/helpers@0.5.21)
- postcss: 8.5.10
+ '@swc/core': 1.15.43(@swc/helpers@0.5.23)
+ postcss: 8.5.15
typescript: 6.0.3
transitivePeerDependencies:
- jiti
@@ -13319,27 +11982,12 @@ snapshots:
- tsx
- yaml
- tsx@4.21.0:
+ tsx@4.22.4:
dependencies:
- esbuild: 0.27.7
- get-tsconfig: 4.13.0
+ esbuild: 0.28.1
optionalDependencies:
fsevents: 2.3.3
- tunnel-agent@0.6.0:
- dependencies:
- safe-buffer: 5.2.1
-
- tweetnacl@0.14.5: {}
-
- type-detect@4.0.8: {}
-
- type-fest@0.21.3: {}
-
- type-fest@0.8.1: {}
-
- type-fest@4.41.0: {}
-
type-fest@5.6.0:
dependencies:
tagged-tag: 1.0.0
@@ -13381,7 +12029,7 @@ snapshots:
ua-is-frozen@0.1.2: {}
- ua-parser-js@2.0.9:
+ ua-parser-js@2.0.10:
dependencies:
detect-europe-js: 0.1.2
is-standalone-pwa: 0.1.1
@@ -13389,9 +12037,6 @@ snapshots:
ufo@1.6.1: {}
- uglify-js@3.19.3:
- optional: true
-
unbox-primitive@1.1.0:
dependencies:
call-bound: 1.0.4
@@ -13399,78 +12044,43 @@ snapshots:
has-symbols: 1.1.0
which-boxed-primitive: 1.1.1
- undici-types@7.19.2: {}
+ undici-types@8.3.0: {}
+
+ undici@7.25.0: {}
unicorn-magic@0.3.0: {}
universalify@0.1.2: {}
- universalify@2.0.1: {}
+ until-async@3.0.2: {}
- unrs-resolver@1.11.1:
+ update-browserslist-db@1.2.3(browserslist@4.28.4):
dependencies:
- napi-postinstall: 0.3.4
- optionalDependencies:
- '@unrs/resolver-binding-android-arm-eabi': 1.11.1
- '@unrs/resolver-binding-android-arm64': 1.11.1
- '@unrs/resolver-binding-darwin-arm64': 1.11.1
- '@unrs/resolver-binding-darwin-x64': 1.11.1
- '@unrs/resolver-binding-freebsd-x64': 1.11.1
- '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1
- '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1
- '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1
- '@unrs/resolver-binding-linux-arm64-musl': 1.11.1
- '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1
- '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1
- '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1
- '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1
- '@unrs/resolver-binding-linux-x64-gnu': 1.11.1
- '@unrs/resolver-binding-linux-x64-musl': 1.11.1
- '@unrs/resolver-binding-wasm32-wasi': 1.11.1
- '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1
- '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
- '@unrs/resolver-binding-win32-x64-msvc': 1.11.1
-
- untildify@4.0.0: {}
-
- update-browserslist-db@1.1.3(browserslist@4.25.2):
- dependencies:
- browserslist: 4.25.2
+ browserslist: 4.28.4
escalade: 3.2.0
picocolors: 1.1.1
- update-browserslist-db@1.2.3(browserslist@4.28.2):
+ use-intl@4.13.0(react@19.2.7):
dependencies:
- browserslist: 4.28.2
- escalade: 3.2.0
- picocolors: 1.1.1
-
- use-intl@4.9.1(react@19.2.5):
- dependencies:
- '@formatjs/fast-memoize': 3.1.2
+ '@formatjs/fast-memoize': 3.1.6
'@schummar/icu-type-parser': 1.21.5
- icu-minify: 4.9.1
- intl-messageformat: 11.2.1
- react: 19.2.5
+ icu-minify: 4.13.0
+ intl-messageformat: 11.2.8
+ react: 19.2.7
- use-sync-external-store@1.6.0(react@19.2.5):
+ use-sync-external-store@1.6.0(react@19.2.7):
dependencies:
- react: 19.2.5
+ react: 19.2.7
util-deprecate@1.0.2: {}
uuid@13.0.0: {}
uuid@8.3.2: {}
+ uuid@14.0.1: {}
v8-compile-cache-lib@3.0.1: {}
- v8-to-istanbul@9.3.0:
- dependencies:
- '@jridgewell/trace-mapping': 0.3.31
- '@types/istanbul-lib-coverage': 2.0.6
- convert-source-map: 2.0.0
-
valibot@1.2.0(typescript@6.0.3):
optionalDependencies:
typescript: 6.0.3
@@ -13482,18 +12092,67 @@ snapshots:
vary@1.1.2: {}
- verror@1.10.0:
+ vite@8.0.11(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.22.4):
dependencies:
- assert-plus: 1.0.0
- core-util-is: 1.0.2
- extsprintf: 1.3.0
+ lightningcss: 1.32.0
+ picomatch: 4.0.4
+ postcss: 8.5.15
+ rolldown: 1.0.0-rc.18
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ '@types/node': 26.0.1
+ esbuild: 0.28.1
+ fsevents: 2.3.3
+ jiti: 2.7.0
+ terser: 5.46.1
+ tsx: 4.22.4
- walker@1.0.8:
+ vitest@4.1.9(@types/node@26.0.1)(jsdom@29.1.1)(msw@2.14.6(@types/node@26.0.1)(typescript@6.0.3))(vite@8.0.11(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.22.4)):
dependencies:
- makeerror: 1.0.12
+ '@vitest/expect': 4.1.9
+ '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@26.0.1)(typescript@6.0.3))(vite@8.0.11(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.22.4))
+ '@vitest/pretty-format': 4.1.9
+ '@vitest/runner': 4.1.9
+ '@vitest/snapshot': 4.1.9
+ '@vitest/spy': 4.1.9
+ '@vitest/utils': 4.1.9
+ es-module-lexer: 2.1.0
+ expect-type: 1.3.0
+ magic-string: 0.30.21
+ obug: 2.1.3
+ pathe: 2.0.3
+ picomatch: 4.0.4
+ std-env: 4.1.0
+ tinybench: 2.9.0
+ tinyexec: 1.2.4
+ tinyglobby: 0.2.17
+ tinyrainbow: 3.1.0
+ vite: 8.0.11(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.22.4)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 26.0.1
+ jsdom: 29.1.1
+ transitivePeerDependencies:
+ - msw
+
+ w3c-xmlserializer@5.0.0:
+ dependencies:
+ xml-name-validator: 5.0.0
web-streams-polyfill@3.3.3: {}
+ webidl-conversions@8.0.1: {}
+
+ whatwg-mimetype@5.0.0: {}
+
+ whatwg-url@16.0.1:
+ dependencies:
+ '@exodus/bytes': 1.15.0
+ tr46: 6.0.0
+ webidl-conversions: 8.0.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
which-boxed-primitive@1.1.1:
dependencies:
is-bigint: 1.1.0
@@ -13545,13 +12204,10 @@ snapshots:
dependencies:
isexe: 2.0.0
- wordwrap@1.0.0: {}
-
- wrap-ansi@6.2.0:
+ why-is-node-running@2.3.0:
dependencies:
- ansi-styles: 4.3.0
- string-width: 4.2.3
- strip-ansi: 6.0.1
+ siginfo: 2.0.0
+ stackback: 0.0.2
wrap-ansi@7.0.0:
dependencies:
@@ -13567,10 +12223,9 @@ snapshots:
wrappy@1.0.2: {}
- write-file-atomic@5.0.1:
- dependencies:
- imurmurhash: 0.1.4
- signal-exit: 4.1.0
+ xml-name-validator@5.0.0: {}
+
+ xmlchars@2.2.0: {}
xtend@4.0.2: {}
@@ -13618,25 +12273,18 @@ snapshots:
y18n: 5.0.8
yargs-parser: 21.1.1
- yauzl@2.10.0:
- dependencies:
- buffer-crc32: 0.2.13
- fd-slicer: 1.1.0
-
yn@3.1.1: {}
- yocto-queue@0.1.0: {}
-
zeptomatch@2.1.0:
dependencies:
grammex: 3.1.12
graphmatch: 1.1.1
- zod@4.3.6: {}
+ zod@4.4.3: {}
- zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)):
+ zustand@5.0.14(@types/react@19.2.17)(immer@11.1.8)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)):
optionalDependencies:
- '@types/react': 19.2.14
- immer: 11.1.4
- react: 19.2.5
- use-sync-external-store: 1.6.0(react@19.2.5)
+ '@types/react': 19.2.17
+ immer: 11.1.8
+ react: 19.2.7
+ use-sync-external-store: 1.6.0(react@19.2.7)
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 5e236fb7f..4effe91c4 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,10 +1,12 @@
packages:
- '**'
-ignoredBuiltDependencies:
- - cypress
- - esbuild
- - sharp
-onlyBuiltDependencies:
- - '@prisma/client'
- - '@prisma/engines'
- - prisma
+ - '!dist'
+ - '!**/dist/**'
+allowBuilds:
+ '@parcel/watcher': false
+ '@prisma/engines': true
+ '@swc/core': false
+ esbuild: false
+ msw: false
+ prisma: true
+ sharp: false
diff --git a/podman/env.sample b/podman/env.sample
index 06435e56b..832161a04 100644
--- a/podman/env.sample
+++ b/podman/env.sample
@@ -10,6 +10,11 @@ DATABASE_TYPE=postgresql
# A secret string used by Umami (replace with a strong random string)
APP_SECRET=replace-me-with-a-random-string
+# Optional API base URL for internal UI API calls. Relative paths are served under BASE_PATH;
+# absolute URLs are called directly by the browser.
+# Examples: /internal-api or https://api.example.com/api
+API_URL=
+
# Postgres container defaults.
POSTGRES_DB=umami
POSTGRES_USER=umami
diff --git a/prisma/migrations/20_add_heatmap/migration.sql b/prisma/migrations/20_add_heatmap/migration.sql
new file mode 100644
index 000000000..4263c921c
--- /dev/null
+++ b/prisma/migrations/20_add_heatmap/migration.sql
@@ -0,0 +1,33 @@
+ALTER TABLE "website" RENAME COLUMN "replay_enabled" TO "recorder_enabled";
+
+UPDATE "website"
+SET "replay_config" = COALESCE("replay_config", '{}'::jsonb) || '{"replayEnabled": true}'::jsonb
+WHERE "recorder_enabled" = true;
+
+-- CreateTable
+CREATE TABLE "heatmap_event" (
+ "heatmap_event_id" UUID NOT NULL,
+ "website_id" UUID NOT NULL,
+ "session_id" UUID NOT NULL,
+ "visit_id" UUID NOT NULL,
+ "url_path" VARCHAR(500) NOT NULL,
+ "event_type" INTEGER NOT NULL,
+ "x" INTEGER,
+ "y" INTEGER,
+ "page_x" INTEGER,
+ "page_y" INTEGER,
+ "page_w" INTEGER,
+ "viewport_w" INTEGER,
+ "viewport_h" INTEGER,
+ "page_h" INTEGER,
+ "scroll_pct" INTEGER,
+ "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "heatmap_event_pkey" PRIMARY KEY ("heatmap_event_id")
+);
+
+-- CreateIndex
+CREATE INDEX "heatmap_event_website_id_idx" ON "heatmap_event"("website_id");
+CREATE INDEX "heatmap_event_visit_id_idx" ON "heatmap_event"("visit_id");
+CREATE INDEX "heatmap_event_website_id_created_at_idx" ON "heatmap_event"("website_id", "created_at");
+CREATE INDEX "heatmap_event_website_id_url_path_event_type_created_at_idx" ON "heatmap_event"("website_id", "url_path", "event_type", "created_at");
\ No newline at end of file
diff --git a/prisma/migrations/21_add_session_link/migration.sql b/prisma/migrations/21_add_session_link/migration.sql
new file mode 100644
index 000000000..1be077332
--- /dev/null
+++ b/prisma/migrations/21_add_session_link/migration.sql
@@ -0,0 +1,11 @@
+-- CreateTable
+CREATE TABLE "session_link" (
+ "website_id" UUID NOT NULL,
+ "session_id" UUID NOT NULL,
+ "distinct_id" VARCHAR(50) NOT NULL,
+ "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "session_link_pkey" PRIMARY KEY ("website_id","distinct_id","session_id")
+);
+
+CREATE INDEX "session_link_website_id_session_id_idx" ON "session_link"("website_id", "session_id");
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 2e391a482..2dcbd89d2 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -66,22 +66,35 @@ model Session {
@@map("session")
}
+model SessionLink {
+ websiteId String @map("website_id") @db.Uuid
+ sessionId String @map("session_id") @db.Uuid
+ distinctId String @map("distinct_id") @db.VarChar(50)
+ createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
+
+ @@id([websiteId, distinctId, sessionId])
+ @@index([websiteId, sessionId])
+ @@map("session_link")
+}
+
model Website {
- id String @id @map("website_id") @db.Uuid
- name String @db.VarChar(100)
- domain String? @db.VarChar(500)
- resetAt DateTime? @map("reset_at") @db.Timestamptz(6)
- userId String? @map("user_id") @db.Uuid
- createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
- updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
- deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
- createdBy String? @map("created_by") @db.Uuid
- teamId String? @map("team_id") @db.Uuid
- replayEnabled Boolean @default(false) @map("replay_enabled")
- replayConfig Json? @map("replay_config")
- user User? @relation("user", fields: [userId], references: [id])
- createUser User? @relation("createUser", fields: [createdBy], references: [id])
- team Team? @relation(fields: [teamId], references: [id])
+ id String @id() @map("website_id") @db.Uuid
+ name String @db.VarChar(100)
+ domain String? @db.VarChar(500)
+ resetAt DateTime? @map("reset_at") @db.Timestamptz(6)
+ userId String? @map("user_id") @db.Uuid
+ teamId String? @map("team_id") @db.Uuid
+ createdBy String? @map("created_by") @db.Uuid
+ createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
+ updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
+ deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
+
+ recorderEnabled Boolean @default(false) @map("recorder_enabled")
+ replayConfig Json? @map("replay_config")
+
+ user User? @relation("user", fields: [userId], references: [id])
+ createUser User? @relation("createUser", fields: [createdBy], references: [id])
+ team Team? @relation(fields: [teamId], references: [id])
eventData EventData[]
reports Report[]
revenue Revenue[]
@@ -89,6 +102,7 @@ model Website {
sessionData SessionData[]
sessionReplays SessionReplay[]
sessionReplaysSaved SessionReplaySaved[]
+ heatmapEvents HeatmapEvent[]
@@index([userId])
@@index([teamId])
diff --git a/public/images/country/sy.png b/public/images/country/sy.png
index fe0384257..5eb6add63 100644
Binary files a/public/images/country/sy.png and b/public/images/country/sy.png differ
diff --git a/public/intl/messages/ar-SA.json b/public/intl/messages/ar-SA.json
index dfae6b16d..6c817eb79 100644
--- a/public/intl/messages/ar-SA.json
+++ b/public/intl/messages/ar-SA.json
@@ -40,6 +40,7 @@
"campaign": "حملة",
"campaigns": "حملات",
"cancel": "إلغاء",
+ "close": "إغلاق",
"change-password": "تغيير كلمة المرور",
"channel": "قناة",
"channels": "قنوات",
@@ -102,7 +103,9 @@
"download": "تحميل",
"dropoff": "إنزال",
"duration": "المدة",
+ "min-duration-seconds": "الحد الأدنى للمدة (ثواني)",
"edit": "تعديل",
+ "design": "تصميم",
"edit-member": "عدّل العضو",
"email": "بريد إلكتروني",
"enable-share-url": "فعّل مشاركة الرابط",
@@ -124,6 +127,7 @@
"filter-combined": "مُجمّعة",
"filter-raw": "خام",
"filters": "التصفيات",
+ "filters-enabled": "الفلاتر مفعّلة",
"first-click": "النقرة الأولى",
"first-seen": "أول ظهور",
"funnel": "قمع",
@@ -300,6 +304,8 @@
"sum": "المجموع",
"support": "دعم",
"switch-account": "تبديل الحساب",
+ "switch-to-card-view": "التبديل إلى عرض البطاقات",
+ "switch-to-table-view": "التبديل إلى عرض الجدول",
"table": "جدول",
"tablet": "تابلت",
"tag": "الوسم",
@@ -379,7 +385,7 @@
"confirm-reset": "هل أنت متأكد من اعادة تعيين الإحصائيات لـ {target}؟",
"delete-team-warning": "سيؤدي حذف الفريق أيضًا إلى حذف كافة مواقع الفريق",
"delete-website-warning": "سيتم حذف كافة بيانات الموقع.",
- "empty-dashboard": "لوحة التحكم الخاصة بك فارغة. انقر فوق تحرير لإضافة مكونك الأول.",
+ "empty-dashboard": "لوحة التحكم الخاصة بك فارغة. انقر فوق تصميم لإضافة مكونك الأول.",
"error": "حدث خطأ ما.",
"event-log": "{event} في {url}",
"forbidden": "محظور",
@@ -416,6 +422,7 @@
"triggered-event": "أُطلق الحدث",
"unauthorized": "غير مصرح",
"user-deleted": "تم حذف المستخدم.",
+ "replay-min-duration-description": "إظهار التسجيلات ذات المدة الأطول أو المساوية لهذه المدة فقط.",
"viewed-page": "شوهدت الصفحة",
"visitor-log": "زائر من {country} يستخدم {browser} على {os} {device}"
}
diff --git a/public/intl/messages/be-BY.json b/public/intl/messages/be-BY.json
index 4c1d35461..3df2c2fa3 100644
--- a/public/intl/messages/be-BY.json
+++ b/public/intl/messages/be-BY.json
@@ -40,6 +40,7 @@
"campaign": "Кампанія",
"campaigns": "Кампаніі",
"cancel": "Адмена",
+ "close": "Закрыць",
"change-password": "Змяніць пароль",
"channel": "Канал",
"channels": "Каналы",
@@ -102,7 +103,9 @@
"download": "Спампаваць",
"dropoff": "Адмовы",
"duration": "Працягласць",
+ "min-duration-seconds": "Мінімальная працягласць (секунды)",
"edit": "Змяніць",
+ "design": "Дызайн",
"edit-member": "Рэдагаваць удзельніка",
"email": "Электронная пошта",
"enable-share-url": "Дазволіць дзяліцца спасылкай",
@@ -124,6 +127,7 @@
"filter-combined": "Камбініраваны",
"filter-raw": "Сырыя",
"filters": "Фільтры",
+ "filters-enabled": "Фільтры ўключаны",
"first-click": "Першы клік",
"first-seen": "Першы раз убачана",
"funnel": "Варонка",
@@ -300,6 +304,8 @@
"sum": "Сума",
"support": "Падтрымка",
"switch-account": "Змяніць акаўнт",
+ "switch-to-card-view": "Пераключыцца на выгляд картак",
+ "switch-to-table-view": "Пераключыцца на выгляд табліцы",
"table": "Табліца",
"tablet": "Планшэт",
"tag": "Тэг",
@@ -379,7 +385,7 @@
"confirm-reset": "Вы дакладна хочаце скінуць {target} статыстыку?",
"delete-team-warning": "Выдаленне каманды таксама выдаліць усе сайты каманды.",
"delete-website-warning": "Усе асацыяваныя дадзеныя будуць таксама выдалены.",
- "empty-dashboard": "Ваша панэль кіравання пустая. Націсніце Рэдагаваць, каб дадаць першы кампанент.",
+ "empty-dashboard": "Ваша панэль кіравання пустая. Націсніце Дызайн, каб дадаць першы кампанент.",
"error": "Нешта пайшло не так.",
"event-log": "{event} на {url}",
"forbidden": "Доступ забаронены",
@@ -416,6 +422,7 @@
"triggered-event": "Падзея якая спрацавала",
"unauthorized": "Не аўтарызаваны",
"user-deleted": "Карыстальнік выдалены.",
+ "replay-min-duration-description": "Паказваць толькі паўторы з такой або большай працягласцю.",
"viewed-page": "Праглядзеў старонку",
"visitor-log": "Наведвальнік з {country} праз {browser} на {os} {device}"
}
diff --git a/public/intl/messages/bg-BG.json b/public/intl/messages/bg-BG.json
index fa58a6878..89ae8be41 100644
--- a/public/intl/messages/bg-BG.json
+++ b/public/intl/messages/bg-BG.json
@@ -40,6 +40,7 @@
"campaign": "Кампания",
"campaigns": "Кампании",
"cancel": "Отмени",
+ "close": "Затвори",
"change-password": "Смени парола",
"channel": "Канал",
"channels": "Канали",
@@ -102,7 +103,9 @@
"download": "Изтегляне",
"dropoff": "Отпадане",
"duration": "Продължителност",
+ "min-duration-seconds": "Минимална продължителност (секунди)",
"edit": "Редактирай",
+ "design": "Дизайн",
"edit-member": "Редактирай член",
"email": "Имейл",
"enable-share-url": "Активирай Линк за споделяне",
@@ -124,6 +127,7 @@
"filter-combined": "Комбиниран",
"filter-raw": "Суров",
"filters": "Филтри",
+ "filters-enabled": "Филтрите са активирани",
"first-click": "Първо кликване",
"first-seen": "Първо видяно",
"funnel": "Фуния",
@@ -300,6 +304,8 @@
"sum": "Сума",
"support": "Поддръжка",
"switch-account": "Смяна на акаунт",
+ "switch-to-card-view": "Превключване към изглед на карти",
+ "switch-to-table-view": "Превключване към изглед на таблица",
"table": "Таблица",
"tablet": "Таблет",
"tag": "Етикет",
@@ -379,7 +385,7 @@
"confirm-reset": "Сигурни ли сте, че искате да нулирате {target}?",
"delete-team-warning": "Изтриването на екип ще изтрие и всички уебсайтове създадени от екипа.",
"delete-website-warning": "Всички данни за уебсайта ще бъдат изтрити.",
- "empty-dashboard": "Вашето табло е празно. Щракнете Редактиране, за да добавите първия си компонент.",
+ "empty-dashboard": "Вашето табло е празно. Щракнете Дизайн, за да добавите първия си компонент.",
"error": "Възникна грешка.",
"event-log": "{event} на {url}",
"forbidden": "Забранено",
@@ -416,6 +422,7 @@
"triggered-event": "Активирано събитие",
"unauthorized": "Неоторизиран достъп",
"user-deleted": "Потребителят е изтрит.",
+ "replay-min-duration-description": "Показвай само записи с тази или по-голяма продължителност.",
"viewed-page": "Страницата е видяна",
"visitor-log": "Посетител от {country}, използващ {browser} на {os} {device}"
}
diff --git a/public/intl/messages/bn-BD.json b/public/intl/messages/bn-BD.json
index 85c33ea21..35c5f160a 100644
--- a/public/intl/messages/bn-BD.json
+++ b/public/intl/messages/bn-BD.json
@@ -40,6 +40,7 @@
"campaign": "প্রচারণা",
"campaigns": "প্রচারণা",
"cancel": "বাতিল",
+ "close": "বন্ধ করুন",
"change-password": "পাসওয়ার্ড পরিবর্তন করুন",
"channel": "চ্যানেল",
"channels": "চ্যানেলসমূহ",
@@ -102,7 +103,9 @@
"download": "ডাউনলোড",
"dropoff": "ছেড়ে যাওয়া",
"duration": "সময়কাল",
+ "min-duration-seconds": "ন্যূনতম সময়কাল (সেকেন্ড)",
"edit": "সম্পাদনা করুন",
+ "design": "ডিজাইন",
"edit-member": "সদস্য সম্পাদনা করুন",
"email": "ইমেইল",
"enable-share-url": "শেয়ার ইউআরএল শেয়ার করুন",
@@ -124,6 +127,7 @@
"filter-combined": "সম্মিলিত",
"filter-raw": "অপরিশোধিত",
"filters": "ফিল্টারসমূহ",
+ "filters-enabled": "ফিল্টার সক্রিয়",
"first-click": "প্রথম ক্লিক",
"first-seen": "প্রথম দেখা",
"funnel": "ফানেল",
@@ -300,6 +304,8 @@
"sum": "যোগফল",
"support": "সহায়তা",
"switch-account": "অ্যাকাউন্ট পরিবর্তন",
+ "switch-to-card-view": "কার্ড ভিউতে স্যুইচ করুন",
+ "switch-to-table-view": "টেবিল ভিউতে স্যুইচ করুন",
"table": "টেবিল",
"tablet": "ট্যাবলেট",
"tag": "ট্যাগ",
@@ -379,7 +385,7 @@
"confirm-reset": "আপনি কি নিশ্চিত যে আপনি {target} এর পরিসংখ্যান পুনরায় সেট করতে চান?",
"delete-team-warning": "একটি টিম মুছে ফেললে টিমের সব ওয়েবসাইটও মুছে যাবে।",
"delete-website-warning": "সমস্ত সম্পর্কিত ডেটা পাশাপাশি মুছে ফেলা হবে।",
- "empty-dashboard": "আপনার ড্যাশবোর্ড খালি। প্রথম উপাদান যোগ করতে সম্পাদনা ক্লিক করুন।",
+ "empty-dashboard": "আপনার ড্যাশবোর্ড খালি। আপনার প্রথম উপাদান যোগ করতে ডিজাইন ক্লিক করুন.",
"error": "কিছু ভুল হয়েছে।",
"event-log": "{url}-এ {event}",
"forbidden": "নিষিদ্ধ",
@@ -416,6 +422,7 @@
"triggered-event": "ট্রিগার করা ইভেন্ট",
"unauthorized": "অননুমোদিত",
"user-deleted": "ব্যবহারকারী মুছে ফেলা হয়েছে।",
+ "replay-min-duration-description": "শুধুমাত্র এই সময়কাল বা তার উপরে রিপ্লেগুলি দেখান।",
"viewed-page": "দেখা পৃষ্ঠা",
"visitor-log": "{country} থেকে একজন ভিসিটর {ব্রাউজার}, ব্যবহার করছেন {os} {device} এর মধ্যে।"
}
diff --git a/public/intl/messages/bs-BA.json b/public/intl/messages/bs-BA.json
index a11f610bd..4ecfe3754 100644
--- a/public/intl/messages/bs-BA.json
+++ b/public/intl/messages/bs-BA.json
@@ -39,6 +39,7 @@
"campaign": "Kampanja",
"campaigns": "Kampanje",
"cancel": "Otkaži",
+ "close": "Zatvori",
"change-password": "Promijeni šifru",
"channel": "Kanal",
"channels": "Kanali",
@@ -103,7 +104,9 @@
"download": "Preuzmi",
"dropoff": "Odlazak",
"duration": "Trajanje",
+ "min-duration-seconds": "Minimalno trajanje (sekunde)",
"edit": "Uredi",
+ "design": "Dizajn",
"edit-member": "Uredi člana",
"email": "E-mail",
"enable-share-url": "Omogući URL za dijeljenje",
@@ -125,6 +128,7 @@
"filter-combined": "Kombinovano",
"filter-raw": "Sirovo",
"filters": "Filtri",
+ "filters-enabled": "Filteri omogućeni",
"first-click": "Prvi klik",
"first-seen": "Prvi put viđeno",
"funnel": "Lijevak",
@@ -255,7 +259,7 @@
"play": "Reprodukuj",
"replay": "Ponovna reprodukcija",
"replay-id": "Replay ID",
- "replay-code": "Kod ponavljanja sesije",
+ "recorder-code": "Kod ponavljanja sesije",
"replay-enabled": "Ponovna reprodukcija omogućena",
"replays": "Ponovne reprodukcije",
"reports": "Izvještaji",
@@ -304,6 +308,8 @@
"sum": "Suma",
"support": "Podrška",
"switch-account": "Promijeni račun",
+ "switch-to-card-view": "Prebaci na prikaz kartica",
+ "switch-to-table-view": "Prebaci na tabelarni prikaz",
"table": "Tabela",
"tablet": "Tablet",
"tag": "Oznaka",
@@ -390,7 +396,7 @@
"delete-team-warning": "Brisanje tima će također obrisati sve web stranice tima.",
"delete-website-warning": "Svi podaci web stranice biće obrisani.",
"error": "Nešto je pošlo po zlu.",
- "empty-dashboard": "Vaša kontrolna tabla je prazna. Kliknite Uredi da dodate prvu komponentu.",
+ "empty-dashboard": "Vaša kontrolna tabla je prazna. Kliknite Dizajn da dodate prvu komponentu.",
"event-log": "{event} na {url}",
"forbidden": "Zabranjeno",
"go-to-settings": "Idi na postavke",
@@ -427,6 +433,7 @@
"triggered-event": "Trigerovani događaj",
"unauthorized": "Neautorizirano",
"user-deleted": "Korisnik obrisan.",
+ "replay-min-duration-description": "Prikaži samo snimke s ovim ili dužim trajanjem.",
"viewed-page": "Pogledana stranica",
"upgrade-required": "Ova funkcija zahtijeva pretplatu na plan {plan}.",
"visitor-log": "Posjetitelj iz {country} koristi {browser} na {os} {device}"
diff --git a/public/intl/messages/ca-ES.json b/public/intl/messages/ca-ES.json
index 0a804c0e1..b63429c90 100644
--- a/public/intl/messages/ca-ES.json
+++ b/public/intl/messages/ca-ES.json
@@ -39,6 +39,7 @@
"campaign": "Campanya",
"campaigns": "Campanyes",
"cancel": "Cancel·la",
+ "close": "Tanca",
"change-password": "Canvia la contrasenya",
"channel": "Canal",
"channels": "Canals",
@@ -103,7 +104,9 @@
"download": "Descarregar",
"dropoff": "Abandonament",
"duration": "Durada",
+ "min-duration-seconds": "Durada mínima (segons)",
"edit": "Edita",
+ "design": "Disseny",
"edit-member": "Edita membre",
"email": "Correu electrònic",
"enable-share-url": "Activa l'enllaç per compartir",
@@ -125,6 +128,7 @@
"filter-combined": "Combinat",
"filter-raw": "En cru",
"filters": "Filtres",
+ "filters-enabled": "Filtres activats",
"first-click": "Primer clic",
"first-seen": "Vist per primer cop",
"funnel": "Embut",
@@ -255,7 +259,7 @@
"play": "Reprodueix",
"replay": "Repetició",
"replay-id": "Replay ID",
- "replay-code": "Codi de repetició de sessió",
+ "recorder-code": "Codi de repetició de sessió",
"replay-enabled": "Repetició activada",
"replays": "Repeticions",
"reports": "Informes",
@@ -304,6 +308,8 @@
"sum": "Suma",
"support": "Suport",
"switch-account": "Canviar de compte",
+ "switch-to-card-view": "Canvia a la vista de targetes",
+ "switch-to-table-view": "Canvia a la vista de taula",
"table": "Taula",
"tablet": "Tauleta",
"tag": "Etiqueta",
@@ -390,7 +396,7 @@
"delete-team-warning": "Al eliminar un equip també s'eliminaran tots els llocs web de l'equip.",
"delete-website-warning": "També s'esborraran totes les dades relacionades.",
"error": "S'ha produït un error.",
- "empty-dashboard": "El teu tauler és buit. Fes clic a Edita per afegir el teu primer component.",
+ "empty-dashboard": "El teu tauler és buit. Fes clic a Disseny per afegir el teu primer component.",
"event-log": "{event} a {url}",
"forbidden": "Prohibit",
"go-to-settings": "Vés a la configuració",
@@ -427,6 +433,7 @@
"triggered-event": "Esdeveniment desencadenat",
"unauthorized": "No autoritzat",
"user-deleted": "Usuari eliminat.",
+ "replay-min-duration-description": "Mostra només les reproduccions amb aquesta durada o superior.",
"viewed-page": "Pàgina vista",
"upgrade-required": "Aquesta funció requereix una subscripció al pla {plan}.",
"visitor-log": "Visitant de {country} usant {browser} a {os} {device}"
diff --git a/public/intl/messages/cs-CZ.json b/public/intl/messages/cs-CZ.json
index 764dfd2ac..92a4a43a4 100644
--- a/public/intl/messages/cs-CZ.json
+++ b/public/intl/messages/cs-CZ.json
@@ -39,6 +39,7 @@
"campaign": "Kampaň",
"campaigns": "Kampaně",
"cancel": "Zrušit",
+ "close": "Zavřít",
"change-password": "Změnit heslo",
"channel": "Kanál",
"channels": "Kanály",
@@ -103,7 +104,9 @@
"download": "Stáhnout",
"dropoff": "Opuštění",
"duration": "Trvání",
+ "min-duration-seconds": "Minimální délka (sekundy)",
"edit": "Upravit",
+ "design": "Návrh",
"edit-member": "Upravit člena",
"email": "E-mail",
"enable-share-url": "Povolit sdílení URL",
@@ -125,6 +128,7 @@
"filter-combined": "Kombinace",
"filter-raw": "Nezpracované",
"filters": "Filtry",
+ "filters-enabled": "Filtry povoleny",
"first-click": "První kliknutí",
"first-seen": "Poprvé viděno",
"funnel": "Trychtýř",
@@ -255,7 +259,7 @@
"play": "Přehrát",
"replay": "Přehrávání",
"replay-id": "Replay ID",
- "replay-code": "Kód přehrání relace",
+ "recorder-code": "Kód přehrání relace",
"replay-enabled": "Přehrávání povoleno",
"replays": "Přehrávání",
"reports": "Hlášení",
@@ -304,6 +308,8 @@
"sum": "Součet",
"support": "Podpora",
"switch-account": "Přepnout účet",
+ "switch-to-card-view": "Přepnout na zobrazení karet",
+ "switch-to-table-view": "Přepnout na zobrazení tabulky",
"table": "Tabulka",
"tablet": "Tablet",
"tag": "Štítek",
@@ -390,7 +396,7 @@
"delete-team-warning": "Smazání týmu smaže také všechny weby týmu.",
"delete-website-warning": "Všechna související data budou také smazána.",
"error": "Něco se pokazilo.",
- "empty-dashboard": "Váš řídicí panel je prázdný. Klikněte na Upravit a přidejte první komponentu.",
+ "empty-dashboard": "Váš řídicí panel je prázdný. Klikněte na Návrh a přidejte první komponentu.",
"event-log": "{event} na {url}",
"forbidden": "Zakázáno",
"go-to-settings": "Jít do nastavení",
@@ -427,6 +433,7 @@
"triggered-event": "Spuštěná událost",
"unauthorized": "Neautorizováno",
"user-deleted": "Uživatel smazán.",
+ "replay-min-duration-description": "Zobrazit pouze záznamy s touto nebo delší délkou.",
"viewed-page": "Zobrazená stránka",
"upgrade-required": "Tato funkce vyžaduje předplatné plánu {plan}.",
"visitor-log": "Návštěvník z {country} s prohlížečem {browser} na {os} {device}"
diff --git a/public/intl/messages/da-DK.json b/public/intl/messages/da-DK.json
index 2f005c7f6..9bda981ef 100644
--- a/public/intl/messages/da-DK.json
+++ b/public/intl/messages/da-DK.json
@@ -39,6 +39,7 @@
"campaign": "Kampagne",
"campaigns": "Kampagner",
"cancel": "Afvis",
+ "close": "Luk",
"change-password": "Skift adgangskode",
"channel": "Kanal",
"channels": "Kanaler",
@@ -103,7 +104,9 @@
"download": "Download",
"dropoff": "Frafald",
"duration": "Varighed",
+ "min-duration-seconds": "Minimumsvarighed (sekunder)",
"edit": "Rediger",
+ "design": "Design",
"edit-member": "Rediger medlem",
"email": "E-mail",
"enable-share-url": "Aktivér delings-URL",
@@ -125,6 +128,7 @@
"filter-combined": "Kombineret",
"filter-raw": "Rå",
"filters": "Filtre",
+ "filters-enabled": "Filtre aktiveret",
"first-click": "Første klik",
"first-seen": "Først set",
"funnel": "Tragt",
@@ -255,7 +259,7 @@
"play": "Afspil",
"replay": "Afspilning",
"replay-id": "Replay ID",
- "replay-code": "Kode til sessionsgengivelse",
+ "recorder-code": "Kode til sessionsgengivelse",
"replay-enabled": "Afspilning aktiveret",
"replays": "Afspilninger",
"reports": "Rapporter",
@@ -304,6 +308,8 @@
"sum": "Sum",
"support": "Support",
"switch-account": "Skift konto",
+ "switch-to-card-view": "Skift til kortvisning",
+ "switch-to-table-view": "Skift til tabelvisning",
"table": "Tabel",
"tablet": "Tablet",
"tag": "Tag",
@@ -390,7 +396,7 @@
"delete-team-warning": "Sletning af et hold sletter også alle holdets websites.",
"delete-website-warning": "Alle tilknyttede data slettes også.",
"error": "Noget gik galt.",
- "empty-dashboard": "Dit dashboard er tomt. Klik på Rediger for at tilføje din første komponent.",
+ "empty-dashboard": "Dit dashboard er tomt. Klik på Design for at tilføje din første komponent.",
"event-log": "{event} på {url}",
"forbidden": "Forbudt",
"go-to-settings": "Gå til betjeningspanel",
@@ -427,6 +433,7 @@
"triggered-event": "Udløst begivenhed",
"unauthorized": "Ikke autoriseret",
"user-deleted": "Bruger slettet.",
+ "replay-min-duration-description": "Vis kun gengivelser med denne varighed eller derover.",
"viewed-page": "Vist side",
"upgrade-required": "Denne funktion kræver et {plan}-abonnement.",
"visitor-log": "Besøgende fra {country} bruger {browser} på {os} {device}"
diff --git a/public/intl/messages/de-CH.json b/public/intl/messages/de-CH.json
index 50fedf16c..1b41f8310 100644
--- a/public/intl/messages/de-CH.json
+++ b/public/intl/messages/de-CH.json
@@ -39,6 +39,7 @@
"campaign": "Kampagne",
"campaigns": "Kampagne",
"cancel": "Abbreche",
+ "close": "Schliessen",
"change-password": "Passwort ändere",
"channel": "Kanal",
"channels": "Kanäle",
@@ -103,7 +104,9 @@
"download": "Herunterladen",
"dropoff": "Absprung",
"duration": "Dauer",
+ "min-duration-seconds": "Mindestdauer (Sekunden)",
"edit": "Bearbeite",
+ "design": "Design",
"edit-member": "Mitglied bearbeite",
"email": "E-Mail",
"enable-share-url": "Freigab-URL aktiviere",
@@ -125,6 +128,7 @@
"filter-combined": "Kombiniert",
"filter-raw": "Rohdate",
"filters": "Filter",
+ "filters-enabled": "Filter aktiviert",
"first-click": "Erste Klick",
"first-seen": "Erstmal gse",
"funnel": "Tunnel",
@@ -255,7 +259,7 @@
"play": "Abspielen",
"replay": "Wiedergabe",
"replay-id": "Replay ID",
- "replay-code": "Session-Replay-Code",
+ "recorder-code": "Session-Replay-Code",
"replay-enabled": "Wiedergabe aktiviert",
"replays": "Wiedergaben",
"reports": "Brichte",
@@ -304,6 +308,8 @@
"sum": "Summe",
"support": "Support",
"switch-account": "Konto wechseln",
+ "switch-to-card-view": "Zur Kartenansicht wechseln",
+ "switch-to-table-view": "Zur Tabellenansicht wechseln",
"table": "Tabelle",
"tablet": "Tablet",
"tag": "Tag",
@@ -390,7 +396,7 @@
"delete-team-warning": "Es Team lösche dued ebefalls alli team Websiite lösche.",
"delete-website-warning": "Alli dezueghörige Date werded ebefalls glöscht.",
"error": "Es isch en Fehler ufträte.",
- "empty-dashboard": "Ihr Dashboard ist leer. Klicken Sie auf Bearbeiten, um Ihre erste Komponente hinzuzufügen.",
+ "empty-dashboard": "Ihr Dashboard ist leer. Klicken Sie auf Design, um Ihre erste Komponente hinzuzufügen.",
"event-log": "{event} uf {url}",
"forbidden": "Verboten",
"go-to-settings": "Zu de Istellige",
@@ -427,6 +433,7 @@
"triggered-event": "Usglösts Ereigniss",
"unauthorized": "Nicht autorisiert",
"user-deleted": "Bnutzer glöscht.",
+ "replay-min-duration-description": "Nur Wiedergaben mit dieser oder längerer Dauer anzeigen.",
"viewed-page": "Siite agluegt",
"upgrade-required": "Diese Funktion erfordert ein {plan}-Abonnement.",
"visitor-log": "Bsuecher us {country} nutzt {browser} uf {os} {device}"
diff --git a/public/intl/messages/de-DE.json b/public/intl/messages/de-DE.json
index cb422375a..0eef1b012 100644
--- a/public/intl/messages/de-DE.json
+++ b/public/intl/messages/de-DE.json
@@ -39,6 +39,7 @@
"campaign": "Kampagne",
"campaigns": "Kampagnen",
"cancel": "Abbrechen",
+ "close": "Schliessen",
"change-password": "Passwort ändern",
"channel": "Kanal",
"channels": "Kanäle",
@@ -47,8 +48,8 @@
"city": "Stadt",
"clear-all": "Alles löschen",
"cls": "CLS",
- "cohort": "Gruppe",
- "cohorts": "Kohorten",
+ "cohort": "Nutzergruppe",
+ "cohorts": "Nutzergruppen",
"compare": "Vergleichen",
"compare-dates": "Daten vergleichen",
"confirm": "Bestätigen",
@@ -103,7 +104,9 @@
"download": "Herunterladen",
"dropoff": "Absprung",
"duration": "Dauer",
+ "min-duration-seconds": "Mindestdauer (Sekunden)",
"edit": "Bearbeiten",
+ "design": "Design",
"edit-member": "Mitglied bearbeiten",
"email": "E-Mail",
"enable-share-url": "Freigabe-URL aktivieren",
@@ -125,11 +128,12 @@
"filter-combined": "Kombiniert",
"filter-raw": "Rohdaten",
"filters": "Filter",
+ "filters-enabled": "Filter aktiviert",
"first-click": "Erster Klick",
"first-seen": "Erstmalig gesehen",
- "funnel": "Trichter",
+ "funnel": "Funnel",
"funnel-description": "Verstehen Sie die Konversions- und Absprungrate Ihrer Nutzer.",
- "funnels": "Trichter",
+ "funnels": "Funnels",
"goal": "Ziel",
"goals": "Ziele",
"good": "Gut",
@@ -138,6 +142,7 @@
"greater-than-equals": "Größer oder gleich",
"grouped": "Gruppiert",
"growth": "Wachstum",
+ "heatmaps": "Heatmaps",
"hostname": "Hostname",
"hour": "Stunde",
"includes": "Enthält",
@@ -156,7 +161,7 @@
"join-team": "Team beitreten",
"journey": "Reise",
"journey-description": "Verstehen Sie, wie Nutzer auf Ihrer Website navigieren.",
- "journeys": "Reisen",
+ "journeys": "Nutzerpfade",
"language": "Sprache",
"languages": "Sprachen",
"laptop": "Laptop",
@@ -255,14 +260,14 @@
"play": "Abspielen",
"replay": "Wiedergabe",
"replay-id": "Replay ID",
- "replay-code": "Session-Replay-Code",
+ "recorder-code": "Session-Replay-Code",
"replay-enabled": "Wiedergabe aktiviert",
"replays": "Wiedergaben",
"reports": "Berichte",
"required": "Erforderlich",
"reset": "Zurücksetzen",
"reset-website": "Statistik zurücksetzen",
- "retention": "Erhalt",
+ "retention": "Nutzerbindung",
"retention-days": "Aufbewahrungstage",
"retention-description": "Messen Sie die Verweildauer auf Ihrer Website, indem Sie verfolgen, wie oft die Nutzer zurückkehren.",
"revenue": "Umsatz",
@@ -270,7 +275,7 @@
"role": "Rolle",
"run-query": "Abfrage starten",
"save": "Speichern",
- "save-cohort": "Kohorte speichern",
+ "save-cohort": "Nutzergruppe speichern",
"save-replay": "Replay speichern",
"save-segment": "Segment speichern",
"sample-rate": "Abtastrate",
@@ -304,6 +309,8 @@
"sum": "Summe",
"support": "Support",
"switch-account": "Konto wechseln",
+ "switch-to-card-view": "Zur Kartenansicht wechseln",
+ "switch-to-table-view": "Zur Tabellenansicht wechseln",
"table": "Tabelle",
"tablet": "Tablet",
"tag": "Tag",
@@ -390,7 +397,7 @@
"delete-team-warning": "Ein Team zu löschen, wird auch alle Team-Websites löschen.",
"delete-website-warning": "Alle zugehörigen Daten werden ebenfalls gelöscht.",
"error": "Es ist ein Fehler aufgetreten.",
- "empty-dashboard": "Ihr Dashboard ist leer. Klicken Sie auf Bearbeiten, um Ihre erste Komponente hinzuzufügen.",
+ "empty-dashboard": "Ihr Dashboard ist leer. Klicken Sie auf Design, um Ihre erste Komponente hinzuzufügen.",
"event-log": "{event} auf {url}",
"forbidden": "Verboten",
"go-to-settings": "Zu den Einstellungen",
@@ -427,6 +434,7 @@
"triggered-event": "Ereignis ausgelöst",
"unauthorized": "Nicht autorisiert",
"user-deleted": "Benutzer gelöscht.",
+ "replay-min-duration-description": "Nur Wiedergaben mit dieser oder längerer Dauer anzeigen.",
"viewed-page": "Seite besucht",
"upgrade-required": "Diese Funktion erfordert ein {plan}-Abonnement.",
"visitor-log": "Besucher aus {country} benutzt {browser} auf {os} {device}"
diff --git a/public/intl/messages/el-GR.json b/public/intl/messages/el-GR.json
index 199becdc4..da2288e0b 100644
--- a/public/intl/messages/el-GR.json
+++ b/public/intl/messages/el-GR.json
@@ -40,6 +40,7 @@
"campaign": "Καμπάνια",
"campaigns": "Καμπάνιες",
"cancel": "Ακύρωση",
+ "close": "Κλείσιμο",
"change-password": "Αλλαγή κωδικού",
"channel": "Κανάλι",
"channels": "Κανάλια",
@@ -102,7 +103,9 @@
"download": "Λήψη",
"dropoff": "Εγκατάλειψη",
"duration": "Διάρκεια",
+ "min-duration-seconds": "Ελάχιστη διάρκεια (δευτερόλεπτα)",
"edit": "Επεξεργασία",
+ "design": "Σχεδίαση",
"edit-member": "Επεξεργασία μέλους",
"email": "Email",
"enable-share-url": "Ενεργοποίηση κοινής χρήσης URL",
@@ -124,6 +127,7 @@
"filter-combined": "Σε συνδυασμό",
"filter-raw": "Ακατέργαστο",
"filters": "Φίλτρα",
+ "filters-enabled": "Φίλτρα ενεργοποιημένα",
"first-click": "Πρώτο κλικ",
"first-seen": "Πρώτη εμφάνιση",
"funnel": "Χωνί",
@@ -300,6 +304,8 @@
"sum": "Άθροισμα",
"support": "Υποστήριξη",
"switch-account": "Αλλαγή λογαριασμού",
+ "switch-to-card-view": "Εναλλαγή σε προβολή καρτών",
+ "switch-to-table-view": "Εναλλαγή σε προβολή πίνακα",
"table": "Πίνακας",
"tablet": "Τάμπλετ",
"tag": "Ετικέτα",
@@ -379,7 +385,7 @@
"confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"delete-team-warning": "Η διαγραφή μιας ομάδας θα διαγράψει επίσης όλους τους ιστότοπους της ομάδας.",
"delete-website-warning": "Όλα τα σχετικά δεδομένα θα διαγραφούν επίσης.",
- "empty-dashboard": "Ο πίνακας ελέγχου σας είναι κενός. Κάντε κλικ στην Επεξεργασία για να προσθέσετε το πρώτο σας στοιχείο.",
+ "empty-dashboard": "Ο πίνακας ελέγχου σας είναι κενός. Κάντε κλικ στη Σχεδίαση για να προσθέσετε το πρώτο σας στοιχείο.",
"error": "Κάτι πήγε στραβά.",
"event-log": "{event} στο {url}",
"forbidden": "Απαγορευμένο",
@@ -416,6 +422,7 @@
"triggered-event": "Ενεργοποιημένο συμβάν",
"unauthorized": "Μη εξουσιοδοτημένο",
"user-deleted": "Ο χρήστης διαγράφηκε.",
+ "replay-min-duration-description": "Εμφάνιση μόνο αναπαραγωγών με αυτή ή μεγαλύτερη διάρκεια.",
"viewed-page": "Προβολή σελίδας",
"visitor-log": "Επισκέπτης από {country} χρησιμοποιώντας {browser} σε {os} {device}"
}
diff --git a/public/intl/messages/en-GB.json b/public/intl/messages/en-GB.json
index 9de60575c..1f6fccdf8 100644
--- a/public/intl/messages/en-GB.json
+++ b/public/intl/messages/en-GB.json
@@ -39,6 +39,7 @@
"campaign": "Campaign",
"campaigns": "Campaigns",
"cancel": "Cancel",
+ "close": "Close",
"change-password": "Change password",
"channel": "Channel",
"channels": "Channels",
@@ -103,7 +104,9 @@
"download": "Download",
"dropoff": "Dropoff",
"duration": "Duration",
+ "min-duration-seconds": "Minimum duration (seconds)",
"edit": "Edit",
+ "design": "Design",
"edit-member": "Edit member",
"email": "Email",
"enable-share-url": "Enable share URL",
@@ -125,6 +128,7 @@
"filter-combined": "Combined",
"filter-raw": "Raw",
"filters": "Filters",
+ "filters-enabled": "Filters enabled",
"first-click": "First click",
"first-seen": "First seen",
"funnel": "Funnel",
@@ -255,7 +259,7 @@
"play": "Play",
"replay": "Replay",
"replay-id": "Replay ID",
- "replay-code": "Session replay code",
+ "recorder-code": "Recorder code",
"replay-enabled": "Replay enabled",
"replays": "Replays",
"reports": "Reports",
@@ -304,6 +308,8 @@
"sum": "Sum",
"support": "Support",
"switch-account": "Switch account",
+ "switch-to-card-view": "Switch to card view",
+ "switch-to-table-view": "Switch to table view",
"table": "Table",
"tablet": "Tablet",
"tag": "Tag",
@@ -390,7 +396,7 @@
"delete-team-warning": "Deleting a team will also delete all team websites.",
"delete-website-warning": "All associated data will be deleted as well.",
"error": "Something went wrong.",
- "empty-dashboard": "Your dashboard is empty. Click Edit to add your first component.",
+ "empty-dashboard": "Your dashboard is empty. Click Design to add your first component.",
"event-log": "{event} on {url}",
"forbidden": "Forbidden",
"go-to-settings": "Go to settings",
@@ -427,6 +433,7 @@
"triggered-event": "Triggered event",
"unauthorized": "Unauthorized",
"user-deleted": "User deleted.",
+ "replay-min-duration-description": "Only show replays at or above this duration.",
"viewed-page": "Viewed page",
"upgrade-required": "This feature requires a {plan} plan subscription.",
"visitor-log": "Visitor from {country} using {browser} on {os} {device}"
diff --git a/public/intl/messages/en-US.json b/public/intl/messages/en-US.json
index 980c6c02e..2091ce2be 100644
--- a/public/intl/messages/en-US.json
+++ b/public/intl/messages/en-US.json
@@ -39,6 +39,7 @@
"campaign": "Campaign",
"campaigns": "Campaigns",
"cancel": "Cancel",
+ "close": "Close",
"change-password": "Change password",
"channel": "Channel",
"channels": "Channels",
@@ -71,6 +72,7 @@
"created": "Created",
"created-by": "Created By",
"criteria": "Criteria",
+ "cumulative": "Cumulative",
"currency": "Currency",
"current": "Current",
"current-password": "Current password",
@@ -103,7 +105,9 @@
"download": "Download",
"dropoff": "Dropoff",
"duration": "Duration",
+ "min-duration-seconds": "Minimum duration (seconds)",
"edit": "Edit",
+ "design": "Design",
"edit-member": "Edit member",
"email": "Email",
"enable-share-url": "Enable share URL",
@@ -125,7 +129,9 @@
"filter-combined": "Combined",
"filter-raw": "Raw",
"filters": "Filters",
+ "filters-enabled": "Filters enabled",
"property-filter": "Property Filter",
+ "activity-by-property": "Activity by property",
"first-click": "First click",
"first-seen": "First seen",
"funnel": "Funnel",
@@ -139,6 +145,9 @@
"greater-than-equals": "Greater than or equals",
"grouped": "Grouped",
"growth": "Growth",
+ "heatmap": "Heatmap",
+ "heatmap-description": "Visualize where visitors click and how far they scroll on your pages.",
+ "heatmaps": "Heatmaps",
"hostname": "Hostname",
"hour": "Hour",
"includes": "Includes",
@@ -175,6 +184,7 @@
"links": "Links",
"location": "Location",
"login": "Login",
+ "llm": "LLM",
"logout": "Logout",
"manage": "Manage",
"manager": "Manager",
@@ -216,6 +226,7 @@
"page-of": "Page {current} of {total}",
"page-views": "Page views",
"pageTitle": "Page title",
+ "period": "Period",
"pages": "Pages",
"paid-ads": "Paid ads",
"paid-search": "Paid search",
@@ -256,7 +267,7 @@
"play": "Play",
"replay": "Replay",
"replay-id": "Replay ID",
- "replay-code": "Session replay code",
+ "recorder-code": "Recorder code",
"replay-enabled": "Replay enabled",
"replays": "Replays",
"reports": "Reports",
@@ -306,6 +317,8 @@
"sum": "Sum",
"support": "Support",
"switch-account": "Switch account",
+ "switch-to-card-view": "Switch to card view",
+ "switch-to-table-view": "Switch to table view",
"table": "Table",
"tablet": "Tablet",
"tag": "Tag",
@@ -422,7 +435,7 @@
"delete-team-warning": "Deleting a team will also delete all team websites.",
"delete-website-warning": "All website data will be deleted.",
"error": "Something went wrong.",
- "empty-dashboard": "Your dashboard is empty. Click Edit to add your first component.",
+ "empty-dashboard": "Your dashboard is empty. Click Design to add your first component.",
"event-log": "{event} on {url}",
"forbidden": "Forbidden",
"go-to-settings": "Go to settings",
@@ -459,6 +472,7 @@
"triggered-event": "Triggered event",
"unauthorized": "Unauthorized",
"user-deleted": "User deleted.",
+ "replay-min-duration-description": "Only show replays at or above this duration.",
"viewed-page": "Viewed page",
"upgrade-required": "This feature requires a {plan} plan subscription.",
"visitor-log": "Visitor from {country} using {browser} on {os} {device}",
diff --git a/public/intl/messages/es-ES.json b/public/intl/messages/es-ES.json
index 31861c0b9..920f24d09 100644
--- a/public/intl/messages/es-ES.json
+++ b/public/intl/messages/es-ES.json
@@ -39,6 +39,7 @@
"campaign": "Campaña",
"campaigns": "Campañas",
"cancel": "Cancelar",
+ "close": "Cerrar",
"change-password": "Cambiar contraseña",
"channel": "Canal",
"channels": "Canales",
@@ -71,6 +72,7 @@
"created": "Creado",
"created-by": "Creado por",
"criteria": "Criterios",
+ "cumulative": "Acumulativo",
"currency": "Moneda",
"current": "Actual",
"current-password": "Contraseña actual",
@@ -103,7 +105,9 @@
"download": "Descargar",
"dropoff": "Abandono",
"duration": "Duración",
+ "min-duration-seconds": "Duración mínima (segundos)",
"edit": "Editar",
+ "design": "Diseño",
"edit-member": "Editar miembro",
"email": "Correo electrónico",
"enable-share-url": "Habilitar compartir URL",
@@ -125,6 +129,9 @@
"filter-combined": "Combinado",
"filter-raw": "En crudo",
"filters": "Filtros",
+ "filters-enabled": "Filtros activados",
+ "property-filter": "Filtro de propiedades",
+ "activity-by-property": "Actividad por propiedad",
"first-click": "Primer clic",
"first-seen": "Primera vez visto",
"funnel": "Embudo",
@@ -138,6 +145,9 @@
"greater-than-equals": "Mayor que o igual a",
"grouped": "Agrupado",
"growth": "Crecimiento",
+ "heatmap": "Mapa de calor",
+ "heatmap-description": "Visualiza donde los visitantes hacen clic y cuánto se desplazan por sus páginas.",
+ "heatmaps": "Mapas de calor",
"hostname": "Nombre de host",
"hour": "Hora",
"includes": "Incluye",
@@ -173,6 +183,7 @@
"link": "Enlace",
"links": "Enlaces",
"location": "Ubicación",
+ "llm": "LLM",
"login": "Iniciar sesión",
"logout": "Cerrar sesión",
"manage": "Administrar",
@@ -216,6 +227,7 @@
"page-views": "Vistas",
"pageTitle": "Título de página",
"pages": "Páginas",
+ "period": "Periodo",
"paid-ads": "Anuncios pagados",
"paid-search": "Búsqueda pagada",
"paid-shopping": "Compras pagadas",
@@ -255,7 +267,7 @@
"play": "Reproducir",
"replay": "Repetición",
"replay-id": "Replay ID",
- "replay-code": "Código de repetición de sesión",
+ "recorder-code": "Código de repetición de sesión",
"replay-enabled": "Repetición activada",
"replays": "Repeticiones",
"reports": "Informes",
@@ -287,6 +299,7 @@
"select-filter": "Seleccionar filtro",
"select-role": "Seleccionar rol",
"select-component": "Seleccionar componente",
+ "select-event": "Seleccionar evento",
"select-website": "Seleccionar sitio web",
"session": "Sesión",
"session-data": "Datos de sesión",
@@ -304,6 +317,8 @@
"sum": "Suma",
"support": "Soporte",
"switch-account": "Cambiar cuenta",
+ "switch-to-card-view": "Cambiar a vista de tarjetas",
+ "switch-to-table-view": "Cambiar a vista de tabla",
"table": "Tabla",
"tablet": "Tableta",
"tag": "Etiqueta",
@@ -392,7 +407,7 @@
"delete-team-warning": "Al eliminar un equipo, también se eliminarán todos los sitios web del equipo.",
"delete-website-warning": "Toda la información relacionada será eliminada.",
"error": "Algo falló.",
- "empty-dashboard": "Tu panel está vacío. Haz clic en Editar para añadir tu primer componente.",
+ "empty-dashboard": "Tu panel está vacío. Haz clic en Diseño para añadir tu primer componente.",
"event-log": "{event} en {url}",
"forbidden": "Prohibido",
"go-to-settings": "Ir a la configuración",
@@ -429,6 +444,7 @@
"triggered-event": "Evento lanzado",
"unauthorized": "No autorizado",
"user-deleted": "Usuario eliminado.",
+ "replay-min-duration-description": "Mostrar solo las repeticiones con esta duración o superior.",
"viewed-page": "Página vista",
"upgrade-required": "Esta función requiere una suscripción al plan {plan}.",
"visitor-log": "Visitante desde {country} usando {browser} en {os} {device}"
diff --git a/public/intl/messages/fa-IR.json b/public/intl/messages/fa-IR.json
index cd7352ac0..930f03a0d 100644
--- a/public/intl/messages/fa-IR.json
+++ b/public/intl/messages/fa-IR.json
@@ -40,6 +40,7 @@
"campaign": "کمپین",
"campaigns": "کمپینها",
"cancel": "انصراف",
+ "close": "بستن",
"change-password": "تغییر رمز",
"channel": "کانال",
"channels": "کانالها",
@@ -102,7 +103,9 @@
"download": "دانلود",
"dropoff": "رها کردن",
"duration": "مدت",
+ "min-duration-seconds": "حداقل مدت زمان (ثانیه)",
"edit": "ویرایش",
+ "design": "طراحی",
"edit-member": "ویرایش عضو",
"email": "ایمیل",
"enable-share-url": "فعال کردن اشتراک گذاری آدرس اینترنتی",
@@ -124,6 +127,7 @@
"filter-combined": "ترکیب شده",
"filter-raw": "خام",
"filters": "فیلترها",
+ "filters-enabled": "فیلترها فعال",
"first-click": "اولین کلیک",
"first-seen": "اولین بار دیده شده",
"funnel": "فانل",
@@ -300,6 +304,8 @@
"sum": "جمع",
"support": "پشتیبانی",
"switch-account": "تغییر حساب",
+ "switch-to-card-view": "تغییر به نمای کارت",
+ "switch-to-table-view": "تغییر به نمای جدول",
"table": "جدول",
"tablet": "تبلت",
"tag": "برچسب",
@@ -379,7 +385,7 @@
"confirm-reset": "آیا مطمئن هستید میخواهید {target} را بازنشانی کنید؟",
"delete-team-warning": "با حذف تیم، تمامی وبسایتهای تیم هم حذف خواهند شد.",
"delete-website-warning": "همهی دادههای وبسایت هم حذف خواهد شد.",
- "empty-dashboard": "داشبورد شما خالی است. روی ویرایش کلیک کنید تا اولین کامپوننت خود را اضافه کنید.",
+ "empty-dashboard": "داشبورد شما خالی است. روی طراحی کلیک کنید تا اولین کامپوننت خود را اضافه کنید.",
"error": "مشکلی پیش آمده است.",
"event-log": "{event} در {url}",
"forbidden": "ممنوع",
@@ -416,6 +422,7 @@
"triggered-event": "رویداد فعال شده",
"unauthorized": "غیرمجاز",
"user-deleted": "کاربر حذف شد.",
+ "replay-min-duration-description": "فقط پخشهایی را نشان بده که مدت زمان آنها برابر یا بیشتر از این مقدار است.",
"viewed-page": "صفحه مشاهده شد",
"visitor-log": "بازدیدکننده از کشور {country} با مروگر {browser} در {os} {device}"
}
diff --git a/public/intl/messages/fi-FI.json b/public/intl/messages/fi-FI.json
index 049906bf3..2ac2f5e2e 100644
--- a/public/intl/messages/fi-FI.json
+++ b/public/intl/messages/fi-FI.json
@@ -39,6 +39,7 @@
"campaign": "Kampanja",
"campaigns": "Kampanjat",
"cancel": "Peruuta",
+ "close": "Sulje",
"change-password": "Vaihda salasana",
"channel": "Kanava",
"channels": "Kanavat",
@@ -103,7 +104,9 @@
"download": "Lataa",
"dropoff": "Poistuminen",
"duration": "Kesto",
+ "min-duration-seconds": "Vähimmäiskesto (sekuntia)",
"edit": "Muokkaa",
+ "design": "Suunnittelu",
"edit-member": "Muokkaa jäsentä",
"email": "Sähköposti",
"enable-share-url": "Ota jakamisen URL-osoite käyttöön",
@@ -125,6 +128,7 @@
"filter-combined": "Yhdistetty",
"filter-raw": "Käsittelemätön",
"filters": "Suodattimet",
+ "filters-enabled": "Suodattimet käytössä",
"first-click": "Ensimmäinen klikkaus",
"first-seen": "Ensimmäinen havainto",
"funnel": "Suppilo",
@@ -255,7 +259,7 @@
"play": "Toista",
"replay": "Toisto",
"replay-id": "Replay ID",
- "replay-code": "Istunnon toistokoodi",
+ "recorder-code": "Istunnon toistokoodi",
"replay-enabled": "Toisto käytössä",
"replays": "Toistot",
"reports": "Raportit",
@@ -304,6 +308,8 @@
"sum": "Summa",
"support": "Tuki",
"switch-account": "Vaihda tiliä",
+ "switch-to-card-view": "Vaihda korttinäkymään",
+ "switch-to-table-view": "Vaihda taulukkonäkymään",
"table": "Taulukko",
"tablet": "Tabletti",
"tag": "Tunniste",
@@ -390,7 +396,7 @@
"delete-team-warning": "Tiimin poistaminen poistaa myös kaikki tiimin sivustot.",
"delete-website-warning": "Kaikki siihen liittyvät tiedot poistetaan.",
"error": "Jotain meni pieleen.",
- "empty-dashboard": "Koontinäyttösi on tyhjä. Napsauta Muokkaa lisätäksesi ensimmäisen komponentin.",
+ "empty-dashboard": "Koontinäyttösi on tyhjä. Napsauta Suunnittelu lisätäksesi ensimmäisen komponentin.",
"event-log": "{event} sivulla {url}",
"forbidden": "Kielletty",
"go-to-settings": "Mene asetuksiin",
@@ -427,6 +433,7 @@
"triggered-event": "Laukaistu tapahtuma",
"unauthorized": "Ei oikeuksia",
"user-deleted": "Käyttäjä poistettu.",
+ "replay-min-duration-description": "Näytä vain toistot, joiden kesto on tämä tai enemmän.",
"viewed-page": "Katsottu sivu",
"upgrade-required": "Tämä ominaisuus vaatii {plan}-tilauksen.",
"visitor-log": "Vierailija maasta {country} selaimella {browser} laitteella {os} {device}"
diff --git a/public/intl/messages/fo-FO.json b/public/intl/messages/fo-FO.json
index e01a37717..6f29bc9cf 100644
--- a/public/intl/messages/fo-FO.json
+++ b/public/intl/messages/fo-FO.json
@@ -39,6 +39,7 @@
"campaign": "Herferð",
"campaigns": "Herferðir",
"cancel": "Strika",
+ "close": "Lat aftur",
"change-password": "Skift loyniorð",
"channel": "Rás",
"channels": "Rásir",
@@ -103,7 +104,9 @@
"download": "Tak niður",
"dropoff": "Fráfall",
"duration": "Tíðarlongd",
+ "min-duration-seconds": "Lágmarkslongd (sekund)",
"edit": "Ger broyting",
+ "design": "Design",
"edit-member": "Ritstjórna lim",
"email": "Teldupostur",
"enable-share-url": "Virkja deili leinki",
@@ -125,6 +128,7 @@
"filter-combined": "Samansett",
"filter-raw": "Óviðgjørt",
"filters": "Síur",
+ "filters-enabled": "Síur virknaðar",
"first-click": "Fyrsta trýst",
"first-seen": "Fyrst sæddur",
"funnel": "Traktari",
@@ -255,7 +259,7 @@
"play": "Spæla",
"replay": "Uppspæling",
"replay-id": "Replay ID",
- "replay-code": "Kodi fyri endurspæling av setuni",
+ "recorder-code": "Kodi fyri endurspæling av setuni",
"replay-enabled": "Uppspæling virkjað",
"replays": "Uppspælingar",
"reports": "Frágreiðingar",
@@ -304,6 +308,8 @@
"sum": "Samanlagt",
"support": "Stuðul",
"switch-account": "Skift konto",
+ "switch-to-card-view": "Skifta til kortsýn",
+ "switch-to-table-view": "Skifta til tabellsýn",
"table": "Tabell",
"tablet": "Teldil",
"tag": "Merki",
@@ -390,7 +396,7 @@
"delete-team-warning": "At strika eitt lið strikar eisini allar heimasíður hjá liðnum.",
"delete-website-warning": "Øll data ið er knýtt at verður eisini strika.",
"error": "Okkurt bleiv gali.",
- "empty-dashboard": "Stjórnarborðið títt er tómt. Trýst á Rætta fyri at leggja afturat fyrstu íhlut.",
+ "empty-dashboard": "Stjórnarborðið títt er tómt. Trýst á Design fyri at leggja afturat fyrstu íhlut.",
"event-log": "{event} á {url}",
"forbidden": "Forboðið",
"go-to-settings": "Far til stillingar",
@@ -427,6 +433,7 @@
"triggered-event": "Avbjóðað hending",
"unauthorized": "Ikki góðkent",
"user-deleted": "Brúkari strikaður.",
+ "replay-min-duration-description": "Vís bert endurspælingar við hesa ella longri longd.",
"viewed-page": "Síð vís",
"upgrade-required": "Hesin virknisháttur krevur eitt {plan}-lyftirtak.",
"visitor-log": "Vitjandi frá {country} brúkar {browser} á {os} {device}"
diff --git a/public/intl/messages/fr-FR.json b/public/intl/messages/fr-FR.json
index b6810ad56..657a91c4a 100644
--- a/public/intl/messages/fr-FR.json
+++ b/public/intl/messages/fr-FR.json
@@ -41,6 +41,7 @@
"campaign": "Campagne",
"campaigns": "Campagnes",
"cancel": "Annuler",
+ "close": "Fermer",
"change-password": "Changer le mot de passe",
"channel": "Canal",
"channels": "Canaux",
@@ -104,7 +105,9 @@
"download": "Télécharger",
"dropoff": "Abandons",
"duration": "Durée",
+ "min-duration-seconds": "Durée minimale (secondes)",
"edit": "Modifier",
+ "design": "Conception",
"edit-member": "Modifier le membre",
"email": "E-mail",
"enable-share-url": "Activer l'URL de partage",
@@ -126,6 +129,7 @@
"filter-combined": "Combiné",
"filter-raw": "Brut",
"filters": "Filtres",
+ "filters-enabled": "Filtres activés",
"first-click": "Premier clic",
"first-seen": "Vu pour la première fois",
"funnel": "Entonnoir",
@@ -258,7 +262,7 @@
"remove": "Retirer",
"remove-member": "Retirer le membre",
"replay": "Relecture",
- "replay-code": "Code de relecture de session",
+ "recorder-code": "Code de relecture de session",
"replay-enabled": "Relecture activée",
"replay-id": "Replay ID",
"replays": "Relectures",
@@ -309,6 +313,8 @@
"sum": "Somme",
"support": "Support",
"switch-account": "Changer de compte",
+ "switch-to-card-view": "Passer à la vue cartes",
+ "switch-to-table-view": "Passer à la vue tableau",
"table": "Tableau",
"tablet": "Tablette",
"tag": "Étiquette",
@@ -419,7 +425,7 @@
"confirm-reset": "Êtes-vous sûr de vouloir réinitialiser les statistiques de {target} ?",
"delete-team-warning": "Supprimer une équipe supprimera aussi tous les sites de cette équipe.",
"delete-website-warning": "Toutes les données associées seront supprimées.",
- "empty-dashboard": "Votre tableau de bord est vide. Cliquez sur Modifier pour ajouter votre premier composant.",
+ "empty-dashboard": "Votre tableau de bord est vide. Cliquez sur Conception pour ajouter votre premier composant.",
"error": "Un problème est survenu.",
"event-log": "{event} sur {url}",
"forbidden": "Interdit",
@@ -458,6 +464,7 @@
"unauthorized": "Non authorisé!",
"upgrade-required": "Cette fonctionnalité nécessite un abonnement {plan}.",
"user-deleted": "Utilisateur supprimé.",
+ "replay-min-duration-description": "Afficher uniquement les lectures de cette durée ou plus.",
"viewed-page": "Page vue",
"visitor-log": "Visiteur de {country} utilisant {browser} sur {os} {device}",
"two-factor-setup-description": "À chaque connexion, en plus de votre mot de passe, vous devrez utiliser une application d’authentification pour générer et utiliser un code unique.",
diff --git a/public/intl/messages/ga-ES.json b/public/intl/messages/ga-ES.json
index a9e180864..eadff6499 100644
--- a/public/intl/messages/ga-ES.json
+++ b/public/intl/messages/ga-ES.json
@@ -39,6 +39,7 @@
"campaign": "Campaña",
"campaigns": "Campañas",
"cancel": "Cancelar",
+ "close": "Pechar",
"change-password": "Mudar contrasinal",
"channel": "Canle",
"channels": "Canles",
@@ -103,7 +104,9 @@
"download": "Descargar",
"dropoff": "Disminución",
"duration": "Duración",
+ "min-duration-seconds": "Duración mínima (segundos)",
"edit": "Editar",
+ "design": "Deseño",
"edit-member": "Editar membro",
"email": "Correo electrónico",
"enable-share-url": "Activar URL de compartición",
@@ -125,6 +128,7 @@
"filter-combined": "Combinado",
"filter-raw": "Crú",
"filters": "Filtros",
+ "filters-enabled": "Filtros activados",
"first-click": "Primeiro clic",
"first-seen": "Primeira visita",
"funnel": "Funil",
@@ -255,7 +259,7 @@
"play": "Reproducir",
"replay": "Reprodución",
"replay-id": "Replay ID",
- "replay-code": "Código de repetición da sesión",
+ "recorder-code": "Código de repetición da sesión",
"replay-enabled": "Reprodución activada",
"replays": "Reproducións",
"reports": "Reportes",
@@ -304,6 +308,8 @@
"sum": "Suma",
"support": "Soporte",
"switch-account": "Cambiar de conta",
+ "switch-to-card-view": "Cambiar á vista de tarxetas",
+ "switch-to-table-view": "Cambiar á vista de táboa",
"table": "Táboa",
"tablet": "Tableta",
"tag": "Etiqueta",
@@ -390,7 +396,7 @@
"delete-team-warning": "Eliminar un equipo tamén eliminará tódolos sitios web do equipo.",
"delete-website-warning": "Tamén serán borrados tódolos datos asociados.",
"error": "Houbo un fallo.",
- "empty-dashboard": "O teu panel está baleiro. Fai clic en Editar para engadir o teu primeiro compoñente.",
+ "empty-dashboard": "O teu panel está baleiro. Fai clic en Deseño para engadir o teu primeiro compoñente.",
"event-log": "{event} en {url}",
"forbidden": "Prohibido",
"go-to-settings": "Ir aos axustes",
@@ -427,6 +433,7 @@
"triggered-event": "Activou o evento",
"unauthorized": "Non autorizado",
"user-deleted": "Usuario eliminado.",
+ "replay-min-duration-description": "Mostrar só as repeticións con esta duración ou superior.",
"viewed-page": "Páxina vista",
"upgrade-required": "Esta función require unha subscrición ao plan {plan}.",
"visitor-log": "Visitante desde {country} usando {browser} en {os} {device}"
diff --git a/public/intl/messages/he-IL.json b/public/intl/messages/he-IL.json
index f5ed72c2f..eefcc6dfd 100644
--- a/public/intl/messages/he-IL.json
+++ b/public/intl/messages/he-IL.json
@@ -40,6 +40,7 @@
"campaign": "מסע פרסום",
"campaigns": "קמפיינים",
"cancel": "ביטול",
+ "close": "סגור",
"change-password": "שינוי סיסמה",
"channel": "ערוץ",
"channels": "ערוצים",
@@ -102,7 +103,9 @@
"download": "הורדה",
"dropoff": "עזיבה",
"duration": "משך",
+ "min-duration-seconds": "משך מינימלי (שניות)",
"edit": "עריכה",
+ "design": "עיצוב",
"edit-member": "ערוך חבר",
"email": "אימייל",
"enable-share-url": "הפעלת URL שיתוף",
@@ -124,6 +127,7 @@
"filter-combined": "משותף",
"filter-raw": "גולמי",
"filters": "מסננים",
+ "filters-enabled": "מסננים מופעלים",
"first-click": "קליק ראשון",
"first-seen": "נראה לראשונה",
"funnel": "משפך",
@@ -300,6 +304,8 @@
"sum": "סכום",
"support": "תמיכה",
"switch-account": "החלף חשבון",
+ "switch-to-card-view": "החלף לתצוגת כרטיסים",
+ "switch-to-table-view": "החלף לתצוגת טבלה",
"table": "טבלה",
"tablet": "טאבלט",
"tag": "תגית",
@@ -379,7 +385,7 @@
"confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"delete-team-warning": "מחיקת צוות תמחק גם את כל אתרי הצוות.",
"delete-website-warning": "כל המידע המקושר יימחק",
- "empty-dashboard": "לוח המחוונים שלך ריק. לחץ על עריכה כדי להוסיף את הרכיב הראשון שלך.",
+ "empty-dashboard": "לוח המחוונים שלך ריק. לחץ על עיצוב כדי להוסיף את הרכיב הראשון שלך.",
"error": "משהו השתבש",
"event-log": "{event} ב-{url}",
"forbidden": "אסור",
@@ -416,6 +422,7 @@
"triggered-event": "אירוע שהופעל",
"unauthorized": "לא מורשה",
"user-deleted": "משתמש נמחק.",
+ "replay-min-duration-description": "הצג רק הקלטות בהפעלה חוזרת בעלות משך זמן זה או ארוך יותר.",
"viewed-page": "עמוד שנצפה",
"visitor-log": "מבקר ממדינת {country} משתמבש בדפדפן {browser} ב-{os} {device}"
}
diff --git a/public/intl/messages/hi-IN.json b/public/intl/messages/hi-IN.json
index a1b7d396f..0f91b8ec1 100644
--- a/public/intl/messages/hi-IN.json
+++ b/public/intl/messages/hi-IN.json
@@ -40,6 +40,7 @@
"campaign": "अभियान",
"campaigns": "अभियान",
"cancel": "रद्द करें",
+ "close": "बंद करें",
"change-password": "पासवर्ड बदलें",
"channel": "चैनल",
"channels": "चैनल",
@@ -102,7 +103,9 @@
"download": "डाउनलोड",
"dropoff": "ड्रॉपऑफ",
"duration": "अवधि",
+ "min-duration-seconds": "न्यूनतम अवधि (सेकंड)",
"edit": "संपादित करें",
+ "design": "डिज़ाइन",
"edit-member": "सदस्य संपादित करें",
"email": "ईमेल",
"enable-share-url": "शेयर URL सक्षम करें",
@@ -124,6 +127,7 @@
"filter-combined": "संयुक्त",
"filter-raw": "रॉ",
"filters": "फ़िल्टर",
+ "filters-enabled": "फ़िल्टर सक्षम",
"first-click": "पहला क्लिक",
"first-seen": "पहली बार देखा गया",
"funnel": "फनल",
@@ -300,6 +304,8 @@
"sum": "योग",
"support": "सहायता",
"switch-account": "खाता बदलें",
+ "switch-to-card-view": "कार्ड दृश्य पर स्विच करें",
+ "switch-to-table-view": "तालिका दृश्य पर स्विच करें",
"table": "तालिका",
"tablet": "टैबलेट",
"tag": "टैग",
@@ -379,7 +385,7 @@
"confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"delete-team-warning": "टीम हटाने से टीम की सभी वेबसाइटें भी हट जाएँगी।",
"delete-website-warning": "सभी संबद्ध डेटा को भी हटा दिया जाएगा।",
- "empty-dashboard": "आपका डैशबोर्ड खाली है। अपना पहला घटक जोड़ने के लिए संपादित करें पर क्लिक करें।",
+ "empty-dashboard": "आपका डैशबोर्ड खाली है। अपना पहला घटक जोड़ने के लिए डिज़ाइन पर क्लिक करें।",
"error": "कुछ गलत हो गया।",
"event-log": "{url} पर {event}",
"forbidden": "प्रतिबंधित",
@@ -416,6 +422,7 @@
"triggered-event": "ट्रिगर किया गया ईवेंट",
"unauthorized": "अनधिकृत",
"user-deleted": "उपयोगकर्ता हटाया गया।",
+ "replay-min-duration-description": "केवल इस अवधि या उससे अधिक की रिप्ले दिखाएं।",
"viewed-page": "देखा गया पेज",
"visitor-log": "{country} का आगंतुक, जो {browser} का उपयोग करता है, {os} यन्त्र पर"
}
diff --git a/public/intl/messages/hr-HR.json b/public/intl/messages/hr-HR.json
index 963b86a50..41fb434b7 100644
--- a/public/intl/messages/hr-HR.json
+++ b/public/intl/messages/hr-HR.json
@@ -39,6 +39,7 @@
"campaign": "Kampanja",
"campaigns": "Kampanje",
"cancel": "Odustani",
+ "close": "Zatvori",
"change-password": "Promijeni lozinku",
"channel": "Kanal",
"channels": "Kanali",
@@ -103,7 +104,9 @@
"download": "Preuzmi",
"dropoff": "Odlazak",
"duration": "Trajanje",
+ "min-duration-seconds": "Minimalno trajanje (sekunde)",
"edit": "Uredi",
+ "design": "Dizajn",
"edit-member": "Uredi člana",
"email": "E-mail",
"enable-share-url": "Omogući dijeljenje poveznice",
@@ -125,6 +128,7 @@
"filter-combined": "Kombinirano",
"filter-raw": "Neobrađeno",
"filters": "Filteri",
+ "filters-enabled": "Filtri omogućeni",
"first-click": "Prvi klik",
"first-seen": "Prvi put viđeno",
"funnel": "Lijevak",
@@ -255,7 +259,7 @@
"play": "Reproduciraj",
"replay": "Ponovna reprodukcija",
"replay-id": "Replay ID",
- "replay-code": "Kôd ponavljanja sesije",
+ "recorder-code": "Kôd ponavljanja sesije",
"replay-enabled": "Ponovna reprodukcija omogućena",
"replays": "Ponovne reprodukcije",
"reports": "Izvješća",
@@ -304,6 +308,8 @@
"sum": "Zbroj",
"support": "Podrška",
"switch-account": "Promijeni račun",
+ "switch-to-card-view": "Prebaci na prikaz kartica",
+ "switch-to-table-view": "Prebaci na tablični prikaz",
"table": "Tablica",
"tablet": "Tablet",
"tag": "Oznaka",
@@ -390,7 +396,7 @@
"delete-team-warning": "Brisanje tima će izbrisati i sve web stranice tima.",
"delete-website-warning": "Svi podaci web stranice bit će izbrisani.",
"error": "Nešto je pošlo po krivu.",
- "empty-dashboard": "Vaša nadzorna ploča je prazna. Kliknite Uredi da dodate prvu komponentu.",
+ "empty-dashboard": "Vaša nadzorna ploča je prazna. Kliknite Dizajn da dodate prvu komponentu.",
"event-log": "{event} na {url}",
"forbidden": "Zabranjeno",
"go-to-settings": "Idi u postavke",
@@ -427,6 +433,7 @@
"triggered-event": "Pokrenuti događaj",
"unauthorized": "Neautorizirano",
"user-deleted": "Korisnik izbrisan.",
+ "replay-min-duration-description": "Prikaži samo snimke s ovim ili dužim trajanjem.",
"viewed-page": "Pregledana stranica",
"upgrade-required": "Ova značajka zahtijeva pretplatu na plan {plan}.",
"visitor-log": "Posjetitelj iz {country} koristi {browser} na {os} {device}"
diff --git a/public/intl/messages/hu-HU.json b/public/intl/messages/hu-HU.json
index aea98d3b2..ef62a8691 100644
--- a/public/intl/messages/hu-HU.json
+++ b/public/intl/messages/hu-HU.json
@@ -39,6 +39,7 @@
"campaign": "Kampány",
"campaigns": "Kampányok",
"cancel": "Mégsem",
+ "close": "Bezár",
"change-password": "Jelszó módosítása",
"channel": "Csatorna",
"channels": "Csatornák",
@@ -103,7 +104,9 @@
"download": "Letöltés",
"dropoff": "Lemorzsolódás",
"duration": "Időtartam",
+ "min-duration-seconds": "Minimális időtartam (másodperc)",
"edit": "Módosítás",
+ "design": "Tervezés",
"edit-member": "Tag szerkesztése",
"email": "E-mail",
"enable-share-url": "URL-megosztás engedélyezése",
@@ -125,6 +128,7 @@
"filter-combined": "Összevont",
"filter-raw": "Nyers",
"filters": "Szűrők",
+ "filters-enabled": "Szűrők engedélyezve",
"first-click": "Első kattintás",
"first-seen": "Első megtekintés",
"funnel": "Tölcsér",
@@ -255,7 +259,7 @@
"play": "Lejátszás",
"replay": "Visszajátszás",
"replay-id": "Replay ID",
- "replay-code": "Munkamenet-újrajátszási kód",
+ "recorder-code": "Munkamenet-újrajátszási kód",
"replay-enabled": "Visszajátszás engedélyezve",
"replays": "Visszajátszások",
"reports": "Jelentések",
@@ -304,6 +308,8 @@
"sum": "Összeg",
"support": "Támogatás",
"switch-account": "Fiók váltása",
+ "switch-to-card-view": "Váltás kártya nézetre",
+ "switch-to-table-view": "Váltás táblázat nézetre",
"table": "Táblázat",
"tablet": "Táblagép",
"tag": "Címke",
@@ -390,7 +396,7 @@
"delete-team-warning": "A csapat törlése az összes csapat weboldalt is törli.",
"delete-website-warning": "Minden társított adat törlésre kerül.",
"error": "Valami baj történt.",
- "empty-dashboard": "Az irányítópult üres. Kattintson a Szerkesztés gombra az első komponens hozzáadásához.",
+ "empty-dashboard": "Az irányítópult üres. Kattintson a Tervezés gombra az első komponens hozzáadásához.",
"event-log": "{event} itt: {url}",
"forbidden": "Tiltott",
"go-to-settings": "Tovább a beállításokhoz",
@@ -427,6 +433,7 @@
"triggered-event": "Kiváltott esemény",
"unauthorized": "Nincs jogosultság",
"user-deleted": "Felhasználó törölve.",
+ "replay-min-duration-description": "Csak az ezzel az időtartammal vagy annál hosszabb visszajátszásokat mutasd.",
"viewed-page": "Megtekintett oldal",
"upgrade-required": "Ez a funkció {plan} előfizetést igényel.",
"visitor-log": "Látógató {country} területéről, {os} {device} eszközön, {browser} böngészőből."
diff --git a/public/intl/messages/id-ID.json b/public/intl/messages/id-ID.json
index 1bc6b433c..4450576f9 100644
--- a/public/intl/messages/id-ID.json
+++ b/public/intl/messages/id-ID.json
@@ -39,6 +39,7 @@
"campaign": "Kampanye",
"campaigns": "Kampanye",
"cancel": "Batal",
+ "close": "Tutup",
"change-password": "Ganti kata sandi",
"channel": "Saluran",
"channels": "Saluran",
@@ -103,7 +104,9 @@
"download": "Unduh",
"dropoff": "Penurunan",
"duration": "Durasi",
+ "min-duration-seconds": "Durasi minimum (detik)",
"edit": "Sunting",
+ "design": "Desain",
"edit-member": "Sunting anggota",
"email": "Email",
"enable-share-url": "Aktifkan URL berbagi",
@@ -125,6 +128,7 @@
"filter-combined": "Gabungan",
"filter-raw": "Mentah",
"filters": "Filter",
+ "filters-enabled": "Filter diaktifkan",
"first-click": "Klik pertama",
"first-seen": "Pertama kali dilihat",
"funnel": "Corong",
@@ -255,7 +259,7 @@
"play": "Putar",
"replay": "Pemutaran ulang",
"replay-id": "Replay ID",
- "replay-code": "Kode putar ulang sesi",
+ "recorder-code": "Kode putar ulang sesi",
"replay-enabled": "Pemutaran ulang diaktifkan",
"replays": "Pemutaran ulang",
"reports": "Laporan",
@@ -304,6 +308,8 @@
"sum": "Jumlah",
"support": "Dukungan",
"switch-account": "Ganti akun",
+ "switch-to-card-view": "Beralih ke tampilan kartu",
+ "switch-to-table-view": "Beralih ke tampilan tabel",
"table": "Tabel",
"tablet": "Tablet",
"tag": "Tag",
@@ -390,7 +396,7 @@
"delete-team-warning": "Menghapus tim juga akan menghapus semua situs web yang terkait.",
"delete-website-warning": "Semua data terkait juga akan dihapus.",
"error": "Ada yang salah.",
- "empty-dashboard": "Dasbor Anda kosong. Klik Edit untuk menambahkan komponen pertama Anda.",
+ "empty-dashboard": "Dasbor Anda kosong. Klik Desain untuk menambahkan komponen pertama Anda.",
"event-log": "{event} di {url}",
"forbidden": "Dilarang",
"go-to-settings": "Pergi ke pengaturan",
@@ -427,6 +433,7 @@
"triggered-event": "Peristiwa terjadi",
"unauthorized": "Tidak diotorisasi",
"user-deleted": "Pengguna telah dihapus.",
+ "replay-min-duration-description": "Hanya tampilkan ulangan dengan durasi ini atau lebih.",
"viewed-page": "Halaman dilihat",
"upgrade-required": "Fitur ini memerlukan langganan paket {plan}.",
"visitor-log": "Pengunjung dari {country} dengan {browser} di {device} {os}"
diff --git a/public/intl/messages/it-IT.json b/public/intl/messages/it-IT.json
index a4589faa7..13dd1bfb7 100644
--- a/public/intl/messages/it-IT.json
+++ b/public/intl/messages/it-IT.json
@@ -39,6 +39,7 @@
"campaign": "Campagna",
"campaigns": "Campagne",
"cancel": "Annulla",
+ "close": "Chiudi",
"change-password": "Modifica password",
"channel": "Canale",
"channels": "Canali",
@@ -103,7 +104,9 @@
"download": "Scarica",
"dropoff": "Abbandono",
"duration": "Durata",
+ "min-duration-seconds": "Durata minima (secondi)",
"edit": "Modifica",
+ "design": "Design",
"edit-member": "Modifica membro",
"email": "Email",
"enable-share-url": "Abilita URL di condivisione",
@@ -125,6 +128,7 @@
"filter-combined": "Aggregati",
"filter-raw": "Grezzo",
"filters": "Filtri",
+ "filters-enabled": "Filtri abilitati",
"first-click": "Primo clic",
"first-seen": "Prima visualizzazione",
"funnel": "Funnel",
@@ -255,7 +259,7 @@
"play": "Riproduci",
"replay": "Replay",
"replay-id": "Replay ID",
- "replay-code": "Codice replay sessione",
+ "recorder-code": "Codice replay sessione",
"replay-enabled": "Replay abilitato",
"replays": "Replay",
"reports": "Report",
@@ -304,6 +308,8 @@
"sum": "Somma",
"support": "Supporto",
"switch-account": "Cambia account",
+ "switch-to-card-view": "Passa alla vista schede",
+ "switch-to-table-view": "Passa alla vista tabella",
"table": "Tabella",
"tablet": "Tablet",
"tag": "Etichetta",
@@ -390,7 +396,7 @@
"delete-team-warning": "L'eliminazione di un team eliminerà anche tutti i siti web del team.",
"delete-website-warning": "Saranno eliminati anche tutti i dati associati.",
"error": "Si è verificato un errore.",
- "empty-dashboard": "La tua dashboard è vuota. Fai clic su Modifica per aggiungere il tuo primo componente.",
+ "empty-dashboard": "La tua dashboard è vuota. Fai clic su Design per aggiungere il tuo primo componente.",
"event-log": "{event} su {url}",
"forbidden": "Vietato",
"go-to-settings": "Vai alle impostazioni",
@@ -427,6 +433,7 @@
"triggered-event": "Evento attivato",
"unauthorized": "Non autorizzato",
"user-deleted": "Utente eliminato.",
+ "replay-min-duration-description": "Mostra solo le riproduzioni con questa durata o superiore.",
"viewed-page": "Pagina visualizzata",
"upgrade-required": "Questa funzionalità richiede un abbonamento al piano {plan}.",
"visitor-log": "Utenti da {country} tramite {browser} su {os} {device}"
diff --git a/public/intl/messages/ja-JP.json b/public/intl/messages/ja-JP.json
index bcf304f8e..9f8097eb8 100644
--- a/public/intl/messages/ja-JP.json
+++ b/public/intl/messages/ja-JP.json
@@ -40,6 +40,7 @@
"campaign": "キャンペーン",
"campaigns": "キャンペーン",
"cancel": "キャンセル",
+ "close": "閉じる",
"change-password": "パスワードの変更",
"channel": "チャネル",
"channels": "チャンネル",
@@ -102,7 +103,9 @@
"download": "ダウンロード",
"dropoff": "切り捨て",
"duration": "継続時間",
+ "min-duration-seconds": "最小時間(秒)",
"edit": "編集",
+ "design": "デザイン",
"edit-member": "メンバーの編集",
"email": "メール",
"enable-share-url": "共有URLを有効にする",
@@ -124,6 +127,7 @@
"filter-combined": "結合",
"filter-raw": "RAW",
"filters": "フィルター",
+ "filters-enabled": "フィルター有効",
"first-click": "最初のクリック",
"first-seen": "初回ログイン",
"funnel": "ファネル",
@@ -300,6 +304,8 @@
"sum": "合計",
"support": "サポート",
"switch-account": "アカウントを切り替え",
+ "switch-to-card-view": "カード表示に切り替え",
+ "switch-to-table-view": "テーブル表示に切り替え",
"table": "テーブル",
"tablet": "タブレット",
"tag": "タグ",
@@ -379,7 +385,7 @@
"confirm-reset": "{target}をリセットしてもよろしいですか?",
"delete-team-warning": "チームを削除すると、そのチームが管理しているWebサイトもすべて削除されます。",
"delete-website-warning": "Webサイトのデータがすべて削除されます。",
- "empty-dashboard": "ダッシュボードは空です。編集をクリックして最初のコンポーネントを追加してください。",
+ "empty-dashboard": "ダッシュボードは空です。デザインをクリックして最初のコンポーネントを追加してください。",
"error": "未知のエラーが発生しました。",
"event-log": "{url}の{event}",
"forbidden": "アクセス禁止",
@@ -416,6 +422,7 @@
"triggered-event": "トリガーされたイベント",
"unauthorized": "認証が必要です",
"user-deleted": "ユーザーが削除されました。",
+ "replay-min-duration-description": "この時間以上の再生のみ表示します。",
"viewed-page": "閲覧されたページ",
"visitor-log": "{os}({device})で{browser}を使用している{country}からの訪問者"
}
diff --git a/public/intl/messages/km-KH.json b/public/intl/messages/km-KH.json
index 9fb936623..76b8ca0c2 100644
--- a/public/intl/messages/km-KH.json
+++ b/public/intl/messages/km-KH.json
@@ -40,6 +40,7 @@
"campaign": "យុទ្ធនាការ",
"campaigns": "យុទ្ធនាការ",
"cancel": "បោះបង់",
+ "close": "បិទ",
"change-password": "ផ្លាស់ប្តូរពាក្យសម្ងាត់",
"channel": "ឆានែល",
"channels": "ឆានែល",
@@ -102,7 +103,9 @@
"download": "ទាញយក",
"dropoff": "ការចាកចេញ",
"duration": "រយៈពេល",
+ "min-duration-seconds": "រយៈពេលអប្បបរមា (វិនាទី)",
"edit": "កែប្រែ",
+ "design": "រចនា",
"edit-member": "កែព័ត៌មានសមាជិក",
"email": "អ៊ីមែល",
"enable-share-url": "បើកការចែករំលែក URL",
@@ -124,6 +127,7 @@
"filter-combined": "រួមបញ្ចូលគ្នា",
"filter-raw": "ដើម",
"filters": "ចម្រោះ",
+ "filters-enabled": "ការច្រោះបានបើក",
"first-click": "ចុចដំបូង",
"first-seen": "ឃើញដំបូង",
"funnel": "ផ្លូវបង្ហាញ",
@@ -300,6 +304,8 @@
"sum": "ផលបូក",
"support": "ជំនួយ",
"switch-account": "ប្ដូរគណនី",
+ "switch-to-card-view": "ប្ដូរទៅរូបរាងកាត",
+ "switch-to-table-view": "ប្ដូរទៅរូបរាងតារាង",
"table": "តារាង",
"tablet": "ថេប្លេត",
"tag": "ស្លាក",
@@ -379,7 +385,7 @@
"confirm-reset": "តើអ្នកប្រាកដថាចង់កំណត់ស្ថិតិរបស់ {target} ឡើងវិញទេ?",
"delete-team-warning": "ពេលលុបក្រុម គេហទំព័ររបស់ក្រុមក៏នឹងត្រូវលប់ចោលទាំងអស់ផងដែរ។",
"delete-website-warning": "ទិន្នន័យរបស់គេហទំព័រទាំងអស់នឹងត្រូវលុបចោល។",
- "empty-dashboard": "ផ្ទាំងគ្រប់គ្រងរបស់អ្នកទទេ។ ចុចកែសម្រួលដើម្បីបន្ថែមធាតុដំបូងរបស់អ្នក។",
+ "empty-dashboard": "ផ្ទាំងគ្រប់គ្រងរបស់អ្នកទទេ។ ចុច រចនា ដើម្បីបន្ថែមធាតុដំបូងរបស់អ្នក។",
"error": "មានអ្វីមួយមិនប្រក្រតី។",
"event-log": "{event} នៅ {url}",
"forbidden": "ហាមឃាត់",
@@ -416,6 +422,7 @@
"triggered-event": "ព្រឹត្តិការណ៍ដែលបានដំណើរការ",
"unauthorized": "មិនមានសិទ្ធិ",
"user-deleted": "អ្នកប្រើប្រាស់ត្រូវបានលុបចោល។",
+ "replay-min-duration-description": "បង្ហាញតែការចាក់ឡើងវិញដែលមានរយៈពេលនេះ ឬខ្ពស់ជាងនេះ។",
"viewed-page": "ទំព័រដែលបានមើល",
"visitor-log": "អ្នកមើលពីប្រទេស {country} ប្រើប្រាស់កម្មវិធី {browser} លើឧបករណ៍ {os} {device}"
}
diff --git a/public/intl/messages/ko-KR.json b/public/intl/messages/ko-KR.json
index 579b9b625..3635dadef 100644
--- a/public/intl/messages/ko-KR.json
+++ b/public/intl/messages/ko-KR.json
@@ -5,6 +5,7 @@
"action": "작업",
"actions": "동작",
"activity": "활동",
+ "activity-by-property": "속성별 활동",
"add": "추가",
"add-board": "보드 추가",
"add-description": "설명 추가",
@@ -32,6 +33,7 @@
"before": "이전",
"behavior": "행동",
"block-selector": "블록 선택기",
+ "board-type": "보드 유형",
"boards": "보드",
"bounce-rate": "이탈률",
"breakdown": "세부 사항",
@@ -40,6 +42,7 @@
"campaign": "캠페인",
"campaigns": "캠페인",
"cancel": "취소",
+ "close": "닫기",
"change-password": "비밀번호 변경",
"channel": "채널",
"channels": "채널",
@@ -70,10 +73,12 @@
"created": "생성됨",
"created-by": "작성자",
"criteria": "기준",
+ "cumulative": "누적",
"currency": "통화",
"current": "현재",
"current-password": "현재 비밀번호",
"custom-range": "범위 지정",
+ "customers": "고객",
"dashboard": "대시보드",
"data": "데이터",
"date": "날짜",
@@ -102,7 +107,13 @@
"download": "다운로드",
"dropoff": "이탈",
"duration": "재생 시간",
+ "heatmap": "히트맵",
+ "heatmap-description": "방문자가 페이지에서 어디를 클릭하고 얼마나 스크롤하는지 시각화합니다.",
+ "heatmaps": "히트맵",
+ "llm": "LLM",
+ "min-duration-seconds": "최소 지속 시간(초)",
"edit": "편집",
+ "design": "디자인",
"edit-member": "멤버 편집",
"email": "이메일",
"enable-share-url": "URL 공유 활성화",
@@ -124,6 +135,7 @@
"filter-combined": "합쳐 보기",
"filter-raw": "전체 보기",
"filters": "필터",
+ "filters-enabled": "필터 활성화됨",
"first-click": "첫 클릭",
"first-seen": "첫 접속",
"funnel": "퍼널",
@@ -201,6 +213,8 @@
"number-of-records": "{x}개 레코드",
"ok": "확인",
"online": "온라인",
+ "open": "혼합",
+ "orders": "주문",
"organic-search": "자연 검색",
"organic-shopping": "자연 쇼핑",
"organic-social": "자연 소셜",
@@ -223,6 +237,7 @@
"path": "패스",
"paths": "패스",
"performance": "성능",
+ "period": "기간",
"pixel": "픽셀",
"pixels": "픽셀",
"play": "재생",
@@ -236,11 +251,13 @@
"profiles": "프로필",
"properties": "속성",
"property": "속성",
+ "property-filter": "속성 필터",
"queries": "쿼리",
"query": "쿼리",
"query-parameters": "쿼리 매개 변수",
"realtime": "실시간",
"recorded": "기록 시간",
+ "recorder-code": "레코더 코드",
"referral": "추천",
"referrer": "리퍼러",
"referrers": "리퍼러",
@@ -255,7 +272,7 @@
"remove-member": "멤버 제거",
"replay": "다시보기",
"replay-enabled": "다시보기 활성화",
- "replay-id": "Replay ID",
+ "replay-id": "다시보기 ID",
"replays": "다시보기",
"reports": "보고서",
"required": "필수",
@@ -272,7 +289,9 @@
"sample-size": "샘플 크기",
"save": "저장",
"save-cohort": "코호트 저장",
+ "save-replay": "다시보기 저장",
"save-segment": "세그먼트 저장",
+ "saved": "저장됨",
"screen": "화면",
"screens": "화면",
"search": "검색",
@@ -281,7 +300,10 @@
"select": "선택",
"select-component": "컴포넌트 선택",
"select-date": "날짜 선택",
+ "select-event": "이벤트 선택",
"select-filter": "필터 선택",
+ "select-link": "링크 선택",
+ "select-pixel": "픽셀 선택",
"select-role": "역할 선택",
"select-website": "웹사이트 선택",
"session": "세션",
@@ -300,6 +322,8 @@
"sum": "합계",
"support": "지원",
"switch-account": "계정 전환",
+ "switch-to-card-view": "카드 보기로 전환",
+ "switch-to-table-view": "테이블 보기로 전환",
"table": "표",
"tablet": "태블릿",
"tag": "태그",
@@ -341,6 +365,7 @@
"unknown": "알 수 없음",
"untitled": "제목 없음",
"update": "업데이트",
+ "upgrade": "업그레이드",
"url": "URL",
"user": "사용자",
"username": "사용자 이름",
@@ -379,7 +404,7 @@
"confirm-reset": "{target}을(를) 초기화하시겠습니까?",
"delete-team-warning": "팀을 삭제하면 팀에 등록된 모든 웹사이트도 삭제됩니다.",
"delete-website-warning": "관련된 모든 데이터가 삭제됩니다.",
- "empty-dashboard": "대시보드가 비어 있습니다. 편집을 클릭하여 첫 번째 구성 요소를 추가하세요.",
+ "empty-dashboard": "대시보드가 비어 있습니다. 디자인을 클릭하여 첫 번째 구성 요소를 추가하세요.",
"error": "문제가 발생했습니다.",
"event-log": "{event} - {url}",
"forbidden": "접근 금지",
@@ -402,6 +427,7 @@
"reset-website": "이 웹사이트를 초기화하려면 아래 상자에 {confirmation}을(를) 입력하세요.",
"reset-website-warning": "이 웹사이트의 모든 통계가 삭제되지만 설정은 그대로 유지됩니다.",
"saved": "저장했습니다.",
+ "select-board-entity-first": "웹사이트, 픽셀 또는 링크를 먼저 선택하세요.",
"select-component-preview": "미리 볼 컴포넌트를 선택하세요",
"select-website-first": "먼저 웹사이트를 선택하세요",
"sever-error": "서버 오류",
@@ -415,7 +441,9 @@
"transfer-website": "웹사이트 소유권을 계정이나 다른 팀으로 전송합니다.",
"triggered-event": "트리거된 이벤트",
"unauthorized": "인증되지 않음",
+ "upgrade-required": "이 기능은 {plan} 요금제 구독이 필요합니다.",
"user-deleted": "사용자를 삭제했습니다.",
+ "replay-min-duration-description": "이 지속 시간 이상의 리플레이만 표시합니다.",
"viewed-page": "조회한 페이지",
"visitor-log": "{os} {device}에서 {browser}을(를) 사용하는 {country}의 방문자"
}
diff --git a/public/intl/messages/lt-LT.json b/public/intl/messages/lt-LT.json
index c0b2aaa0c..5559460d6 100644
--- a/public/intl/messages/lt-LT.json
+++ b/public/intl/messages/lt-LT.json
@@ -39,6 +39,7 @@
"campaign": "Kampanija",
"campaigns": "Kampanijos",
"cancel": "Atšaukti",
+ "close": "Uždaryti",
"change-password": "Pakeisti slaptažodį",
"channel": "Kanalas",
"channels": "Kanalai",
@@ -103,7 +104,9 @@
"download": "Atsisiųsti",
"dropoff": "Atsitraukimas",
"duration": "Trukmė",
+ "min-duration-seconds": "Minimali trukmė (sekundės)",
"edit": "Redaguoti",
+ "design": "Dizainas",
"edit-member": "Redaguoti narį",
"email": "El. paštas",
"enable-share-url": "Įjungti bendrinimą su nuoroda",
@@ -125,6 +128,7 @@
"filter-combined": "Kombinuoti",
"filter-raw": "Neapdoroti",
"filters": "Filtrai",
+ "filters-enabled": "Filtrai įjungti",
"first-click": "Pirmas paspaudimas",
"first-seen": "Pirmą kartą matyta",
"funnel": "Piltuvas",
@@ -255,7 +259,7 @@
"play": "Leisti",
"replay": "Pakartojimas",
"replay-id": "Replay ID",
- "replay-code": "Sesijos pakartojimo kodas",
+ "recorder-code": "Sesijos pakartojimo kodas",
"replay-enabled": "Pakartojimas įjungtas",
"replays": "Pakartojimai",
"reports": "Ataskaitos",
@@ -304,6 +308,8 @@
"sum": "Suma",
"support": "Palaikymas",
"switch-account": "Perjungti paskyrą",
+ "switch-to-card-view": "Perjungti į kortelių rodinį",
+ "switch-to-table-view": "Perjungti į lentelės rodinį",
"table": "Lentelė",
"tablet": "Planšetė",
"tag": "Žyma",
@@ -390,7 +396,7 @@
"delete-team-warning": "Ištrinant komandą bus ištrintos ir visos komandos svetainės.",
"delete-website-warning": "Visi susiję duomenys taip pat bus ištrinti.",
"error": "Kažkas įvyko ne taip.",
- "empty-dashboard": "Jūsų prietaisų skydelis tuščias. Spustelėkite Redaguoti, kad pridėtumėte pirmąjį komponentą.",
+ "empty-dashboard": "Jūsų prietaisų skydelis tuščias. Spustelėkite Dizainas, kad pridėtumėte pirmąjį komponentą.",
"event-log": "{event} puslapyje {url}",
"forbidden": "Draudžiama",
"go-to-settings": "Eiti į nustatymus",
@@ -427,6 +433,7 @@
"triggered-event": "Suaktyvintas įvykis",
"unauthorized": "Neautorizuota",
"user-deleted": "Vartotojas ištrintas.",
+ "replay-min-duration-description": "Rodyti tik atkūrimus su šia arba didesne trukme.",
"viewed-page": "Peržiūrėtas puslapis",
"upgrade-required": "Šiai funkcijai reikalinga {plan} plano prenumerata.",
"visitor-log": "Lankytojas iš {country}, naudojantis {browser} sistemoje {os} {device}"
diff --git a/public/intl/messages/mn-MN.json b/public/intl/messages/mn-MN.json
index a9841a278..b3bf38b98 100644
--- a/public/intl/messages/mn-MN.json
+++ b/public/intl/messages/mn-MN.json
@@ -40,6 +40,7 @@
"campaign": "Кампанит ажил",
"campaigns": "Аянууд",
"cancel": "Цуцлах",
+ "close": "Хаах",
"change-password": "Нууц үг солих",
"channel": "Суваг",
"channels": "Суваг",
@@ -102,7 +103,9 @@
"download": "Татах",
"dropoff": "Уналт",
"duration": "Хугацаа",
+ "min-duration-seconds": "Хамгийн бага үргэлжлэх хугацаа (секунд)",
"edit": "Засах",
+ "design": "Дизайн",
"edit-member": "Гишүүн засах",
"email": "Имэйл",
"enable-share-url": "Хуваалцах холбоос идэвхжүүлэх",
@@ -124,6 +127,7 @@
"filter-combined": "Нэгтгэсэн",
"filter-raw": "Түүхий",
"filters": "Шүүлтүүр",
+ "filters-enabled": "Шүүлтүүр идэвхжүүлэгдсэн",
"first-click": "Эхний даралт",
"first-seen": "Анх харсан",
"funnel": "Цутгал",
@@ -300,6 +304,8 @@
"sum": "Нийлбэр",
"support": "Дэмжлэг",
"switch-account": "Бүртгэл солих",
+ "switch-to-card-view": "Картын харагдац руу шилжих",
+ "switch-to-table-view": "Хүснэгт харагдац руу шилжих",
"table": "Хүснэгт",
"tablet": "Таблет",
"tag": "Таг",
@@ -379,7 +385,7 @@
"confirm-reset": "Та {target}-н тоон үзүүлэлтүүдийг устгахдаа итгэлтэй байна уу?",
"delete-team-warning": "Баг устгах нь мөн түүнд харъяалагдах вебүүдийг устгах болно.",
"delete-website-warning": "Энэ вебтэй холбоотой бүх өгөгдөл устах болно.",
- "empty-dashboard": "Таны хяналтын самбар хоосон байна. Эхний бүрэлдэхүүн хэсгийг нэмэхийн тулд Засах дарна уу.",
+ "empty-dashboard": "Таны хяналтын самбар хоосон байна. Эхний бүрэлдэхүүн хэсгийг нэмэхийн тулд Дизайн дарна уу.",
"error": "Ямар нэг зүйл буруу боллоо.",
"event-log": "{url}-д {event}",
"forbidden": "Хориотой",
@@ -416,6 +422,7 @@
"triggered-event": "Өдөөсөн үйлдэл",
"unauthorized": "Зөвшөөрөлгүй",
"user-deleted": "Хэрэглэгч устсан.",
+ "replay-min-duration-description": "Зөвхөн энэ эсвэл үүнээс их үргэлжлэх хугацаатай тоглуулалтуудыг харуул.",
"viewed-page": "Үзсэн хуудас",
"visitor-log": "{country} улсаас {os} {device} дээр {browser} хөтөч ашиглан орсон"
}
diff --git a/public/intl/messages/ms-MY.json b/public/intl/messages/ms-MY.json
index d34510f54..c8c5d0a47 100644
--- a/public/intl/messages/ms-MY.json
+++ b/public/intl/messages/ms-MY.json
@@ -39,6 +39,7 @@
"campaign": "Kempen",
"campaigns": "Kempen",
"cancel": "Batal",
+ "close": "Tutup",
"change-password": "Tukar kata laluan",
"channel": "Saluran",
"channels": "Saluran",
@@ -103,7 +104,9 @@
"download": "Muat turun",
"dropoff": "Tercicir",
"duration": "Tempoh",
+ "min-duration-seconds": "Tempoh minimum (saat)",
"edit": "Sunting",
+ "design": "Reka Bentuk",
"edit-member": "Sunting ahli",
"email": "E-mel",
"enable-share-url": "Aktifkan url berkongsi",
@@ -125,6 +128,7 @@
"filter-combined": "Digabungkan",
"filter-raw": "Mentah",
"filters": "Tapis",
+ "filters-enabled": "Penapis diaktifkan",
"first-click": "Klik pertama",
"first-seen": "Pertama dilihat",
"funnel": "Corong",
@@ -255,7 +259,7 @@
"play": "Main",
"replay": "Main semula",
"replay-id": "Replay ID",
- "replay-code": "Kod main semula sesi",
+ "recorder-code": "Kod main semula sesi",
"replay-enabled": "Main semula diaktifkan",
"replays": "Main semula",
"reports": "Laporan",
@@ -304,6 +308,8 @@
"sum": "Jumlah",
"support": "Sokongan",
"switch-account": "Tukar akaun",
+ "switch-to-card-view": "Tukar ke paparan kad",
+ "switch-to-table-view": "Tukar ke paparan jadual",
"table": "Jadual",
"tablet": "Tablet",
"tag": "Tag",
@@ -390,7 +396,7 @@
"delete-team-warning": "Memadamkan pasukan juga akan memadamkan semua laman web pasukan.",
"delete-website-warning": "Semua data yang berkaitan juga akan dihapuskan.",
"error": "Ada yang tidak kena.",
- "empty-dashboard": "Papan pemuka anda kosong. Klik Edit untuk menambah komponen pertama anda.",
+ "empty-dashboard": "Papan pemuka anda kosong. Klik Reka Bentuk untuk menambah komponen pertama anda.",
"event-log": "{event} di {url}",
"forbidden": "Dilarang",
"go-to-settings": "Pergi ke tetapan",
@@ -427,6 +433,7 @@
"triggered-event": "Peristiwa yang dicetuskan",
"unauthorized": "Tidak dibenarkan",
"user-deleted": "Pengguna dipadamkan.",
+ "replay-min-duration-description": "Tunjukkan sahaja ulangan dengan tempoh ini atau lebih.",
"viewed-page": "Halaman dilihat",
"upgrade-required": "Ciri ini memerlukan langganan pelan {plan}.",
"visitor-log": "Pelawat dari {country} mengguna {browser} pada {os} {device}"
diff --git a/public/intl/messages/my-MM.json b/public/intl/messages/my-MM.json
index 74bea7fb6..0e11dad4c 100644
--- a/public/intl/messages/my-MM.json
+++ b/public/intl/messages/my-MM.json
@@ -40,6 +40,7 @@
"campaign": "ကမ်ပိန်း",
"campaigns": "ကမ်ပိန်းများ",
"cancel": "မလုပ်တော့ပါ",
+ "close": "ပိတ်",
"change-password": "စကားဝှက် ပြောင်းမည်",
"channel": "ချန်နယ်",
"channels": "ချန်နယ်များ",
@@ -102,7 +103,9 @@
"download": "ဒေါင်းလုဒ်လုပ်မည်",
"dropoff": "ထွက်ခွာမှု",
"duration": "ကြာချိန်",
+ "min-duration-seconds": "အနည်းဆုံး ကြာချိန် (စက္ကန့်)",
"edit": "ပြုပြင်မည်",
+ "design": "ဒီဇိုင်း",
"edit-member": "အဖွဲ့ဝင် ပြင်ဆင်မည်",
"email": "အီးမေးလ်",
"enable-share-url": "ဝေငှခြင်းကိုလင့်ကို ဖွင့်မည်",
@@ -124,6 +127,7 @@
"filter-combined": "ပေါင်းစပ်ပြီး",
"filter-raw": "အရှိအတိုင်း",
"filters": "Filter များ",
+ "filters-enabled": "စစ်ထုတ်မှုများ ဖွင့်ထားသည်",
"first-click": "ပထမဆုံးနှိပ်ချက်",
"first-seen": "ပထမဆုံးတွေ့ရှိချိန်",
"funnel": "ဖန်နယ်",
@@ -300,6 +304,8 @@
"sum": "ပေါင်းလဒ်",
"support": "ပံ့ပိုးကူညီမှု",
"switch-account": "အကောင့်ပြောင်းမည်",
+ "switch-to-card-view": "ကတ်အမြင်သို့ ပြောင်းမည်",
+ "switch-to-table-view": "ဇယားအမြင်သို့ ပြောင်းမည်",
"table": "ဇယား",
"tablet": "တက်ဘလက်",
"tag": "တဂ်",
@@ -379,7 +385,7 @@
"confirm-reset": "{target} ကို ဖျက်၍ပြန်စလုပ်ရန် သေချာပါသလား?",
"delete-team-warning": "အသင်းကိုဖျက်ပါက အသင်းဝက်ဘ်ဆိုဒ်များအားလုံးကိုပါ ဖျက်ပစ်မည်။",
"delete-website-warning": "ဝက်ဘ်ဆိုဒ် ဒေတာအကုန် ဖျက်မည်",
- "empty-dashboard": "သင်၏ ဒက်ရ်ဘုတ် ဗလာဖြစ်နေသည်။ ပထမဆုံး အစိတ်အပိုင်း ထည့်ရန် တည်းဖြတ်ကို နှိပ်ပါ။",
+ "empty-dashboard": "သင်၏ ဒက်ရှ်ဘုတ် ဗလာဖြစ်နေသည်။ ပထမဆုံး အစိတ်အပိုင်း ထည့်ရန် ဒီဇိုင်း ကို နှိပ်ပါ။",
"error": "မှားယွင်းမှုတစ်ခု ရှိသွားပါသည်",
"event-log": "{url} တွင် {event}",
"forbidden": "ခွင့်ပြုချက်မရှိပါ",
@@ -416,6 +422,7 @@
"triggered-event": "အစပျိုးလိုက်သော အဖြစ်အပျက်",
"unauthorized": "ခွင့်ပြုချက်မရှိပါ",
"user-deleted": "အသုံးပြုသူ ဖျက်ပြီးပါပြီ",
+ "replay-min-duration-description": "ဤကြာချိန် သို့မဟုတ် ၎င်းထက်ပိုသော ပြန်ဖွင့်မှုများကိုသာ ပြပါ။",
"viewed-page": "ကြည့်ရှုခဲ့သောစာမျက်နှာ",
"visitor-log": "{country} မှ {browser} ဖြင့် {os} {device} တွင် ဝင်ရောက်ကြည့်ရှုသူ"
}
diff --git a/public/intl/messages/nb-NO.json b/public/intl/messages/nb-NO.json
index 699500727..ae91c20c8 100644
--- a/public/intl/messages/nb-NO.json
+++ b/public/intl/messages/nb-NO.json
@@ -39,6 +39,7 @@
"campaign": "Kampanje",
"campaigns": "Kampanjer",
"cancel": "Avvis",
+ "close": "Lukk",
"change-password": "Bytt passord",
"channel": "Kanal",
"channels": "Kanaler",
@@ -103,7 +104,9 @@
"download": "Last ned",
"dropoff": "Frafall",
"duration": "Varighet",
+ "min-duration-seconds": "Minimumsvarighet (sekunder)",
"edit": "Rediger",
+ "design": "Design",
"edit-member": "Rediger bruker",
"email": "E-post",
"enable-share-url": "Aktiver delings-URL",
@@ -125,6 +128,7 @@
"filter-combined": "Kombinert",
"filter-raw": "Rå",
"filters": "Filter",
+ "filters-enabled": "Filtre aktivert",
"first-click": "Første klikk",
"first-seen": "Først sett",
"funnel": "Trakt",
@@ -255,7 +259,7 @@
"play": "Spill av",
"replay": "Avspilling",
"replay-id": "Replay ID",
- "replay-code": "Kode for øktavspilling",
+ "recorder-code": "Kode for øktavspilling",
"replay-enabled": "Avspilling aktivert",
"replays": "Avspillinger",
"reports": "Rapporter",
@@ -304,6 +308,8 @@
"sum": "Sum",
"support": "Støtte",
"switch-account": "Bytt konto",
+ "switch-to-card-view": "Bytt til kortvisning",
+ "switch-to-table-view": "Bytt til tabellvisning",
"table": "Tabell",
"tablet": "Nettbrett",
"tag": "Tagg",
@@ -390,7 +396,7 @@
"delete-team-warning": "Å slette et team vil også slette alle teamets nettsteder.",
"delete-website-warning": "Alle tilknyttede data vil også bli slettet.",
"error": "Noe gikk galt.",
- "empty-dashboard": "Dashbordet ditt er tomt. Klikk Rediger for å legge til din første komponent.",
+ "empty-dashboard": "Dashbordet ditt er tomt. Klikk Design for å legge til din første komponent.",
"event-log": "{event} på {url}",
"forbidden": "Forbudt",
"go-to-settings": "Gå til innstillinger",
@@ -427,6 +433,7 @@
"triggered-event": "Utløst hendelse",
"unauthorized": "Ikke autorisert",
"user-deleted": "Bruker slettet.",
+ "replay-min-duration-description": "Vis bare avspillinger med denne varigheten eller lenger.",
"viewed-page": "Vist side",
"upgrade-required": "Denne funksjonen krever et {plan}-abonnement.",
"visitor-log": "Besøkende fra {country} med {browser} på {os} {device}"
diff --git a/public/intl/messages/nl-NL.json b/public/intl/messages/nl-NL.json
index 0749ac70e..a6ebbc41d 100644
--- a/public/intl/messages/nl-NL.json
+++ b/public/intl/messages/nl-NL.json
@@ -39,6 +39,7 @@
"campaign": "Campagne",
"campaigns": "Campagnes",
"cancel": "Annuleren",
+ "close": "Sluiten",
"change-password": "Wachtwoord wijzigen",
"channel": "Kanaal",
"channels": "Kanalen",
@@ -103,7 +104,9 @@
"download": "Downloaden",
"dropoff": "Uitval",
"duration": "Duur",
+ "min-duration-seconds": "Minimale duur (seconden)",
"edit": "Bewerken",
+ "design": "Ontwerp",
"edit-member": "Gebruiker aanpassen",
"email": "E-mail",
"enable-share-url": "Sta delen via openbare URL toe",
@@ -125,6 +128,7 @@
"filter-combined": "Gecombineerd",
"filter-raw": "Ruw",
"filters": "Filters",
+ "filters-enabled": "Filters ingeschakeld",
"first-click": "Eerste klik",
"first-seen": "Eerst gezien",
"funnel": "Trechter",
@@ -255,7 +259,7 @@
"play": "Afspelen",
"replay": "Herhaling",
"replay-id": "Replay ID",
- "replay-code": "Sessieherhalingscode",
+ "recorder-code": "Sessieherhalingscode",
"replay-enabled": "Herhaling ingeschakeld",
"replays": "Herhalingen",
"reports": "Rapporten",
@@ -304,6 +308,8 @@
"sum": "Som",
"support": "Ondersteuning",
"switch-account": "Wissel van account",
+ "switch-to-card-view": "Overschakelen naar kaartweergave",
+ "switch-to-table-view": "Overschakelen naar tabelweergave",
"table": "Tabel",
"tablet": "Tablet",
"tag": "Label",
@@ -390,7 +396,7 @@
"delete-team-warning": "Als een team wordt verwijderd, worden ook alle websites van dat team verwijderd.",
"delete-website-warning": "Alle verwante gegevens zullen ook verwijderd worden.",
"error": "Er is iets misgegaan.",
- "empty-dashboard": "Uw dashboard is leeg. Klik op Bewerken om uw eerste component toe te voegen.",
+ "empty-dashboard": "Uw dashboard is leeg. Klik op Ontwerp om uw eerste component toe te voegen.",
"event-log": "{event} op {url}",
"forbidden": "Verboden",
"go-to-settings": "Naar instellingen",
@@ -427,6 +433,7 @@
"triggered-event": "Getriggerde gebeurtenis",
"unauthorized": "Niet geautoriseerd",
"user-deleted": "Gebruiker verwijderd",
+ "replay-min-duration-description": "Toon alleen herhalingen met deze of een langere duur.",
"viewed-page": "Bekeken pagina",
"upgrade-required": "Deze functie vereist een {plan}-abonnement.",
"visitor-log": "Bezoeker uit {country} met {browser} op een {os} {device}"
diff --git a/public/intl/messages/pl-PL.json b/public/intl/messages/pl-PL.json
index ff3051a38..c58ee505b 100644
--- a/public/intl/messages/pl-PL.json
+++ b/public/intl/messages/pl-PL.json
@@ -39,6 +39,7 @@
"campaign": "Kampania",
"campaigns": "Kampanie",
"cancel": "Anuluj",
+ "close": "Zamknij",
"change-password": "Zmień hasło",
"channel": "Kanał",
"channels": "Kanały",
@@ -103,7 +104,9 @@
"download": "Pobierz",
"dropoff": "Odpływ",
"duration": "Czas trwania",
+ "min-duration-seconds": "Minimalny czas trwania (sekundy)",
"edit": "Edytuj",
+ "design": "Projekt",
"edit-member": "Edytuj członka",
"email": "E-mail",
"enable-share-url": "Włącz udostępnianie adresu URL",
@@ -125,6 +128,7 @@
"filter-combined": "Połączone",
"filter-raw": "Surowe dane",
"filters": "Filtry",
+ "filters-enabled": "Filtry włączone",
"first-click": "Pierwsze kliknięcie",
"first-seen": "Pierwsza wizyta",
"funnel": "Lejek",
@@ -255,7 +259,7 @@
"play": "Odtwórz",
"replay": "Odtwarzanie",
"replay-id": "Replay ID",
- "replay-code": "Kod odtworzenia sesji",
+ "recorder-code": "Kod odtworzenia sesji",
"replay-enabled": "Odtwarzanie włączone",
"replays": "Odtwarzania",
"reports": "Raporty",
@@ -304,6 +308,8 @@
"sum": "Suma",
"support": "Wsparcie",
"switch-account": "Przełącz konto",
+ "switch-to-card-view": "Przełącz na widok kart",
+ "switch-to-table-view": "Przełącz na widok tabeli",
"table": "Tabela",
"tablet": "Tablet",
"tag": "Tag",
@@ -390,7 +396,7 @@
"delete-team-warning": "Usunięcie zespołu usunie wszystkie jego witryny.",
"delete-website-warning": "Wszystkie powiązane dane również zostaną usunięte.",
"error": "Coś poszło nie tak.",
- "empty-dashboard": "Twój pulpit jest pusty. Kliknij Edytuj, aby dodać pierwszy komponent.",
+ "empty-dashboard": "Twój pulpit jest pusty. Kliknij Projekt, aby dodać pierwszy komponent.",
"event-log": "{event} na {url}",
"forbidden": "Zabronione",
"go-to-settings": "Przejdź do ustawień",
@@ -427,6 +433,7 @@
"triggered-event": "Zdarzenie wyzwalające",
"unauthorized": "Brak autoryzacji",
"user-deleted": "Użytkownik usunięty.",
+ "replay-min-duration-description": "Pokazuj tylko nagrania o tej lub dłuższej długości.",
"viewed-page": "Obejrzana strona",
"upgrade-required": "Ta funkcja wymaga subskrypcji planu {plan}.",
"visitor-log": "Odwiedzający z {country} używa {browser} na {os} {device}"
diff --git a/public/intl/messages/pt-BR.json b/public/intl/messages/pt-BR.json
index 9ee595584..55d1a158c 100644
--- a/public/intl/messages/pt-BR.json
+++ b/public/intl/messages/pt-BR.json
@@ -39,6 +39,7 @@
"campaign": "Campanha",
"campaigns": "Campanhas",
"cancel": "Cancelar",
+ "close": "Fechar",
"change-password": "Alterar senha",
"channel": "Canal",
"channels": "Canais",
@@ -103,7 +104,9 @@
"download": "Download",
"dropoff": "Abandono",
"duration": "Duração",
+ "min-duration-seconds": "Duração mínima (segundos)",
"edit": "Editar",
+ "design": "Design",
"edit-member": "Editar membro",
"email": "E-mail",
"enable-share-url": "Ativar link para compartilhar",
@@ -125,6 +128,7 @@
"filter-combined": "Combinado",
"filter-raw": "Bruto",
"filters": "Filtros",
+ "filters-enabled": "Filtros ativados",
"first-click": "Primeiro clique",
"first-seen": "Visto pela primeira vez",
"funnel": "Funil",
@@ -255,7 +259,7 @@
"play": "Reproduzir",
"replay": "Reprodução",
"replay-id": "Replay ID",
- "replay-code": "Código de replay da sessão",
+ "recorder-code": "Código de replay da sessão",
"replay-enabled": "Reprodução habilitada",
"replays": "Reproduções",
"reports": "Relatórios",
@@ -304,6 +308,8 @@
"sum": "Soma",
"support": "Suporte",
"switch-account": "Trocar conta",
+ "switch-to-card-view": "Alternar para visualização em cartões",
+ "switch-to-table-view": "Alternar para visualização em tabela",
"table": "Tabela",
"tablet": "Tablet",
"tag": "Tag",
@@ -390,7 +396,7 @@
"delete-team-warning": "Excluir a equipe também excluirá todos os sites da equipe.",
"delete-website-warning": "Todos os dados relacionados serão excluídos.",
"error": "Ocorreu um erro.",
- "empty-dashboard": "Seu painel está vazio. Clique em Editar para adicionar seu primeiro componente.",
+ "empty-dashboard": "Seu painel está vazio. Clique em Design para adicionar seu primeiro componente.",
"event-log": "{event} em {url}",
"forbidden": "Proibido",
"go-to-settings": "Ir para as configurações",
@@ -427,6 +433,7 @@
"triggered-event": "Evento disparado",
"unauthorized": "Não autorizado",
"user-deleted": "Usuário excluído.",
+ "replay-min-duration-description": "Mostrar apenas repetições com esta duração ou superior.",
"viewed-page": "Página visualizada",
"upgrade-required": "Este recurso requer uma assinatura do plano {plan}.",
"visitor-log": "Visitante de {country} usando o navegador {browser} em um {device} com sistema operacional {os}."
diff --git a/public/intl/messages/pt-PT.json b/public/intl/messages/pt-PT.json
index 625b60d01..cba9a33d1 100644
--- a/public/intl/messages/pt-PT.json
+++ b/public/intl/messages/pt-PT.json
@@ -39,6 +39,7 @@
"campaign": "Campanha",
"campaigns": "Campanhas",
"cancel": "Cancelar",
+ "close": "Fechar",
"change-password": "Alterar senha",
"channel": "Canal",
"channels": "Canais",
@@ -103,7 +104,9 @@
"download": "Transferir",
"dropoff": "Abandono",
"duration": "Duração",
+ "min-duration-seconds": "Duração mínima (segundos)",
"edit": "Editar",
+ "design": "Design",
"edit-member": "Editar membro",
"email": "E-mail",
"enable-share-url": "Ativar link de partilha",
@@ -125,6 +128,7 @@
"filter-combined": "Combinado",
"filter-raw": "Dados brutos",
"filters": "Filtros",
+ "filters-enabled": "Filtros ativados",
"first-click": "Primeiro clique",
"first-seen": "Primeira visualização",
"funnel": "Funil",
@@ -255,7 +259,7 @@
"play": "Reproduzir",
"replay": "Reprodução",
"replay-id": "Replay ID",
- "replay-code": "Código de repetição de sessão",
+ "recorder-code": "Código de repetição de sessão",
"replay-enabled": "Reprodução ativada",
"replays": "Reproduções",
"reports": "Relatórios",
@@ -304,6 +308,8 @@
"sum": "Soma",
"support": "Suporte",
"switch-account": "Trocar de conta",
+ "switch-to-card-view": "Mudar para vista de cartões",
+ "switch-to-table-view": "Mudar para vista de tabela",
"table": "Tabela",
"tablet": "Tablet",
"tag": "Etiqueta",
@@ -390,7 +396,7 @@
"delete-team-warning": "Eliminar uma equipa também elimina todos os websites da equipa.",
"delete-website-warning": "Todos os dados associados também serão eliminados.",
"error": "Ocorreu um erro.",
- "empty-dashboard": "O seu painel está vazio. Clique em Editar para adicionar o seu primeiro componente.",
+ "empty-dashboard": "O seu painel está vazio. Clique em Design para adicionar o seu primeiro componente.",
"event-log": "{event} em {url}",
"forbidden": "Proibido",
"go-to-settings": "Ir para as definições",
@@ -427,6 +433,7 @@
"triggered-event": "Evento acionado",
"unauthorized": "Não autorizado",
"user-deleted": "Utilizador eliminado.",
+ "replay-min-duration-description": "Mostrar apenas repetições com esta duração ou superior.",
"viewed-page": "Página visualizada",
"upgrade-required": "Esta funcionalidade requer uma subscrição do plano {plan}.",
"visitor-log": "Visitante de {country} a usar {browser} no {device} {os}"
diff --git a/public/intl/messages/ro-RO.json b/public/intl/messages/ro-RO.json
index b9f010d09..6e44c9d2a 100644
--- a/public/intl/messages/ro-RO.json
+++ b/public/intl/messages/ro-RO.json
@@ -39,6 +39,7 @@
"campaign": "Campanie",
"campaigns": "Campanii",
"cancel": "Anulează",
+ "close": "Închide",
"change-password": "Schimbare parolă",
"channel": "Canal",
"channels": "Canale",
@@ -103,7 +104,9 @@
"download": "Descarcă",
"dropoff": "Rată de abandon",
"duration": "Durată",
+ "min-duration-seconds": "Durată minimă (secunde)",
"edit": "Editare",
+ "design": "Design",
"edit-member": "Editare membru",
"email": "Email",
"enable-share-url": "Activare adresă URL de distribuire",
@@ -125,6 +128,7 @@
"filter-combined": "Combinat",
"filter-raw": "Brut",
"filters": "Filtre",
+ "filters-enabled": "Filtre activate",
"first-click": "Primul click",
"first-seen": "Văzut pentru prima dată",
"funnel": "Parcursul utilizatorului",
@@ -255,7 +259,7 @@
"play": "Redă",
"replay": "Reluare",
"replay-id": "Replay ID",
- "replay-code": "Cod de reluare sesiune",
+ "recorder-code": "Cod de reluare sesiune",
"replay-enabled": "Reluare activată",
"replays": "Reluări",
"reports": "Rapoarte",
@@ -304,6 +308,8 @@
"sum": "Sumă",
"support": "Suport",
"switch-account": "Schimbă contul",
+ "switch-to-card-view": "Comută la vizualizarea card",
+ "switch-to-table-view": "Comută la vizualizarea tabel",
"table": "Tabel",
"tablet": "Tabletă",
"tag": "Etichetă",
@@ -390,7 +396,7 @@
"delete-team-warning": "Ștergerea unei echipe va șterge și toate website-urile echipei.",
"delete-website-warning": "Toate datele asociate vor fi șterse, de asemenea.",
"error": "Ceva n-a mers bine.",
- "empty-dashboard": "Tabloul de bord este gol. Faceți clic pe Editare pentru a adăuga primul component.",
+ "empty-dashboard": "Tabloul de bord este gol. Faceți clic pe Design pentru a adăuga primul component.",
"event-log": "{event} la {url}",
"forbidden": "Interzis",
"go-to-settings": "Mergi la Setări",
@@ -427,6 +433,7 @@
"triggered-event": "Eveniment declanșat",
"unauthorized": "Neautorizat",
"user-deleted": "Utilizator șters.",
+ "replay-min-duration-description": "Afișează doar reluările cu această durată sau mai mare.",
"viewed-page": "Pagină vizualizată",
"upgrade-required": "Această funcție necesită un abonament {plan}.",
"visitor-log": "Vizitator din {country} folosind {browser} pe {os} {device}"
diff --git a/public/intl/messages/ru-RU.json b/public/intl/messages/ru-RU.json
index 5bf41593c..2b649bb5b 100644
--- a/public/intl/messages/ru-RU.json
+++ b/public/intl/messages/ru-RU.json
@@ -33,6 +33,7 @@
"behavior": "Поведение",
"block-selector": "Выбор блока",
"boards": "Доски",
+ "board-type": "Тип доски",
"bounce-rate": "Отказы",
"breakdown": "Авария",
"browser": "Браузер",
@@ -40,6 +41,7 @@
"campaign": "Кампания",
"campaigns": "Кампании",
"cancel": "Отменить",
+ "close": "Закрыть",
"change-password": "Изменить пароль",
"channel": "Канал",
"channels": "Каналы",
@@ -70,6 +72,7 @@
"created": "Создано",
"created-by": "Создано",
"criteria": "Критерии",
+ "cumulative": "Накопительно",
"currency": "Валюта",
"current": "Текущий",
"current-password": "Текущий пароль",
@@ -102,7 +105,9 @@
"download": "Скачать",
"dropoff": "Высадка",
"duration": "Длительность",
+ "min-duration-seconds": "Минимальная продолжительность (секунды)",
"edit": "Изменить",
+ "design": "Дизайн",
"edit-member": "Редактировать участника",
"email": "Электронная почта",
"enable-share-url": "Разрешить делиться ссылкой",
@@ -124,6 +129,9 @@
"filter-combined": "Объединенные",
"filter-raw": "Сырые данные",
"filters": "Фильтры",
+ "filters-enabled": "Фильтры включены",
+ "property-filter": "Фильтр по свойству",
+ "activity-by-property": "Активность по свойству",
"first-click": "Первый клик",
"first-seen": "Первый вход",
"funnel": "Воронка",
@@ -137,6 +145,9 @@
"greater-than-equals": "Больше или равно",
"grouped": "Группировано",
"growth": "Рост",
+ "heatmap": "Тепловая карта",
+ "heatmap-description": "Визуализируйте, где посетители кликают и как далеко прокручивают ваши страницы.",
+ "heatmaps": "Тепловые карты",
"hostname": "Имя хоста",
"hour": "Час",
"includes": "Включает",
@@ -173,6 +184,7 @@
"links": "Ссылки",
"location": "Местоположение",
"login": "Войти",
+ "llm": "LLM",
"logout": "Выйти",
"manage": "Управление",
"manager": "Менеджер",
@@ -201,6 +213,7 @@
"number-of-records": "{x} {x, plural, one {запись} other {записи}}",
"ok": "ОК",
"online": "Онлайн",
+ "open": "Смешанный",
"organic-search": "Органический поиск",
"organic-shopping": "Органические покупки",
"organic-social": "Органические соцсети",
@@ -214,6 +227,7 @@
"page-views": "Просмотры страниц",
"pages": "Страницы",
"pageTitle": "Название страницы",
+ "period": "Период",
"paid-ads": "Платная реклама",
"paid-search": "Платный поиск",
"paid-shopping": "Платные покупки",
@@ -256,6 +270,7 @@
"replay": "Запись сессии",
"replay-enabled": "Запись сессий включена",
"replay-id": "Replay ID",
+ "recorder-code": "Код записи",
"replays": "Записи сессий",
"reports": "Отчеты",
"required": "Обязательное",
@@ -272,6 +287,7 @@
"sample-size": "Размер выборки",
"save": "Сохранить",
"save-cohort": "Сохранить когорту",
+ "save-replay": "Сохранить запись",
"save-segment": "Сохранить сегмент",
"screen": "Экран",
"screens": "Экраны",
@@ -279,7 +295,10 @@
"segment": "Сегмент",
"segments": "Сегменты",
"select": "Выберите",
+ "select-link": "Выберите ссылку",
+ "select-pixel": "Выберите пиксель",
"select-component": "Выберите компонент",
+ "select-event": "Выберите событие",
"select-date": "Выберите дату",
"select-filter": "Выберите фильтр",
"select-role": "Выберите роль",
@@ -300,6 +319,8 @@
"sum": "Сумма",
"support": "Поддержка",
"switch-account": "Сменить аккаунт",
+ "switch-to-card-view": "Переключиться в режим карточек",
+ "switch-to-table-view": "Переключиться в режим таблицы",
"table": "Таблица",
"tablet": "Планшет",
"tag": "Тег",
@@ -338,8 +359,12 @@
"unique-events": "Уникальные события",
"unique-visitors": "Уникальные посетители",
"uniqueCustomers": "Уникальные клиенты",
+ "customers": "Клиенты",
+ "orders": "Заказы",
+ "saved": "Сохранено",
"unknown": "Неизвестно",
"untitled": "Без названия",
+ "upgrade": "Улучшить план",
"update": "Обновление",
"url": "URL",
"user": "Пользователь",
@@ -379,7 +404,7 @@
"confirm-reset": "Вы уверены, что хотите сбросить статистику {target}?",
"delete-team-warning": "При удалении команды будут удалены и все ее веб-сайты.",
"delete-website-warning": "Все связанные данные будут также удалены.",
- "empty-dashboard": "Ваша панель управления пуста. Нажмите Изменить, чтобы добавить первый компонент.",
+ "empty-dashboard": "Ваша панель управления пуста. Нажмите Дизайн, чтобы добавить первый компонент.",
"error": "Что-то пошло не так.",
"event-log": "{event} на {url}",
"forbidden": "Доступ запрещён",
@@ -402,6 +427,7 @@
"reset-website": "Для сброса введите RESET",
"reset-website-warning": "Вся статистика для этого сайта будет удалена, но ваш код отслеживания останется нетронутым.",
"saved": "Успешно сохранено.",
+ "select-board-entity-first": "Сначала выберите сайт, пиксель или ссылку.",
"select-component-preview": "Выберите компонент для предпросмотра",
"select-website-first": "Сначала выберите сайт",
"sever-error": "Ошибка сервера",
@@ -416,7 +442,9 @@
"triggered-event": "Запущенное событие",
"unauthorized": "Не авторизован",
"user-deleted": "Пользователь удален.",
+ "replay-min-duration-description": "Показывать только воспроизведения с такой или большей продолжительностью.",
"viewed-page": "Просмотренная страница",
+ "upgrade-required": "Для этой функции требуется подписка на план {plan}.",
"visitor-log": "Посетитель из {country} используя {browser} на {os} {device}"
}
}
diff --git a/public/intl/messages/si-LK.json b/public/intl/messages/si-LK.json
index 570687e4b..89bb298e8 100644
--- a/public/intl/messages/si-LK.json
+++ b/public/intl/messages/si-LK.json
@@ -40,6 +40,7 @@
"campaign": "ව්යාපාරය",
"campaigns": "ව්යාපාර",
"cancel": "අවලංගු කරන්න",
+ "close": "වසන්න",
"change-password": "මුරපදය වෙනස් කරන්න",
"channel": "නාලිකාව",
"channels": "නාලිකා",
@@ -102,7 +103,9 @@
"download": "බාගන්න",
"dropoff": "අතහැර යෑම",
"duration": "කාලය",
+ "min-duration-seconds": "අවම කාලය (තත්පර)",
"edit": "සංස්කරණය කරන්න",
+ "design": "සැලසුම",
"edit-member": "සාමාජිකයා සංස්කරණය",
"email": "විද්යුත් තැපෑල",
"enable-share-url": "බෙදාගැනීමේ URL සබල කරන්න",
@@ -124,6 +127,7 @@
"filter-combined": "ඒකාබද්ධ",
"filter-raw": "අමු",
"filters": "පෙරහන්",
+ "filters-enabled": "පෙරහන් සක්රීය කර ඇත",
"first-click": "පළමු ක්ලික්",
"first-seen": "මුලින් දුටු",
"funnel": "පුනීලය",
@@ -300,6 +304,8 @@
"sum": "එකතුව",
"support": "සහාය",
"switch-account": "ගිණුම මාරු කරන්න",
+ "switch-to-card-view": "කාඩ් දසුනට මාරු කරන්න",
+ "switch-to-table-view": "වගු දසුනට මාරු කරන්න",
"table": "වගුව",
"tablet": "ටැබ්ලටය",
"tag": "ටැගය",
@@ -379,7 +385,7 @@
"confirm-reset": "{target} ට අදාල සංඛ්යාලේඛන නැවත පිහිටුවීමට අවශ්යද?",
"delete-team-warning": "කණ්ඩායමක් මකා දැමීමෙන් එහි සියලු වෙබ් අඩවි ද මැකෙනු ඇත.",
"delete-website-warning": "සියලුම වෙබ් අඩවි දත්ත මකා දැමෙනු ඇත.",
- "empty-dashboard": "ඔබේ ඩැෂ්බෝඩ් හිස් ය. ඔබේ පළමු කොටස එක් කිරීමට සංස්කරණය ක්ලික් කරන්න.",
+ "empty-dashboard": "ඔබේ ඩැෂ්බෝඩ් හිස් ය. ඔබේ පළමු කොටස එක් කිරීමට සැලසුම ක්ලික් කරන්න.",
"error": "යමක් වැරදී ගියා.",
"event-log": "{url} හි {event}",
"forbidden": "තහනම්",
@@ -416,6 +422,7 @@
"triggered-event": "අවුලුවන ලද සිදුවීම",
"unauthorized": "අනවසරයි",
"user-deleted": "පරිශීලකයා මකා දමන ලදි.",
+ "replay-min-duration-description": "මෙම කාලය හෝ ඊට වඩා වැඩි රිප්ලේ පමණක් පෙන්වන්න.",
"viewed-page": "නැරඹූ පිටුව",
"visitor-log": "{country} සිට {os} {device} හි {browser} භාවිතා කරන නරඹන්නා"
}
diff --git a/public/intl/messages/sk-SK.json b/public/intl/messages/sk-SK.json
index d55ea416d..8bb215381 100644
--- a/public/intl/messages/sk-SK.json
+++ b/public/intl/messages/sk-SK.json
@@ -39,6 +39,7 @@
"campaign": "Kampaň",
"campaigns": "Kampane",
"cancel": "Zrušiť",
+ "close": "Zavrieť",
"change-password": "Zmeniť heslo",
"channel": "Kanál",
"channels": "Kanály",
@@ -103,7 +104,9 @@
"download": "Stiahnuť",
"dropoff": "Odchod",
"duration": "Trvanie",
+ "min-duration-seconds": "Minimálne trvanie (sekundy)",
"edit": "Upraviť",
+ "design": "Návrh",
"edit-member": "Upraviť člena",
"email": "E-mail",
"enable-share-url": "Povoliť zdielanie URL",
@@ -125,6 +128,7 @@
"filter-combined": "Kombinácie",
"filter-raw": "Nezpracované",
"filters": "Filtre",
+ "filters-enabled": "Filtre povolené",
"first-click": "Prvé kliknutie",
"first-seen": "Prvýkrát videné",
"funnel": "Lievik",
@@ -255,7 +259,7 @@
"play": "Prehrať",
"replay": "Prehrávanie",
"replay-id": "Replay ID",
- "replay-code": "Kód prehrania relácie",
+ "recorder-code": "Kód prehrania relácie",
"replay-enabled": "Prehrávanie povolené",
"replays": "Prehrávanie",
"reports": "Správy",
@@ -304,6 +308,8 @@
"sum": "Súčet",
"support": "Podpora",
"switch-account": "Prepnúť účet",
+ "switch-to-card-view": "Prepnúť na zobrazenie kariet",
+ "switch-to-table-view": "Prepnúť na zobrazenie tabuľky",
"table": "Tabuľka",
"tablet": "Tablet",
"tag": "Značka",
@@ -390,7 +396,7 @@
"delete-team-warning": "Vymazanie tímu vymaže aj všetky weby tímu.",
"delete-website-warning": "Všetky príbuzné data budu tiež zmazané.",
"error": "Niečo sa pokazilo.",
- "empty-dashboard": "Váš dashboard je prázdny. Kliknite na Upraviť a pridajte prvý komponent.",
+ "empty-dashboard": "Váš dashboard je prázdny. Kliknite na Návrh a pridajte prvý komponent.",
"event-log": "{event} na {url}",
"forbidden": "Zakázané",
"go-to-settings": "Ísť do nastavení",
@@ -427,6 +433,7 @@
"triggered-event": "Spustená udalosť",
"unauthorized": "Neautorizované",
"user-deleted": "Používateľ vymazaný.",
+ "replay-min-duration-description": "Zobrazovať len nahrávky s touto alebo dlhšou dĺžkou.",
"viewed-page": "Zobrazená stránka",
"upgrade-required": "Táto funkcia vyžaduje predplatné plánu {plan}.",
"visitor-log": "Návštevník z {country} s prehliadačom {browser} na {os} {device}"
diff --git a/public/intl/messages/sl-SI.json b/public/intl/messages/sl-SI.json
index 7e4573297..bdda7edb6 100644
--- a/public/intl/messages/sl-SI.json
+++ b/public/intl/messages/sl-SI.json
@@ -39,6 +39,7 @@
"campaign": "Kampanja",
"campaigns": "Kampanje",
"cancel": "Prekliči",
+ "close": "Zapri",
"change-password": "Zamenjaj geslo",
"channel": "Kanal",
"channels": "Kanali",
@@ -103,7 +104,9 @@
"download": "Prenesi",
"dropoff": "Zapustitev",
"duration": "Trajanje",
+ "min-duration-seconds": "Minimalno trajanje (sekunde)",
"edit": "Uredi",
+ "design": "Oblikovanje",
"edit-member": "Uredi člana",
"email": "E-pošta",
"enable-share-url": "Omogoči povezavo za deljenje",
@@ -125,6 +128,7 @@
"filter-combined": "Skupaj",
"filter-raw": "Neobdelano",
"filters": "Filtri",
+ "filters-enabled": "Filtri omogočeni",
"first-click": "Prvi klik",
"first-seen": "Prvič viden",
"funnel": "Prodajni lijak",
@@ -255,7 +259,7 @@
"play": "Predvajaj",
"replay": "Predvajanje",
"replay-id": "Replay ID",
- "replay-code": "Koda ponovitve seje",
+ "recorder-code": "Koda ponovitve seje",
"replay-enabled": "Predvajanje omogočeno",
"replays": "Predvajanja",
"reports": "Poročila",
@@ -304,6 +308,8 @@
"sum": "Seštevek",
"support": "Podpora",
"switch-account": "Zamenjaj račun",
+ "switch-to-card-view": "Preklopi na pogled kartic",
+ "switch-to-table-view": "Preklopi na pogled tabele",
"table": "Tabela",
"tablet": "Tablični računalnik",
"tag": "Oznaka",
@@ -393,7 +399,7 @@
"delete-team-warning": "Brisanje ekipe bo izbrisalo tudi vsa spletna mesta ekipe.",
"delete-website-warning": "Izbrisani bodo tudi vsi pripadajoči podatki.",
"error": "Nekaj je šlo narobe.",
- "empty-dashboard": "Vaša nadzorna plošča je prazna. Kliknite Uredi, da dodate svojo prvo komponento.",
+ "empty-dashboard": "Vaša nadzorna plošča je prazna. Kliknite Oblikovanje, da dodate svojo prvo komponento.",
"event-log": "{event} na {url}",
"forbidden": "Prepovedano",
"go-to-settings": "Pojdi v nastavitve",
@@ -430,6 +436,7 @@
"triggered-event": "Sprožen dogodek",
"unauthorized": "Nepooblaščen dostop",
"user-deleted": "Uporabnik je izbrisan.",
+ "replay-min-duration-description": "Prikaži samo posnetke s tem ali daljšim trajanjem.",
"viewed-page": "Ogledana stran",
"upgrade-required": "Ta funkcija zahteva naročnino na paket {plan}.",
"visitor-log": "Obiskovalec iz {country} uporablja {browser} na {os} {device}",
diff --git a/public/intl/messages/sv-SE.json b/public/intl/messages/sv-SE.json
index cc175b334..4d82f0a81 100644
--- a/public/intl/messages/sv-SE.json
+++ b/public/intl/messages/sv-SE.json
@@ -39,6 +39,7 @@
"campaign": "Kampanj",
"campaigns": "Kampanjer",
"cancel": "Avbryt",
+ "close": "Stäng",
"change-password": "Byt lösenord",
"channel": "Kanal",
"channels": "Kanaler",
@@ -103,7 +104,9 @@
"download": "Ladda ner",
"dropoff": "Bortfall",
"duration": "Varaktighet",
+ "min-duration-seconds": "Minsta varaktighet (sekunder)",
"edit": "Redigera",
+ "design": "Design",
"edit-member": "Redigera medlem",
"email": "E-post",
"enable-share-url": "Aktivera delningslänk",
@@ -125,6 +128,7 @@
"filter-combined": "Kombinerade",
"filter-raw": "Rådata",
"filters": "Filter",
+ "filters-enabled": "Filter aktiverade",
"first-click": "Första klicket",
"first-seen": "Först sedd",
"funnel": "Tratt",
@@ -255,7 +259,7 @@
"play": "Spela upp",
"replay": "Uppspelning",
"replay-id": "Replay ID",
- "replay-code": "Kod för sessionsuppspelning",
+ "recorder-code": "Kod för sessionsuppspelning",
"replay-enabled": "Uppspelning aktiverad",
"replays": "Uppspelningar",
"reports": "Rapporter",
@@ -304,6 +308,8 @@
"sum": "Summa",
"support": "Support",
"switch-account": "Byt konto",
+ "switch-to-card-view": "Byt till kortvy",
+ "switch-to-table-view": "Byt till tabellvy",
"table": "Tabell",
"tablet": "Surfplatta",
"tag": "Tagg",
@@ -390,7 +396,7 @@
"delete-team-warning": "Att radera ett team raderar även alla teamets webbplatser.",
"delete-website-warning": "All tillhörande data kommer också att raderas.",
"error": "Något gick fel.",
- "empty-dashboard": "Din instrumentpanel är tom. Klicka på Redigera för att lägga till din första komponent.",
+ "empty-dashboard": "Din instrumentpanel är tom. Klicka på Design för att lägga till din första komponent.",
"event-log": "{event} på {url}",
"forbidden": "Förbjudet",
"go-to-settings": "Gå till inställningar",
@@ -427,6 +433,7 @@
"triggered-event": "Utlöst händelse",
"unauthorized": "Inte auktoriserad",
"user-deleted": "Användaren har raderats.",
+ "replay-min-duration-description": "Visa bara uppspelningar med den här varaktigheten eller längre.",
"viewed-page": "Visad sida",
"upgrade-required": "Den här funktionen kräver en {plan}-prenumeration.",
"visitor-log": "Besökare från {country} med {browser} på {os} {device}"
diff --git a/public/intl/messages/ta-IN.json b/public/intl/messages/ta-IN.json
index 5092f810d..d5c953af8 100644
--- a/public/intl/messages/ta-IN.json
+++ b/public/intl/messages/ta-IN.json
@@ -40,6 +40,7 @@
"campaign": "பிரச்சாரம்",
"campaigns": "பிரச்சாரங்கள்",
"cancel": "ரத்துசெய்",
+ "close": "மூடு",
"change-password": "கடவுச்சொல்லை மாற்று",
"channel": "சேனல்",
"channels": "சேனல்கள்",
@@ -102,7 +103,9 @@
"download": "பதிவிறக்கு",
"dropoff": "விலகல்",
"duration": "கால அளவு",
+ "min-duration-seconds": "குறைந்தபட்ச கால அளவு (விநாடிகள்)",
"edit": "திருத்துதல்",
+ "design": "வடிவமைப்பு",
"edit-member": "உறுப்பினரைத் திருத்து",
"email": "மின்னஞ்சல்",
"enable-share-url": "கள முகவரியை பகிரலாம்",
@@ -124,6 +127,7 @@
"filter-combined": "ஒருங்கிணைந்த",
"filter-raw": "மூல",
"filters": "வடிகட்டிகள்",
+ "filters-enabled": "வடிகட்டிகள் இயக்கப்பட்டன",
"first-click": "முதல் கிளிக்",
"first-seen": "முதலில் பார்த்தது",
"funnel": "புனல்",
@@ -300,6 +304,8 @@
"sum": "கூட்டுத்தொகை",
"support": "ஆதரவு",
"switch-account": "கணக்கை மாற்று",
+ "switch-to-card-view": "அட்டை காட்சிக்கு மாற்று",
+ "switch-to-table-view": "அட்டவணை காட்சிக்கு மாற்று",
"table": "அட்டவணை",
"tablet": "கையடக்க கணினி",
"tag": "குறிச்சொல்",
@@ -379,7 +385,7 @@
"confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"delete-team-warning": "ஒரு குழுவை நீக்கினால் அனைத்து குழு வலைத்தளங்களும் நீக்கப்படும்.",
"delete-website-warning": "தொடர்புடைய எல்லா தரவும் நீக்கப்படும்.",
- "empty-dashboard": "உங்கள் டாஷ்போர்டு காலியாக உள்ளது. உங்கள் முதல் கூறை சேர்க்க திருத்து கிளிக் செய்யுங்கள்.",
+ "empty-dashboard": "உங்கள் டாஷ்போர்டு காலியாக உள்ளது. உங்கள் முதல் கூறை சேர்க்க வடிவமைப்பு கிளிக் செய்யுங்கள்.",
"error": "ஏதோ தவறு நடந்துவிட்டது.",
"event-log": "{event} {url} இல்",
"forbidden": "தடைசெய்யப்பட்டது",
@@ -416,6 +422,7 @@
"triggered-event": "தூண்டப்பட்ட நிகழ்வு",
"unauthorized": "அங்கீகரிக்கப்படவில்லை",
"user-deleted": "பயனர் நீக்கப்பட்டார்.",
+ "replay-min-duration-description": "இந்த காலவரையறை அல்லது அதற்கும் அதிகமான மறுஒலிப்புகளை மட்டும் காட்டு.",
"viewed-page": "பார்வையிட்ட பக்கம்",
"visitor-log": "{country}வில் இருந்து பார்வையாளர் {browser} ஐ {os} {device}லில் பயன்படுத்துகிறார்"
}
diff --git a/public/intl/messages/th-TH.json b/public/intl/messages/th-TH.json
index 275152e4b..2cf3298ce 100644
--- a/public/intl/messages/th-TH.json
+++ b/public/intl/messages/th-TH.json
@@ -40,6 +40,7 @@
"campaign": "แคมเปญ",
"campaigns": "แคมเปญ",
"cancel": "ยกเลิก",
+ "close": "ปิด",
"change-password": "เปลี่ยนรหัสผ่าน",
"channel": "ช่องทาง",
"channels": "ช่องทาง",
@@ -102,7 +103,9 @@
"download": "ดาวน์โหลด",
"dropoff": "การออกจากระบบ",
"duration": "ระยะเวลา",
+ "min-duration-seconds": "ระยะเวลาขั้นต่ำ (วินาที)",
"edit": "แก้ไข",
+ "design": "ออกแบบ",
"edit-member": "แก้ไขสมาชิก",
"email": "อีเมล",
"enable-share-url": "เปิดใช้งานการแชร์ลิงก์",
@@ -124,6 +127,7 @@
"filter-combined": "ข้อมูลรวม",
"filter-raw": "ข้อมูลดิบ",
"filters": "ตัวกรอง",
+ "filters-enabled": "เปิดใช้งานตัวกรอง",
"first-click": "คลิกแรก",
"first-seen": "เห็นครั้งแรก",
"funnel": "ช่องทางขาย",
@@ -300,6 +304,8 @@
"sum": "ผลรวม",
"support": "การสนับสนุน",
"switch-account": "สลับบัญชี",
+ "switch-to-card-view": "เปลี่ยนเป็นมุมมองการ์ด",
+ "switch-to-table-view": "เปลี่ยนเป็นมุมมองตาราง",
"table": "ตาราง",
"tablet": "แท็บเล็ต",
"tag": "แท็ก",
@@ -379,7 +385,7 @@
"confirm-reset": "คุณแน่ใจหรือไม่ว่าต้องการรีเซตข้อมูลสถิติของ {target} ?",
"delete-team-warning": "การลบทีมจะลบเว็บไซต์ทั้งหมดของทีมด้วย",
"delete-website-warning": "ข้อมูลที่เกี่ยวข้องทั้งหมดจะถูกลบ.",
- "empty-dashboard": "แดชบอร์ดของคุณว่างเปล่า คลิกแก้ไขเพื่อเพิ่มส่วนประกอบแรก",
+ "empty-dashboard": "แดชบอร์ดของคุณว่างเปล่า คลิก ออกแบบ เพื่อเพิ่มส่วนประกอบแรก",
"error": "เกิดข้อผิดพลาด.",
"event-log": "{event} บน {url}",
"forbidden": "ไม่ได้รับอนุญาต",
@@ -416,6 +422,7 @@
"triggered-event": "เหตุการณ์ที่ถูกเรียก",
"unauthorized": "ไม่ได้รับอนุญาต",
"user-deleted": "ผู้ใช้ถูกลบแล้ว",
+ "replay-min-duration-description": "แสดงเฉพาะการเล่นซ้ำที่มีระยะเวลาเท่ากับหรือมากกว่านี้",
"viewed-page": "หน้าที่ดู",
"visitor-log": "ผู้เข้าชมจาก {country} กำลังใช้งานผ่าน {browser} บน {os} {device}"
}
diff --git a/public/intl/messages/tr-TR.json b/public/intl/messages/tr-TR.json
index 7c915c393..10d27ede5 100644
--- a/public/intl/messages/tr-TR.json
+++ b/public/intl/messages/tr-TR.json
@@ -39,6 +39,7 @@
"campaign": "Kampanya",
"campaigns": "Kampanyalar",
"cancel": "İptal",
+ "close": "Kapat",
"change-password": "Şifre değiştir",
"channel": "Kanal",
"channels": "Kanallar",
@@ -103,7 +104,9 @@
"download": "İndir",
"dropoff": "Bırakma",
"duration": "Süre",
+ "min-duration-seconds": "Minimum süre (saniye)",
"edit": "Düzenle",
+ "design": "Tasarım",
"edit-member": "Üyeyi düzenle",
"email": "E-posta",
"enable-share-url": "Anonim paylaşım URL'i aktif",
@@ -125,6 +128,7 @@
"filter-combined": "Birleşik filtre",
"filter-raw": "Ham filtre",
"filters": "Filtreler",
+ "filters-enabled": "Filtreler etkin",
"first-click": "İlk tıklama",
"first-seen": "İlk görülme",
"funnel": "Huni",
@@ -255,7 +259,7 @@
"play": "Oynat",
"replay": "Tekrar oynatma",
"replay-id": "Replay ID",
- "replay-code": "Oturum tekrar oynatma kodu",
+ "recorder-code": "Oturum tekrar oynatma kodu",
"replay-enabled": "Tekrar oynatma etkin",
"replays": "Tekrar oynatmalar",
"reports": "Raporlar",
@@ -304,6 +308,8 @@
"sum": "Toplam",
"support": "Destek",
"switch-account": "Hesap değiştir",
+ "switch-to-card-view": "Kart görünümüne geç",
+ "switch-to-table-view": "Tablo görünümüne geç",
"table": "Tablo",
"tablet": "Tablet",
"tag": "Etiket",
@@ -390,7 +396,7 @@
"delete-team-warning": "Bir takımı silmek tüm takım web sitelerini de silecektir.",
"delete-website-warning": "İlişkili tüm veriler de silinecektir.",
"error": "Bir şeyler ters gitti!",
- "empty-dashboard": "Gösterge paneliniz boş. İlk bileşeninizi eklemek için Düzenle'ye tıklayın.",
+ "empty-dashboard": "Gösterge paneliniz boş. İlk bileşeninizi eklemek için Tasarım'a tıklayın.",
"event-log": "{event} sayfasında {url}",
"forbidden": "Yasaklandı",
"go-to-settings": "Ayarlara git",
@@ -427,6 +433,7 @@
"triggered-event": "Tetiklenen olay",
"unauthorized": "Yetkisiz",
"user-deleted": "Kullanıcı silindi.",
+ "replay-min-duration-description": "Yalnızca bu süre veya daha uzun süren yeniden oynatmaları göster.",
"viewed-page": "Görüntülenen sayfa",
"upgrade-required": "Bu özellik {plan} plan aboneliği gerektirir.",
"visitor-log": "Yeni ziyaretçi: {country}, {os}, {device}, {browser}"
diff --git a/public/intl/messages/uk-UA.json b/public/intl/messages/uk-UA.json
index 9d298a3f3..080fd9bb4 100644
--- a/public/intl/messages/uk-UA.json
+++ b/public/intl/messages/uk-UA.json
@@ -40,6 +40,7 @@
"campaign": "Кампанія",
"campaigns": "Кампанії",
"cancel": "Відмінити",
+ "close": "Закрити",
"change-password": "Змінити пароль",
"channel": "Канал",
"channels": "Канали",
@@ -102,7 +103,9 @@
"download": "Завантажити",
"dropoff": "Відсів",
"duration": "Тривалість",
+ "min-duration-seconds": "Мінімальна тривалість (секунди)",
"edit": "Редагувати",
+ "design": "Дизайн",
"edit-member": "Редагувати учасника",
"email": "Електронна пошта",
"enable-share-url": "Увімкнути спільне посилання",
@@ -124,6 +127,7 @@
"filter-combined": "Об'єднані",
"filter-raw": "Сирі дані",
"filters": "Фільтри",
+ "filters-enabled": "Фільтри увімкнено",
"first-click": "Перший клік",
"first-seen": "Перший візит",
"funnel": "Воронка",
@@ -300,6 +304,8 @@
"sum": "Сума",
"support": "Підтримка",
"switch-account": "Змінити обліковий запис",
+ "switch-to-card-view": "Перейти до перегляду карток",
+ "switch-to-table-view": "Перейти до перегляду таблиці",
"table": "Таблиця",
"tablet": "Планшет",
"tag": "Тег",
@@ -379,7 +385,7 @@
"confirm-reset": "Ви впевнені, що бажаєте скинути статистику для {target}?",
"delete-team-warning": "Видалення команди також призведе до видалення всіх її веб-сайтів.",
"delete-website-warning": "Усі пов'язані дані будуть видалені також.",
- "empty-dashboard": "Ваша панель інструментів порожня. Натисніть Редагувати, щоб додати перший компонент.",
+ "empty-dashboard": "Ваша панель інструментів порожня. Натисніть Дизайн, щоб додати перший компонент.",
"error": "Щось пішло не так.",
"event-log": "{event} на {url}",
"forbidden": "Доступ заборонено",
@@ -416,6 +422,7 @@
"triggered-event": "Подія, що спрацювала",
"unauthorized": "Не авторизовано",
"user-deleted": "Користувача видалено.",
+ "replay-min-duration-description": "Показувати лише відтворення з такою або більшою тривалістю.",
"viewed-page": "Переглянута сторінка",
"visitor-log": "Відвідувач з {country} використовуючи {browser} на {os} {device}"
}
diff --git a/public/intl/messages/ur-PK.json b/public/intl/messages/ur-PK.json
index 016af6ee0..922aa72c3 100644
--- a/public/intl/messages/ur-PK.json
+++ b/public/intl/messages/ur-PK.json
@@ -40,6 +40,7 @@
"campaign": "مہم",
"campaigns": "مہمات",
"cancel": "منسوخ",
+ "close": "بند کریں",
"change-password": "پاس ورڈ تبدیل کریں",
"channel": "چینل",
"channels": "چینلز",
@@ -102,7 +103,9 @@
"download": "ڈاؤن لوڈ",
"dropoff": "ڈراپ آف",
"duration": "مدت",
+ "min-duration-seconds": "کم از کم مدت (سیکنڈ)",
"edit": "ترمیم",
+ "design": "ڈیزائن",
"edit-member": "رکن میں ترمیم",
"email": "ای میل",
"enable-share-url": "شیئر یو آر ایل کو فعال کریں",
@@ -124,6 +127,7 @@
"filter-combined": "مشترکہ",
"filter-raw": "خام",
"filters": "فلٹرز",
+ "filters-enabled": "فلٹرز فعال",
"first-click": "پہلا کلک",
"first-seen": "پہلی بار دیکھا گیا",
"funnel": "فنل",
@@ -300,6 +304,8 @@
"sum": "کل",
"support": "معاونت",
"switch-account": "اکاؤنٹ تبدیل کریں",
+ "switch-to-card-view": "کارڈ ویو پر سوئچ کریں",
+ "switch-to-table-view": "ٹیبل ویو پر سوئچ کریں",
"table": "ٹیبل",
"tablet": "ٹیبلیٹ",
"tag": "ٹیگ",
@@ -379,7 +385,7 @@
"confirm-reset": "کیا آپ واقعی {target} کے اعدادوشمار کو دوبارہ ترتیب دینا چاہتے ہیں؟",
"delete-team-warning": "ٹیم حذف کرنے سے ٹیم کی تمام ویب سائٹس بھی حذف ہو جائیں گی۔",
"delete-website-warning": "تمام متعلقہ ڈیٹا بھی حذف کر دیا جائے گا۔",
- "empty-dashboard": "آپ کا ڈیش بورڈ خالی ہے۔ اپنا پہلا جزو شامل کرنے کے لیے ترمیم پر کلک کریں۔",
+ "empty-dashboard": "آپ کا ڈیش بورڈ خالی ہے۔ اپنا پہلا جزو شامل کرنے کے لیے ڈیزائن پر کلک کریں۔",
"error": "کچھ غلط ہو گیا.",
"event-log": "{url} پر {event}",
"forbidden": "ممنوع",
@@ -416,6 +422,7 @@
"triggered-event": "متحرک واقعہ",
"unauthorized": "غیر مجاز",
"user-deleted": "صارف حذف ہو گیا۔",
+ "replay-min-duration-description": "صرف وہ ری پلے دکھائیں جو اس مدت یا اس سے زیادہ ہوں۔",
"viewed-page": "دیکھا گیا صفحہ",
"visitor-log": "{os} {device} پر {browser} کا استعمال کرتے ہوئے {country} سے آنے والا"
}
diff --git a/public/intl/messages/uz-UZ.json b/public/intl/messages/uz-UZ.json
index 7d61b3301..8252ada05 100644
--- a/public/intl/messages/uz-UZ.json
+++ b/public/intl/messages/uz-UZ.json
@@ -39,6 +39,7 @@
"campaign": "Kampaniya",
"campaigns": "Kampaniyalar",
"cancel": "Bekor qilish",
+ "close": "Yopish",
"change-password": "Parolni oʻzgartirish",
"channel": "Kanal",
"channels": "Kanallar",
@@ -103,7 +104,9 @@
"download": "Yuklab olish",
"dropoff": "Tashlab ketish",
"duration": "Davomiylik",
+ "min-duration-seconds": "Minimal davomiylik (soniya)",
"edit": "Tahrirlash",
+ "design": "Dizayn",
"edit-member": "A'zoni tahrirlash",
"email": "Elektron pochta",
"enable-share-url": "Ulashish URL'ini yoqish",
@@ -125,6 +128,7 @@
"filter-combined": "Birlashtirilgan",
"filter-raw": "Xom",
"filters": "Filtrlar",
+ "filters-enabled": "Filtrlar yoqilgan",
"first-click": "Birinchi bosish",
"first-seen": "Birinchi koʻrilgan",
"funnel": "Voronka",
@@ -255,7 +259,7 @@
"play": "Ijro etish",
"replay": "Qayta ijro",
"replay-id": "Replay ID",
- "replay-code": "Sessiya takrorlash kodi",
+ "recorder-code": "Sessiya takrorlash kodi",
"replay-enabled": "Qayta ijro yoqilgan",
"replays": "Qayta ijrolar",
"reports": "Hisobotlar",
@@ -304,6 +308,8 @@
"sum": "Yigʻindi",
"support": "Qo'llab-quvvatlash",
"switch-account": "Hisobni almashtirish",
+ "switch-to-card-view": "Karta ko'rinishiga o'tish",
+ "switch-to-table-view": "Jadval ko'rinishiga o'tish",
"table": "Jadval",
"tablet": "Planshet",
"tag": "Yorliq",
@@ -395,7 +401,7 @@
"delete-team-warning": "Jamoani oʻchirish, shuningdek, barcha jamoa veb-saytlarini ham oʻchiradi.",
"delete-website-warning": "Barcha veb-sayt ma'lumotlari oʻchiriladi.",
"error": "Nimadir xato ketdi.",
- "empty-dashboard": "Sizning boshqaruv panelingiz bo'sh. Birinchi komponentni qo'shish uchun Tahrirlash tugmasini bosing.",
+ "empty-dashboard": "Sizning boshqaruv panelingiz bo'sh. Birinchi komponentni qo'shish uchun Dizayn tugmasini bosing.",
"event-log": "**{url}** da **{event}** hodisasi",
"forbidden": "Taqiqlangan",
"go-to-settings": "Sozlamalarga oʻtish",
@@ -432,6 +438,7 @@
"triggered-event": "Hodisa ishga tushirildi",
"unauthorized": "Ruxsatsiz",
"user-deleted": "Foydalanuvchi oʻchirildi.",
+ "replay-min-duration-description": "Faqat shu yoki undan yuqori davomiylikdagi takrorlarni ko'rsatish.",
"viewed-page": "Sahifa koʻrildi",
"upgrade-required": "Bu xususiyat {plan} tarif obunasini talab qiladi.",
"visitor-log": "{os} {device} da {browser} dan foydalanayotgan {country} dan tashrif buyuruvchi",
diff --git a/public/intl/messages/vi-VN.json b/public/intl/messages/vi-VN.json
index eec7e013b..d628de547 100644
--- a/public/intl/messages/vi-VN.json
+++ b/public/intl/messages/vi-VN.json
@@ -40,6 +40,7 @@
"campaign": "Chiến dịch",
"campaigns": "Các chiến dịch",
"cancel": "Hủy bỏ",
+ "close": "Đóng",
"change-password": "Đổi mật khẩu",
"channel": "Kênh",
"channels": "Các kênh",
@@ -102,7 +103,9 @@
"download": "Tải xuống",
"dropoff": "Tỷ lệ bỏ qua",
"duration": "Thời lượng",
+ "min-duration-seconds": "Thời lượng tối thiểu (giây)",
"edit": "Chỉnh sửa",
+ "design": "Thiết kế",
"edit-member": "Chỉnh sửa thành viên",
"email": "Email",
"enable-share-url": "Bật chia sẻ URL",
@@ -124,6 +127,7 @@
"filter-combined": "Kết hợp lọc",
"filter-raw": "Lọc thô",
"filters": "Bộ lọc",
+ "filters-enabled": "Bộ lọc đã bật",
"first-click": "Nhấp đầu tiên",
"first-seen": "Lần đầu tiên nhìn thấy",
"funnel": "Phễu",
@@ -303,6 +307,8 @@
"sum": "Tổng",
"support": "Hỗ trợ",
"switch-account": "Chuyển tài khoản",
+ "switch-to-card-view": "Chuyển sang chế độ xem thẻ",
+ "switch-to-table-view": "Chuyển sang chế độ xem bảng",
"table": "Bảng",
"tablet": "Máy tính bảng",
"tag": "Thẻ",
@@ -384,7 +390,7 @@
"confirm-reset": "Bạn có chắc chắn muốn đặt lại thống kê {target}?",
"delete-team-warning": "Việc xóa một nhóm cũng sẽ xóa tất cả các website của nhóm.",
"delete-website-warning": "Tất cả dữ liệu liên quan cũng sẽ bị xóa.",
- "empty-dashboard": "Bảng điều khiển của bạn đang trống. Nhấp Chỉnh sửa để thêm thành phần đầu tiên.",
+ "empty-dashboard": "Bảng điều khiển của bạn đang trống. Nhấp Thiết kế để thêm thành phần đầu tiên.",
"error": "Đã xảy ra lỗi.",
"event-log": "{event} trên {url}",
"forbidden": "Bị cấm",
@@ -421,6 +427,7 @@
"triggered-event": "Sự kiện được kích hoạt",
"unauthorized": "Chưa được xác thực",
"user-deleted": "Người dùng đã bị xóa.",
+ "replay-min-duration-description": "Chỉ hiển thị các lần phát lại có thời lượng bằng hoặc trên mức này.",
"viewed-page": "Đã xem trang",
"visitor-log": "Khách từ {country} đang sử dụng {browser} trên {os} {device}",
"visitors-dropped-off": "Khách truy cập đã rời đi"
diff --git a/public/intl/messages/zh-CN.json b/public/intl/messages/zh-CN.json
index e552cf158..e23822c94 100644
--- a/public/intl/messages/zh-CN.json
+++ b/public/intl/messages/zh-CN.json
@@ -40,6 +40,7 @@
"campaign": "推广活动",
"campaigns": "活动",
"cancel": "取消",
+ "close": "关闭",
"change-password": "修改密码",
"channel": "渠道",
"channels": "渠道",
@@ -102,7 +103,9 @@
"download": "下载",
"dropoff": "丢弃",
"duration": "持续时间",
+ "min-duration-seconds": "最短时长(秒)",
"edit": "编辑",
+ "design": "设计",
"edit-member": "编辑成员",
"email": "邮箱",
"enable-share-url": "启用共享链接",
@@ -124,6 +127,7 @@
"filter-combined": "合并",
"filter-raw": "原始",
"filters": "筛选",
+ "filters-enabled": "过滤器已启用",
"first-click": "首次点击",
"first-seen": "首次出现",
"funnel": "分析",
@@ -300,6 +304,8 @@
"sum": "总和",
"support": "支持",
"switch-account": "切换账户",
+ "switch-to-card-view": "切换到卡片视图",
+ "switch-to-table-view": "切换到表格视图",
"table": "表格",
"tablet": "平板",
"tag": "标签",
@@ -379,7 +385,7 @@
"confirm-reset": "您确定要重置 {target} 的数据吗?",
"delete-team-warning": "删除团队也会删除所有团队网站。",
"delete-website-warning": "所有相关数据将会被删除。",
- "empty-dashboard": "您的仪表板为空。点击编辑以添加您的第一个组件。",
+ "empty-dashboard": "您的仪表板为空。点击设计以添加您的第一个组件。",
"error": "发生错误。",
"event-log": "{url} 上的 {event}",
"forbidden": "禁止访问",
@@ -416,6 +422,7 @@
"triggered-event": "触发事件",
"unauthorized": "未授权",
"user-deleted": "用户已删除。",
+ "replay-min-duration-description": "只显示时长达到或超过此值的回放。",
"viewed-page": "已浏览页面",
"visitor-log": "来自 {country} 的访客在搭载 {os} 的 {device} 上使用 {browser} 浏览器进行访问。"
}
diff --git a/public/intl/messages/zh-TW.json b/public/intl/messages/zh-TW.json
index 1f644123c..9d0d3d12c 100644
--- a/public/intl/messages/zh-TW.json
+++ b/public/intl/messages/zh-TW.json
@@ -40,6 +40,7 @@
"campaign": "活動",
"campaigns": "活動",
"cancel": "取消",
+ "close": "關閉",
"change-password": "更改密碼",
"channel": "頻道",
"channels": "頻道",
@@ -102,7 +103,9 @@
"download": "下載",
"dropoff": "離開",
"duration": "持續時間",
+ "min-duration-seconds": "最短時長(秒)",
"edit": "編輯",
+ "design": "設計",
"edit-member": "編輯成員",
"email": "電子郵件",
"enable-share-url": "啟用分享連結",
@@ -124,6 +127,7 @@
"filter-combined": "組合",
"filter-raw": "原始",
"filters": "篩選條件",
+ "filters-enabled": "篩選器已啟用",
"first-click": "首次點擊",
"first-seen": "首次造訪",
"funnel": "漏斗分析",
@@ -300,6 +304,8 @@
"sum": "總和",
"support": "支援",
"switch-account": "切換帳號",
+ "switch-to-card-view": "切換到卡片檢視",
+ "switch-to-table-view": "切換到表格檢視",
"table": "表格",
"tablet": "平板",
"tag": "標籤",
@@ -379,7 +385,7 @@
"confirm-reset": "您確定要重設 {target} 的統計資料嗎?",
"delete-team-warning": "刪除團隊的同時也會刪除所有團隊的網站。",
"delete-website-warning": "所有網站資料都將被刪除。",
- "empty-dashboard": "您的儀表板為空。點擊編輯以新增您的第一個元件。",
+ "empty-dashboard": "您的儀表板為空。點擊設計以新增您的第一個元件。",
"error": "發生錯誤。",
"event-log": "在 {url} 上的 {event}",
"forbidden": "禁止存取",
@@ -416,6 +422,7 @@
"triggered-event": "已觸發的事件",
"unauthorized": "未經授權",
"user-deleted": "使用者已刪除。",
+ "replay-min-duration-description": "只顯示時長達到或超過此值的回放。",
"viewed-page": "已瀏覽的網頁",
"visitor-log": "來自 {country} 的訪客在 {device} 上的 {os} 使用 {browser} 瀏覽。"
}
diff --git a/rollup.recorder.config.js b/rollup.recorder.config.js
index c1dd305e7..2f6607985 100644
--- a/rollup.recorder.config.js
+++ b/rollup.recorder.config.js
@@ -15,7 +15,6 @@ export default {
commonjs(),
replace({
__COLLECT_API_HOST__: process.env.COLLECT_API_HOST || '',
- __COLLECT_REPLAY_ENDPOINT__: process.env.COLLECT_REPLAY_ENDPOINT || '/api/record',
delimiters: ['', ''],
preventAssignment: true,
}),
diff --git a/rollup.tracker.config.js b/rollup.tracker.config.js
index 202db36c9..8d8d277cf 100644
--- a/rollup.tracker.config.js
+++ b/rollup.tracker.config.js
@@ -1,14 +1,16 @@
import 'dotenv/config';
import replace from '@rollup/plugin-replace';
import terser from '@rollup/plugin-terser';
+import typescript from '@rollup/plugin-typescript';
export default {
- input: 'src/tracker/index.js',
+ input: 'src/tracker/index.ts',
output: {
file: 'public/script.js',
format: 'iife',
},
plugins: [
+ typescript({ tsconfig: './tsconfig.tracker.json' }),
replace({
__COLLECT_API_HOST__: process.env.COLLECT_API_HOST || '',
__COLLECT_API_ENDPOINT__: process.env.COLLECT_API_ENDPOINT || '/api/send',
diff --git a/scripts/check-db.js b/scripts/check-db.js
index 8c806cd8f..434a52b66 100644
--- a/scripts/check-db.js
+++ b/scripts/check-db.js
@@ -3,10 +3,10 @@ import 'dotenv/config';
import { execSync } from 'node:child_process';
import { PrismaPg } from '@prisma/adapter-pg';
import chalk from 'chalk';
-import semver from 'semver';
import { PrismaClient } from '../generated/prisma/client.js';
const MIN_VERSION = '9.4.0';
+const MIN_VERSION_NUM = 90400;
if (process.env.SKIP_DB_CHECK) {
console.log('Skipping database check.');
@@ -53,10 +53,14 @@ async function checkConnection() {
}
async function checkDatabaseVersion() {
- const query = await prisma.$queryRaw`select version() as version`;
- const version = semver.valid(semver.coerce(query[0].version));
+ const query = await prisma.$queryRaw`select current_setting('server_version_num') as version_num`;
+ const version = Number(query[0]?.version_num);
- if (semver.lt(version, MIN_VERSION)) {
+ if (!Number.isFinite(version)) {
+ throw new Error('Unable to determine database version.');
+ }
+
+ if (version < MIN_VERSION_NUM) {
throw new Error(
`Database version is not compatible. Please upgrade to ${MIN_VERSION} or greater.`,
);
@@ -67,7 +71,12 @@ async function checkDatabaseVersion() {
async function applyMigration() {
if (!process.env.SKIP_DB_MIGRATION) {
- console.log(execSync('prisma migrate deploy').toString());
+ const directUrl = process.env.DIRECT_DATABASE_URL || process.env.DATABASE_URL;
+ console.log(
+ execSync('prisma migrate deploy', {
+ env: { ...process.env, DATABASE_URL: directUrl },
+ }).toString(),
+ );
success('Database is up to date.');
}
diff --git a/scripts/download-country-names.js b/scripts/download-country-names.js
index 474869d44..71cf58691 100644
--- a/scripts/download-country-names.js
+++ b/scripts/download-country-names.js
@@ -1,9 +1,9 @@
/* eslint-disable no-console */
+import fs from 'node:fs';
import https from 'node:https';
import path from 'node:path';
import chalk from 'chalk';
-import fs from 'node:fs';
const src = path.resolve(process.cwd(), 'public/intl/messages');
const dest = path.resolve(process.cwd(), 'public/intl/country');
diff --git a/scripts/download-language-names.js b/scripts/download-language-names.js
index 17d7aa72e..b10285674 100644
--- a/scripts/download-language-names.js
+++ b/scripts/download-language-names.js
@@ -1,9 +1,9 @@
/* eslint-disable no-console */
+import fs from 'node:fs';
import https from 'node:https';
import path from 'node:path';
import chalk from 'chalk';
-import fs from 'node:fs';
const src = path.resolve(process.cwd(), 'public/intl/messages');
const dest = path.resolve(process.cwd(), 'public/intl/language');
diff --git a/scripts/start-docker.sh b/scripts/start-docker.sh
new file mode 100755
index 000000000..2ad0c3c2e
--- /dev/null
+++ b/scripts/start-docker.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+# Container startup: run the one-shot DB check/migration and tracker update, then
+# exec the long-running server so it replaces this shell as PID 1.
+#
+# This avoids `npm-run-all` keeping idle `npm`/`npm-run-all` parent processes
+# resident for the lifetime of the container (each a full Node VM holding tens
+# of MiB of anonymous memory). `exec` leaves a single `node server.js` process.
+#
+# PATH is extended so check-db.js's `prisma migrate deploy` resolves the prisma
+# CLI from node_modules/.bin the way `npm run` would have injected it.
+set -e
+export PATH="$PWD/node_modules/.bin:$PATH"
+
+node scripts/check-db.js
+node scripts/update-tracker.js
+exec node server.js
diff --git a/scripts/start-env.js b/scripts/start-env.js
index 970414ae3..223f8d1f2 100644
--- a/scripts/start-env.js
+++ b/scripts/start-env.js
@@ -1,5 +1,5 @@
import 'dotenv/config';
-import cli from 'next/dist/cli/next-start';
+import cli from 'next/dist/cli/next-start.js';
cli.nextStart({
port: process.env.PORT || 3000,
diff --git a/src/app/(collect)/p/[slug]/route.ts b/src/app/(collect)/p/[slug]/route.ts
index fd18a2834..d7fc24167 100644
--- a/src/app/(collect)/p/[slug]/route.ts
+++ b/src/app/(collect)/p/[slug]/route.ts
@@ -21,6 +21,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
return findPixel({
where: {
slug,
+ deletedAt: null,
},
});
},
@@ -34,6 +35,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
pixel = await findPixel({
where: {
slug,
+ deletedAt: null,
},
});
@@ -47,7 +49,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
payload: {
pixel: pixel.id,
url: request.url,
- referrer: request.headers.get("referer") || undefined,
+ referrer: request.headers.get('referer') || undefined,
},
};
diff --git a/src/app/(collect)/q/[slug]/route.ts b/src/app/(collect)/q/[slug]/route.ts
index aa9c26f3f..5d4229e90 100644
--- a/src/app/(collect)/q/[slug]/route.ts
+++ b/src/app/(collect)/q/[slug]/route.ts
@@ -19,6 +19,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
return findLink({
where: {
slug,
+ deletedAt: null,
},
});
},
@@ -32,6 +33,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
link = await findLink({
where: {
slug,
+ deletedAt: null,
},
});
@@ -45,7 +47,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
payload: {
link: link.id,
url: request.url,
- referrer: request.headers.get("referer") || undefined,
+ referrer: request.headers.get('referer') || undefined,
},
};
diff --git a/src/app/(main)/MobileNav.tsx b/src/app/(main)/MobileNav.tsx
index ff54eb75f..1ef458638 100644
--- a/src/app/(main)/MobileNav.tsx
+++ b/src/app/(main)/MobileNav.tsx
@@ -1,7 +1,7 @@
import { Column, Grid, Row, Text } from '@umami/react-zen';
-import Link from '@/components/common/Link';
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
import { IconLabel } from '@/components/common/IconLabel';
+import Link from '@/components/common/Link';
import { useMessages, useNavigation } from '@/components/hooks';
import { Globe, Grid2x2, LayoutDashboard, LinkIcon } from '@/components/icons';
import { MobileMenuButton } from '@/components/input/MobileMenuButton';
diff --git a/src/app/(main)/SideNav.tsx b/src/app/(main)/SideNav.tsx
index 2f2c27ec4..cac1fa4cf 100644
--- a/src/app/(main)/SideNav.tsx
+++ b/src/app/(main)/SideNav.tsx
@@ -1,18 +1,3 @@
-import { AdminNav } from '@/app/(main)/admin/AdminNav';
-import { SettingsNav } from '@/app/(main)/settings/SettingsNav';
-import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
-import { IconLabel } from '@/components/common/IconLabel';
-import { useGlobalState, useMessages, useNavigation } from '@/components/hooks';
-import {
- Globe,
- Grid2x2,
- LayoutDashboard,
- LinkIcon,
- PanelLeft,
- PanelsLeftBottom,
-} from '@/components/icons';
-import { UserButton } from '@/components/input/UserButton';
-import { Logo } from '@/components/svg';
import {
Button,
type ButtonProps,
@@ -24,7 +9,22 @@ import {
Tooltip,
TooltipTrigger,
} from '@umami/react-zen';
+import { AdminNav } from '@/app/(main)/admin/AdminNav';
+import { SettingsNav } from '@/app/(main)/settings/SettingsNav';
+import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
+import { IconLabel } from '@/components/common/IconLabel';
import Link from '@/components/common/Link';
+import { useGlobalState, useMessages, useNavigation } from '@/components/hooks';
+import {
+ Globe,
+ Grid2x2,
+ LayoutDashboard,
+ LinkIcon,
+ PanelLeft,
+ PanelsLeftBottom,
+} from '@/components/icons';
+import { UserButton } from '@/components/input/UserButton';
+import { Logo } from '@/components/svg';
export function SideNav(props: any) {
const { t, labels } = useMessages();
@@ -74,7 +74,7 @@ export function SideNav(props: any) {
backgroundColor="surface-base"
border
borderRadius
- paddingX="2"
+ padding="2"
flexGrow="1"
minHeight="0"
margin="2"
@@ -87,10 +87,10 @@ export function SideNav(props: any) {
-
+
{!isCollapsed && (
}>
umami
@@ -137,7 +137,7 @@ export function SideNav(props: any) {
)}
-
+
diff --git a/src/app/(main)/admin/AdminNav.tsx b/src/app/(main)/admin/AdminNav.tsx
index 511f9c8b1..4f55a3a0f 100644
--- a/src/app/(main)/admin/AdminNav.tsx
+++ b/src/app/(main)/admin/AdminNav.tsx
@@ -1,4 +1,6 @@
+import { Column, Focusable, Row, Tooltip, TooltipTrigger } from '@umami/react-zen';
import { IconLabel } from '@/components/common/IconLabel';
+import Link from '@/components/common/Link';
import { NavMenu } from '@/components/common/NavMenu';
import { useMessages, useNavigation } from '@/components/hooks';
import { ArrowLeft, Globe, ShieldCheck, User, Users } from '@/components/icons';
diff --git a/src/app/(main)/admin/teams/AdminTeamsTable.tsx b/src/app/(main)/admin/teams/AdminTeamsTable.tsx
index 8213b6d04..24002f2d9 100644
--- a/src/app/(main)/admin/teams/AdminTeamsTable.tsx
+++ b/src/app/(main)/admin/teams/AdminTeamsTable.tsx
@@ -1,7 +1,8 @@
import { DataColumn, DataTable, Dialog, Icon, MenuItem, Modal, Row, Text } from '@umami/react-zen';
-import Link from '@/components/common/Link';
import { useState } from 'react';
import { DateDistance } from '@/components/common/DateDistance';
+import Link from '@/components/common/Link';
+import { SortableLabel } from '@/components/common/SortableLabel';
import { useMessages } from '@/components/hooks';
import { Edit, Trash } from '@/components/icons';
import { MenuButton } from '@/components/input/MenuButton';
@@ -21,7 +22,11 @@ export function AdminTeamsTable({
return (
<>
-
+ }
+ width="1fr"
+ >
{(row: any) => {row.name}}
@@ -41,7 +46,13 @@ export function AdminTeamsTable({
);
}}
-
+
+ }
+ width="160px"
+ >
{(row: any) => }
{showActions && (
diff --git a/src/app/(main)/admin/users/UsersTable.tsx b/src/app/(main)/admin/users/UsersTable.tsx
index fafe0e139..55d46b7c9 100644
--- a/src/app/(main)/admin/users/UsersTable.tsx
+++ b/src/app/(main)/admin/users/UsersTable.tsx
@@ -1,7 +1,8 @@
import { DataColumn, DataTable, Icon, MenuItem, Modal, Row, Text } from '@umami/react-zen';
-import Link from '@/components/common/Link';
import { useState } from 'react';
import { DateDistance } from '@/components/common/DateDistance';
+import Link from '@/components/common/Link';
+import { SortableLabel } from '@/components/common/SortableLabel';
import { useMessages } from '@/components/hooks';
import { Edit, Trash } from '@/components/icons';
import { MenuButton } from '@/components/input/MenuButton';
@@ -22,10 +23,14 @@ export function UsersTable({
return (
<>
-
+ }
+ width="2fr"
+ >
{(row: any) => {row.username}}
-
+ }>
{(row: any) =>
t(labels[Object.keys(ROLES).find(key => ROLES[key] === row.role)] || labels.unknown)
}
@@ -33,7 +38,12 @@ export function UsersTable({
{(row: any) => row._count.websites}
-
+
+ }
+ >
{(row: any) => }
{showActions && (
diff --git a/src/app/(main)/admin/websites/AdminWebsitesTable.tsx b/src/app/(main)/admin/websites/AdminWebsitesTable.tsx
index 751dc7583..f33b4c35d 100644
--- a/src/app/(main)/admin/websites/AdminWebsitesTable.tsx
+++ b/src/app/(main)/admin/websites/AdminWebsitesTable.tsx
@@ -1,11 +1,13 @@
import { DataColumn, DataTable, Dialog, Icon, MenuItem, Modal, Row, Text } from '@umami/react-zen';
-import Link from '@/components/common/Link';
import { useState } from 'react';
import { WebsiteDeleteForm } from '@/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm';
import { DateDistance } from '@/components/common/DateDistance';
+import Link from '@/components/common/Link';
+import { SortableLabel } from '@/components/common/SortableLabel';
import { useMessages } from '@/components/hooks';
import { Edit, Trash, Users } from '@/components/icons';
import { MenuButton } from '@/components/input/MenuButton';
+import { decodePunycodeDomain } from '@/lib/format';
export function AdminWebsitesTable({ data = [], ...props }: { data: any[] }) {
const { t, labels } = useMessages();
@@ -14,15 +16,15 @@ export function AdminWebsitesTable({ data = [], ...props }: { data: any[] }) {
return (
<>
-
+ }>
{(row: any) => (
{row.name}
)}
-
- {(row: any) => {row.domain}}
+ }>
+ {(row: any) => {decodePunycodeDomain(row.domain)}}
{(row: any) => {
@@ -45,7 +47,13 @@ export function AdminWebsitesTable({ data = [], ...props }: { data: any[] }) {
);
}}
-
+
+ }
+ width="180px"
+ >
{(row: any) => }
diff --git a/src/app/(main)/boards/BoardCloneButton.tsx b/src/app/(main)/boards/BoardCloneButton.tsx
new file mode 100644
index 000000000..3f6c7732b
--- /dev/null
+++ b/src/app/(main)/boards/BoardCloneButton.tsx
@@ -0,0 +1,32 @@
+import { Copy } from '@/components/icons';
+import { useNavigation } from '@/components/hooks';
+import { DialogButton } from '@/components/input/DialogButton';
+import type { Board } from '@/lib/types';
+import { BoardCloneForm } from './BoardCloneForm';
+
+export function BoardCloneButton({
+ boardId,
+ showLabel = false,
+}: {
+ boardId: string;
+ showLabel?: boolean;
+}) {
+ const { router, renderUrl } = useNavigation();
+
+ const handleSave = (board: Board) => {
+ router.push(renderUrl(`/boards/${board.id}/design`, false));
+ };
+
+ return (
+ }
+ label={showLabel ? 'Clone' : undefined}
+ title="Clone board"
+ aria-label="Clone"
+ variant={showLabel ? undefined : 'quiet'}
+ width="600px"
+ >
+ {({ close }) => }
+
+ );
+}
diff --git a/src/app/(main)/boards/BoardCloneForm.tsx b/src/app/(main)/boards/BoardCloneForm.tsx
new file mode 100644
index 000000000..ec35f90cb
--- /dev/null
+++ b/src/app/(main)/boards/BoardCloneForm.tsx
@@ -0,0 +1,164 @@
+import {
+ Box,
+ Button,
+ Form,
+ FormField,
+ FormSubmitButton,
+ Loading,
+ Row,
+ Text,
+ TextField,
+} from '@umami/react-zen';
+import { useBoardQuery, useMessages, useUpdateQuery } from '@/components/hooks';
+import { LinkSelect } from '@/components/input/LinkSelect';
+import { PixelSelect } from '@/components/input/PixelSelect';
+import { WebsiteSelect } from '@/components/input/WebsiteSelect';
+import {
+ BOARD_TYPES,
+ type BoardType,
+ getBoardEntity,
+ getBoardType,
+ requiresBoardEntity,
+ setBoardEntity,
+} from '@/lib/boards';
+import type { Board } from '@/lib/types';
+
+interface BoardCloneFormValues {
+ name: string;
+ description: string;
+ type: BoardType;
+ entityId: string;
+}
+
+function getDefaultValues(board?: Partial): BoardCloneFormValues {
+ const boardType = getBoardType(board, { coerceDashboard: true });
+ const { entityId } = getBoardEntity(board);
+
+ return {
+ name: board?.name ? `${board.name} copy` : '',
+ description: board?.description ?? '',
+ type: boardType,
+ entityId: entityId ?? '',
+ };
+}
+
+export function BoardCloneForm({
+ boardId,
+ onSave,
+ onClose,
+}: {
+ boardId: string;
+ onSave?: (board: Board) => void | Promise;
+ onClose?: () => void;
+}) {
+ const { t, labels, messages, getErrorMessage } = useMessages();
+ const { data: board, isLoading } = useBoardQuery(boardId);
+ const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(`/boards/${boardId}/clone`);
+ const values = getDefaultValues(board);
+ const boardType = getBoardType(board, { coerceDashboard: true });
+ const resolvedTeamId = board?.teamId;
+
+ const handleSubmit = async (data: BoardCloneFormValues) => {
+ const result = await mutateAsync({
+ name: data.name,
+ description: data.description,
+ parameters: setBoardEntity(board?.parameters, boardType, data.entityId || undefined),
+ });
+
+ toast(t(messages.saved));
+ touch('boards');
+ touch(`board:${result.id}`);
+ await onSave?.(result);
+ onClose?.();
+ };
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/boards/BoardDesignButton.tsx b/src/app/(main)/boards/BoardDesignButton.tsx
index 20a06f62c..2c70647e0 100644
--- a/src/app/(main)/boards/BoardDesignButton.tsx
+++ b/src/app/(main)/boards/BoardDesignButton.tsx
@@ -1,13 +1,18 @@
import { Icon } from '@umami/react-zen';
import { LinkButton } from '@/components/common/LinkButton';
-import { useNavigation } from '@/components/hooks';
+import { useMessages, useNavigation } from '@/components/hooks';
import { LayoutDashboard } from '@/components/icons';
export function BoardDesignButton({ boardId }: { boardId: string }) {
+ const { t, labels } = useMessages();
const { renderUrl } = useNavigation();
return (
-
+
diff --git a/src/app/(main)/boards/BoardEditForm.tsx b/src/app/(main)/boards/BoardEditForm.tsx
index ec405ff35..0f5ded174 100644
--- a/src/app/(main)/boards/BoardEditForm.tsx
+++ b/src/app/(main)/boards/BoardEditForm.tsx
@@ -8,6 +8,7 @@ import {
Loading,
Row,
Select,
+ Text,
TextField,
} from '@umami/react-zen';
import { useBoardQuery, useMessages, useNavigation, useUpdateQuery } from '@/components/hooks';
@@ -97,6 +98,14 @@ export function BoardEditForm({
: type === BOARD_TYPES.link
? t(labels.link)
: t(labels.website);
+ const boardTypeLabel =
+ type === BOARD_TYPES.mixed
+ ? t(labels.open)
+ : type === BOARD_TYPES.pixel
+ ? t(labels.pixel)
+ : type === BOARD_TYPES.link
+ ? t(labels.link)
+ : t(labels.website);
const handleTypeChange = (value: string) => {
setValue('type', value as BoardType, { shouldDirty: true });
@@ -126,12 +135,16 @@ export function BoardEditForm({
rules={{ required: t(labels.required) }}
>
-
+ {boardId ? (
+ {boardTypeLabel}
+ ) : (
+
+ )}
{requiresBoardEntity(type) && (
diff --git a/src/app/(main)/boards/BoardEntityBadge.tsx b/src/app/(main)/boards/BoardEntityBadge.tsx
index 31d7081ab..70d4784f8 100644
--- a/src/app/(main)/boards/BoardEntityBadge.tsx
+++ b/src/app/(main)/boards/BoardEntityBadge.tsx
@@ -15,13 +15,7 @@ export function BoardEntityBadge({
return (
- {type === 'pixel' ? (
-
- ) : type === 'link' ? (
-
- ) : (
-
- )}
+ {type === 'pixel' ? : type === 'link' ? : }
{name}
diff --git a/src/app/(main)/boards/BoardProvider.tsx b/src/app/(main)/boards/BoardProvider.tsx
index dc5168424..4ec649520 100644
--- a/src/app/(main)/boards/BoardProvider.tsx
+++ b/src/app/(main)/boards/BoardProvider.tsx
@@ -3,8 +3,8 @@ import { Loading, useToast } from '@umami/react-zen';
import { createContext, type ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { v4 as uuid } from 'uuid';
import { useApi, useMessages, useModified, useNavigation } from '@/components/hooks';
-import { BOARD_TYPES, getBoardType } from '@/lib/boards';
import { useBoardQuery } from '@/components/hooks/queries/useBoardQuery';
+import { BOARD_TYPES, getBoardType } from '@/lib/boards';
import type { Board, BoardParameters } from '@/lib/types';
import { getComponentDefinition } from './boardComponentRegistry';
diff --git a/src/app/(main)/boards/BoardsTable.tsx b/src/app/(main)/boards/BoardsTable.tsx
index b21cbee00..346e92497 100644
--- a/src/app/(main)/boards/BoardsTable.tsx
+++ b/src/app/(main)/boards/BoardsTable.tsx
@@ -1,6 +1,7 @@
import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
-import Link from '@/components/common/Link';
import { DateDistance } from '@/components/common/DateDistance';
+import Link from '@/components/common/Link';
+import { SortableLabel } from '@/components/common/SortableLabel';
import { useMessages, useNavigation } from '@/components/hooks';
import { BoardDeleteButton } from './BoardDeleteButton';
import { BoardDesignButton } from './BoardDesignButton';
@@ -12,16 +13,25 @@ export function BoardsTable(props: DataTableProps) {
return (
-
+ }>
{({ id, name }: any) => {
return {name};
}}
-
-
- {({ type }: any) => type ? type.charAt(0).toUpperCase() + type.slice(1) : ''}
+ }
+ />
+ }>
+ {({ type }: any) => (type ? type.charAt(0).toUpperCase() + type.slice(1) : '')}
-
+
+ }
+ width="200px"
+ >
{(row: any) => }
diff --git a/src/app/(main)/boards/[boardId]/BoardComponentRenderer.tsx b/src/app/(main)/boards/[boardId]/BoardComponentRenderer.tsx
index ec2a30df1..242a33436 100644
--- a/src/app/(main)/boards/[boardId]/BoardComponentRenderer.tsx
+++ b/src/app/(main)/boards/[boardId]/BoardComponentRenderer.tsx
@@ -7,10 +7,12 @@ function BoardComponentRendererComponent({
config,
websiteId,
entityType,
+ isPreview,
}: {
config: BoardComponentConfig;
websiteId?: string;
entityType?: string;
+ isPreview?: boolean;
}) {
const definition = getComponentDefinition(config.type);
@@ -33,7 +35,13 @@ function BoardComponentRendererComponent({
);
}
- return ;
+ return (
+
+ );
}
export const BoardComponentRenderer = memo(
@@ -41,6 +49,7 @@ export const BoardComponentRenderer = memo(
(prevProps, nextProps) =>
prevProps.websiteId === nextProps.websiteId &&
prevProps.entityType === nextProps.entityType &&
+ prevProps.isPreview === nextProps.isPreview &&
prevProps.config === nextProps.config,
);
diff --git a/src/app/(main)/boards/[boardId]/BoardComponentSelect.tsx b/src/app/(main)/boards/[boardId]/BoardComponentSelect.tsx
index 3a03ffea0..b87ebbac8 100644
--- a/src/app/(main)/boards/[boardId]/BoardComponentSelect.tsx
+++ b/src/app/(main)/boards/[boardId]/BoardComponentSelect.tsx
@@ -9,11 +9,10 @@ import {
TextField,
} from '@umami/react-zen';
import { useEffect, useMemo, useState } from 'react';
-import { useMessages } from '@/components/hooks';
+import { useApi, useMessages } from '@/components/hooks';
import { LinkSelect } from '@/components/input/LinkSelect';
import { PixelSelect } from '@/components/input/PixelSelect';
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
-import type { BoardComponentConfig } from '@/lib/types';
import {
BOARD_ENTITY_TYPES,
type BoardEntityType,
@@ -22,6 +21,7 @@ import {
isBoardComponentSupported,
isOpenBoardType,
} from '@/lib/boards';
+import type { BoardComponentConfig } from '@/lib/types';
import {
type ComponentDefinition,
type ConfigField,
@@ -29,6 +29,16 @@ import {
} from '../boardComponentRegistry';
import { BoardComponentRenderer } from './BoardComponentRenderer';
+const COMPONENT_GROUP_ORDER: string[] = [
+ 'Traffic',
+ 'Events',
+ 'Behavior',
+ 'Realtime',
+ 'Growth',
+ 'Revenue',
+ 'Content',
+];
+
export function BoardComponentSelect({
teamId,
boardType,
@@ -47,28 +57,28 @@ export function BoardComponentSelect({
onClose: () => void;
}) {
const { t, labels, messages } = useMessages();
+ const { get, useQuery } = useApi();
const initialEntity = getComponentEntity(initialConfig);
const [selectedDef, setSelectedDef] = useState(null);
const [configValues, setConfigValues] = useState>({});
const [selectedEntityType, setSelectedEntityType] = useState(
initialEntity.entityType || boardEntityType || BOARD_ENTITY_TYPES.website,
);
- const [selectedEntityId, setSelectedEntityId] = useState(
- initialEntity.entityId || boardEntityId,
- );
+ const [selectedEntityId, setSelectedEntityId] = useState(initialEntity.entityId || boardEntityId);
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
- const allDefinitions = useMemo(
- () => getComponentDefinitions().toSorted((a, b) => a.name.localeCompare(b.name)),
- [],
- );
+ const allDefinitions = useMemo(() => getComponentDefinitions(), []);
const activeEntityType = isOpenBoardType(boardType) ? selectedEntityType : boardEntityType;
const isSelectedDefSupported = selectedDef
? isBoardComponentSupported(selectedDef.type, activeEntityType)
: false;
- const getDefaultConfigValues = (def: ComponentDefinition, config?: BoardComponentConfig) => {
+ const getDefaultConfigValues = (
+ def: ComponentDefinition,
+ config?: BoardComponentConfig,
+ entityType?: BoardEntityType,
+ ) => {
const defaults: Record = {};
for (const field of def.configFields ?? []) {
@@ -79,6 +89,19 @@ export function BoardComponentSelect({
Object.assign(defaults, def.defaultProps);
}
+ for (const field of def.configFields ?? []) {
+ if (field.type !== 'select') {
+ continue;
+ }
+
+ const options = (entityType && field.optionsByEntityType?.[entityType]) ?? field.options;
+ const currentValue = defaults[field.name];
+
+ if (options?.length && !options.some(option => String(option.value) === String(currentValue))) {
+ defaults[field.name] = options[0].value;
+ }
+ }
+
if (config?.props) {
Object.assign(defaults, config.props);
}
@@ -98,10 +121,9 @@ export function BoardComponentSelect({
}
setSelectedDef(definition);
- setConfigValues(getDefaultConfigValues(definition, initialConfig));
- setSelectedEntityType(
- initialEntity.entityType || boardEntityType || BOARD_ENTITY_TYPES.website,
- );
+ const entityType = initialEntity.entityType || boardEntityType || BOARD_ENTITY_TYPES.website;
+ setConfigValues(getDefaultConfigValues(definition, initialConfig, entityType));
+ setSelectedEntityType(entityType);
setSelectedEntityId(initialEntity.entityId || boardEntityId);
setTitle(initialConfig.title ?? definition.name);
setDescription(initialConfig.description || '');
@@ -116,7 +138,7 @@ export function BoardComponentSelect({
const handleSelectComponent = (def: ComponentDefinition) => {
setSelectedDef(def);
- setConfigValues(getDefaultConfigValues(def));
+ setConfigValues(getDefaultConfigValues(def, undefined, activeEntityType));
setTitle(def.name);
setDescription('');
};
@@ -138,10 +160,55 @@ export function BoardComponentSelect({
? selectedEntityId
: boardEntityId
: undefined;
+ const reportFields = selectedDef?.configFields?.filter(
+ field => field.type === 'report' && field.reportType,
+ );
+ const { data: reportOptionsData, isLoading: isLoadingReportOptions } = useQuery({
+ queryKey: [
+ 'board-component-report-options',
+ {
+ websiteId: resolvedEntityId,
+ reportTypes: reportFields?.map(field => field.reportType).join(','),
+ },
+ ],
+ queryFn: async () => {
+ const types = [...new Set(reportFields?.map(field => field.reportType).filter(Boolean))];
+
+ const entries = await Promise.all(
+ types.map(async type => {
+ const response = await get('/reports', {
+ websiteId: resolvedEntityId,
+ type,
+ pageSize: 1000,
+ });
+
+ return [type, response.data] as const;
+ }),
+ );
+
+ return Object.fromEntries(entries);
+ },
+ enabled: !!resolvedEntityId && !!reportFields?.length,
+ });
+ const reportOptionsByType = reportOptionsData ?? {};
+ const hasRequiredConfig = selectedDef
+ ? (selectedDef.configFields ?? []).every(field => {
+ if (!field.required) {
+ return true;
+ }
+
+ const value = configValues[field.name];
+ return value !== undefined && value !== null && value !== '';
+ })
+ : false;
const handleEntityTypeChange = (value: string) => {
setSelectedEntityType(value as BoardEntityType);
setSelectedEntityId(undefined);
+ setSelectedDef(null);
+ setConfigValues({});
+ setTitle('');
+ setDescription('');
};
const handleAdd = () => {
@@ -188,7 +255,11 @@ export function BoardComponentSelect({
[selectedDef, configValues],
);
- const canSave = !!selectedDef && isSelectedDefSupported && (!needsWebsite || !!resolvedEntityId);
+ const canSave =
+ !!selectedDef &&
+ isSelectedDefSupported &&
+ hasRequiredConfig &&
+ (!needsWebsite || !!resolvedEntityId);
const availableDefinitions = useMemo(
() =>
allDefinitions.filter(
@@ -197,6 +268,33 @@ export function BoardComponentSelect({
),
[activeEntityType, allDefinitions, selectedDef?.type],
);
+ const groupedDefinitions = useMemo(() => {
+ const order = new Map(COMPONENT_GROUP_ORDER.map((group, index) => [group, index]));
+ const groups = new Map();
+
+ for (const def of availableDefinitions) {
+ const group = def.group ?? 'Other';
+ const items = groups.get(group) ?? [];
+ items.push(def);
+ groups.set(group, items);
+ }
+
+ return Array.from(groups.entries())
+ .sort(([a], [b]) => {
+ const left = order.get(a) ?? Number.MAX_SAFE_INTEGER;
+ const right = order.get(b) ?? Number.MAX_SAFE_INTEGER;
+
+ if (left !== right) {
+ return left - right;
+ }
+
+ return a.localeCompare(b);
+ })
+ .map(([group, definitions]) => ({
+ group,
+ definitions,
+ }));
+ }, [availableDefinitions]);
return (
@@ -312,6 +410,35 @@ export function BoardComponentSelect({
style={{ minHeight: 200 }}
/>
)}
+
+ {field.type === 'report' && (
+ <>
+
+ {isLoadingReportOptions && (
+
+ Loading options...
+
+ )}
+ {!isLoadingReportOptions &&
+ resolvedEntityId &&
+ (reportOptionsByType[field.reportType ?? '']?.length ?? 0) === 0 && (
+
+ No saved items found.
+
+ )}
+ >
+ )}
))}
@@ -323,40 +450,47 @@ export function BoardComponentSelect({
{hasSelectedEntity ? (
- {availableDefinitions.map(def => {
- const Icon = def.icon;
+ {groupedDefinitions.map(({ group, definitions }) => (
+
+
+ {group}
+
+ {definitions.map(def => {
+ const Icon = def.icon;
- return (
-
- handleSelectComponent(def)}
- >
-
-
-
+ handleSelectComponent(def)}
>
- {def.name}
-
-
- {def.description}
-
-
-
-
- );
- })}
+
+
+
+ {def.name}
+
+
+ {def.description}
+
+
+
+
+ );
+ })}
+
+ ))}
) : (
@@ -370,7 +504,12 @@ export function BoardComponentSelect({
Preview
{hasSelectedEntity && previewConfig && (!needsWebsite || resolvedEntityId) ? (
-
+
) : (
diff --git a/src/app/(main)/boards/[boardId]/BoardEditColumn.tsx b/src/app/(main)/boards/[boardId]/BoardEditColumn.tsx
index 0a89ea25c..cdc053a43 100644
--- a/src/app/(main)/boards/[boardId]/BoardEditColumn.tsx
+++ b/src/app/(main)/boards/[boardId]/BoardEditColumn.tsx
@@ -1,8 +1,3 @@
-import { Panel } from '@/components/common/Panel';
-import { useBoard, useMessages, useNavigation } from '@/components/hooks';
-import { Pencil, Plus, X } from '@/components/icons';
-import { getBoardEntity, getBoardType, getResolvedComponentEntity } from '@/lib/boards';
-import type { BoardComponentConfig } from '@/lib/types';
import {
Box,
Button,
@@ -15,6 +10,11 @@ import {
TooltipTrigger,
} from '@umami/react-zen';
import { useMemo, useState } from 'react';
+import { Panel } from '@/components/common/Panel';
+import { useBoard, useMessages, useNavigation } from '@/components/hooks';
+import { Pencil, Plus, X } from '@/components/icons';
+import { getBoardEntity, getBoardType, getResolvedComponentEntity } from '@/lib/boards';
+import type { BoardComponentConfig } from '@/lib/types';
import { getComponentDefinition } from '../boardComponentRegistry';
import { BoardComponentRenderer } from './BoardComponentRenderer';
import { BoardComponentSelect } from './BoardComponentSelect';
@@ -48,7 +48,9 @@ export function BoardEditColumn({
return null;
}
- return ;
+ return (
+
+ );
}, [component, definition?.requiresWebsite, entityId, entityType]);
const handleSelect = (config: BoardComponentConfig) => {
@@ -95,9 +97,7 @@ export function BoardEditColumn({
>
{t(hasComponent ? labels.edit : labels.selectComponent)}
diff --git a/src/app/(main)/boards/[boardId]/BoardShareButton.tsx b/src/app/(main)/boards/[boardId]/BoardShareButton.tsx
index 77d6cc609..86c8d05c7 100644
--- a/src/app/(main)/boards/[boardId]/BoardShareButton.tsx
+++ b/src/app/(main)/boards/[boardId]/BoardShareButton.tsx
@@ -1,5 +1,5 @@
-import { Share } from '@/components/icons';
import { useMessages } from '@/components/hooks';
+import { Share } from '@/components/icons';
import { DialogButton } from '@/components/input/DialogButton';
import { BoardShareDialog } from './BoardShareDialog';
@@ -7,12 +7,7 @@ export function BoardShareButton({ boardId }: { boardId: string }) {
const { t, labels } = useMessages();
return (
- }
- label={t(labels.share)}
- title={null}
- width="900px"
- >
+ } label={t(labels.share)} title={null} width="900px">
);
diff --git a/src/app/(main)/boards/[boardId]/BoardShareCreateForm.tsx b/src/app/(main)/boards/[boardId]/BoardShareCreateForm.tsx
index 1842edcc9..3598bfe2f 100644
--- a/src/app/(main)/boards/[boardId]/BoardShareCreateForm.tsx
+++ b/src/app/(main)/boards/[boardId]/BoardShareCreateForm.tsx
@@ -1,6 +1,16 @@
-import { Button, Column, Form, FormField, FormSubmitButton, Row, TextField } from '@umami/react-zen';
+import {
+ Button,
+ Checkbox,
+ Column,
+ Form,
+ FormField,
+ FormSubmitButton,
+ Row,
+ TextField,
+} from '@umami/react-zen';
import { useState } from 'react';
import { useApi, useMessages, useModified } from '@/components/hooks';
+import { ThemeModeSelector } from '@/components/input/ThemeModeSelector';
export function BoardShareCreateForm({
boardId,
@@ -15,15 +25,19 @@ export function BoardShareCreateForm({
const { touch } = useModified();
const { t, labels, getErrorMessage } = useMessages();
const [isPending, setIsPending] = useState(false);
- const [error, setError] = useState(null);
+ const [error, setError] = useState(null);
- const handleSubmit = async (data: { name: string }) => {
+ const handleSubmit = async (data: { name: string; allowFilter?: boolean; theme?: string }) => {
setIsPending(true);
setError(null);
try {
await post(`/boards/${boardId}/shares`, {
name: data.name,
+ parameters: {
+ allowFilter: data.allowFilter ?? true,
+ theme: data.theme === 'system' ? undefined : data.theme,
+ },
});
touch('shares');
@@ -36,22 +50,38 @@ export function BoardShareCreateForm({
};
return (
-
);
}
diff --git a/src/app/(main)/boards/[boardId]/BoardShareDialog.tsx b/src/app/(main)/boards/[boardId]/BoardShareDialog.tsx
index ba7a84774..14989a0b4 100644
--- a/src/app/(main)/boards/[boardId]/BoardShareDialog.tsx
+++ b/src/app/(main)/boards/[boardId]/BoardShareDialog.tsx
@@ -1,9 +1,9 @@
import { Button, Column, Heading, Row, Text } from '@umami/react-zen';
import { useState } from 'react';
-import { Plus } from '@/components/icons';
-import { LoadingPanel } from '@/components/common/LoadingPanel';
import { IconLabel } from '@/components/common/IconLabel';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
import { useBoardSharesQuery, useMessages } from '@/components/hooks';
+import { Plus } from '@/components/icons';
import { BoardShareCreateForm } from './BoardShareCreateForm';
import { BoardSharesTable } from './BoardSharesTable';
@@ -49,9 +49,7 @@ function BoardShareDialogContent({
onCancel={() => setIsCreating(false)}
/>
)}
- {hasShares && (
- {t(messages.shareUrl)}
- )}
+ {hasShares && {t(messages.shareUrl)}}
{!showCreateForm &&
(hasShares ? (
diff --git a/src/app/(main)/boards/[boardId]/BoardSharesTable.tsx b/src/app/(main)/boards/[boardId]/BoardSharesTable.tsx
index 3f4e81531..e8e5ff78f 100644
--- a/src/app/(main)/boards/[boardId]/BoardSharesTable.tsx
+++ b/src/app/(main)/boards/[boardId]/BoardSharesTable.tsx
@@ -1,8 +1,10 @@
+import { DataColumn, DataTable, Icon, type DataTableProps, Row, Text } from '@umami/react-zen';
import { ShareDeleteButton } from '@/app/(main)/websites/[websiteId]/settings/ShareDeleteButton';
import { CopyButton } from '@/components/common/CopyButton';
-import { ExternalLink } from '@/components/common/ExternalLink';
+import Link from '@/components/common/Link';
import { useConfig, useMessages, useMobile } from '@/components/hooks';
-import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
+import { ExternalLink } from '@/components/icons';
+import { SimpleShareEditButton } from '@/components/share/SimpleShareEditButton';
export function BoardSharesTable(props: DataTableProps) {
const { t, labels } = useMessages();
@@ -22,23 +24,31 @@ export function BoardSharesTable(props: DataTableProps) {
{({ name }: any) => name}
-
+
{({ slug }: any) => {
const url = getUrl(slug);
return (
-
-
- {isMobile ? slug : url}
-
+
+
+
+
+ {isMobile ? slug : url}
+
+
+
+
+
+
);
}}
-
+
{({ id, slug }: any) => (
+
)}
diff --git a/src/app/(main)/boards/[boardId]/BoardViewHeader.tsx b/src/app/(main)/boards/[boardId]/BoardViewHeader.tsx
index 8533e4534..5b8758c9b 100644
--- a/src/app/(main)/boards/[boardId]/BoardViewHeader.tsx
+++ b/src/app/(main)/boards/[boardId]/BoardViewHeader.tsx
@@ -3,8 +3,9 @@ import { IconLabel } from '@/components/common/IconLabel';
import { LinkButton } from '@/components/common/LinkButton';
import { PageHeader } from '@/components/common/PageHeader';
import { useBoard, useMessages, useNavigation } from '@/components/hooks';
-import { getBoardEntity } from '@/lib/boards';
import { Edit, LayoutDashboard } from '@/components/icons';
+import { getBoardEntity } from '@/lib/boards';
+import { BoardCloneButton } from '../BoardCloneButton';
import { BoardEntityBadge } from '../BoardEntityBadge';
import { useBoardEntityBadgeProps } from '../useBoardEntityBadgeProps';
@@ -26,13 +27,14 @@ export function BoardViewHeader({
{showEntityBadge && entityBadge && }
{showActions && board?.id && (
- <>
-
- }>Design
-
+ <>
+
+ }>{t(labels.design)}
+
}>{t(labels.edit)}
+
>
)}
diff --git a/src/app/(main)/boards/[boardId]/edit/BoardEditPage.tsx b/src/app/(main)/boards/[boardId]/edit/BoardEditPage.tsx
index 69c813822..083390be3 100644
--- a/src/app/(main)/boards/[boardId]/edit/BoardEditPage.tsx
+++ b/src/app/(main)/boards/[boardId]/edit/BoardEditPage.tsx
@@ -1,12 +1,12 @@
'use client';
import { Column } from '@umami/react-zen';
-import Link from '@/components/common/Link';
-import { BoardEditForm } from '@/app/(main)/boards/BoardEditForm';
import { BoardShareDialog } from '@/app/(main)/boards/[boardId]/BoardShareDialog';
+import { BoardEditForm } from '@/app/(main)/boards/BoardEditForm';
import { IconLabel } from '@/components/common/IconLabel';
-import { Panel } from '@/components/common/Panel';
+import Link from '@/components/common/Link';
import { PageBody } from '@/components/common/PageBody';
import { PageHeader } from '@/components/common/PageHeader';
+import { Panel } from '@/components/common/Panel';
import { useBoardQuery, useMessages, useNavigation } from '@/components/hooks';
import { ArrowLeft, LayoutDashboard } from '@/components/icons';
@@ -17,12 +17,7 @@ export function BoardEditPage({ boardId }: { boardId: string }) {
return (
-
+
<>
diff --git a/src/app/(main)/boards/boardComponentRegistry.tsx b/src/app/(main)/boards/boardComponentRegistry.tsx
index 2af1c7006..1af813051 100644
--- a/src/app/(main)/boards/boardComponentRegistry.tsx
+++ b/src/app/(main)/boards/boardComponentRegistry.tsx
@@ -1,30 +1,47 @@
+import type { ComponentType } from 'react';
import { TextBlock } from '@/app/(main)/boards/TextBlock';
+import { BoardFunnel } from '@/app/(main)/websites/[websiteId]/(reports)/funnels/BoardFunnel';
import { LinkMetricsBar } from '@/app/(main)/links/[linkId]/LinkMetricsBar';
import { PixelMetricsBar } from '@/app/(main)/pixels/[pixelId]/PixelMetricsBar';
+import { BoardGoal } from '@/app/(main)/websites/[websiteId]/(reports)/goals/BoardGoal';
+import { BoardRevenueChart } from '@/app/(main)/websites/[websiteId]/(reports)/revenue/BoardRevenueChart';
+import { BoardRevenueMetricsBar } from '@/app/(main)/websites/[websiteId]/(reports)/revenue/BoardRevenueMetricsBar';
+import { BoardRevenueMetricsTable } from '@/app/(main)/websites/[websiteId]/(reports)/revenue/BoardRevenueMetricsTable';
+import { BoardUTM } from '@/app/(main)/websites/[websiteId]/(reports)/utm/BoardUTM';
+import { EventsMetricsBar } from '@/app/(main)/websites/[websiteId]/events/EventsMetricsBar';
+import { BoardRealtimeActiveUsers } from '@/app/(main)/websites/[websiteId]/realtime/BoardRealtimeActiveUsers';
+import { BoardRealtimeChart } from '@/app/(main)/websites/[websiteId]/realtime/BoardRealtimeChart';
+import { BoardRealtimeMetricsBar } from '@/app/(main)/websites/[websiteId]/realtime/BoardRealtimeMetricsBar';
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
import { WebsiteMetricsBar } from '@/app/(main)/websites/[websiteId]/WebsiteMetricsBar';
import {
+ Activity,
Calendar,
ChartColumnBig,
ChartPie,
FileText,
+ GitBranch,
Globe,
PanelTop,
Sheet,
+ Target,
+ Users,
} from '@/components/icons';
import { EventsChart } from '@/components/metrics/EventsChart';
import { MetricsTable } from '@/components/metrics/MetricsTable';
import { WeeklyTraffic } from '@/components/metrics/WeeklyTraffic';
import { WorldMap } from '@/components/metrics/WorldMap';
-import type { ComponentType } from 'react';
+import { CURRENCIES, DEFAULT_CURRENCY } from '@/lib/constants';
export interface ConfigField {
name: string;
label: string;
- type: 'select' | 'number' | 'text' | 'textarea';
+ type: 'select' | 'number' | 'text' | 'textarea' | 'report';
options?: { label: string; value: string }[];
optionsByEntityType?: Record;
defaultValue?: any;
+ reportType?: string;
+ required?: boolean;
}
export interface ComponentDefinition {
@@ -32,6 +49,7 @@ export interface ComponentDefinition {
name: string;
description: string;
category: string;
+ group?: string;
icon: ComponentType;
component: ComponentType;
componentByEntityType?: Record>;
@@ -49,6 +67,7 @@ export const CATEGORIES = [
const METRIC_TYPES = [
{ label: 'Path', value: 'path' },
+ { label: 'URL', value: 'fullPath' },
{ label: 'Entry page', value: 'entry' },
{ label: 'Exit page', value: 'exit' },
{ label: 'Title', value: 'title' },
@@ -75,18 +94,13 @@ const METRIC_TYPES = [
const PIXEL_LINK_METRIC_TYPES = METRIC_TYPES.filter(({ value }) =>
[
'referrer',
- 'country',
- 'region',
- 'city',
+ 'channel',
'browser',
'os',
'device',
- 'query',
- 'utmSource',
- 'utmMedium',
- 'utmCampaign',
- 'utmContent',
- 'utmTerm',
+ 'country',
+ 'region',
+ 'city',
].includes(value),
);
@@ -96,6 +110,26 @@ const LIMIT_OPTIONS = [
{ label: '20', value: '20' },
];
+const UTM_PARAM_OPTIONS = [
+ { label: 'Source', value: 'utm_source' },
+ { label: 'Medium', value: 'utm_medium' },
+ { label: 'Campaign', value: 'utm_campaign' },
+ { label: 'Content', value: 'utm_content' },
+ { label: 'Term', value: 'utm_term' },
+];
+
+const REVENUE_METRIC_TYPE_OPTIONS = [
+ { label: 'Referrers', value: 'referrer' },
+ { label: 'Channels', value: 'channel' },
+ { label: 'Countries', value: 'country' },
+ { label: 'Regions', value: 'region' },
+];
+
+const CURRENCY_OPTIONS = CURRENCIES.map(({ id, name }) => ({
+ label: `${id} - ${name}`,
+ value: id,
+}));
+
const PixelMetricsBarAdapter = ({ websiteId }: { websiteId?: string }) =>
websiteId ? : null;
@@ -109,6 +143,7 @@ const componentDefinitions: ComponentDefinition[] = [
name: 'Metrics bar',
description: 'Key metrics: views, visitors, bounces, time on site',
category: 'overview',
+ group: 'Traffic',
icon: PanelTop,
component: WebsiteMetricsBar,
componentByEntityType: {
@@ -116,14 +151,177 @@ const componentDefinitions: ComponentDefinition[] = [
link: LinkMetricsBarAdapter,
},
},
+ {
+ type: 'EventsMetricsBar',
+ name: 'Event metrics bar',
+ description: 'Key metrics: visitors, visits, events, unique events',
+ category: 'overview',
+ group: 'Events',
+ icon: PanelTop,
+ component: EventsMetricsBar,
+ },
+ {
+ type: 'Goal',
+ name: 'Goal',
+ description: 'Conversion progress for a saved goal',
+ category: 'overview',
+ group: 'Behavior',
+ icon: Target,
+ component: BoardGoal,
+ configFields: [
+ {
+ name: 'reportId',
+ label: 'Saved goal',
+ type: 'report',
+ reportType: 'goal',
+ required: true,
+ },
+ ],
+ },
+ {
+ type: 'Funnel',
+ name: 'Funnel',
+ description: 'Step conversion for a saved funnel',
+ category: 'overview',
+ group: 'Behavior',
+ icon: GitBranch,
+ component: BoardFunnel,
+ configFields: [
+ {
+ name: 'reportId',
+ label: 'Saved funnel',
+ type: 'report',
+ reportType: 'funnel',
+ required: true,
+ },
+ ],
+ },
{
type: 'WebsiteChart',
- name: 'Website chart',
+ name: 'Visitors chart',
description: 'Page views and visitors over time',
category: 'overview',
+ group: 'Traffic',
icon: ChartColumnBig,
component: WebsiteChart,
},
+ {
+ type: 'UTM',
+ name: 'UTM',
+ description: 'List and pie chart for one UTM parameter',
+ category: 'visualization',
+ group: 'Growth',
+ icon: ChartPie,
+ component: BoardUTM,
+ defaultProps: { param: 'utm_source', limit: 10 },
+ configFields: [
+ {
+ name: 'param',
+ label: 'UTM param',
+ type: 'select',
+ options: UTM_PARAM_OPTIONS,
+ defaultValue: 'utm_source',
+ },
+ {
+ name: 'limit',
+ label: 'Rows',
+ type: 'select',
+ options: LIMIT_OPTIONS,
+ defaultValue: '10',
+ },
+ ],
+ },
+ {
+ type: 'RealtimeHeader',
+ name: 'Realtime metrics bar',
+ description: 'Live views, visitors, events, and countries',
+ category: 'overview',
+ group: 'Realtime',
+ icon: PanelTop,
+ component: BoardRealtimeMetricsBar,
+ },
+ {
+ type: 'RealtimeChart',
+ name: 'Realtime chart',
+ description: 'Live visitors and page views by minute',
+ category: 'visualization',
+ group: 'Realtime',
+ icon: Activity,
+ component: BoardRealtimeChart,
+ },
+ {
+ type: 'RealtimeActiveUsers',
+ name: 'Active users',
+ description: 'Current visitors online',
+ category: 'overview',
+ group: 'Realtime',
+ icon: Users,
+ component: BoardRealtimeActiveUsers,
+ },
+ {
+ type: 'RevenueMetricsBar',
+ name: 'Revenue metrics bar',
+ description: 'Revenue summary: total, AOV, ARPU, orders, and customers',
+ category: 'overview',
+ group: 'Revenue',
+ icon: PanelTop,
+ component: BoardRevenueMetricsBar,
+ defaultProps: { currency: DEFAULT_CURRENCY },
+ configFields: [
+ {
+ name: 'currency',
+ label: 'Currency',
+ type: 'select',
+ options: CURRENCY_OPTIONS,
+ defaultValue: DEFAULT_CURRENCY,
+ },
+ ],
+ },
+ {
+ type: 'RevenueChart',
+ name: 'Revenue chart',
+ description: 'Revenue over time',
+ category: 'visualization',
+ group: 'Revenue',
+ icon: ChartColumnBig,
+ component: BoardRevenueChart,
+ defaultProps: { currency: DEFAULT_CURRENCY },
+ configFields: [
+ {
+ name: 'currency',
+ label: 'Currency',
+ type: 'select',
+ options: CURRENCY_OPTIONS,
+ defaultValue: DEFAULT_CURRENCY,
+ },
+ ],
+ },
+ {
+ type: 'RevenueMetricsTable',
+ name: 'Revenue metrics table',
+ description: 'Revenue breakdown by source or location',
+ category: 'tables',
+ group: 'Revenue',
+ icon: Sheet,
+ component: BoardRevenueMetricsTable,
+ defaultProps: { type: 'referrer', currency: DEFAULT_CURRENCY },
+ configFields: [
+ {
+ name: 'type',
+ label: 'Metric type',
+ type: 'select',
+ options: REVENUE_METRIC_TYPE_OPTIONS,
+ defaultValue: 'referrer',
+ },
+ {
+ name: 'currency',
+ label: 'Currency',
+ type: 'select',
+ options: CURRENCY_OPTIONS,
+ defaultValue: DEFAULT_CURRENCY,
+ },
+ ],
+ },
// Tables
{
@@ -131,6 +329,7 @@ const componentDefinitions: ComponentDefinition[] = [
name: 'Metrics table',
description: 'Table of metrics by dimension',
category: 'tables',
+ group: 'Traffic',
icon: Sheet,
component: MetricsTable,
defaultProps: { type: 'path', limit: 10 },
@@ -162,6 +361,7 @@ const componentDefinitions: ComponentDefinition[] = [
name: 'World map',
description: 'Geographic distribution of visitors',
category: 'visualization',
+ group: 'Traffic',
icon: Globe,
component: WorldMap,
},
@@ -170,6 +370,7 @@ const componentDefinitions: ComponentDefinition[] = [
name: 'Weekly traffic',
description: 'Traffic heatmap by day and hour',
category: 'visualization',
+ group: 'Traffic',
icon: Calendar,
component: WeeklyTraffic,
},
@@ -178,6 +379,7 @@ const componentDefinitions: ComponentDefinition[] = [
name: 'Events chart',
description: 'Custom events over time',
category: 'visualization',
+ group: 'Events',
icon: ChartPie,
component: EventsChart,
},
@@ -188,6 +390,7 @@ const componentDefinitions: ComponentDefinition[] = [
name: 'Text',
description: 'Free-form text content',
category: 'content',
+ group: 'Content',
icon: FileText,
component: TextBlock,
requiresWebsite: false,
diff --git a/src/app/(main)/console/[websiteId]/TestConsolePage.tsx b/src/app/(main)/console/[websiteId]/TestConsolePage.tsx
index e5dcd345a..26b85cf42 100644
--- a/src/app/(main)/console/[websiteId]/TestConsolePage.tsx
+++ b/src/app/(main)/console/[websiteId]/TestConsolePage.tsx
@@ -1,8 +1,8 @@
'use client';
import { Button, Column, Grid, Heading } from '@umami/react-zen';
-import Link from '@/components/common/Link';
import Script from 'next/script';
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
+import Link from '@/components/common/Link';
import { PageBody } from '@/components/common/PageBody';
import { PageHeader } from '@/components/common/PageHeader';
import { Panel } from '@/components/common/Panel';
@@ -23,7 +23,7 @@ export function TestConsolePage({ websiteId }: { websiteId: string }) {
test: 'test-data',
boolean: true,
booleanError: 'true',
- time: new Date(),
+ time: new Date().toISOString(),
user: `user${Math.round(Math.random() * 10)}`,
number: 1,
number2: Math.random() * 100,
@@ -87,7 +87,7 @@ export function TestConsolePage({ websiteId }: { websiteId: string }) {
test: 'test-data',
boolean: true,
booleanError: 'true',
- time: new Date(),
+ time: new Date().toISOString(),
time2: new Date().toISOString(),
nested: {
test: 'test-data',
diff --git a/src/app/(main)/dashboard/DashboardViewHeader.tsx b/src/app/(main)/dashboard/DashboardViewHeader.tsx
index e4328fce8..18d89033d 100644
--- a/src/app/(main)/dashboard/DashboardViewHeader.tsx
+++ b/src/app/(main)/dashboard/DashboardViewHeader.tsx
@@ -11,7 +11,7 @@ export function DashboardViewHeader() {
return (
- }>Design
+ }>{t(labels.design)}
);
diff --git a/src/app/(main)/links/LinkEditButton.tsx b/src/app/(main)/links/LinkEditButton.tsx
index 44598a5db..b1358a564 100644
--- a/src/app/(main)/links/LinkEditButton.tsx
+++ b/src/app/(main)/links/LinkEditButton.tsx
@@ -1,7 +1,6 @@
import { Icon } from '@umami/react-zen';
import { LinkButton } from '@/components/common/LinkButton';
-import { useNavigation } from '@/components/hooks';
-import { useMessages } from '@/components/hooks';
+import { useMessages, useNavigation } from '@/components/hooks';
import { Edit } from '@/components/icons';
export function LinkEditButton({ linkId }: { linkId: string }) {
@@ -11,7 +10,6 @@ export function LinkEditButton({ linkId }: { linkId: string }) {
return (
diff --git a/src/app/(main)/links/LinksTable.tsx b/src/app/(main)/links/LinksTable.tsx
index bf9b5ef7f..0ba231d11 100644
--- a/src/app/(main)/links/LinksTable.tsx
+++ b/src/app/(main)/links/LinksTable.tsx
@@ -1,8 +1,11 @@
import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
-import Link from '@/components/common/Link';
+import { useMemo } from 'react';
+import { WebsiteSparkline } from '@/app/(main)/websites/WebsiteSparkline';
import { DateDistance } from '@/components/common/DateDistance';
import { ExternalLink } from '@/components/common/ExternalLink';
-import { useMessages, useNavigation, useSlug } from '@/components/hooks';
+import Link from '@/components/common/Link';
+import { SortableLabel } from '@/components/common/SortableLabel';
+import { useLinkListChartsQuery, useMessages, useNavigation, useSlug } from '@/components/hooks';
import { LinkDeleteButton } from './LinkDeleteButton';
import { LinkEditButton } from './LinkEditButton';
@@ -10,37 +13,72 @@ export interface LinksTableProps extends DataTableProps {
showActions?: boolean;
}
-export function LinksTable({ showActions, ...props }: LinksTableProps) {
+export function LinksTable({ showActions, data = [], ...props }: LinksTableProps & { data?: any[] }) {
const { t, labels } = useMessages();
const { websiteId, renderUrl } = useNavigation();
const { getSlugUrl } = useSlug('link');
+ const linkIds = useMemo(() => data.map(row => row.id), [data]);
+ const chartsQuery = useLinkListChartsQuery(linkIds);
+ const charts = chartsQuery.data?.data || {};
+ const isChartLoading = chartsQuery.isLoading && !chartsQuery.data;
return (
-
-
+
+ }>
{({ id, name }: any) => {
return {name};
}}
-
+ }
+ style={{ minWidth: 0 }}
+ >
{({ slug }: any) => {
const url = getSlugUrl(slug);
return {url};
}}
-
+ }
+ style={{ minWidth: 0 }}
+ >
{({ url }: any) => {
return {url};
}}
-
+ {`${t(labels.visitors)} (7d)`}}
+ style={{ minWidth: 0 }}
+ >
+ {(row: any) => {
+ const chart = charts[row.id];
+
+ return (
+
+ );
+ }}
+
+
+ }
+ width="180px"
+ >
{(row: any) => }
{showActions && (
-
+
{({ id, name }: any) => {
return (
-
+
diff --git a/src/app/(main)/links/[linkId]/LinkControls.tsx b/src/app/(main)/links/[linkId]/LinkControls.tsx
index 1d1147a82..ad09457e8 100644
--- a/src/app/(main)/links/[linkId]/LinkControls.tsx
+++ b/src/app/(main)/links/[linkId]/LinkControls.tsx
@@ -1,9 +1,11 @@
import { Column, Row } from '@umami/react-zen';
+import { useShare } from '@/components/hooks';
import { ExportButton } from '@/components/input/ExportButton';
import { FilterBar } from '@/components/input/FilterBar';
import { MonthFilter } from '@/components/input/MonthFilter';
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton';
+import { allowShareFilter } from '@/lib/share';
export function LinkControls({
linkId: websiteId,
@@ -18,15 +20,18 @@ export function LinkControls({
allowMonthFilter?: boolean;
allowDownload?: boolean;
}) {
+ const share = useShare();
+ const showFilter = allowFilter && allowShareFilter(share?.parameters);
+
return (
- {allowFilter ? : }
+ {showFilter ? : }
{allowDateFilter && }
{allowDownload && }
{allowMonthFilter && }
- {allowFilter && }
+ {showFilter && }
);
}
diff --git a/src/app/(main)/links/[linkId]/LinkPage.tsx b/src/app/(main)/links/[linkId]/LinkPage.tsx
index 5d10fa82b..910d1f51a 100644
--- a/src/app/(main)/links/[linkId]/LinkPage.tsx
+++ b/src/app/(main)/links/[linkId]/LinkPage.tsx
@@ -10,7 +10,7 @@ import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
import { PageBody } from '@/components/common/PageBody';
import { Panel } from '@/components/common/Panel';
-const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event'];
+const excludedIds = ['path', 'fullPath', 'entry', 'exit', 'title', 'language', 'screen', 'event'];
export function LinkPage({
linkId,
diff --git a/src/app/(main)/links/[linkId]/LinkShareForm.tsx b/src/app/(main)/links/[linkId]/LinkShareForm.tsx
index 3486a71ea..a486c6fb0 100644
--- a/src/app/(main)/links/[linkId]/LinkShareForm.tsx
+++ b/src/app/(main)/links/[linkId]/LinkShareForm.tsx
@@ -1,7 +1,7 @@
import { Button, Column, Heading, Row, Text } from '@umami/react-zen';
import { useState } from 'react';
-import { LoadingPanel } from '@/components/common/LoadingPanel';
import { IconLabel } from '@/components/common/IconLabel';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
import { useLinkSharesQuery, useMessages } from '@/components/hooks';
import { Plus } from '@/components/icons';
import { SimpleShareCreateForm } from '@/components/share/SimpleShareCreateForm';
@@ -49,9 +49,7 @@ function LinkShareFormContent({
onCancel={() => setIsCreating(false)}
/>
)}
- {hasShares && (
- {t(messages.shareUrl)}
- )}
+ {hasShares && {t(messages.shareUrl)}}
{!showCreateForm &&
(hasShares ? (
diff --git a/src/app/(main)/links/[linkId]/edit/LinkEditPage.tsx b/src/app/(main)/links/[linkId]/edit/LinkEditPage.tsx
index c87bec864..013ef02da 100644
--- a/src/app/(main)/links/[linkId]/edit/LinkEditPage.tsx
+++ b/src/app/(main)/links/[linkId]/edit/LinkEditPage.tsx
@@ -1,12 +1,12 @@
'use client';
import { Column } from '@umami/react-zen';
-import Link from '@/components/common/Link';
+import { LinkShareForm } from '@/app/(main)/links/[linkId]/LinkShareForm';
import { LinkEditForm } from '@/app/(main)/links/LinkEditForm';
import { LinkProvider } from '@/app/(main)/links/LinkProvider';
-import { LinkShareForm } from '@/app/(main)/links/[linkId]/LinkShareForm';
-import { Panel } from '@/components/common/Panel';
import { IconLabel } from '@/components/common/IconLabel';
+import Link from '@/components/common/Link';
import { PageHeader } from '@/components/common/PageHeader';
+import { Panel } from '@/components/common/Panel';
import { useLink, useMessages, useNavigation } from '@/components/hooks';
import { ArrowLeft, Link as LinkIcon } from '@/components/icons';
diff --git a/src/app/(main)/pixels/PixelEditButton.tsx b/src/app/(main)/pixels/PixelEditButton.tsx
index 8a8f141d6..18f50aaae 100644
--- a/src/app/(main)/pixels/PixelEditButton.tsx
+++ b/src/app/(main)/pixels/PixelEditButton.tsx
@@ -1,7 +1,6 @@
import { Icon } from '@umami/react-zen';
import { LinkButton } from '@/components/common/LinkButton';
-import { useNavigation } from '@/components/hooks';
-import { useMessages } from '@/components/hooks';
+import { useMessages, useNavigation } from '@/components/hooks';
import { Edit } from '@/components/icons';
export function PixelEditButton({ pixelId }: { pixelId: string }) {
@@ -11,7 +10,6 @@ export function PixelEditButton({ pixelId }: { pixelId: string }) {
return (
diff --git a/src/app/(main)/pixels/PixelsTable.tsx b/src/app/(main)/pixels/PixelsTable.tsx
index e9bc76ad8..c53c2c9b5 100644
--- a/src/app/(main)/pixels/PixelsTable.tsx
+++ b/src/app/(main)/pixels/PixelsTable.tsx
@@ -1,8 +1,11 @@
import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
-import Link from '@/components/common/Link';
+import { useMemo } from 'react';
+import { WebsiteSparkline } from '@/app/(main)/websites/WebsiteSparkline';
import { DateDistance } from '@/components/common/DateDistance';
import { ExternalLink } from '@/components/common/ExternalLink';
-import { useMessages, useNavigation, useSlug } from '@/components/hooks';
+import Link from '@/components/common/Link';
+import { SortableLabel } from '@/components/common/SortableLabel';
+import { useMessages, useNavigation, usePixelListChartsQuery, useSlug } from '@/components/hooks';
import { PixelDeleteButton } from './PixelDeleteButton';
import { PixelEditButton } from './PixelEditButton';
@@ -10,19 +13,27 @@ export interface PixelsTableProps extends DataTableProps {
showActions?: boolean;
}
-export function PixelsTable({ showActions, ...props }: PixelsTableProps) {
+export function PixelsTable({
+ showActions,
+ data = [],
+ ...props
+}: PixelsTableProps & { data?: any[] }) {
const { t, labels } = useMessages();
const { renderUrl } = useNavigation();
const { getSlugUrl } = useSlug('pixel');
+ const pixelIds = useMemo(() => data.map(row => row.id), [data]);
+ const chartsQuery = usePixelListChartsQuery(pixelIds);
+ const charts = chartsQuery.data?.data || {};
+ const isChartLoading = chartsQuery.isLoading && !chartsQuery.data;
return (
-
-
+
+ }>
{({ id, name }: any) => {
return {name};
}}
-
+ } style={{ minWidth: 0 }}>
{({ slug }: any) => {
const url = getSlugUrl(slug);
return (
@@ -32,16 +43,39 @@ export function PixelsTable({ showActions, ...props }: PixelsTableProps) {
);
}}
-
+ {`${t(labels.visitors)} (7d)`}}
+ style={{ minWidth: 0 }}
+ >
+ {(row: any) => {
+ const chart = charts[row.id];
+
+ return (
+
+ );
+ }}
+
+
+ }
+ width="180px"
+ >
{(row: any) => }
{showActions && (
-
+
{(row: any) => {
const { id, name } = row;
return (
-
+
diff --git a/src/app/(main)/pixels/[pixelId]/PixelControls.tsx b/src/app/(main)/pixels/[pixelId]/PixelControls.tsx
index 55dcd5764..539b0adca 100644
--- a/src/app/(main)/pixels/[pixelId]/PixelControls.tsx
+++ b/src/app/(main)/pixels/[pixelId]/PixelControls.tsx
@@ -1,9 +1,11 @@
import { Column, Row } from '@umami/react-zen';
+import { useShare } from '@/components/hooks';
import { ExportButton } from '@/components/input/ExportButton';
import { FilterBar } from '@/components/input/FilterBar';
import { MonthFilter } from '@/components/input/MonthFilter';
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton';
+import { allowShareFilter } from '@/lib/share';
export function PixelControls({
pixelId: websiteId,
@@ -18,15 +20,18 @@ export function PixelControls({
allowMonthFilter?: boolean;
allowDownload?: boolean;
}) {
+ const share = useShare();
+ const showFilter = allowFilter && allowShareFilter(share?.parameters);
+
return (
- {allowFilter ? : }
+ {showFilter ? : }
{allowDateFilter && }
{allowDownload && }
{allowMonthFilter && }
- {allowFilter && }
+ {showFilter && }
);
}
diff --git a/src/app/(main)/pixels/[pixelId]/PixelPage.tsx b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx
index e8d605d24..09d6ce4e2 100644
--- a/src/app/(main)/pixels/[pixelId]/PixelPage.tsx
+++ b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx
@@ -10,7 +10,7 @@ import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
import { PageBody } from '@/components/common/PageBody';
import { Panel } from '@/components/common/Panel';
-const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event'];
+const excludedIds = ['path', 'fullPath', 'entry', 'exit', 'title', 'language', 'screen', 'event'];
export function PixelPage({
pixelId,
diff --git a/src/app/(main)/pixels/[pixelId]/PixelShareForm.tsx b/src/app/(main)/pixels/[pixelId]/PixelShareForm.tsx
index 4c31bbd74..1044cff7a 100644
--- a/src/app/(main)/pixels/[pixelId]/PixelShareForm.tsx
+++ b/src/app/(main)/pixels/[pixelId]/PixelShareForm.tsx
@@ -1,7 +1,7 @@
import { Button, Column, Heading, Row, Text } from '@umami/react-zen';
import { useState } from 'react';
-import { LoadingPanel } from '@/components/common/LoadingPanel';
import { IconLabel } from '@/components/common/IconLabel';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
import { useMessages, usePixelSharesQuery } from '@/components/hooks';
import { Plus } from '@/components/icons';
import { SimpleShareCreateForm } from '@/components/share/SimpleShareCreateForm';
@@ -49,9 +49,7 @@ function PixelShareFormContent({
onCancel={() => setIsCreating(false)}
/>
)}
- {hasShares && (
- {t(messages.shareUrl)}
- )}
+ {hasShares && {t(messages.shareUrl)}}
{!showCreateForm &&
(hasShares ? (
diff --git a/src/app/(main)/pixels/[pixelId]/edit/PixelEditPage.tsx b/src/app/(main)/pixels/[pixelId]/edit/PixelEditPage.tsx
index d339c756e..04e3ea753 100644
--- a/src/app/(main)/pixels/[pixelId]/edit/PixelEditPage.tsx
+++ b/src/app/(main)/pixels/[pixelId]/edit/PixelEditPage.tsx
@@ -1,12 +1,12 @@
'use client';
import { Column } from '@umami/react-zen';
-import Link from '@/components/common/Link';
+import { PixelShareForm } from '@/app/(main)/pixels/[pixelId]/PixelShareForm';
import { PixelEditForm } from '@/app/(main)/pixels/PixelEditForm';
import { PixelProvider } from '@/app/(main)/pixels/PixelProvider';
-import { PixelShareForm } from '@/app/(main)/pixels/[pixelId]/PixelShareForm';
-import { Panel } from '@/components/common/Panel';
import { IconLabel } from '@/components/common/IconLabel';
+import Link from '@/components/common/Link';
import { PageHeader } from '@/components/common/PageHeader';
+import { Panel } from '@/components/common/Panel';
import { useMessages, useNavigation, usePixel } from '@/components/hooks';
import { ArrowLeft, Grid2x2 } from '@/components/icons';
diff --git a/src/app/(main)/settings/SettingsNav.tsx b/src/app/(main)/settings/SettingsNav.tsx
index 8102ad7a6..6f728307f 100644
--- a/src/app/(main)/settings/SettingsNav.tsx
+++ b/src/app/(main)/settings/SettingsNav.tsx
@@ -1,6 +1,6 @@
import { Column, Focusable, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen';
-import Link from '@/components/common/Link';
import { IconLabel } from '@/components/common/IconLabel';
+import Link from '@/components/common/Link';
import { useMessages, useNavigation } from '@/components/hooks';
import { ArrowLeft, Settings2, ShieldCheck, UserCircle, Users } from '@/components/icons';
diff --git a/src/app/(main)/settings/preferences/ThemeSetting.tsx b/src/app/(main)/settings/preferences/ThemeSetting.tsx
index 03bd6a6e4..b9938fa92 100644
--- a/src/app/(main)/settings/preferences/ThemeSetting.tsx
+++ b/src/app/(main)/settings/preferences/ThemeSetting.tsx
@@ -1,21 +1,10 @@
-import { Button, Icon, Row, useTheme } from '@umami/react-zen';
-import { Moon, Sun } from '@/components/icons';
+import { useTheme } from '@umami/react-zen';
+import { ThemeModeSelector } from '@/components/input/ThemeModeSelector';
export function ThemeSetting() {
const { theme, setTheme } = useTheme();
return (
-
-
-
-
+ setTheme(value as 'light' | 'dark')} />
);
}
diff --git a/src/app/(main)/teams/TeamsDataTable.tsx b/src/app/(main)/teams/TeamsDataTable.tsx
index 6decff1be..a2dbce597 100644
--- a/src/app/(main)/teams/TeamsDataTable.tsx
+++ b/src/app/(main)/teams/TeamsDataTable.tsx
@@ -1,5 +1,5 @@
-import Link from '@/components/common/Link';
import { DataGrid } from '@/components/common/DataGrid';
+import Link from '@/components/common/Link';
import { useLoginQuery, useNavigation, useUserTeamsQuery } from '@/components/hooks';
import { TeamsTable } from './TeamsTable';
diff --git a/src/app/(main)/teams/TeamsTable.tsx b/src/app/(main)/teams/TeamsTable.tsx
index 66c7355ac..77515b7b2 100644
--- a/src/app/(main)/teams/TeamsTable.tsx
+++ b/src/app/(main)/teams/TeamsTable.tsx
@@ -1,5 +1,6 @@
import { DataColumn, DataTable, type DataTableProps } from '@umami/react-zen';
import type { ReactNode } from 'react';
+import { SortableLabel } from '@/components/common/SortableLabel';
import { useMessages } from '@/components/hooks';
import { ROLES } from '@/lib/constants';
@@ -12,7 +13,7 @@ export function TeamsTable({ renderLink, ...props }: TeamsTableProps) {
return (
-
+ }>
{renderLink}
diff --git a/src/app/(main)/teams/[teamId]/TeamWebsitesTable.tsx b/src/app/(main)/teams/[teamId]/TeamWebsitesTable.tsx
index 86d846bb2..b06840d1e 100644
--- a/src/app/(main)/teams/[teamId]/TeamWebsitesTable.tsx
+++ b/src/app/(main)/teams/[teamId]/TeamWebsitesTable.tsx
@@ -1,7 +1,8 @@
import { DataColumn, DataTable, Row } from '@umami/react-zen';
-import Link from '@/components/common/Link';
import { TeamMemberEditButton } from '@/app/(main)/teams/[teamId]/TeamMemberEditButton';
import { TeamMemberRemoveButton } from '@/app/(main)/teams/[teamId]/TeamMemberRemoveButton';
+import Link from '@/components/common/Link';
+import { SortableLabel } from '@/components/common/SortableLabel';
import { useMessages } from '@/components/hooks';
import { ROLES } from '@/lib/constants';
@@ -18,10 +19,10 @@ export function TeamWebsitesTable({
return (
-
+ }>
{(row: any) => {row.name}}
-
+ } />
{(row: any) => row?.createUser?.username}
diff --git a/src/app/(main)/websites/WebsiteAddButton.tsx b/src/app/(main)/websites/WebsiteAddButton.tsx
index 340c6fdc0..7d26b2e3c 100644
--- a/src/app/(main)/websites/WebsiteAddButton.tsx
+++ b/src/app/(main)/websites/WebsiteAddButton.tsx
@@ -1,23 +1,77 @@
-import { useToast } from '@umami/react-zen';
-import { useMessages, useModified } from '@/components/hooks';
+import { Button, Column, Row, useToast } from '@umami/react-zen';
+import { useState } from 'react';
+import { IconLabel } from '@/components/common/IconLabel';
+import { useConfig, useMessages, useModified } from '@/components/hooks';
import { Plus } from '@/components/icons';
import { DialogButton } from '@/components/input/DialogButton';
+import { WebsiteTrackingCode } from './[websiteId]/settings/WebsiteTrackingCode';
import { WebsiteAddForm } from './WebsiteAddForm';
export function WebsiteAddButton({ teamId, onSave }: { teamId: string; onSave?: () => void }) {
const { t, labels, messages } = useMessages();
const { toast } = useToast();
const { touch } = useModified();
+ const { cloudMode } = useConfig();
+ const [isOpen, setIsOpen] = useState(false);
+ const [createdWebsite, setCreatedWebsite] = useState<{ id: string } | null>(null);
- const handleSave = async () => {
+ const handleSave = async (website: { id: string }) => {
toast(t(messages.saved));
touch('websites');
onSave?.();
+
+ if (cloudMode) {
+ setCreatedWebsite(website);
+ } else {
+ setIsOpen(false);
+ }
+ };
+
+ const handleOpenChange = (open: boolean) => {
+ setIsOpen(open);
+
+ if (!open) {
+ setCreatedWebsite(null);
+ }
};
return (
- } label={t(labels.addWebsite)} variant="primary" width="400px">
- {({ close }) => }
-
+ <>
+
+
+ {({ close }) =>
+ createdWebsite ? (
+ handleOpenChange(false)} />
+ ) : (
+
+ )
+ }
+
+ >
+ );
+}
+
+function TrackingCodeStep({ websiteId, onClose }: { websiteId: string; onClose: () => void }) {
+ const { t, labels } = useMessages();
+
+ return (
+
+
+
+
+
+
);
}
diff --git a/src/app/(main)/websites/WebsiteAddForm.tsx b/src/app/(main)/websites/WebsiteAddForm.tsx
index 7f28651fc..b4f98b44f 100644
--- a/src/app/(main)/websites/WebsiteAddForm.tsx
+++ b/src/app/(main)/websites/WebsiteAddForm.tsx
@@ -6,21 +6,24 @@ export function WebsiteAddForm({
teamId,
onSave,
onClose,
+ closeOnSave = true,
}: {
teamId?: string;
- onSave?: () => void;
+ onSave?: (website: any) => void | Promise;
onClose?: () => void;
+ closeOnSave?: boolean;
}) {
const { t, labels, messages } = useMessages();
const { mutateAsync, error, isPending } = useUpdateQuery('/websites', { teamId });
const handleSubmit = async (data: any) => {
- await mutateAsync(data, {
- onSuccess: async () => {
- onSave?.();
- onClose?.();
- },
- });
+ const website = await mutateAsync(data);
+
+ await onSave?.(website);
+
+ if (closeOnSave) {
+ onClose?.();
+ }
};
return (
diff --git a/src/app/(main)/websites/WebsiteSparkline.tsx b/src/app/(main)/websites/WebsiteSparkline.tsx
new file mode 100644
index 000000000..569450463
--- /dev/null
+++ b/src/app/(main)/websites/WebsiteSparkline.tsx
@@ -0,0 +1,79 @@
+import { useTheme } from '@umami/react-zen';
+import { useMemo } from 'react';
+import { useMessages } from '@/components/hooks';
+import { getThemeColors } from '@/lib/colors';
+
+export function WebsiteSparkline({
+ values = [],
+ total = 0,
+ isLoading = false,
+}: {
+ values?: number[];
+ total?: number;
+ isLoading?: boolean;
+}) {
+ const { theme } = useTheme();
+ const { t, labels } = useMessages();
+ const { colors } = useMemo(() => getThemeColors(theme), [theme]);
+ const tooltipLabel = `${total.toLocaleString()} ${t(labels.visitors).toLocaleLowerCase()}`;
+
+ const points = useMemo(() => {
+ if (!values.length) {
+ return '';
+ }
+
+ const width = 96;
+ const height = 24;
+ const padding = 2;
+ const max = Math.max(...values, 0);
+ const min = Math.min(...values, 0);
+ const range = max - min || 1;
+
+ return values
+ .map((value, index) => {
+ const x = values.length === 1 ? width / 2 : (index / (values.length - 1)) * width;
+ const y =
+ height - padding - ((value - min) / range) * Math.max(height - padding * 2, 1);
+
+ return `${x},${y}`;
+ })
+ .join(' ');
+ }, [values]);
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/websites/WebsitesDataTable.tsx b/src/app/(main)/websites/WebsitesDataTable.tsx
index 552784871..e29106fc4 100644
--- a/src/app/(main)/websites/WebsitesDataTable.tsx
+++ b/src/app/(main)/websites/WebsitesDataTable.tsx
@@ -1,6 +1,6 @@
-import { Icon, Row } from '@umami/react-zen';
-import Link from '@/components/common/Link';
+import { Icon, Row, Text } from '@umami/react-zen';
import { DataGrid } from '@/components/common/DataGrid';
+import Link from '@/components/common/Link';
import { useLoginQuery, useNavigation, useUserWebsitesQuery } from '@/components/hooks';
import { Favicon } from '@/index';
import { WebsitesTable } from './WebsitesTable';
@@ -23,11 +23,13 @@ export function WebsitesDataTable({
const { renderUrl } = useNavigation();
const renderLink = (row: any) => (
-
-
+
+
- {row.name}
+
+ {row.name}
+
);
diff --git a/src/app/(main)/websites/WebsitesTable.tsx b/src/app/(main)/websites/WebsitesTable.tsx
index 714e1c66c..4b3e13ce3 100644
--- a/src/app/(main)/websites/WebsitesTable.tsx
+++ b/src/app/(main)/websites/WebsitesTable.tsx
@@ -1,8 +1,12 @@
-import { DataColumn, DataTable, type DataTableProps, Icon } from '@umami/react-zen';
-import type { ReactNode } from 'react';
+import { DataColumn, DataTable, type DataTableProps, Icon, Text } from '@umami/react-zen';
+import { useMemo, type ReactNode } from 'react';
+import { DateDistance } from '@/components/common/DateDistance';
import { LinkButton } from '@/components/common/LinkButton';
-import { useMessages, useNavigation } from '@/components/hooks';
+import { SortableLabel } from '@/components/common/SortableLabel';
+import { useMessages, useNavigation, useWebsiteListChartsQuery } from '@/components/hooks';
import { SquarePen } from '@/components/icons';
+import { decodePunycodeDomain } from '@/lib/format';
+import { WebsiteSparkline } from './WebsiteSparkline';
export interface WebsitesTableProps extends DataTableProps {
showActions?: boolean;
@@ -11,18 +15,71 @@ export interface WebsitesTableProps extends DataTableProps {
renderLink?: (row: any) => ReactNode;
}
-export function WebsitesTable({ showActions, renderLink, ...props }: WebsitesTableProps) {
+export function WebsitesTable({
+ showActions,
+ renderLink,
+ data = [],
+ ...props
+}: WebsitesTableProps & { data?: any[] }) {
const { t, labels } = useMessages();
const { renderUrl } = useNavigation();
+ const websiteIds = useMemo(() => data.map(row => row.id), [data]);
+ const chartsQuery = useWebsiteListChartsQuery(websiteIds);
+ const charts = chartsQuery.data?.data || {};
+ const isChartLoading = chartsQuery.isLoading && !chartsQuery.data;
return (
-
-
+
+ }
+ style={{ minWidth: 0 }}
+ >
{renderLink}
-
+ }
+ style={{ minWidth: 0 }}
+ >
+ {(row: any) => (
+
+ {decodePunycodeDomain(row.domain)}
+
+ )}
+
+ {`${t(labels.visitors)} (7d)`}}
+ style={{ minWidth: 0 }}
+ >
+ {(row: any) => {
+ const chart = charts[row.id];
+
+ return (
+
+ );
+ }}
+
+
+ }
+ width="180px"
+ >
+ {(row: any) => }
+
{showActions && (
-
+
{(row: any) => {
const websiteId = row.id;
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx
index 1c3ad7c6d..05b914cea 100644
--- a/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx
+++ b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx
@@ -6,8 +6,8 @@ import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteContro
import { Panel } from '@/components/common/Panel';
import { useDateRange, useMessages, useResultQuery } from '@/components/hooks';
import { ListCheck } from '@/components/icons';
-import { DownloadButton } from '@/components/input/DownloadButton';
import { DialogButton } from '@/components/input/DialogButton';
+import { DownloadButton } from '@/components/input/DownloadButton';
import { Breakdown } from './Breakdown';
export function BreakdownPage({ websiteId }: { websiteId: string }) {
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/BoardFunnel.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/BoardFunnel.tsx
new file mode 100644
index 000000000..4905170df
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/BoardFunnel.tsx
@@ -0,0 +1,61 @@
+import { Text } from '@umami/react-zen';
+import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { Link2Off } from '@/components/icons';
+import { useReportQuery } from '@/components/hooks/queries/useReportQuery';
+import { Funnel } from './Funnel';
+
+export function BoardFunnel({
+ websiteId,
+ reportId,
+ isPreview,
+}: {
+ websiteId: string;
+ reportId?: string;
+ isPreview?: boolean;
+}) {
+ const { data, isLoading, error, isFetching } = useReportQuery(reportId || '');
+
+ if (!reportId) {
+ return (
+ ,
+ title: 'Reconnect funnel',
+ description: 'Choose a funnel for this website.',
+ })}
+ />
+ );
+ }
+
+ if (data && (data.type !== 'funnel' || data.websiteId !== websiteId)) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {data ? (
+
+ ) : (
+ Funnel unavailable
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/EventDataFilterRow.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/EventDataFilterRow.tsx
index f970c62ff..6f673869b 100644
--- a/src/app/(main)/websites/[websiteId]/(reports)/funnels/EventDataFilterRow.tsx
+++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/EventDataFilterRow.tsx
@@ -1,7 +1,6 @@
import {
Button,
Column,
- ComboBox,
FormField,
Grid,
Icon,
@@ -12,6 +11,7 @@ import {
} from '@umami/react-zen';
import { endOfDay, subMonths } from 'date-fns';
import { useState } from 'react';
+import { ComboBox } from '@/components/common/ComboBox';
import { Empty } from '@/components/common/Empty';
import { useApi, useMessages, useMobile } from '@/components/hooks';
import { X } from '@/components/icons';
@@ -56,7 +56,7 @@ function PropertySelect({
enabled: !!websiteId,
});
- const properties = [...new Set(data?.map(d => d.propertyName) ?? [])];
+ const properties: string[] = Array.from(new Set(data?.map(d => d.propertyName) ?? []));
return (
d.value) ?? [];
+ const values: string[] = data?.map(d => d.value) ?? [];
return (
;
websiteId: string;
+ allowEdit?: boolean;
}
-export function Funnel({ id, name, type, parameters, websiteId }: FunnelProps) {
+export function Funnel({
+ id,
+ name,
+ type,
+ parameters,
+ websiteId,
+ allowEdit = true,
+}: FunnelProps) {
const { t, labels } = useMessages();
const { pathname } = useNavigation();
const isSharePage = pathname.includes('/share/');
@@ -39,7 +47,7 @@ export function Funnel({ id, name, type, parameters, websiteId }: FunnelProps) {
- {!isSharePage && (
+ {allowEdit && !isSharePage && (
-
- {valueField}
-
+ {valueField}
- {!isSharePage && (
+ {allowEdit && !isSharePage && (
-
+
{({ close }) => }
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.module.css b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.module.css
new file mode 100644
index 000000000..350542e16
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.module.css
@@ -0,0 +1,230 @@
+.layoutGrid {
+ height: 100%;
+ min-height: 0;
+ overflow: visible;
+}
+
+.pageList {
+ min-width: 0;
+ padding-right: 6px;
+}
+
+.railDivider {
+ width: 1px;
+ height: 100%;
+ background: var(--gray-300, #d1d5db);
+ justify-self: center;
+}
+
+.contentColumn {
+ min-width: 0;
+ min-height: 0;
+ padding-left: 20px;
+}
+
+.pageListItems {
+ overflow-y: auto;
+ padding-top: 8px;
+ padding-right: 8px;
+ max-height: 850px;
+ scrollbar-gutter: stable;
+}
+
+.pageButton {
+ width: 100%;
+ text-align: left;
+ border: 0;
+ background: transparent;
+ border-radius: 6px;
+ padding: 8px 10px 8px 6px;
+ cursor: pointer;
+ font: inherit;
+ color: inherit;
+}
+
+.pageMetric {
+ flex: 0 0 88px;
+ text-align: right;
+ font-variant-numeric: tabular-nums;
+}
+
+.pageButton:hover {
+ background: var(--surface-sunken);
+}
+
+.pageButtonSelected,
+.pageButtonSelected:hover {
+ background: var(--surface-sunken);
+ font-weight: 600;
+}
+
+.summaryHeader {
+ min-width: 0;
+}
+
+.summaryPath {
+ display: block;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.summaryStats {
+ flex-wrap: wrap;
+}
+
+.summaryStat {
+ display: inline-flex;
+ align-items: center;
+ padding: 4px 10px;
+ border-radius: 999px;
+ background: var(--surface-raised);
+ border: 1px solid var(--border-base);
+ white-space: nowrap;
+}
+
+.screenWidthControl {
+ flex: 0 0 auto;
+ margin-left: auto;
+}
+
+.screenWidthLabel,
+.screenWidthValue {
+ white-space: nowrap;
+}
+
+.screenWidthValue {
+ min-width: 0;
+}
+
+.screenWidthCount {
+ margin-left: auto;
+ font-variant-numeric: tabular-nums;
+}
+
+.scrollBand {
+ position: absolute;
+ left: 0;
+ right: 0;
+ pointer-events: auto;
+ display: flex;
+ align-items: flex-start;
+ justify-content: flex-end;
+ padding: 2px 8px;
+ mix-blend-mode: multiply;
+}
+
+.scrollBandLabel {
+ font-size: 11px;
+ font-weight: 600;
+ color: rgba(0, 0, 0, 0.7);
+ background: rgba(255, 255, 255, 0.85);
+ border-radius: 4px;
+ padding: 1px 6px;
+ white-space: nowrap;
+ /* Counter-scaled inline via transform: scale(1 / fit.scale). Anchor to the
+ top-right corner (where the band right-aligns it) so it grows inward and
+ stays pinned to the edge instead of drifting off-canvas. */
+ transform-origin: top right;
+}
+
+.canvasWrapper {
+ width: 100%;
+ min-width: 0;
+ max-width: 100%;
+ max-height: 75vh;
+ overflow-x: hidden;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+}
+
+.canvas {
+ position: relative;
+ overflow: hidden;
+ border: 1px solid var(--border-base);
+ border-radius: 8px;
+ background: var(--surface-sunken);
+ flex: 0 0 auto;
+ margin-inline: auto;
+}
+
+.canvasSurface {
+ position: absolute;
+ top: 0;
+ left: 0;
+ transform-origin: top left;
+ border-radius: inherit;
+ background: inherit;
+}
+
+.snapshotClip {
+ position: absolute;
+ inset: 0;
+ overflow: hidden;
+ border-radius: inherit;
+ background: inherit;
+}
+
+.snapshot {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 1;
+ transform-origin: top left;
+ pointer-events: none;
+ overflow: hidden;
+}
+
+.snapshotImage {
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: fill;
+ user-select: none;
+ -webkit-user-drag: none;
+}
+
+.snapshotIframe {
+ display: block;
+ width: 100%;
+ height: 100%;
+ border: 0;
+ background: var(--surface-base);
+ overflow: hidden;
+ pointer-events: none;
+}
+
+.overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 3;
+ pointer-events: none;
+}
+
+.canvasLoading {
+ position: absolute;
+ inset: 0;
+ z-index: 2;
+ background: var(--surface-sunken);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.dot {
+ position: absolute;
+ border-radius: 50%;
+ background: radial-gradient(
+ circle,
+ rgba(255, 60, 0, 0.9) 0%,
+ rgba(255, 200, 0, 0.6) 45%,
+ rgba(255, 200, 0, 0) 80%
+ );
+ pointer-events: auto;
+ mix-blend-mode: multiply;
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx
new file mode 100644
index 000000000..da6392142
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx
@@ -0,0 +1,958 @@
+'use client';
+import {
+ Column,
+ Grid,
+ Heading,
+ Icon,
+ ListItem,
+ Loading,
+ Row,
+ Select,
+ Text,
+} from '@umami/react-zen';
+import { Laptop, Monitor, Smartphone, Tablet } from 'lucide-react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useResultQuery } from '@/components/hooks';
+import { formatLongNumber } from '@/lib/format';
+import type { HeatmapMode, HeatmapPoint, HeatmapResult, HeatmapSnapshot } from '@/queries/sql';
+import styles from './Heatmap.module.css';
+
+const SCROLL_BUCKET_SIZE = 10;
+const SCREEN_WIDTH_BUCKETS = [320, 375, 425, 768, 1024, 1440, 1920] as const;
+
+interface ScreenWidthBucket {
+ width: number;
+ viewportH: number;
+ pageW: number;
+ pageH: number;
+ positions: number;
+ count: number;
+ minViewportW: number;
+ maxViewportW: number;
+}
+
+interface ScreenWidthMetric {
+ pageW: number;
+ pageH: number;
+ viewportW: number;
+ viewportH: number;
+ count: number;
+}
+
+interface ScreenWidthBucketOptions {
+ pageSize?: 'max' | 'weightedAverage';
+}
+
+interface HeatmapProps {
+ websiteId: string;
+ urlPath: string;
+ onUrlPathChange: (urlPath: string) => void;
+ mode: HeatmapMode;
+ search: string;
+}
+
+export function Heatmap({ websiteId, urlPath, onUrlPathChange, mode, search }: HeatmapProps) {
+ const {
+ data: pagesData,
+ error,
+ isLoading,
+ } = useResultQuery('heatmap', {
+ websiteId,
+ mode,
+ });
+
+ const {
+ data: detailData,
+ isLoading: isDetailLoading,
+ isFetching: isDetailFetching,
+ } = useResultQuery(
+ 'heatmap',
+ {
+ websiteId,
+ urlPath: urlPath || undefined,
+ mode,
+ },
+ {
+ enabled: Boolean(urlPath),
+ },
+ );
+
+ const pages = pagesData?.pages ?? [];
+ const filteredPages = useMemo(() => {
+ if (!search) {
+ return pages;
+ }
+
+ const value = search.toLowerCase();
+
+ return pages.filter(page => page.urlPath.toLowerCase().includes(value));
+ }, [pages, search]);
+ const points = detailData?.points ?? [];
+ const scroll = detailData?.scroll;
+ const snapshot = detailData?.snapshot ?? null;
+ const detailLoading = Boolean(urlPath) && (isDetailLoading || isDetailFetching);
+
+ useEffect(() => {
+ if (isLoading) {
+ return;
+ }
+
+ if (filteredPages.length === 0) {
+ if (urlPath) {
+ onUrlPathChange('');
+ }
+ return;
+ }
+
+ if (!urlPath || filteredPages.some(page => page.urlPath === urlPath)) {
+ return;
+ }
+
+ onUrlPathChange(filteredPages[0].urlPath);
+ }, [filteredPages, isLoading, onUrlPathChange, urlPath]);
+
+ if (!isLoading && pages.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {urlPath ? (
+ mode === 'scroll' ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ )}
+
+
+
+ );
+}
+
+function PageList({
+ pages,
+ selected,
+ onSelect,
+ mode,
+ hasSearch,
+}: {
+ pages: HeatmapResult['pages'];
+ selected: string;
+ onSelect: (urlPath: string) => void;
+ mode: HeatmapMode;
+ hasSearch: boolean;
+}) {
+ const getPageMetricTitle = (page: HeatmapResult['pages'][number]) => {
+ const metricLabel = mode === 'scroll' ? 'scroll events' : 'clicks';
+
+ return `${formatLongNumber(page.sessions)} visitors - ${formatLongNumber(page.count)} ${metricLabel}`;
+ };
+
+ return (
+
+ Pages
+
+ {pages.length === 0 && hasSearch && No matching pages}
+ {pages.map(page => (
+
+ ))}
+
+
+ );
+}
+
+function getScreenWidthBucketWidth(viewportW: number) {
+ return SCREEN_WIDTH_BUCKETS.reduce((best, width) => {
+ const bestDistance = Math.abs(viewportW - best);
+ const distance = Math.abs(viewportW - width);
+
+ return distance < bestDistance ? width : best;
+ }, SCREEN_WIDTH_BUCKETS[0]);
+}
+
+function getScreenWidthBuckets(
+ metrics: ScreenWidthMetric[],
+ options: ScreenWidthBucketOptions = {},
+): ScreenWidthBucket[] {
+ if (!metrics.length) {
+ return [];
+ }
+
+ const buckets = new Map<
+ number,
+ ScreenWidthBucket & {
+ weightedPageW: number;
+ weightedPageH: number;
+ weightedViewportH: number;
+ }
+ >();
+ const pageSize = options.pageSize ?? 'max';
+
+ for (const metric of metrics) {
+ const width = getScreenWidthBucketWidth(metric.viewportW);
+ const scale = width / Math.max(1, metric.viewportW);
+ const scaledViewportH = metric.viewportH * scale;
+ const scaledPageW = Math.max(width, metric.pageW * scale);
+ const scaledPageH = Math.max(scaledViewportH, metric.pageH * scale);
+ const existing = buckets.get(width);
+
+ if (existing) {
+ existing.positions += 1;
+ existing.count += metric.count;
+ existing.pageW = Math.max(existing.pageW, scaledPageW);
+ existing.pageH = Math.max(existing.pageH, scaledPageH);
+ existing.weightedPageW += scaledPageW * metric.count;
+ existing.weightedPageH += scaledPageH * metric.count;
+ existing.weightedViewportH += scaledViewportH * metric.count;
+ existing.minViewportW = Math.min(existing.minViewportW, metric.viewportW);
+ existing.maxViewportW = Math.max(existing.maxViewportW, metric.viewportW);
+ continue;
+ }
+
+ buckets.set(width, {
+ width,
+ viewportH: scaledViewportH,
+ pageW: scaledPageW,
+ pageH: scaledPageH,
+ positions: 1,
+ count: metric.count,
+ minViewportW: metric.viewportW,
+ maxViewportW: metric.viewportW,
+ weightedPageW: scaledPageW * metric.count,
+ weightedPageH: scaledPageH * metric.count,
+ weightedViewportH: scaledViewportH * metric.count,
+ });
+ }
+
+ return SCREEN_WIDTH_BUCKETS.map(width => buckets.get(width))
+ .filter(
+ (
+ bucket,
+ ): bucket is ScreenWidthBucket & {
+ weightedPageW: number;
+ weightedPageH: number;
+ weightedViewportH: number;
+ } => Boolean(bucket),
+ )
+ .map(({ weightedPageW, weightedPageH, weightedViewportH, ...bucket }) => ({
+ ...bucket,
+ viewportH: Math.max(1, Math.round(weightedViewportH / Math.max(1, bucket.count))),
+ pageW: Math.max(
+ bucket.width,
+ Math.round(
+ pageSize === 'weightedAverage' ? weightedPageW / Math.max(1, bucket.count) : bucket.pageW,
+ ),
+ ),
+ pageH: Math.max(
+ 640,
+ Math.round(
+ pageSize === 'weightedAverage' ? weightedPageH / Math.max(1, bucket.count) : bucket.pageH,
+ ),
+ ),
+ }));
+}
+
+function getDefaultScreenWidthBucket(buckets: ScreenWidthBucket[]) {
+ return buckets.reduce(
+ (best, bucket) => (!best || bucket.count > best.count ? bucket : best),
+ null,
+ );
+}
+
+function normalizePointToBucket(point: HeatmapPoint, bucket: ScreenWidthBucket): HeatmapPoint {
+ const scale = bucket.width / Math.max(1, point.viewportW);
+ const viewportH = Math.max(1, Math.round(point.viewportH * scale));
+
+ return {
+ ...point,
+ x: point.x * scale,
+ y: point.y * scale,
+ pageX: point.pageX * scale,
+ pageY: point.pageY * scale,
+ pageW: Math.max(bucket.width, point.pageW * scale),
+ pageH: Math.max(viewportH, point.pageH * scale),
+ viewportW: bucket.width,
+ viewportH,
+ };
+}
+
+function getNormalizedBucketPoints(points: HeatmapPoint[], bucket: ScreenWidthBucket) {
+ const groupedPoints = new Map();
+
+ for (const point of points) {
+ if (getScreenWidthBucketWidth(point.viewportW) !== bucket.width) {
+ continue;
+ }
+
+ const normalized = normalizePointToBucket(point, bucket);
+ const pageX = Math.round(normalized.pageX);
+ const pageY = Math.round(normalized.pageY);
+ const key = `${pageX}:${pageY}`;
+ const existing = groupedPoints.get(key);
+
+ if (existing) {
+ existing.count += normalized.count;
+ existing.pageW = Math.max(existing.pageW, normalized.pageW);
+ existing.pageH = Math.max(existing.pageH, normalized.pageH);
+ continue;
+ }
+
+ groupedPoints.set(key, {
+ ...normalized,
+ x: Math.round(normalized.x),
+ y: Math.round(normalized.y),
+ pageX,
+ pageY,
+ });
+ }
+
+ return Array.from(groupedPoints.values());
+}
+
+function useSelectedScreenWidthBucket(screenWidthBuckets: ScreenWidthBucket[]) {
+ const [selectedScreenWidth, setSelectedScreenWidth] = useState(null);
+ const defaultScreenWidth = useMemo(
+ () => getDefaultScreenWidthBucket(screenWidthBuckets)?.width ?? null,
+ [screenWidthBuckets],
+ );
+
+ useEffect(() => {
+ const availableWidths = new Set(screenWidthBuckets.map(bucket => bucket.width));
+
+ setSelectedScreenWidth(current => {
+ if (!defaultScreenWidth) {
+ return null;
+ }
+
+ return current && availableWidths.has(current) ? current : defaultScreenWidth;
+ });
+ }, [defaultScreenWidth, screenWidthBuckets]);
+
+ const viewport = useMemo(() => {
+ const activeWidth = selectedScreenWidth ?? defaultScreenWidth;
+
+ return screenWidthBuckets.find(bucket => bucket.width === activeWidth) ?? null;
+ }, [defaultScreenWidth, screenWidthBuckets, selectedScreenWidth]);
+
+ return { viewport, setSelectedScreenWidth };
+}
+
+function getScrollScreenWidthMetrics(scroll: HeatmapResult['scroll'] | undefined) {
+ return (
+ scroll?.buckets.map(bucket => ({
+ pageW: bucket.pageW,
+ pageH: bucket.pageH,
+ viewportW: bucket.viewportW,
+ viewportH: bucket.viewportH,
+ count: bucket.sessions,
+ })) ?? []
+ );
+}
+
+function getSelectedScrollBuckets(
+ scroll: HeatmapResult['scroll'] | undefined,
+ bucket: ScreenWidthBucket | null,
+) {
+ if (!scroll || !bucket) {
+ return [];
+ }
+
+ const sessionsByDepth = new Map();
+
+ for (const row of scroll.buckets) {
+ if (getScreenWidthBucketWidth(row.viewportW) !== bucket.width) {
+ continue;
+ }
+
+ sessionsByDepth.set(row.depth, (sessionsByDepth.get(row.depth) ?? 0) + row.sessions);
+ }
+
+ return Array.from(sessionsByDepth.entries())
+ .map(([depth, sessions]) => ({ depth, sessions }))
+ .sort((a, b) => a.depth - b.depth);
+}
+
+function useCanvasFit(renderWidth: number, renderHeight: number) {
+ const wrapperRef = useRef(null);
+ const [availableWidth, setAvailableWidth] = useState(0);
+
+ useEffect(() => {
+ const updateAvailableSize = () => {
+ const width = wrapperRef.current?.clientWidth ?? 0;
+
+ setAvailableWidth(current => (current === width ? current : width));
+ };
+
+ updateAvailableSize();
+
+ const resizeObserver =
+ typeof ResizeObserver !== 'undefined' ? new ResizeObserver(updateAvailableSize) : null;
+
+ if (wrapperRef.current && resizeObserver) {
+ resizeObserver.observe(wrapperRef.current);
+ }
+
+ window.addEventListener('resize', updateAvailableSize);
+
+ return () => {
+ resizeObserver?.disconnect();
+ window.removeEventListener('resize', updateAvailableSize);
+ };
+ }, []);
+
+ const safeWidth = Math.max(1, renderWidth);
+ const safeHeight = Math.max(1, renderHeight);
+ const scale = availableWidth ? Math.min(1, availableWidth / safeWidth) : 1;
+
+ return {
+ wrapperRef,
+ scale,
+ width: Math.max(1, Math.round(safeWidth * scale)),
+ height: Math.max(1, Math.round(safeHeight * scale)),
+ };
+}
+
+function ScreenWidthSelect({
+ buckets,
+ value,
+ onChange,
+}: {
+ buckets: ScreenWidthBucket[];
+ value: number | null;
+ onChange: (value: number) => void;
+}) {
+ const bucketsByWidth = useMemo(
+ () => new Map(buckets.map(bucket => [bucket.width, bucket])),
+ [buckets],
+ );
+
+ if (!value || buckets.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+ Screen width:
+
+
+
+ );
+}
+
+function ScreenWidthValue({ width }: { width: number }) {
+ return (
+
+
+ {width} px
+
+ );
+}
+
+function ScreenWidthIcon({ width }: { width: number }) {
+ const DeviceIcon =
+ width < 768 ? Smartphone : width < 1200 ? Tablet : width < 1600 ? Laptop : Monitor;
+
+ return (
+
+
+
+ );
+}
+
+function ClickHeatmapView({
+ urlPath,
+ points,
+ snapshot,
+ isLoading,
+}: {
+ urlPath: string;
+ points: HeatmapPoint[];
+ snapshot: HeatmapSnapshot | null;
+ isLoading: boolean;
+}) {
+ const [snapshotReady, setSnapshotReady] = useState(false);
+ const screenWidthBuckets = useMemo(() => getScreenWidthBuckets(points), [points]);
+ const { viewport, setSelectedScreenWidth } = useSelectedScreenWidthBucket(screenWidthBuckets);
+
+ const visible = useMemo(() => {
+ if (!viewport) {
+ return [];
+ }
+
+ return getNormalizedBucketPoints(points, viewport);
+ }, [points, viewport]);
+
+ const maxCount = useMemo(
+ () => visible.reduce((max, point) => (point.count > max ? point.count : max), 1),
+ [visible],
+ );
+
+ const handleSnapshotReady = useCallback(() => setSnapshotReady(true), []);
+ const hasSnapshot = Boolean(snapshot);
+
+ useEffect(() => {
+ setSnapshotReady(!hasSnapshot);
+ }, [hasSnapshot, snapshot?.id]);
+ const overlayGutter = Math.max(48, Math.round((viewport?.width ?? 1920) * 0.04));
+ const maxPointX = visible.reduce((max, point) => Math.max(max, point.pageX), 0);
+ const snapshotHeight = snapshot ? getSnapshotFrameHeight(snapshot) : 0;
+ // Keep the canvas sized to real content and clip outlier clicks instead of stretching it.
+ const baseWidth = Math.max(viewport?.pageW ?? 0, maxPointX + overlayGutter, 1);
+ const renderWidth = viewport?.width ?? snapshot?.viewportW ?? baseWidth;
+ // Match the canvas height to the snapshot height we actually render.
+ const contentHeight = snapshotHeight || viewport?.pageH || 0;
+ const renderHeight = Math.max(contentHeight, 640);
+ const hasMeasuredWidth = Boolean(viewport?.width || snapshot?.viewportW || maxPointX);
+ const fit = useCanvasFit(renderWidth, renderHeight);
+ const canvasWidth = hasMeasuredWidth ? `${fit.width}px` : '100%';
+ const canvasHeight = hasMeasuredWidth ? `${fit.height}px` : undefined;
+ const overlayPageW = renderWidth;
+ const shouldRenderSnapshot = renderWidth > 0 && hasSnapshot;
+ const showOverlay = !shouldRenderSnapshot || snapshotReady;
+ const totalClicks = visible.reduce((sum, point) => sum + point.count, 0);
+ const bucketDescription = viewport
+ ? viewport.minViewportW === viewport.maxViewportW
+ ? `Recorded at ${viewport.minViewportW}px wide`
+ : `Grouped recorded widths from ${viewport.minViewportW}px to ${viewport.maxViewportW}px`
+ : undefined;
+ const showLoading = isLoading;
+
+ return (
+
+
+
+
+ {urlPath}
+
+
+ {showLoading ? (
+
+
+ Loading Heatmap...
+
+
+ ) : (
+
+
+ {viewport
+ ? `${visible.length} positions - ${formatLongNumber(totalClicks)} clicks`
+ : 'No click data for this page yet.'}
+
+
+
+ )}
+
+
+
+
+ {showLoading ? (
+
+ ) : !viewport || visible.length === 0 ? (
+
+ ) : (
+
+
+ {shouldRenderSnapshot && !snapshotReady && }
+ {shouldRenderSnapshot && snapshot && (
+
+ )}
+
+ {showOverlay && (
+
+ {visible.map((point, index) => {
+ const intensity = Math.min(1, point.count / maxCount);
+ const desiredSize = 24 + intensity * 36;
+ const size = desiredSize;
+ const centerX = Math.max(0, Math.min(overlayPageW, point.pageX));
+ // Don't clamp to the canvas height: points below the real
+ // content overflow and are clipped by the canvas instead of
+ // piling up on the bottom edge.
+ const centerY = Math.max(0, point.pageY);
+
+ return (
+
+ );
+ })}
+
+ )}
+
+ )}
+
+
+
+ );
+}
+
+function ScrollHeatmapView({
+ urlPath,
+ scroll,
+ snapshot,
+ isLoading,
+}: {
+ urlPath: string;
+ scroll: HeatmapResult['scroll'] | undefined;
+ snapshot: HeatmapSnapshot | null;
+ isLoading: boolean;
+}) {
+ const [snapshotReady, setSnapshotReady] = useState(false);
+ const handleSnapshotReady = useCallback(() => setSnapshotReady(true), []);
+ const hasSnapshot = Boolean(snapshot);
+
+ useEffect(() => {
+ setSnapshotReady(!hasSnapshot);
+ }, [hasSnapshot, snapshot?.id]);
+ const scrollMetrics = useMemo(() => getScrollScreenWidthMetrics(scroll), [scroll]);
+ const screenWidthBuckets = useMemo(
+ () => getScreenWidthBuckets(scrollMetrics, { pageSize: 'weightedAverage' }),
+ [scrollMetrics],
+ );
+ const { viewport, setSelectedScreenWidth } = useSelectedScreenWidthBucket(screenWidthBuckets);
+ const selectedBuckets = useMemo(
+ () => getSelectedScrollBuckets(scroll, viewport),
+ [scroll, viewport],
+ );
+ const totalSessions = viewport?.count ?? 0;
+ const pageW = viewport?.pageW ?? scroll?.pageW ?? 0;
+ const pageH = viewport?.pageH ?? scroll?.pageH ?? 0;
+ const viewportW = viewport?.width ?? scroll?.viewportW ?? 0;
+ const viewportH = viewport?.viewportH ?? scroll?.viewportH ?? 0;
+ const snapshotHeight = snapshot ? getSnapshotFrameHeight(snapshot) : 0;
+ const baseWidth = Math.max(pageW, 1);
+ const baseHeight = Math.max(snapshotHeight || pageH, 640);
+ const renderWidth = viewport?.width ?? snapshot?.viewportW ?? viewportW ?? baseWidth;
+ const renderHeight = baseHeight;
+ const hasMeasuredWidth = Boolean(viewport?.width || snapshot?.viewportW || viewportW || pageW);
+ const fit = useCanvasFit(renderWidth, renderHeight);
+ const canvasWidth = hasMeasuredWidth ? `${fit.width}px` : '100%';
+ const canvasHeight = hasMeasuredWidth ? `${fit.height}px` : undefined;
+ const shouldRenderSnapshot = renderWidth > 0 && hasSnapshot;
+ const showOverlay = !shouldRenderSnapshot || snapshotReady;
+ const hasScrollData = Boolean(
+ viewport && selectedBuckets.length > 0 && totalSessions > 0 && pageW && pageH && viewportW,
+ );
+ const showLoading = isLoading;
+ const bucketDescription = viewport
+ ? viewport.minViewportW === viewport.maxViewportW
+ ? `Recorded at ${viewport.minViewportW}px wide`
+ : `Grouped recorded widths from ${viewport.minViewportW}px to ${viewport.maxViewportW}px`
+ : undefined;
+
+ type Band = { fromPct: number; toPct: number; reached: number; ratio: number };
+ const bands: Band[] = [];
+ const sessionsByDepth = new Map(selectedBuckets.map(bucket => [bucket.depth, bucket.sessions]));
+ let dropped = 0;
+
+ for (let depth = 0; depth < 100; depth += SCROLL_BUCKET_SIZE) {
+ const reached = Math.max(0, totalSessions - dropped);
+ dropped += sessionsByDepth.get(depth) ?? 0;
+ const nextReached = Math.max(0, totalSessions - dropped);
+ const ratio = totalSessions ? nextReached / totalSessions : 0;
+
+ if (reached > 0) {
+ bands.push({
+ fromPct: depth,
+ toPct: Math.min(100, depth + SCROLL_BUCKET_SIZE),
+ reached: nextReached,
+ ratio,
+ });
+ }
+ }
+
+ return (
+
+
+ {urlPath}
+
+ {showLoading ? (
+
+
+ Loading Heatmap...
+
+
+ ) : (
+
+
+ {hasScrollData
+ ? `${formatLongNumber(totalSessions)} sessions - page ${pageW}x${pageH}${viewportH ? ` - viewport ${viewportW}x${viewportH}` : ''}`
+ : 'No scroll data for this page yet.'}
+
+
+
+ )}
+
+
+
+ {showLoading ? (
+
+ ) : !hasScrollData ? (
+
+ ) : (
+
+ {shouldRenderSnapshot && !snapshotReady &&
}
+ {shouldRenderSnapshot && snapshot && (
+
+ )}
+ {showOverlay && (
+
+ {bands.map(band => {
+ const intensity = band.ratio;
+ const hue = Math.round(60 - intensity * 60);
+
+ return (
+
0
+ ? `hsla(${hue}, 90%, 55%, ${0.12 + intensity * 0.45})`
+ : 'none',
+ }}
+ title={`${band.toPct}% depth - ${formatLongNumber(band.reached)} sessions reached`}
+ >
+
+ {band.toPct}% depth - {Math.round(intensity * 100)}% reached
+
+
+ );
+ })}
+
+ )}
+
+ )}
+
+
+
+ );
+}
+
+function getSnapshotFrameHeight(snapshot: HeatmapSnapshot) {
+ const { pageH, viewportH } = snapshot;
+
+ // Use the recorded viewport height for near-single-screen pages so `100vh` matches the visitor's screen.
+ if (pageH <= viewportH * 1.25) {
+ return viewportH;
+ }
+
+ return pageH;
+}
+
+function SnapshotPreview({
+ snapshot,
+ onReady,
+}: {
+ snapshot: HeatmapSnapshot;
+ onReady: () => void;
+}) {
+ return ;
+}
+
+function IframeSnapshot({
+ snapshot,
+ onReady,
+}: {
+ snapshot: HeatmapSnapshot;
+ onReady: () => void;
+}) {
+ const [available, setAvailable] = useState(true);
+ const iframeUrl = snapshot.url;
+ const frameHeight = getSnapshotFrameHeight(snapshot);
+
+ useEffect(() => {
+ setAvailable(true);
+
+ const readyTimer = window.setTimeout(() => onReady(), 1500);
+
+ return () => window.clearTimeout(readyTimer);
+ }, [onReady, snapshot.id]);
+
+ const handleLoad = useCallback(() => onReady(), [onReady]);
+ const handleError = useCallback(() => {
+ setAvailable(false);
+ onReady();
+ }, [onReady]);
+
+ if (!available) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
+
+function CanvasLoading() {
+ return (
+
+
+
+ );
+}
+
+function EmptyState({ message }: { message?: string } = {}) {
+ return (
+
+ {!message && Select a page}
+ {message ?? 'Choose a page from the list to view its heatmap.'}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/HeatmapsPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/HeatmapsPage.tsx
new file mode 100644
index 000000000..c3b968b46
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/HeatmapsPage.tsx
@@ -0,0 +1,100 @@
+'use client';
+import { Button, Column, Row, SearchField } from '@umami/react-zen';
+import { useState } from 'react';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder';
+import { Panel } from '@/components/common/Panel';
+import { useMessages, useMobile, useSubscription, useWebsite } from '@/components/hooks';
+import { Flame } from '@/components/icons';
+import { FilterButtons } from '@/components/input/FilterButtons';
+import type { HeatmapMode } from '@/queries/sql';
+import { Heatmap } from './Heatmap';
+import styles from './Heatmap.module.css';
+
+export function HeatmapsPage({ websiteId }: { websiteId: string }) {
+ const [urlPathByMode, setUrlPathByMode] = useState>({
+ click: '',
+ scroll: '',
+ });
+ const [mode, setMode] = useState('click');
+ const [search, setSearch] = useState('');
+ const { isPhone } = useMobile();
+ const website = useWebsite();
+ const { t, labels, messages } = useMessages();
+ const { hasFeature, cloudMode } = useSubscription(website?.teamId);
+
+ const buttons = [
+ { id: 'click', label: 'Clicks' },
+ { id: 'scroll', label: 'Scroll' },
+ ];
+
+ if (cloudMode && !hasFeature('replays')) {
+ return (
+
+
+ }
+ title={t(messages.upgradeRequired, { plan: 'Business' })}
+ description="View click and scroll heatmaps for your pages."
+ >
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {isPhone ? (
+
+
+
+
+
+ setMode(value as HeatmapMode)}
+ />
+
+
+ ) : (
+
+
+ setMode(value as HeatmapMode)}
+ />
+
+ )}
+
+
+ setUrlPathByMode(state => ({ ...state, [mode]: urlPath }))}
+ mode={mode}
+ search={search}
+ />
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/page.tsx b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/page.tsx
new file mode 100644
index 000000000..d980279ce
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { HeatmapsPage } from './HeatmapsPage';
+
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
+ const { websiteId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Heatmaps',
+};
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/retention/Retention.tsx b/src/app/(main)/websites/[websiteId]/(reports)/retention/Retention.tsx
index 89b14080b..127618ad9 100644
--- a/src/app/(main)/websites/[websiteId]/(reports)/retention/Retention.tsx
+++ b/src/app/(main)/websites/[websiteId]/(reports)/retention/Retention.tsx
@@ -1,6 +1,6 @@
import { Column, Grid, Icon, Row, Text } from '@umami/react-zen';
-import type { ReactNode } from 'react';
import { differenceInCalendarDays } from 'date-fns';
+import type { ReactNode } from 'react';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Panel } from '@/components/common/Panel';
import { useLocale, useMessages, useResultQuery } from '@/components/hooks';
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/revenue/BoardRevenueChart.tsx b/src/app/(main)/websites/[websiteId]/(reports)/revenue/BoardRevenueChart.tsx
new file mode 100644
index 000000000..f5febf290
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/revenue/BoardRevenueChart.tsx
@@ -0,0 +1,34 @@
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useDateRange, useTimezone } from '@/components/hooks';
+import { useRevenueChartQuery } from '@/components/hooks/queries/useRevenueChartQuery';
+import { RevenueChart } from './RevenueChart';
+import { useBoardRevenueCurrency } from './useBoardRevenueCurrency';
+
+export function BoardRevenueChart({
+ websiteId,
+ currency: selectedCurrency,
+}: {
+ websiteId: string;
+ currency?: string;
+}) {
+ const currency = useBoardRevenueCurrency(selectedCurrency);
+ const { timezone } = useTimezone();
+ const {
+ dateRange: { startDate, endDate, unit },
+ } = useDateRange({ timezone });
+ const { data, isLoading, isFetching, error } = useRevenueChartQuery(websiteId, currency);
+
+ return (
+
+ {data && (
+
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/revenue/BoardRevenueMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/(reports)/revenue/BoardRevenueMetricsBar.tsx
new file mode 100644
index 000000000..e10d79cde
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/revenue/BoardRevenueMetricsBar.tsx
@@ -0,0 +1,27 @@
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useDateRange } from '@/components/hooks';
+import { useRevenueStatsQuery } from '@/components/hooks/queries/useRevenueStatsQuery';
+import { RevenueMetricsBar } from './RevenueMetricsBar';
+import { useBoardRevenueCurrency } from './useBoardRevenueCurrency';
+
+export function BoardRevenueMetricsBar({
+ websiteId,
+ currency: selectedCurrency,
+}: {
+ websiteId: string;
+ currency?: string;
+}) {
+ const currency = useBoardRevenueCurrency(selectedCurrency);
+ const { compare } = useDateRange();
+ const { data, isLoading, isFetching, error } = useRevenueStatsQuery({
+ websiteId,
+ currency,
+ compare,
+ });
+
+ return (
+
+ {data && }
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/revenue/BoardRevenueMetricsTable.tsx b/src/app/(main)/websites/[websiteId]/(reports)/revenue/BoardRevenueMetricsTable.tsx
new file mode 100644
index 000000000..20a95c191
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/revenue/BoardRevenueMetricsTable.tsx
@@ -0,0 +1,53 @@
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useDateRange, useMessages } from '@/components/hooks';
+import type { RevenueMetricType } from '@/components/hooks/queries/useRevenueMetricsQuery';
+import { useRevenueStatsQuery } from '@/components/hooks/queries/useRevenueStatsQuery';
+import { RevenueMetricsTable } from './RevenueMetricsTable';
+import { useBoardRevenueCurrency } from './useBoardRevenueCurrency';
+
+function getRevenueTableTitle(type: RevenueMetricType, labels: Record, t: (key: string) => string) {
+ switch (type) {
+ case 'referrer':
+ return t(labels.referrer);
+ case 'channel':
+ return t(labels.channel);
+ case 'country':
+ return t(labels.country);
+ case 'region':
+ return t(labels.region);
+ }
+}
+
+export function BoardRevenueMetricsTable({
+ websiteId,
+ type = 'referrer',
+ currency: selectedCurrency,
+}: {
+ websiteId: string;
+ type?: RevenueMetricType;
+ currency?: string;
+}) {
+ const currency = useBoardRevenueCurrency(selectedCurrency);
+ const { compare } = useDateRange();
+ const { t, labels } = useMessages();
+ const { data, isLoading, isFetching, error } = useRevenueStatsQuery({
+ websiteId,
+ currency,
+ compare,
+ });
+
+ return (
+
+ {data && (
+
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/revenue/Revenue.tsx b/src/app/(main)/websites/[websiteId]/(reports)/revenue/Revenue.tsx
index 6dce11d8c..112eedf55 100644
--- a/src/app/(main)/websites/[websiteId]/(reports)/revenue/Revenue.tsx
+++ b/src/app/(main)/websites/[websiteId]/(reports)/revenue/Revenue.tsx
@@ -1,6 +1,16 @@
-import { Column, Grid, Heading, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
+import {
+ Column,
+ Grid,
+ Heading,
+ ListItem,
+ Row,
+ Select,
+ Tab,
+ TabList,
+ TabPanel,
+ Tabs,
+} from '@umami/react-zen';
import { useMemo, useState } from 'react';
-import { DataGrid } from '@/components/common/DataGrid';
import { GridRow } from '@/components/common/GridRow';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Panel } from '@/components/common/Panel';
@@ -8,43 +18,21 @@ import {
useDateRange,
useMessages,
useNavigation,
- useResultQuery,
- useRevenueSessionsQuery,
+ useRevenueChartQuery,
+ useRevenueStatsQuery,
} from '@/components/hooks';
import { CurrencySelect } from '@/components/input/CurrencySelect';
-import { ListTable } from '@/components/metrics/ListTable';
-import { MetricCard } from '@/components/metrics/MetricCard';
-import { MetricLabel } from '@/components/metrics/MetricLabel';
-import { MetricsBar } from '@/components/metrics/MetricsBar';
-import { RevenueChart } from '@/components/metrics/RevenueChart';
import { CURRENCY_CONFIG, DEFAULT_CURRENCY } from '@/lib/constants';
-import { formatLongCurrency, formatLongNumber } from '@/lib/format';
import { getItem, setItem } from '@/lib/storage';
import { SessionModal } from '../../sessions/SessionModal';
-import { SessionsTable } from '../../sessions/SessionsTable';
+import { RevenueChart } from './RevenueChart';
+import { RevenueMetricsBar } from './RevenueMetricsBar';
+import { RevenueMetricsTable } from './RevenueMetricsTable';
+import { RevenueSessionsDataTable } from './RevenueSessionsDataTable';
-function RevenueSessionsDataTable({
- websiteId,
- currency,
-}: {
- websiteId: string;
- currency: string;
-}) {
- const { updateParams } = useNavigation();
- const queryResult = useRevenueSessionsQuery(websiteId, currency);
-
- return (
-
- {({ data }) => (
- updateParams({ session: row.id })}
- />
- )}
-
- );
-}
+type RevenueChartMode = 'period' | 'cumulative';
+type SourceTab = 'referrer' | 'channel';
+type LocationTab = 'country' | 'region';
export interface RevenueProps {
websiteId: string;
@@ -54,104 +42,67 @@ export interface RevenueProps {
}
export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
+ const [sourceTab, setSourceTab] = useState('referrer');
+ const [locationTab, setLocationTab] = useState('country');
const [currency, setCurrency] = useState(
getItem(CURRENCY_CONFIG) || process.env.defaultCurrency || DEFAULT_CURRENCY,
);
+ const {
+ router,
+ query: { revenueChart },
+ updateParams,
+ } = useNavigation();
+ const revenueChartMode: RevenueChartMode =
+ revenueChart === 'cumulative' ? 'cumulative' : 'period';
const handleCurrencyChange = (value: string) => {
setCurrency(value);
setItem(CURRENCY_CONFIG, value);
};
+ const handleRevenueChartModeChange = (value: RevenueChartMode) => {
+ router.push(updateParams({ revenueChart: value }));
+ };
+
const { t, labels } = useMessages();
- const { compare, isAllTime } = useDateRange();
- const { data, error, isLoading } = useResultQuery('revenue', {
- websiteId,
- startDate,
- endDate,
- currency,
- compare,
- });
-
- const metrics = useMemo(() => {
- if (!data) return [];
-
- const { sum, count, average, unique_count, arpu, comparison } = data.total;
-
- return [
- {
- value: sum,
- label: t(labels.total),
- change: comparison ? sum - comparison.sum : 0,
- formatValue: (n: number) => formatLongCurrency(n, currency),
- },
- {
- value: average,
- label: t(labels.aov),
- tooltip: (
- <>
- Average Order Value
- (Total Revenue / Orders)
- >
- ),
- change: comparison ? average - comparison.average : 0,
- formatValue: (n: number) => formatLongCurrency(n, currency),
- },
- {
- value: arpu,
- label: t(labels.arpu),
- tooltip: (
- <>
- Average Revenue Per User
- (Total Revenue / All Sessions)
- >
- ),
- change: comparison ? arpu - (comparison.arpu ?? 0) : 0,
- formatValue: (n: number) => formatLongCurrency(n, currency),
- },
- {
- value: count,
- label: t(labels.orders),
- change: comparison ? count - comparison.count : 0,
- formatValue: formatLongNumber,
- },
- {
- value: unique_count,
- label: t(labels.uniqueCustomers),
- change: comparison ? unique_count - comparison.unique_count : 0,
- formatValue: formatLongNumber,
- },
- ] as any;
- }, [data]);
-
- const renderLabel = (type: string) => (data: any) => ;
+ const { compare } = useDateRange();
+ const chartQuery = useRevenueChartQuery(websiteId, currency);
+ const statsQuery = useRevenueStatsQuery({ websiteId, currency, compare });
+ const data = useMemo(
+ () =>
+ chartQuery.data && statsQuery.data
+ ? { chart: chartQuery.data.chart, total: statsQuery.data }
+ : null,
+ [chartQuery.data, statsQuery.data],
+ );
+ const error = chartQuery.error || statsQuery.error;
+ const isFetching = chartQuery.isFetching || statsQuery.isFetching;
+ const isLoading = chartQuery.isLoading || statsQuery.isLoading;
return (
-
+
{data && (
-
- {metrics?.map(({ label, value, change, formatValue, tooltip }) => {
- return (
-
- );
- })}
-
+
+
+
+
{t(labels.sources)}
-
+ setSourceTab(String(key) as SourceTab)}
+ >
{t(labels.referrers)}
{t(labels.channels)}
- ({
- label: name,
- count: Number(value),
- percent: (value / data?.total.sum) * 100,
- }),
- )}
- formatCount={(n: number) => formatLongCurrency(n, currency)}
- renderLabel={renderLabel('referrer')}
+ totalRevenue={Number(data?.total.sum) || 0}
+ enabled={sourceTab === 'referrer'}
/>
- ({
- label: name,
- count: Number(value),
- percent: (value / data?.total.sum) * 100,
- }),
- )}
- formatCount={(n: number) => formatLongCurrency(n, currency)}
- renderLabel={renderLabel('channel')}
+ totalRevenue={Number(data?.total.sum) || 0}
+ enabled={sourceTab === 'channel'}
/>
{t(labels.location)}
-
+ setLocationTab(String(key) as LocationTab)}
+ >
{t(labels.countries)}
{t(labels.regions)}
- ({
- label: name,
- count: Number(value),
- percent: (value / data?.total.sum) * 100,
- }),
- )}
- formatCount={(n: number) => formatLongCurrency(n, currency)}
- renderLabel={renderLabel('country')}
+ totalRevenue={Number(data?.total.sum) || 0}
+ enabled={locationTab === 'country'}
/>
- ({
- label: name,
- country,
- count: Number(value),
- percent: (value / data?.total.sum) * 100,
- }),
- )}
- formatCount={(n: number) => formatLongCurrency(n, currency)}
- renderLabel={renderLabel('region')}
+ totalRevenue={Number(data?.total.sum) || 0}
+ enabled={locationTab === 'region'}
/>
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenueChart.tsx b/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenueChart.tsx
new file mode 100644
index 000000000..7b743a36b
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenueChart.tsx
@@ -0,0 +1,158 @@
+import { colord } from 'colord';
+import { useCallback, useMemo } from 'react';
+import { BarChart } from '@/components/charts/BarChart';
+import { useLocale, useMessages } from '@/components/hooks';
+import { renderDateLabels } from '@/lib/charts';
+import { CHART_COLORS } from '@/lib/constants';
+import { DATE_FORMATS, DATE_FUNCTIONS, formatDate, generateTimeSeries } from '@/lib/date';
+
+export interface RevenueChartProps {
+ data: { x: string; t: string; y: number; count: number }[];
+ mode?: 'period' | 'cumulative';
+ unit: string;
+ minDate: Date;
+ maxDate: Date;
+ currency: string;
+}
+
+export function RevenueChart({
+ data,
+ mode = 'period',
+ unit,
+ minDate,
+ maxDate,
+ currency,
+}: RevenueChartProps) {
+ const { t, labels } = useMessages();
+ const { locale, dateLocale } = useLocale();
+ const isCumulative = mode === 'cumulative';
+
+ const chartData: any = useMemo(() => {
+ if (!data?.length) return { datasets: [] };
+
+ if (isCumulative) {
+ const cumulativeMaxDate = maxDate > new Date() ? new Date() : maxDate;
+ const cutoff = formatDate(cumulativeMaxDate, DATE_FORMATS[unit], dateLocale);
+ const bucketDates = getBucketDates(minDate, maxDate, unit, dateLocale);
+ const totals = data.reduce(
+ (obj, { t, y }) => {
+ obj[t] = (obj[t] || 0) + y;
+ return obj;
+ },
+ {} as Record,
+ );
+
+ let total = 0;
+ const timeseries = generateTimeSeries(
+ Object.entries(totals).map(([x, y]) => ({ x, y })),
+ minDate,
+ maxDate,
+ unit,
+ dateLocale,
+ ).map(point => {
+ const d = point.d ?? bucketDates.get(point.x);
+
+ if (point.x > cutoff) {
+ return {
+ ...point,
+ d,
+ y: null,
+ };
+ }
+
+ total += point.y || 0;
+
+ return {
+ ...point,
+ d,
+ y: total,
+ };
+ });
+
+ const color = colord(CHART_COLORS[0]);
+
+ return {
+ datasets: [
+ {
+ type: 'line',
+ label: t(labels.revenue),
+ data: timeseries,
+ backgroundColor: (context: any) => {
+ const { chart } = context;
+ const { ctx, chartArea } = chart;
+
+ if (!chartArea) {
+ return color.alpha(0.18).toRgbString();
+ }
+
+ const gradient = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
+ gradient.addColorStop(0, color.alpha(0.22).toRgbString());
+ gradient.addColorStop(1, color.alpha(0.02).toRgbString());
+
+ return gradient;
+ },
+ borderColor: color.alpha(0.95).toRgbString(),
+ borderWidth: 2,
+ pointRadius: 2,
+ tension: 0.25,
+ fill: true,
+ },
+ ],
+ };
+ }
+
+ const map = data.reduce(
+ (obj, { x, t, y }) => {
+ if (!obj[x]) obj[x] = [];
+ obj[x].push({ x: t, y });
+ return obj;
+ },
+ {} as Record,
+ );
+
+ return {
+ datasets: Object.keys(map).map((key, index) => {
+ const color = colord(CHART_COLORS[index % CHART_COLORS.length]);
+ return {
+ label: key,
+ data: generateTimeSeries(map[key], minDate, maxDate, unit, dateLocale),
+ backgroundColor: color.alpha(0.6).toRgbString(),
+ borderColor: color.alpha(0.7).toRgbString(),
+ borderWidth: 1,
+ };
+ }),
+ };
+ }, [data, minDate, maxDate, unit, dateLocale, isCumulative, t, labels.revenue]);
+
+ const renderXLabel = useCallback(renderDateLabels(unit, locale), [unit, locale]);
+
+ return (
+
+ );
+}
+
+function getBucketDates(minDate: Date, maxDate: Date, unit: string, locale: string) {
+ const add = DATE_FUNCTIONS[unit].add;
+ const start = DATE_FUNCTIONS[unit].start;
+ const fmt = DATE_FORMATS[unit];
+ const dates = new Map();
+
+ let current = start(minDate);
+ const end = start(maxDate);
+
+ while (current <= end) {
+ dates.set(formatDate(current, fmt, locale), current);
+ current = add(current, 1);
+ }
+
+ return dates;
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenueMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenueMetricsBar.tsx
new file mode 100644
index 000000000..376e8649f
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenueMetricsBar.tsx
@@ -0,0 +1,88 @@
+import type { ReactNode } from 'react';
+import { useDateRange, useMessages } from '@/components/hooks';
+import type { RevenueStatsData } from '@/components/hooks/queries/useRevenueStatsQuery';
+import { MetricCard } from '@/components/metrics/MetricCard';
+import { MetricsBar } from '@/components/metrics/MetricsBar';
+import { formatLongCurrency, formatLongNumber } from '@/lib/format';
+
+export interface RevenueMetricsBarProps {
+ data: RevenueStatsData;
+ currency: string;
+}
+
+interface RevenueMetricItem {
+ value: number;
+ label: string;
+ change: number;
+ formatValue: (n: number) => string;
+ tooltip?: ReactNode;
+}
+
+export function RevenueMetricsBar({ data, currency }: RevenueMetricsBarProps) {
+ const { t, labels } = useMessages();
+ const { isAllTime } = useDateRange();
+ const { sum, count, average, unique_count, arpu, comparison } = data;
+
+ const metrics: RevenueMetricItem[] = [
+ {
+ value: sum,
+ label: t(labels.total),
+ change: comparison ? sum - comparison.sum : 0,
+ formatValue: (n: number) => formatLongCurrency(n, currency),
+ },
+ {
+ value: average,
+ label: t(labels.aov),
+ tooltip: (
+ <>
+ Average Order Value
+ (Total Revenue / Orders)
+ >
+ ),
+ change: comparison ? average - comparison.average : 0,
+ formatValue: (n: number) => formatLongCurrency(n, currency),
+ },
+ {
+ value: arpu,
+ label: t(labels.arpu),
+ tooltip: (
+ <>
+ Average Revenue Per User
+ (Total Revenue / All Sessions)
+ >
+ ),
+ change: comparison ? arpu - (comparison.arpu ?? 0) : 0,
+ formatValue: (n: number) => formatLongCurrency(n, currency),
+ },
+ {
+ value: count,
+ label: t(labels.orders),
+ change: comparison ? count - comparison.count : 0,
+ formatValue: formatLongNumber,
+ },
+ {
+ value: unique_count,
+ label: t(labels.uniqueCustomers),
+ change: comparison ? unique_count - comparison.unique_count : 0,
+ formatValue: formatLongNumber,
+ },
+ ];
+
+ return (
+
+ {metrics.map(({ label, value, change, formatValue, tooltip }) => {
+ return (
+
+ );
+ })}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenueMetricsTable.tsx b/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenueMetricsTable.tsx
new file mode 100644
index 000000000..f593ec536
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenueMetricsTable.tsx
@@ -0,0 +1,69 @@
+import { Grid } from '@umami/react-zen';
+import { useMemo } from 'react';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { type RevenueMetricType, useMessages, useRevenueMetricsQuery } from '@/components/hooks';
+import { ListTable } from '@/components/metrics/ListTable';
+import { MetricLabel } from '@/components/metrics/MetricLabel';
+import { formatLongCurrency } from '@/lib/format';
+
+const MAX_ROWS = 10;
+
+export interface RevenueMetricsTableProps {
+ websiteId: string;
+ currency: string;
+ type: RevenueMetricType;
+ title: string;
+ totalRevenue: number;
+ enabled: boolean;
+}
+
+export function RevenueMetricsTable({
+ websiteId,
+ currency,
+ type,
+ title,
+ totalRevenue,
+ enabled,
+}: RevenueMetricsTableProps) {
+ const { t, labels } = useMessages();
+ const { data, isLoading, isFetching, error } = useRevenueMetricsQuery(
+ websiteId,
+ { type, currency },
+ { enabled },
+ );
+
+ const tableData = useMemo(
+ () =>
+ data
+ ?.map(({ name, value, country }) => ({
+ label: name,
+ country,
+ count: Number(value),
+ percent: totalRevenue > 0 ? (value / totalRevenue) * 100 : 0,
+ }))
+ .slice(0, MAX_ROWS) || [],
+ [data, totalRevenue],
+ );
+
+ return (
+
+
+ {data && (
+ formatLongCurrency(n, currency)}
+ renderLabel={(row: any) => }
+ />
+ )}
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenueSessionsDataTable.tsx b/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenueSessionsDataTable.tsx
new file mode 100644
index 000000000..f3120cc1a
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenueSessionsDataTable.tsx
@@ -0,0 +1,25 @@
+import { DataGrid } from '@/components/common/DataGrid';
+import { useNavigation, useRevenueSessionsQuery } from '@/components/hooks';
+import { SessionsTable } from '../../sessions/SessionsTable';
+
+export interface RevenueSessionsDataTableProps {
+ websiteId: string;
+ currency: string;
+}
+
+export function RevenueSessionsDataTable({ websiteId, currency }: RevenueSessionsDataTableProps) {
+ const { updateParams } = useNavigation();
+ const queryResult = useRevenueSessionsQuery(websiteId, currency);
+
+ return (
+
+ {({ data }) => (
+ updateParams({ session: row.id })}
+ />
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/revenue/useBoardRevenueCurrency.ts b/src/app/(main)/websites/[websiteId]/(reports)/revenue/useBoardRevenueCurrency.ts
new file mode 100644
index 000000000..a4a50c4f7
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/revenue/useBoardRevenueCurrency.ts
@@ -0,0 +1,6 @@
+import { CURRENCY_CONFIG, DEFAULT_CURRENCY } from '@/lib/constants';
+import { getItem } from '@/lib/storage';
+
+export function useBoardRevenueCurrency(currency?: string) {
+ return currency || getItem(CURRENCY_CONFIG) || process.env.defaultCurrency || DEFAULT_CURRENCY;
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/utm/BoardUTM.tsx b/src/app/(main)/websites/[websiteId]/(reports)/utm/BoardUTM.tsx
new file mode 100644
index 000000000..5444e5e71
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/utm/BoardUTM.tsx
@@ -0,0 +1,112 @@
+import { Column, Heading, Text } from '@umami/react-zen';
+import { useEffect, useRef, useState } from 'react';
+import { PieChart } from '@/components/charts/PieChart';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useDateRange, useMessages, useResultQuery } from '@/components/hooks';
+import { ListTable } from '@/components/metrics/ListTable';
+import { CHART_COLORS } from '@/lib/constants';
+
+const STACKED_LAYOUT_WIDTH = 900;
+
+export function BoardUTM({
+ websiteId,
+ param = 'utm_source',
+ limit = 10,
+}: {
+ websiteId: string;
+ param?: string;
+ limit?: number | string;
+}) {
+ const wrapperRef = useRef(null);
+ const [isStacked, setIsStacked] = useState(false);
+ const {
+ dateRange: { startDate, endDate },
+ } = useDateRange();
+ const { t, labels } = useMessages();
+ const { data, error, isLoading } = useResultQuery('utm', {
+ websiteId,
+ startDate,
+ endDate,
+ });
+
+ const itemLimit = Number(limit) || 10;
+ const items = (data?.[param] ?? []).slice(0, itemLimit);
+ const total = items.reduce((sum, { views }) => {
+ return +sum + +views;
+ }, 0);
+ const chartData = {
+ labels: items.map(({ utm }) => utm),
+ datasets: [
+ {
+ data: items.map(({ views }) => views),
+ backgroundColor: CHART_COLORS,
+ borderWidth: 0,
+ },
+ ],
+ };
+
+ useEffect(() => {
+ const updateLayout = () => {
+ const width = wrapperRef.current?.clientWidth ?? 0;
+
+ setIsStacked(current => {
+ const next = width > 0 && width < STACKED_LAYOUT_WIDTH;
+ return current === next ? current : next;
+ });
+ };
+
+ updateLayout();
+
+ const resizeObserver =
+ typeof ResizeObserver !== 'undefined' ? new ResizeObserver(updateLayout) : null;
+
+ if (wrapperRef.current && resizeObserver) {
+ resizeObserver.observe(wrapperRef.current);
+ }
+
+ window.addEventListener('resize', updateLayout);
+
+ return () => {
+ resizeObserver?.disconnect();
+ window.removeEventListener('resize', updateLayout);
+ };
+ }, []);
+
+ return (
+
+
+
+
+
+ {param.replace(/^utm_/, '')}
+
+ ({
+ label: utm,
+ count: views,
+ percent: total ? (views / total) * 100 : 0,
+ }))}
+ />
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/@modal/(.)replays/[sessionId]/page.tsx b/src/app/(main)/websites/[websiteId]/@modal/(.)replays/[sessionId]/page.tsx
index 2d498d84f..4950ccb54 100644
--- a/src/app/(main)/websites/[websiteId]/@modal/(.)replays/[sessionId]/page.tsx
+++ b/src/app/(main)/websites/[websiteId]/@modal/(.)replays/[sessionId]/page.tsx
@@ -7,5 +7,5 @@ export default async function ({
}) {
const { websiteId, sessionId } = await params;
- return ;
+ return ;
}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx b/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx
index bab23bc5b..a46489a13 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx
@@ -1,9 +1,11 @@
import { Column, Grid, Row } from '@umami/react-zen';
+import { useShare } from '@/components/hooks';
import { ExportButton } from '@/components/input/ExportButton';
import { FilterBar } from '@/components/input/FilterBar';
import { MonthFilter } from '@/components/input/MonthFilter';
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton';
+import { allowShareFilter } from '@/lib/share';
export function WebsiteControls({
websiteId,
@@ -22,11 +24,16 @@ export function WebsiteControls({
allowDownload?: boolean;
allowCompare?: boolean;
}) {
+ const share = useShare();
+ const showFilter = allowFilter && allowShareFilter(share?.parameters);
+
return (
- {allowFilter && }
+ {showFilter && (
+
+ )}
{allowDateFilter && (
@@ -36,7 +43,7 @@ export function WebsiteControls({
{allowMonthFilter && }
- {allowFilter && }
+ {showFilter && }
);
}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteExpandedMenu.tsx b/src/app/(main)/websites/[websiteId]/WebsiteExpandedMenu.tsx
index c49eaa02f..e3c333693 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteExpandedMenu.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteExpandedMenu.tsx
@@ -25,6 +25,7 @@ import {
Tag,
Target,
Type,
+ Waypoints,
} from '@/components/icons';
import { Lightning } from '@/components/svg';
@@ -45,7 +46,7 @@ export function WebsiteExpandedMenu({
const items = [
{
- label: 'URL',
+ label: t(labels.pages),
items: [
{
id: 'path',
@@ -53,6 +54,12 @@ export function WebsiteExpandedMenu({
path: updateParams({ view: 'path' }),
icon: ,
},
+ {
+ id: 'fullPath',
+ label: t(labels.url),
+ path: updateParams({ view: 'fullPath' }),
+ icon: ,
+ },
{
id: 'entry',
label: t(labels.entry),
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx b/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx
index 71fbdc703..ebe8d976c 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx
@@ -17,6 +17,7 @@ export function WebsiteExpandedView({
const {
query: { view },
} = useNavigation();
+ const title = view === 'fullPath' ? t(labels.url) : t(labels[view]);
return (
@@ -44,12 +45,7 @@ export function WebsiteExpandedView({
-
+
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx
index 9b4e7bded..3f8833838 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx
@@ -1,6 +1,6 @@
import { Column, Focusable, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen';
-import Link from '@/components/common/Link';
import { IconLabel } from '@/components/common/IconLabel';
+import Link from '@/components/common/Link';
import { useMessages, useNavigation, useWebsiteNavItems } from '@/components/hooks';
import { ArrowLeft } from '@/components/icons';
diff --git a/src/app/(main)/websites/[websiteId]/WebsitePanels.tsx b/src/app/(main)/websites/[websiteId]/WebsitePanels.tsx
index 7d5d08c8c..f19a7b400 100644
--- a/src/app/(main)/websites/[websiteId]/WebsitePanels.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsitePanels.tsx
@@ -26,12 +26,16 @@ export function WebsitePanels({ websiteId }: { websiteId: string }) {
{t(labels.path)}
+ {t(labels.url)}
{t(labels.entry)}
{t(labels.exit)}
+
+
+
diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx
index 5641ed40c..d7b9e62d8 100644
--- a/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx
+++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx
@@ -1,8 +1,8 @@
import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
-import Link from '@/components/common/Link';
import { CohortDeleteButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton';
import { CohortEditButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortEditButton';
import { DateDistance } from '@/components/common/DateDistance';
+import Link from '@/components/common/Link';
import { useMessages, useNavigation } from '@/components/hooks';
import { filtersObjectToArray } from '@/lib/params';
diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterButton.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterButton.tsx
deleted file mode 100644
index 4f39b41e2..000000000
--- a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterButton.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-'use client';
-import { useMessages } from '@/components/hooks';
-import { ListFilter } from '@/components/icons';
-import { DialogButton } from '@/components/input/DialogButton';
-import type { EventPropertyFilter } from '@/lib/types';
-import { EventDataFilterEditForm } from './EventDataFilterEditForm';
-
-export function EventDataFilterButton({
- websiteId,
- eventName,
- eventFilters,
- onApply,
-}: {
- websiteId: string;
- eventName: string;
- eventFilters: EventPropertyFilter[];
- onApply: (filters: EventPropertyFilter[]) => void;
-}) {
- const { t, labels } = useMessages();
-
- return (
- } label={t(labels.propertyFilter)} variant="outline">
- {({ close }) => (
-
- )}
-
- );
-}
diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx
deleted file mode 100644
index 16097db1d..000000000
--- a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-'use client';
-import { Button, Column, Grid, Icon, Label, ListItem, Loading, Select, TextField } from '@umami/react-zen';
-import { useState } from 'react';
-import { Empty } from '@/components/common/Empty';
-import { MultiSelect } from '@/components/common/MultiSelect';
-import { useEventDataValuesQuery, useMessages } from '@/components/hooks';
-import { X } from '@/components/icons';
-import { OPERATORS } from '@/lib/constants';
-import type { EventPropertyFilter, Operator } from '@/lib/types';
-
-const STRING_OPERATORS: Operator[] = [
- OPERATORS.equals,
- OPERATORS.notEquals,
- OPERATORS.contains,
- OPERATORS.doesNotContain,
- OPERATORS.regex,
- OPERATORS.notRegex,
-];
-const NUMERIC_OPERATORS: Operator[] = [
- OPERATORS.equals,
- OPERATORS.notEquals,
- OPERATORS.greaterThan,
- OPERATORS.lessThan,
- OPERATORS.greaterThanEquals,
- OPERATORS.lessThanEquals,
-];
-const MULTI_OPERATORS: Operator[] = [OPERATORS.equals, OPERATORS.notEquals];
-const FREE_TEXT_OPERATORS: Operator[] = [
- OPERATORS.contains,
- OPERATORS.doesNotContain,
- OPERATORS.regex,
- OPERATORS.notRegex,
-];
-
-export function EventDataFilterRecord({
- websiteId,
- eventName,
- filter,
- onChange,
- onRemove,
-}: {
- websiteId: string;
- eventName: string;
- filter: EventPropertyFilter;
- onChange: (filter: EventPropertyFilter) => void;
- onRemove: () => void;
-}) {
- const { t, labels, messages } = useMessages();
- const [search, setSearch] = useState('');
-
- const isNumeric = filter.dataType === 2;
- const operators = isNumeric ? NUMERIC_OPERATORS : STRING_OPERATORS;
- const isFreeText = FREE_TEXT_OPERATORS.includes(filter.operator);
-
- const { data, isLoading } = useEventDataValuesQuery(
- websiteId,
- eventName,
- filter.propertyName,
- { enabled: !isNumeric && !isFreeText },
- );
-
- const values = (data as Array<{ value: string }> | undefined)?.map(d => d.value) ?? [];
- const filteredValues = search
- ? values.filter(v => v.toLowerCase().includes(search.toLowerCase()))
- : values;
- const selected = filter.value ? filter.value.split(',').filter(Boolean) : [];
-
- const operatorLabel = (op: Operator) => {
- switch (op) {
- case OPERATORS.equals: return t(labels.is);
- case OPERATORS.notEquals: return t(labels.isNot);
- case OPERATORS.contains: return t(labels.contains);
- case OPERATORS.doesNotContain: return t(labels.doesNotContain);
- case OPERATORS.regex: return t(labels.regexMatch);
- case OPERATORS.notRegex: return t(labels.regexNotMatch);
- case OPERATORS.greaterThan: return t(labels.greaterThan);
- case OPERATORS.lessThan: return t(labels.lessThan);
- case OPERATORS.greaterThanEquals: return t(labels.greaterThanEquals);
- case OPERATORS.lessThanEquals: return t(labels.lessThanEquals);
- default: return op;
- }
- };
-
- const handleOperatorChange = (op: Operator) => {
- // clear value when switching between multi-select and free-text modes
- const wasMulti = MULTI_OPERATORS.includes(filter.operator);
- const isMulti = MULTI_OPERATORS.includes(op);
- onChange({
- ...filter,
- operator: op,
- value: wasMulti === isMulti ? filter.value : '',
- });
- };
-
- return (
-
-
-
-
-
- {isNumeric || isFreeText ? (
- onChange({ ...filter, value: v })}
- inputMode={isNumeric ? 'numeric' : 'text'}
- />
- ) : (
- onChange({ ...filter, value: vals.join(',') })}
- searchValue={search}
- onSearch={setSearch}
- allowSearch
- renderValue={vals => (vals.length > 0 ? vals.join(', ') : undefined)}
- renderEmptyState={() =>
- isLoading ? (
-
- ) : (
-
- )
- }
- >
- {filteredValues.map(v => (
-
- {v}
-
- ))}
-
- )}
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataPivotTable.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataPivotTable.tsx
index ac8b74dd2..1ebc7e63e 100644
--- a/src/app/(main)/websites/[websiteId]/event-data/EventDataPivotTable.tsx
+++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataPivotTable.tsx
@@ -7,7 +7,13 @@ import { Empty } from '@/components/common/Empty';
import Link from '@/components/common/Link';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Pager } from '@/components/common/Pager';
-import { useEventDataPivotQuery, useEventDataPropertiesQuery, useMessages, useMobile, useNavigation } from '@/components/hooks';
+import {
+ useEventDataPivotQuery,
+ useEventDataPropertiesQuery,
+ useMessages,
+ useMobile,
+ useNavigation,
+} from '@/components/hooks';
import type { EventPropertyFilter } from '@/lib/types';
export function EventDataPivotTable({
@@ -34,10 +40,22 @@ export function EventDataPivotTable({
return [...new Set(keys)];
}, [propertiesQuery.data, eventName]);
+ const tableMinWidth = useMemo(() => {
+ return `${72 + 220 + 180 + propertyKeys.length * 160}px`;
+ }, [propertyKeys.length]);
+
const tableData = useMemo(() => {
if (!pivotQuery.data?.data) return [];
return pivotQuery.data.data.map(
- (row: { eventId: string; sessionId: string; eventName: string; urlPath: string; createdAt: string; propertyKeys: string[]; propertyValues: string[] }) => {
+ (row: {
+ eventId: string;
+ sessionId: string;
+ eventName: string;
+ urlPath: string;
+ createdAt: string;
+ propertyKeys: string[];
+ propertyValues: string[];
+ }) => {
const flat: Record = {
eventId: row.eventId,
sessionId: row.sessionId,
@@ -53,10 +71,7 @@ export function EventDataPivotTable({
}, [pivotQuery.data]);
const tableQuery = useMemo(
- () =>
- pivotQuery.data
- ? { ...pivotQuery.data, data: tableData }
- : undefined,
+ () => (pivotQuery.data ? { ...pivotQuery.data, data: tableData } : undefined),
[pivotQuery.data, tableData],
);
@@ -66,6 +81,14 @@ export function EventDataPivotTable({
router.push(updateParams({ page }));
};
+ const renderTruncatedText = (value: string) => (
+
+
+ {value}
+
+
+ );
+
return (
}
>
-
-
-
+ {isMobile ? (
+
+
{(row: any) => (
)}
-
- {(row: any) => (
-
- {row.urlPath}
-
- )}
+
+ {(row: any) => renderTruncatedText(row.urlPath ?? '')}
{propertyKeys.map(key => (
-
- {(row: any) => (
-
- {row[key] ?? ''}
-
- )}
+
+ {(row: any) => renderTruncatedText(row[key] ?? '')}
))}
-
+
{(row: any) => }
-
+ ) : (
+
+
+
+
+ {(row: any) => (
+
+
+
+ )}
+
+
+ {(row: any) => renderTruncatedText(row.urlPath ?? '')}
+
+ {propertyKeys.map(key => (
+
+ {(row: any) => renderTruncatedText(row[key] ?? '')}
+
+ ))}
+
+ {(row: any) => }
+
+
+
+
+ )}
{showPager && tableQuery && (
([]);
+ const [propertyFilters, setPropertyFilters] = useState([]);
const { t, labels } = useMessages();
+ useEffect(() => {
+ setPropertyName('');
+ setPropertyFilters([]);
+ }, [eventName]);
+
const { data, isLoading, isFetching, error } = useEventDataPropertiesQuery(websiteId);
const eventNames = useMemo(() => {
@@ -33,10 +48,8 @@ export function EventProperties({ websiteId }: { websiteId: string }) {
return data
.filter((field: { eventName: string }) => field.eventName === eventName)
.filter((field: { propertyName: string; dataType: number }) => {
- const key = `${field.propertyName}:${field.dataType}`;
-
- if (seen.has(key)) return false;
- seen.add(key);
+ if (seen.has(field.propertyName)) return false;
+ seen.add(field.propertyName);
return true;
});
@@ -48,10 +61,24 @@ export function EventProperties({ websiteId }: { websiteId: string }) {
);
}, [properties, propertyName]);
+ const filteredEventNames = useMemo(() => {
+ if (!eventSearch) {
+ return eventNames;
+ }
+
+ const normalizedSearch = eventSearch.toLowerCase();
+
+ return eventNames.filter(name => name.toLowerCase().includes(normalizedSearch));
+ }, [eventNames, eventSearch]);
+
const handleEventChange = (value: string) => {
setEventName(value);
setPropertyName('');
- setEventFilters([]);
+ setPropertyFilters([]);
+ };
+
+ const handleEventOpenChange = () => {
+ setEventSearch('');
};
return (
@@ -83,10 +110,18 @@ export function EventProperties({ websiteId }: { websiteId: string }) {
)}
)}
- {eventName && (
-
- )}
+ {eventName && }
{eventName && propertyName && (
-
+
{(selectedProperty?.dataType === DATA_TYPE.string ||
selectedProperty?.dataType === DATA_TYPE.boolean) && (
-
)}
{selectedProperty?.dataType === DATA_TYPE.number && (
-
)}
{selectedProperty?.dataType === DATA_TYPE.date && (
-
)}
{selectedProperty?.dataType === DATA_TYPE.array && (
-
)}
)}
{eventName && (
-
+
+
+
)}
diff --git a/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx
index ff5bd74f1..d6b4cf566 100644
--- a/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx
+++ b/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx
@@ -1,40 +1,70 @@
import { LoadingPanel } from '@/components/common/LoadingPanel';
-import { useMessages } from '@/components/hooks';
-import { useWebsiteSessionStatsQuery } from '@/components/hooks/queries/useWebsiteSessionStatsQuery';
+import { useDateRange, useMessages } from '@/components/hooks';
+import { useEventStatsQuery } from '@/components/hooks/queries/useEventStatsQuery';
import { MetricCard } from '@/components/metrics/MetricCard';
import { MetricsBar } from '@/components/metrics/MetricsBar';
import { formatLongNumber } from '@/lib/format';
export function EventsMetricsBar({ websiteId }: { websiteId: string }) {
- const { t, labels } = useMessages();
- const { data, isLoading, isFetching, error } = useWebsiteSessionStatsQuery(websiteId);
+ const { isAllTime } = useDateRange();
+ const { t, labels, getErrorMessage } = useMessages();
+ const { data, isLoading, isFetching, error } = useEventStatsQuery({
+ websiteId,
+ });
+
+ const { events, visitors, visits, uniqueEvents, comparison } = data || {};
+
+ const metrics = data
+ ? [
+ {
+ value: visitors,
+ label: t(labels.visitors),
+ change: visitors - comparison.visitors,
+ formatValue: formatLongNumber,
+ },
+ {
+ value: visits,
+ label: t(labels.visits),
+ change: visits - comparison.visits,
+ formatValue: formatLongNumber,
+ },
+ {
+ value: events,
+ label: t(labels.events),
+ change: events - comparison.events,
+ formatValue: formatLongNumber,
+ },
+ {
+ value: uniqueEvents,
+ label: t(labels.uniqueEvents),
+ change: uniqueEvents - comparison.uniqueEvents,
+ formatValue: formatLongNumber,
+ },
+ ]
+ : null;
return (
-
- {data && (
-
-
-
-
-
-
- )}
+
+
+ {metrics?.map(({ label, value, change, formatValue }) => {
+ return (
+
+ );
+ })}
+
);
}
diff --git a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx
index 5f5ab82a8..2f653665d 100644
--- a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx
+++ b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx
@@ -3,101 +3,45 @@ import { Column, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
import { type Key, useState } from 'react';
import { SessionModal } from '@/app/(main)/websites/[websiteId]/sessions/SessionModal';
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
-import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Panel } from '@/components/common/Panel';
-import { useDateRange, useMessages } from '@/components/hooks';
-import { useEventStatsQuery } from '@/components/hooks/queries/useEventStatsQuery';
+import { useMessages } from '@/components/hooks';
import { EventsChart } from '@/components/metrics/EventsChart';
-import { MetricCard } from '@/components/metrics/MetricCard';
-import { MetricsBar } from '@/components/metrics/MetricsBar';
import { MetricsTable } from '@/components/metrics/MetricsTable';
-import { formatLongNumber } from '@/lib/format';
import { getItem, setItem } from '@/lib/storage';
import { EventProperties } from './EventProperties';
import { EventsDataTable } from './EventsDataTable';
+import { EventsMetricsBar } from './EventsMetricsBar';
const KEY_NAME = 'umami.events.tab';
export function EventsPage({ websiteId }) {
const [tab, setTab] = useState(getItem(KEY_NAME) || 'chart');
- const { isAllTime } = useDateRange();
- const { t, labels, getErrorMessage } = useMessages();
- const { data, isLoading, isFetching, error } = useEventStatsQuery({
- websiteId,
- });
+ const { t, labels } = useMessages();
const handleSelect = (value: Key) => {
setItem(KEY_NAME, value);
setTab(value);
};
- const { events, visitors, visits, uniqueEvents, comparison } = data || {};
-
- const metrics = data
- ? [
- {
- value: visitors,
- label: t(labels.visitors),
- change: visitors - comparison.visitors,
- formatValue: formatLongNumber,
- },
- {
- value: visits,
- label: t(labels.visits),
- change: visits - comparison.visits,
- formatValue: formatLongNumber,
- },
- {
- value: events,
- label: t(labels.events),
- change: events - comparison.events,
- formatValue: formatLongNumber,
- },
- {
- value: uniqueEvents,
- label: t(labels.uniqueEvents),
- change: uniqueEvents - comparison.uniqueEvents,
- formatValue: formatLongNumber,
- },
- ]
- : null;
-
return (
-
-
- {metrics?.map(({ label, value, change, formatValue }) => {
- return (
-
- );
- })}
-
-
-
- handleSelect(key)}>
+
+
+ handleSelect(key)}
+ style={{ minWidth: 0, width: '100%' }}
+ >
{t(labels.chart)}
{t(labels.activity)}
{t(labels.properties)}
-
+
-
+
@@ -111,7 +55,7 @@ export function EventsPage({ websiteId }) {
/>
-
+
diff --git a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx
index 57688e55c..8e07880bb 100644
--- a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx
+++ b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx
@@ -10,10 +10,10 @@ import {
Row,
Text,
} from '@umami/react-zen';
-import Link from '@/components/common/Link';
import { Avatar } from '@/components/common/Avatar';
import { DateDistance } from '@/components/common/DateDistance';
import { IconLabel } from '@/components/common/IconLabel';
+import Link from '@/components/common/Link';
import { TypeIcon } from '@/components/common/TypeIcon';
import { useFormat, useMessages, useNavigation } from '@/components/hooks';
import { Eye, FileText } from '@/components/icons';
diff --git a/src/app/(main)/websites/[websiteId]/realtime/BoardRealtimeActiveUsers.tsx b/src/app/(main)/websites/[websiteId]/realtime/BoardRealtimeActiveUsers.tsx
new file mode 100644
index 000000000..104453d1c
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/realtime/BoardRealtimeActiveUsers.tsx
@@ -0,0 +1,25 @@
+import { Column, Text } from '@umami/react-zen';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useActiveUsersQuery, useMessages } from '@/components/hooks';
+import { REALTIME_INTERVAL } from '@/lib/constants';
+
+export function BoardRealtimeActiveUsers({ websiteId }: { websiteId: string }) {
+ const { t, labels } = useMessages();
+ const { data, isLoading, error } = useActiveUsersQuery(websiteId, {
+ refetchInterval: REALTIME_INTERVAL,
+ });
+ const count = data?.visitors ?? 0;
+
+ return (
+
+
+
+ {count}
+
+
+ {t(labels.online)}
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/realtime/BoardRealtimeChart.tsx b/src/app/(main)/websites/[websiteId]/realtime/BoardRealtimeChart.tsx
new file mode 100644
index 000000000..c0f142804
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/realtime/BoardRealtimeChart.tsx
@@ -0,0 +1,13 @@
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useRealtimeQuery } from '@/components/hooks';
+import { RealtimeChart } from '@/components/metrics/RealtimeChart';
+
+export function BoardRealtimeChart({ websiteId }: { websiteId: string }) {
+ const { data, isLoading, error } = useRealtimeQuery(websiteId);
+
+ return (
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/realtime/BoardRealtimeMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/realtime/BoardRealtimeMetricsBar.tsx
new file mode 100644
index 000000000..15ffedf6d
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/realtime/BoardRealtimeMetricsBar.tsx
@@ -0,0 +1,13 @@
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useRealtimeQuery } from '@/components/hooks';
+import { RealtimeHeader } from './RealtimeHeader';
+
+export function BoardRealtimeMetricsBar({ websiteId }: { websiteId: string }) {
+ const { data, isLoading, error } = useRealtimeQuery(websiteId);
+
+ return (
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx
index a7af9205b..e5ded892b 100644
--- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx
+++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx
@@ -1,13 +1,16 @@
-import { useMessages } from '@/components/hooks';
+import { useMessages, useMobile } from '@/components/hooks';
import { MetricCard } from '@/components/metrics/MetricCard';
import { MetricsBar } from '@/components/metrics/MetricsBar';
export function RealtimeHeader({ data }: { data: any }) {
const { t, labels } = useMessages();
+ const { isPhone } = useMobile();
const { totals }: any = data || {};
return (
-
+
diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx
index 17daaeed9..8c3b56514 100644
--- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx
+++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx
@@ -1,12 +1,11 @@
import { Column, Heading, Row, SearchField, Text } from '@umami/react-zen';
-import Link from '@/components/common/Link';
import { useMemo, useState } from 'react';
import { List, type RowComponentProps } from 'react-window';
import { SessionModal } from '@/app/(main)/websites/[websiteId]/sessions/SessionModal';
-import { useFormat } from '@/components//hooks/useFormat';
import { Avatar } from '@/components/common/Avatar';
import { Empty } from '@/components/common/Empty';
import { IconLabel } from '@/components/common/IconLabel';
+import Link from '@/components/common/Link';
import {
useCountryNames,
useLocale,
@@ -16,6 +15,7 @@ import {
useTimezone,
useWebsite,
} from '@/components/hooks';
+import { useFormat } from '@/components/hooks/useFormat';
import { Eye, User } from '@/components/icons';
import { FilterButtons } from '@/components/input/FilterButtons';
import { Lightning } from '@/components/svg';
@@ -25,6 +25,8 @@ const TYPE_ALL = 'all';
const TYPE_PAGEVIEW = 'pageview';
const TYPE_SESSION = 'session';
const TYPE_EVENT = 'event';
+const MAX_LIST_HEIGHT = 500;
+const ROW_HEIGHT = 50;
const icons = {
[TYPE_PAGEVIEW]: ,
@@ -124,7 +126,7 @@ export function RealtimeLog({ data }: { data: any }) {
const TableRow = ({ index, style, logs }: RowComponentProps<{ logs: any[] }>) => {
const row = logs[index];
return (
-
+
@@ -133,10 +135,8 @@ export function RealtimeLog({ data }: { data: any }) {
{getTime(row)}
-
-
- {getDetail(row)}
-
+
+ {getDetail(row)}
);
@@ -173,12 +173,14 @@ export function RealtimeLog({ data }: { data: any }) {
return logs;
}, [data, filter, formatValue, search]);
+ const listHeight = Math.min(logs.length * ROW_HEIGHT, MAX_LIST_HEIGHT);
+
return (
-
+
{t(labels.activity)}
{isPhone ? (
<>
-
+
@@ -192,16 +194,16 @@ export function RealtimeLog({ data }: { data: any }) {
)}
-
+
{logs?.length === 0 && }
- {logs?.length > 0 && (
+ {logs.length > 0 && (
)}
diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimePaths.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimePaths.tsx
index e69d9e645..2a91e01ee 100644
--- a/src/app/(main)/websites/[websiteId]/realtime/RealtimePaths.tsx
+++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimePaths.tsx
@@ -1,4 +1,4 @@
-import thenby from 'thenby';
+import { firstBy } from 'thenby';
import { useMessages, useWebsite } from '@/components/hooks';
import { ListTable } from '@/components/metrics/ListTable';
import { percentFilter } from '@/lib/filters';
@@ -26,7 +26,7 @@ export function RealtimePaths({ data }: { data: any }) {
y: urls[key],
};
})
- .sort(thenby.firstBy('y', -1))
+ .sort(firstBy('y', -1))
.slice(0, limit),
);
diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeReferrers.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeReferrers.tsx
index fa2a56b9f..e2d2305b0 100644
--- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeReferrers.tsx
+++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeReferrers.tsx
@@ -1,7 +1,7 @@
-import thenby from 'thenby';
import { useMessages, useWebsite } from '@/components/hooks';
import { ListTable } from '@/components/metrics/ListTable';
import { percentFilter } from '@/lib/filters';
+import { firstBy } from 'thenby';
export function RealtimeReferrers({ data }: { data: any }) {
const website = useWebsite();
@@ -26,7 +26,7 @@ export function RealtimeReferrers({ data }: { data: any }) {
y: referrers[key],
};
})
- .sort(thenby.firstBy('y', -1))
+ .sort(firstBy('y', -1))
.slice(0, limit),
);
diff --git a/src/app/(main)/websites/[websiteId]/replays/ReplayFilterButton.tsx b/src/app/(main)/websites/[websiteId]/replays/ReplayFilterButton.tsx
new file mode 100644
index 000000000..da9b1d8b0
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/replays/ReplayFilterButton.tsx
@@ -0,0 +1,96 @@
+'use client';
+
+import { Button, Column, Label, Row, Text, TextField } from '@umami/react-zen';
+import { useState } from 'react';
+import { useMessages, useNavigation } from '@/components/hooks';
+import { ListFilter } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+
+const DEFAULT_MIN_DURATION = '5';
+
+function getMinDurationValue(value: unknown) {
+ if (value === '0') {
+ return '';
+ }
+
+ if (typeof value !== 'string' || !/^\d+$/.test(value)) {
+ return DEFAULT_MIN_DURATION;
+ }
+
+ return Number(value) > 0 ? value : DEFAULT_MIN_DURATION;
+}
+
+function ReplayFilterForm({
+ minDuration,
+ onClose,
+}: {
+ minDuration: string;
+ onClose?: () => void;
+}) {
+ const { t, labels, messages } = useMessages();
+ const { router, updateParams } = useNavigation();
+ const [value, setValue] = useState(minDuration);
+
+ const handleChange = (nextValue: string) => {
+ if (/^\d*$/.test(nextValue)) {
+ setValue(nextValue);
+ }
+ };
+
+ const handleReset = () => {
+ setValue(DEFAULT_MIN_DURATION);
+ };
+
+ const handleApply = () => {
+ const nextValue = value ? Number(value) : 0;
+
+ router.push(
+ updateParams({
+ minDuration: nextValue > 0 ? nextValue : 0,
+ page: 1,
+ }),
+ );
+
+ onClose?.();
+ };
+
+ return (
+
+
+
+ {t(messages.replayMinDurationDescription)}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function ReplayFilterButton() {
+ const { t, labels } = useMessages();
+ const { query } = useNavigation();
+ const minDuration = getMinDurationValue(query.minDuration);
+ const title = `${t(labels.replay)} ${t(labels.filter)}`;
+ const label = minDuration ? `${title} (${minDuration}s+)` : title;
+
+ return (
+ } label={label} title={title} variant="outline">
+ {({ close }) => }
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/replays/ReplayModal.module.css b/src/app/(main)/websites/[websiteId]/replays/ReplayModal.module.css
new file mode 100644
index 000000000..627ea7140
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/replays/ReplayModal.module.css
@@ -0,0 +1,9 @@
+.modal,
+.sheet {
+ border-radius: 8px 8px 0 0;
+}
+
+.modal {
+ width: min(calc(100dvw - 48px), 1320px);
+ overflow: hidden;
+}
diff --git a/src/app/(main)/websites/[websiteId]/replays/ReplayModal.tsx b/src/app/(main)/websites/[websiteId]/replays/ReplayModal.tsx
index 7127a613d..6078b4bbd 100644
--- a/src/app/(main)/websites/[websiteId]/replays/ReplayModal.tsx
+++ b/src/app/(main)/websites/[websiteId]/replays/ReplayModal.tsx
@@ -2,38 +2,54 @@
import { Column, Dialog, Modal, type ModalProps } from '@umami/react-zen';
import { ReplayPlayback } from '@/app/(main)/websites/[websiteId]/replays/[replayId]/ReplayPlayback';
import { useNavigation } from '@/components/hooks';
+import { buildPath } from '@/lib/url';
+import styles from './ReplayModal.module.css';
export interface ReplayModalProps extends ModalProps {
websiteId: string;
+ replayId?: string;
}
-export function ReplayModal({ websiteId, ...props }: ReplayModalProps) {
+export function ReplayModal({ websiteId, replayId, className, ...props }: ReplayModalProps) {
const {
router,
query: { replay },
+ searchParams,
updateParams,
} = useNavigation();
+ const activeReplayId = replayId || replay;
+ const modalClassName = [styles.modal, className].filter(Boolean).join(' ');
const handleOpenChange = (isOpen: boolean) => {
if (!isOpen) {
- router.push(updateParams({ replay: undefined }));
+ if (replayId) {
+ const query = Object.fromEntries(searchParams.entries());
+ delete query.replay;
+
+ router.push(buildPath(`/websites/${websiteId}/replays`, query));
+ } else {
+ router.push(updateParams({ replay: undefined }));
+ }
}
};
return (
-
-
diff --git a/src/components/share/SimpleShareCreateForm.tsx b/src/components/share/SimpleShareCreateForm.tsx
index 9ab850bcb..28ff801cc 100644
--- a/src/components/share/SimpleShareCreateForm.tsx
+++ b/src/components/share/SimpleShareCreateForm.tsx
@@ -1,6 +1,16 @@
-import { Button, Column, Form, FormField, FormSubmitButton, Row, TextField } from '@umami/react-zen';
+import {
+ Button,
+ Column,
+ Form,
+ FormField,
+ FormSubmitButton,
+ Row,
+ Switch,
+ TextField,
+} from '@umami/react-zen';
import { useState } from 'react';
import { useApi, useMessages, useModified } from '@/components/hooks';
+import { ThemeModeSelector } from '@/components/input/ThemeModeSelector';
export function SimpleShareCreateForm({
createPath,
@@ -15,15 +25,19 @@ export function SimpleShareCreateForm({
const { touch } = useModified();
const { t, labels, getErrorMessage } = useMessages();
const [isPending, setIsPending] = useState(false);
- const [error, setError] = useState(null);
+ const [error, setError] = useState(null);
- const handleSubmit = async (data: { name: string }) => {
+ const handleSubmit = async (data: { name: string; allowFilter?: boolean; theme?: string }) => {
setIsPending(true);
setError(null);
try {
await post(createPath, {
name: data.name,
+ parameters: {
+ allowFilter: data.allowFilter ?? true,
+ theme: data.theme === 'system' ? undefined : data.theme,
+ },
});
touch('shares');
@@ -36,22 +50,45 @@ export function SimpleShareCreateForm({
};
return (
-
);
}
diff --git a/src/components/share/SimpleShareEditForm.tsx b/src/components/share/SimpleShareEditForm.tsx
index 3c83fed52..ddfbda9f8 100644
--- a/src/components/share/SimpleShareEditForm.tsx
+++ b/src/components/share/SimpleShareEditForm.tsx
@@ -7,10 +7,12 @@ import {
Label,
Loading,
Row,
+ Switch,
TextField,
} from '@umami/react-zen';
import { useEffect, useState } from 'react';
import { useApi, useConfig, useMessages, useModified } from '@/components/hooks';
+import { ThemeModeSelector } from '@/components/input/ThemeModeSelector';
export function SimpleShareEditForm({
shareId,
@@ -49,7 +51,7 @@ export function SimpleShareEditForm({
loadShare();
}, [get, modified, shareId]);
- const handleSubmit = async (data: { name: string }) => {
+ const handleSubmit = async (data: { name: string; allowFilter?: boolean; theme?: string }) => {
setIsPending(true);
setError(null);
@@ -57,7 +59,11 @@ export function SimpleShareEditForm({
await post(`/share/id/${shareId}`, {
name: data.name,
slug: share.slug,
- parameters: share.parameters || {},
+ parameters: {
+ ...(share.parameters || {}),
+ allowFilter: data.allowFilter ?? true,
+ theme: data.theme === 'system' ? undefined : data.theme,
+ },
});
touch('shares');
@@ -78,27 +84,50 @@ export function SimpleShareEditForm({
);
}
diff --git a/src/components/share/SimpleSharesTable.tsx b/src/components/share/SimpleSharesTable.tsx
index 13313f7b9..b79c16513 100644
--- a/src/components/share/SimpleSharesTable.tsx
+++ b/src/components/share/SimpleSharesTable.tsx
@@ -1,8 +1,9 @@
+import { DataColumn, DataTable, Icon, type DataTableProps, Row, Text } from '@umami/react-zen';
import { ShareDeleteButton } from '@/app/(main)/websites/[websiteId]/settings/ShareDeleteButton';
import { CopyButton } from '@/components/common/CopyButton';
-import { ExternalLink } from '@/components/common/ExternalLink';
+import Link from '@/components/common/Link';
import { useConfig, useMessages, useMobile } from '@/components/hooks';
-import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
+import { ExternalLink } from '@/components/icons';
import { SimpleShareEditButton } from './SimpleShareEditButton';
export function SimpleSharesTable(props: DataTableProps) {
@@ -24,15 +25,22 @@ export function SimpleSharesTable(props: DataTableProps) {
{({ name }: any) => name}
-
+
{({ slug }: any) => {
const url = getUrl(slug);
return (
-
-
- {isMobile ? slug : url}
-
+
+
+
+
+ {isMobile ? slug : url}
+
+
+
+
+
+
);
diff --git a/src/lib/__tests__/boards.test.ts b/src/lib/__tests__/boards.test.ts
deleted file mode 100644
index 4d6a8f435..000000000
--- a/src/lib/__tests__/boards.test.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import {
- BOARD_ENTITY_TYPES,
- isBoardComponentSupported,
-} from '../boards';
-import {
- BOARD_COMPONENT_COMPATIBILITY_MATRIX,
- getSupportedBoardComponentEntityTypes,
-} from '../boardComponentCompatibility';
-
-test('isBoardComponentSupported allows events chart on website boards', () => {
- expect(isBoardComponentSupported('EventsChart', BOARD_ENTITY_TYPES.website)).toBe(true);
-});
-
-test('isBoardComponentSupported rejects events chart on pixel and link boards', () => {
- expect(isBoardComponentSupported('EventsChart', BOARD_ENTITY_TYPES.pixel)).toBe(false);
- expect(isBoardComponentSupported('EventsChart', BOARD_ENTITY_TYPES.link)).toBe(false);
-});
-
-test('board component compatibility matrix defines website-only components explicitly', () => {
- expect(BOARD_COMPONENT_COMPATIBILITY_MATRIX.EventsChart).toEqual([BOARD_ENTITY_TYPES.website]);
- expect(getSupportedBoardComponentEntityTypes('EventsChart')).toEqual([
- BOARD_ENTITY_TYPES.website,
- ]);
-});
-
-test('isBoardComponentSupported leaves other components available for all board entities', () => {
- expect(isBoardComponentSupported('WebsiteChart', BOARD_ENTITY_TYPES.pixel)).toBe(true);
- expect(isBoardComponentSupported('TextBlock', BOARD_ENTITY_TYPES.link)).toBe(true);
-});
diff --git a/src/lib/__tests__/detect.test.ts b/src/lib/__tests__/detect.test.ts
deleted file mode 100644
index c41523d15..000000000
--- a/src/lib/__tests__/detect.test.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { getIpAddress } from '../ip';
-
-const IP = '127.0.0.1';
-const BAD_IP = '127.127.127.127';
-
-test('getIpAddress: Custom header', () => {
- process.env.CLIENT_IP_HEADER = 'x-custom-ip-header';
-
- expect(getIpAddress(new Headers({ 'x-custom-ip-header': IP }))).toEqual(IP);
-});
-
-test('getIpAddress: CloudFlare header', () => {
- expect(getIpAddress(new Headers({ 'cf-connecting-ip': IP }))).toEqual(IP);
-});
-
-test('getIpAddress: Standard header', () => {
- expect(getIpAddress(new Headers({ 'x-forwarded-for': IP }))).toEqual(IP);
-});
-
-test('getIpAddress: No header', () => {
- expect(getIpAddress(new Headers())).toEqual(undefined);
-});
diff --git a/src/lib/api-url.test.ts b/src/lib/api-url.test.ts
new file mode 100644
index 000000000..d0c6cbb10
--- /dev/null
+++ b/src/lib/api-url.test.ts
@@ -0,0 +1,53 @@
+import { describe, expect, test } from 'vitest';
+import { getApiUrl } from './api-url';
+
+describe('getApiUrl', () => {
+ test('calls an absolute API_URL directly', () => {
+ expect(
+ getApiUrl('/websites', {
+ apiUrl: 'https://gateway-eu.umami.dev/api',
+ basePath: '/analytics',
+ }),
+ ).toBe('https://gateway-eu.umami.dev/api/websites');
+ });
+
+ test('keeps app routes on the local api path when API_URL is absolute', () => {
+ expect(
+ getApiUrl('/auth/verify', {
+ apiUrl: 'https://gateway-eu.umami.dev/api',
+ basePath: '/analytics',
+ }),
+ ).toBe('/analytics/api/auth/verify');
+ });
+
+ test('uses a relative API_URL under the base path', () => {
+ expect(
+ getApiUrl('/websites', {
+ apiUrl: '/internal-api',
+ basePath: '/analytics',
+ }),
+ ).toBe('/analytics/internal-api/websites');
+ });
+
+ test('keeps app routes on the local api path', () => {
+ expect(
+ getApiUrl('/auth/verify', {
+ apiUrl: '/internal-api',
+ basePath: '/analytics',
+ }),
+ ).toBe('/analytics/api/auth/verify');
+ });
+
+ test('keeps config routes on the local api path', () => {
+ expect(
+ getApiUrl('/config', {
+ apiUrl: '/internal-api',
+ basePath: '/analytics',
+ }),
+ ).toBe('/analytics/api/config');
+ });
+
+ test('returns absolute input urls unchanged', () => {
+ expect(getApiUrl('https://example.com/api/websites')).toBe('https://example.com/api/websites');
+ });
+});
diff --git a/src/lib/api-url.ts b/src/lib/api-url.ts
new file mode 100644
index 000000000..190618a73
--- /dev/null
+++ b/src/lib/api-url.ts
@@ -0,0 +1,47 @@
+type ApiUrlOptions = {
+ apiUrl?: string;
+ basePath?: string;
+};
+
+const APP_ROUTE_PATTERNS: RegExp[] = [/^\/auth(\/|$)/, /^\/config(\/|$)/];
+
+function trimTrailingSlash(value: string) {
+ return value.replace(/\/+$/, '');
+}
+
+function trimLeadingSlash(value: string) {
+ return value.replace(/^\/+/, '');
+}
+
+function joinPath(base: string, path: string) {
+ if (!base) {
+ return `/${trimLeadingSlash(path)}`;
+ }
+
+ return `${trimTrailingSlash(base)}/${trimLeadingSlash(path)}`;
+}
+
+function isAbsoluteUrl(url: string) {
+ return /^https?:\/\//i.test(url);
+}
+
+function isAppRoute(url: string) {
+ const path = `/${trimLeadingSlash(url.split('?')[0])}`;
+ return APP_ROUTE_PATTERNS.some(re => re.test(path));
+}
+
+export function getApiUrl(url: string, options: ApiUrlOptions = {}) {
+ if (isAbsoluteUrl(url)) {
+ return url;
+ }
+
+ const { apiUrl = process.env.apiUrl || '', basePath = process.env.basePath || '' } = options;
+ const useApiUrl = apiUrl && !isAppRoute(url);
+ const baseUrl = useApiUrl
+ ? isAbsoluteUrl(apiUrl)
+ ? apiUrl
+ : joinPath(basePath, apiUrl)
+ : joinPath(basePath, '/api');
+
+ return joinPath(baseUrl, url);
+}
diff --git a/src/lib/auth.test.ts b/src/lib/auth.test.ts
new file mode 100644
index 000000000..fed2dad29
--- /dev/null
+++ b/src/lib/auth.test.ts
@@ -0,0 +1,121 @@
+import { beforeEach, describe, expect, test, vi } from 'vitest';
+import { hash } from '@/lib/crypto';
+import { parseSecureToken } from '@/lib/jwt';
+import redis from '@/lib/redis';
+import { getUser } from '@/queries/prisma/user';
+import { checkAuth } from './auth';
+
+vi.mock('@/lib/jwt', () => ({
+ parseSecureToken: vi.fn(),
+ parseToken: vi.fn(() => null),
+}));
+
+vi.mock('@/queries/prisma/user', () => ({
+ getUser: vi.fn(),
+}));
+
+vi.mock('@/lib/redis', () => ({
+ default: {
+ enabled: false,
+ client: {
+ get: vi.fn(),
+ },
+ },
+}));
+
+const parseSecureTokenMock = vi.mocked(parseSecureToken);
+const getUserMock = vi.mocked(getUser);
+const redisMock = redis as unknown as {
+ enabled: boolean;
+ client: {
+ get: ReturnType;
+ };
+};
+
+const PASSWORD_HASH = '$2b$10$currentpasswordhashvalue';
+
+function authedRequest() {
+ return new Request('http://localhost/api/test', {
+ headers: { authorization: 'Bearer secure-token' },
+ });
+}
+
+function mockUser() {
+ getUserMock.mockResolvedValue({
+ id: 'user-1',
+ username: 'bob',
+ role: 'user',
+ password: PASSWORD_HASH,
+ } as any);
+}
+
+beforeEach(() => {
+ parseSecureTokenMock.mockReset();
+ getUserMock.mockReset();
+ redisMock.enabled = false;
+ redisMock.client.get.mockReset();
+});
+
+describe('checkAuth password fingerprint', () => {
+ test('authorizes a stateless token whose fingerprint matches the current password', async () => {
+ parseSecureTokenMock.mockReturnValue({ userId: 'user-1', pwd: hash(PASSWORD_HASH) } as any);
+ mockUser();
+
+ const result = await checkAuth(authedRequest());
+
+ expect(result?.user?.id).toBe('user-1');
+ });
+
+ test('authorizes a legacy stateless token that does not include a password fingerprint', async () => {
+ parseSecureTokenMock.mockReturnValue({ userId: 'user-1' } as any);
+ mockUser();
+
+ const result = await checkAuth(authedRequest());
+
+ expect(result?.user?.id).toBe('user-1');
+ });
+
+ test('rejects a stateless token whose fingerprint predates a password change', async () => {
+ // Token minted against the old password must stop working once the password changes.
+ parseSecureTokenMock.mockReturnValue({
+ userId: 'user-1',
+ pwd: hash('old-password-hash'),
+ } as any);
+ mockUser();
+
+ const result = await checkAuth(authedRequest());
+
+ expect(result).toBeNull();
+ });
+
+ test('does not expose the password hash on the returned user', async () => {
+ parseSecureTokenMock.mockReturnValue({ userId: 'user-1', pwd: hash(PASSWORD_HASH) } as any);
+ mockUser();
+
+ const result = await checkAuth(authedRequest());
+
+ expect(result?.user).not.toHaveProperty('password');
+ });
+
+ test('authorizes a Redis session whose fingerprint matches the current password', async () => {
+ redisMock.enabled = true;
+ parseSecureTokenMock.mockReturnValue({ authKey: 'auth:session-key' } as any);
+ redisMock.client.get.mockResolvedValue({ userId: 'user-1', pwd: hash(PASSWORD_HASH) });
+ mockUser();
+
+ const result = await checkAuth(authedRequest());
+
+ expect(result?.user?.id).toBe('user-1');
+ });
+
+ test('rejects a Redis session whose fingerprint predates a password change', async () => {
+ redisMock.enabled = true;
+ parseSecureTokenMock.mockReturnValue({ authKey: 'auth:session-key' } as any);
+ redisMock.client.get.mockResolvedValue({ userId: 'user-1', pwd: hash('old-password-hash') });
+ mockUser();
+
+ const result = await checkAuth(authedRequest());
+
+ expect(result).toBeNull();
+ });
+});
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index 49de033ec..559c7f90e 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -1,6 +1,12 @@
import debug from 'debug';
-import { ROLE_PERMISSIONS, ROLES, SHARE_CONTEXT_HEADER, SHARE_TOKEN_HEADER } from '@/lib/constants';
-import { createAuthKey, secret } from '@/lib/crypto';
+import {
+ ROLE_PERMISSIONS,
+ ROLES,
+ SHARE_CONTEXT_HEADER,
+ SHARE_TOKEN_HEADER,
+ SHARE_TOKEN_TYPE,
+} from '@/lib/constants';
+import { createAuthKey, hash, secret } from '@/lib/crypto';
import { createSecureToken, parseSecureToken, parseToken } from '@/lib/jwt';
import redis from '@/lib/redis';
import { ensureArray } from '@/lib/utils';
@@ -23,16 +29,33 @@ export async function checkAuth(request: Request) {
const { userId, authKey } = payload || {};
if (userId) {
- user = await getUser(userId);
+ user = await getUser(userId, { includePassword: true });
+
+ // Reject tokens issued before the current password.
+ // Allow legacy stateless tokens that were minted without a password fingerprint.
+ if (user && payload.pwd && hash(user.password) !== payload.pwd) {
+ user = null;
+ }
} else if (redis.enabled && authKey) {
const key = await redis.client.get(authKey);
if (key?.userId) {
- user = await getUser(key.userId);
+ user = await getUser(key.userId, { includePassword: true });
+
+ // Only enforce password-change invalidation for sessions that include a password fingerprint.
+ if (user && key.pwd && hash(user.password) !== key.pwd) {
+ user = null;
+ }
}
}
- log({ token, payload, authKey, shareToken, user });
+ log({
+ hasToken: !!token,
+ hasPayload: !!payload,
+ hasAuthKey: !!authKey,
+ hasShareToken: !!shareToken,
+ userId: user?.id,
+ });
if (!user?.id && !shareToken) {
log('User not authorized');
@@ -48,6 +71,7 @@ export async function checkAuth(request: Request) {
}
if (user) {
+ delete user.password;
user.isAdmin = user.role === ROLES.admin;
}
@@ -79,7 +103,16 @@ export async function hasPermission(role: string, permission: string | string[])
export function parseShareToken(request: Request) {
try {
- return parseToken(request.headers.get(SHARE_TOKEN_HEADER), secret());
+ const token: any = parseToken(request.headers.get(SHARE_TOKEN_HEADER), secret());
+
+ // Only accept tokens explicitly minted as share tokens. This prevents other
+ // tokens signed with the same secret (e.g. the cache token from /api/send)
+ // from being replayed as share tokens to gain analytics access.
+ if (token?.type !== SHARE_TOKEN_TYPE) {
+ return null;
+ }
+
+ return token;
} catch (e) {
log(e);
return null;
diff --git a/src/lib/boardComponentCompatibility.ts b/src/lib/boardComponentCompatibility.ts
index 943701962..d04ced5e5 100644
--- a/src/lib/boardComponentCompatibility.ts
+++ b/src/lib/boardComponentCompatibility.ts
@@ -1,7 +1,18 @@
import type { BoardEntityType } from './boards';
export const BOARD_COMPONENT_COMPATIBILITY_MATRIX = {
+ EventsMetricsBar: ['website'],
EventsChart: ['website'],
+ Funnel: ['website'],
+ Goal: ['website'],
+ RealtimeActiveUsers: ['website'],
+ RealtimeHeader: ['website'],
+ RealtimeChart: ['website'],
+ RevenueMetricsBar: ['website'],
+ RevenueChart: ['website'],
+ RevenueMetricsTable: ['website'],
+ UTM: ['website'],
+ WeeklyTraffic: ['website'],
} as const satisfies Partial>;
export function getSupportedBoardComponentEntityTypes(componentType: string) {
diff --git a/src/lib/boards.test.ts b/src/lib/boards.test.ts
new file mode 100644
index 000000000..f80e7c79e
--- /dev/null
+++ b/src/lib/boards.test.ts
@@ -0,0 +1,115 @@
+import { expect, test } from 'vitest';
+import { getComponentDefinition } from '@/app/(main)/boards/boardComponentRegistry';
+import {
+ BOARD_COMPONENT_COMPATIBILITY_MATRIX,
+ getSupportedBoardComponentEntityTypes,
+} from './boardComponentCompatibility';
+import { BOARD_ENTITY_TYPES, isBoardComponentSupported } from './boards';
+
+test('isBoardComponentSupported allows events chart on website boards', () => {
+ expect(isBoardComponentSupported('EventsChart', BOARD_ENTITY_TYPES.website)).toBe(true);
+});
+
+test('isBoardComponentSupported rejects events chart on pixel and link boards', () => {
+ expect(isBoardComponentSupported('EventsChart', BOARD_ENTITY_TYPES.pixel)).toBe(false);
+ expect(isBoardComponentSupported('EventsChart', BOARD_ENTITY_TYPES.link)).toBe(false);
+});
+
+test('isBoardComponentSupported rejects event metrics bar and weekly traffic on pixel and link boards', () => {
+ expect(isBoardComponentSupported('EventsMetricsBar', BOARD_ENTITY_TYPES.pixel)).toBe(false);
+ expect(isBoardComponentSupported('EventsMetricsBar', BOARD_ENTITY_TYPES.link)).toBe(false);
+ expect(isBoardComponentSupported('WeeklyTraffic', BOARD_ENTITY_TYPES.pixel)).toBe(false);
+ expect(isBoardComponentSupported('WeeklyTraffic', BOARD_ENTITY_TYPES.link)).toBe(false);
+});
+
+test('board component compatibility matrix defines website-only components explicitly', () => {
+ expect(BOARD_COMPONENT_COMPATIBILITY_MATRIX.EventsMetricsBar).toEqual([
+ BOARD_ENTITY_TYPES.website,
+ ]);
+ expect(BOARD_COMPONENT_COMPATIBILITY_MATRIX.EventsChart).toEqual([BOARD_ENTITY_TYPES.website]);
+ expect(BOARD_COMPONENT_COMPATIBILITY_MATRIX.Funnel).toEqual([BOARD_ENTITY_TYPES.website]);
+ expect(BOARD_COMPONENT_COMPATIBILITY_MATRIX.Goal).toEqual([BOARD_ENTITY_TYPES.website]);
+ expect(BOARD_COMPONENT_COMPATIBILITY_MATRIX.RealtimeActiveUsers).toEqual([
+ BOARD_ENTITY_TYPES.website,
+ ]);
+ expect(BOARD_COMPONENT_COMPATIBILITY_MATRIX.RealtimeHeader).toEqual([
+ BOARD_ENTITY_TYPES.website,
+ ]);
+ expect(BOARD_COMPONENT_COMPATIBILITY_MATRIX.RealtimeChart).toEqual([
+ BOARD_ENTITY_TYPES.website,
+ ]);
+ expect(BOARD_COMPONENT_COMPATIBILITY_MATRIX.RevenueMetricsBar).toEqual([
+ BOARD_ENTITY_TYPES.website,
+ ]);
+ expect(BOARD_COMPONENT_COMPATIBILITY_MATRIX.RevenueChart).toEqual([
+ BOARD_ENTITY_TYPES.website,
+ ]);
+ expect(BOARD_COMPONENT_COMPATIBILITY_MATRIX.RevenueMetricsTable).toEqual([
+ BOARD_ENTITY_TYPES.website,
+ ]);
+ expect(BOARD_COMPONENT_COMPATIBILITY_MATRIX.UTM).toEqual([BOARD_ENTITY_TYPES.website]);
+ expect(BOARD_COMPONENT_COMPATIBILITY_MATRIX.WeeklyTraffic).toEqual([
+ BOARD_ENTITY_TYPES.website,
+ ]);
+ expect(getSupportedBoardComponentEntityTypes('EventsMetricsBar')).toEqual([
+ BOARD_ENTITY_TYPES.website,
+ ]);
+ expect(getSupportedBoardComponentEntityTypes('EventsChart')).toEqual([
+ BOARD_ENTITY_TYPES.website,
+ ]);
+ expect(getSupportedBoardComponentEntityTypes('Funnel')).toEqual([BOARD_ENTITY_TYPES.website]);
+ expect(getSupportedBoardComponentEntityTypes('Goal')).toEqual([BOARD_ENTITY_TYPES.website]);
+ expect(getSupportedBoardComponentEntityTypes('RealtimeActiveUsers')).toEqual([
+ BOARD_ENTITY_TYPES.website,
+ ]);
+ expect(getSupportedBoardComponentEntityTypes('RealtimeHeader')).toEqual([
+ BOARD_ENTITY_TYPES.website,
+ ]);
+ expect(getSupportedBoardComponentEntityTypes('RealtimeChart')).toEqual([
+ BOARD_ENTITY_TYPES.website,
+ ]);
+ expect(getSupportedBoardComponentEntityTypes('RevenueMetricsBar')).toEqual([
+ BOARD_ENTITY_TYPES.website,
+ ]);
+ expect(getSupportedBoardComponentEntityTypes('RevenueChart')).toEqual([
+ BOARD_ENTITY_TYPES.website,
+ ]);
+ expect(getSupportedBoardComponentEntityTypes('RevenueMetricsTable')).toEqual([
+ BOARD_ENTITY_TYPES.website,
+ ]);
+ expect(getSupportedBoardComponentEntityTypes('UTM')).toEqual([BOARD_ENTITY_TYPES.website]);
+ expect(getSupportedBoardComponentEntityTypes('WeeklyTraffic')).toEqual([
+ BOARD_ENTITY_TYPES.website,
+ ]);
+});
+
+test('isBoardComponentSupported leaves other components available for all board entities', () => {
+ expect(isBoardComponentSupported('WebsiteChart', BOARD_ENTITY_TYPES.pixel)).toBe(true);
+ expect(isBoardComponentSupported('TextBlock', BOARD_ENTITY_TYPES.link)).toBe(true);
+});
+
+test('metrics table limits pixel and link boards to overview-page metric types', () => {
+ const definition = getComponentDefinition('MetricsTable');
+ const options = definition?.configFields?.find(field => field.name === 'type')?.optionsByEntityType;
+
+ expect(options?.pixel?.map(option => option.value)).toEqual([
+ 'referrer',
+ 'channel',
+ 'country',
+ 'region',
+ 'city',
+ 'browser',
+ 'os',
+ 'device',
+ ]);
+ expect(options?.link?.map(option => option.value)).toEqual([
+ 'referrer',
+ 'channel',
+ 'country',
+ 'region',
+ 'city',
+ 'browser',
+ 'os',
+ 'device',
+ ]);
+});
diff --git a/src/lib/boards.ts b/src/lib/boards.ts
index 77199fb88..c53865584 100644
--- a/src/lib/boards.ts
+++ b/src/lib/boards.ts
@@ -1,5 +1,5 @@
-import type { Board, BoardComponentConfig, BoardParameters } from './types';
import { isBoardComponentSupportedByEntityType } from './boardComponentCompatibility';
+import type { Board, BoardComponentConfig, BoardParameters } from './types';
export const BOARD_TYPES = {
dashboard: 'dashboard',
@@ -72,9 +72,7 @@ export function isOpenBoardType(type?: string) {
}
export function requiresBoardEntity(type?: string) {
- return (
- type === BOARD_TYPES.website || type === BOARD_TYPES.pixel || type === BOARD_TYPES.link
- );
+ return type === BOARD_TYPES.website || type === BOARD_TYPES.pixel || type === BOARD_TYPES.link;
}
export function getBoardEntity(board?: Pick | Partial): {
@@ -141,10 +139,7 @@ export function getResolvedComponentEntity(
return getComponentEntity(config);
}
-export function isBoardComponentSupported(
- componentType: string,
- entityType?: BoardEntityType,
-) {
+export function isBoardComponentSupported(componentType: string, entityType?: BoardEntityType) {
return isBoardComponentSupportedByEntityType(componentType, entityType);
}
diff --git a/src/lib/__tests__/charts.test.ts b/src/lib/charts.test.ts
similarity index 92%
rename from src/lib/__tests__/charts.test.ts
rename to src/lib/charts.test.ts
index e81be161c..884022b0e 100644
--- a/src/lib/__tests__/charts.test.ts
+++ b/src/lib/charts.test.ts
@@ -1,4 +1,5 @@
-import { renderNumberLabels } from '../charts';
+import { describe, expect, test } from 'vitest';
+import { renderNumberLabels } from './charts';
// test for renderNumberLabels
diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts
index 3329a2b75..bf9d72677 100644
--- a/src/lib/clickhouse.ts
+++ b/src/lib/clickhouse.ts
@@ -1,10 +1,10 @@
-import { CLICKHOUSE } from '@/lib/db';
import { type ClickHouseClient, createClient } from '@clickhouse/client';
import { formatInTimeZone } from 'date-fns-tz';
import debug from 'debug';
-import { DEFAULT_PAGE_SIZE, FILTER_COLUMNS, OPERATORS } from './constants';
+import { CLICKHOUSE } from '@/lib/db';
+import { DATA_TYPE, DEFAULT_PAGE_SIZE, FILTER_COLUMNS, OPERATORS } from './constants';
import { filtersObjectToArray } from './params';
-import type { EventPropertyFilter, Operator, QueryFilters, QueryOptions } from './types';
+import type { Operator, PropertyFilter, QueryFilters, QueryOptions } from './types';
export const CLICKHOUSE_DATE_FORMATS = {
utc: '%Y-%m-%dT%H:%i:%SZ',
@@ -79,7 +79,13 @@ function getSearchSQL(column: string, param: string = 'search'): string {
return `and positionCaseInsensitive(${column}, {${param}:String}) > 0`;
}
-function mapFilter(column: string, operator: Operator, name: string, type: string = 'String', paramName?: string) {
+function mapFilter(
+ column: string,
+ operator: Operator,
+ name: string,
+ type: string = 'String',
+ paramName?: string,
+) {
const param = paramName ?? name;
const value = `{${param}:${type}}`;
@@ -238,7 +244,11 @@ function parseFilters(filters: Record, options?: QueryOptions) {
};
}
-function getEventPropertyFilterQuery(filters: EventPropertyFilter[] = []): {
+function getPropertyFilterQuery(
+ filters: PropertyFilter[] = [],
+ propertyType: 'event' | 'session' = 'event',
+ timezone?: string,
+): {
sql: string;
params: Record;
} {
@@ -246,66 +256,113 @@ function getEventPropertyFilterQuery(filters: EventPropertyFilter[] = []): {
const parts: string[] = [];
const params: Record = {};
+ const table = propertyType === 'event' ? 'event_data' : 'session_data final';
+ const column = propertyType === 'event' ? 'event_id' : 'session_id';
+ const outerColumn = propertyType === 'event' ? 'event_id' : 'website_event.session_id';
+ const dateFilter =
+ propertyType === 'event'
+ ? `and created_at between {startDate:DateTime64} and {endDate:DateTime64}`
+ : '';
filters.forEach(({ propertyName, dataType, operator, value }, i) => {
- const keyParam = `epf_key_${i}`;
- const valParam = `epf_val_${i}`;
+ const keyParam = `pf_key_${i}`;
+ const valParam = `pf_val_${i}`;
params[keyParam] = propertyName;
- const isNumeric = dataType === 2;
- const col = isNumeric ? 'number_value' : 'string_value';
-
let condition: string;
- if (isNumeric) {
- params[valParam] = parseFloat(value) || 0;
- const opMap: Record = {
- [OPERATORS.equals]: `${col} = {${valParam}:Float64}`,
- [OPERATORS.notEquals]: `${col} != {${valParam}:Float64}`,
- [OPERATORS.greaterThan]: `${col} > {${valParam}:Float64}`,
- [OPERATORS.lessThan]: `${col} < {${valParam}:Float64}`,
- [OPERATORS.greaterThanEquals]: `${col} >= {${valParam}:Float64}`,
- [OPERATORS.lessThanEquals]: `${col} <= {${valParam}:Float64}`,
- };
- condition = opMap[operator] ?? `${col} = {${valParam}:Float64}`;
- } else if (EQUALITY_OPERATORS.includes(operator)) {
- const vals = value.split(',').filter(Boolean);
- if (!vals.length) return;
- params[valParam] = vals;
- condition = mapFilter(
- col,
- operator === OPERATORS.equals ? OPERATORS.equals : OPERATORS.notEquals,
- valParam,
- 'String',
- );
- } else if (REGEX_OPERATORS.includes(operator)) {
- if (!value) return;
- params[valParam] = value;
- condition = mapFilter(
- col,
- operator === OPERATORS.regex ? OPERATORS.regex : OPERATORS.notRegex,
- valParam,
- 'String',
- );
- } else {
- if (!value) return;
- params[valParam] = value;
- condition = mapFilter(
- col,
- operator === OPERATORS.contains ? OPERATORS.contains : OPERATORS.doesNotContain,
- valParam,
- 'String',
- );
+ switch (dataType) {
+ case DATA_TYPE.number: {
+ const col = 'number_value';
+ params[valParam] = parseFloat(value) || 0;
+ const opMap: Record = {
+ [OPERATORS.equals]: `${col} = {${valParam}:Float64}`,
+ [OPERATORS.notEquals]: `${col} != {${valParam}:Float64}`,
+ [OPERATORS.greaterThan]: `${col} > {${valParam}:Float64}`,
+ [OPERATORS.lessThan]: `${col} < {${valParam}:Float64}`,
+ [OPERATORS.greaterThanEquals]: `${col} >= {${valParam}:Float64}`,
+ [OPERATORS.lessThanEquals]: `${col} <= {${valParam}:Float64}`,
+ };
+ condition = opMap[operator] ?? `${col} = {${valParam}:Float64}`;
+ break;
+ }
+ case DATA_TYPE.date: {
+ if (!value) return;
+ params[valParam] = value;
+ const dateCol = timezone
+ ? `toDate(toTimezone(date_value, {timezone:String}))`
+ : `toDate(toTimezone(date_value, 'UTC'))`;
+ const opMap: Record = {
+ [OPERATORS.before]: `${dateCol} < {${valParam}:Date}`,
+ [OPERATORS.after]: `${dateCol} > {${valParam}:Date}`,
+ };
+ condition = opMap[operator] ?? `${dateCol} = {${valParam}:Date}`;
+ break;
+ }
+ case DATA_TYPE.array: {
+ if (!value) return;
+ params[valParam] = value;
+ condition =
+ operator === OPERATORS.contains
+ ? `has(JSONExtract(ifNull(string_value, '[]'), 'Array(String)'), {${valParam}:String})`
+ : `not has(JSONExtract(ifNull(string_value, '[]'), 'Array(String)'), {${valParam}:String})`;
+ break;
+ }
+ default: {
+ const col = 'string_value';
+
+ if (EQUALITY_OPERATORS.includes(operator)) {
+ const vals = value.split(',').filter(Boolean);
+ if (!vals.length) return;
+ params[valParam] = vals;
+ condition = mapFilter(
+ col,
+ operator === OPERATORS.equals ? OPERATORS.equals : OPERATORS.notEquals,
+ valParam,
+ 'String',
+ );
+ } else if (REGEX_OPERATORS.includes(operator)) {
+ if (!value) return;
+ params[valParam] = value;
+ condition = mapFilter(
+ col,
+ operator === OPERATORS.regex ? OPERATORS.regex : OPERATORS.notRegex,
+ valParam,
+ 'String',
+ );
+ } else {
+ if (!value) return;
+ params[valParam] = value;
+ condition = mapFilter(
+ col,
+ operator === OPERATORS.contains ? OPERATORS.contains : OPERATORS.doesNotContain,
+ valParam,
+ 'String',
+ );
+ }
+ break;
+ }
}
- parts.push(`and event_id in (
- select event_id
- from event_data
+ if (propertyType === 'session') {
+ parts.push(`and tuple(website_event.website_id, website_event.session_id) in (
+ select website_id, session_id
+ from ${table}
where website_id = {websiteId:UUID}
- and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and data_key = {${keyParam}:String}
and data_type = ${dataType}
and ${condition}
)`);
+ } else {
+ parts.push(`and ${outerColumn} in (
+ select ${column}
+ from ${table}
+ where website_id = {websiteId:UUID}
+ ${dateFilter}
+ and data_key = {${keyParam}:String}
+ and data_type = ${dataType}
+ and ${condition}
+ )`);
+ }
});
return { sql: parts.join('\n'), params };
@@ -329,13 +386,23 @@ async function pagedRawQuery(
.filter(n => n)
.join('\n');
- const count = await rawQuery(`select count(*) as num from (${query}) t`, queryParams).then(
- res => res[0].num,
- );
+ const { maxResults } = filters;
+ const countQuery = maxResults
+ ? `select count(*) as num from (select 1 from (${query}) t limit ${+maxResults}) t2`
+ : `select count(*) as num from (${query}) t`;
+ const count = await rawQuery(countQuery, queryParams).then(res => res[0].num);
const data = await rawQuery(`${query}${statements}`, queryParams, name);
- return { data, count, page: +page, pageSize: size, orderBy, search };
+ return {
+ data,
+ count,
+ page: +page,
+ pageSize: size,
+ orderBy,
+ search,
+ isCapped: !!maxResults && +count >= +maxResults,
+ };
}
async function rawQuery(
@@ -397,7 +464,7 @@ export default {
getDateSQL,
getSearchSQL,
getFilterQuery,
- getEventPropertyFilterQuery,
+ getPropertyFilterQuery,
getUTCString,
parseFilters,
pagedRawQuery,
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index f7c182b15..5a5234d1c 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -10,6 +10,8 @@ export const LAST_TEAM_CONFIG = 'umami.last-team';
export const VERSION_CHECK = 'umami.version-check';
export const SHARE_TOKEN_HEADER = 'x-umami-share-token';
export const SHARE_CONTEXT_HEADER = 'x-umami-share-context';
+export const SHARE_TOKEN_TYPE = 'share';
+export const CACHE_TOKEN_TYPE = 'cache';
export const HOMEPAGE_URL = 'https://umami.is';
export const DOCS_URL = 'https://umami.is/docs';
export const REPO_URL = 'https://github.com/umami-software/umami';
@@ -26,6 +28,7 @@ export const DEFAULT_DATE_RANGE_VALUE = '24hour';
export const DEFAULT_WEBSITE_LIMIT = 10;
export const DEFAULT_RESET_DATE = '2000-01-01';
export const DEFAULT_PAGE_SIZE = 20;
+export const MAX_PAGING_RESULTS = 10000;
export const DEFAULT_DATE_COMPARE = 'prev';
export const DEFAULT_CURRENCY = 'USD';
@@ -36,6 +39,7 @@ export const UNIT_TYPES = ['year', 'month', 'hour', 'day', 'minute'];
export const EVENT_COLUMNS = [
'path',
+ 'fullPath',
'entry',
'exit',
'referrer',
@@ -119,6 +123,11 @@ export const EVENT_TYPE = {
performance: 5,
} as const;
+export const HEATMAP_EVENT_TYPE = {
+ click: 1,
+ scroll: 2,
+} as const;
+
export const ENTITY_TYPE = {
website: 1,
link: 2,
@@ -171,6 +180,13 @@ export const ROLES = {
teamViewOnly: 'team-view-only',
} as const;
+export const TEAM_ROLE_RANK: Record = {
+ [ROLES.teamViewOnly]: 0,
+ [ROLES.teamMember]: 1,
+ [ROLES.teamManager]: 2,
+ [ROLES.teamOwner]: 3,
+} as const;
+
export const PERMISSIONS = {
all: 'all',
websiteCreate: 'website:create',
@@ -250,11 +266,28 @@ export const DOMAIN_REGEX =
/^(localhost(:[1-9]\d{0,4})?|((?=[a-z0-9-_]{1,63}\.)(xn--)?[a-z0-9-_]+(-[a-z0-9-_]+)*\.)+(xn--)?[a-z0-9-_]{2,63})$/;
export const SHARE_ID_REGEX = /^[a-zA-Z0-9]{8,50}$/;
export const DATETIME_REGEX =
- /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{3}(Z|\+[0-9]{2}:[0-9]{2})?)?$/;
+ /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{3})?(Z|\+[0-9]{2}:[0-9]{2})?$/;
-export const URL_LENGTH = 500;
-export const PAGE_TITLE_LENGTH = 500;
-export const EVENT_NAME_LENGTH = 50;
+export const FIELD_LENGTH = {
+ browser: 20,
+ os: 20,
+ device: 20,
+ screen: 11,
+ language: 35,
+ country: 2,
+ region: 20,
+ city: 50,
+ distinctId: 50,
+ url: 500,
+ pageTitle: 500,
+ eventName: 50,
+ tag: 50,
+ hostname: 100,
+ fieldValue: 255,
+ dataKey: 500,
+ stringValue: 500,
+ currency: 10,
+} as const;
export const UTM_PARAMS = ['utm_campaign', 'utm_content', 'utm_medium', 'utm_source', 'utm_term'];
@@ -271,6 +304,7 @@ export const BROWSERS = {
aol: 'AOL',
bb10: 'BlackBerry 10',
beaker: 'Beaker',
+ browser: 'Unknown',
chrome: 'Chrome',
'chromium-webview': 'Chrome (webview)',
crios: 'Chrome (iOS)',
@@ -318,16 +352,23 @@ export const SOCIAL_DOMAINS = [
export const SEARCH_DOMAINS = [
'baidu.com',
'bing.com',
- 'chatgpt.com',
'duckduckgo.com',
'ecosia.org',
'google.',
'msn.com',
- 'perplexity.ai',
'search.brave.com',
'yandex.',
];
+export const LLM_DOMAINS = [
+ 'chatgpt.com',
+ 'claude.ai',
+ 'copilot.microsoft.com',
+ 'gemini.google.com',
+ 'meta.ai',
+ 'perplexity.ai',
+];
+
export const SHOPPING_DOMAINS = [
'alibaba.com',
'aliexpress.com',
@@ -356,7 +397,6 @@ export const PAID_AD_PARAMS = [
'aid=',
'dclid=',
'epik=',
- 'fbclid=',
'gclid=',
'li_fat_id=',
'msclkid=',
diff --git a/src/lib/data.test.ts b/src/lib/data.test.ts
new file mode 100644
index 000000000..c2000d774
--- /dev/null
+++ b/src/lib/data.test.ts
@@ -0,0 +1,17 @@
+import { describe, expect, test } from 'vitest';
+import { DATA_TYPE, FIELD_LENGTH } from './constants';
+import { getStoredStringValue } from './data';
+
+describe('getStoredStringValue', () => {
+ test('truncates oversized string values to the storage limit', () => {
+ expect(getStoredStringValue('x'.repeat(FIELD_LENGTH.stringValue + 25), DATA_TYPE.string)).toHaveLength(
+ FIELD_LENGTH.stringValue,
+ );
+ });
+
+ test('drops oversized array payloads instead of storing invalid truncated JSON', () => {
+ const oversizedArray = JSON.stringify([`x${'y'.repeat(FIELD_LENGTH.stringValue)}`]);
+
+ expect(getStoredStringValue(oversizedArray, DATA_TYPE.array)).toBeNull();
+ });
+});
diff --git a/src/lib/data.ts b/src/lib/data.ts
index fe69edf04..70cbdfed8 100644
--- a/src/lib/data.ts
+++ b/src/lib/data.ts
@@ -1,27 +1,27 @@
-import { DATA_TYPE, DATETIME_REGEX } from './constants';
+import { DATA_TYPE, DATETIME_REGEX, FIELD_LENGTH } from './constants';
+import { truncateString } from './format';
import type { DynamicDataType } from './types';
-export function flattenJSON(
- eventData: Record,
- keyValues: { key: string; value: any; dataType: DynamicDataType }[] = [],
- parentKey = '',
-): { key: string; value: any; dataType: DynamicDataType }[] {
- return Object.keys(eventData).reduce(
- (acc, key) => {
- const value = eventData[key];
- const type = typeof eventData[key];
+export interface KeyValueData {
+ key: string;
+ value: any;
+ dataType: DynamicDataType;
+}
- // nested object
- if (value && type === 'object' && !Array.isArray(value) && !isValidDateValue(value)) {
- flattenJSON(value, acc.keyValues, getKeyName(key, parentKey));
- } else {
- createKey(getKeyName(key, parentKey), value, acc);
+export function flattenJSON(eventData: Record): KeyValueData[] {
+ function flatten(obj: Record, parentKey: string): KeyValueData[] {
+ return Object.entries(obj).flatMap(([key, value]) => {
+ const fullKey = parentKey ? `${parentKey}.${key}` : key;
+
+ if (value && typeof value === 'object' && !Array.isArray(value) && !isValidDateValue(value)) {
+ return flatten(value, fullKey);
}
- return acc;
- },
- { keyValues, parentKey },
- ).keyValues;
+ return [createKeyValue(fullKey, value)];
+ });
+ }
+
+ return flatten(eventData, '');
}
export function isValidDateValue(value: string) {
@@ -50,10 +50,20 @@ export function getStringValue(value: string, dataType: number) {
return value;
}
-function createKey(key: string, value: string, acc: { keyValues: any[]; parentKey: string }) {
- const type = getDataType(value);
+export function getStoredStringValue(value: string, dataType: number) {
+ const stringValue = getStringValue(value, dataType);
- let dataType = null;
+ if (dataType === DATA_TYPE.array && stringValue.length > FIELD_LENGTH.stringValue) {
+ return null;
+ }
+
+ return truncateString(stringValue, FIELD_LENGTH.stringValue);
+}
+
+export function createKeyValue(key: string, value: any): KeyValueData {
+ const type = getDataType(value);
+ let dataType: DynamicDataType;
+ let processedValue = value;
switch (type) {
case 'number':
@@ -64,29 +74,21 @@ function createKey(key: string, value: string, acc: { keyValues: any[]; parentKe
break;
case 'boolean':
dataType = DATA_TYPE.boolean;
- value = value ? 'true' : 'false';
+ processedValue = value ? 'true' : 'false';
break;
case 'date':
dataType = DATA_TYPE.date;
break;
case 'object':
dataType = DATA_TYPE.array;
- value = JSON.stringify(value);
+ processedValue = JSON.stringify(value);
break;
default:
dataType = DATA_TYPE.string;
break;
}
- acc.keyValues.push({ key, value, dataType });
-}
-
-function getKeyName(key: string, parentKey: string) {
- if (!parentKey) {
- return key;
- }
-
- return `${parentKey}.${key}`;
+ return { key, value: processedValue, dataType };
}
export function objectToArray(obj: object) {
diff --git a/src/lib/date.ts b/src/lib/date.ts
index ae5ce2a76..82fba23f8 100644
--- a/src/lib/date.ts
+++ b/src/lib/date.ts
@@ -113,6 +113,10 @@ export function normalizeTimezone(timezone: string): string {
return TIMEZONE_MAPPINGS[timezone] || timezone;
}
+export function getMaxSelectableDate(now = new Date()) {
+ return max([endOfYear(now), addMonths(now, 6)]);
+}
+
export function isValidTimezone(timezone: string) {
try {
const normalizedTimezone = normalizeTimezone(timezone);
diff --git a/src/lib/db.ts b/src/lib/db.ts
index 7b6e8368b..f363a9df9 100644
--- a/src/lib/db.ts
+++ b/src/lib/db.ts
@@ -5,7 +5,7 @@ export const KAFKA = 'kafka';
export const KAFKA_PRODUCER = 'kafka-producer';
// Fixes issue with converting bigint values
-BigInt.prototype.toJSON = function () {
+(BigInt.prototype as unknown as { toJSON(): number }).toJSON = function () {
return Number(this);
};
diff --git a/src/lib/detect.test.ts b/src/lib/detect.test.ts
new file mode 100644
index 000000000..804c84029
--- /dev/null
+++ b/src/lib/detect.test.ts
@@ -0,0 +1,77 @@
+import { beforeEach, expect, test, vi } from 'vitest';
+import { getLocation, hasBlockedIp } from './detect';
+import { getIpAddress } from './ip';
+
+const IP = '127.0.0.1';
+
+const isLocalhost = vi.mocked(await import('is-localhost-ip'));
+
+vi.mock('is-localhost-ip', () => ({
+ default: vi.fn(),
+}));
+
+beforeEach(() => {
+ vi.resetAllMocks();
+
+ delete process.env.CLIENT_IP_HEADER;
+ delete process.env.IGNORE_IP;
+ delete process.env.SKIP_LOCATION_HEADERS;
+});
+
+test('getIpAddress: Custom header', () => {
+ process.env.CLIENT_IP_HEADER = 'x-custom-ip-header';
+
+ expect(getIpAddress(new Headers({ 'x-custom-ip-header': IP }))).toEqual(IP);
+});
+
+test('getIpAddress: CloudFlare header', () => {
+ expect(getIpAddress(new Headers({ 'cf-connecting-ip': IP }))).toEqual(IP);
+});
+
+test('getIpAddress: Standard header', () => {
+ expect(getIpAddress(new Headers({ 'x-forwarded-for': IP }))).toEqual(IP);
+});
+
+test('getIpAddress: No header', () => {
+ expect(getIpAddress(new Headers())).toEqual(undefined);
+});
+
+test('getLocation: returns null for malformed ip', async () => {
+ await expect(
+ getLocation(
+ 'not-an-ip',
+ new Headers({
+ 'cf-ipcountry': 'US',
+ 'cf-region-code': 'CA',
+ 'cf-ipcity': 'Los Angeles',
+ }),
+ false,
+ ),
+ ).resolves.toEqual(null);
+});
+
+test('getLocation: treats localhost check errors as non-local', async () => {
+ isLocalhost.default.mockRejectedValue(new Error('DNS Lookup failed.'));
+
+ await expect(
+ getLocation(
+ '8.8.8.8',
+ new Headers({
+ 'cf-ipcountry': 'US',
+ 'cf-region-code': 'CA',
+ 'cf-ipcity': 'Los Angeles',
+ }),
+ false,
+ ),
+ ).resolves.toEqual({
+ country: 'US',
+ region: 'US-CA',
+ city: 'Los Angeles',
+ });
+});
+
+test('hasBlockedIp: returns false for malformed client ip with cidr block', () => {
+ process.env.IGNORE_IP = '10.0.0.0/8';
+
+ expect(hasBlockedIp('not-an-ip')).toBe(false);
+});
diff --git a/src/lib/detect.ts b/src/lib/detect.ts
index 2e78a1f35..fb36ba212 100644
--- a/src/lib/detect.ts
+++ b/src/lib/detect.ts
@@ -76,9 +76,19 @@ function decodeHeader(s: string | undefined | null): string | undefined | null {
return Buffer.from(s, 'latin1').toString('utf-8');
}
+async function isLocalIp(ip: string) {
+ try {
+ return await isLocalhost(ip);
+ } catch {
+ return false;
+ }
+}
+
export async function getLocation(ip: string = '', headers: Headers, skipHeaders: boolean) {
- // Ignore local ips
- if (!ip || (await isLocalhost(ip))) {
+ const cleanIp = stripPort(ip);
+
+ // Ignore local or invalid ips
+ if (!cleanIp || !ipaddr.isValid(cleanIp) || (await isLocalIp(cleanIp))) {
return null;
}
@@ -108,7 +118,7 @@ export async function getLocation(ip: string = '', headers: Headers, skipHeaders
);
}
- const result = globalThis[MAXMIND]?.get(stripPort(ip));
+ const result = globalThis[MAXMIND]?.get(cleanIp);
if (result) {
const country = result.country?.iso_code ?? result?.registered_country?.iso_code;
@@ -140,31 +150,31 @@ export async function getClientInfo(request: Request, payload: Record n.trim()));
+ const ips = ignoreIps.split(',').map(n => n.trim());
+
+ return ips.some(ip => {
+ if (ip === clientIp) {
+ return true;
}
- return ips.find(ip => {
- if (ip === clientIp) {
- return true;
- }
-
- // CIDR notation
- if (ip.indexOf('/') > 0) {
+ // CIDR notation
+ if (ip.indexOf('/') > 0) {
+ try {
const addr = ipaddr.parse(clientIp);
const range = ipaddr.parseCIDR(ip);
if (addr.kind() === range[0].kind() && addr.match(range)) {
return true;
}
+ } catch {
+ // Ignore parsing errors
}
+ }
- return false;
- });
- }
-
- return false;
+ return false;
+ });
}
diff --git a/src/lib/__tests__/format.test.ts b/src/lib/format.test.ts
similarity index 66%
rename from src/lib/__tests__/format.test.ts
rename to src/lib/format.test.ts
index 6e1b319f0..b70b5f2e1 100644
--- a/src/lib/__tests__/format.test.ts
+++ b/src/lib/format.test.ts
@@ -1,4 +1,5 @@
-import * as format from '../format';
+import { expect, test } from 'vitest';
+import * as format from './format';
test('parseTime', () => {
expect(format.parseTime(86400 + 3600 + 60 + 1)).toEqual({
@@ -36,3 +37,14 @@ test('stringToColor', () => {
expect(format.stringToColor('hello')).toBe('#d218e9');
expect(format.stringToColor('goodbye')).toBe('#11e956');
});
+
+test('truncateString', () => {
+ expect(format.truncateString('hello', 3)).toBe('hel');
+ expect(format.truncateString(undefined, 3)).toBeUndefined();
+});
+
+test('decodePunycodeDomain', () => {
+ expect(format.decodePunycodeDomain('xn--bcher-kva.example')).toBe('bücher.example');
+ expect(format.decodePunycodeDomain('example.com')).toBe('example.com');
+ expect(format.decodePunycodeDomain(undefined)).toBeUndefined();
+});
diff --git a/src/lib/format.ts b/src/lib/format.ts
index 035a18111..07110d935 100644
--- a/src/lib/format.ts
+++ b/src/lib/format.ts
@@ -1,3 +1,4 @@
+import { toUnicode } from 'punycode';
import { DEFAULT_CURRENCY } from './constants';
export function parseTime(val: number) {
@@ -118,3 +119,22 @@ export function formatLongCurrency(value: number, currency: string, locale = 'en
return formatCurrency(n, currency, locale);
}
+
+export function decodePunycodeDomain(domain?: string | null) {
+ if (!domain) {
+ return domain;
+ }
+
+ try {
+ return toUnicode(domain);
+ } catch {
+ return domain;
+ }
+}
+
+export function truncateString(
+ value: T,
+ maxLength: number,
+): T extends string ? string : T {
+ return (value ? value.substring(0, maxLength) : value) as T extends string ? string : T;
+}
diff --git a/src/lib/generate.ts b/src/lib/generate.ts
index 752c0b609..0d3a0de84 100644
--- a/src/lib/generate.ts
+++ b/src/lib/generate.ts
@@ -1,11 +1,8 @@
-import { uniformInt } from 'pure-rand/distribution/uniformInt';
-import { xoroshiro128plus } from 'pure-rand/generator/xoroshiro128plus';
-
-const seed = Date.now() ^ (Math.random() * 0x100000000);
-const rng = xoroshiro128plus(seed);
-
export function random(min: number, max: number) {
- return uniformInt(rng, min, max);
+ const range = max - min + 1;
+ const buf = new Uint32Array(1);
+ globalThis.crypto.getRandomValues(buf);
+ return min + (buf[0] % range);
}
export function getRandomChars(
diff --git a/src/lib/__tests__/get-base-url.test.ts b/src/lib/get-base-url.test.ts
similarity index 88%
rename from src/lib/__tests__/get-base-url.test.ts
rename to src/lib/get-base-url.test.ts
index 5bca0f193..f43148d5c 100644
--- a/src/lib/__tests__/get-base-url.test.ts
+++ b/src/lib/get-base-url.test.ts
@@ -1,5 +1,6 @@
-import { HOMEPAGE_URL } from '../constants';
-import { getBaseUrl } from '../get-base-url';
+import { expect, test } from 'vitest';
+import { HOMEPAGE_URL } from './constants';
+import { getBaseUrl } from './get-base-url';
function createHeaders(entries: Record) {
return {
diff --git a/src/lib/ip.ts b/src/lib/ip.ts
index a78c7e56e..075a481f5 100644
--- a/src/lib/ip.ts
+++ b/src/lib/ip.ts
@@ -16,11 +16,6 @@ export const IP_ADDRESS_HEADERS = [
'x-forwarded',
];
-/**
- * Normalize IP strings to a canonical form:
- * - strips IPv4-mapped IPv6 (e.g. ::ffff:192.0.2.1 -> 192.0.2.1)
- * - keeps valid IPv4/IPv6 as-is (canonically formatted by ipaddr.js)
- */
function normalizeIp(ip?: string | null) {
if (!ip) return ip;
@@ -84,9 +79,7 @@ export function getIpAddress(headers: Headers) {
if (header === 'forwarded') {
const match = ip.match(/for=(\[?[0-9a-fA-F:.]+]?)/);
- if (match) {
- return resolveIp(match[1]);
- }
+ return match ? resolveIp(match[1]) : undefined;
}
return resolveIp(ip);
diff --git a/src/lib/kafka.ts b/src/lib/kafka.ts
index 1d60e1f28..3e25eae4d 100644
--- a/src/lib/kafka.ts
+++ b/src/lib/kafka.ts
@@ -8,11 +8,35 @@ const log = debug('umami:kafka');
const CONNECT_TIMEOUT = 5000;
const SEND_TIMEOUT = 3000;
const ACKS = 1;
+const DEFAULT_MAX_MESSAGE_BYTES = 900_000;
let kafka: Kafka;
let producer: Producer;
const enabled = Boolean(process.env.KAFKA_URL && process.env.KAFKA_BROKER);
+type KafkaMessage = Record;
+type KafkaProducerMessage = { value: string };
+
+function getMaxMessageBytes() {
+ const size = Number(process.env.KAFKA_MAX_MESSAGE_BYTES);
+
+ return Number.isFinite(size) && size > 0 ? size : DEFAULT_MAX_MESSAGE_BYTES;
+}
+
+function getMessageSize(value: string) {
+ return Buffer.byteLength(value, 'utf8');
+}
+
+function getMessages(message: KafkaMessage | KafkaMessage[]) {
+ const items = Array.isArray(message) ? message : [message];
+
+ return items.map(item => {
+ const value = JSON.stringify(item);
+
+ return { value, size: getMessageSize(value) };
+ });
+}
+
function getClient() {
const { username, password } = new URL(process.env.KAFKA_URL);
const brokers = process.env.KAFKA_BROKER.split(',');
@@ -23,7 +47,7 @@ function getClient() {
username && password
? {
ssl: {
- rejectUnauthorized: false,
+ rejectUnauthorized: process.env.KAFKA_SSL_ALLOW_UNAUTHORIZED !== 'true',
},
sasl: {
mechanism,
@@ -65,28 +89,57 @@ async function getProducer(): Promise {
async function sendMessage(
topic: string,
- message: Record | Record[],
+ message: KafkaMessage | KafkaMessage[],
): Promise {
try {
await connect();
- return producer.send({
- topic,
- messages: Array.isArray(message)
- ? message.map(a => {
- return { value: JSON.stringify(a) };
- })
- : [
- {
- value: JSON.stringify(message),
- },
- ],
- timeout: SEND_TIMEOUT,
- acks: ACKS,
- });
+ const maxMessageBytes = getMaxMessageBytes();
+ const messages = getMessages(message);
+ const result: RecordMetadata[] = [];
+ let batch: KafkaProducerMessage[] = [];
+ let batchSize = 0;
+
+ for (const { value, size } of messages) {
+ if (size > maxMessageBytes) {
+ log('Kafka message dropped: topic=%s size=%d max=%d', topic, size, maxMessageBytes);
+ continue;
+ }
+
+ if (batch.length && batchSize + size > maxMessageBytes) {
+ result.push(
+ ...(await producer.send({
+ topic,
+ messages: batch,
+ timeout: SEND_TIMEOUT,
+ acks: ACKS,
+ })),
+ );
+ batch = [];
+ batchSize = 0;
+ }
+
+ batch.push({ value });
+ batchSize += size;
+ }
+
+ if (batch.length) {
+ result.push(
+ ...(await producer.send({
+ topic,
+ messages: batch,
+ timeout: SEND_TIMEOUT,
+ acks: ACKS,
+ })),
+ );
+ }
+
+ return result;
} catch (e) {
// eslint-disable-next-line no-console
console.log('KAFKA ERROR:', serializeError(e));
+
+ return [];
}
}
diff --git a/src/lib/__tests__/match-configured-path.test.ts b/src/lib/match-configured-path.test.ts
similarity index 84%
rename from src/lib/__tests__/match-configured-path.test.ts
rename to src/lib/match-configured-path.test.ts
index f70ba9c1e..5b957bb52 100644
--- a/src/lib/__tests__/match-configured-path.test.ts
+++ b/src/lib/match-configured-path.test.ts
@@ -1,4 +1,5 @@
-import { matchesConfiguredPath } from '../match-configured-path';
+import { expect, test } from 'vitest';
+import { matchesConfiguredPath } from './match-configured-path';
test('matches the exact configured path', () => {
expect(matchesConfiguredPath('/d.js', 'd.js')).toBe(true);
diff --git a/src/lib/params.ts b/src/lib/params.ts
index 0f3c7f905..f924ee30a 100644
--- a/src/lib/params.ts
+++ b/src/lib/params.ts
@@ -1,14 +1,12 @@
-import { FILTER_COLUMNS, OPERATORS } from '@/lib/constants';
-import type { EventPropertyFilter, Filter, Operator, QueryFilters, QueryOptions } from '@/lib/types';
+import { DATA_TYPE, FILTER_COLUMNS, OPERATORS } from '@/lib/constants';
+import type { Filter, Operator, PropertyFilter, QueryFilters, QueryOptions } from '@/lib/types';
const VALID_OPERATORS: Operator[] = Object.values(OPERATORS);
-const NUMERIC_EVENT_PROPERTY_OPERATORS: Operator[] = [
- OPERATORS.greaterThan,
- OPERATORS.lessThan,
- OPERATORS.greaterThanEquals,
- OPERATORS.lessThanEquals,
-];
-const EQUALITY_OPERATORS: Operator[] = [OPERATORS.equals, OPERATORS.notEquals];
+const VALID_EVENT_DATA_TYPES = Object.values(DATA_TYPE);
+
+function isValidEventDataType(value: number): value is (typeof VALID_EVENT_DATA_TYPES)[number] {
+ return VALID_EVENT_DATA_TYPES.includes(value as (typeof VALID_EVENT_DATA_TYPES)[number]);
+}
function resolveOperator(value?: string): Operator | undefined {
if (!value) {
@@ -106,36 +104,75 @@ export function filtersArrayToObject(filters: Filter[]) {
}, {});
}
-export function parseEventPropertyFilters(query: Record): EventPropertyFilter[] {
- return Object.entries(query)
- .filter(([key]) => /^epf_/.test(key))
- .flatMap(([key, val]) => {
- const dotIndex = (val as string).indexOf('.');
- if (dotIndex < 1) return [];
- const withoutPrefix = key.slice(4); // strip "epf_"
- const propertyName = withoutPrefix.replace(/\d+$/, ''); // strip trailing index digits
- const rawOperator = (val as string).slice(0, dotIndex);
- const operator = resolveOperator(rawOperator);
- if (!operator) {
- return [];
- }
- const value = (val as string).slice(dotIndex + 1);
- const isNumeric =
- NUMERIC_EVENT_PROPERTY_OPERATORS.includes(operator) ||
- (EQUALITY_OPERATORS.includes(operator) &&
- value !== '' &&
- !Number.isNaN(Number(value)));
- return [{ propertyName, dataType: isNumeric ? 2 : 1, operator, value }];
- });
+function getPropertyFilterPrefix(key: string, prefixes: string[]) {
+ return prefixes.find(prefix => key.startsWith(`${prefix}_`));
}
-export function serializeEventPropertyFilters(filters: EventPropertyFilter[]): Record {
+export function parsePropertyFilters(
+ query: Record,
+ prefixes: string[] = ['pf'],
+): PropertyFilter[] {
+ return Object.entries(query).flatMap(([key, val]) => {
+ const prefix = getPropertyFilterPrefix(key, prefixes);
+
+ if (!prefix) {
+ return [];
+ }
+
+ const stringValue = String(val);
+ const withoutPrefix = key.slice(prefix.length + 1);
+ const propertyName = withoutPrefix.replace(/\d+$/, ''); // strip trailing index digits
+ const prefixedDotMatch = stringValue.match(/^(\d+)\.([^.]+)\.(.*)$/);
+ const untypedDotMatch = stringValue.match(/^([^.]+)\.(.*)$/);
+ const explicitDataType = prefixedDotMatch ? Number(prefixedDotMatch[1]) : undefined;
+ const rawOperator = prefixedDotMatch ? prefixedDotMatch[2] : untypedDotMatch?.[1];
+ const operator = resolveOperator(rawOperator);
+
+ if (
+ !operator ||
+ (explicitDataType !== undefined && !isValidEventDataType(explicitDataType))
+ ) {
+ return [];
+ }
+
+ const value = prefixedDotMatch ? prefixedDotMatch[3] : untypedDotMatch?.[2];
+
+ if (value === undefined) {
+ return [];
+ }
+
+ return [
+ {
+ propertyName,
+ dataType: explicitDataType ?? DATA_TYPE.string,
+ operator,
+ value,
+ },
+ ];
+ });
+}
+
+export function parseEventPropertyFilters(query: Record) {
+ return parsePropertyFilters(query, ['pf']);
+}
+
+export function serializePropertyFilters(
+ filters: PropertyFilter[],
+ prefix = 'pf',
+): Record {
const counts: Record = {};
return Object.fromEntries(
filters.map(f => {
const n = counts[f.propertyName] ?? 0;
counts[f.propertyName] = n + 1;
- return [`epf_${f.propertyName}${n > 0 ? n : ''}`, `${f.operator}.${f.value}`];
+ return [
+ `${prefix}_${f.propertyName}${n > 0 ? n : ''}`,
+ `${f.dataType}.${f.operator}.${f.value}`,
+ ];
}),
);
}
+
+export function serializeEventPropertyFilters(filters: PropertyFilter[]) {
+ return serializePropertyFilters(filters, 'pf');
+}
diff --git a/src/lib/prisma.test.ts b/src/lib/prisma.test.ts
new file mode 100644
index 000000000..3a8e9df2e
--- /dev/null
+++ b/src/lib/prisma.test.ts
@@ -0,0 +1,82 @@
+import { beforeAll, describe, expect, test, vi } from 'vitest';
+
+process.env.DATABASE_URL ??= 'postgresql://user:pass@localhost:5432/umami?schema=public';
+delete process.env.DATABASE_REPLICA_URL;
+
+vi.mock('@prisma/adapter-pg', () => ({
+ PrismaPg: class PrismaPg {},
+}));
+
+vi.mock('@prisma/extension-read-replicas', () => ({
+ readReplicas: () => () => ({}),
+}));
+
+vi.mock('@/generated/prisma/client', () => ({
+ PrismaClient: class PrismaClient {
+ $executeRawUnsafe = vi.fn();
+ $queryRawUnsafe = vi.fn();
+ $transaction = vi.fn();
+ $on = vi.fn();
+ $extends() {
+ return this;
+ }
+ },
+}));
+
+let getRawQueryClient!: typeof import('./prisma').getRawQueryClient;
+
+beforeAll(async () => {
+ ({ getRawQueryClient } = await import('./prisma'));
+});
+
+interface RawQueryClient {
+ $executeRawUnsafe: (query: string, ...params: any[]) => unknown;
+ $queryRawUnsafe: (query: string, ...params: any[]) => unknown;
+ $primary?: () => unknown;
+ $replica?: () => unknown;
+}
+
+function createClient(): RawQueryClient {
+ return {
+ $executeRawUnsafe: vi.fn(),
+ $queryRawUnsafe: vi.fn(),
+ };
+}
+
+describe('getRawQueryClient', () => {
+ test('uses a replica client for read queries when replicas are enabled', () => {
+ const replica = createClient();
+ const client = {
+ ...createClient(),
+ $replica: vi.fn(() => replica),
+ };
+
+ expect(getRawQueryClient(client, { useReplica: true })).toBe(replica);
+ });
+
+ test('keeps read queries on the primary client when replicas are disabled', () => {
+ const client = {
+ ...createClient(),
+ $replica: vi.fn(() => createClient()),
+ };
+
+ expect(getRawQueryClient(client, { useReplica: false })).toBe(client);
+ });
+
+ test('uses the primary client for raw writes when available', () => {
+ const primary = createClient();
+ const client = {
+ ...createClient(),
+ $primary: vi.fn(() => primary),
+ $replica: vi.fn(() => createClient()),
+ };
+
+ expect(getRawQueryClient(client, { useReplica: true, write: true })).toBe(primary);
+ });
+
+ test('falls back to the current client for raw writes without a primary helper', () => {
+ const client = createClient();
+
+ expect(getRawQueryClient(client, { write: true })).toBe(client);
+ });
+});
diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts
index 8b9f143fc..1d5fe46b0 100644
--- a/src/lib/prisma.ts
+++ b/src/lib/prisma.ts
@@ -1,10 +1,16 @@
-import { PrismaClient } from '@/generated/prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import { readReplicas } from '@prisma/extension-read-replicas';
import debug from 'debug';
-import { DEFAULT_PAGE_SIZE, FILTER_COLUMNS, OPERATORS, SESSION_COLUMNS } from './constants';
+import { PrismaClient } from '@/generated/prisma/client';
+import {
+ DATA_TYPE,
+ DEFAULT_PAGE_SIZE,
+ FILTER_COLUMNS,
+ OPERATORS,
+ SESSION_COLUMNS,
+} from './constants';
import { filtersObjectToArray } from './params';
-import type { EventPropertyFilter, Operator, QueryFilters, QueryOptions } from './types';
+import type { Operator, PropertyFilter, QueryFilters, QueryOptions } from './types';
const log = debug('umami:prisma');
@@ -23,6 +29,46 @@ const PRISMA_LOG_OPTIONS = {
],
};
+export interface RawQueryExecutor {
+ $executeRawUnsafe: (query: string, ...params: any[]) => unknown;
+ $queryRawUnsafe: (query: string, ...params: any[]) => unknown;
+}
+
+export interface RawQueryClient extends RawQueryExecutor {
+ $primary?: () => unknown;
+ $replica?: () => unknown;
+}
+
+function isRawQueryExecutor(value: unknown): value is RawQueryExecutor {
+ return !!value && typeof value === 'object' && '$executeRawUnsafe' in value && '$queryRawUnsafe' in value;
+}
+
+export function getRawQueryClient(
+ client: RawQueryClient,
+ {
+ useReplica = false,
+ write = false,
+ }: {
+ useReplica?: boolean;
+ write?: boolean;
+ } = {},
+) {
+ if (write) {
+ const primary = typeof client.$primary === 'function' ? client.$primary() : null;
+ return isRawQueryExecutor(primary) ? primary : client;
+ }
+
+ if (useReplica && typeof client.$replica === 'function') {
+ const replica = client.$replica();
+
+ if (isRawQueryExecutor(replica)) {
+ return replica;
+ }
+ }
+
+ return client;
+}
+
const DATE_FORMATS = {
minute: 'YYYY-MM-DD HH24:MI:00',
hour: 'YYYY-MM-DD HH24:00:00',
@@ -44,6 +90,10 @@ const DATE_STRING_FORMATS = {
second: 'YYYY-MM-DD"T"HH24:MI:SS',
};
+function isUtcTimezone(timezone?: string) {
+ return timezone?.toLowerCase() === 'utc';
+}
+
function getAddIntervalQuery(field: string, interval: string): string {
return `${field} + interval '${interval}'`;
}
@@ -57,15 +107,19 @@ function getCastColumnQuery(field: string, type: string): string {
}
function getDateSQL(field: string, unit: string, timezone?: string): string {
- if (timezone && timezone !== 'utc') {
+ if (timezone && !isUtcTimezone(timezone)) {
return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${DATE_FORMATS[unit]}')`;
}
return `to_char(date_trunc('${unit}', ${field}), '${DATE_FORMATS_UTC[unit]}')`;
}
-function getDateStringSQL(field: string, unit: keyof typeof DATE_STRING_FORMATS = 'utc', timezone?: string): string {
- if (timezone && timezone !== 'utc') {
+function getDateStringSQL(
+ field: string,
+ unit: keyof typeof DATE_STRING_FORMATS = 'utc',
+ timezone?: string,
+): string {
+ if (timezone && !isUtcTimezone(timezone)) {
return `to_char(${field} at time zone '${timezone}', '${DATE_STRING_FORMATS[unit]}')`;
}
@@ -269,7 +323,11 @@ function parseFilters(filters: Record, options?: QueryOptions) {
};
}
-function getEventPropertyFilterQuery(filters: EventPropertyFilter[] = []): {
+function getPropertyFilterQuery(
+ filters: PropertyFilter[] = [],
+ propertyType: 'event' | 'session' = 'event',
+ timezone?: string,
+): {
sql: string;
params: Record;
} {
@@ -277,64 +335,127 @@ function getEventPropertyFilterQuery(filters: EventPropertyFilter[] = []): {
const parts: string[] = [];
const params: Record = {};
+ const table = propertyType === 'event' ? 'event_data' : 'session_data';
+ const column = propertyType === 'event' ? 'website_event_id' : 'session_id';
+ const outerColumn =
+ propertyType === 'event' ? 'website_event.event_id' : 'website_event.session_id';
+ const dateFilter =
+ propertyType === 'event' ? `and created_at between {{startDate}} and {{endDate}}` : '';
filters.forEach(({ propertyName, dataType, operator, value }, i) => {
- const keyParam = `epf_key_${i}`;
- const valParam = `epf_val_${i}`;
+ const keyParam = `pf_key_${i}`;
+ const valParam = `pf_val_${i}`;
params[keyParam] = propertyName;
- const isNumeric = dataType === 2;
- const col = isNumeric ? 'cast(number_value as decimal)' : 'string_value';
-
let condition: string;
- if (isNumeric) {
- params[valParam] = parseFloat(value) || 0;
- const opMap: Record = {
- [OPERATORS.equals]: `${col} = {{${valParam}}}`,
- [OPERATORS.notEquals]: `${col} != {{${valParam}}}`,
- [OPERATORS.greaterThan]: `${col} > {{${valParam}}}`,
- [OPERATORS.lessThan]: `${col} < {{${valParam}}}`,
- [OPERATORS.greaterThanEquals]: `${col} >= {{${valParam}}}`,
- [OPERATORS.lessThanEquals]: `${col} <= {{${valParam}}}`,
- };
- condition = opMap[operator] ?? `${col} = {{${valParam}}}`;
- } else if (EQUALITY_OPERATORS.includes(operator)) {
- const vals = value.split(',').filter(Boolean);
- if (!vals.length) return;
- params[valParam] = vals;
- condition =
- operator === OPERATORS.equals
- ? `${col} = ANY({{${valParam}::text[]}})`
- : `${col} != ALL({{${valParam}::text[]}})`;
- } else if (REGEX_OPERATORS.includes(operator)) {
- if (!value) return;
- params[valParam] = value;
- condition =
- operator === OPERATORS.regex ? `${col} ~* {{${valParam}}}` : `${col} !~* {{${valParam}}}`;
- } else {
- if (!value) return;
- params[valParam] = `%${value}%`;
- condition =
- operator === OPERATORS.contains
- ? `${col} ilike {{${valParam}}}`
- : `${col} not ilike {{${valParam}}}`;
+ switch (dataType) {
+ case DATA_TYPE.number: {
+ const col = 'cast(number_value as decimal)';
+ params[valParam] = parseFloat(value) || 0;
+ const opMap: Record = {
+ [OPERATORS.equals]: `${col} = {{${valParam}}}`,
+ [OPERATORS.notEquals]: `${col} != {{${valParam}}}`,
+ [OPERATORS.greaterThan]: `${col} > {{${valParam}}}`,
+ [OPERATORS.lessThan]: `${col} < {{${valParam}}}`,
+ [OPERATORS.greaterThanEquals]: `${col} >= {{${valParam}}}`,
+ [OPERATORS.lessThanEquals]: `${col} <= {{${valParam}}}`,
+ };
+ condition = opMap[operator] ?? `${col} = {{${valParam}}}`;
+ break;
+ }
+ case DATA_TYPE.date: {
+ if (!value) return;
+ params[valParam] = value;
+ const dateCol =
+ timezone && !isUtcTimezone(timezone)
+ ? `(date_value at time zone {{timezone}})::date`
+ : `(date_value at time zone 'utc')::date`;
+ const opMap: Record = {
+ [OPERATORS.before]: `${dateCol} < {{${valParam}::date}}`,
+ [OPERATORS.after]: `${dateCol} > {{${valParam}::date}}`,
+ };
+ condition = opMap[operator] ?? `${dateCol} = {{${valParam}::date}}`;
+ break;
+ }
+ case DATA_TYPE.array: {
+ if (!value) return;
+ params[valParam] = value;
+ condition =
+ operator === OPERATORS.contains
+ ? `exists (
+ select 1
+ from jsonb_array_elements_text(coalesce(string_value, '[]')::jsonb) as array_item(value)
+ where array_item.value = {{${valParam}}}
+ )`
+ : `not exists (
+ select 1
+ from jsonb_array_elements_text(coalesce(string_value, '[]')::jsonb) as array_item(value)
+ where array_item.value = {{${valParam}}}
+ )`;
+ break;
+ }
+ default: {
+ const col = 'string_value';
+
+ if (EQUALITY_OPERATORS.includes(operator)) {
+ const vals = value.split(',').filter(Boolean);
+ if (!vals.length) return;
+ params[valParam] = vals;
+ condition =
+ operator === OPERATORS.equals
+ ? `${col} = ANY({{${valParam}}})`
+ : `${col} != ALL({{${valParam}}})`;
+ } else if (REGEX_OPERATORS.includes(operator)) {
+ if (!value) return;
+ params[valParam] = value;
+ condition =
+ operator === OPERATORS.regex
+ ? `${col} ~* {{${valParam}}}`
+ : `${col} !~* {{${valParam}}}`;
+ } else {
+ if (!value) return;
+ params[valParam] = `%${value}%`;
+ condition =
+ operator === OPERATORS.contains
+ ? `${col} ilike {{${valParam}}}`
+ : `${col} not ilike {{${valParam}}}`;
+ }
+ break;
+ }
}
- parts.push(`and website_event.event_id in (
- select website_event_id
- from event_data
- where website_id = {{websiteId::uuid}}
- and created_at between {{startDate}} and {{endDate}}
+ if (propertyType === 'session') {
+ parts.push(`and exists (
+ select 1
+ from ${table}
+ where website_id = website_event.website_id
+ and session_id = website_event.session_id
and data_key = {{${keyParam}}}
and data_type = ${dataType}
and ${condition}
)`);
+ } else {
+ parts.push(`and ${outerColumn} in (
+ select ${column}
+ from ${table}
+ where website_id = {{websiteId::uuid}}
+ ${dateFilter}
+ and data_key = {{${keyParam}}}
+ and data_type = ${dataType}
+ and ${condition}
+ )`);
+ }
});
return { sql: parts.join('\n'), params };
}
-async function rawQuery(sql: string, data: Record, name?: string): Promise {
+async function executeRawQuery(
+ sql: string,
+ data: Record,
+ name?: string,
+ write = false,
+): Promise {
if (process.env.LOG_QUERY) {
log('QUERY:\n', sql);
log('PARAMETERS:\n', data);
@@ -343,10 +464,6 @@ async function rawQuery(sql: string, data: Record, name?: string):
const params = [];
const schema = getSchema();
- if (schema) {
- await client.$executeRawUnsafe(`SET search_path TO "${schema}";`);
- }
-
const query = sql?.replaceAll(/\{\{\s*(\w+)(::\w+)?\s*}}/g, (...args) => {
const [, name, type] = args;
@@ -357,10 +474,24 @@ async function rawQuery(sql: string, data: Record, name?: string):
return `$${params.length}${type ?? ''}`;
});
- if (process.env.DATABASE_REPLICA_URL && '$replica' in client) {
- return client.$replica().$queryRawUnsafe(query, ...params);
+ const queryClient = getRawQueryClient(client, {
+ useReplica: !!process.env.DATABASE_REPLICA_URL,
+ write,
+ });
+
+ if (schema) {
+ await queryClient.$executeRawUnsafe(`SET search_path TO "${schema}";`);
}
- return client.$queryRawUnsafe(query, ...params);
+
+ return queryClient.$queryRawUnsafe(query, ...params);
+}
+
+async function rawQuery(sql: string, data: Record, name?: string): Promise {
+ return executeRawQuery(sql, data, name);
+}
+
+async function writeRawQuery(sql: string, data: Record, name?: string): Promise {
+ return executeRawQuery(sql, data, name, true);
}
async function pagedQuery(model: string, criteria: T, filters?: QueryFilters) {
@@ -404,13 +535,22 @@ async function pagedRawQuery(
.filter(n => n)
.join('\n');
- const count = await rawQuery(`select count(*) as num from (${query}) t`, queryParams).then(
- res => res[0].num,
- );
+ const { maxResults } = filters;
+ const countQuery = maxResults
+ ? `select count(*) as num from (select 1 from (${query}) t limit ${+maxResults}) t2`
+ : `select count(*) as num from (${query}) t`;
+ const count = await rawQuery(countQuery, queryParams).then(res => Number(res[0].num));
const data = await rawQuery(`${query}${statements}`, queryParams, name);
- return { data, count, page: +page, pageSize: size, orderBy };
+ return {
+ data,
+ count,
+ page: +page,
+ pageSize: size,
+ orderBy,
+ isCapped: !!maxResults && +count >= +maxResults,
+ };
}
function getSearchParameters(query: string, filters: Record[]) {
@@ -520,7 +660,7 @@ export default {
getDateStringSQL,
getDateWeeklySQL,
getFilterQuery,
- getEventPropertyFilterQuery,
+ getPropertyFilterQuery,
getSearchParameters,
getTimestampDiffSQL,
getSearchSQL,
@@ -528,4 +668,5 @@ export default {
pagedRawQuery,
parseFilters,
rawQuery,
+ writeRawQuery,
};
diff --git a/src/lib/react.ts b/src/lib/react.ts
index 668cdf1fb..c1cf1414a 100644
--- a/src/lib/react.ts
+++ b/src/lib/react.ts
@@ -8,9 +8,11 @@ import {
type ReactNode,
} from 'react';
+type ReactElementWithChildren = ReactElement<{ children?: ReactNode }>;
+
export function getFragmentChildren(children: ReactNode) {
- return (children as ReactElement)?.type === Fragment
- ? (children as ReactElement).props.children
+ return (children as ReactElementWithChildren)?.type === Fragment
+ ? (children as ReactElementWithChildren).props.children
: children;
}
diff --git a/src/lib/recorder.ts b/src/lib/recorder.ts
new file mode 100644
index 000000000..53d57f77a
--- /dev/null
+++ b/src/lib/recorder.ts
@@ -0,0 +1,54 @@
+export interface RecorderConfig {
+ replayEnabled?: boolean;
+ heatmapEnabled?: boolean;
+ sampleRate?: number;
+ heatmapSampleRate?: number;
+ maskLevel?: 'strict' | 'moderate';
+ maxDuration?: number;
+ blockSelector?: string;
+}
+
+export function getRecorderConfig(value: unknown): RecorderConfig {
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
+ return {};
+ }
+
+ const config = value as Record;
+ const nextConfig: RecorderConfig = {};
+
+ if (config.replayEnabled === true) {
+ nextConfig.replayEnabled = true;
+ }
+
+ if (config.heatmapEnabled === true) {
+ nextConfig.heatmapEnabled = true;
+ }
+
+ if (typeof config.sampleRate === 'number') {
+ nextConfig.sampleRate = config.sampleRate;
+ }
+
+ if (typeof config.heatmapSampleRate === 'number') {
+ nextConfig.heatmapSampleRate = config.heatmapSampleRate;
+ }
+
+ if (config.maskLevel === 'strict' || config.maskLevel === 'moderate') {
+ nextConfig.maskLevel = config.maskLevel;
+ }
+
+ if (typeof config.maxDuration === 'number' && Number.isFinite(config.maxDuration)) {
+ nextConfig.maxDuration = Math.round(config.maxDuration);
+ }
+
+ if (typeof config.blockSelector === 'string') {
+ nextConfig.blockSelector = config.blockSelector;
+ }
+
+ return nextConfig;
+}
+
+export function getRecorderEnabled(config: unknown) {
+ const { replayEnabled, heatmapEnabled } = getRecorderConfig(config);
+
+ return replayEnabled === true || heatmapEnabled === true;
+}
diff --git a/src/lib/replay.test.ts b/src/lib/replay.test.ts
new file mode 100644
index 000000000..db93bfad7
--- /dev/null
+++ b/src/lib/replay.test.ts
@@ -0,0 +1,137 @@
+import { expect, test } from 'vitest';
+import {
+ canReplayEvents,
+ getReplayEventCount,
+ getReplayPlayerEvents,
+ getReplayViewport,
+ hasReplayableFullSnapshot,
+ hasReplayFullSnapshot,
+ hasReplayMeta,
+ REPLAY_EVENT_FRAGMENT_TYPE,
+ restoreReplayEventFragments,
+} from './replay';
+
+const fullSnapshot = {
+ type: 2,
+ timestamp: 1781553116151,
+ data: {
+ node: {
+ type: 0,
+ id: 1,
+ childNodes: [{ type: 2, id: 2, tagName: 'html', attributes: {}, childNodes: [] }],
+ },
+ initialOffset: { left: 0, top: 0 },
+ },
+};
+
+test('hasReplayFullSnapshot returns true when a full snapshot is present', () => {
+ expect(hasReplayFullSnapshot([{ type: 4 }, { type: 2 }, { type: 3 }])).toBe(true);
+});
+
+test('hasReplayMeta returns true when a meta event is present', () => {
+ expect(hasReplayMeta([{ type: 4 }, { type: 2 }, { type: 3 }])).toBe(true);
+});
+
+test('getReplayViewport returns the recorded viewport dimensions', () => {
+ expect(getReplayViewport(null)).toBe(null);
+ expect(getReplayViewport([{ type: 4, data: { width: 0, height: 768 } }])).toBe(null);
+ expect(getReplayViewport([{ type: 4, data: { width: '1024', height: '1365' } }])).toEqual({
+ width: 1024,
+ height: 1365,
+ });
+});
+
+test('hasReplayableFullSnapshot returns true when a usable full snapshot is present', () => {
+ expect(hasReplayableFullSnapshot([{ type: 4 }, fullSnapshot, { type: 3 }])).toBe(true);
+});
+
+test('canReplayEvents requires events that can be normalized with a full snapshot', () => {
+ expect(canReplayEvents([{ type: 4, timestamp: 1781553116150 }, fullSnapshot])).toBe(true);
+ expect(canReplayEvents([fullSnapshot])).toBe(true);
+ expect(canReplayEvents([{ type: 4, timestamp: 1781553116150 }, { type: 2 }])).toBe(false);
+ expect(canReplayEvents([{ type: 4, timestamp: 1781553116150 }, { type: 3 }])).toBe(false);
+ expect(canReplayEvents([{ ...fullSnapshot, timestamp: undefined }])).toBe(true);
+});
+
+test('getReplayPlayerEvents normalizes events for rrweb-player', () => {
+ expect(getReplayPlayerEvents(null)).toEqual([]);
+ expect(getReplayPlayerEvents([{ type: 3, timestamp: 1781553116160 }])).toEqual([]);
+
+ expect(getReplayPlayerEvents([fullSnapshot])).toEqual([
+ fullSnapshot,
+ { ...fullSnapshot, timestamp: fullSnapshot.timestamp + 1 },
+ ]);
+
+ expect(
+ getReplayPlayerEvents([
+ { type: 0, timestamp: '1781553116149' },
+ { ...fullSnapshot, timestamp: undefined },
+ ]),
+ ).toEqual([
+ { type: 0, timestamp: 1781553116149 },
+ { ...fullSnapshot, timestamp: 1781553116150 },
+ ]);
+});
+
+test('hasReplayFullSnapshot returns false when only incremental events are present', () => {
+ expect(hasReplayFullSnapshot([{ type: 4 }, { type: 3 }, { type: 3 }])).toBe(false);
+});
+
+test('hasReplayFullSnapshot handles missing events', () => {
+ expect(hasReplayFullSnapshot(null)).toBe(false);
+ expect(hasReplayFullSnapshot(undefined)).toBe(false);
+});
+
+test('restoreReplayEventFragments restores fragmented events', () => {
+ const serialized = JSON.stringify(fullSnapshot);
+ const splitAt = Math.floor(serialized.length / 2);
+
+ const events = restoreReplayEventFragments([
+ { type: 4, timestamp: 1781553116150 },
+ {
+ type: REPLAY_EVENT_FRAGMENT_TYPE,
+ timestamp: fullSnapshot.timestamp,
+ data: {
+ id: 'snapshot-1',
+ index: 0,
+ total: 2,
+ value: serialized.slice(0, splitAt),
+ },
+ },
+ {
+ type: REPLAY_EVENT_FRAGMENT_TYPE,
+ timestamp: fullSnapshot.timestamp,
+ data: {
+ id: 'snapshot-1',
+ index: 1,
+ total: 2,
+ value: serialized.slice(splitAt),
+ },
+ },
+ { type: 3, timestamp: 1781553116160 },
+ ]);
+
+ expect(events).toEqual([
+ { type: 4, timestamp: 1781553116150 },
+ fullSnapshot,
+ { type: 3, timestamp: 1781553116160 },
+ ]);
+ expect(hasReplayFullSnapshot(events)).toBe(true);
+});
+
+test('getReplayEventCount counts a fragment group as one event', () => {
+ expect(
+ getReplayEventCount([
+ { type: 4 },
+ {
+ type: REPLAY_EVENT_FRAGMENT_TYPE,
+ data: { id: 'snapshot-1', index: 0, total: 2, value: '{' },
+ },
+ {
+ type: REPLAY_EVENT_FRAGMENT_TYPE,
+ data: { id: 'snapshot-1', index: 1, total: 2, value: '}' },
+ },
+ { type: 3 },
+ ]),
+ ).toBe(3);
+});
diff --git a/src/lib/replay.ts b/src/lib/replay.ts
new file mode 100644
index 000000000..5a8b3c8cb
--- /dev/null
+++ b/src/lib/replay.ts
@@ -0,0 +1,211 @@
+export const RRWEB_EVENT_TYPE = {
+ Meta: 4,
+ FullSnapshot: 2,
+} as const;
+
+export const REPLAY_EVENT_FRAGMENT_TYPE = 'umami:rrweb-event-fragment';
+
+interface ReplayEventFragment {
+ type: typeof REPLAY_EVENT_FRAGMENT_TYPE;
+ timestamp?: number;
+ data: {
+ id: string;
+ index: number;
+ total: number;
+ value: string;
+ };
+}
+
+export function isReplayEventFragment(event: any): event is ReplayEventFragment {
+ const { data } = event || {};
+
+ return (
+ event?.type === REPLAY_EVENT_FRAGMENT_TYPE &&
+ typeof data?.id === 'string' &&
+ Number.isInteger(data?.index) &&
+ Number.isInteger(data?.total) &&
+ data.index >= 0 &&
+ data.index < data.total &&
+ typeof data?.value === 'string'
+ );
+}
+
+export function getReplayEventCount(events: any[] | null | undefined) {
+ if (!Array.isArray(events)) {
+ return 0;
+ }
+
+ return events.reduce((count, event) => {
+ if (isReplayEventFragment(event)) {
+ return count + (event.data.index === 0 ? 1 : 0);
+ }
+
+ return count + 1;
+ }, 0);
+}
+
+export function restoreReplayEventFragments(events: any[] | null | undefined) {
+ if (!Array.isArray(events)) {
+ return [];
+ }
+
+ const restored: any[] = [];
+ const pending = new Map<
+ string,
+ {
+ total: number;
+ values: Map;
+ received: number;
+ }
+ >();
+
+ for (const event of events) {
+ if (!isReplayEventFragment(event)) {
+ restored.push(event);
+ continue;
+ }
+
+ const { id, index, total, value } = event.data;
+ let fragment = pending.get(id);
+
+ if (!fragment || fragment.total !== total) {
+ fragment = {
+ total,
+ values: new Map(),
+ received: 0,
+ };
+ pending.set(id, fragment);
+ }
+
+ if (!fragment.values.has(index)) {
+ fragment.values.set(index, value);
+ fragment.received += 1;
+ }
+
+ if (fragment.received === fragment.total) {
+ pending.delete(id);
+
+ try {
+ let serialized = '';
+
+ for (let i = 0; i < fragment.total; i++) {
+ serialized += fragment.values.get(i) || '';
+ }
+
+ restored.push(JSON.parse(serialized));
+ } catch {
+ // Ignore malformed fragment groups. A partial replay is better than failing the whole response.
+ }
+ }
+ }
+
+ return restored;
+}
+
+export function hasReplayFullSnapshot(events: any[] | null | undefined) {
+ return (
+ Array.isArray(events) && events.some(event => event?.type === RRWEB_EVENT_TYPE.FullSnapshot)
+ );
+}
+
+export function hasReplayableFullSnapshot(events: any[] | null | undefined) {
+ return (
+ Array.isArray(events) &&
+ events.some(event => {
+ const { data } = event || {};
+ const { node } = data || {};
+
+ return (
+ event?.type === RRWEB_EVENT_TYPE.FullSnapshot &&
+ Number.isInteger(node?.type) &&
+ Array.isArray(node?.childNodes)
+ );
+ })
+ );
+}
+
+export function hasReplayMeta(events: any[] | null | undefined) {
+ return Array.isArray(events) && events.some(event => event?.type === RRWEB_EVENT_TYPE.Meta);
+}
+
+function getReplayDimension(value: any) {
+ const dimension = Number(value);
+
+ return Number.isFinite(dimension) && dimension > 0 ? dimension : null;
+}
+
+export function getReplayViewport(events: any[] | null | undefined) {
+ if (!Array.isArray(events)) {
+ return null;
+ }
+
+ const meta = events.find(event => {
+ const width = getReplayDimension(event?.data?.width);
+ const height = getReplayDimension(event?.data?.height);
+
+ return event?.type === RRWEB_EVENT_TYPE.Meta && width && height;
+ });
+
+ if (!meta) {
+ return null;
+ }
+
+ return {
+ width: getReplayDimension(meta.data.width) as number,
+ height: getReplayDimension(meta.data.height) as number,
+ };
+}
+
+function getReplayTimestamp(event: any) {
+ const timestamp = Number(event?.timestamp);
+
+ return Number.isFinite(timestamp) ? timestamp : null;
+}
+
+export function getReplayPlayerEvents(events: any[] | null | undefined) {
+ if (!Array.isArray(events)) {
+ return [];
+ }
+
+ const replayEvents = events.filter(
+ event => event && typeof event === 'object' && event.type !== undefined && event.type !== null,
+ );
+
+ if (!hasReplayableFullSnapshot(replayEvents)) {
+ return [];
+ }
+
+ const firstTimestamp =
+ replayEvents.map(getReplayTimestamp).find(timestamp => timestamp !== null) ?? Date.now();
+ let lastTimestamp = firstTimestamp - 1;
+
+ const normalizedEvents = replayEvents.map(event => {
+ const timestamp = getReplayTimestamp(event);
+
+ if (timestamp !== null) {
+ lastTimestamp = timestamp;
+
+ return typeof event.timestamp === 'number' ? event : { ...event, timestamp };
+ }
+
+ lastTimestamp += 1;
+
+ return { ...event, timestamp: lastTimestamp };
+ });
+
+ if (normalizedEvents.length === 1) {
+ return [
+ normalizedEvents[0],
+ {
+ ...normalizedEvents[0],
+ timestamp: normalizedEvents[0].timestamp + 1,
+ },
+ ];
+ }
+
+ return normalizedEvents;
+}
+
+export function canReplayEvents(events: any[] | null | undefined) {
+ return getReplayPlayerEvents(events).length >= 2;
+}
diff --git a/src/lib/request.ts b/src/lib/request.ts
index 40ffabe06..ddf7b7614 100644
--- a/src/lib/request.ts
+++ b/src/lib/request.ts
@@ -1,3 +1,5 @@
+import { startOfMonth, subMonths } from 'date-fns';
+import { z } from 'zod';
import { checkAuth } from '@/lib/auth';
import { DEFAULT_PAGE_SIZE, FILTER_COLUMNS, OPERATORS } from '@/lib/constants';
import { getAllowedUnits, getMinimumUnit, maxDate, parseDateRange } from '@/lib/date';
@@ -6,8 +8,6 @@ import { filtersArrayToObject } from '@/lib/params';
import { badRequest, unauthorized } from '@/lib/response';
import type { QueryFilters } from '@/lib/types';
import { getWebsiteSegment } from '@/queries/prisma';
-import { startOfMonth, subMonths } from 'date-fns';
-import { z } from 'zod';
export async function parseRequest(
request: Request,
@@ -32,7 +32,7 @@ export async function parseRequest(
// Re-add dynamic params stripped by Zod schema: suffixed filter params (browser1, os2)
for (const key of Object.keys(rawQuery)) {
- if ((/\d+$/.test(key) || /^epf_/.test(key)) && !(key in query)) {
+ if ((/\d+$/.test(key) || /^pf_/.test(key)) && !(key in query)) {
query[key] = rawQuery[key];
}
}
@@ -168,11 +168,13 @@ export async function getQueryFilters(
...dateRange,
...filters,
match,
+ minDuration: params?.minDuration,
page: params?.page,
pageSize: params?.pageSize ? params?.pageSize || DEFAULT_PAGE_SIZE : undefined,
orderBy: params?.orderBy,
sortDescending: params?.sortDescending,
search: params?.search,
compare: params?.compare,
+ maxResults: params?.maxResults,
};
}
diff --git a/src/lib/response.test.ts b/src/lib/response.test.ts
new file mode 100644
index 000000000..594ed3095
--- /dev/null
+++ b/src/lib/response.test.ts
@@ -0,0 +1,32 @@
+import { describe, expect, test, vi } from 'vitest';
+import { serverError } from './response';
+
+describe('serverError', () => {
+ test('does not expose internal error details in the response body', async () => {
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
+
+ const response = serverError(new Error('database exploded'));
+
+ expect(await response.json()).toEqual({
+ error: {
+ message: 'Server error',
+ code: 'server-error',
+ status: 500,
+ },
+ });
+
+ logSpy.mockRestore();
+ });
+
+ test('allows intentional server error messages', async () => {
+ const response = serverError('Redis is disabled');
+
+ expect(await response.json()).toEqual({
+ error: {
+ message: 'Redis is disabled',
+ code: 'server-error',
+ status: 500,
+ },
+ });
+ });
+});
diff --git a/src/lib/response.ts b/src/lib/response.ts
index f1ad5c7bf..4c3f071a7 100644
--- a/src/lib/response.ts
+++ b/src/lib/response.ts
@@ -1,3 +1,5 @@
+import { serializeError } from 'serialize-error';
+
export function ok() {
return Response.json({ ok: true });
}
@@ -36,6 +38,15 @@ export function forbidden(error?: Record) {
);
}
+export function payloadTooLarge(error?: Record) {
+ return Response.json(
+ {
+ error: { message: 'Payload too large', code: 'payload-too-large', status: 413, ...error },
+ },
+ { status: 413 },
+ );
+}
+
export function notFound(error?: Record) {
return Response.json(
{ error: { message: 'Not found', code: 'not-found', status: 404, ...error } },
@@ -43,14 +54,18 @@ export function notFound(error?: Record) {
);
}
-export function serverError(error?: Record) {
+export function serverError(error?: unknown) {
+ if (error && typeof error !== 'string') {
+ // eslint-disable-next-line no-console
+ console.log(serializeError(error));
+ }
+
return Response.json(
{
error: {
- message: 'Server error',
+ message: typeof error === 'string' ? error : 'Server error',
code: 'server-error',
status: 500,
- ...error,
},
},
{ status: 500 },
diff --git a/src/lib/schema.ts b/src/lib/schema.ts
index 2567c3de6..a58623850 100644
--- a/src/lib/schema.ts
+++ b/src/lib/schema.ts
@@ -74,13 +74,28 @@ export const searchParams = {
search: z.string().optional(),
};
+export const replayParams = {
+ minDuration: z.coerce.number().int().nonnegative().optional(),
+};
+
export const pagingParams = {
page: z.coerce.number().int().positive().optional(),
pageSize: z.coerce.number().int().positive().optional(),
+ maxResults: z.coerce.number().int().positive().optional(),
};
export const sortingParams = {
orderBy: z.string().optional(),
+ sortDescending: z
+ .enum(['true', 'false'])
+ .optional()
+ .transform(value => {
+ if (value === undefined) {
+ return undefined;
+ }
+
+ return value === 'true';
+ }),
};
export const userRoleParam = z.enum(['admin', 'user', 'view-only']);
@@ -131,6 +146,7 @@ export const reportTypeParam = z.enum([
'breakdown',
'funnel',
'goal',
+ 'heatmap',
'journey',
'performance',
'retention',
@@ -159,13 +175,12 @@ export const operatorParam = z.enum([
export const goalReportSchema = z.object({
type: z.literal('goal'),
- parameters: z
- .object({
- startDate: z.coerce.date(),
- endDate: z.coerce.date(),
- type: z.string(),
- value: z.string(),
- }),
+ parameters: z.object({
+ startDate: z.coerce.date(),
+ endDate: z.coerce.date(),
+ type: z.string(),
+ value: z.string(),
+ }),
});
export const funnelReportSchema = z.object({
@@ -268,6 +283,16 @@ export const breakdownReportSchema = z.object({
}),
});
+export const heatmapReportSchema = z.object({
+ type: z.literal('heatmap'),
+ parameters: z.object({
+ startDate: z.coerce.date(),
+ endDate: z.coerce.date(),
+ urlPath: z.string().max(500).optional(),
+ mode: z.enum(['click', 'scroll']).optional(),
+ }),
+});
+
export const reportBaseSchema = z.object({
websiteId: z.uuid(),
type: reportTypeParam,
@@ -286,6 +311,7 @@ export const reportTypeSchema = z.discriminatedUnion('type', [
revenueReportSchema,
attributionReportSchema,
breakdownReportSchema,
+ heatmapReportSchema,
]);
export const reportSchema = reportBaseSchema;
@@ -293,7 +319,7 @@ export const reportSchema = reportBaseSchema;
export const reportResultSchema = z.intersection(
z.object({
websiteId: z.uuid(),
- filters: z.object({ ...filterParams }),
+ filters: z.object({ ...filterParams }).passthrough(),
}),
reportTypeSchema,
);
diff --git a/src/lib/session.test.ts b/src/lib/session.test.ts
new file mode 100644
index 000000000..154dd5d18
--- /dev/null
+++ b/src/lib/session.test.ts
@@ -0,0 +1,24 @@
+import { expect, test } from 'vitest';
+import { uuid } from './crypto';
+
+const sourceId = '2e2fc91f-77a7-4e5d-a7b1-18a74c67b4e0';
+const sessionSalt = 'monthly-session-salt';
+
+test('keeps session IDs stable on the same device', () => {
+ const args = [sourceId, '192.0.2.1', 'Desktop Browser', sessionSalt] as const;
+
+ expect(uuid(...args)).toBe(uuid(...args));
+});
+
+test('creates separate sessions on different devices', () => {
+ const desktopSessionId = uuid(sourceId, '192.0.2.1', 'Desktop Browser', sessionSalt);
+ const mobileSessionId = uuid(sourceId, '198.51.100.2', 'Mobile Browser', sessionSalt);
+
+ expect(desktopSessionId).not.toBe(mobileSessionId);
+});
+
+test('preserves anonymous session IDs', () => {
+ expect(uuid(sourceId, '192.0.2.1', 'Desktop Browser', sessionSalt)).toBe(
+ uuid(sourceId, '192.0.2.1', 'Desktop Browser', sessionSalt),
+ );
+});
diff --git a/src/lib/share.ts b/src/lib/share.ts
new file mode 100644
index 000000000..4fc20366c
--- /dev/null
+++ b/src/lib/share.ts
@@ -0,0 +1,20 @@
+import { FILTER_COLUMNS } from './constants';
+import type { ShareParameters, ShareTheme } from './types';
+
+const FILTER_QUERY_PARAMS = new Set(['cohort', 'excludeBounce', 'match', 'segment']);
+
+export function allowShareFilter(parameters?: ShareParameters | null) {
+ return parameters?.allowFilter !== false;
+}
+
+export function getShareTheme(parameters?: ShareParameters | null): ShareTheme | undefined {
+ return parameters?.theme === 'light' || parameters?.theme === 'dark'
+ ? parameters.theme
+ : undefined;
+}
+
+export function excludeShareFilterParam(key: string) {
+ const baseName = key.replace(/\d+$/, '');
+
+ return baseName in FILTER_COLUMNS || FILTER_QUERY_PARAMS.has(key);
+}
diff --git a/src/lib/sort.ts b/src/lib/sort.ts
new file mode 100644
index 000000000..881c2371a
--- /dev/null
+++ b/src/lib/sort.ts
@@ -0,0 +1,27 @@
+import type { QueryFilters } from './types';
+
+export function sanitizeSortFilters(
+ filters: QueryFilters = {},
+ allowedFields: T,
+ defaults: Partial> = {},
+): QueryFilters {
+ const { orderBy, sortDescending, ...rest } = filters;
+ const fallbackOrderBy = defaults.orderBy;
+ const fallbackSortDescending = defaults.sortDescending;
+ const isAllowed = orderBy ? allowedFields.includes(orderBy as T[number]) : false;
+
+ return {
+ ...rest,
+ ...(isAllowed
+ ? {
+ orderBy,
+ sortDescending,
+ }
+ : {
+ ...(fallbackOrderBy && { orderBy: fallbackOrderBy }),
+ ...(fallbackSortDescending !== undefined && {
+ sortDescending: fallbackSortDescending,
+ }),
+ }),
+ };
+}
diff --git a/src/lib/subscription.test.ts b/src/lib/subscription.test.ts
new file mode 100644
index 000000000..ea9de44e6
--- /dev/null
+++ b/src/lib/subscription.test.ts
@@ -0,0 +1,73 @@
+import { describe, expect, test } from 'vitest';
+import {
+ CLOUD_FREE_TEAM_LIMIT,
+ CLOUD_FREE_WEBSITE_LIMIT,
+ CLOUD_PRO_TEAM_LIMIT,
+ CLOUD_PRO_WEBSITE_LIMIT,
+ getCloudTeamLimit,
+ getCloudWebsiteLimit,
+} from './subscription';
+
+describe('getCloudWebsiteLimit', () => {
+ test('limits accounts without a subscription to the free website limit', () => {
+ expect(getCloudWebsiteLimit(null)).toBe(CLOUD_FREE_WEBSITE_LIMIT);
+ expect(getCloudWebsiteLimit({ hasSubscription: false })).toBe(CLOUD_FREE_WEBSITE_LIMIT);
+ expect(getCloudWebsiteLimit({ hasSubscription: false, unlimitedWebsites: true })).toBe(
+ CLOUD_FREE_WEBSITE_LIMIT,
+ );
+ });
+
+ test('does not limit subscribed accounts with unlimited websites enabled', () => {
+ expect(getCloudWebsiteLimit({ hasSubscription: true, unlimitedWebsites: true })).toBeNull();
+ });
+
+ test('limits Pro accounts to the Pro website limit', () => {
+ expect(getCloudWebsiteLimit({ hasSubscription: true, isPro: true })).toBe(
+ CLOUD_PRO_WEBSITE_LIMIT,
+ );
+ });
+
+ test('does not limit Business or no-billing accounts', () => {
+ expect(getCloudWebsiteLimit({ hasSubscription: true, isBusiness: true })).toBeNull();
+ expect(getCloudWebsiteLimit({ hasSubscription: true, isNoBilling: true })).toBeNull();
+ });
+
+ // Team-owned websites pass the team cache object (derived from the team owner's account)
+ // through this same function, so members inherit the owner's unlimitedWebsites entitlement.
+ test('does not limit teams that inherit unlimited websites from a subscribed owner', () => {
+ expect(
+ getCloudWebsiteLimit({ hasSubscription: true, isPro: true, unlimitedWebsites: true }),
+ ).toBeNull();
+ });
+
+ test('still limits teams whose owner has no subscription even if unlimited websites is set', () => {
+ expect(
+ getCloudWebsiteLimit({ hasSubscription: false, unlimitedWebsites: true }),
+ ).toBe(CLOUD_FREE_WEBSITE_LIMIT);
+ });
+});
+
+describe('getCloudTeamLimit', () => {
+ test('does not allow teams for accounts without a subscription', () => {
+ expect(CLOUD_FREE_TEAM_LIMIT).toBe(0);
+ expect(getCloudTeamLimit(null)).toBe(CLOUD_FREE_TEAM_LIMIT);
+ expect(getCloudTeamLimit(undefined)).toBe(CLOUD_FREE_TEAM_LIMIT);
+ expect(getCloudTeamLimit({ hasSubscription: false })).toBe(CLOUD_FREE_TEAM_LIMIT);
+ });
+
+ test('limits Pro accounts to the Pro team limit', () => {
+ expect(getCloudTeamLimit({ hasSubscription: true, isPro: true })).toBe(CLOUD_PRO_TEAM_LIMIT);
+ });
+
+ test('does not limit Business or no-billing accounts', () => {
+ expect(getCloudTeamLimit({ hasSubscription: true, isBusiness: true })).toBeNull();
+ expect(getCloudTeamLimit({ hasSubscription: true, isNoBilling: true })).toBeNull();
+ });
+
+ // unlimitedWebsites is a website-only entitlement; it must not lift the team limit.
+ test('ignores the unlimited websites flag for team limits', () => {
+ expect(getCloudTeamLimit({ hasSubscription: true, isPro: true, unlimitedWebsites: true })).toBe(
+ CLOUD_PRO_TEAM_LIMIT,
+ );
+ });
+});
diff --git a/src/lib/subscription.ts b/src/lib/subscription.ts
new file mode 100644
index 000000000..0b29f9a68
--- /dev/null
+++ b/src/lib/subscription.ts
@@ -0,0 +1,49 @@
+export interface SubscriptionAccount {
+ isPro?: boolean | null;
+ isBusiness?: boolean | null;
+ isNoBilling?: boolean | null;
+ hasSubscription?: boolean | null;
+ unlimitedWebsites?: boolean | null;
+}
+
+export const CLOUD_FREE_WEBSITE_LIMIT = 1;
+export const CLOUD_PRO_WEBSITE_LIMIT = 20;
+
+export const CLOUD_FREE_TEAM_LIMIT = 0;
+export const CLOUD_PRO_TEAM_LIMIT = 10;
+
+export function getCloudWebsiteLimit(account?: SubscriptionAccount | null): number | null {
+ if (!account?.hasSubscription) {
+ return CLOUD_FREE_WEBSITE_LIMIT;
+ }
+
+ if (account.unlimitedWebsites) {
+ return null;
+ }
+
+ if (account.isNoBilling || account.isBusiness) {
+ return null;
+ }
+
+ if (account.isPro) {
+ return CLOUD_PRO_WEBSITE_LIMIT;
+ }
+
+ return null;
+}
+
+export function getCloudTeamLimit(account?: SubscriptionAccount | null): number | null {
+ if (!account?.hasSubscription) {
+ return CLOUD_FREE_TEAM_LIMIT;
+ }
+
+ if (account.isNoBilling || account.isBusiness) {
+ return null;
+ }
+
+ if (account.isPro) {
+ return CLOUD_PRO_TEAM_LIMIT;
+ }
+
+ return null;
+}
diff --git a/src/lib/types.ts b/src/lib/types.ts
index c93854910..9544263bd 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -20,6 +20,7 @@ export interface Auth {
isAdmin: boolean;
};
shareToken?: {
+ shareType?: number;
websiteId?: string;
websiteIds?: string[];
boardId?: string;
@@ -27,16 +28,27 @@ export interface Auth {
pixelIds?: string[];
linkId?: string;
linkIds?: string[];
+ parameters?: ShareParameters;
};
}
-export interface EventPropertyFilter {
+export type ShareTheme = 'light' | 'dark';
+
+export interface ShareParameters {
+ allowFilter?: boolean;
+ theme?: ShareTheme;
+ [key: string]: boolean | ShareTheme | undefined;
+}
+
+export interface PropertyFilter {
propertyName: string;
dataType: number;
operator: Operator;
value: string;
}
+export type EventPropertyFilter = PropertyFilter;
+
export interface Filter {
name: string;
operator: Operator;
@@ -79,6 +91,23 @@ export interface EventDataNumericStats {
min: number;
}
+export interface SessionDataPivotRow {
+ sessionId: string;
+ distinctId: string;
+ createdAt: string | Date;
+ propertyKeys: string[];
+ propertyValues: string[];
+}
+
+export interface PropertyLeaderboardRow {
+ label: string;
+ activity: number;
+ sessions: number;
+ visits: number;
+ views: number;
+ events: number;
+}
+
export interface QueryOptions {
joinSession?: boolean;
columns?: Record;
@@ -95,6 +124,7 @@ export interface QueryFilters
SortParams,
PageParams,
SegmentParams {
+ minDuration?: number;
cohortFilters?: QueryFilters;
}
@@ -138,6 +168,7 @@ export interface SortParams {
export interface PageParams {
page?: number;
pageSize?: number;
+ maxResults?: number;
}
export interface SegmentParams {
@@ -153,6 +184,7 @@ export interface PageResult {
orderBy?: string;
sortDescending?: boolean;
search?: string;
+ isCapped?: boolean;
}
export interface RealtimeData {
diff --git a/src/lib/url.ts b/src/lib/url.ts
index f6772feec..a4cc7cb30 100644
--- a/src/lib/url.ts
+++ b/src/lib/url.ts
@@ -2,7 +2,7 @@ export function getQueryString(params: object = {}): string {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
- if (value !== undefined) {
+ if (value !== undefined && value !== null && !Number.isNaN(value)) {
searchParams.append(key, value);
}
});
diff --git a/src/permissions/board.test.ts b/src/permissions/board.test.ts
new file mode 100644
index 000000000..97344b57c
--- /dev/null
+++ b/src/permissions/board.test.ts
@@ -0,0 +1,258 @@
+import { beforeEach, expect, test, vi } from 'vitest';
+import { BOARD_TYPES } from '@/lib/boards';
+import { getReport } from '@/queries/prisma';
+import {
+ canViewBoardEntities,
+ hasValidBoardReports,
+ stripInvalidBoardReports,
+} from './board';
+import { canViewLink } from './link';
+import { canViewPixel } from './pixel';
+import { canViewWebsite } from './website';
+
+vi.mock('@/queries/prisma', () => ({
+ getBoard: vi.fn(),
+ getReport: vi.fn(),
+ getTeamUser: vi.fn(),
+}));
+
+vi.mock('@/lib/prisma', () => ({
+ default: {},
+}));
+
+vi.mock('./website', () => ({
+ canViewWebsite: vi.fn(),
+}));
+
+vi.mock('./pixel', () => ({
+ canViewPixel: vi.fn(),
+}));
+
+vi.mock('./link', () => ({
+ canViewLink: vi.fn(),
+}));
+
+const auth = {
+ user: {
+ id: 'user-1',
+ username: 'user',
+ role: 'user',
+ isAdmin: false,
+ },
+ shareToken: {
+ websiteIds: ['victim-website-id'],
+ },
+};
+
+beforeEach(() => {
+ vi.mocked(canViewWebsite).mockReset();
+ vi.mocked(canViewPixel).mockReset();
+ vi.mocked(canViewLink).mockReset();
+ vi.mocked(getReport).mockReset();
+});
+
+test('canViewBoardEntities validates board IDs with user auth only', async () => {
+ vi.mocked(canViewWebsite).mockResolvedValue(true);
+
+ await expect(
+ canViewBoardEntities(auth, BOARD_TYPES.website, { websiteId: 'owned-website-id' }),
+ ).resolves.toBe(true);
+
+ expect(canViewWebsite).toHaveBeenCalledWith(
+ {
+ user: auth.user,
+ },
+ 'owned-website-id',
+ );
+});
+
+test('canViewBoardEntities rejects IDs not accessible to the user', async () => {
+ vi.mocked(canViewWebsite).mockResolvedValue(false);
+
+ await expect(
+ canViewBoardEntities(auth, BOARD_TYPES.mixed, {
+ rows: [
+ {
+ id: 'row-1',
+ columns: [
+ {
+ id: 'column-1',
+ component: {
+ type: 'WebsiteChart',
+ entityType: 'website',
+ entityId: 'victim-website-id',
+ },
+ },
+ ],
+ },
+ ],
+ }),
+ ).resolves.toBe(false);
+});
+
+test('hasValidBoardReports allows saved reports for the board website', async () => {
+ vi.mocked(getReport).mockResolvedValue({
+ id: 'report-1',
+ websiteId: 'website-1',
+ type: 'goal',
+ } as any);
+
+ await expect(
+ hasValidBoardReports(BOARD_TYPES.website, {
+ websiteId: 'website-1',
+ rows: [
+ {
+ id: 'row-1',
+ columns: [
+ {
+ id: 'column-1',
+ component: {
+ type: 'Goal',
+ props: {
+ reportId: 'report-1',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ }),
+ ).resolves.toBe(true);
+});
+
+test('hasValidBoardReports rejects saved reports from a different website', async () => {
+ vi.mocked(getReport).mockResolvedValue({
+ id: 'report-1',
+ websiteId: 'website-2',
+ type: 'goal',
+ } as any);
+
+ await expect(
+ hasValidBoardReports(BOARD_TYPES.website, {
+ websiteId: 'website-1',
+ rows: [
+ {
+ id: 'row-1',
+ columns: [
+ {
+ id: 'column-1',
+ component: {
+ type: 'Goal',
+ props: {
+ reportId: 'report-1',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ }),
+ ).resolves.toBe(false);
+});
+
+test('hasValidBoardReports rejects saved reports with the wrong type', async () => {
+ vi.mocked(getReport).mockResolvedValue({
+ id: 'report-1',
+ websiteId: 'website-1',
+ type: 'funnel',
+ } as any);
+
+ await expect(
+ hasValidBoardReports(BOARD_TYPES.website, {
+ websiteId: 'website-1',
+ rows: [
+ {
+ id: 'row-1',
+ columns: [
+ {
+ id: 'column-1',
+ component: {
+ type: 'Goal',
+ props: {
+ reportId: 'report-1',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ }),
+ ).resolves.toBe(false);
+});
+
+test('hasValidBoardReports rejects report widgets without a website context', async () => {
+ await expect(
+ hasValidBoardReports(BOARD_TYPES.pixel, {
+ pixelId: 'pixel-1',
+ rows: [
+ {
+ id: 'row-1',
+ columns: [
+ {
+ id: 'column-1',
+ component: {
+ type: 'Goal',
+ props: {
+ reportId: 'report-1',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ }),
+ ).resolves.toBe(false);
+});
+
+test('stripInvalidBoardReports removes only invalid report IDs', async () => {
+ vi.mocked(getReport).mockResolvedValue({
+ id: 'report-1',
+ websiteId: 'website-2',
+ type: 'goal',
+ } as any);
+
+ await expect(
+ stripInvalidBoardReports(BOARD_TYPES.website, {
+ websiteId: 'website-1',
+ rows: [
+ {
+ id: 'row-1',
+ columns: [
+ {
+ id: 'column-1',
+ component: {
+ type: 'Goal',
+ title: 'Saved signup goal',
+ description: 'Should stay on the board',
+ props: {
+ reportId: 'report-1',
+ customValue: 'keep-me',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ }),
+ ).resolves.toEqual({
+ websiteId: 'website-1',
+ rows: [
+ {
+ id: 'row-1',
+ columns: [
+ {
+ id: 'column-1',
+ component: {
+ type: 'Goal',
+ title: 'Saved signup goal',
+ description: 'Should stay on the board',
+ props: {
+ customValue: 'keep-me',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ });
+});
diff --git a/src/permissions/board.ts b/src/permissions/board.ts
index 67c25d03a..01082ce8e 100644
--- a/src/permissions/board.ts
+++ b/src/permissions/board.ts
@@ -1,7 +1,155 @@
import { hasPermission } from '@/lib/auth';
+import { BOARD_ENTITY_TYPES, getBoardEntityIds, getResolvedComponentEntity } from '@/lib/boards';
import { PERMISSIONS } from '@/lib/constants';
-import type { Auth } from '@/lib/types';
-import { getBoard, getTeamUser } from '@/queries/prisma';
+import type { Auth, BoardComponentConfig, BoardParameters } from '@/lib/types';
+import { getBoard, getReport, getTeamUser } from '@/queries/prisma';
+import { canViewLink } from './link';
+import { canViewPixel } from './pixel';
+import { canViewWebsite } from './website';
+
+const BOARD_COMPONENT_REPORT_TYPES = {
+ Funnel: 'funnel',
+ Goal: 'goal',
+} as const;
+
+function getExpectedBoardReportType(componentType?: string) {
+ return componentType && componentType in BOARD_COMPONENT_REPORT_TYPES
+ ? BOARD_COMPONENT_REPORT_TYPES[componentType as keyof typeof BOARD_COMPONENT_REPORT_TYPES]
+ : null;
+}
+
+async function isValidBoardReportReference(
+ board: { type?: string; parameters?: BoardParameters },
+ component: BoardComponentConfig,
+ reportCache: Map>,
+) {
+ const expectedReportType = getExpectedBoardReportType(component.type);
+
+ if (!expectedReportType) {
+ return true;
+ }
+
+ const reportId = component.props?.reportId;
+
+ if (reportId == null || reportId === '') {
+ return true;
+ }
+
+ if (typeof reportId !== 'string') {
+ return false;
+ }
+
+ const entity = getResolvedComponentEntity(board, component);
+
+ if (entity.entityType !== BOARD_ENTITY_TYPES.website || !entity.entityId) {
+ return false;
+ }
+
+ const reportPromise = reportCache.get(reportId) ?? getReport(reportId);
+ reportCache.set(reportId, reportPromise);
+
+ const report = await reportPromise;
+
+ return !!report && report.type === expectedReportType && report.websiteId === entity.entityId;
+}
+
+async function checkBoardEntityAccess(check: Promise) {
+ try {
+ return await check;
+ } catch {
+ return false;
+ }
+}
+
+export async function canViewBoardEntities(
+ auth: Auth,
+ type: string | undefined,
+ parameters: BoardParameters = {},
+) {
+ const { websiteIds, pixelIds, linkIds } = getBoardEntityIds({ type, parameters });
+ const userOnlyAuth: Auth = { user: auth.user };
+ const checks = [
+ ...websiteIds.map(id => checkBoardEntityAccess(canViewWebsite(userOnlyAuth, id))),
+ ...pixelIds.map(id => checkBoardEntityAccess(canViewPixel(userOnlyAuth, id))),
+ ...linkIds.map(id => checkBoardEntityAccess(canViewLink(userOnlyAuth, id))),
+ ];
+
+ const results = await Promise.all(checks);
+
+ return results.every(Boolean);
+}
+
+export async function hasValidBoardReports(
+ type: string | undefined,
+ parameters: BoardParameters = {},
+) {
+ const board = { type, parameters };
+ const reportCache = new Map>();
+
+ for (const row of parameters.rows ?? []) {
+ for (const column of row.columns ?? []) {
+ const component = column.component;
+ if (!component) {
+ continue;
+ }
+
+ if (!(await isValidBoardReportReference(board, component, reportCache))) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+export async function stripInvalidBoardReports(
+ type: string | undefined,
+ parameters: BoardParameters = {},
+) {
+ const board = { type, parameters };
+ const reportCache = new Map>();
+ let hasChanges = false;
+
+ const rows = await Promise.all(
+ (parameters.rows ?? []).map(async row => {
+ const columns = await Promise.all(
+ row.columns.map(async column => {
+ const component = column.component;
+
+ if (!component || (await isValidBoardReportReference(board, component, reportCache))) {
+ return column;
+ }
+
+ hasChanges = true;
+
+ const { reportId: _reportId, ...nextProps } = component.props ?? {};
+
+ return {
+ ...column,
+ component: {
+ ...component,
+ props: Object.keys(nextProps).length > 0 ? nextProps : undefined,
+ },
+ };
+ }),
+ );
+
+ return {
+ ...row,
+ columns,
+ };
+ }),
+ );
+
+ if (!hasChanges) {
+ return parameters;
+ }
+
+ return {
+ ...parameters,
+ rows,
+ };
+}
export async function canViewBoard({ user, shareToken }: Auth, boardId: string) {
if (user?.isAdmin) {
diff --git a/src/permissions/index.ts b/src/permissions/index.ts
index c566d77c6..f19c55e2c 100644
--- a/src/permissions/index.ts
+++ b/src/permissions/index.ts
@@ -3,6 +3,7 @@ export * from './entity';
export * from './link';
export * from './pixel';
export * from './report';
+export * from './share';
export * from './team';
export * from './user';
export * from './website';
diff --git a/src/permissions/link.ts b/src/permissions/link.ts
index f274c7b25..58b36cf33 100644
--- a/src/permissions/link.ts
+++ b/src/permissions/link.ts
@@ -8,7 +8,11 @@ export async function canViewLink({ user, shareToken }: Auth, linkId: string) {
return true;
}
- if (shareToken?.linkId === linkId || shareToken?.websiteId === linkId || shareToken?.linkIds?.includes(linkId)) {
+ if (
+ shareToken?.linkId === linkId ||
+ shareToken?.websiteId === linkId ||
+ shareToken?.linkIds?.includes(linkId)
+ ) {
return true;
}
@@ -18,6 +22,10 @@ export async function canViewLink({ user, shareToken }: Auth, linkId: string) {
const link = await getLink(linkId);
+ if (!link) {
+ return false;
+ }
+
if (link.userId) {
return user.id === link.userId;
}
@@ -42,6 +50,10 @@ export async function canUpdateLink({ user }: Auth, linkId: string) {
const link = await getLink(linkId);
+ if (!link) {
+ return false;
+ }
+
if (link.userId) {
return user.id === link.userId;
}
@@ -66,6 +78,10 @@ export async function canDeleteLink({ user }: Auth, linkId: string) {
const link = await getLink(linkId);
+ if (!link) {
+ return false;
+ }
+
if (link.userId) {
return user.id === link.userId;
}
diff --git a/src/permissions/pixel.ts b/src/permissions/pixel.ts
index 79b1b5976..23715bb84 100644
--- a/src/permissions/pixel.ts
+++ b/src/permissions/pixel.ts
@@ -8,7 +8,11 @@ export async function canViewPixel({ user, shareToken }: Auth, pixelId: string)
return true;
}
- if (shareToken?.pixelId === pixelId || shareToken?.websiteId === pixelId || shareToken?.pixelIds?.includes(pixelId)) {
+ if (
+ shareToken?.pixelId === pixelId ||
+ shareToken?.websiteId === pixelId ||
+ shareToken?.pixelIds?.includes(pixelId)
+ ) {
return true;
}
@@ -18,6 +22,10 @@ export async function canViewPixel({ user, shareToken }: Auth, pixelId: string)
const pixel = await getPixel(pixelId);
+ if (!pixel) {
+ return false;
+ }
+
if (pixel.userId) {
return user.id === pixel.userId;
}
@@ -42,6 +50,10 @@ export async function canUpdatePixel({ user }: Auth, pixelId: string) {
const pixel = await getPixel(pixelId);
+ if (!pixel) {
+ return false;
+ }
+
if (pixel.userId) {
return user.id === pixel.userId;
}
@@ -66,6 +78,10 @@ export async function canDeletePixel({ user }: Auth, pixelId: string) {
const pixel = await getPixel(pixelId);
+ if (!pixel) {
+ return false;
+ }
+
if (pixel.userId) {
return user.id === pixel.userId;
}
diff --git a/src/permissions/report.test.ts b/src/permissions/report.test.ts
new file mode 100644
index 000000000..46be11227
--- /dev/null
+++ b/src/permissions/report.test.ts
@@ -0,0 +1,125 @@
+import { expect, test, vi } from 'vitest';
+import { ENTITY_TYPE } from '@/lib/constants';
+import { canViewReport, getReportSection } from './report';
+import { canViewWebsite } from './website';
+
+vi.mock('./website', () => ({
+ canViewWebsite: vi.fn(),
+}));
+
+test('getReportSection maps saved report types to share sections', () => {
+ expect(getReportSection('goal')).toBe('goals');
+ expect(getReportSection('funnel')).toBe('funnels');
+ expect(getReportSection('journey')).toBe('journeys');
+ expect(getReportSection('revenue')).toBe('revenue');
+ expect(getReportSection('heatmap')).toBe(null);
+});
+
+test('canViewReport allows board shares to fetch included saved goal reports', async () => {
+ await expect(
+ canViewReport(
+ {
+ shareToken: {
+ shareType: ENTITY_TYPE.board,
+ websiteIds: ['website-1'],
+ parameters: {},
+ },
+ },
+ {
+ id: 'report-1',
+ userId: 'owner-1',
+ websiteId: 'website-1',
+ type: 'goal',
+ } as any,
+ ),
+ ).resolves.toBe(true);
+});
+
+test('canViewReport respects share section flags for saved funnel and goal reports', async () => {
+ await expect(
+ canViewReport(
+ {
+ shareToken: {
+ shareType: ENTITY_TYPE.website,
+ websiteId: 'website-1',
+ parameters: {
+ overview: true,
+ funnels: false,
+ goals: false,
+ },
+ },
+ },
+ {
+ id: 'report-1',
+ userId: 'owner-1',
+ websiteId: 'website-1',
+ type: 'funnel',
+ } as any,
+ ),
+ ).resolves.toBe(false);
+
+ await expect(
+ canViewReport(
+ {
+ shareToken: {
+ shareType: ENTITY_TYPE.website,
+ websiteId: 'website-1',
+ parameters: {
+ overview: true,
+ funnels: false,
+ goals: true,
+ },
+ },
+ },
+ {
+ id: 'report-2',
+ userId: 'owner-1',
+ websiteId: 'website-1',
+ type: 'goal',
+ } as any,
+ ),
+ ).resolves.toBe(true);
+});
+
+test('canViewReport denies share-token access to report types without a share section', async () => {
+ await expect(
+ canViewReport(
+ {
+ shareToken: {
+ shareType: ENTITY_TYPE.website,
+ websiteId: 'website-1',
+ parameters: {},
+ },
+ },
+ {
+ id: 'report-1',
+ userId: 'owner-1',
+ websiteId: 'website-1',
+ type: 'heatmap',
+ } as any,
+ ),
+ ).resolves.toBe(false);
+});
+
+test('canViewReport falls back to website access for authenticated users', async () => {
+ vi.mocked(canViewWebsite).mockResolvedValue(true);
+
+ await expect(
+ canViewReport(
+ {
+ user: {
+ id: 'user-1',
+ username: 'user',
+ role: 'user',
+ isAdmin: false,
+ },
+ },
+ {
+ id: 'report-1',
+ userId: 'owner-1',
+ websiteId: 'website-1',
+ type: 'heatmap',
+ } as any,
+ ),
+ ).resolves.toBe(true);
+});
diff --git a/src/permissions/report.ts b/src/permissions/report.ts
index fec37d8ca..cdec31ddc 100644
--- a/src/permissions/report.ts
+++ b/src/permissions/report.ts
@@ -1,8 +1,34 @@
import type { Report } from '@/generated/prisma/client';
import type { Auth } from '@/lib/types';
+import type { ShareSection } from './share';
+import { canViewWebsiteSection } from './share';
import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from './website';
-export async function canViewReport(auth: Auth, report: Report) {
+export function getReportSection(type?: string): ShareSection | null {
+ switch (type) {
+ case 'attribution':
+ case 'breakdown':
+ case 'performance':
+ case 'retention':
+ case 'revenue':
+ case 'utm':
+ return type;
+ case 'funnel':
+ return 'funnels';
+ case 'goal':
+ return 'goals';
+ case 'journey':
+ return 'journeys';
+ default:
+ return null;
+ }
+}
+
+export async function canViewReport(auth: Auth, report: Report | null) {
+ if (!report) {
+ return false;
+ }
+
if (auth.user?.isAdmin) {
return true;
}
@@ -11,7 +37,13 @@ export async function canViewReport(auth: Auth, report: Report) {
return true;
}
- return !!(await canViewWebsite(auth, report.websiteId));
+ const section = getReportSection(report.type);
+
+ if (section) {
+ return !!(await canViewWebsiteSection(auth, report.websiteId, section));
+ }
+
+ return !!auth.user && !!(await canViewWebsite(auth, report.websiteId));
}
export async function canUpdateReport(auth: Auth, report: Report) {
diff --git a/src/permissions/share.test.ts b/src/permissions/share.test.ts
new file mode 100644
index 000000000..693ed9af6
--- /dev/null
+++ b/src/permissions/share.test.ts
@@ -0,0 +1,148 @@
+import { expect, test, vi } from 'vitest';
+import { ENTITY_TYPE } from '@/lib/constants';
+import {
+ canViewSharedWebsite,
+ canViewSharedWebsiteFilters,
+ canViewWebsiteSection,
+} from './share';
+
+vi.mock('./website', () => ({
+ canViewWebsite: vi.fn(),
+}));
+
+test('canViewWebsiteSection allows board shares for included websites', async () => {
+ await expect(
+ canViewWebsiteSection(
+ {
+ shareToken: {
+ shareType: ENTITY_TYPE.board,
+ websiteIds: ['website-1'],
+ parameters: {},
+ },
+ },
+ 'website-1',
+ 'goals',
+ ),
+ ).resolves.toBe(true);
+});
+
+test('canViewWebsiteSection respects section flags on website shares', async () => {
+ await expect(
+ canViewWebsiteSection(
+ {
+ shareToken: {
+ shareType: ENTITY_TYPE.website,
+ websiteId: 'website-1',
+ parameters: {
+ overview: true,
+ goals: false,
+ },
+ },
+ },
+ 'website-1',
+ 'goals',
+ ),
+ ).resolves.toBe(false);
+});
+
+test('canViewWebsiteSection allows any requested enabled section', async () => {
+ await expect(
+ canViewWebsiteSection(
+ {
+ shareToken: {
+ shareType: ENTITY_TYPE.website,
+ websiteId: 'website-1',
+ parameters: {
+ overview: true,
+ compare: false,
+ },
+ },
+ },
+ 'website-1',
+ ['overview', 'compare'],
+ ),
+ ).resolves.toBe(true);
+});
+
+test('canViewSharedWebsite allows board shares for included websites', async () => {
+ await expect(
+ canViewSharedWebsite(
+ {
+ shareToken: {
+ shareType: ENTITY_TYPE.board,
+ websiteIds: ['website-1'],
+ parameters: {},
+ },
+ },
+ 'website-1',
+ ),
+ ).resolves.toBe(true);
+});
+
+test('canViewSharedWebsiteFilters requires allowFilter for share tokens', async () => {
+ await expect(
+ canViewSharedWebsiteFilters(
+ {
+ shareToken: {
+ shareType: ENTITY_TYPE.website,
+ websiteId: 'website-1',
+ parameters: {
+ allowFilter: false,
+ },
+ },
+ },
+ 'website-1',
+ ),
+ ).resolves.toBe(false);
+
+ await expect(
+ canViewSharedWebsiteFilters(
+ {
+ shareToken: {
+ shareType: ENTITY_TYPE.website,
+ websiteId: 'website-1',
+ parameters: {
+ allowFilter: true,
+ },
+ },
+ },
+ 'website-1',
+ ),
+ ).resolves.toBe(true);
+});
+
+test('canViewWebsiteSection allows pixel shares for the shared entity id', async () => {
+ await expect(
+ canViewWebsiteSection(
+ {
+ shareToken: {
+ shareType: ENTITY_TYPE.pixel,
+ pixelId: 'pixel-1',
+ parameters: {
+ overview: true,
+ },
+ },
+ },
+ 'pixel-1',
+ 'overview',
+ ),
+ ).resolves.toBe(true);
+});
+
+test('canViewWebsiteSection allows link shares for the shared entity id', async () => {
+ await expect(
+ canViewWebsiteSection(
+ {
+ shareToken: {
+ shareType: ENTITY_TYPE.link,
+ linkId: 'link-1',
+ parameters: {
+ overview: true,
+ },
+ },
+ },
+ 'link-1',
+ 'overview',
+ ),
+ ).resolves.toBe(true);
+});
diff --git a/src/permissions/share.ts b/src/permissions/share.ts
new file mode 100644
index 000000000..f041b74f1
--- /dev/null
+++ b/src/permissions/share.ts
@@ -0,0 +1,110 @@
+import { ENTITY_TYPE } from '@/lib/constants';
+import type { Auth } from '@/lib/types';
+import { canViewWebsite } from './website';
+
+export type ShareSection =
+ | 'overview'
+ | 'events'
+ | 'sessions'
+ | 'realtime'
+ | 'performance'
+ | 'compare'
+ | 'breakdown'
+ | 'goals'
+ | 'funnels'
+ | 'journeys'
+ | 'retention'
+ | 'utm'
+ | 'revenue'
+ | 'attribution';
+
+const SHARE_SECTIONS: ShareSection[] = [
+ 'overview',
+ 'events',
+ 'sessions',
+ 'realtime',
+ 'performance',
+ 'compare',
+ 'breakdown',
+ 'goals',
+ 'funnels',
+ 'journeys',
+ 'retention',
+ 'utm',
+ 'revenue',
+ 'attribution',
+];
+
+type ShareSectionInput = ShareSection | ShareSection[];
+
+function shareTokenIncludesWebsite(auth: Auth | null | undefined, websiteId: string) {
+ const { shareToken } = auth || {};
+
+ return (
+ shareToken?.websiteId === websiteId ||
+ shareToken?.pixelId === websiteId ||
+ shareToken?.linkId === websiteId ||
+ shareToken?.websiteIds?.includes(websiteId) ||
+ shareToken?.pixelIds?.includes(websiteId) ||
+ shareToken?.linkIds?.includes(websiteId)
+ );
+}
+
+export async function canViewWebsiteSection(
+ auth: Auth | null | undefined,
+ websiteId: string,
+ section: ShareSectionInput,
+) {
+ if (auth?.user) {
+ return canViewWebsite(auth, websiteId);
+ }
+
+ const { shareToken } = auth || {};
+
+ if (!shareToken || !shareTokenIncludesWebsite(auth, websiteId)) {
+ return false;
+ }
+
+ const sections = Array.isArray(section) ? section : [section];
+ const hasSectionParameters = SHARE_SECTIONS.some(
+ key => typeof shareToken.parameters?.[key] === 'boolean',
+ );
+
+ if (!hasSectionParameters) {
+ return true;
+ }
+
+ return sections.some(key => shareToken.parameters?.[key] === true);
+}
+
+export async function canViewSharedWebsite(auth: Auth | null | undefined, websiteId: string) {
+ if (auth?.user) {
+ return canViewWebsite(auth, websiteId);
+ }
+
+ return shareTokenIncludesWebsite(auth, websiteId);
+}
+
+export async function canViewSharedWebsiteFilters(
+ auth: Auth | null | undefined,
+ websiteId: string,
+) {
+ if (auth?.user) {
+ return canViewWebsite(auth, websiteId);
+ }
+
+ const { shareToken } = auth || {};
+
+ return shareTokenIncludesWebsite(auth, websiteId) && shareToken?.parameters?.allowFilter !== false;
+}
+
+export async function canViewAuthenticatedWebsite(
+ auth: Auth | null | undefined,
+ websiteId: string,
+) {
+ if (!auth?.user) {
+ return false;
+ }
+
+ return canViewWebsite(auth, websiteId);
+}
diff --git a/src/permissions/website.ts b/src/permissions/website.ts
index cf8e8d82d..e549b615e 100644
--- a/src/permissions/website.ts
+++ b/src/permissions/website.ts
@@ -1,6 +1,7 @@
import { hasPermission } from '@/lib/auth';
import { PERMISSIONS } from '@/lib/constants';
import { getEntity } from '@/lib/entity';
+import prisma from '@/lib/prisma';
import type { Auth } from '@/lib/types';
import { getTeamUser, getWebsite } from '@/queries/prisma';
@@ -39,6 +40,76 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri
return false;
}
+export async function canViewBatchWebsites({ user, shareToken }: Auth, websiteIds: string[]) {
+ if (!websiteIds.length) {
+ return [];
+ }
+
+ const requestedIds = Array.from(new Set(websiteIds));
+
+ if (user?.isAdmin) {
+ return requestedIds;
+ }
+
+ const shareAllowedIds = new Set(
+ [
+ shareToken?.websiteId,
+ shareToken?.pixelId,
+ shareToken?.linkId,
+ ...(shareToken?.websiteIds ?? []),
+ ...(shareToken?.pixelIds ?? []),
+ ...(shareToken?.linkIds ?? []),
+ ].filter((id): id is string => Boolean(id)),
+ );
+
+ if (!user) {
+ return requestedIds.filter(id => shareAllowedIds.has(id));
+ }
+
+ const websites = await prisma.client.website.findMany({
+ where: {
+ id: {
+ in: requestedIds,
+ },
+ },
+ select: {
+ id: true,
+ userId: true,
+ teamId: true,
+ },
+ });
+
+ const ownedIds = new Set(
+ websites.filter(website => website.userId === user.id).map(website => website.id),
+ );
+ const teamIds = Array.from(
+ new Set(websites.map(website => website.teamId).filter((teamId): teamId is string => Boolean(teamId))),
+ );
+ const teamUsers = teamIds.length
+ ? await prisma.client.teamUser.findMany({
+ where: {
+ userId: user.id,
+ teamId: {
+ in: teamIds,
+ },
+ },
+ select: {
+ teamId: true,
+ },
+ })
+ : [];
+ const allowedTeamIds = new Set(teamUsers.map(teamUser => teamUser.teamId));
+ const teamOwnedIds = new Set(
+ websites
+ .filter(website => website.teamId && allowedTeamIds.has(website.teamId))
+ .map(website => website.id),
+ );
+
+ return requestedIds.filter(
+ id => shareAllowedIds.has(id) || ownedIds.has(id) || teamOwnedIds.has(id),
+ );
+}
+
export async function canViewAllWebsites({ user }: Auth) {
return user?.isAdmin ?? false;
}
diff --git a/src/queries/prisma/board.ts b/src/queries/prisma/board.ts
index 862ec36dd..d433850d7 100644
--- a/src/queries/prisma/board.ts
+++ b/src/queries/prisma/board.ts
@@ -1,8 +1,11 @@
import type { Prisma } from '@/generated/prisma/client';
import { BOARD_TYPES } from '@/lib/boards';
import prisma from '@/lib/prisma';
+import { sanitizeSortFilters } from '@/lib/sort';
import type { QueryFilters } from '@/lib/types';
+const BOARD_SORT_FIELDS = ['name', 'description', 'type', 'createdAt'] as const;
+
export async function findBoard(criteria: Prisma.BoardFindUniqueArgs) {
return prisma.client.board.findUnique(criteria);
}
@@ -16,7 +19,8 @@ export async function getBoard(boardId: string) {
}
export async function getBoards(criteria: Prisma.BoardFindManyArgs, filters: QueryFilters = {}) {
- const { search } = filters;
+ const sortFilters = sanitizeSortFilters(filters, BOARD_SORT_FIELDS);
+ const { search } = sortFilters;
const { getSearchParameters, pagedQuery } = prisma;
const where: Prisma.BoardWhereInput = {
@@ -24,7 +28,7 @@ export async function getBoards(criteria: Prisma.BoardFindManyArgs, filters: Que
...getSearchParameters(search, [{ name: 'contains' }, { description: 'contains' }]),
};
- return pagedQuery('board', { ...criteria, where }, filters);
+ return pagedQuery('board', { ...criteria, where }, sortFilters);
}
export async function getUserBoards(userId: string, filters?: QueryFilters) {
diff --git a/src/queries/prisma/link.ts b/src/queries/prisma/link.ts
index 9b971dec4..c59adc025 100644
--- a/src/queries/prisma/link.ts
+++ b/src/queries/prisma/link.ts
@@ -1,7 +1,10 @@
import type { Prisma } from '@/generated/prisma/client';
import prisma from '@/lib/prisma';
+import { sanitizeSortFilters } from '@/lib/sort';
import type { QueryFilters } from '@/lib/types';
+const LINK_SORT_FIELDS = ['name', 'slug', 'url', 'createdAt'] as const;
+
export async function findLink(criteria: Prisma.LinkFindUniqueArgs) {
return prisma.client.link.findUnique(criteria);
}
@@ -15,7 +18,8 @@ export async function getLink(linkId: string) {
}
export async function getLinks(criteria: Prisma.LinkFindManyArgs, filters: QueryFilters = {}) {
- const { search } = filters;
+ const sortFilters = sanitizeSortFilters(filters, LINK_SORT_FIELDS);
+ const { search } = sortFilters;
const { getSearchParameters, pagedQuery } = prisma;
const where: Prisma.LinkWhereInput = {
@@ -27,7 +31,7 @@ export async function getLinks(criteria: Prisma.LinkFindManyArgs, filters: Query
]),
};
- return pagedQuery('link', { ...criteria, where }, filters);
+ return pagedQuery('link', { ...criteria, where }, sortFilters);
}
export async function getUserLinks(userId: string, filters?: QueryFilters) {
diff --git a/src/queries/prisma/pixel.ts b/src/queries/prisma/pixel.ts
index 4c9e132d5..2ea9c6f28 100644
--- a/src/queries/prisma/pixel.ts
+++ b/src/queries/prisma/pixel.ts
@@ -1,7 +1,10 @@
import type { Prisma } from '@/generated/prisma/client';
import prisma from '@/lib/prisma';
+import { sanitizeSortFilters } from '@/lib/sort';
import type { QueryFilters } from '@/lib/types';
+const PIXEL_SORT_FIELDS = ['name', 'slug', 'createdAt'] as const;
+
export async function findPixel(criteria: Prisma.PixelFindUniqueArgs) {
return prisma.client.pixel.findUnique(criteria);
}
@@ -15,14 +18,15 @@ export async function getPixel(pixelId: string) {
}
export async function getPixels(criteria: Prisma.PixelFindManyArgs, filters: QueryFilters = {}) {
- const { search } = filters;
+ const sortFilters = sanitizeSortFilters(filters, PIXEL_SORT_FIELDS);
+ const { search } = sortFilters;
const where: Prisma.PixelWhereInput = {
...criteria.where,
...prisma.getSearchParameters(search, [{ name: 'contains' }, { slug: 'contains' }]),
};
- return prisma.pagedQuery('pixel', { ...criteria, where }, filters);
+ return prisma.pagedQuery('pixel', { ...criteria, where }, sortFilters);
}
export async function getUserPixels(userId: string, filters?: QueryFilters) {
diff --git a/src/queries/prisma/report.ts b/src/queries/prisma/report.ts
index 4a5b755c8..f8a59d4a9 100644
--- a/src/queries/prisma/report.ts
+++ b/src/queries/prisma/report.ts
@@ -18,6 +18,7 @@ export async function getReport(reportId: string) {
export async function getReports(criteria: ReportFindManyArgs, filters: QueryFilters = {}) {
const { search } = filters;
+ const orderBy = criteria.orderBy ?? [{ name: 'asc' }, { id: 'asc' }];
const where: Prisma.ReportWhereInput = {
...criteria.where,
@@ -43,7 +44,7 @@ export async function getReports(criteria: ReportFindManyArgs, filters: QueryFil
]),
};
- return prisma.pagedQuery('report', { ...criteria, where }, filters);
+ return prisma.pagedQuery('report', { ...criteria, where, orderBy }, filters);
}
export async function getUserReports(userId: string, filters?: QueryFilters) {
diff --git a/src/queries/prisma/team.ts b/src/queries/prisma/team.ts
index de938df35..3428d9ee0 100644
--- a/src/queries/prisma/team.ts
+++ b/src/queries/prisma/team.ts
@@ -2,10 +2,14 @@ import { Prisma, type Team } from '@/generated/prisma/client';
import { ROLES } from '@/lib/constants';
import { uuid } from '@/lib/crypto';
import prisma from '@/lib/prisma';
+import redis from '@/lib/redis';
+import { sanitizeSortFilters } from '@/lib/sort';
import type { PageResult, QueryFilters } from '@/lib/types';
import TeamFindManyArgs = Prisma.TeamFindManyArgs;
+const TEAM_SORT_FIELDS = ['name', 'createdAt'] as const;
+
export async function findTeam(criteria: Prisma.TeamFindUniqueArgs): Promise {
return prisma.client.team.findUnique(criteria);
}
@@ -29,7 +33,8 @@ export async function getTeams(
filters: QueryFilters,
): Promise> {
const { getSearchParameters } = prisma;
- const { search } = filters;
+ const sortFilters = sanitizeSortFilters(filters, TEAM_SORT_FIELDS);
+ const { search } = sortFilters;
const where: Prisma.TeamWhereInput = {
...criteria.where,
@@ -42,7 +47,7 @@ export async function getTeams(
...criteria,
where,
},
- filters,
+ sortFilters,
);
}
@@ -100,6 +105,17 @@ export async function getAllUserTeams(userId: string) {
});
}
+export async function getUserOwnedTeamCount(userId: string) {
+ return prisma.client.team.count({
+ where: {
+ deletedAt: null,
+ members: {
+ some: { userId, role: ROLES.teamOwner },
+ },
+ },
+ });
+}
+
export async function getTeamOwner(teamId: string) {
return prisma.client.teamUser.findFirst({
where: { teamId, role: ROLES.teamOwner },
@@ -144,6 +160,31 @@ export async function deleteTeam(teamId: string) {
const { client, transaction } = prisma;
const cloudMode = !!process.env.CLOUD_MODE;
+ const [links, pixels, boards] = await Promise.all([
+ client.link.findMany({
+ where: { teamId },
+ select: { id: true, slug: true, deletedAt: true },
+ }),
+ client.pixel.findMany({
+ where: { teamId },
+ select: { id: true, slug: true, deletedAt: true },
+ }),
+ client.board.findMany({ where: { teamId }, select: { id: true } }),
+ ]);
+ const entityIds = [...links.map(l => l.id), ...pixels.map(p => p.id), ...boards.map(b => b.id)];
+ // Only invalidate Redis cache for slugs that are still live (not already soft-deleted).
+ const linkSlugs = links.filter(l => !l.deletedAt).map(l => l.slug);
+ const pixelSlugs = pixels.filter(p => !p.deletedAt).map(p => p.slug);
+
+ const invalidateRedis = async () => {
+ if (redis.enabled && (linkSlugs.length || pixelSlugs.length)) {
+ await Promise.all([
+ ...linkSlugs.map(slug => redis.client.del(`link:${slug}`)),
+ ...pixelSlugs.map(slug => redis.client.del(`pixel:${slug}`)),
+ ]);
+ }
+ };
+
if (cloudMode) {
return transaction([
client.team.update({
@@ -154,7 +195,21 @@ export async function deleteTeam(teamId: string) {
id: teamId,
},
}),
- ]);
+ client.share.deleteMany({ where: { entityId: { in: entityIds } } }),
+ // deletedAt: null avoids restamping rows that were already soft-deleted earlier.
+ client.link.updateMany({
+ data: { deletedAt: new Date() },
+ where: { teamId, deletedAt: null },
+ }),
+ client.pixel.updateMany({
+ data: { deletedAt: new Date() },
+ where: { teamId, deletedAt: null },
+ }),
+ client.board.deleteMany({ where: { teamId } }),
+ ]).then(async result => {
+ await invalidateRedis();
+ return result;
+ });
}
return transaction([
@@ -163,10 +218,17 @@ export async function deleteTeam(teamId: string) {
teamId,
},
}),
+ client.share.deleteMany({ where: { entityId: { in: entityIds } } }),
+ client.link.deleteMany({ where: { teamId } }),
+ client.pixel.deleteMany({ where: { teamId } }),
+ client.board.deleteMany({ where: { teamId } }),
client.team.delete({
where: {
id: teamId,
},
}),
- ]);
+ ]).then(async result => {
+ await invalidateRedis();
+ return result;
+ });
}
diff --git a/src/queries/prisma/user.ts b/src/queries/prisma/user.ts
index 0de5a71b7..17a254ca5 100644
--- a/src/queries/prisma/user.ts
+++ b/src/queries/prisma/user.ts
@@ -2,10 +2,14 @@ import { Prisma } from '@/generated/prisma/client';
import { ROLES } from '@/lib/constants';
import { getRandomChars } from '@/lib/generate';
import prisma from '@/lib/prisma';
+import redis from '@/lib/redis';
+import { sanitizeSortFilters } from '@/lib/sort';
import type { QueryFilters, Role } from '@/lib/types';
import UserFindManyArgs = Prisma.UserFindManyArgs;
+const USER_SORT_FIELDS = ['username', 'role', 'createdAt'] as const;
+
export interface GetUserOptions {
includePassword?: boolean;
showDeleted?: boolean;
@@ -43,11 +47,15 @@ export async function getUser(userId: string, options: GetUserOptions = {}) {
}
export async function getUserByUsername(username: string, options: GetUserOptions = {}) {
- return findUser({ where: { username } }, options);
+ return findUser({ where: { username: username.toLowerCase() } }, options);
}
export async function getUsers(criteria: UserFindManyArgs, filters: QueryFilters = {}) {
- const { search } = filters;
+ const sortFilters = sanitizeSortFilters(filters, USER_SORT_FIELDS, {
+ orderBy: 'createdAt',
+ sortDescending: true,
+ });
+ const { search } = sortFilters;
const where: Prisma.UserWhereInput = {
...criteria.where,
@@ -61,11 +69,7 @@ export async function getUsers(criteria: UserFindManyArgs, filters: QueryFilters
...criteria,
where,
},
- {
- orderBy: 'createdAt',
- sortDescending: true,
- ...filters,
- },
+ sortFilters,
);
}
@@ -127,6 +131,33 @@ export async function deleteUser(userId: string) {
const teamIds = teams.map(a => a.id);
+ const ownedFilter = cloudMode ? { userId } : { OR: [{ userId }, { teamId: { in: teamIds } }] };
+
+ const [links, pixels, boards] = await Promise.all([
+ client.link.findMany({
+ where: ownedFilter,
+ select: { id: true, slug: true, deletedAt: true },
+ }),
+ client.pixel.findMany({
+ where: ownedFilter,
+ select: { id: true, slug: true, deletedAt: true },
+ }),
+ client.board.findMany({ where: ownedFilter, select: { id: true } }),
+ ]);
+ const entityIds = [...links.map(l => l.id), ...pixels.map(p => p.id), ...boards.map(b => b.id)];
+ // Only invalidate Redis cache for slugs that are still live (not already soft-deleted).
+ const linkSlugs = links.filter(l => !l.deletedAt).map(l => l.slug);
+ const pixelSlugs = pixels.filter(p => !p.deletedAt).map(p => p.slug);
+
+ const invalidateRedis = async () => {
+ if (redis.enabled && (linkSlugs.length || pixelSlugs.length)) {
+ await Promise.all([
+ ...linkSlugs.map(slug => redis.client.del(`link:${slug}`)),
+ ...pixelSlugs.map(slug => redis.client.del(`pixel:${slug}`)),
+ ]);
+ }
+ };
+
if (cloudMode) {
return transaction([
client.website.updateMany({
@@ -144,7 +175,21 @@ export async function deleteUser(userId: string) {
id: userId,
},
}),
- ]);
+ client.share.deleteMany({ where: { entityId: { in: entityIds } } }),
+ // deletedAt: null avoids restamping rows that were already soft-deleted earlier.
+ client.link.updateMany({
+ data: { deletedAt: new Date() },
+ where: { userId, deletedAt: null },
+ }),
+ client.pixel.updateMany({
+ data: { deletedAt: new Date() },
+ where: { userId, deletedAt: null },
+ }),
+ client.board.deleteMany({ where: { userId } }),
+ ]).then(async result => {
+ await invalidateRedis();
+ return result;
+ });
}
return transaction([
@@ -195,6 +240,10 @@ export async function deleteUser(userId: string) {
],
},
}),
+ client.share.deleteMany({ where: { entityId: { in: entityIds } } }),
+ client.link.deleteMany({ where: ownedFilter }),
+ client.pixel.deleteMany({ where: ownedFilter }),
+ client.board.deleteMany({ where: ownedFilter }),
client.website.deleteMany({
where: { id: { in: websiteIds } },
}),
@@ -203,5 +252,8 @@ export async function deleteUser(userId: string) {
id: userId,
},
}),
- ]);
+ ]).then(async result => {
+ await invalidateRedis();
+ return result;
+ });
}
diff --git a/src/queries/prisma/website.ts b/src/queries/prisma/website.ts
index 6c69431e7..3b647ba2d 100644
--- a/src/queries/prisma/website.ts
+++ b/src/queries/prisma/website.ts
@@ -2,8 +2,11 @@ import type { Prisma, Website } from '@/generated/prisma/client';
import { ROLES } from '@/lib/constants';
import prisma from '@/lib/prisma';
import redis from '@/lib/redis';
+import { sanitizeSortFilters } from '@/lib/sort';
import type { QueryFilters } from '@/lib/types';
+const WEBSITE_SORT_FIELDS = ['name', 'domain', 'createdAt'] as const;
+
export async function findWebsite(criteria: Prisma.WebsiteFindUniqueArgs) {
return prisma.client.website.findUnique(criteria);
}
@@ -23,7 +26,8 @@ export async function getWebsite(websiteId: string) {
}
export async function getWebsites(criteria: Prisma.WebsiteFindManyArgs, filters: QueryFilters) {
- const { search } = filters;
+ const sortFilters = sanitizeSortFilters(filters, WEBSITE_SORT_FIELDS);
+ const { search } = sortFilters;
const { getSearchParameters, pagedQuery } = prisma;
const where: Prisma.WebsiteWhereInput = {
@@ -37,12 +41,15 @@ export async function getWebsites(criteria: Prisma.WebsiteFindManyArgs, filters:
deletedAt: null,
};
- const websites = await pagedQuery('website', { ...criteria, where }, filters);
+ const websites = await pagedQuery('website', { ...criteria, where }, sortFilters);
return attachShareIdToWebsites(websites);
}
-export async function getAllUserWebsitesIncludingTeamOwner(userId: string, filters?: QueryFilters) {
+export async function getAllUserWebsitesIncludingTeamAccess(
+ userId: string,
+ filters?: QueryFilters,
+) {
return getWebsites(
{
where: {
@@ -53,7 +60,7 @@ export async function getAllUserWebsitesIncludingTeamOwner(userId: string, filte
deletedAt: null,
members: {
some: {
- role: ROLES.teamOwner,
+ role: { in: [ROLES.teamOwner, ROLES.teamManager] },
userId,
},
},
@@ -62,10 +69,7 @@ export async function getAllUserWebsitesIncludingTeamOwner(userId: string, filte
],
},
},
- {
- orderBy: 'name',
- ...filters,
- },
+ sanitizeSortFilters(filters, WEBSITE_SORT_FIELDS, { orderBy: 'name' }),
);
}
@@ -84,10 +88,7 @@ export async function getUserWebsites(userId: string, filters?: QueryFilters) {
},
},
},
- {
- orderBy: 'name',
- ...filters,
- },
+ sanitizeSortFilters(filters, WEBSITE_SORT_FIELDS, { orderBy: 'name' }),
);
}
@@ -265,6 +266,15 @@ export async function getWebsiteCount(userId: string) {
});
}
+export async function getTeamWebsiteCount(teamId: string) {
+ return prisma.client.website.count({
+ where: {
+ teamId,
+ deletedAt: null,
+ },
+ });
+}
+
export async function attachShareIdToWebsite(website: Website) {
const share = await prisma.client.share.findFirst({
where: {
diff --git a/src/queries/sql/events/getEventData.ts b/src/queries/sql/events/getEventData.ts
index 4a3fac860..ebd0df983 100644
--- a/src/queries/sql/events/getEventData.ts
+++ b/src/queries/sql/events/getEventData.ts
@@ -39,10 +39,9 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
group by website_event.event_id
`;
- const count = await rawQuery(
- `select count(*) as num from (${eventQuery}) t`,
- queryParams,
- ).then((res: any) => res[0].num);
+ const count = await rawQuery(`select count(*) as num from (${eventQuery}) t`, queryParams).then(
+ (res: any) => res[0].num,
+ );
const data = await rawQuery(
`
@@ -106,10 +105,9 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
group by event_data.event_id
`;
- const count = await rawQuery(
- `select count(*) as num from (${eventQuery}) t`,
- queryParams,
- ).then((res: any) => res[0].num);
+ const count = await rawQuery(`select count(*) as num from (${eventQuery}) t`, queryParams).then(
+ (res: any) => res[0].num,
+ );
const data = await rawQuery(
`
@@ -149,4 +147,4 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
);
return { data, count, page: +page, pageSize: size };
-}
\ No newline at end of file
+}
diff --git a/src/queries/sql/events/getEventDataArraySeries.ts b/src/queries/sql/events/getEventDataArraySeries.ts
index b9aa1e476..8e06e906c 100644
--- a/src/queries/sql/events/getEventDataArraySeries.ts
+++ b/src/queries/sql/events/getEventDataArraySeries.ts
@@ -29,12 +29,13 @@ async function relationalQuery(
eventFilters: EventPropertyFilter[] = [],
): Promise {
const { timezone = 'utc', unit = 'day' } = filters;
- const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = prisma;
+ const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = prisma;
const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
...filters,
websiteId,
+ timezone,
});
- const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters);
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone);
return rawQuery(
`
@@ -56,11 +57,11 @@ async function relationalQuery(
and event_data.data_key = {{propertyName}}
and event_data.data_type = ${DATA_TYPE.array}
${filterQuery}
- ${epfSQL}
+ ${pfSQL}
group by 1, 2
order by 2
`,
- { ...queryParams, eventName, propertyName, ...epfParams },
+ { ...queryParams, eventName, propertyName, ...pfParams },
FUNCTION_NAME,
);
}
@@ -73,9 +74,13 @@ async function clickhouseQuery(
eventFilters: EventPropertyFilter[] = [],
): Promise {
const { timezone = 'UTC', unit = 'day' } = filters;
- const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = clickhouse;
- const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
- const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters);
+ const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = clickhouse;
+ const { filterQuery, cohortQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone);
return rawQuery(
`
@@ -97,14 +102,15 @@ async function clickhouseQuery(
${cohortQuery}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_data.event_name = {eventName:String}
and event_data.data_key = {propertyName:String}
and event_data.data_type = ${DATA_TYPE.array}
${filterQuery}
- ${epfSQL}
+ ${pfSQL}
group by x, t
order by t
`,
- { ...queryParams, eventName, propertyName, ...epfParams },
+ { ...queryParams, eventName, propertyName, ...pfParams },
FUNCTION_NAME,
);
}
diff --git a/src/queries/sql/events/getEventDataDateSeries.ts b/src/queries/sql/events/getEventDataDateSeries.ts
index eda146515..ac4ebff26 100644
--- a/src/queries/sql/events/getEventDataDateSeries.ts
+++ b/src/queries/sql/events/getEventDataDateSeries.ts
@@ -27,14 +27,15 @@ async function relationalQuery(
propertyName: string,
filters: QueryFilters,
eventFilters: EventPropertyFilter[] = [],
-) : Promise {
- const { timezone = 'UTC' } = filters;
- const { rawQuery, parseFilters, getDateStringSQL, getEventPropertyFilterQuery } = prisma;
+): Promise {
+ const { timezone = 'utc' } = filters;
+ const { rawQuery, parseFilters, getDateStringSQL, getPropertyFilterQuery } = prisma;
const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
...filters,
websiteId,
+ timezone,
});
- const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters);
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone);
return rawQuery(
`
@@ -54,11 +55,11 @@ async function relationalQuery(
and event_data.data_key = {{propertyName}}
and event_data.data_type = ${DATA_TYPE.date}
${filterQuery}
- ${epfSQL}
+ ${pfSQL}
group by 1
order by 1
`,
- { ...queryParams, eventName, propertyName, ...epfParams },
+ { ...queryParams, eventName, propertyName, ...pfParams },
FUNCTION_NAME,
);
}
@@ -71,9 +72,13 @@ async function clickhouseQuery(
eventFilters: EventPropertyFilter[] = [],
): Promise {
const { timezone = 'UTC' } = filters;
- const { rawQuery, parseFilters, getDateStringSQL, getEventPropertyFilterQuery } = clickhouse;
- const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
- const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters);
+ const { rawQuery, parseFilters, getDateStringSQL, getPropertyFilterQuery } = clickhouse;
+ const { filterQuery, cohortQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone);
return rawQuery(
`
@@ -94,14 +99,15 @@ async function clickhouseQuery(
${cohortQuery}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_data.event_name = {eventName:String}
and event_data.data_key = {propertyName:String}
and event_data.data_type = ${DATA_TYPE.date}
${filterQuery}
- ${epfSQL}
+ ${pfSQL}
group by t
order by t
`,
- { ...queryParams, eventName, propertyName, ...epfParams },
+ { ...queryParams, eventName, propertyName, ...pfParams },
FUNCTION_NAME,
);
}
diff --git a/src/queries/sql/events/getEventDataEvents.ts b/src/queries/sql/events/getEventDataEvents.ts
index f415e629d..92f69bdae 100644
--- a/src/queries/sql/events/getEventDataEvents.ts
+++ b/src/queries/sql/events/getEventDataEvents.ts
@@ -70,6 +70,8 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
where event_data.website_id = {{websiteId::uuid}}
and event_data.created_at between {{startDate}} and {{endDate}}
${filterQuery}
+ group by website_event.event_name, event_data.data_key, event_data.data_type
+ order by 1 asc, 2 asc
limit 500
`,
queryParams,
@@ -92,10 +94,10 @@ async function clickhouseQuery(
return rawQuery(
`
select
- event_name as eventName,
- data_key as propertyName,
- data_type as dataType,
- string_value as propertyValue,
+ event_data.event_name as eventName,
+ event_data.data_key as propertyName,
+ event_data.data_type as dataType,
+ event_data.string_value as propertyValue,
count(*) as total
from event_data
any left join (
@@ -111,7 +113,11 @@ async function clickhouseQuery(
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
- group by data_key, data_type, string_value, event_name
+ group by
+ event_data.data_key,
+ event_data.data_type,
+ event_data.string_value,
+ event_data.event_name
order by 1 asc, 2 asc, 3 asc, 5 desc
limit 500
`,
@@ -123,9 +129,9 @@ async function clickhouseQuery(
return rawQuery(
`
select
- event_name as eventName,
- data_key as propertyName,
- data_type as dataType,
+ event_data.event_name as eventName,
+ event_data.data_key as propertyName,
+ event_data.data_type as dataType,
count(*) as total
from event_data
any left join (
@@ -141,7 +147,7 @@ async function clickhouseQuery(
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
- group by data_key, data_type, event_name
+ group by event_data.data_key, event_data.data_type, event_data.event_name
order by 1 asc, 2 asc
limit 500
`,
diff --git a/src/queries/sql/events/getEventDataNumericSeries.ts b/src/queries/sql/events/getEventDataNumericSeries.ts
index aa039cdad..d9df94f54 100644
--- a/src/queries/sql/events/getEventDataNumericSeries.ts
+++ b/src/queries/sql/events/getEventDataNumericSeries.ts
@@ -30,16 +30,19 @@ async function relationalQuery(
eventFilters: EventPropertyFilter[] = [],
) {
const { timezone = 'utc', unit = 'day' } = filters;
- const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = prisma;
+ const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = prisma;
const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
...filters,
websiteId,
+ timezone,
});
- const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters);
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone);
const aggSql =
- metric === 'avg' ? 'avg(cast(event_data.number_value as decimal))' :
- metric === 'count' ? 'count(*)' :
- 'sum(cast(event_data.number_value as decimal))';
+ metric === 'avg'
+ ? 'avg(cast(event_data.number_value as decimal))'
+ : metric === 'count'
+ ? 'count(*)'
+ : 'sum(cast(event_data.number_value as decimal))';
return rawQuery(
`
@@ -59,11 +62,11 @@ async function relationalQuery(
and event_data.data_key = {{propertyName}}
and event_data.data_type = 2
${filterQuery}
- ${epfSQL}
+ ${pfSQL}
group by 1
order by 1
`,
- { ...queryParams, eventName, propertyName, ...epfParams },
+ { ...queryParams, eventName, propertyName, ...pfParams },
FUNCTION_NAME,
);
}
@@ -77,13 +80,19 @@ async function clickhouseQuery(
eventFilters: EventPropertyFilter[] = [],
): Promise<{ t: string; y: number }[]> {
const { timezone = 'UTC', unit = 'day' } = filters;
- const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = clickhouse;
- const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
- const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters);
+ const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = clickhouse;
+ const { filterQuery, cohortQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone);
const aggSql =
- metric === 'avg' ? 'avg(event_data.number_value)' :
- metric === 'count' ? 'count()' :
- 'sum(event_data.number_value)';
+ metric === 'avg'
+ ? 'avg(event_data.number_value)'
+ : metric === 'count'
+ ? 'count()'
+ : 'sum(event_data.number_value)';
return rawQuery(
`
@@ -104,14 +113,15 @@ async function clickhouseQuery(
${cohortQuery}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_data.event_name = {eventName:String}
and event_data.data_key = {propertyName:String}
and event_data.data_type = 2
${filterQuery}
- ${epfSQL}
+ ${pfSQL}
group by t
order by t
`,
- { ...queryParams, eventName, propertyName, ...epfParams },
+ { ...queryParams, eventName, propertyName, ...pfParams },
FUNCTION_NAME,
);
}
diff --git a/src/queries/sql/events/getEventDataNumericStats.ts b/src/queries/sql/events/getEventDataNumericStats.ts
index 8b649b792..5977ff3ac 100644
--- a/src/queries/sql/events/getEventDataNumericStats.ts
+++ b/src/queries/sql/events/getEventDataNumericStats.ts
@@ -27,12 +27,14 @@ async function relationalQuery(
filters: QueryFilters,
eventFilters: EventPropertyFilter[] = [],
) {
- const { rawQuery, parseFilters, getEventPropertyFilterQuery } = prisma;
+ const { timezone = 'utc' } = filters;
+ const { rawQuery, parseFilters, getPropertyFilterQuery } = prisma;
const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
...filters,
websiteId,
+ timezone,
});
- const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters);
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone);
return rawQuery(
`
@@ -55,9 +57,9 @@ async function relationalQuery(
and event_data.data_key = {{propertyName}}
and event_data.data_type = 2
${filterQuery}
- ${epfSQL}
+ ${pfSQL}
`,
- { ...queryParams, eventName, propertyName, ...epfParams },
+ { ...queryParams, eventName, propertyName, ...pfParams },
FUNCTION_NAME,
);
}
@@ -69,9 +71,14 @@ async function clickhouseQuery(
filters: QueryFilters,
eventFilters: EventPropertyFilter[] = [],
): Promise {
- const { rawQuery, parseFilters, getEventPropertyFilterQuery } = clickhouse;
- const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
- const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters);
+ const { timezone = 'UTC' } = filters;
+ const { rawQuery, parseFilters, getPropertyFilterQuery } = clickhouse;
+ const { filterQuery, cohortQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone);
return rawQuery(
`
@@ -95,12 +102,13 @@ async function clickhouseQuery(
${cohortQuery}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_data.event_name = {eventName:String}
and event_data.data_key = {propertyName:String}
and event_data.data_type = 2
${filterQuery}
- ${epfSQL}
+ ${pfSQL}
`,
- { ...queryParams, eventName, propertyName, ...epfParams },
+ { ...queryParams, eventName, propertyName, ...pfParams },
FUNCTION_NAME,
);
}
diff --git a/src/queries/sql/events/getEventDataPivot.ts b/src/queries/sql/events/getEventDataPivot.ts
index 00044b649..210a7cdc6 100644
--- a/src/queries/sql/events/getEventDataPivot.ts
+++ b/src/queries/sql/events/getEventDataPivot.ts
@@ -1,5 +1,4 @@
import clickhouse from '@/lib/clickhouse';
-import { DEFAULT_PAGE_SIZE } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import type { EventPropertyFilter, QueryFilters } from '@/lib/types';
@@ -20,42 +19,26 @@ export async function getEventDataPivot(
});
}
-async function relationalQuery(websiteId: string, eventName: string, filters: QueryFilters, eventFilters: EventPropertyFilter[] = []) {
- const { rawQuery, parseFilters, getEventPropertyFilterQuery } = prisma;
- const { page = 1, pageSize } = filters;
- const size = +pageSize || DEFAULT_PAGE_SIZE;
- const offset = +size * (+page - 1);
+async function relationalQuery(
+ websiteId: string,
+ eventName: string,
+ filters: QueryFilters,
+ eventFilters: EventPropertyFilter[] = [],
+) {
+ const { timezone = 'utc' } = filters;
+ const { pagedRawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = prisma;
const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
...filters,
websiteId,
+ timezone,
});
- const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters);
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone);
- const countResult = await rawQuery(
- `
- select count(distinct website_event.event_id) as num
- from website_event
- join event_data on event_data.website_event_id = website_event.event_id
- and event_data.website_id = {{websiteId::uuid}}
- and event_data.created_at between {{startDate}} and {{endDate}}
- ${cohortQuery}
- ${joinSessionQuery}
- where website_event.website_id = {{websiteId::uuid}}
- and website_event.created_at between {{startDate}} and {{endDate}}
- and website_event.event_name = {{eventName}}
- ${filterQuery}
- ${epfSQL}
- `,
- { ...queryParams, eventName, ...epfParams },
- );
-
- const count = countResult[0].num;
-
- const rows = await rawQuery(
+ return pagedRawQuery(
`
with paged_events as (
- select website_event.event_id
+ select website_event.event_id, max(website_event.created_at) as sort_created_at
from website_event
join event_data on event_data.website_event_id = website_event.event_id
and event_data.website_id = {{websiteId::uuid}}
@@ -66,96 +49,81 @@ async function relationalQuery(websiteId: string, eventName: string, filters: Qu
and website_event.created_at between {{startDate}} and {{endDate}}
and website_event.event_name = {{eventName}}
${filterQuery}
- ${epfSQL}
+ ${pfSQL}
group by website_event.event_id
- order by max(website_event.created_at) desc
- limit ${size} offset ${offset}
)
select
website_event.event_id as "eventId",
website_event.session_id as "sessionId",
website_event.event_name as "eventName",
website_event.url_path as "urlPath",
- website_event.created_at as "createdAt",
- event_data.data_key as "dataKey",
- coalesce(
- case when event_data.data_type = 1 then event_data.string_value end,
- case when event_data.data_type = 2 then cast(event_data.number_value as varchar) end,
- case when event_data.data_type = 4 then cast(event_data.date_value as varchar) end,
- ''
- ) as "value",
- event_data.data_type as "dataType"
+ max(website_event.created_at) as "createdAt",
+ array_agg(event_data.data_key order by event_data.data_key asc) as "propertyKeys",
+ array_agg(
+ coalesce(
+ case when event_data.data_type = 1 then event_data.string_value end,
+ case when event_data.data_type = 2 then cast(event_data.number_value as varchar) end,
+ case when event_data.data_type = 3 then event_data.string_value end,
+ case when event_data.data_type = 4 then ${getDateStringSQL('event_data.date_value', 'second', timezone)} end,
+ case when event_data.data_type = 5 then event_data.string_value end,
+ ''
+ )
+ order by event_data.data_key asc
+ ) as "propertyValues"
from event_data
join website_event on website_event.event_id = event_data.website_event_id
and website_event.website_id = {{websiteId::uuid}}
join paged_events on paged_events.event_id = event_data.website_event_id
where event_data.website_id = {{websiteId::uuid}}
and event_data.created_at between {{startDate}} and {{endDate}}
- order by website_event.created_at desc
+ group by
+ website_event.event_id,
+ website_event.session_id,
+ website_event.event_name,
+ website_event.url_path,
+ paged_events.sort_created_at
+ order by paged_events.sort_created_at desc
`,
- { ...queryParams, eventName, ...epfParams },
+ { ...queryParams, eventName, ...pfParams },
+ filters,
FUNCTION_NAME,
);
-
- // Pivot flat rows into one record per event
- const eventMap = new Map<
- string,
- { eventId: string; sessionId: string; eventName: string; urlPath: string; createdAt: Date; propertyKeys: string[]; propertyValues: string[] }
- >();
- for (const { eventId, sessionId, eventName: name, urlPath, createdAt, dataKey, value } of rows) {
- if (!eventMap.has(eventId)) {
- eventMap.set(eventId, { eventId, sessionId, eventName: name, urlPath, createdAt, propertyKeys: [], propertyValues: [] });
- }
- const entry = eventMap.get(eventId);
- entry.propertyKeys.push(dataKey);
- entry.propertyValues.push(value ?? '');
- }
-
- return { data: [...eventMap.values()], count, page: +page, pageSize: size };
}
-async function clickhouseQuery(websiteId: string, eventName: string, filters: QueryFilters, eventFilters: EventPropertyFilter[] = []) {
- const { rawQuery, parseFilters, getEventPropertyFilterQuery } = clickhouse;
- const { page = 1, pageSize } = filters;
- const size = +pageSize || DEFAULT_PAGE_SIZE;
- const offset = +size * (+page - 1);
+async function clickhouseQuery(
+ websiteId: string,
+ eventName: string,
+ filters: QueryFilters,
+ eventFilters: EventPropertyFilter[] = [],
+) {
+ const { timezone = 'UTC' } = filters;
+ const { pagedRawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = clickhouse;
- const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
- const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters);
+ const { filterQuery, cohortQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone);
- const count = await rawQuery(
- `
- select count() as num
- from umami.event_data_pivot
- any left join (
- select *
- from website_event
- where website_id = {websiteId:UUID}
- and created_at between {startDate:DateTime64} and {endDate:DateTime64}
- and event_type = 2
- and event_name = {eventName:String}) website_event
- on website_event.event_id = event_data_pivot.event_id
- and website_event.session_id = event_data_pivot.session_id
- and website_event.website_id = event_data_pivot.website_id
- ${cohortQuery}
- where event_data_pivot.website_id = {websiteId:UUID}
- and event_data_pivot.created_at between {startDate:DateTime64} and {endDate:DateTime64}
- ${filterQuery}
- ${epfSQL}
- `,
- { ...queryParams, eventName, ...epfParams },
- ).then((res: any) => res[0].num);
-
- const data = await rawQuery(
+ return pagedRawQuery(
`
select
- event_id as eventId,
- session_id as sessionId,
- event_name as eventName,
- url_path as urlPath,
- created_at as createdAt,
+ event_data_pivot.event_id as eventId,
+ event_data_pivot.session_id as sessionId,
+ event_data_pivot.event_name as eventName,
+ event_data_pivot.url_path as urlPath,
+ event_data_pivot.created_at as createdAt,
groupArrayMerge(property_keys) as propertyKeys,
- groupArrayMerge(property_values) as propertyValues
+ arrayMap(
+ (value, dataType) -> if(
+ dataType = 4,
+ ${getDateStringSQL('parseDateTimeBestEffortOrNull(value)', 'second', timezone)},
+ value
+ ),
+ groupArrayMerge(property_values),
+ groupArrayMerge(property_types)
+ ) as propertyValues
from umami.event_data_pivot
any left join (
select *
@@ -170,15 +138,19 @@ async function clickhouseQuery(websiteId: string, eventName: string, filters: Qu
${cohortQuery}
where event_data_pivot.website_id = {websiteId:UUID}
and event_data_pivot.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_data_pivot.event_name = {eventName:String}
${filterQuery}
- ${epfSQL}
- group by event_id, session_id, event_name, url_path, created_at
- order by created_at desc
- limit ${size} offset ${offset}
+ ${pfSQL}
+ group by
+ event_data_pivot.event_id,
+ event_data_pivot.session_id,
+ event_data_pivot.event_name,
+ event_data_pivot.url_path,
+ event_data_pivot.created_at
+ order by event_data_pivot.created_at desc
`,
- { ...queryParams, eventName, ...epfParams },
+ { ...queryParams, eventName, ...pfParams },
+ filters,
FUNCTION_NAME,
);
-
- return { data, count, page: +page, pageSize: size };
}
diff --git a/src/queries/sql/events/getEventDataProperties.ts b/src/queries/sql/events/getEventDataProperties.ts
index b7b7c7379..943dca84d 100644
--- a/src/queries/sql/events/getEventDataProperties.ts
+++ b/src/queries/sql/events/getEventDataProperties.ts
@@ -66,9 +66,9 @@ async function clickhouseQuery(
return rawQuery(
`
select
- event_name as eventName,
- data_key as propertyName,
- data_type as dataType,
+ event_data.event_name as eventName,
+ event_data.data_key as propertyName,
+ event_data.data_type as dataType,
count(*) as total
from event_data
any left join (
@@ -84,7 +84,7 @@ async function clickhouseQuery(
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
- group by event_name, data_key, data_type
+ group by event_data.event_name, event_data.data_key, event_data.data_type
order by 1, 4 desc
limit 500
`,
diff --git a/src/queries/sql/events/getEventDataPropertySeries.ts b/src/queries/sql/events/getEventDataPropertySeries.ts
index e75004fa0..7cc7071c6 100644
--- a/src/queries/sql/events/getEventDataPropertySeries.ts
+++ b/src/queries/sql/events/getEventDataPropertySeries.ts
@@ -29,12 +29,13 @@ async function relationalQuery(
eventFilters: EventPropertyFilter[] = [],
): Promise {
const { timezone = 'utc', unit = 'day' } = filters;
- const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = prisma;
+ const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = prisma;
const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
...filters,
websiteId,
+ timezone,
});
- const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters);
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone);
return rawQuery(
`
@@ -55,11 +56,11 @@ async function relationalQuery(
and event_data.data_key = {{propertyName}}
and event_data.data_type in (${DATA_TYPE.string}, ${DATA_TYPE.boolean})
${filterQuery}
- ${epfSQL}
+ ${pfSQL}
group by 1, 2
order by 2
`,
- { ...queryParams, eventName, propertyName, ...epfParams },
+ { ...queryParams, eventName, propertyName, ...pfParams },
FUNCTION_NAME,
);
}
@@ -72,9 +73,13 @@ async function clickhouseQuery(
eventFilters: EventPropertyFilter[] = [],
): Promise {
const { timezone = 'UTC', unit = 'day' } = filters;
- const { rawQuery, getDateSQL, parseFilters, getEventPropertyFilterQuery } = clickhouse;
- const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
- const { sql: epfSQL, params: epfParams } = getEventPropertyFilterQuery(eventFilters);
+ const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = clickhouse;
+ const { filterQuery, cohortQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(eventFilters, 'event', timezone);
return rawQuery(
`
@@ -96,14 +101,15 @@ async function clickhouseQuery(
${cohortQuery}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_data.event_name = {eventName:String}
and event_data.data_key = {propertyName:String}
and event_data.data_type in (${DATA_TYPE.string}, ${DATA_TYPE.boolean})
${filterQuery}
- ${epfSQL}
+ ${pfSQL}
group by x, t
order by t
`,
- { ...queryParams, eventName, propertyName, ...epfParams },
+ { ...queryParams, eventName, propertyName, ...pfParams },
FUNCTION_NAME,
);
}
diff --git a/src/queries/sql/events/getEventDataValues.ts b/src/queries/sql/events/getEventDataValues.ts
index f644934e3..70e134e5b 100644
--- a/src/queries/sql/events/getEventDataValues.ts
+++ b/src/queries/sql/events/getEventDataValues.ts
@@ -1,4 +1,5 @@
import clickhouse from '@/lib/clickhouse';
+import { DATA_TYPE } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import type { QueryFilters } from '@/lib/types';
@@ -11,7 +12,11 @@ interface WebsiteEventData {
}
export async function getEventDataValues(
- ...args: [websiteId: string, filters: QueryFilters & { propertyName?: string }]
+ ...args: [
+ websiteId: string,
+ eventName: string,
+ filters: QueryFilters & { propertyName?: string; dataType?: number },
+ ]
): Promise {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -21,20 +26,51 @@ export async function getEventDataValues(
async function relationalQuery(
websiteId: string,
- filters: QueryFilters & { propertyName?: string },
+ eventName: string,
+ filters: QueryFilters & { propertyName?: string; dataType?: number },
) {
const { rawQuery, parseFilters, getDateSQL } = prisma;
+ const { dataType } = filters;
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
+ if (dataType === DATA_TYPE.array) {
+ return rawQuery(
+ `
+ select
+ array_item.value as "value",
+ count(*) as "total"
+ from event_data
+ join website_event on website_event.event_id = event_data.website_event_id
+ and website_event.website_id = {{websiteId::uuid}}
+ and website_event.created_at between {{startDate}} and {{endDate}}
+ and website_event.event_type = 2
+ and website_event.event_name = {{eventName}}
+ cross join lateral jsonb_array_elements_text(coalesce(event_data.string_value, '[]')::jsonb) as array_item(value)
+ ${cohortQuery}
+ ${joinSessionQuery}
+ where event_data.website_id = {{websiteId::uuid}}
+ and event_data.created_at between {{startDate}} and {{endDate}}
+ and event_data.data_key = {{propertyName}}
+ and event_data.data_type = ${DATA_TYPE.array}
+ ${filterQuery}
+ group by array_item.value
+ order by 2 desc
+ limit 100
+ `,
+ { ...queryParams, eventName },
+ FUNCTION_NAME,
+ );
+ }
+
return rawQuery(
`
select
- case
- when data_type = 2 then replace(string_value, '.0000', '')
- when data_type = 4 then ${getDateSQL('date_value', 'hour')}
+ case
+ when data_type = 2 then replace(string_value, '.0000', '')
+ when data_type = 4 then ${getDateSQL('date_value', 'hour')}
else string_value
end as "value",
count(*) as "total"
@@ -42,28 +78,66 @@ async function relationalQuery(
join website_event on website_event.event_id = event_data.website_event_id
and website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
+ and website_event.event_type = 2
+ and website_event.event_name = {{eventName}}
${cohortQuery}
${joinSessionQuery}
where event_data.website_id = {{websiteId::uuid}}
and event_data.created_at between {{startDate}} and {{endDate}}
and event_data.data_key = {{propertyName}}
+ ${dataType ? `and event_data.data_type = ${dataType}` : ''}
${filterQuery}
group by value
order by 2 desc
limit 100
`,
- queryParams,
+ { ...queryParams, eventName },
FUNCTION_NAME,
);
}
async function clickhouseQuery(
websiteId: string,
- filters: QueryFilters & { propertyName?: string },
+ eventName: string,
+ filters: QueryFilters & { propertyName?: string; dataType?: number },
): Promise<{ value: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
+ const { dataType } = filters;
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
+ if (dataType === DATA_TYPE.array) {
+ return rawQuery(
+ `
+ select
+ arrayJoin(JSONExtract(ifNull(event_data.string_value, '[]'), 'Array(String)')) as "value",
+ count(*) as "total"
+ from event_data
+ any left join (
+ select *
+ from website_event
+ where website_id = {websiteId:UUID}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_type = 2
+ and event_name = {eventName:String}) website_event
+ on website_event.event_id = event_data.event_id
+ and website_event.session_id = event_data.session_id
+ and website_event.website_id = event_data.website_id
+ ${cohortQuery}
+ where event_data.website_id = {websiteId:UUID}
+ and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_data.event_name = {eventName:String}
+ and event_data.data_key = {propertyName:String}
+ and event_data.data_type = ${DATA_TYPE.array}
+ ${filterQuery}
+ group by value
+ order by 2 desc
+ limit 100
+ `,
+ { ...queryParams, eventName },
+ FUNCTION_NAME,
+ );
+ }
+
return rawQuery(
`
select
@@ -71,26 +145,29 @@ async function clickhouseQuery(
data_type = 4, toString(date_trunc('hour', date_value)),
string_value) as "value",
count(*) as "total"
- from event_data
+ from event_data
any left join (
- select *
+ select *
from website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
- and event_type = 2) website_event
+ and event_type = 2
+ and event_name = {eventName:String}) website_event
on website_event.event_id = event_data.event_id
and website_event.session_id = event_data.session_id
and website_event.website_id = event_data.website_id
${cohortQuery}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_data.event_name = {eventName:String}
and event_data.data_key = {propertyName:String}
+ ${dataType ? `and event_data.data_type = ${dataType}` : ''}
${filterQuery}
group by value
order by 2 desc
limit 100
`,
- queryParams,
+ { ...queryParams, eventName },
FUNCTION_NAME,
);
}
diff --git a/src/queries/sql/events/getWebsiteEvents.ts b/src/queries/sql/events/getWebsiteEvents.ts
index f11d3ff13..d7a8ecb63 100644
--- a/src/queries/sql/events/getWebsiteEvents.ts
+++ b/src/queries/sql/events/getWebsiteEvents.ts
@@ -1,4 +1,5 @@
import clickhouse from '@/lib/clickhouse';
+import { EVENT_TYPE } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import type { QueryFilters } from '@/lib/types';
@@ -55,6 +56,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
join session on session.session_id = website_event.session_id
and session.website_id = website_event.website_id
where website_event.website_id = {{websiteId::uuid}}
+ and website_event.event_type != ${EVENT_TYPE.performance}
${dateQuery}
${filterQuery}
${searchQuery}
@@ -107,6 +109,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
+ and event_type != ${EVENT_TYPE.performance}
${dateQuery}
${filterQuery}
${searchQuery}
diff --git a/src/queries/sql/events/saveEvent.ts b/src/queries/sql/events/saveEvent.ts
index b668b7ea7..b274a1b6e 100644
--- a/src/queries/sql/events/saveEvent.ts
+++ b/src/queries/sql/events/saveEvent.ts
@@ -1,7 +1,8 @@
import clickhouse from '@/lib/clickhouse';
-import { EVENT_NAME_LENGTH, PAGE_TITLE_LENGTH, URL_LENGTH } from '@/lib/constants';
+import { FIELD_LENGTH } from '@/lib/constants';
import { uuid } from '@/lib/crypto';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import { truncateString } from '@/lib/format';
import kafka from '@/lib/kafka';
import prisma from '@/lib/prisma';
import { saveEventData } from './saveEventData';
@@ -110,27 +111,27 @@ async function relationalQuery({
websiteId,
sessionId,
visitId,
- urlPath: urlPath?.substring(0, URL_LENGTH),
- urlQuery: urlQuery?.substring(0, URL_LENGTH),
- utmSource,
- utmMedium,
- utmCampaign,
- utmContent,
- utmTerm,
- referrerPath: referrerPath?.substring(0, URL_LENGTH),
- referrerQuery: referrerQuery?.substring(0, URL_LENGTH),
- referrerDomain: referrerDomain?.substring(0, URL_LENGTH),
- pageTitle: pageTitle?.substring(0, PAGE_TITLE_LENGTH),
- gclid,
- fbclid,
- msclkid,
- ttclid,
- lifatid,
- twclid,
+ urlPath: truncateString(urlPath, FIELD_LENGTH.url),
+ urlQuery: truncateString(urlQuery, FIELD_LENGTH.url),
+ utmSource: truncateString(utmSource, FIELD_LENGTH.fieldValue),
+ utmMedium: truncateString(utmMedium, FIELD_LENGTH.fieldValue),
+ utmCampaign: truncateString(utmCampaign, FIELD_LENGTH.fieldValue),
+ utmContent: truncateString(utmContent, FIELD_LENGTH.fieldValue),
+ utmTerm: truncateString(utmTerm, FIELD_LENGTH.fieldValue),
+ referrerPath: truncateString(referrerPath, FIELD_LENGTH.url),
+ referrerQuery: truncateString(referrerQuery, FIELD_LENGTH.url),
+ referrerDomain: truncateString(referrerDomain, FIELD_LENGTH.url),
+ pageTitle: truncateString(pageTitle, FIELD_LENGTH.pageTitle),
+ gclid: truncateString(gclid, FIELD_LENGTH.fieldValue),
+ fbclid: truncateString(fbclid, FIELD_LENGTH.fieldValue),
+ msclkid: truncateString(msclkid, FIELD_LENGTH.fieldValue),
+ ttclid: truncateString(ttclid, FIELD_LENGTH.fieldValue),
+ lifatid: truncateString(lifatid, FIELD_LENGTH.fieldValue),
+ twclid: truncateString(twclid, FIELD_LENGTH.fieldValue),
eventType,
- eventName: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null,
- tag,
- hostname,
+ eventName: truncateString(eventName, FIELD_LENGTH.eventName) ?? null,
+ tag: truncateString(tag, FIELD_LENGTH.tag),
+ hostname: truncateString(hostname, FIELD_LENGTH.hostname),
lcp,
inp,
cls,
@@ -145,8 +146,8 @@ async function relationalQuery({
websiteId,
sessionId,
eventId: websiteEventId,
- urlPath: urlPath?.substring(0, URL_LENGTH),
- eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
+ urlPath: truncateString(urlPath, FIELD_LENGTH.url),
+ eventName: truncateString(eventName, FIELD_LENGTH.eventName),
eventData,
createdAt,
});
@@ -158,7 +159,7 @@ async function relationalQuery({
websiteId,
sessionId,
eventId: websiteEventId,
- eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
+ eventName: truncateString(eventName, FIELD_LENGTH.eventName),
currency,
revenue,
createdAt,
@@ -218,37 +219,40 @@ async function clickhouseQuery({
session_id: sessionId,
visit_id: visitId,
event_id: eventId,
- country: country,
- region: country && region ? (region.includes('-') ? region : `${country}-${region}`) : null,
- city: city,
- url_path: urlPath?.substring(0, URL_LENGTH),
- url_query: urlQuery?.substring(0, URL_LENGTH),
- utm_source: utmSource,
- utm_medium: utmMedium,
- utm_campaign: utmCampaign,
- utm_content: utmContent,
- utm_term: utmTerm,
- referrer_path: referrerPath?.substring(0, URL_LENGTH),
- referrer_query: referrerQuery?.substring(0, URL_LENGTH),
- referrer_domain: referrerDomain?.substring(0, URL_LENGTH),
- page_title: pageTitle?.substring(0, PAGE_TITLE_LENGTH),
- gclid: gclid,
- fbclid: fbclid,
- msclkid: msclkid,
- ttclid: ttclid,
- li_fat_id: lifatid,
- twclid: twclid,
+ region: truncateString(
+ country && region ? (region.includes('-') ? region : `${country}-${region}`) : null,
+ FIELD_LENGTH.region,
+ ),
+ city: truncateString(city, FIELD_LENGTH.city),
+ url_path: truncateString(urlPath, FIELD_LENGTH.url),
+ url_query: truncateString(urlQuery, FIELD_LENGTH.url),
+ utm_source: truncateString(utmSource, FIELD_LENGTH.fieldValue),
+ utm_medium: truncateString(utmMedium, FIELD_LENGTH.fieldValue),
+ utm_campaign: truncateString(utmCampaign, FIELD_LENGTH.fieldValue),
+ utm_content: truncateString(utmContent, FIELD_LENGTH.fieldValue),
+ utm_term: truncateString(utmTerm, FIELD_LENGTH.fieldValue),
+ referrer_path: truncateString(referrerPath, FIELD_LENGTH.url),
+ referrer_query: truncateString(referrerQuery, FIELD_LENGTH.url),
+ referrer_domain: truncateString(referrerDomain, FIELD_LENGTH.url),
+ page_title: truncateString(pageTitle, FIELD_LENGTH.pageTitle),
+ gclid: truncateString(gclid, FIELD_LENGTH.fieldValue),
+ fbclid: truncateString(fbclid, FIELD_LENGTH.fieldValue),
+ msclkid: truncateString(msclkid, FIELD_LENGTH.fieldValue),
+ ttclid: truncateString(ttclid, FIELD_LENGTH.fieldValue),
+ li_fat_id: truncateString(lifatid, FIELD_LENGTH.fieldValue),
+ twclid: truncateString(twclid, FIELD_LENGTH.fieldValue),
event_type: eventType,
- event_name: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null,
- tag: tag,
- distinct_id: distinctId,
+ event_name: truncateString(eventName, FIELD_LENGTH.eventName) ?? null,
+ tag: truncateString(tag, FIELD_LENGTH.tag),
+ distinct_id: truncateString(distinctId, FIELD_LENGTH.distinctId),
created_at: getUTCString(createdAt),
- browser: browser,
- os: os,
- device: device,
- screen: screen,
- language: language,
- hostname: hostname,
+ browser: truncateString(browser, FIELD_LENGTH.browser),
+ os: truncateString(os, FIELD_LENGTH.os),
+ device: truncateString(device, FIELD_LENGTH.device),
+ screen: truncateString(screen, FIELD_LENGTH.screen),
+ language: truncateString(language, FIELD_LENGTH.language),
+ hostname: truncateString(hostname, FIELD_LENGTH.hostname),
+ country: truncateString(country, FIELD_LENGTH.country),
lcp: lcp,
inp: inp,
cls: cls,
@@ -267,8 +271,8 @@ async function clickhouseQuery({
websiteId,
sessionId,
eventId,
- urlPath: urlPath?.substring(0, URL_LENGTH),
- eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
+ urlPath: truncateString(urlPath, FIELD_LENGTH.url),
+ eventName: truncateString(eventName, FIELD_LENGTH.eventName),
eventData,
createdAt,
});
diff --git a/src/queries/sql/events/saveEventData.ts b/src/queries/sql/events/saveEventData.ts
index b8b0e02fc..589e8ce6a 100644
--- a/src/queries/sql/events/saveEventData.ts
+++ b/src/queries/sql/events/saveEventData.ts
@@ -1,8 +1,9 @@
import clickhouse from '@/lib/clickhouse';
-import { DATA_TYPE } from '@/lib/constants';
+import { DATA_TYPE, FIELD_LENGTH } from '@/lib/constants';
import { uuid } from '@/lib/crypto';
-import { flattenJSON, getStringValue } from '@/lib/data';
+import { flattenJSON, getStoredStringValue } from '@/lib/data';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import { truncateString } from '@/lib/format';
import kafka from '@/lib/kafka';
import prisma from '@/lib/prisma';
import type { DynamicData } from '@/lib/types';
@@ -34,8 +35,8 @@ async function relationalQuery(data: SaveEventDataArgs) {
id: uuid(),
websiteEventId: eventId,
websiteId,
- dataKey: a.key,
- stringValue: getStringValue(a.value, a.dataType),
+ dataKey: truncateString(a.key, FIELD_LENGTH.dataKey),
+ stringValue: getStoredStringValue(a.value, a.dataType),
numberValue: a.dataType === DATA_TYPE.number ? a.value : null,
dateValue: a.dataType === DATA_TYPE.date ? new Date(a.value) : null,
dataType: a.dataType,
@@ -60,11 +61,11 @@ async function clickhouseQuery(data: SaveEventDataArgs) {
website_id: websiteId,
session_id: sessionId,
event_id: eventId,
- url_path: urlPath,
- event_name: eventName,
- data_key: key,
+ url_path: truncateString(urlPath, FIELD_LENGTH.url),
+ event_name: truncateString(eventName, FIELD_LENGTH.eventName),
+ data_key: truncateString(key, FIELD_LENGTH.dataKey),
data_type: dataType,
- string_value: getStringValue(value, dataType),
+ string_value: getStoredStringValue(value, dataType),
number_value: dataType === DATA_TYPE.number ? value : null,
date_value: dataType === DATA_TYPE.date ? getUTCString(value) : null,
created_at: getUTCString(createdAt),
diff --git a/src/queries/sql/events/saveRevenue.ts b/src/queries/sql/events/saveRevenue.ts
index a38df83a0..ae6e75ad4 100644
--- a/src/queries/sql/events/saveRevenue.ts
+++ b/src/queries/sql/events/saveRevenue.ts
@@ -1,5 +1,7 @@
+import { FIELD_LENGTH } from '@/lib/constants';
import { uuid } from '@/lib/crypto';
import { PRISMA, runQuery } from '@/lib/db';
+import { truncateString } from '@/lib/format';
import prisma from '@/lib/prisma';
export interface SaveRevenueArgs {
@@ -27,8 +29,8 @@ async function relationalQuery(data: SaveRevenueArgs) {
websiteId,
sessionId,
eventId,
- eventName,
- currency,
+ eventName: truncateString(eventName, FIELD_LENGTH.eventName),
+ currency: truncateString(currency, FIELD_LENGTH.currency),
revenue,
createdAt,
},
diff --git a/src/queries/sql/getChannelExpandedMetrics.ts b/src/queries/sql/getChannelExpandedMetrics.ts
index c43e2544b..71985a831 100644
--- a/src/queries/sql/getChannelExpandedMetrics.ts
+++ b/src/queries/sql/getChannelExpandedMetrics.ts
@@ -1,6 +1,7 @@
import clickhouse from '@/lib/clickhouse';
import {
EMAIL_DOMAINS,
+ LLM_DOMAINS,
PAID_AD_PARAMS,
SEARCH_DOMAINS,
SHOPPING_DOMAINS,
@@ -61,9 +62,8 @@ async function relationalQuery(
website_event.session_id,
website_event.visit_id,
website_event.hostname,
- count(*) c,
- min(website_event.created_at) min_time,
- max(website_event.created_at) max_time
+ website_event.event_id,
+ website_event.created_at
from website_event
${cohortQuery}
${excludeBounceQuery}
@@ -71,15 +71,7 @@ async function relationalQuery(
where website_event.website_id = {{websiteId::uuid}}
and website_event.event_type NOT IN (2, 5)
${dateQuery}
- ${filterQuery}
- group by prefix,
- website_event.referrer_domain,
- website_event.url_query,
- website_event.utm_medium,
- website_event.utm_source,
- website_event.session_id,
- website_event.visit_id,
- website_event.hostname),
+ ${filterQuery}),
channels as (
select case
@@ -88,6 +80,7 @@ async function relationalQuery(
when ${toPostgresPositionClause('utm_medium', ['referral', 'app', 'link'])} then 'referral'
when utm_medium ilike '%affiliate%' then 'affiliate'
when utm_medium ilike '%sms%' or utm_source ilike '%sms%' then 'sms'
+ when ${toPostgresPositionClause('referrer_domain', LLM_DOMAINS)} then 'llm'
when ${toPostgresPositionClause('referrer_domain', SEARCH_DOMAINS)} or utm_medium ilike '%organic%' then concat(prefix, 'Search')
when ${toPostgresPositionClause('referrer_domain', SOCIAL_DOMAINS)} then concat(prefix, 'Social')
when ${toPostgresPositionClause('referrer_domain', EMAIL_DOMAINS)} or utm_medium ilike '%mail%' then 'email'
@@ -97,21 +90,50 @@ async function relationalQuery(
else '' end as "name",
session_id,
visit_id,
- c,
- min_time,
- max_time
- from prefix)
+ event_id,
+ created_at
+ from prefix),
+
+ visit_channels as (
+ select
+ session_id,
+ visit_id,
+ coalesce(nullif(name, ''), 'direct') as "name"
+ from (
+ select
+ name,
+ session_id,
+ visit_id,
+ row_number() over (
+ partition by session_id, visit_id
+ order by case when name != '' then 0 else 1 end, created_at, event_id
+ ) as row_num
+ from channels
+ ) as ranked_channels
+ where row_num = 1),
+
+ visit_stats as (
+ select
+ session_id,
+ visit_id,
+ count(*) as c,
+ min(created_at) as min_time,
+ max(created_at) as max_time
+ from prefix
+ group by session_id, visit_id)
select
- name,
- sum(c) as "pageviews",
- count(distinct session_id) as "visitors",
- count(distinct visit_id) as "visits",
- sum(case when c = 1 then 1 else 0 end) as "bounces",
- sum(${getTimestampDiffSQL('min_time', 'max_time')}) as "totaltime"
- from channels
- where name != ''
- group by name
+ visit_channels.name,
+ sum(visit_stats.c) as "pageviews",
+ count(distinct visit_stats.session_id) as "visitors",
+ count(distinct visit_stats.visit_id) as "visits",
+ sum(case when visit_stats.c = 1 then 1 else 0 end) as "bounces",
+ sum(${getTimestampDiffSQL('visit_stats.min_time', 'visit_stats.max_time')}) as "totaltime"
+ from visit_stats
+ join visit_channels
+ on visit_channels.session_id = visit_stats.session_id
+ and visit_channels.visit_id = visit_stats.visit_id
+ group by visit_channels.name
order by visitors desc, visits desc
`,
queryParams,
@@ -140,6 +162,14 @@ async function clickhouseQuery(
sum(max_time-min_time) as "totaltime"
from (
select
+ session_id,
+ visit_id,
+ coalesce(nullIf(argMin(name, tuple(if(name != '', 0, 1), created_at, event_id)), ''), 'direct') as name,
+ count(*) c,
+ min(created_at) min_time,
+ max(created_at) max_time
+ from (
+ select
case when multiSearchAny(lower(utm_medium), ['cp', 'ppc', 'retargeting', 'paid']) != 0 then 'paid' else 'organic' end prefix,
case
when referrer_domain = '' and url_query = '' then 'direct'
@@ -149,6 +179,9 @@ async function clickhouseQuery(
when multiSearchAny(lower(utm_medium), ['referral', 'app','link']) != 0 then 'referral'
when position(lower(utm_medium), 'affiliate') > 0 then 'affiliate'
when position(lower(utm_medium), 'sms') > 0 or position(lower(utm_source), 'sms') > 0 then 'sms'
+ when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
+ LLM_DOMAINS,
+ )}]) != 0 then 'llm'
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
SEARCH_DOMAINS,
)}]) != 0 or position(lower(utm_medium), 'organic') > 0 then concat(prefix, 'Search')
@@ -168,18 +201,17 @@ async function clickhouseQuery(
else '' end AS "name",
session_id,
visit_id,
- count(*) c,
- min(created_at) min_time,
- max(created_at) max_time
- from website_event
- ${cohortQuery}
- ${excludeBounceQuery}
- where website_id = {websiteId:UUID}
- and created_at between {startDate:DateTime64} and {endDate:DateTime64}
- and event_type NOT IN (2, 5)
- and name != ''
- ${filterQuery}
- group by prefix, name, session_id, visit_id
+ event_id,
+ created_at
+ from website_event
+ ${cohortQuery}
+ ${excludeBounceQuery}
+ where website_id = {websiteId:UUID}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_type NOT IN (2, 5)
+ ${filterQuery}
+ )
+ group by session_id, visit_id
) as t
group by name
order by visitors desc, visits desc;
diff --git a/src/queries/sql/getChannelMetrics.ts b/src/queries/sql/getChannelMetrics.ts
index da28c0492..98e2274a7 100644
--- a/src/queries/sql/getChannelMetrics.ts
+++ b/src/queries/sql/getChannelMetrics.ts
@@ -1,6 +1,7 @@
import clickhouse from '@/lib/clickhouse';
import {
EMAIL_DOMAINS,
+ LLM_DOMAINS,
PAID_AD_PARAMS,
SEARCH_DOMAINS,
SHOPPING_DOMAINS,
@@ -40,6 +41,9 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
website_event.utm_medium,
website_event.utm_source,
website_event.session_id,
+ website_event.visit_id,
+ website_event.event_id,
+ website_event.created_at,
website_event.hostname
from website_event
${cohortQuery}
@@ -57,6 +61,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
when ${toPostgresLikeClause('utm_medium', ['referral', 'app', 'link'])} then 'referral'
when utm_medium ilike '%affiliate%' then 'affiliate'
when utm_medium ilike '%sms%' or utm_source ilike '%sms%' then 'sms'
+ when ${toPostgresLikeClause('referrer_domain', LLM_DOMAINS)} then 'llm'
when ${toPostgresLikeClause('referrer_domain', SEARCH_DOMAINS)} or utm_medium ilike '%organic%' then concat(prefix, 'Search')
when ${toPostgresLikeClause('referrer_domain', SOCIAL_DOMAINS)} then concat(prefix, 'Social')
when ${toPostgresLikeClause('referrer_domain', EMAIL_DOMAINS)} or utm_medium ilike '%mail%' then 'email'
@@ -64,14 +69,32 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
when ${toPostgresLikeClause('referrer_domain', VIDEO_DOMAINS)} or utm_medium ilike '%video%' then concat(prefix, 'Video')
when referrer_domain != regexp_replace(hostname, '^www.', '') and referrer_domain != '' then 'referral'
else '' end AS x,
- count(distinct session_id) y
- from prefix
- group by 1
- order by y desc)
+ session_id,
+ visit_id,
+ event_id,
+ created_at
+ from prefix),
- select x, sum(y) y
- from channels
- where x != ''
+ visit_channels as (
+ select
+ session_id,
+ visit_id,
+ coalesce(nullif(x, ''), 'direct') as x
+ from (
+ select
+ x,
+ session_id,
+ visit_id,
+ row_number() over (
+ partition by session_id, visit_id
+ order by case when x != '' then 0 else 1 end, created_at, event_id
+ ) as row_num
+ from channels
+ ) as ranked_channels
+ where row_num = 1)
+
+ select x, count(distinct session_id) y
+ from visit_channels
group by x
order by y desc;
`,
@@ -92,6 +115,11 @@ async function clickhouseQuery(
const sql = `
WITH channels as (
+ select
+ session_id,
+ visit_id,
+ coalesce(nullIf(argMin(x, tuple(if(x != '', 0, 1), created_at, event_id)), ''), 'direct') as x
+ from (
select
case when multiSearchAny(lower(utm_medium), ['cp', 'ppc', 'retargeting', 'paid']) != 0 then 'paid' else 'organic' end prefix,
case
@@ -102,6 +130,9 @@ async function clickhouseQuery(
when multiSearchAny(lower(utm_medium), ['referral', 'app','link']) != 0 then 'referral'
when position(lower(utm_medium), 'affiliate') > 0 then 'affiliate'
when position(lower(utm_medium), 'sms') > 0 or position(lower(utm_source), 'sms') > 0 then 'sms'
+ when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
+ LLM_DOMAINS,
+ )}]) != 0 then 'llm'
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
SEARCH_DOMAINS,
)}]) != 0 or position(lower(utm_medium), 'organic') > 0 then concat(prefix, 'Search')
@@ -119,7 +150,10 @@ async function clickhouseQuery(
)}]) != 0 or position(lower(utm_medium), 'video') > 0 then concat(prefix, 'Video')
when referrer_domain != hostname and referrer_domain != '' then 'referral'
else '' end AS x,
- count(distinct session_id) y
+ session_id,
+ visit_id,
+ event_id,
+ created_at
from website_event
${cohortQuery}
${excludeBounceQuery}
@@ -127,12 +161,11 @@ async function clickhouseQuery(
and event_type NOT IN (2, 5)
${dateQuery}
${filterQuery}
- group by 1, 2
- order by y desc)
+ )
+ group by session_id, visit_id)
- select x, sum(y) y
+ select x, uniq(session_id) y
from channels
- where x != ''
group by x
order by y desc;
`;
diff --git a/src/queries/sql/getRealtimeActivity.ts b/src/queries/sql/getRealtimeActivity.ts
index c847b6f7d..f328d6488 100644
--- a/src/queries/sql/getRealtimeActivity.ts
+++ b/src/queries/sql/getRealtimeActivity.ts
@@ -1,4 +1,5 @@
import clickhouse from '@/lib/clickhouse';
+import { EVENT_TYPE } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import type { QueryFilters } from '@/lib/types';
@@ -38,6 +39,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
on session.session_id = website_event.session_id
and session.website_id = website_event.website_id
where website_event.website_id = {{websiteId::uuid}}
+ and website_event.event_type != ${EVENT_TYPE.performance}
${filterQuery}
${dateQuery}
order by website_event.created_at desc
@@ -71,6 +73,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promis
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
+ and event_type != ${EVENT_TYPE.performance}
${filterQuery}
${dateQuery}
order by createdAt desc
diff --git a/src/queries/sql/getValues.ts b/src/queries/sql/getValues.ts
index 908be4126..bd462dc0d 100644
--- a/src/queries/sql/getValues.ts
+++ b/src/queries/sql/getValues.ts
@@ -96,10 +96,6 @@ async function clickhouseQuery(websiteId: string, column: string, filters: Query
excludeDomain = `and referrer_domain != hostname and referrer_domain != ''`;
}
- if (search) {
- searchQuery = `and positionCaseInsensitive(${column}, {search:String}) > 0`;
- }
-
if (search) {
if (decodeURIComponent(search).includes(',')) {
searchQuery = `AND (${decodeURIComponent(search)
diff --git a/src/queries/sql/getWebsiteListCharts.ts b/src/queries/sql/getWebsiteListCharts.ts
new file mode 100644
index 000000000..ac7a424a6
--- /dev/null
+++ b/src/queries/sql/getWebsiteListCharts.ts
@@ -0,0 +1,200 @@
+import { addHours } from 'date-fns';
+import { formatInTimeZone } from 'date-fns-tz';
+import clickhouse from '@/lib/clickhouse';
+import { EVENT_TYPE } from '@/lib/constants';
+import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import prisma from '@/lib/prisma';
+
+const FUNCTION_NAME = 'getWebsiteListCharts';
+const DEFAULT_TIMEZONE = 'UTC';
+const BUCKET_HOURS = 12;
+
+interface WebsiteListChartPoint {
+ websiteId: string;
+ x: string | null;
+ y: number;
+}
+
+export interface WebsiteListChartData {
+ values: number[];
+ total: number;
+}
+
+export async function getWebsiteListCharts(
+ websiteIds: string[],
+ {
+ startDate,
+ endDate,
+ timezone = DEFAULT_TIMEZONE,
+ eventType,
+ }: {
+ startDate: Date;
+ endDate: Date;
+ timezone?: string;
+ eventType?: number;
+ },
+): Promise> {
+ if (!websiteIds.length) {
+ return {};
+ }
+
+ return runQuery({
+ [PRISMA]: async () => {
+ const points = await relationalQuery(websiteIds, startDate, endDate, timezone, eventType);
+
+ return formatResults({ points, websiteIds, startDate, endDate, timezone });
+ },
+ [CLICKHOUSE]: async () => {
+ const points = await clickhouseQuery(websiteIds, startDate, endDate, timezone, eventType);
+
+ return formatResults({ points, websiteIds, startDate, endDate, timezone });
+ },
+ });
+}
+
+async function relationalQuery(
+ websiteIds: string[],
+ startDate: Date,
+ endDate: Date,
+ timezone: string,
+ eventType?: number,
+): Promise {
+ const { rawQuery } = prisma;
+ const eventTypeQuery =
+ eventType != null
+ ? `and website_event.event_type = ${eventType}`
+ : `and website_event.event_type NOT IN (${EVENT_TYPE.customEvent}, ${EVENT_TYPE.performance})`;
+ const bucketSql =
+ timezone.toLowerCase() === 'utc'
+ ? `
+ to_char(
+ date_trunc('day', website_event.created_at)
+ + floor(extract(hour from website_event.created_at) / ${BUCKET_HOURS}) * interval '${BUCKET_HOURS} hour',
+ 'YYYY-MM-DD HH24:00:00'
+ )
+ `
+ : `
+ to_char(
+ date_trunc('day', website_event.created_at at time zone '${timezone}')
+ + floor(extract(hour from website_event.created_at at time zone '${timezone}') / ${BUCKET_HOURS}) * interval '${BUCKET_HOURS} hour',
+ 'YYYY-MM-DD HH24:00:00'
+ )
+ `;
+
+ return rawQuery(
+ `
+ select
+ "websiteId",
+ x,
+ count(distinct session_id) as y
+ from (
+ select
+ website_event.website_id as "websiteId",
+ ${bucketSql} as x,
+ website_event.session_id
+ from website_event
+ where website_event.website_id = any({{websiteIds}}::uuid[])
+ ${eventTypeQuery}
+ and website_event.created_at between {{startDate}} and {{endDate}}
+ ) as events
+ group by grouping sets (("websiteId", x), ("websiteId"))
+ order by 1, 2
+ `,
+ { websiteIds, startDate, endDate },
+ FUNCTION_NAME,
+ );
+}
+
+async function clickhouseQuery(
+ websiteIds: string[],
+ startDate: Date,
+ endDate: Date,
+ timezone: string,
+ eventType?: number,
+): Promise {
+ const { rawQuery } = clickhouse;
+ const eventTypeQuery =
+ eventType != null
+ ? `and event_type = ${eventType}`
+ : `and event_type NOT IN (${EVENT_TYPE.customEvent}, ${EVENT_TYPE.performance})`;
+ const localTime = `toTimeZone(website_event.created_at, '${timezone}')`;
+ const bucketSql = `
+ formatDateTime(
+ toStartOfDay(${localTime}) + toIntervalHour(intDiv(toHour(${localTime}), ${BUCKET_HOURS}) * ${BUCKET_HOURS}),
+ '%Y-%m-%d %H:00:00'
+ )
+ `;
+
+ return rawQuery(
+ `
+ select
+ website_id as websiteId,
+ ${bucketSql} as x,
+ uniq(session_id) as y
+ from website_event_stats_hourly as website_event
+ where website_id in {websiteIds:Array(UUID)}
+ ${eventTypeQuery}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ group by grouping sets ((website_id, x), (website_id))
+ order by websiteId, x
+ `,
+ { websiteIds, startDate, endDate },
+ FUNCTION_NAME,
+ );
+}
+
+function formatResults(
+ {
+ points,
+ websiteIds,
+ startDate,
+ endDate,
+ timezone,
+ }: {
+ points: WebsiteListChartPoint[];
+ websiteIds: string[];
+ startDate: Date;
+ endDate: Date;
+ timezone: string;
+ },
+) {
+ const buckets: string[] = [];
+
+ for (
+ let current = new Date(startDate);
+ current <= endDate;
+ current = addHours(current, BUCKET_HOURS)
+ ) {
+ buckets.push(formatInTimeZone(current, timezone, 'yyyy-MM-dd HH:00:00'));
+ }
+
+ const bucketIndex = new Map(buckets.map((bucket, index) => [bucket, index]));
+ const charts = websiteIds.reduce>((result, websiteId) => {
+ result[websiteId] = {
+ values: Array.from({ length: buckets.length }, () => 0),
+ total: 0,
+ };
+ return result;
+ }, {});
+
+ points.forEach(({ websiteId, x, y }) => {
+ if (!charts[websiteId]) {
+ return;
+ }
+
+ // Subtotal rows from GROUPING SETS (null x on Postgres, '' on ClickHouse)
+ // carry the window-wide distinct total per website
+ if (!x) {
+ charts[websiteId].total = Number(y);
+ return;
+ }
+
+ const index = bucketIndex.get(String(x).slice(0, 19));
+
+ if (index !== undefined) {
+ charts[websiteId].values[index] = Number(y);
+ }
+ });
+
+ return charts;
+}
diff --git a/src/queries/sql/heatmap/extractHeatmapEvents.ts b/src/queries/sql/heatmap/extractHeatmapEvents.ts
new file mode 100644
index 000000000..41e0aa2da
--- /dev/null
+++ b/src/queries/sql/heatmap/extractHeatmapEvents.ts
@@ -0,0 +1,115 @@
+import { HEATMAP_EVENT_TYPE } from '@/lib/constants';
+
+const RRWEB_TYPE_INCREMENTAL = 3;
+const RRWEB_TYPE_META = 4;
+const RRWEB_TYPE_CUSTOM = 5;
+const RRWEB_SOURCE_MOUSE_INTERACTION = 2;
+const RRWEB_SOURCE_VIEWPORT_RESIZE = 4;
+const RRWEB_MOUSE_CLICK = 2;
+
+export interface ExtractedHeatmapEvent {
+ eventType: number;
+ x: number | null;
+ y: number | null;
+ viewportW: number | null;
+ viewportH: number | null;
+ pageH: number | null;
+ scrollPct: number | null;
+ urlPath: string;
+ createdAt: Date;
+}
+
+interface ExtractHeatmapEventOptions {
+ chunkIndex?: number;
+}
+
+function safePathname(href: unknown): string | null {
+ if (typeof href !== 'string') return null;
+ try {
+ return new URL(href).pathname || '/';
+ } catch {
+ return href.startsWith('/') ? href.split(/[?#]/)[0] : null;
+ }
+}
+
+export function extractHeatmapEvents(events: any[], _options: ExtractHeatmapEventOptions = {}) {
+ if (!Array.isArray(events) || events.length === 0) return [];
+
+ let urlPath: string | null = null;
+ let viewportW: number | null = null;
+ let viewportH: number | null = null;
+ const out: ExtractedHeatmapEvent[] = [];
+
+ for (const ev of events) {
+ if (!ev || typeof ev !== 'object') continue;
+ const replayTimeMs =
+ typeof ev.timestamp === 'number' && Number.isFinite(ev.timestamp) ? ev.timestamp : null;
+
+ if (ev.type === RRWEB_TYPE_META && ev.data) {
+ const path = safePathname(ev.data.href);
+ if (path) urlPath = path;
+ if (typeof ev.data.width === 'number') viewportW = ev.data.width;
+ if (typeof ev.data.height === 'number') viewportH = ev.data.height;
+ continue;
+ }
+
+ if (ev.type === RRWEB_TYPE_CUSTOM && ev.data) {
+ if (ev.data.tag === 'url-change') {
+ const path = safePathname(ev.data.payload?.url);
+ if (path) urlPath = path;
+ continue;
+ }
+
+ if (ev.data.tag === 'scroll-progress' && ev.data.payload) {
+ const p = ev.data.payload;
+ const path = safePathname(p.url) ?? urlPath;
+ if (path === null) continue;
+ out.push({
+ eventType: HEATMAP_EVENT_TYPE.scroll,
+ x: null,
+ y: null,
+ viewportW: typeof p.viewportW === 'number' ? p.viewportW : viewportW,
+ viewportH: typeof p.viewportH === 'number' ? p.viewportH : viewportH,
+ pageH: typeof p.pageH === 'number' ? p.pageH : null,
+ scrollPct:
+ typeof p.scrollPct === 'number'
+ ? Math.max(0, Math.min(100, Math.round(p.scrollPct)))
+ : null,
+ urlPath: path,
+ createdAt: new Date(replayTimeMs ?? Date.now()),
+ });
+ }
+ continue;
+ }
+
+ if (ev.type !== RRWEB_TYPE_INCREMENTAL || !ev.data) continue;
+
+ const d = ev.data;
+
+ if (d.source === RRWEB_SOURCE_VIEWPORT_RESIZE) {
+ if (typeof d.width === 'number') viewportW = d.width;
+ if (typeof d.height === 'number') viewportH = d.height;
+ continue;
+ }
+
+ if (
+ d.source === RRWEB_SOURCE_MOUSE_INTERACTION &&
+ d.type === RRWEB_MOUSE_CLICK &&
+ urlPath !== null
+ ) {
+ out.push({
+ eventType: HEATMAP_EVENT_TYPE.click,
+ x: typeof d.x === 'number' ? Math.round(d.x) : null,
+ y: typeof d.y === 'number' ? Math.round(d.y) : null,
+ viewportW,
+ viewportH,
+ pageH: null,
+ scrollPct: null,
+ urlPath,
+ createdAt: new Date(replayTimeMs ?? Date.now()),
+ });
+ }
+ }
+
+ return out;
+}
diff --git a/src/queries/sql/heatmap/getHeatmap.ts b/src/queries/sql/heatmap/getHeatmap.ts
new file mode 100644
index 000000000..2872b7c57
--- /dev/null
+++ b/src/queries/sql/heatmap/getHeatmap.ts
@@ -0,0 +1,947 @@
+import clickhouse from '@/lib/clickhouse';
+import { HEATMAP_EVENT_TYPE, OPERATORS } from '@/lib/constants';
+import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import { filtersObjectToArray } from '@/lib/params';
+import prisma from '@/lib/prisma';
+import type { QueryFilters } from '@/lib/types';
+import { getWebsite } from '@/queries/prisma';
+
+const FUNCTION_NAME = 'getHeatmap';
+
+const POINT_LIMIT = 5000;
+const PAGE_LIMIT = 100;
+const SCROLL_BUCKET_SIZE = 10;
+
+export type HeatmapMode = 'click' | 'scroll';
+
+export interface HeatmapParameters extends QueryFilters {
+ urlPath?: string;
+ mode?: HeatmapMode;
+}
+
+export interface HeatmapPage {
+ urlPath: string;
+ count: number;
+ sessions: number;
+}
+
+export interface HeatmapPoint {
+ x: number;
+ y: number;
+ pageX: number;
+ pageY: number;
+ pageW: number;
+ pageH: number;
+ viewportW: number;
+ viewportH: number;
+ count: number;
+}
+
+export interface HeatmapScrollBucket {
+ depth: number;
+ sessions: number;
+ pageW: number;
+ pageH: number;
+ viewportW: number;
+ viewportH: number;
+}
+
+export interface HeatmapSnapshotIframe {
+ kind: 'iframe';
+ id: string;
+ url: string;
+ pageW: number;
+ pageH: number;
+ viewportW: number;
+ viewportH: number;
+}
+
+export type HeatmapSnapshot = HeatmapSnapshotIframe;
+
+export interface HeatmapResult {
+ mode: HeatmapMode;
+ pages: HeatmapPage[];
+ points: HeatmapPoint[];
+ snapshot: HeatmapSnapshot | null;
+ scroll: {
+ buckets: HeatmapScrollBucket[];
+ totalSessions: number;
+ pageW: number | null;
+ pageH: number | null;
+ viewportW: number | null;
+ viewportH: number | null;
+ };
+}
+
+export async function getHeatmap(
+ websiteId: string,
+ parameters: HeatmapParameters,
+): Promise {
+ return runQuery({
+ [PRISMA]: () => relationalQuery(websiteId, parameters),
+ [CLICKHOUSE]: () => clickhouseQuery(websiteId, parameters),
+ });
+}
+
+interface HeatmapFilterContext {
+ joinQuery: string;
+ filterQuery: string;
+ queryParams: Record;
+}
+
+async function relationalQuery(
+ websiteId: string,
+ parameters: HeatmapParameters,
+): Promise {
+ const { rawQuery } = prisma;
+ const { startDate, endDate, urlPath, mode = 'click' } = parameters;
+ const eventType = mode === 'scroll' ? HEATMAP_EVENT_TYPE.scroll : HEATMAP_EVENT_TYPE.click;
+ const filterContext = getRelationalHeatmapFilterContext(websiteId, parameters);
+ const pageFilter =
+ mode === 'click'
+ ? `
+ and x is not null
+ and y is not null
+ and page_x is not null
+ and page_y is not null
+ and page_w is not null
+ and page_h is not null
+ and viewport_w is not null
+ and viewport_h is not null
+ `
+ : `
+ and scroll_pct is not null
+ and page_w is not null
+ and page_h is not null
+ and viewport_w is not null
+ and viewport_h is not null
+ `;
+
+ const rawPages: HeatmapPage[] = await rawQuery(
+ `
+ select
+ h.url_path as "urlPath",
+ count(*)::int as count,
+ count(distinct h.visit_id)::int as sessions
+ from heatmap_event h
+ ${filterContext.joinQuery}
+ where h.website_id = {{websiteId::uuid}}
+ and h.event_type = {{eventType}}
+ and h.created_at between {{startDate}} and {{endDate}}
+ ${filterContext.filterQuery}
+ ${pageFilter}
+ group by h.url_path
+ order by sessions desc, count desc
+ limit ${PAGE_LIMIT}
+ `,
+ { ...filterContext.queryParams, websiteId, eventType, startDate, endDate },
+ FUNCTION_NAME,
+ );
+ const pages = rawPages;
+
+ if (!urlPath) {
+ return { mode, pages, points: [], snapshot: null, scroll: emptyScroll() };
+ }
+
+ if (mode === 'scroll') {
+ const bucketRows: {
+ depth: number | string;
+ sessions: number | string;
+ pageW: number | string;
+ pageH: number | string;
+ viewportW: number | string;
+ viewportH: number | string;
+ }[] = await rawQuery(
+ `
+ select
+ (floor(max_pct / ${SCROLL_BUCKET_SIZE}) * ${SCROLL_BUCKET_SIZE})::int as depth,
+ count(*)::int as sessions,
+ page_w::int as "pageW",
+ page_h::int as "pageH",
+ viewport_w::int as "viewportW",
+ viewport_h::int as "viewportH"
+ from (
+ select
+ h.visit_id,
+ max(h.scroll_pct) as max_pct,
+ (mode() within group (order by h.page_w))::int as page_w,
+ (mode() within group (order by h.page_h))::int as page_h,
+ (mode() within group (order by h.viewport_w))::int as viewport_w,
+ (mode() within group (order by h.viewport_h))::int as viewport_h
+ from heatmap_event h
+ ${filterContext.joinQuery}
+ where h.website_id = {{websiteId::uuid}}
+ and h.event_type = {{eventType}}
+ and h.url_path = {{urlPath}}
+ and h.created_at between {{startDate}} and {{endDate}}
+ ${filterContext.filterQuery}
+ and h.scroll_pct is not null
+ and h.page_w is not null
+ and h.page_h is not null
+ and h.viewport_w is not null
+ and h.viewport_h is not null
+ group by h.visit_id
+ ) per_session
+ group by depth, page_w, page_h, viewport_w, viewport_h
+ order by depth
+ `,
+ { ...filterContext.queryParams, websiteId, eventType, urlPath, startDate, endDate },
+ FUNCTION_NAME,
+ );
+
+ const scrollBuckets = bucketRows.map(r => ({
+ depth: Number(r.depth),
+ sessions: Number(r.sessions),
+ pageW: Number(r.pageW),
+ pageH: Number(r.pageH),
+ viewportW: Number(r.viewportW),
+ viewportH: Number(r.viewportH),
+ }));
+ const viewport = pickScrollSnapshotViewport(scrollBuckets);
+ const scroll = {
+ buckets: scrollBuckets,
+ totalSessions: scrollBuckets.reduce((sum, bucket) => sum + bucket.sessions, 0),
+ pageW: viewport?.pageW ?? null,
+ pageH: viewport?.pageH ?? null,
+ viewportW: viewport?.width ?? null,
+ viewportH: viewport?.height ?? null,
+ };
+ const snapshot = await resolveHeatmapSnapshot({
+ websiteId,
+ urlPath,
+ viewportW: viewport?.width ?? null,
+ viewportH: viewport?.height ?? null,
+ pageW: viewport?.pageW ?? null,
+ pageH: viewport?.pageH ?? null,
+ });
+
+ return {
+ mode,
+ pages,
+ points: [],
+ snapshot,
+ scroll,
+ };
+ }
+
+ const rawPoints: HeatmapPoint[] = await rawQuery(
+ `
+ select
+ h.x,
+ h.y,
+ h.page_x as "pageX",
+ h.page_y as "pageY",
+ h.page_w as "pageW",
+ h.page_h as "pageH",
+ h.viewport_w as "viewportW",
+ h.viewport_h as "viewportH",
+ count(*)::int as count
+ from heatmap_event h
+ ${filterContext.joinQuery}
+ where h.website_id = {{websiteId::uuid}}
+ and h.event_type = {{eventType}}
+ and h.url_path = {{urlPath}}
+ and h.created_at between {{startDate}} and {{endDate}}
+ ${filterContext.filterQuery}
+ and h.x is not null
+ and h.y is not null
+ and h.page_x is not null
+ and h.page_y is not null
+ and h.page_w is not null
+ and h.page_h is not null
+ and h.viewport_w is not null
+ and h.viewport_h is not null
+ group by
+ h.x,
+ h.y,
+ h.page_x,
+ h.page_y,
+ h.page_w,
+ h.page_h,
+ h.viewport_w,
+ h.viewport_h
+ order by count desc
+ limit ${POINT_LIMIT}
+ `,
+ { ...filterContext.queryParams, websiteId, eventType, urlPath, startDate, endDate },
+ FUNCTION_NAME,
+ );
+
+ const viewport = pickSnapshotViewport(rawPoints);
+ const snapshot = await resolveHeatmapSnapshot({
+ websiteId,
+ urlPath,
+ viewportW: viewport?.width ?? null,
+ viewportH: viewport?.height ?? null,
+ pageW: viewport?.pageW ?? null,
+ pageH: viewport?.pageH ?? null,
+ });
+
+ return { mode, pages, points: rawPoints, snapshot, scroll: emptyScroll() };
+}
+
+async function clickhouseQuery(
+ websiteId: string,
+ parameters: HeatmapParameters,
+): Promise {
+ const { rawQuery } = clickhouse;
+ const { startDate, endDate, urlPath, mode = 'click' } = parameters;
+ const eventType = mode === 'scroll' ? HEATMAP_EVENT_TYPE.scroll : HEATMAP_EVENT_TYPE.click;
+ const filterContext = getClickhouseHeatmapFilterContext(websiteId, parameters);
+ const pageFilter =
+ mode === 'click'
+ ? `
+ and x is not null
+ and y is not null
+ and page_x is not null
+ and page_y is not null
+ and page_w is not null
+ and page_h is not null
+ and viewport_w is not null
+ and viewport_h is not null
+ `
+ : `
+ and scroll_pct is not null
+ and page_w is not null
+ and page_h is not null
+ and viewport_w is not null
+ and viewport_h is not null
+ `;
+
+ const pageRows = await rawQuery<
+ { urlPath: string; count: string | number; sessions: string | number }[]
+ >(
+ `
+ select
+ h.url_path as urlPath,
+ count() as count,
+ uniq(h.visit_id) as sessions
+ from heatmap_event h
+ ${filterContext.joinQuery}
+ where h.website_id = {websiteId:UUID}
+ and h.event_type = {eventType:UInt8}
+ and h.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ ${filterContext.filterQuery}
+ ${pageFilter}
+ group by h.url_path
+ order by sessions desc, count desc
+ limit ${PAGE_LIMIT}
+ `,
+ { ...filterContext.queryParams, websiteId, eventType, startDate, endDate },
+ FUNCTION_NAME,
+ );
+
+ const pages: HeatmapPage[] = pageRows
+ .map(p => ({
+ urlPath: p.urlPath,
+ count: Number(p.count),
+ sessions: Number(p.sessions),
+ }));
+
+ if (!urlPath) {
+ return { mode, pages, points: [], snapshot: null, scroll: emptyScroll() };
+ }
+
+ if (mode === 'scroll') {
+ const bucketRows = await rawQuery<
+ {
+ depth: number | string;
+ sessions: number | string;
+ pageW: number | string;
+ pageH: number | string;
+ viewportW: number | string;
+ viewportH: number | string;
+ }[]
+ >(
+ `
+ select
+ intDiv(max_pct, ${SCROLL_BUCKET_SIZE}) * ${SCROLL_BUCKET_SIZE} as depth,
+ count() as sessions,
+ pageW,
+ pageH,
+ viewportW,
+ viewportH
+ from (
+ select
+ h.visit_id,
+ max(h.scroll_pct) as max_pct,
+ toInt32OrNull(toString(arrayElement(topK(1)(h.page_w), 1))) as pageW,
+ toInt32OrNull(toString(arrayElement(topK(1)(h.page_h), 1))) as pageH,
+ toInt32OrNull(toString(arrayElement(topK(1)(h.viewport_w), 1))) as viewportW,
+ toInt32OrNull(toString(arrayElement(topK(1)(h.viewport_h), 1))) as viewportH
+ from heatmap_event h
+ ${filterContext.joinQuery}
+ where h.website_id = {websiteId:UUID}
+ and h.event_type = {eventType:UInt8}
+ and h.url_path = {urlPath:String}
+ and h.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ ${filterContext.filterQuery}
+ and h.scroll_pct is not null
+ and h.page_w is not null
+ and h.page_h is not null
+ and h.viewport_w is not null
+ and h.viewport_h is not null
+ group by h.visit_id
+ )
+ where pageW is not null
+ and pageH is not null
+ and viewportW is not null
+ and viewportH is not null
+ group by depth, pageW, pageH, viewportW, viewportH
+ order by depth
+ `,
+ { ...filterContext.queryParams, websiteId, eventType, urlPath, startDate, endDate },
+ FUNCTION_NAME,
+ );
+
+ const scrollBuckets = bucketRows.map(r => ({
+ depth: Number(r.depth),
+ sessions: Number(r.sessions),
+ pageW: Number(r.pageW),
+ pageH: Number(r.pageH),
+ viewportW: Number(r.viewportW),
+ viewportH: Number(r.viewportH),
+ }));
+ const viewport = pickScrollSnapshotViewport(scrollBuckets);
+ const scroll = {
+ buckets: scrollBuckets,
+ totalSessions: scrollBuckets.reduce((sum, bucket) => sum + bucket.sessions, 0),
+ pageW: viewport?.pageW ?? null,
+ pageH: viewport?.pageH ?? null,
+ viewportW: viewport?.width ?? null,
+ viewportH: viewport?.height ?? null,
+ };
+ const snapshot = await resolveHeatmapSnapshot({
+ websiteId,
+ urlPath,
+ viewportW: viewport?.width ?? null,
+ viewportH: viewport?.height ?? null,
+ pageW: viewport?.pageW ?? null,
+ pageH: viewport?.pageH ?? null,
+ });
+
+ return {
+ mode,
+ pages,
+ points: [],
+ snapshot,
+ scroll,
+ };
+ }
+
+ const pointRows = await rawQuery<
+ {
+ x: number;
+ y: number;
+ pageX: number;
+ pageY: number;
+ pageW: number;
+ pageH: number;
+ viewportW: number;
+ viewportH: number;
+ count: string | number;
+ }[]
+ >(
+ `
+ select
+ h.x,
+ h.y,
+ h.page_x as pageX,
+ h.page_y as pageY,
+ h.page_w as pageW,
+ h.page_h as pageH,
+ h.viewport_w as viewportW,
+ h.viewport_h as viewportH,
+ count() as count
+ from heatmap_event h
+ ${filterContext.joinQuery}
+ where h.website_id = {websiteId:UUID}
+ and h.event_type = {eventType:UInt8}
+ and h.url_path = {urlPath:String}
+ and h.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ ${filterContext.filterQuery}
+ and h.x is not null
+ and h.y is not null
+ and h.page_x is not null
+ and h.page_y is not null
+ and h.page_w is not null
+ and h.page_h is not null
+ and h.viewport_w is not null
+ and h.viewport_h is not null
+ group by
+ h.x,
+ h.y,
+ h.page_x,
+ h.page_y,
+ h.page_w,
+ h.page_h,
+ h.viewport_w,
+ h.viewport_h
+ order by count desc
+ limit ${POINT_LIMIT}
+ `,
+ { ...filterContext.queryParams, websiteId, eventType, urlPath, startDate, endDate },
+ FUNCTION_NAME,
+ );
+
+ const points: HeatmapPoint[] = pointRows.map(p => ({
+ x: Number(p.x),
+ y: Number(p.y),
+ pageX: Number(p.pageX),
+ pageY: Number(p.pageY),
+ pageW: Number(p.pageW),
+ pageH: Number(p.pageH),
+ viewportW: Number(p.viewportW),
+ viewportH: Number(p.viewportH),
+ count: Number(p.count),
+ }));
+
+ const viewport = pickSnapshotViewport(points);
+ const snapshot = await resolveHeatmapSnapshot({
+ websiteId,
+ urlPath,
+ viewportW: viewport?.width ?? null,
+ viewportH: viewport?.height ?? null,
+ pageW: viewport?.pageW ?? null,
+ pageH: viewport?.pageH ?? null,
+ });
+
+ return { mode, pages, points, snapshot, scroll: emptyScroll() };
+}
+
+function emptyScroll(): HeatmapResult['scroll'] {
+ return {
+ buckets: [],
+ totalSessions: 0,
+ pageW: null,
+ pageH: null,
+ viewportW: null,
+ viewportH: null,
+ };
+}
+
+async function resolveHeatmapSnapshot({
+ websiteId,
+ urlPath,
+ viewportW,
+ viewportH,
+ pageW,
+ pageH,
+}: {
+ websiteId: string;
+ urlPath: string;
+ viewportW: number | null;
+ viewportH: number | null;
+ pageW: number | null;
+ pageH: number | null;
+}): Promise {
+ return getIframeSnapshot({
+ websiteId,
+ urlPath,
+ viewportW,
+ viewportH,
+ pageW,
+ pageH,
+ });
+}
+
+async function getIframeSnapshot({
+ websiteId,
+ urlPath,
+ viewportW,
+ viewportH,
+ pageW,
+ pageH,
+}: {
+ websiteId: string;
+ urlPath: string;
+ viewportW: number | null;
+ viewportH: number | null;
+ pageW: number | null;
+ pageH: number | null;
+}): Promise {
+ if (!urlPath || !viewportW || !pageW || !pageH) {
+ return null;
+ }
+
+ const website = await getWebsite(websiteId);
+ const url = buildHeatmapPageUrl(website?.domain, urlPath);
+
+ if (!url) {
+ return null;
+ }
+
+ const fallbackViewportH = Math.min(Math.max(pageH, 640), 1080);
+ const height = viewportH || fallbackViewportH;
+
+ return {
+ kind: 'iframe',
+ id: `iframe:${websiteId}:${urlPath}:${viewportW}x${height}`,
+ url,
+ pageW,
+ pageH,
+ viewportW,
+ viewportH: height,
+ };
+}
+
+function getFirstDomain(domain?: string | null) {
+ return domain?.split(',')[0]?.trim() || null;
+}
+
+function getWebsiteOrigin(domain?: string | null) {
+ const host = getFirstDomain(domain);
+
+ if (!host) {
+ return null;
+ }
+
+ if (host.startsWith('http://') || host.startsWith('https://')) {
+ return new URL(host);
+ }
+
+ const protocol =
+ host.startsWith('localhost') || host.startsWith('127.0.0.1') || host.startsWith('[::1]')
+ ? 'http'
+ : 'https';
+
+ return new URL(`${protocol}://${host}`);
+}
+
+function buildHeatmapPageUrl(domain: string | null | undefined, urlPath: string) {
+ try {
+ const origin = getWebsiteOrigin(domain);
+
+ if (!origin) {
+ return null;
+ }
+
+ return new URL(urlPath || '/', origin).toString();
+ } catch {
+ return null;
+ }
+}
+
+function pickSnapshotViewport(
+ points: HeatmapPoint[],
+): { width: number; height: number; pageW: number; pageH: number } | null {
+ const viewportBuckets = new Map<
+ string,
+ {
+ width: number;
+ height: number;
+ count: number;
+ maxPageW: number;
+ maxPageH: number;
+ }
+ >();
+
+ for (const p of points) {
+ const viewportKey = `${p.viewportW}x${p.viewportH}`;
+ const viewportBucket = viewportBuckets.get(viewportKey);
+
+ if (viewportBucket) {
+ viewportBucket.count += p.count;
+ viewportBucket.maxPageW = Math.max(viewportBucket.maxPageW, p.pageW);
+ viewportBucket.maxPageH = Math.max(viewportBucket.maxPageH, p.pageH);
+ } else {
+ viewportBuckets.set(viewportKey, {
+ width: p.viewportW,
+ height: p.viewportH,
+ count: p.count,
+ maxPageW: p.pageW,
+ maxPageH: p.pageH,
+ });
+ }
+ }
+
+ let bestViewport: {
+ width: number;
+ height: number;
+ count: number;
+ maxPageW: number;
+ maxPageH: number;
+ } | null = null;
+
+ for (const bucket of viewportBuckets.values()) {
+ if (!bestViewport || bucket.count > bestViewport.count) {
+ bestViewport = bucket;
+ }
+ }
+
+ if (!bestViewport) {
+ return null;
+ }
+
+ return {
+ width: bestViewport.width,
+ height: bestViewport.height,
+ pageW: bestViewport.maxPageW,
+ pageH: bestViewport.maxPageH,
+ };
+}
+
+function pickScrollSnapshotViewport(
+ buckets: HeatmapScrollBucket[],
+): { width: number; height: number; pageW: number; pageH: number } | null {
+ const viewportBuckets = new Map<
+ string,
+ {
+ width: number;
+ height: number;
+ sessions: number;
+ maxPageW: number;
+ maxPageH: number;
+ }
+ >();
+
+ for (const bucket of buckets) {
+ const viewportKey = `${bucket.viewportW}x${bucket.viewportH}`;
+ const viewportBucket = viewportBuckets.get(viewportKey);
+
+ if (viewportBucket) {
+ viewportBucket.sessions += bucket.sessions;
+ viewportBucket.maxPageW = Math.max(viewportBucket.maxPageW, bucket.pageW);
+ viewportBucket.maxPageH = Math.max(viewportBucket.maxPageH, bucket.pageH);
+ } else {
+ viewportBuckets.set(viewportKey, {
+ width: bucket.viewportW,
+ height: bucket.viewportH,
+ sessions: bucket.sessions,
+ maxPageW: bucket.pageW,
+ maxPageH: bucket.pageH,
+ });
+ }
+ }
+
+ let bestViewport: {
+ width: number;
+ height: number;
+ sessions: number;
+ maxPageW: number;
+ maxPageH: number;
+ } | null = null;
+
+ for (const bucket of viewportBuckets.values()) {
+ if (!bestViewport || bucket.sessions > bestViewport.sessions) {
+ bestViewport = bucket;
+ }
+ }
+
+ if (!bestViewport) {
+ return null;
+ }
+
+ return {
+ width: bestViewport.width,
+ height: bestViewport.height,
+ pageW: bestViewport.maxPageW,
+ pageH: bestViewport.maxPageH,
+ };
+}
+
+function getHeatmapPathFilters(filters: QueryFilters) {
+ return filtersObjectToArray(filters).filter(filter => filter.name === 'path');
+}
+
+function omitHeatmapPathFilters(filters: QueryFilters): QueryFilters {
+ return Object.fromEntries(
+ Object.entries(filters).filter(([key]) => key.replace(/\d+$/, '') !== 'path'),
+ ) as QueryFilters;
+}
+
+function getRelationalHeatmapPathFilterContext(filters: QueryFilters) {
+ const pathFilters = getHeatmapPathFilters(filters);
+
+ if (!pathFilters.length) {
+ return { filterQuery: '', queryParams: {} };
+ }
+
+ const clauses = pathFilters.map(({ operator, paramName, name }) => {
+ const key = paramName ?? name;
+
+ switch (operator) {
+ case OPERATORS.equals:
+ return `h.url_path = ANY({{${key}}})`;
+ case OPERATORS.notEquals:
+ return `h.url_path != ALL({{${key}}})`;
+ case OPERATORS.contains:
+ return `h.url_path ilike {{${key}}}`;
+ case OPERATORS.doesNotContain:
+ return `h.url_path not ilike {{${key}}}`;
+ case OPERATORS.regex:
+ return `h.url_path ~* {{${key}}}`;
+ case OPERATORS.notRegex:
+ return `h.url_path !~* {{${key}}}`;
+ default:
+ return '';
+ }
+ });
+
+ const queryParams = Object.fromEntries(
+ pathFilters.map(({ operator, value, paramName, name }) => {
+ const key = paramName ?? name;
+
+ if (operator === OPERATORS.contains || operator === OPERATORS.doesNotContain) {
+ return [key, `%${value}%`];
+ }
+
+ if (operator === OPERATORS.equals || operator === OPERATORS.notEquals) {
+ return [key, Array.isArray(value) ? value : [value]];
+ }
+
+ return [key, Array.isArray(value) ? value[0] : value];
+ }),
+ );
+
+ const joinedClauses =
+ filters.match === 'any'
+ ? clauses.filter(Boolean).join('\n or ')
+ : clauses.filter(Boolean).join('\n and ');
+
+ return {
+ filterQuery:
+ filters.match === 'any'
+ ? `and (\n ${joinedClauses}\n )`
+ : clauses.length
+ ? `and ${joinedClauses}`
+ : '',
+ queryParams,
+ };
+}
+
+function getClickhouseHeatmapPathFilterContext(filters: QueryFilters) {
+ const pathFilters = getHeatmapPathFilters(filters);
+
+ if (!pathFilters.length) {
+ return { filterQuery: '', queryParams: {} };
+ }
+
+ const clauses = pathFilters.map(({ operator, paramName, name }) => {
+ const key = paramName ?? name;
+
+ switch (operator) {
+ case OPERATORS.equals:
+ return `h.url_path IN {${key}:Array(String)}`;
+ case OPERATORS.notEquals:
+ return `h.url_path NOT IN {${key}:Array(String)}`;
+ case OPERATORS.contains:
+ return `positionCaseInsensitive(h.url_path, {${key}:String}) > 0`;
+ case OPERATORS.doesNotContain:
+ return `positionCaseInsensitive(h.url_path, {${key}:String}) = 0`;
+ case OPERATORS.regex:
+ return `match(h.url_path, concat('(?i)', {${key}:String}))`;
+ case OPERATORS.notRegex:
+ return `not match(h.url_path, concat('(?i)', {${key}:String}))`;
+ default:
+ return '';
+ }
+ });
+
+ const queryParams = Object.fromEntries(
+ pathFilters.map(({ operator, value, paramName, name }) => {
+ const key = paramName ?? name;
+
+ if (operator === OPERATORS.equals || operator === OPERATORS.notEquals) {
+ return [key, Array.isArray(value) ? value : [value]];
+ }
+
+ return [key, Array.isArray(value) ? value[0] : value];
+ }),
+ );
+
+ const joinedClauses =
+ filters.match === 'any'
+ ? clauses.filter(Boolean).join('\n or ')
+ : clauses.filter(Boolean).join('\n and ');
+
+ return {
+ filterQuery:
+ filters.match === 'any'
+ ? `and (\n ${joinedClauses}\n )`
+ : clauses.length
+ ? `and ${joinedClauses}`
+ : '',
+ queryParams,
+ };
+}
+
+function getRelationalHeatmapFilterContext(
+ websiteId: string,
+ filters: QueryFilters,
+): HeatmapFilterContext {
+ const { parseFilters } = prisma;
+ const pathFilterContext = getRelationalHeatmapPathFilterContext(filters);
+ const { filterQuery, cohortQuery, excludeBounceQuery, joinSessionQuery, queryParams } =
+ parseFilters({
+ ...omitHeatmapPathFilters(filters),
+ websiteId,
+ });
+
+ if (!(filterQuery || cohortQuery || excludeBounceQuery)) {
+ return {
+ joinQuery: '',
+ filterQuery: pathFilterContext.filterQuery,
+ queryParams: pathFilterContext.queryParams,
+ };
+ }
+
+ return {
+ joinQuery: `
+ inner join (
+ select distinct website_event.website_id, website_event.session_id, website_event.visit_id
+ from website_event
+ ${joinSessionQuery}
+ ${cohortQuery}
+ ${excludeBounceQuery}
+ where website_event.website_id = {{websiteId::uuid}}
+ and website_event.created_at between {{startDate}} and {{endDate}}
+ ${filterQuery}
+ ) filtered_visits
+ on filtered_visits.website_id = h.website_id
+ and filtered_visits.session_id = h.session_id
+ and filtered_visits.visit_id = h.visit_id
+ `,
+ filterQuery: pathFilterContext.filterQuery,
+ queryParams: { ...queryParams, ...pathFilterContext.queryParams },
+ };
+}
+
+function getClickhouseHeatmapFilterContext(
+ websiteId: string,
+ filters: QueryFilters,
+): HeatmapFilterContext {
+ const { parseFilters } = clickhouse;
+ const pathFilterContext = getClickhouseHeatmapPathFilterContext(filters);
+ const { filterQuery, cohortQuery, excludeBounceQuery, queryParams } = parseFilters({
+ ...omitHeatmapPathFilters(filters),
+ websiteId,
+ });
+
+ if (!(filterQuery || cohortQuery || excludeBounceQuery)) {
+ return {
+ joinQuery: '',
+ filterQuery: pathFilterContext.filterQuery,
+ queryParams: pathFilterContext.queryParams,
+ };
+ }
+
+ return {
+ joinQuery: `
+ inner join (
+ select distinct website_id, session_id, visit_id
+ from website_event
+ ${cohortQuery}
+ ${excludeBounceQuery}
+ where website_id = {websiteId:UUID}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ ${filterQuery}
+ ) filtered_visits
+ on filtered_visits.website_id = h.website_id
+ and filtered_visits.session_id = h.session_id
+ and filtered_visits.visit_id = h.visit_id
+ `,
+ filterQuery: pathFilterContext.filterQuery,
+ queryParams: { ...queryParams, ...pathFilterContext.queryParams },
+ };
+}
diff --git a/src/queries/sql/heatmap/saveHeatmapEvents.ts b/src/queries/sql/heatmap/saveHeatmapEvents.ts
new file mode 100644
index 000000000..60277bc78
--- /dev/null
+++ b/src/queries/sql/heatmap/saveHeatmapEvents.ts
@@ -0,0 +1,110 @@
+import clickhouse from '@/lib/clickhouse';
+import { uuid } from '@/lib/crypto';
+import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import kafka from '@/lib/kafka';
+import prisma from '@/lib/prisma';
+
+export interface HeatmapEventRow {
+ websiteId: string;
+ sessionId: string;
+ visitId: string;
+ urlPath: string;
+ eventType: number;
+ x: number | null;
+ y: number | null;
+ pageX: number | null;
+ pageY: number | null;
+ pageW: number | null;
+ viewportW: number | null;
+ viewportH: number | null;
+ pageH: number | null;
+ scrollPct: number | null;
+ createdAt: Date;
+}
+
+export async function saveHeatmapEvents(rows: HeatmapEventRow[]) {
+ if (!rows?.length) return;
+
+ const normalizedRows = rows.map(r => ({
+ ...r,
+ x: toInt(r.x),
+ y: toInt(r.y),
+ pageX: toInt(r.pageX),
+ pageY: toInt(r.pageY),
+ pageW: toInt(r.pageW),
+ viewportW: toInt(r.viewportW),
+ viewportH: toInt(r.viewportH),
+ pageH: toInt(r.pageH),
+ scrollPct: toScrollPct(r.scrollPct),
+ }));
+
+ return runQuery({
+ [PRISMA]: () => relationalQuery(normalizedRows),
+ [CLICKHOUSE]: () => clickhouseQuery(normalizedRows),
+ });
+}
+
+function toInt(value: number | null) {
+ return typeof value === 'number' && Number.isFinite(value) ? Math.round(value) : null;
+}
+
+function toScrollPct(value: number | null) {
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
+ return null;
+ }
+
+ return Math.max(0, Math.min(100, Math.round(value)));
+}
+
+async function relationalQuery(rows: HeatmapEventRow[]) {
+ return prisma.client.heatmapEvent.createMany({
+ data: rows.map(r => ({
+ id: uuid(),
+ websiteId: r.websiteId,
+ sessionId: r.sessionId,
+ visitId: r.visitId,
+ urlPath: r.urlPath,
+ eventType: r.eventType,
+ x: r.x,
+ y: r.y,
+ pageX: r.pageX,
+ pageY: r.pageY,
+ pageW: r.pageW,
+ viewportW: r.viewportW,
+ viewportH: r.viewportH,
+ pageH: r.pageH,
+ scrollPct: r.scrollPct,
+ createdAt: r.createdAt,
+ })) as any,
+ });
+}
+
+async function clickhouseQuery(rows: HeatmapEventRow[]) {
+ const { insert, getUTCString } = clickhouse;
+ const { sendMessage } = kafka;
+
+ const messages = rows.map(r => ({
+ heatmap_event_id: uuid(),
+ website_id: r.websiteId,
+ session_id: r.sessionId,
+ visit_id: r.visitId,
+ url_path: r.urlPath,
+ event_type: r.eventType,
+ x: r.x,
+ y: r.y,
+ page_x: r.pageX,
+ page_y: r.pageY,
+ page_w: r.pageW,
+ viewport_w: r.viewportW,
+ viewport_h: r.viewportH,
+ page_h: r.pageH,
+ scroll_pct: r.scrollPct,
+ created_at: getUTCString(r.createdAt),
+ }));
+
+ if (kafka.enabled) {
+ return sendMessage('heatmap_event', messages);
+ }
+
+ return insert('heatmap_event', messages);
+}
diff --git a/src/queries/sql/index.ts b/src/queries/sql/index.ts
index f5a05d758..4833adfcc 100644
--- a/src/queries/sql/index.ts
+++ b/src/queries/sql/index.ts
@@ -22,10 +22,14 @@ export * from './getChannelExpandedMetrics';
export * from './getChannelMetrics';
export * from './getRealtimeActivity';
export * from './getRealtimeData';
+export * from './getWebsiteListCharts';
export * from './getValues';
export * from './getWebsiteDateRange';
export * from './getWebsiteStats';
export * from './getWeeklyTraffic';
+export * from './heatmap/extractHeatmapEvents';
+export * from './heatmap/getHeatmap';
+export * from './heatmap/saveHeatmapEvents';
export * from './pageviews/getPageviewExpandedMetrics';
export * from './pageviews/getPageviewMetrics';
export * from './pageviews/getPageviewStats';
@@ -39,9 +43,19 @@ export * from './reports/getJourney';
export * from './reports/getRetention';
export * from './reports/getUTM';
export * from './sessions/createSession';
+export * from './sessions/getLinkedDistinctIds';
+export * from './sessions/getLinkedSessionIds';
export * from './sessions/getSessionActivity';
+export * from './sessions/getSessionActivityBounds';
export * from './sessions/getSessionData';
+export * from './sessions/getSessionDataActivityStats';
+export * from './sessions/getSessionDataArraySeries';
+export * from './sessions/getSessionDataDateSeries';
+export * from './sessions/getSessionDataNumericSeries';
+export * from './sessions/getSessionDataNumericStats';
+export * from './sessions/getSessionDataPivot';
export * from './sessions/getSessionDataProperties';
+export * from './sessions/getSessionDataPropertySeries';
export * from './sessions/getSessionDataValues';
export * from './sessions/getSessionExpandedMetrics';
export * from './sessions/getSessionMetrics';
@@ -50,3 +64,5 @@ export * from './sessions/getWebsiteSession';
export * from './sessions/getWebsiteSessionStats';
export * from './sessions/getWebsiteSessions';
export * from './sessions/saveSessionData';
+export * from './sessions/saveSessionLink';
+export * from './sessions/updateSession';
diff --git a/src/queries/sql/pageviews/getPageviewExpandedMetrics.ts b/src/queries/sql/pageviews/getPageviewExpandedMetrics.ts
index a0c489ac8..e25a0242c 100644
--- a/src/queries/sql/pageviews/getPageviewExpandedMetrics.ts
+++ b/src/queries/sql/pageviews/getPageviewExpandedMetrics.ts
@@ -36,7 +36,7 @@ async function relationalQuery(
filters: QueryFilters,
): Promise {
const { type, limit = 500, offset = 0 } = parameters;
- let column = FILTER_COLUMNS[type] || type;
+ let column = getPageviewColumn(type);
const { rawQuery, parseFilters, getTimestampDiffSQL } = prisma;
const { filterQuery, joinSessionQuery, cohortQuery, excludeBounceQuery, queryParams } =
parseFilters(
@@ -46,10 +46,13 @@ async function relationalQuery(
},
{ joinSession: SESSION_COLUMNS.includes(type) },
);
+ const fullPathSearchQuery =
+ type === 'fullPath' && filters.search
+ ? `and (case when website_event.url_query != '' then website_event.url_path || '?' || website_event.url_query else website_event.url_path end) ilike {{fullPathSearch}}`
+ : '';
let entryExitQuery = '';
let excludeDomain = '';
-
if (column === 'referrer_domain') {
excludeDomain = `and website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '')
and website_event.referrer_domain != ''`;
@@ -76,6 +79,16 @@ async function relationalQuery(
`;
}
+ const selectColumn =
+ type === 'fullPath'
+ ? `case when website_event.url_query != '' then website_event.url_path || '?' || website_event.url_query else website_event.url_path end`
+ : column;
+
+ const groupByColumn =
+ type === 'fullPath'
+ ? `case when website_event.url_query != '' then website_event.url_path || '?' || website_event.url_query else website_event.url_path end`
+ : column;
+
return rawQuery(
`
select
@@ -87,7 +100,7 @@ async function relationalQuery(
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
from (
select
- ${column} as "name",
+ ${selectColumn} as "name",
website_event.session_id,
website_event.visit_id,
count(*) as "c",
@@ -102,16 +115,20 @@ async function relationalQuery(
and website_event.created_at between {{startDate}} and {{endDate}}
and website_event.event_type NOT IN (2, 5)
${excludeDomain}
+ ${fullPathSearchQuery}
${filterQuery}
- group by ${column}, website_event.session_id, website_event.visit_id
+ group by ${groupByColumn}, website_event.session_id, website_event.visit_id
) as t
where name != ''
- group by name
+ group by name
order by visitors desc, visits desc
limit ${limit}
offset ${offset}
`,
- queryParams,
+ {
+ ...queryParams,
+ ...(type === 'fullPath' && filters.search ? { fullPathSearch: `%${filters.search}%` } : {}),
+ },
FUNCTION_NAME,
);
}
@@ -122,36 +139,46 @@ async function clickhouseQuery(
filters: QueryFilters,
): Promise<{ x: string; y: number }[]> {
const { type, limit = 500, offset = 0 } = parameters;
- let column = FILTER_COLUMNS[type] || type;
+ let column = getPageviewColumn(type);
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, excludeBounceQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
+ const fullPathSearchQuery =
+ type === 'fullPath' && filters.search
+ ? `and positionCaseInsensitive(if(url_query != '', concat(url_path, '?', url_query), url_path), {fullPathSearch:String}) > 0`
+ : '';
let excludeDomain = '';
let entryExitQuery = '';
+ let selectColumn = column;
if (column === 'referrer_domain') {
excludeDomain = `and referrer_domain != hostname and referrer_domain != ''`;
if (type === 'domain') {
column = toClickHouseGroupedReferrer(GROUPED_DOMAINS);
+ selectColumn = column;
}
}
if (type === 'entry' || type === 'exit') {
const aggregrate = type === 'entry' ? 'argMin' : 'argMax';
- column = `x.${column}`;
entryExitQuery = `
JOIN (select visit_id,
- ${aggregrate}(url_path, created_at) url_path
+ ${aggregrate}(url_path, created_at) url_path,
+ ${aggregrate}(url_query, created_at) url_query
from website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type NOT IN (2, 5)
group by visit_id) x
ON x.visit_id = website_event.visit_id`;
+
+ selectColumn = `x.url_path`;
+ } else if (type === 'fullPath') {
+ selectColumn = `if(url_query != '', concat(url_path, '?', url_query), url_path)`;
}
return rawQuery(
@@ -165,7 +192,7 @@ async function clickhouseQuery(
sum(max_time-min_time) as "totaltime"
from (
select
- ${column} name,
+ ${selectColumn} name,
session_id,
visit_id,
count(*) c,
@@ -180,15 +207,20 @@ async function clickhouseQuery(
and event_type NOT IN (2, 5)
and name != ''
${excludeDomain}
+ ${fullPathSearchQuery}
${filterQuery}
group by name, session_id, visit_id
) as t
- group by name
+ group by name
order by visitors desc, visits desc
limit ${limit}
offset ${offset}
`,
- { ...queryParams, ...parameters },
+ {
+ ...queryParams,
+ ...parameters,
+ ...(type === 'fullPath' && filters.search ? { fullPathSearch: filters.search } : {}),
+ },
FUNCTION_NAME,
);
}
@@ -228,3 +260,11 @@ export function toPostgresGroupedReferrer(
function toPostgresLikeClause(column: string, arr: string[]) {
return arr.map(val => `${column} ilike '%${val.replace(/'/g, "''")}%'`).join(' OR\n ');
}
+
+function getPageviewColumn(type: string) {
+ if (type === 'fullPath') {
+ return 'url_path';
+ }
+
+ return FILTER_COLUMNS[type] || type;
+}
diff --git a/src/queries/sql/pageviews/getPageviewMetrics.ts b/src/queries/sql/pageviews/getPageviewMetrics.ts
index a7a705f1d..737469025 100644
--- a/src/queries/sql/pageviews/getPageviewMetrics.ts
+++ b/src/queries/sql/pageviews/getPageviewMetrics.ts
@@ -32,7 +32,7 @@ async function relationalQuery(
filters: QueryFilters,
): Promise {
const { type, limit = 500, offset = 0 } = parameters;
- let column = FILTER_COLUMNS[type] || type;
+ let column = getPageviewColumn(type);
const { rawQuery, parseFilters } = prisma;
const { filterQuery, joinSessionQuery, cohortQuery, excludeBounceQuery, queryParams } =
parseFilters(
@@ -42,10 +42,13 @@ async function relationalQuery(
},
{ joinSession: SESSION_COLUMNS.includes(type) },
);
+ const fullPathSearchQuery =
+ type === 'fullPath' && filters.search
+ ? `and (case when website_event.url_query != '' then website_event.url_path || '?' || website_event.url_query else website_event.url_path end) ilike {{fullPathSearch}}`
+ : '';
let entryExitQuery = '';
let excludeDomain = '';
-
if (column === 'referrer_domain') {
excludeDomain = `and website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '')
and website_event.referrer_domain != ''`;
@@ -53,13 +56,13 @@ async function relationalQuery(
if (type === 'entry' || type === 'exit') {
const order = type === 'entry' ? 'asc' : 'desc';
- column = `x.${column}`;
entryExitQuery = `
join (
select distinct on (visit_id)
visit_id,
- url_path
+ url_path,
+ url_query
from website_event
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
@@ -68,11 +71,18 @@ async function relationalQuery(
) x
on x.visit_id = website_event.visit_id
`;
+
+ column = `x.${FILTER_COLUMNS[type] || type}`;
}
+ const selectColumn =
+ type === 'fullPath'
+ ? `case when website_event.url_query != '' then website_event.url_path || '?' || website_event.url_query else website_event.url_path end`
+ : column;
+
return rawQuery(
`
- select ${column} x,
+ select ${selectColumn} x,
count(distinct website_event.session_id) as y
from website_event
${cohortQuery}
@@ -84,13 +94,18 @@ async function relationalQuery(
and website_event.event_type NOT IN (2, 5)
and ${column} != ''
${excludeDomain}
+ ${fullPathSearchQuery}
${filterQuery}
group by 1
order by 2 desc
limit ${limit}
offset ${offset}
`,
- { ...queryParams, ...parameters },
+ {
+ ...queryParams,
+ ...parameters,
+ ...(type === 'fullPath' && filters.search ? { fullPathSearch: `%${filters.search}%` } : {}),
+ },
FUNCTION_NAME,
);
}
@@ -101,18 +116,22 @@ async function clickhouseQuery(
filters: QueryFilters,
): Promise<{ x: string; y: number }[]> {
const { type, limit = 500, offset = 0 } = parameters;
- let column = FILTER_COLUMNS[type] || type;
+ let column = getPageviewColumn(type);
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, excludeBounceQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
+ const fullPathSearchQuery =
+ type === 'fullPath' && filters.search
+ ? `and positionCaseInsensitive(if(url_query != '', concat(url_path, '?', url_query), url_path), {fullPathSearch:String}) > 0`
+ : '';
let sql = '';
let excludeDomain = '';
-
- if (EVENT_COLUMNS.some(item => Object.keys(filters).includes(item))) {
+ if (type === 'fullPath' || EVENT_COLUMNS.some(item => Object.keys(filters).includes(item))) {
let entryExitQuery = '';
+ let selectColumn = column;
if (column === 'referrer_domain') {
excludeDomain = `and referrer_domain != hostname and referrer_domain != ''`;
@@ -120,21 +139,26 @@ async function clickhouseQuery(
if (type === 'entry' || type === 'exit') {
const aggregrate = type === 'entry' ? 'argMin' : 'argMax';
- column = `x.${column}`;
entryExitQuery = `
JOIN (select visit_id,
- ${aggregrate}(url_path, created_at) url_path
+ ${aggregrate}(url_path, created_at) url_path,
+ ${aggregrate}(url_query, created_at) url_query
from website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type NOT IN (2, 5)
group by visit_id) x
ON x.visit_id = website_event.visit_id`;
+
+ column = `x.url_path`;
+ selectColumn = `x.url_path`;
+ } else if (type === 'fullPath') {
+ selectColumn = `if(url_query != '', concat(url_path, '?', url_query), url_path)`;
}
sql = `
- select ${column} x,
+ select ${selectColumn} x,
uniq(website_event.session_id) as y
from website_event
${cohortQuery}
@@ -145,6 +169,7 @@ async function clickhouseQuery(
and event_type NOT IN (2, 5)
and ${column} != ''
${excludeDomain}
+ ${fullPathSearchQuery}
${filterQuery}
group by x
order by y desc
@@ -175,7 +200,7 @@ async function clickhouseQuery(
select g.t as x,
uniq(s) as y
from (
- select session_id s,
+ select session_id s,
${columnQuery} as t
from website_event_stats_hourly as website_event
${cohortQuery}
@@ -193,5 +218,21 @@ async function clickhouseQuery(
`;
}
- return rawQuery(sql, { ...queryParams, ...parameters }, FUNCTION_NAME);
+ return rawQuery(
+ sql,
+ {
+ ...queryParams,
+ ...parameters,
+ ...(type === 'fullPath' && filters.search ? { fullPathSearch: filters.search } : {}),
+ },
+ FUNCTION_NAME,
+ );
+}
+
+function getPageviewColumn(type: string) {
+ if (type === 'fullPath') {
+ return 'url_path';
+ }
+
+ return FILTER_COLUMNS[type] || type;
}
diff --git a/src/queries/sql/replays/getReplayChunks.ts b/src/queries/sql/replays/getReplayChunks.ts
index 5c192172e..182c0703a 100644
--- a/src/queries/sql/replays/getReplayChunks.ts
+++ b/src/queries/sql/replays/getReplayChunks.ts
@@ -15,15 +15,39 @@ export interface ReplayChunk {
endedAt: Date;
}
-export async function getReplayChunks(websiteId: string, visitId: string): Promise {
+interface GetReplayChunksOptions {
+ endAt?: Date;
+ endChunkIndex?: number;
+}
+
+export async function getReplayChunks(
+ websiteId: string,
+ visitId: string,
+ options: GetReplayChunksOptions = {},
+): Promise {
return runQuery({
- [PRISMA]: () => relationalQuery(websiteId, visitId),
- [CLICKHOUSE]: () => clickhouseQuery(websiteId, visitId),
+ [PRISMA]: () => relationalQuery(websiteId, visitId, options),
+ [CLICKHOUSE]: () => clickhouseQuery(websiteId, visitId, options),
});
}
-async function relationalQuery(websiteId: string, visitId: string): Promise {
+async function relationalQuery(
+ websiteId: string,
+ visitId: string,
+ { endAt, endChunkIndex }: GetReplayChunksOptions,
+): Promise {
const { rawQuery } = prisma;
+ const endAtFilter = endAt
+ ? `
+ and started_at <= {{endAt}}
+ `
+ : '';
+ const endChunkFilter =
+ endChunkIndex !== undefined
+ ? `
+ and chunk_index <= {{endChunkIndex}}
+ `
+ : '';
const chunks: {
sessionId: string;
@@ -46,9 +70,11 @@ async function relationalQuery(websiteId: string, visitId: string): Promise {
+async function clickhouseQuery(
+ websiteId: string,
+ visitId: string,
+ { endAt, endChunkIndex }: GetReplayChunksOptions,
+): Promise {
const { rawQuery } = clickhouse;
+ const endAtFilter = endAt
+ ? `
+ and started_at <= {endAt:DateTime64}
+ `
+ : '';
+ const endChunkFilter =
+ endChunkIndex !== undefined
+ ? `
+ and chunk_index <= {endChunkIndex:UInt32}
+ `
+ : '';
const results = await rawQuery<
{
@@ -82,11 +123,13 @@ async function clickhouseQuery(websiteId: string, visitId: string): Promise 0 ? minDuration * 1000 : undefined;
const { filterQuery, cohortQuery, queryParams, joinSessionQuery } = parseFilters({
...filters,
websiteId,
@@ -47,6 +48,10 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, session
or session.device ilike {{search}})`
: '';
+ const havingQuery = minDurationMs
+ ? `having sum(extract(epoch from sr.ended_at - sr.started_at) * 1000) >= {{minDurationMs}}`
+ : '';
+
return pagedRawQuery(
`
select
@@ -80,9 +85,10 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, session
session.device,
session.country,
session.city
+ ${havingQuery}
order by max(sr.created_at) desc
`,
- { ...queryParams, sessionId },
+ { ...queryParams, sessionId, minDurationMs },
filters,
FUNCTION_NAME,
);
@@ -90,7 +96,8 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, session
async function clickhouseQuery(websiteId: string, filters: QueryFilters, sessionId?: string) {
const { pagedRawQuery, parseFilters } = clickhouse;
- const { search } = filters;
+ const { search, minDuration } = filters;
+ const minDurationMs = minDuration && minDuration > 0 ? minDuration * 1000 : undefined;
const { queryParams, cohortQuery, filterQuery } = parseFilters({
...filters,
websiteId,
@@ -106,6 +113,10 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, session
or (positionCaseInsensitive(device, {search:String}) > 0))`
: '';
+ const havingQuery = minDurationMs
+ ? `having toInt64(sum(dateDiff('millisecond', session_replay.started_at, session_replay.ended_at))) >= {minDurationMs:Int64}`
+ : '';
+
return pagedRawQuery(
`
select
@@ -140,9 +151,10 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, session
and session_replay.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${sessionFilter}
group by session_replay.visit_id, session_replay.session_id, session_replay.website_id, website_event.browser, website_event.os, website_event.device, website_event.country, website_event.city
+ ${havingQuery}
order by max(created_at) desc
`,
- { ...queryParams, sessionId },
+ { ...queryParams, sessionId, minDurationMs },
filters,
FUNCTION_NAME,
);
diff --git a/src/queries/sql/reports/getRevenue.ts b/src/queries/sql/reports/getRevenueChart.ts
similarity index 98%
rename from src/queries/sql/reports/getRevenue.ts
rename to src/queries/sql/reports/getRevenueChart.ts
index 183861e12..a7c491f2e 100644
--- a/src/queries/sql/reports/getRevenue.ts
+++ b/src/queries/sql/reports/getRevenueChart.ts
@@ -12,7 +12,7 @@ export interface RevenuParameters {
compare?: string;
}
-export async function getRevenue(
+export async function getRevenueChart(
...args: [websiteId: string, parameters: RevenuParameters, filters: QueryFilters]
) {
return runQuery({
diff --git a/src/queries/sql/reports/getRevenueMetrics.ts b/src/queries/sql/reports/getRevenueMetrics.ts
index 6553d2e09..c1a9bcaf9 100644
--- a/src/queries/sql/reports/getRevenueMetrics.ts
+++ b/src/queries/sql/reports/getRevenueMetrics.ts
@@ -1,6 +1,7 @@
import clickhouse from '@/lib/clickhouse';
import {
EMAIL_DOMAINS,
+ LLM_DOMAINS,
PAID_AD_PARAMS,
SEARCH_DOMAINS,
SHOPPING_DOMAINS,
@@ -10,7 +11,7 @@ import {
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import type { QueryFilters } from '@/lib/types';
-import type { RevenuParameters } from './getRevenue';
+import type { RevenuParameters } from './getRevenueChart';
export interface RevenueMetricsResult {
country: { name: string; value: number }[];
@@ -19,8 +20,15 @@ export interface RevenueMetricsResult {
channel: { name: string; value: number }[];
}
+export type RevenueMetricType = keyof RevenueMetricsResult;
+
export async function getRevenueMetrics(
- ...args: [websiteId: string, parameters: RevenuParameters, filters: QueryFilters]
+ ...args: [
+ websiteId: string,
+ parameters: RevenuParameters,
+ filters: QueryFilters,
+ type: RevenueMetricType,
+ ]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -32,7 +40,8 @@ async function relationalQuery(
websiteId: string,
parameters: RevenuParameters,
filters: QueryFilters,
-): Promise {
+ type: RevenueMetricType,
+): Promise {
const { startDate, endDate, currency } = parameters;
const { rawQuery, parseFilters } = prisma;
const { queryParams, filterQuery, cohortQuery, joinSessionQuery } = parseFilters({
@@ -55,100 +64,106 @@ async function relationalQuery(
and website_event.event_id = revenue.event_id`
: '';
- const country = await rawQuery(
- `
- select
- session.country as "name",
- sum(revenue) as "value"
- from revenue
- ${joinQuery}
- join session
- on session.website_id = revenue.website_id
- and session.session_id = revenue.session_id
- ${cohortQuery}
- where revenue.website_id = {{websiteId::uuid}}
- and revenue.created_at between {{startDate}} and {{endDate}}
- and upper(revenue.currency) = {{currency}}
- ${filterQuery}
- group by session.country
- order by value desc
- `,
- queryParams,
- );
-
- const region = await rawQuery(
- `
- select
- session.country,
- session.region as "name",
- sum(revenue.revenue) as "value"
- from revenue
- ${joinQuery}
- join session
- on session.website_id = revenue.website_id
- and session.session_id = revenue.session_id
- ${cohortQuery}
- where revenue.website_id = {{websiteId::uuid}}
- and revenue.created_at between {{startDate}} and {{endDate}}
- and upper(revenue.currency) = {{currency}}
- ${filterQuery}
- group by session.country, session.region
- order by value desc
- `,
- queryParams,
- );
-
- const referrer = await rawQuery(
- `
- WITH events AS (
+ if (type === 'country') {
+ return rawQuery(
+ `
select
- revenue.website_id,
- revenue.session_id,
- sum(revenue.revenue) as "value"
+ session.country as "name",
+ sum(revenue) as "value"
from revenue
${joinQuery}
+ join session
+ on session.website_id = revenue.website_id
+ and session.session_id = revenue.session_id
${cohortQuery}
- ${joinSessionQuery}
where revenue.website_id = {{websiteId::uuid}}
and revenue.created_at between {{startDate}} and {{endDate}}
and upper(revenue.currency) = {{currency}}
${filterQuery}
- group by revenue.website_id, revenue.session_id),
+ group by session.country
+ order by value desc
+ `,
+ queryParams,
+ );
+ }
- revenue_data AS (
+ if (type === 'region') {
+ return rawQuery(
+ `
select
- e.website_id,
- e.session_id,
- e.value,
- we.min_date as created_at
- from events e
+ session.country,
+ session.region as "name",
+ sum(revenue.revenue) as "value"
+ from revenue
+ ${joinQuery}
+ join session
+ on session.website_id = revenue.website_id
+ and session.session_id = revenue.session_id
+ ${cohortQuery}
+ where revenue.website_id = {{websiteId::uuid}}
+ and revenue.created_at between {{startDate}} and {{endDate}}
+ and upper(revenue.currency) = {{currency}}
+ ${filterQuery}
+ group by session.country, session.region
+ order by value desc
+ `,
+ queryParams,
+ );
+ }
+
+ if (type === 'referrer') {
+ return rawQuery(
+ `
+ WITH events AS (
+ select
+ revenue.website_id,
+ revenue.session_id,
+ sum(revenue.revenue) as "value"
+ from revenue
+ ${joinQuery}
+ ${cohortQuery}
+ ${joinSessionQuery}
+ where revenue.website_id = {{websiteId::uuid}}
+ and revenue.created_at between {{startDate}} and {{endDate}}
+ and upper(revenue.currency) = {{currency}}
+ ${filterQuery}
+ group by revenue.website_id, revenue.session_id),
+
+ revenue_data AS (
+ select
+ e.website_id,
+ e.session_id,
+ e.value,
+ we.min_date as created_at
+ from events e
+ join (
+ select session_id, min(created_at) as min_date
+ from website_event
+ where website_id = {{websiteId::uuid}}
+ and created_at between {{startDate}} and {{endDate}}
+ group by session_id
+ ) we on we.session_id = e.session_id)
+
+ select
+ we.referrer_domain as "name",
+ sum(revenue_data.value) as "value"
+ from revenue_data
join (
- select session_id, min(created_at) as min_date
+ select website_id, session_id, referrer_domain, created_at
from website_event
where website_id = {{websiteId::uuid}}
- and created_at between {{startDate}} and {{endDate}}
- group by session_id
- ) we on we.session_id = e.session_id)
+ and created_at between {{startDate}} and {{endDate}}) we
+ on we.website_id = revenue_data.website_id
+ and we.session_id = revenue_data.session_id
+ and we.created_at = revenue_data.created_at
+ group by we.referrer_domain
+ order by value desc
+ `,
+ queryParams,
+ );
+ }
- select
- we.referrer_domain as "name",
- sum(revenue_data.value) as "value"
- from revenue_data
- join (
- select website_id, session_id, referrer_domain, created_at
- from website_event
- where website_id = {{websiteId::uuid}}
- and created_at between {{startDate}} and {{endDate}}) we
- on we.website_id = revenue_data.website_id
- and we.session_id = revenue_data.session_id
- and we.created_at = revenue_data.created_at
- group by we.referrer_domain
- order by value desc
- `,
- queryParams,
- );
-
- const channel = await rawQuery(
+ return rawQuery(
`
WITH events AS (
select
@@ -210,6 +225,7 @@ async function relationalQuery(
when ${toPostgresLikeClause('utm_medium', ['referral', 'app', 'link'])} then 'referral'
when utm_medium ilike '%affiliate%' then 'affiliate'
when utm_medium ilike '%sms%' or utm_source ilike '%sms%' then 'sms'
+ when ${toPostgresLikeClause('referrer_domain', LLM_DOMAINS)} then 'llm'
when ${toPostgresLikeClause('referrer_domain', SEARCH_DOMAINS)} or utm_medium ilike '%organic%' then concat(prefix, 'Search')
when ${toPostgresLikeClause('referrer_domain', SOCIAL_DOMAINS)} then concat(prefix, 'Social')
when ${toPostgresLikeClause('referrer_domain', EMAIL_DOMAINS)} or utm_medium ilike '%mail%' then 'email'
@@ -227,15 +243,14 @@ async function relationalQuery(
`,
queryParams,
);
-
- return { country, region, referrer, channel };
}
async function clickhouseQuery(
websiteId: string,
parameters: RevenuParameters,
filters: QueryFilters,
-): Promise {
+ type: RevenueMetricType,
+): Promise {
const { startDate, endDate, currency } = parameters;
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, queryParams } = parseFilters({
@@ -258,8 +273,9 @@ async function clickhouseQuery(
and website_event.event_id = website_revenue.event_id`
: '';
- const country = await rawQuery<{ name: string; value: number }[]>(
- `
+ if (type === 'country') {
+ return rawQuery<{ name: string; value: number }[]>(
+ `
select
website_event.country as "name",
sum(website_revenue.revenue) as "value"
@@ -280,12 +296,14 @@ async function clickhouseQuery(
${filterQuery}
group by website_event.country
order by value desc
- `,
- queryParams,
- );
+ `,
+ queryParams,
+ );
+ }
- const region = await rawQuery<{ name: string; value: number; country: string }[]>(
- `
+ if (type === 'region') {
+ return rawQuery<{ name: string; value: number; country: string }[]>(
+ `
select
website_event.country,
website_event.region as "name",
@@ -307,60 +325,63 @@ async function clickhouseQuery(
${filterQuery}
group by 1,2
order by value desc
- `,
- queryParams,
- );
+ `,
+ queryParams,
+ );
+ }
- const referrer = await rawQuery<{ name: string; value: number }[]>(
- `
- WITH events AS (
- select distinct
- website_id,
- session_id,
- sum(revenue) as "value"
- from website_revenue
- ${joinQuery}
- ${cohortQuery}
- where website_id = {websiteId:UUID}
- and created_at between {startDate:DateTime64} and {endDate:DateTime64}
- and upper(currency) = {currency:String}
- ${filterQuery}
- group by 1,2),
-
- revenue AS (
- select
- e.website_id,
- e.session_id,
- e.value,
- we.min_date as created_at
- from events e
- join (select session_id, min(created_at) min_date
- from website_event
- where website_id = {websiteId:UUID}
- and created_at between {startDate:DateTime64} and {endDate:DateTime64}
- group by 1
- ) we
- on we.session_id = e.session_id)
-
- select
- website_event.referrer_domain as "name",
- sum(revenue.value) as "value"
- from revenue
- any left join (
- select website_id, session_id, referrer_domain, created_at
- from website_event
+ if (type === 'referrer') {
+ return rawQuery<{ name: string; value: number }[]>(
+ `
+ WITH events AS (
+ select distinct
+ website_id,
+ session_id,
+ sum(revenue) as "value"
+ from website_revenue
+ ${joinQuery}
+ ${cohortQuery}
where website_id = {websiteId:UUID}
- and created_at between {startDate:DateTime64} and {endDate:DateTime64}) website_event
- on website_event.website_id = revenue.website_id
- and website_event.session_id = revenue.session_id
- and website_event.created_at = revenue.created_at
- group by 1
- order by value desc
- `,
- queryParams,
- );
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and upper(currency) = {currency:String}
+ ${filterQuery}
+ group by 1,2),
- const channel = await rawQuery<{ name: string; value: number }[]>(
+ revenue AS (
+ select
+ e.website_id,
+ e.session_id,
+ e.value,
+ we.min_date as created_at
+ from events e
+ join (select session_id, min(created_at) min_date
+ from website_event
+ where website_id = {websiteId:UUID}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ group by 1
+ ) we
+ on we.session_id = e.session_id)
+
+ select
+ website_event.referrer_domain as "name",
+ sum(revenue.value) as "value"
+ from revenue
+ any left join (
+ select website_id, session_id, referrer_domain, created_at
+ from website_event
+ where website_id = {websiteId:UUID}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}) website_event
+ on website_event.website_id = revenue.website_id
+ and website_event.session_id = revenue.session_id
+ and website_event.created_at = revenue.created_at
+ group by 1
+ order by value desc
+ `,
+ queryParams,
+ );
+ }
+
+ return rawQuery<{ name: string; value: number }[]>(
`
WITH events AS (
select distinct
@@ -402,6 +423,9 @@ async function clickhouseQuery(
when multiSearchAny(lower(utm_medium), ['referral', 'app','link']) != 0 then 'referral'
when position(lower(utm_medium), 'affiliate') > 0 then 'affiliate'
when position(lower(utm_medium), 'sms') > 0 or position(lower(utm_source), 'sms') > 0 then 'sms'
+ when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
+ LLM_DOMAINS,
+ )}]) != 0 then 'llm'
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
SEARCH_DOMAINS,
)}]) != 0 or position(lower(utm_medium), 'organic') > 0 then concat(prefix, 'Search')
@@ -438,8 +462,6 @@ async function clickhouseQuery(
`,
queryParams,
);
-
- return { country, region, referrer, channel };
}
function toClickHouseStringArray(arr: string[]): string {
diff --git a/src/queries/sql/reports/getRevenueSessions.ts b/src/queries/sql/reports/getRevenueSessions.ts
index 3f30a79ab..f7aa70dd0 100644
--- a/src/queries/sql/reports/getRevenueSessions.ts
+++ b/src/queries/sql/reports/getRevenueSessions.ts
@@ -1,4 +1,5 @@
import clickhouse from '@/lib/clickhouse';
+import { EVENT_TYPE } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import type { QueryFilters } from '@/lib/types';
@@ -64,6 +65,7 @@ async function relationalQuery(websiteId: string, currency: string, filters: Que
and upper(currency) = {{currency}}
) rev on rev.session_id = website_event.session_id
where website_event.website_id = {{websiteId::uuid}}
+ and website_event.event_type != ${EVENT_TYPE.performance}
${dateQuery}
${filterQuery}
${searchQuery}
@@ -126,6 +128,7 @@ async function clickhouseQuery(websiteId: string, currency: string, filters: Que
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
+ and event_type != ${EVENT_TYPE.performance}
${dateQuery}
${filterQuery}
${searchQuery}
diff --git a/src/queries/sql/reports/getRevenueStats.ts b/src/queries/sql/reports/getRevenueStats.ts
index 116166133..d3f0dbb46 100644
--- a/src/queries/sql/reports/getRevenueStats.ts
+++ b/src/queries/sql/reports/getRevenueStats.ts
@@ -2,7 +2,7 @@ import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import type { QueryFilters } from '@/lib/types';
-import type { RevenuParameters } from './getRevenue';
+import type { RevenuParameters } from './getRevenueChart';
export interface RevenueStatsResult {
sum: number;
diff --git a/src/queries/sql/sessions/createSession.ts b/src/queries/sql/sessions/createSession.ts
index 8d07a5540..f5e612ea9 100644
--- a/src/queries/sql/sessions/createSession.ts
+++ b/src/queries/sql/sessions/createSession.ts
@@ -1,12 +1,26 @@
import type { Prisma } from '@/generated/prisma/client';
+import { FIELD_LENGTH } from '@/lib/constants';
+import { truncateString } from '@/lib/format';
import prisma from '@/lib/prisma';
const FUNCTION_NAME = 'createSession';
export async function createSession(data: Prisma.SessionCreateInput) {
- const { rawQuery } = prisma;
+ const { writeRawQuery } = prisma;
+ const normalizedData: Prisma.SessionCreateInput = {
+ ...data,
+ browser: truncateString(data.browser, FIELD_LENGTH.browser),
+ os: truncateString(data.os, FIELD_LENGTH.os),
+ device: truncateString(data.device, FIELD_LENGTH.device),
+ screen: truncateString(data.screen, FIELD_LENGTH.screen),
+ language: truncateString(data.language, FIELD_LENGTH.language),
+ country: truncateString(data.country, FIELD_LENGTH.country),
+ region: truncateString(data.region, FIELD_LENGTH.region),
+ city: truncateString(data.city, FIELD_LENGTH.city),
+ distinctId: truncateString(data.distinctId, FIELD_LENGTH.distinctId),
+ };
- await rawQuery(
+ await writeRawQuery(
`
insert into session (
session_id,
@@ -38,7 +52,7 @@ export async function createSession(data: Prisma.SessionCreateInput) {
)
on conflict (session_id) do nothing
`,
- data,
+ normalizedData,
FUNCTION_NAME,
);
}
diff --git a/src/queries/sql/sessions/getLinkedDistinctIds.ts b/src/queries/sql/sessions/getLinkedDistinctIds.ts
new file mode 100644
index 000000000..d3d9953cb
--- /dev/null
+++ b/src/queries/sql/sessions/getLinkedDistinctIds.ts
@@ -0,0 +1,44 @@
+import clickhouse from '@/lib/clickhouse';
+import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import prisma from '@/lib/prisma';
+
+const FUNCTION_NAME = 'getLinkedDistinctIds';
+
+export async function getLinkedDistinctIds(
+ ...args: [websiteId: string, sessionId: string]
+): Promise {
+ return runQuery({
+ [PRISMA]: () => relationalQuery(...args),
+ [CLICKHOUSE]: () => clickhouseQuery(...args),
+ });
+}
+
+async function relationalQuery(websiteId: string, sessionId: string): Promise {
+ const { rawQuery } = prisma;
+
+ return rawQuery(
+ `
+ select distinct distinct_id as "distinctId"
+ from session_link
+ where website_id = {{websiteId::uuid}}
+ and session_id = {{sessionId::uuid}}
+ `,
+ { websiteId, sessionId },
+ FUNCTION_NAME,
+ ).then(result => (result as { distinctId: string }[]).map(({ distinctId }) => distinctId));
+}
+
+async function clickhouseQuery(websiteId: string, sessionId: string): Promise {
+ const { rawQuery } = clickhouse;
+
+ return rawQuery(
+ `
+ select distinct distinct_id as distinctId
+ from session_link
+ where website_id = {websiteId:UUID}
+ and session_id = {sessionId:UUID}
+ `,
+ { websiteId, sessionId },
+ FUNCTION_NAME,
+ ).then(result => (result as { distinctId: string }[]).map(({ distinctId }) => distinctId));
+}
diff --git a/src/queries/sql/sessions/getLinkedSessionIds.ts b/src/queries/sql/sessions/getLinkedSessionIds.ts
new file mode 100644
index 000000000..58f67651f
--- /dev/null
+++ b/src/queries/sql/sessions/getLinkedSessionIds.ts
@@ -0,0 +1,50 @@
+import clickhouse from '@/lib/clickhouse';
+import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import prisma from '@/lib/prisma';
+
+const FUNCTION_NAME = 'getLinkedSessionIds';
+
+export async function getLinkedSessionIds(
+ ...args: [websiteId: string, distinctId: string]
+): Promise<{ sessionId: string; createdAt: string }[]> {
+ return runQuery({
+ [PRISMA]: () => relationalQuery(...args),
+ [CLICKHOUSE]: () => clickhouseQuery(...args),
+ });
+}
+
+async function relationalQuery(websiteId: string, distinctId: string) {
+ const { rawQuery } = prisma;
+
+ return rawQuery(
+ `
+ select
+ session_id as "sessionId",
+ min(created_at) as "createdAt"
+ from session_link
+ where website_id = {{websiteId::uuid}}
+ and distinct_id = {{distinctId}}
+ group by session_id
+ `,
+ { websiteId, distinctId },
+ FUNCTION_NAME,
+ );
+}
+
+async function clickhouseQuery(websiteId: string, distinctId: string) {
+ const { rawQuery } = clickhouse;
+
+ return rawQuery(
+ `
+ select
+ session_id as sessionId,
+ min(created_at) as createdAt
+ from session_link
+ where website_id = {websiteId:UUID}
+ and distinct_id = {distinctId:String}
+ group by session_id
+ `,
+ { websiteId, distinctId },
+ FUNCTION_NAME,
+ );
+}
diff --git a/src/queries/sql/sessions/getSessionActivity.ts b/src/queries/sql/sessions/getSessionActivity.ts
index 1ac7e6ff8..c71b251b9 100644
--- a/src/queries/sql/sessions/getSessionActivity.ts
+++ b/src/queries/sql/sessions/getSessionActivity.ts
@@ -1,4 +1,5 @@
import clickhouse from '@/lib/clickhouse';
+import { EVENT_TYPE } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import type { QueryFilters } from '@/lib/types';
@@ -6,7 +7,7 @@ import type { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getSessionActivity';
export async function getSessionActivity(
- ...args: [websiteId: string, sessionId: string, filters: QueryFilters]
+ ...args: [websiteId: string, sessionIds: string[], filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -14,7 +15,7 @@ export async function getSessionActivity(
});
}
-async function relationalQuery(websiteId: string, sessionId: string, filters: QueryFilters) {
+async function relationalQuery(websiteId: string, sessionIds: string[], filters: QueryFilters) {
const { rawQuery } = prisma;
const { startDate, endDate } = filters;
@@ -36,17 +37,18 @@ async function relationalQuery(websiteId: string, sessionId: string, filters: Qu
and created_at between {{startDate}} and {{endDate}}) AS "hasData"
from website_event
where website_id = {{websiteId::uuid}}
- and session_id = {{sessionId::uuid}}
+ and session_id = any({{sessionIds}}::uuid[])
+ and event_type != ${EVENT_TYPE.performance}
and created_at between {{startDate}} and {{endDate}}
order by created_at desc
limit 500
`,
- { websiteId, sessionId, startDate, endDate },
+ { websiteId, sessionIds, startDate, endDate },
FUNCTION_NAME,
);
}
-async function clickhouseQuery(websiteId: string, sessionId: string, filters: QueryFilters) {
+async function clickhouseQuery(websiteId: string, sessionIds: string[], filters: QueryFilters) {
const { rawQuery } = clickhouse;
const { startDate, endDate } = filters;
@@ -62,19 +64,20 @@ async function clickhouseQuery(websiteId: string, sessionId: string, filters: Qu
event_name as eventName,
visit_id as visitId,
hostname,
- event_id IN (select event_id
- from event_data
- where website_id = {websiteId:UUID}
- and session_id = {sessionId:UUID}
+ event_id IN (select event_id
+ from event_data
+ where website_id = {websiteId:UUID}
+ and session_id in {sessionIds:Array(UUID)}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}) AS hasData
from website_event
where website_id = {websiteId:UUID}
- and session_id = {sessionId:UUID}
+ and session_id in {sessionIds:Array(UUID)}
+ and event_type != ${EVENT_TYPE.performance}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
order by created_at desc
limit 500
`,
- { websiteId, sessionId, startDate, endDate },
+ { websiteId, sessionIds, startDate, endDate },
FUNCTION_NAME,
);
}
diff --git a/src/queries/sql/sessions/getSessionActivityBounds.ts b/src/queries/sql/sessions/getSessionActivityBounds.ts
new file mode 100644
index 000000000..02debf8eb
--- /dev/null
+++ b/src/queries/sql/sessions/getSessionActivityBounds.ts
@@ -0,0 +1,62 @@
+import clickhouse from '@/lib/clickhouse';
+import { EVENT_TYPE } from '@/lib/constants';
+import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import prisma from '@/lib/prisma';
+
+const FUNCTION_NAME = 'getSessionActivityBounds';
+
+interface SessionActivityBounds {
+ firstAt: string | Date;
+ lastAt: string | Date;
+}
+
+export async function getSessionActivityBounds(
+ ...args: [websiteId: string, sessionIds: string[]]
+): Promise {
+ return runQuery({
+ [PRISMA]: () => relationalQuery(...args),
+ [CLICKHOUSE]: () => clickhouseQuery(...args),
+ });
+}
+
+async function relationalQuery(
+ websiteId: string,
+ sessionIds: string[],
+): Promise {
+ const { rawQuery } = prisma;
+
+ return rawQuery(
+ `
+ select
+ min(created_at) as "firstAt",
+ max(created_at) as "lastAt"
+ from website_event
+ where website_id = {{websiteId::uuid}}
+ and session_id = any({{sessionIds}}::uuid[])
+ and event_type != ${EVENT_TYPE.performance}
+ `,
+ { websiteId, sessionIds },
+ FUNCTION_NAME,
+ ).then(result => (result?.[0]?.firstAt && result?.[0]?.lastAt ? result[0] : null));
+}
+
+async function clickhouseQuery(
+ websiteId: string,
+ sessionIds: string[],
+): Promise {
+ const { rawQuery, getDateStringSQL } = clickhouse;
+
+ return rawQuery(
+ `
+ select
+ ${getDateStringSQL('min(created_at)')} as firstAt,
+ ${getDateStringSQL('max(created_at)')} as lastAt
+ from website_event
+ where website_id = {websiteId:UUID}
+ and session_id in {sessionIds:Array(UUID)}
+ and event_type != ${EVENT_TYPE.performance}
+ `,
+ { websiteId, sessionIds },
+ FUNCTION_NAME,
+ ).then(result => (result?.[0]?.firstAt && result?.[0]?.lastAt ? result[0] : null));
+}
diff --git a/src/queries/sql/sessions/getSessionDataActivityStats.ts b/src/queries/sql/sessions/getSessionDataActivityStats.ts
new file mode 100644
index 000000000..724ba4844
--- /dev/null
+++ b/src/queries/sql/sessions/getSessionDataActivityStats.ts
@@ -0,0 +1,198 @@
+import clickhouse from '@/lib/clickhouse';
+import { DATA_TYPE, EVENT_TYPE } from '@/lib/constants';
+import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import prisma from '@/lib/prisma';
+import type { PropertyFilter, PropertyLeaderboardRow, QueryFilters } from '@/lib/types';
+
+const FUNCTION_NAME = 'getSessionDataActivityStats';
+
+export async function getSessionDataActivityStats(
+ ...args: [
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters?: PropertyFilter[],
+ ]
+): Promise {
+ return runQuery({
+ [PRISMA]: () => relationalQuery(...args),
+ [CLICKHOUSE]: () => clickhouseQuery(...args),
+ });
+}
+
+async function relationalQuery(
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters: PropertyFilter[] = [],
+) {
+ const { timezone = 'utc' } = filters;
+ const { rawQuery, parseFilters, getPropertyFilterQuery } = prisma;
+ const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(
+ propertyFilters,
+ 'session',
+ timezone,
+ );
+
+ return rawQuery(
+ `
+ with filtered_sessions as (
+ select distinct website_event.session_id, website_event.website_id
+ from website_event
+ ${cohortQuery}
+ ${joinSessionQuery}
+ where website_event.website_id = {{websiteId::uuid}}
+ and website_event.created_at between {{startDate}} and {{endDate}}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ ${filterQuery}
+ ${pfSQL}
+ ),
+ session_rollup as (
+ select
+ website_event.session_id,
+ website_event.visit_id,
+ count(*) as activity,
+ sum(case when website_event.event_type = 1 then 1 else 0 end) as views,
+ sum(case when website_event.event_type = 2 then 1 else 0 end) as events,
+ min(website_event.created_at) as min_time,
+ max(website_event.created_at) as max_time
+ from website_event
+ join filtered_sessions
+ on filtered_sessions.session_id = website_event.session_id
+ and filtered_sessions.website_id = website_event.website_id
+ where website_event.website_id = {{websiteId::uuid}}
+ and website_event.created_at between {{startDate}} and {{endDate}}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ group by website_event.session_id, website_event.visit_id
+ ),
+ session_stats as (
+ select
+ session_id,
+ count(*) as visits,
+ sum(activity) as activity,
+ sum(views) as views,
+ sum(events) as events
+ from session_rollup
+ group by session_id
+ ),
+ property_values as (
+ select
+ session_data.session_id,
+ session_data.string_value as value
+ from session_data
+ join filtered_sessions
+ on filtered_sessions.session_id = session_data.session_id
+ and filtered_sessions.website_id = session_data.website_id
+ where session_data.website_id = {{websiteId::uuid}}
+ and session_data.data_key = {{propertyName}}
+ and session_data.data_type = ${DATA_TYPE.string}
+ and coalesce(session_data.string_value, '') != ''
+ )
+ select
+ property_values.value as label,
+ coalesce(sum(session_stats.activity), 0) as activity,
+ count(distinct property_values.session_id) as sessions,
+ coalesce(sum(session_stats.visits), 0) as visits,
+ coalesce(sum(session_stats.views), 0) as views,
+ coalesce(sum(session_stats.events), 0) as events
+ from property_values
+ join session_stats on session_stats.session_id = property_values.session_id
+ group by property_values.value
+ order by activity desc, property_values.value asc
+ limit 100
+ `,
+ { ...queryParams, websiteId, propertyName, ...pfParams },
+ FUNCTION_NAME,
+ );
+}
+
+async function clickhouseQuery(
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters: PropertyFilter[] = [],
+) {
+ const { timezone = 'UTC' } = filters;
+ const { rawQuery, parseFilters, getPropertyFilterQuery } = clickhouse;
+ const { filterQuery, cohortQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(
+ propertyFilters,
+ 'session',
+ timezone,
+ );
+
+ return rawQuery(
+ `
+ with filtered_sessions as (
+ select distinct website_event.session_id
+ from website_event
+ ${cohortQuery}
+ where website_event.website_id = {websiteId:UUID}
+ and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ ${filterQuery}
+ ${pfSQL}
+ ),
+ session_rollup as (
+ select
+ website_event.session_id,
+ website_event.visit_id,
+ count() as activity,
+ sumIf(1, website_event.event_type = 1) as views,
+ sumIf(1, website_event.event_type = 2) as events,
+ min(website_event.created_at) as min_time,
+ max(website_event.created_at) as max_time
+ from website_event
+ join filtered_sessions on filtered_sessions.session_id = website_event.session_id
+ where website_event.website_id = {websiteId:UUID}
+ and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ group by website_event.session_id, website_event.visit_id
+ ),
+ session_stats as (
+ select
+ session_id,
+ count() as visits,
+ sum(activity) as activity,
+ sum(views) as views,
+ sum(events) as events
+ from session_rollup
+ group by session_id
+ ),
+ property_values as (
+ select
+ session_data.session_id,
+ session_data.string_value as value
+ from session_data final
+ join filtered_sessions on filtered_sessions.session_id = session_data.session_id
+ where session_data.website_id = {websiteId:UUID}
+ and session_data.data_key = {propertyName:String}
+ and session_data.data_type = ${DATA_TYPE.string}
+ and ifNull(session_data.string_value, '') != ''
+ )
+ select
+ property_values.value as label,
+ ifNull(sum(session_stats.activity), 0) as activity,
+ uniq(property_values.session_id) as sessions,
+ ifNull(sum(session_stats.visits), 0) as visits,
+ ifNull(sum(session_stats.views), 0) as views,
+ ifNull(sum(session_stats.events), 0) as events
+ from property_values
+ join session_stats on session_stats.session_id = property_values.session_id
+ group by property_values.value
+ order by activity desc, property_values.value asc
+ limit 100
+ `,
+ { ...queryParams, websiteId, propertyName, ...pfParams },
+ FUNCTION_NAME,
+ );
+}
diff --git a/src/queries/sql/sessions/getSessionDataArraySeries.ts b/src/queries/sql/sessions/getSessionDataArraySeries.ts
new file mode 100644
index 000000000..840d6b468
--- /dev/null
+++ b/src/queries/sql/sessions/getSessionDataArraySeries.ts
@@ -0,0 +1,113 @@
+import clickhouse from '@/lib/clickhouse';
+import { DATA_TYPE, EVENT_TYPE } from '@/lib/constants';
+import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import prisma from '@/lib/prisma';
+import type { EventDataSeriesPoint, PropertyFilter, QueryFilters } from '@/lib/types';
+
+const FUNCTION_NAME = 'getSessionDataArraySeries';
+
+export async function getSessionDataArraySeries(
+ ...args: [
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters?: PropertyFilter[],
+ ]
+): Promise {
+ return runQuery({
+ [PRISMA]: () => relationalQuery(...args),
+ [CLICKHOUSE]: () => clickhouseQuery(...args),
+ });
+}
+
+async function relationalQuery(
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters: PropertyFilter[] = [],
+): Promise {
+ const { timezone = 'utc', unit = 'day' } = filters;
+ const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = prisma;
+ const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(
+ propertyFilters,
+ 'session',
+ timezone,
+ );
+
+ return rawQuery(
+ `
+ select
+ array_item.value as x,
+ ${getDateSQL('session_data.created_at', unit, timezone)} as t,
+ count(distinct session_data.session_id) as y
+ from website_event
+ ${cohortQuery}
+ ${joinSessionQuery}
+ join session_data
+ on session_data.session_id = website_event.session_id
+ and session_data.website_id = website_event.website_id
+ cross join lateral jsonb_array_elements_text(coalesce(session_data.string_value, '[]')::jsonb) as array_item(value)
+ where website_event.website_id = {{websiteId::uuid}}
+ and website_event.created_at between {{startDate}} and {{endDate}}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ and session_data.data_key = {{propertyName}}
+ and session_data.data_type = ${DATA_TYPE.array}
+ ${filterQuery}
+ ${pfSQL}
+ group by 1, 2
+ order by 2
+ `,
+ { ...queryParams, propertyName, ...pfParams },
+ FUNCTION_NAME,
+ );
+}
+
+async function clickhouseQuery(
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters: PropertyFilter[] = [],
+): Promise {
+ const { timezone = 'UTC', unit = 'day' } = filters;
+ const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = clickhouse;
+ const { filterQuery, cohortQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(
+ propertyFilters,
+ 'session',
+ timezone,
+ );
+
+ return rawQuery(
+ `
+ select
+ arrayJoin(JSONExtract(ifNull(session_data.string_value, '[]'), 'Array(String)')) as x,
+ ${getDateSQL('session_data.created_at', unit, timezone)} as t,
+ uniq(session_data.session_id) as y
+ from website_event
+ ${cohortQuery}
+ join session_data final
+ on session_data.session_id = website_event.session_id
+ and session_data.website_id = {websiteId:UUID}
+ where website_event.website_id = {websiteId:UUID}
+ and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ and session_data.data_key = {propertyName:String}
+ and session_data.data_type = ${DATA_TYPE.array}
+ ${filterQuery}
+ ${pfSQL}
+ group by x, t
+ order by t
+ `,
+ { ...queryParams, propertyName, ...pfParams },
+ FUNCTION_NAME,
+ );
+}
diff --git a/src/queries/sql/sessions/getSessionDataDateSeries.ts b/src/queries/sql/sessions/getSessionDataDateSeries.ts
new file mode 100644
index 000000000..8c17bf929
--- /dev/null
+++ b/src/queries/sql/sessions/getSessionDataDateSeries.ts
@@ -0,0 +1,110 @@
+import clickhouse from '@/lib/clickhouse';
+import { DATA_TYPE, EVENT_TYPE } from '@/lib/constants';
+import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import prisma from '@/lib/prisma';
+import type { EventDataDateSeriesPoint, PropertyFilter, QueryFilters } from '@/lib/types';
+
+const FUNCTION_NAME = 'getSessionDataDateSeries';
+
+export async function getSessionDataDateSeries(
+ ...args: [
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters?: PropertyFilter[],
+ ]
+): Promise {
+ return runQuery({
+ [PRISMA]: () => relationalQuery(...args),
+ [CLICKHOUSE]: () => clickhouseQuery(...args),
+ });
+}
+
+async function relationalQuery(
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters: PropertyFilter[] = [],
+): Promise {
+ const { timezone = 'utc' } = filters;
+ const { rawQuery, parseFilters, getDateStringSQL, getPropertyFilterQuery } = prisma;
+ const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(
+ propertyFilters,
+ 'session',
+ timezone,
+ );
+
+ return rawQuery(
+ `
+ select
+ ${getDateStringSQL('session_data.date_value', 'second', timezone)} as t,
+ count(distinct session_data.session_id) as y
+ from website_event
+ ${cohortQuery}
+ ${joinSessionQuery}
+ join session_data
+ on session_data.session_id = website_event.session_id
+ and session_data.website_id = website_event.website_id
+ where website_event.website_id = {{websiteId::uuid}}
+ and website_event.created_at between {{startDate}} and {{endDate}}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ and session_data.data_key = {{propertyName}}
+ and session_data.data_type = ${DATA_TYPE.date}
+ ${filterQuery}
+ ${pfSQL}
+ group by 1
+ order by 1
+ `,
+ { ...queryParams, propertyName, ...pfParams },
+ FUNCTION_NAME,
+ );
+}
+
+async function clickhouseQuery(
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters: PropertyFilter[] = [],
+): Promise {
+ const { timezone = 'UTC' } = filters;
+ const { rawQuery, parseFilters, getDateStringSQL, getPropertyFilterQuery } = clickhouse;
+ const { filterQuery, cohortQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(
+ propertyFilters,
+ 'session',
+ timezone,
+ );
+
+ return rawQuery(
+ `
+ select
+ ${getDateStringSQL('session_data.date_value', 'second', timezone)} as t,
+ uniq(session_data.session_id) as y
+ from website_event
+ ${cohortQuery}
+ join session_data final
+ on session_data.session_id = website_event.session_id
+ and session_data.website_id = {websiteId:UUID}
+ where website_event.website_id = {websiteId:UUID}
+ and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ and session_data.data_key = {propertyName:String}
+ and session_data.data_type = ${DATA_TYPE.date}
+ ${filterQuery}
+ ${pfSQL}
+ group by t
+ order by t
+ `,
+ { ...queryParams, propertyName, ...pfParams },
+ FUNCTION_NAME,
+ );
+}
diff --git a/src/queries/sql/sessions/getSessionDataNumericSeries.ts b/src/queries/sql/sessions/getSessionDataNumericSeries.ts
new file mode 100644
index 000000000..64f74fc25
--- /dev/null
+++ b/src/queries/sql/sessions/getSessionDataNumericSeries.ts
@@ -0,0 +1,134 @@
+import clickhouse from '@/lib/clickhouse';
+import { EVENT_TYPE } from '@/lib/constants';
+import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import prisma from '@/lib/prisma';
+import type { PropertyFilter, QueryFilters } from '@/lib/types';
+
+const FUNCTION_NAME = 'getSessionDataNumericSeries';
+
+export async function getSessionDataNumericSeries(
+ ...args: [
+ websiteId: string,
+ propertyName: string,
+ metric: 'sum' | 'avg' | 'count',
+ filters: QueryFilters,
+ propertyFilters?: PropertyFilter[],
+ ]
+) {
+ return runQuery({
+ [PRISMA]: () => relationalQuery(...args),
+ [CLICKHOUSE]: () => clickhouseQuery(...args),
+ });
+}
+
+async function relationalQuery(
+ websiteId: string,
+ propertyName: string,
+ metric: 'sum' | 'avg' | 'count',
+ filters: QueryFilters,
+ propertyFilters: PropertyFilter[] = [],
+) {
+ const { timezone = 'utc', unit = 'day' } = filters;
+ const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = prisma;
+ const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(
+ propertyFilters,
+ 'session',
+ timezone,
+ );
+ const aggSql =
+ metric === 'avg'
+ ? 'avg(cast(session_data.number_value as decimal))'
+ : metric === 'count'
+ ? 'count(distinct session_data.session_id)'
+ : 'sum(cast(session_data.number_value as decimal))';
+
+ return rawQuery(
+ `
+ with filtered_sessions as (
+ select distinct website_event.session_id, website_event.website_id
+ from website_event
+ ${cohortQuery}
+ ${joinSessionQuery}
+ where website_event.website_id = {{websiteId::uuid}}
+ and website_event.created_at between {{startDate}} and {{endDate}}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ ${filterQuery}
+ ${pfSQL}
+ )
+ select
+ ${getDateSQL('session_data.created_at', unit, timezone)} t,
+ ${aggSql} y
+ from session_data
+ join filtered_sessions
+ on filtered_sessions.session_id = session_data.session_id
+ and filtered_sessions.website_id = session_data.website_id
+ where session_data.website_id = {{websiteId::uuid}}
+ and session_data.data_key = {{propertyName}}
+ and session_data.data_type = 2
+ group by 1
+ order by 1
+ `,
+ { ...queryParams, propertyName, ...pfParams },
+ FUNCTION_NAME,
+ );
+}
+
+async function clickhouseQuery(
+ websiteId: string,
+ propertyName: string,
+ metric: 'sum' | 'avg' | 'count',
+ filters: QueryFilters,
+ propertyFilters: PropertyFilter[] = [],
+): Promise<{ t: string; y: number }[]> {
+ const { timezone = 'UTC', unit = 'day' } = filters;
+ const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = clickhouse;
+ const { filterQuery, cohortQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(
+ propertyFilters,
+ 'session',
+ timezone,
+ );
+ const aggSql =
+ metric === 'avg'
+ ? 'avg(session_data.number_value)'
+ : metric === 'count'
+ ? 'uniq(session_data.session_id)'
+ : 'sum(session_data.number_value)';
+
+ return rawQuery(
+ `
+ with filtered_sessions as (
+ select distinct website_event.session_id
+ from website_event
+ ${cohortQuery}
+ where website_event.website_id = {websiteId:UUID}
+ and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ ${filterQuery}
+ ${pfSQL}
+ )
+ select
+ ${getDateSQL('session_data.created_at', unit, timezone)} as t,
+ ${aggSql} as y
+ from session_data final
+ join filtered_sessions
+ on filtered_sessions.session_id = session_data.session_id
+ where session_data.website_id = {websiteId:UUID}
+ and session_data.data_key = {propertyName:String}
+ and session_data.data_type = 2
+ group by t
+ order by t
+ `,
+ { ...queryParams, propertyName, ...pfParams },
+ FUNCTION_NAME,
+ );
+}
diff --git a/src/queries/sql/sessions/getSessionDataNumericStats.ts b/src/queries/sql/sessions/getSessionDataNumericStats.ts
new file mode 100644
index 000000000..c7dfdad28
--- /dev/null
+++ b/src/queries/sql/sessions/getSessionDataNumericStats.ts
@@ -0,0 +1,121 @@
+import clickhouse from '@/lib/clickhouse';
+import { EVENT_TYPE } from '@/lib/constants';
+import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import prisma from '@/lib/prisma';
+import type { EventDataNumericStats, PropertyFilter, QueryFilters } from '@/lib/types';
+
+const FUNCTION_NAME = 'getSessionDataNumericStats';
+
+export async function getSessionDataNumericStats(
+ ...args: [
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters?: PropertyFilter[],
+ ]
+): Promise {
+ return runQuery({
+ [PRISMA]: () => relationalQuery(...args),
+ [CLICKHOUSE]: () => clickhouseQuery(...args),
+ }).then(results => results?.[0]);
+}
+
+async function relationalQuery(
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters: PropertyFilter[] = [],
+) {
+ const { timezone = 'utc' } = filters;
+ const { rawQuery, parseFilters, getPropertyFilterQuery } = prisma;
+ const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(
+ propertyFilters,
+ 'session',
+ timezone,
+ );
+
+ return rawQuery(
+ `
+ with filtered_sessions as (
+ select distinct website_event.session_id, website_event.website_id
+ from website_event
+ ${cohortQuery}
+ ${joinSessionQuery}
+ where website_event.website_id = {{websiteId::uuid}}
+ and website_event.created_at between {{startDate}} and {{endDate}}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ ${filterQuery}
+ ${pfSQL}
+ )
+ select
+ coalesce(sum(cast(session_data.number_value as decimal)), 0) as "total",
+ coalesce(avg(cast(session_data.number_value as decimal)), 0) as "average",
+ coalesce(percentile_cont(0.5) within group (order by session_data.number_value), 0) as "median",
+ coalesce(max(session_data.number_value), 0) as "max",
+ coalesce(min(session_data.number_value), 0) as "min"
+ from session_data
+ join filtered_sessions
+ on filtered_sessions.session_id = session_data.session_id
+ and filtered_sessions.website_id = session_data.website_id
+ where session_data.website_id = {{websiteId::uuid}}
+ and session_data.data_key = {{propertyName}}
+ and session_data.data_type = 2
+ `,
+ { ...queryParams, propertyName, ...pfParams },
+ FUNCTION_NAME,
+ );
+}
+
+async function clickhouseQuery(
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters: PropertyFilter[] = [],
+): Promise {
+ const { timezone = 'UTC' } = filters;
+ const { rawQuery, parseFilters, getPropertyFilterQuery } = clickhouse;
+ const { filterQuery, cohortQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(
+ propertyFilters,
+ 'session',
+ timezone,
+ );
+
+ return rawQuery(
+ `
+ with filtered_sessions as (
+ select distinct website_event.session_id
+ from website_event
+ ${cohortQuery}
+ where website_event.website_id = {websiteId:UUID}
+ and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ ${filterQuery}
+ ${pfSQL}
+ )
+ select
+ if(count() = 0, 0, sum(session_data.number_value)) as total,
+ if(count() = 0, 0, avg(session_data.number_value)) as average,
+ if(count() = 0, 0, median(session_data.number_value)) as median,
+ if(count() = 0, 0, max(session_data.number_value)) as max,
+ if(count() = 0, 0, min(session_data.number_value)) as min
+ from session_data final
+ join filtered_sessions
+ on filtered_sessions.session_id = session_data.session_id
+ where session_data.website_id = {websiteId:UUID}
+ and session_data.data_key = {propertyName:String}
+ and session_data.data_type = 2
+ `,
+ { ...queryParams, propertyName, ...pfParams },
+ FUNCTION_NAME,
+ );
+}
diff --git a/src/queries/sql/sessions/getSessionDataPivot.ts b/src/queries/sql/sessions/getSessionDataPivot.ts
new file mode 100644
index 000000000..f8e313ba9
--- /dev/null
+++ b/src/queries/sql/sessions/getSessionDataPivot.ts
@@ -0,0 +1,204 @@
+import clickhouse from '@/lib/clickhouse';
+import { EVENT_TYPE } from '@/lib/constants';
+import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import prisma from '@/lib/prisma';
+import type { PageResult, PropertyFilter, QueryFilters, SessionDataPivotRow } from '@/lib/types';
+
+const FUNCTION_NAME = 'getSessionDataPivot';
+
+export async function getSessionDataPivot(
+ ...args: [
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters?: PropertyFilter[],
+ ]
+): Promise> {
+ return runQuery({
+ [PRISMA]: () => relationalQuery(...args),
+ [CLICKHOUSE]: () => clickhouseQuery(...args),
+ });
+}
+
+async function relationalQuery(
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters: PropertyFilter[] = [],
+) {
+ const { timezone = 'utc' } = filters;
+ const { pagedRawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = prisma;
+
+ const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(
+ propertyFilters,
+ 'session',
+ timezone,
+ );
+
+ return pagedRawQuery(
+ `
+ with filtered_sessions as (
+ select distinct website_event.session_id
+ from website_event
+ ${cohortQuery}
+ ${joinSessionQuery}
+ where website_event.website_id = {{websiteId::uuid}}
+ and website_event.created_at between {{startDate}} and {{endDate}}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ ${filterQuery}
+ ${pfSQL}
+ ),
+ latest_session_properties as (
+ select
+ ranked.session_id,
+ ranked.distinct_id,
+ ranked.created_at,
+ ranked.data_key,
+ ranked.data_type,
+ ranked.string_value,
+ ranked.number_value,
+ ranked.date_value
+ from (
+ select
+ session_data.session_id,
+ session_data.distinct_id,
+ session_data.created_at,
+ session_data.data_key,
+ session_data.data_type,
+ session_data.string_value,
+ session_data.number_value,
+ session_data.date_value,
+ row_number() over (
+ partition by session_data.session_id, session_data.data_key
+ order by session_data.created_at desc, session_data.session_data_id desc
+ ) as row_num
+ from session_data
+ join filtered_sessions
+ on filtered_sessions.session_id = session_data.session_id
+ where session_data.website_id = {{websiteId::uuid}}
+ ) ranked
+ where ranked.row_num = 1
+ ),
+ paged_sessions as (
+ select
+ latest_session_properties.session_id,
+ latest_session_properties.created_at as sort_created_at
+ from latest_session_properties
+ where latest_session_properties.data_key = {{propertyName}}
+ )
+ select
+ latest_session_properties.session_id as "sessionId",
+ coalesce(max(latest_session_properties.distinct_id), '') as "distinctId",
+ max(latest_session_properties.created_at) as "createdAt",
+ array_agg(latest_session_properties.data_key order by latest_session_properties.data_key asc) as "propertyKeys",
+ array_agg(
+ coalesce(
+ case when latest_session_properties.data_type = 1 then latest_session_properties.string_value end,
+ case when latest_session_properties.data_type = 2 then cast(latest_session_properties.number_value as varchar) end,
+ case when latest_session_properties.data_type = 3 then latest_session_properties.string_value end,
+ case when latest_session_properties.data_type = 4 then ${getDateStringSQL('latest_session_properties.date_value', 'second', timezone)} end,
+ case when latest_session_properties.data_type = 5 then latest_session_properties.string_value end,
+ ''
+ )
+ order by latest_session_properties.data_key asc
+ ) as "propertyValues"
+ from latest_session_properties
+ join paged_sessions
+ on paged_sessions.session_id = latest_session_properties.session_id
+ group by latest_session_properties.session_id, paged_sessions.sort_created_at
+ order by paged_sessions.sort_created_at desc
+ `,
+ { ...queryParams, websiteId, propertyName, ...pfParams },
+ filters,
+ FUNCTION_NAME,
+ );
+}
+
+async function clickhouseQuery(
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters: PropertyFilter[] = [],
+) {
+ const { timezone = 'UTC' } = filters;
+ const { pagedRawQuery, parseFilters, getPropertyFilterQuery, getDateStringSQL } = clickhouse;
+
+ const { filterQuery, cohortQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(
+ propertyFilters,
+ 'session',
+ timezone,
+ );
+
+ return pagedRawQuery(
+ `
+ with filtered_sessions as (
+ select distinct website_event.session_id
+ from website_event
+ ${cohortQuery}
+ where website_event.website_id = {websiteId:UUID}
+ and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ ${filterQuery}
+ ${pfSQL}
+ ),
+ latest_session_properties as (
+ select
+ session_data.session_id as session_id,
+ argMax(ifNull(session_data.distinct_id, ''), session_data.created_at) as distinct_id,
+ session_data.data_key as data_key,
+ argMax(session_data.data_type, session_data.created_at) as data_type,
+ argMax(session_data.string_value, session_data.created_at) as string_value,
+ argMax(session_data.number_value, session_data.created_at) as number_value,
+ argMax(session_data.date_value, session_data.created_at) as date_value,
+ max(session_data.created_at) as created_at
+ from session_data final
+ join filtered_sessions
+ on filtered_sessions.session_id = session_data.session_id
+ where session_data.website_id = {websiteId:UUID}
+ group by
+ session_data.session_id,
+ session_data.data_key
+ ),
+ paged_sessions as (
+ select
+ latest_session_properties.session_id,
+ latest_session_properties.created_at as sort_created_at
+ from latest_session_properties
+ where latest_session_properties.data_key = {propertyName:String}
+ )
+ select
+ latest_session_properties.session_id as sessionId,
+ max(latest_session_properties.distinct_id) as distinctId,
+ max(latest_session_properties.created_at) as createdAt,
+ groupArray(latest_session_properties.data_key) as propertyKeys,
+ groupArray(
+ multiIf(
+ latest_session_properties.data_type IN (1, 3, 5), ifNull(latest_session_properties.string_value, ''),
+ latest_session_properties.data_type = 2, toString(ifNull(latest_session_properties.number_value, 0)),
+ latest_session_properties.data_type = 4, ${getDateStringSQL('latest_session_properties.date_value', 'second', timezone)},
+ ''
+ )
+ ) as propertyValues
+ from latest_session_properties
+ join paged_sessions
+ on paged_sessions.session_id = latest_session_properties.session_id
+ group by
+ latest_session_properties.session_id,
+ paged_sessions.sort_created_at
+ order by paged_sessions.sort_created_at desc
+ `,
+ { ...queryParams, websiteId, propertyName, ...pfParams },
+ filters,
+ FUNCTION_NAME,
+ );
+}
diff --git a/src/queries/sql/sessions/getSessionDataProperties.ts b/src/queries/sql/sessions/getSessionDataProperties.ts
index 9b429f9b9..1d81c7bd0 100644
--- a/src/queries/sql/sessions/getSessionDataProperties.ts
+++ b/src/queries/sql/sessions/getSessionDataProperties.ts
@@ -1,12 +1,18 @@
import clickhouse from '@/lib/clickhouse';
+import { EVENT_TYPE } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
-import type { QueryFilters } from '@/lib/types';
+import type { PropertyFilter, QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getSessionDataProperties';
export async function getSessionDataProperties(
- ...args: [websiteId: string, filters: QueryFilters]
+ ...args: [
+ websiteId: string,
+ filters: QueryFilters,
+ propertyFilters?: PropertyFilter[],
+ propertyName?: string,
+ ]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -14,32 +20,59 @@ export async function getSessionDataProperties(
});
}
-async function relationalQuery(websiteId: string, filters: QueryFilters) {
- const { rawQuery, parseFilters } = prisma;
+async function relationalQuery(
+ websiteId: string,
+ filters: QueryFilters,
+ propertyFilters: PropertyFilter[] = [],
+ propertyName?: string,
+) {
+ const { timezone = 'utc' } = filters;
+ const { rawQuery, parseFilters, getPropertyFilterQuery } = prisma;
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
+ timezone,
});
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(
+ propertyFilters,
+ 'session',
+ timezone,
+ );
return rawQuery(
`
+ with filtered_sessions as (
+ select distinct website_event.session_id, website_event.website_id
+ from website_event
+ ${cohortQuery}
+ ${joinSessionQuery}
+ where website_event.website_id = {{websiteId::uuid}}
+ and website_event.created_at between {{startDate}} and {{endDate}}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ ${filterQuery}
+ ${pfSQL}
+ ),
+ selected_property_sessions as (
+ select distinct session_data.session_id, session_data.website_id
+ from session_data
+ join filtered_sessions
+ on filtered_sessions.session_id = session_data.session_id
+ and filtered_sessions.website_id = session_data.website_id
+ ${propertyName ? 'where session_data.data_key = {{propertyName}}' : ''}
+ )
select
data_key as "propertyName",
+ data_type as "dataType",
count(distinct session_data.session_id) as "total"
- from website_event
- ${cohortQuery}
- ${joinSessionQuery}
+ from selected_property_sessions
join session_data
- on session_data.session_id = website_event.session_id
- and session_data.website_id = website_event.website_id
- where website_event.website_id = {{websiteId::uuid}}
- and website_event.created_at between {{startDate}} and {{endDate}}
- ${filterQuery}
- group by 1
- order by 2 desc
+ on session_data.session_id = selected_property_sessions.session_id
+ and session_data.website_id = selected_property_sessions.website_id
+ group by 1, 2
+ order by 3 desc, 1 asc
limit 500
`,
- queryParams,
+ { ...queryParams, websiteId, propertyName, ...pfParams },
FUNCTION_NAME,
);
}
@@ -47,29 +80,57 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
async function clickhouseQuery(
websiteId: string,
filters: QueryFilters,
-): Promise<{ propertyName: string; total: number }[]> {
- const { rawQuery, parseFilters } = clickhouse;
- const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
+ propertyFilters: PropertyFilter[] = [],
+ propertyName?: string,
+): Promise<{ propertyName: string; dataType: number; total: number }[]> {
+ const { timezone = 'UTC' } = filters;
+ const { rawQuery, parseFilters, getPropertyFilterQuery } = clickhouse;
+ const { filterQuery, cohortQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(
+ propertyFilters,
+ 'session',
+ timezone,
+ );
return rawQuery(
`
+ with filtered_sessions as (
+ select distinct website_event.session_id, website_event.website_id
+ from website_event
+ ${cohortQuery}
+ where website_event.website_id = {websiteId:UUID}
+ and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ ${filterQuery}
+ ${pfSQL}
+ ),
+ selected_property_sessions as (
+ select distinct session_data.session_id, session_data.website_id
+ from session_data final
+ join filtered_sessions
+ on filtered_sessions.session_id = session_data.session_id
+ and filtered_sessions.website_id = session_data.website_id
+ ${propertyName ? 'where session_data.data_key = {propertyName:String}' : ''}
+ )
select
data_key as propertyName,
+ data_type as dataType,
count(distinct session_data.session_id) as total
- from website_event
- ${cohortQuery}
+ from selected_property_sessions
join session_data final
- on session_data.session_id = website_event.session_id
- and session_data.website_id = {websiteId:UUID}
- where website_event.website_id = {websiteId:UUID}
- and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ on session_data.session_id = selected_property_sessions.session_id
+ and session_data.website_id = selected_property_sessions.website_id
+ where session_data.website_id = {websiteId:UUID}
and session_data.data_key != ''
- ${filterQuery}
- group by 1
- order by 2 desc
+ group by 1, 2
+ order by 3 desc, 1 asc
limit 500
`,
- queryParams,
+ { ...queryParams, websiteId, propertyName, ...pfParams },
FUNCTION_NAME,
);
}
diff --git a/src/queries/sql/sessions/getSessionDataPropertySeries.ts b/src/queries/sql/sessions/getSessionDataPropertySeries.ts
new file mode 100644
index 000000000..f8ba5be1d
--- /dev/null
+++ b/src/queries/sql/sessions/getSessionDataPropertySeries.ts
@@ -0,0 +1,112 @@
+import clickhouse from '@/lib/clickhouse';
+import { DATA_TYPE, EVENT_TYPE } from '@/lib/constants';
+import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import prisma from '@/lib/prisma';
+import type { EventDataSeriesPoint, PropertyFilter, QueryFilters } from '@/lib/types';
+
+const FUNCTION_NAME = 'getSessionDataPropertySeries';
+
+export async function getSessionDataPropertySeries(
+ ...args: [
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters?: PropertyFilter[],
+ ]
+): Promise {
+ return runQuery({
+ [PRISMA]: () => relationalQuery(...args),
+ [CLICKHOUSE]: () => clickhouseQuery(...args),
+ });
+}
+
+async function relationalQuery(
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters: PropertyFilter[] = [],
+): Promise {
+ const { timezone = 'utc', unit = 'day' } = filters;
+ const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = prisma;
+ const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(
+ propertyFilters,
+ 'session',
+ timezone,
+ );
+
+ return rawQuery(
+ `
+ select
+ session_data.string_value as x,
+ ${getDateSQL('session_data.created_at', unit, timezone)} t,
+ count(distinct session_data.session_id) y
+ from website_event
+ ${cohortQuery}
+ ${joinSessionQuery}
+ join session_data
+ on session_data.session_id = website_event.session_id
+ and session_data.website_id = website_event.website_id
+ where website_event.website_id = {{websiteId::uuid}}
+ and website_event.created_at between {{startDate}} and {{endDate}}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ and session_data.data_key = {{propertyName}}
+ and session_data.data_type in (${DATA_TYPE.string}, ${DATA_TYPE.boolean})
+ ${filterQuery}
+ ${pfSQL}
+ group by 1, 2
+ order by 2
+ `,
+ { ...queryParams, propertyName, ...pfParams },
+ FUNCTION_NAME,
+ );
+}
+
+async function clickhouseQuery(
+ websiteId: string,
+ propertyName: string,
+ filters: QueryFilters,
+ propertyFilters: PropertyFilter[] = [],
+): Promise {
+ const { timezone = 'UTC', unit = 'day' } = filters;
+ const { rawQuery, getDateSQL, parseFilters, getPropertyFilterQuery } = clickhouse;
+ const { filterQuery, cohortQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ timezone,
+ });
+ const { sql: pfSQL, params: pfParams } = getPropertyFilterQuery(
+ propertyFilters,
+ 'session',
+ timezone,
+ );
+
+ return rawQuery(
+ `
+ select
+ session_data.string_value as x,
+ ${getDateSQL('session_data.created_at', unit, timezone)} as t,
+ uniq(session_data.session_id) as y
+ from website_event
+ ${cohortQuery}
+ join session_data final
+ on session_data.session_id = website_event.session_id
+ and session_data.website_id = {websiteId:UUID}
+ where website_event.website_id = {websiteId:UUID}
+ and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ and session_data.data_key = {propertyName:String}
+ and session_data.data_type in (${DATA_TYPE.string}, ${DATA_TYPE.boolean})
+ ${filterQuery}
+ ${pfSQL}
+ group by x, t
+ order by t
+ `,
+ { ...queryParams, propertyName, ...pfParams },
+ FUNCTION_NAME,
+ );
+}
diff --git a/src/queries/sql/sessions/getSessionDataValues.ts b/src/queries/sql/sessions/getSessionDataValues.ts
index 5790141c5..d862fa66a 100644
--- a/src/queries/sql/sessions/getSessionDataValues.ts
+++ b/src/queries/sql/sessions/getSessionDataValues.ts
@@ -1,4 +1,5 @@
import clickhouse from '@/lib/clickhouse';
+import { DATA_TYPE, EVENT_TYPE } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import type { QueryFilters } from '@/lib/types';
@@ -6,7 +7,7 @@ import type { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getSessionDataValues';
export async function getSessionDataValues(
- ...args: [websiteId: string, filters: QueryFilters & { propertyName?: string }]
+ ...args: [websiteId: string, filters: QueryFilters & { propertyName?: string; dataType?: number }]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -16,14 +17,43 @@ export async function getSessionDataValues(
async function relationalQuery(
websiteId: string,
- filters: QueryFilters & { propertyName?: string },
+ filters: QueryFilters & { propertyName?: string; dataType?: number },
) {
const { rawQuery, parseFilters, getDateSQL } = prisma;
+ const { dataType } = filters;
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
+ if (dataType === DATA_TYPE.array) {
+ return rawQuery(
+ `
+ select
+ array_item.value as "value",
+ count(*) as "total"
+ from website_event
+ ${cohortQuery}
+ ${joinSessionQuery}
+ join session_data
+ on session_data.session_id = website_event.session_id
+ and session_data.website_id = website_event.website_id
+ cross join lateral jsonb_array_elements_text(coalesce(session_data.string_value, '[]')::jsonb) as array_item(value)
+ where website_event.website_id = {{websiteId::uuid}}
+ and website_event.created_at between {{startDate}} and {{endDate}}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ and session_data.data_key = {{propertyName}}
+ and session_data.data_type = ${DATA_TYPE.array}
+ ${filterQuery}
+ group by array_item.value
+ order by 2 desc
+ limit 500
+ `,
+ queryParams,
+ FUNCTION_NAME,
+ );
+ }
+
return rawQuery(
`
select
@@ -32,7 +62,7 @@ async function relationalQuery(
when data_type = 4 then ${getDateSQL('date_value', 'hour')}
else string_value
end as "value",
- count(distinct session_data.session_id) as "total"
+ count(*) as "total"
from website_event
${cohortQuery}
${joinSessionQuery}
@@ -41,11 +71,13 @@ async function relationalQuery(
and session_data.website_id = website_event.website_id
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
+ and website_event.event_type != ${EVENT_TYPE.performance}
and session_data.data_key = {{propertyName}}
+ ${dataType ? `and session_data.data_type = ${dataType}` : ''}
${filterQuery}
group by value
order by 2 desc
- limit 100
+ limit 500
`,
queryParams,
FUNCTION_NAME,
@@ -54,18 +86,45 @@ async function relationalQuery(
async function clickhouseQuery(
websiteId: string,
- filters: QueryFilters & { propertyName?: string },
+ filters: QueryFilters & { propertyName?: string; dataType?: number },
): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
+ const { dataType } = filters;
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
+ if (dataType === DATA_TYPE.array) {
+ return rawQuery(
+ `
+ select
+ arrayJoin(JSONExtract(ifNull(session_data.string_value, '[]'), 'Array(String)')) as "value",
+ count() as "total"
+ from website_event
+ ${cohortQuery}
+ join session_data final
+ on session_data.session_id = website_event.session_id
+ and session_data.website_id = {websiteId:UUID}
+ where website_event.website_id = {websiteId:UUID}
+ and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and website_event.event_type != ${EVENT_TYPE.performance}
+ and session_data.data_key = {propertyName:String}
+ and session_data.data_type = ${DATA_TYPE.array}
+ ${filterQuery}
+ group by value
+ order by 2 desc
+ limit 500
+ `,
+ queryParams,
+ FUNCTION_NAME,
+ );
+ }
+
return rawQuery(
`
select
multiIf(data_type = 2, replaceAll(string_value, '.0000', ''),
data_type = 4, toString(date_trunc('hour', date_value)),
string_value) as "value",
- uniq(session_data.session_id) as "total"
+ count() as "total"
from website_event
${cohortQuery}
join session_data final
@@ -73,11 +132,13 @@ async function clickhouseQuery(
and session_data.website_id = {websiteId:UUID}
where website_event.website_id = {websiteId:UUID}
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and website_event.event_type != ${EVENT_TYPE.performance}
and session_data.data_key = {propertyName:String}
+ ${dataType ? `and session_data.data_type = ${dataType}` : ''}
${filterQuery}
group by value
order by 2 desc
- limit 100
+ limit 500
`,
queryParams,
FUNCTION_NAME,
diff --git a/src/queries/sql/sessions/getWebsiteSession.ts b/src/queries/sql/sessions/getWebsiteSession.ts
index 3c1608713..3ad2c1677 100644
--- a/src/queries/sql/sessions/getWebsiteSession.ts
+++ b/src/queries/sql/sessions/getWebsiteSession.ts
@@ -1,4 +1,5 @@
import clickhouse from '@/lib/clickhouse';
+import { EVENT_TYPE } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
@@ -54,6 +55,7 @@ async function relationalQuery(websiteId: string, sessionId: string) {
join website_event on website_event.session_id = session.session_id
where session.website_id = {{websiteId::uuid}}
and session.session_id = {{sessionId::uuid}}
+ and website_event.event_type != ${EVENT_TYPE.performance}
group by session.session_id, session.distinct_id, visit_id, session.website_id, session.browser, session.os, session.device, session.screen, session.language, session.country, session.region, session.city) t
group by id, distinct_id, website_id, browser, os, device, screen, language, country, region, city;
`,
@@ -68,16 +70,16 @@ async function clickhouseQuery(websiteId: string, sessionId: string) {
return rawQuery(
`
select id,
- websiteId,
- distinctId,
- browser,
- os,
- device,
- screen,
- language,
- country,
- region,
- city,
+ any(websiteId) as websiteId,
+ argMax(distinctId, max_time) as distinctId,
+ argMax(browser, max_time) as browser,
+ argMax(os, max_time) as os,
+ argMax(device, max_time) as device,
+ argMax(screen, max_time) as screen,
+ argMax(language, max_time) as language,
+ argMax(country, max_time) as country,
+ argMax(region, max_time) as region,
+ argMax(city, max_time) as city,
${getDateStringSQL('min(min_time)')} as firstAt,
${getDateStringSQL('max(max_time)')} as lastAt,
uniq(visit_id) visits,
@@ -104,8 +106,9 @@ async function clickhouseQuery(websiteId: string, sessionId: string) {
from website_event_stats_hourly
where website_id = {websiteId:UUID}
and session_id = {sessionId:UUID}
+ and event_type != ${EVENT_TYPE.performance}
group by session_id, distinct_id, visit_id, website_id, browser, os, device, screen, language, country, region, city) t
- group by id, websiteId, distinctId, browser, os, device, screen, language, country, region, city;
+ group by id;
`,
{ websiteId, sessionId },
FUNCTION_NAME,
diff --git a/src/queries/sql/sessions/getWebsiteSessionStats.ts b/src/queries/sql/sessions/getWebsiteSessionStats.ts
index a4615ee40..a14fbc5f3 100644
--- a/src/queries/sql/sessions/getWebsiteSessionStats.ts
+++ b/src/queries/sql/sessions/getWebsiteSessionStats.ts
@@ -1,5 +1,5 @@
import clickhouse from '@/lib/clickhouse';
-import { EVENT_COLUMNS } from '@/lib/constants';
+import { EVENT_COLUMNS, EVENT_TYPE } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import type { QueryFilters } from '@/lib/types';
@@ -36,7 +36,7 @@ async function relationalQuery(
return rawQuery(
`
select
- count(*) as "pageviews",
+ sum(case when website_event.event_type = 1 then 1 else 0 end) as "pageviews",
count(distinct website_event.session_id) as "visitors",
count(distinct website_event.visit_id) as "visits",
count(distinct session.country) as "countries",
@@ -47,6 +47,7 @@ async function relationalQuery(
and website_event.website_id = session.website_id
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
+ and website_event.event_type != ${EVENT_TYPE.performance}
${filterQuery}
`,
queryParams,
@@ -75,6 +76,7 @@ async function clickhouseQuery(
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_type != ${EVENT_TYPE.performance}
${filterQuery}
`;
} else {
@@ -89,6 +91,7 @@ async function clickhouseQuery(
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_type != ${EVENT_TYPE.performance}
${filterQuery}
`;
}
diff --git a/src/queries/sql/sessions/getWebsiteSessions.ts b/src/queries/sql/sessions/getWebsiteSessions.ts
index 8efea80df..6e41a47c3 100644
--- a/src/queries/sql/sessions/getWebsiteSessions.ts
+++ b/src/queries/sql/sessions/getWebsiteSessions.ts
@@ -1,10 +1,13 @@
import clickhouse from '@/lib/clickhouse';
-import { EVENT_COLUMNS } from '@/lib/constants';
+import { EVENT_COLUMNS, EVENT_TYPE, FILTER_COLUMNS } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import type { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getWebsiteSessions';
+const QUALIFIED_FILTER_COLUMNS = Object.fromEntries(
+ Object.entries(FILTER_COLUMNS).map(([key, value]) => [key, `website_event.${value}`]),
+);
export async function getWebsiteSessions(...args: [websiteId: string, filters: QueryFilters]) {
return runQuery({
@@ -55,6 +58,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
join session on session.session_id = website_event.session_id
and session.website_id = website_event.website_id
where website_event.website_id = {{websiteId::uuid}}
+ and website_event.event_type != ${EVENT_TYPE.performance}
${dateQuery}
${filterQuery}
${searchQuery}
@@ -80,18 +84,27 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { pagedRawQuery, parseFilters, getDateStringSQL } = clickhouse;
const { search } = filters;
- const { filterQuery, dateQuery, cohortQuery, queryParams } = parseFilters({
- ...filters,
- websiteId,
- });
+ const { filterQuery, dateQuery, cohortQuery, queryParams } = parseFilters(
+ {
+ ...filters,
+ websiteId,
+ },
+ {
+ columns: QUALIFIED_FILTER_COLUMNS,
+ },
+ );
const searchQuery = search
- ? `and ((positionCaseInsensitive(distinct_id, {search:String}) > 0)
- or (positionCaseInsensitive(city, {search:String}) > 0)
- or (positionCaseInsensitive(browser, {search:String}) > 0)
- or (positionCaseInsensitive(os, {search:String}) > 0)
- or (positionCaseInsensitive(device, {search:String}) > 0))`
+ ? `and ((positionCaseInsensitive(website_event.distinct_id, {search:String}) > 0)
+ or (positionCaseInsensitive(website_event.city, {search:String}) > 0)
+ or (positionCaseInsensitive(website_event.browser, {search:String}) > 0)
+ or (positionCaseInsensitive(website_event.os, {search:String}) > 0)
+ or (positionCaseInsensitive(website_event.device, {search:String}) > 0))`
: '';
+ const normalizedFilterQuery = filterQuery.replace(
+ /referrer_domain != hostname/g,
+ 'website_event.referrer_domain != website_event.hostname',
+ );
let sql = '';
@@ -99,58 +112,60 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
sql = `
select
session_id as id,
- website_id as websiteId,
- hostname,
- browser,
- os,
- device,
- screen,
- language,
- country,
- region,
- city,
+ any(website_id) as websiteId,
+ argMax(hostname, created_at) as hostname,
+ argMax(browser, created_at) as browser,
+ argMax(os, created_at) as os,
+ argMax(device, created_at) as device,
+ argMax(screen, created_at) as screen,
+ argMax(language, created_at) as language,
+ argMax(country, created_at) as country,
+ argMax(region, created_at) as region,
+ argMax(city, created_at) as city,
${getDateStringSQL('min(created_at)')} as firstAt,
${getDateStringSQL('max(created_at)')} as lastAt,
uniq(visit_id) as visits,
sumIf(1, event_type = 1) as views,
sumIf(1, event_type = 2) as events,
- lastAt as createdAt
+ max(created_at) as createdAt
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
+ and event_type != ${EVENT_TYPE.performance}
${dateQuery}
- ${filterQuery}
+ ${normalizedFilterQuery}
${searchQuery}
- group by session_id, website_id, hostname, browser, os, device, screen, language, country, region, city
+ group by session_id
order by lastAt desc
`;
} else {
sql = `
select
session_id as id,
- website_id as websiteId,
- arrayFirst(x -> 1, hostname) hostname,
- browser,
- os,
- device,
- screen,
- language,
- country,
- region,
- city,
+ any(website_id) as websiteId,
+ argMax(arrayFirst(x -> 1, hostname), max_time) as hostname,
+ argMax(browser, max_time) as browser,
+ argMax(os, max_time) as os,
+ argMax(device, max_time) as device,
+ argMax(screen, max_time) as screen,
+ argMax(language, max_time) as language,
+ argMax(country, max_time) as country,
+ argMax(region, max_time) as region,
+ argMax(city, max_time) as city,
${getDateStringSQL('min(min_time)')} as firstAt,
${getDateStringSQL('max(max_time)')} as lastAt,
uniq(visit_id) as visits,
sumIf(views, event_type = 1) as views,
sum(length(event_name)) as events,
- lastAt as createdAt
+ max(max_time) as createdAt
from website_event_stats_hourly as website_event
${cohortQuery}
where website_id = {websiteId:UUID}
+ and event_type != ${EVENT_TYPE.performance}
${dateQuery}
- ${filterQuery}
+ ${normalizedFilterQuery}
${searchQuery}
- group by session_id, website_id, hostname, browser, os, device, screen, language, country, region, city
+ group by session_id
order by lastAt desc
`;
}
diff --git a/src/queries/sql/sessions/saveSessionData.ts b/src/queries/sql/sessions/saveSessionData.ts
index cce6cd28c..ec9020256 100644
--- a/src/queries/sql/sessions/saveSessionData.ts
+++ b/src/queries/sql/sessions/saveSessionData.ts
@@ -1,8 +1,9 @@
import clickhouse from '@/lib/clickhouse';
-import { DATA_TYPE } from '@/lib/constants';
+import { DATA_TYPE, FIELD_LENGTH } from '@/lib/constants';
import { uuid } from '@/lib/crypto';
-import { flattenJSON, getStringValue } from '@/lib/data';
+import { flattenJSON, getStoredStringValue } from '@/lib/data';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import { truncateString } from '@/lib/format';
import kafka from '@/lib/kafka';
import prisma from '@/lib/prisma';
import type { DynamicData } from '@/lib/types';
@@ -32,25 +33,24 @@ export async function relationalQuery({
const { client } = prisma;
const jsonKeys = flattenJSON(sessionData);
+ const normalizedDistinctId = truncateString(distinctId, FIELD_LENGTH.distinctId);
const flattenedData = jsonKeys.map(a => ({
id: uuid(),
websiteId,
sessionId,
- dataKey: a.key,
- stringValue: getStringValue(a.value, a.dataType),
+ dataKey: truncateString(a.key, FIELD_LENGTH.dataKey),
+ stringValue: getStoredStringValue(a.value, a.dataType),
numberValue: a.dataType === DATA_TYPE.number ? a.value : null,
dateValue: a.dataType === DATA_TYPE.date ? new Date(a.value) : null,
dataType: a.dataType,
- distinctId,
+ distinctId: normalizedDistinctId,
createdAt,
}));
for (const data of flattenedData) {
const { sessionId, dataKey, ...props } = data;
- // Try to update existing record using compound where clause
- // This is safer than using id from a previous query due to race conditions
const updateResult = await client.sessionData.updateMany({
where: {
sessionId,
@@ -81,17 +81,18 @@ async function clickhouseQuery({
const { sendMessage } = kafka;
const jsonKeys = flattenJSON(sessionData);
+ const normalizedDistinctId = truncateString(distinctId, FIELD_LENGTH.distinctId);
const messages = jsonKeys.map(({ key, value, dataType }) => {
return {
website_id: websiteId,
session_id: sessionId,
- data_key: key,
+ data_key: truncateString(key, FIELD_LENGTH.dataKey),
data_type: dataType,
- string_value: getStringValue(value, dataType),
+ string_value: getStoredStringValue(value, dataType),
number_value: dataType === DATA_TYPE.number ? value : null,
date_value: dataType === DATA_TYPE.date ? getUTCString(value) : null,
- distinct_id: distinctId,
+ distinct_id: normalizedDistinctId,
created_at: getUTCString(createdAt),
};
});
diff --git a/src/queries/sql/sessions/saveSessionLink.ts b/src/queries/sql/sessions/saveSessionLink.ts
new file mode 100644
index 000000000..7fcabe000
--- /dev/null
+++ b/src/queries/sql/sessions/saveSessionLink.ts
@@ -0,0 +1,79 @@
+import clickhouse from '@/lib/clickhouse';
+import { FIELD_LENGTH } from '@/lib/constants';
+import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import { truncateString } from '@/lib/format';
+import kafka from '@/lib/kafka';
+import prisma from '@/lib/prisma';
+
+const FUNCTION_NAME = 'saveSessionLink';
+
+export interface SaveSessionLinkArgs {
+ websiteId: string;
+ sessionId: string;
+ distinctId: string;
+ createdAt?: Date;
+}
+
+export async function saveSessionLink(data: SaveSessionLinkArgs) {
+ return runQuery({
+ [PRISMA]: () => relationalQuery(data),
+ [CLICKHOUSE]: () => clickhouseQuery(data),
+ });
+}
+
+async function relationalQuery({
+ websiteId,
+ sessionId,
+ distinctId,
+ createdAt,
+}: SaveSessionLinkArgs) {
+ const { writeRawQuery } = prisma;
+
+ await writeRawQuery(
+ `
+ insert into session_link (
+ website_id,
+ session_id,
+ distinct_id,
+ created_at
+ )
+ values (
+ {{websiteId}},
+ {{sessionId}},
+ {{distinctId}},
+ {{createdAt}}
+ )
+ on conflict (website_id, distinct_id, session_id) do nothing
+ `,
+ {
+ websiteId,
+ sessionId,
+ distinctId: truncateString(distinctId, FIELD_LENGTH.distinctId),
+ createdAt,
+ },
+ FUNCTION_NAME,
+ );
+}
+
+async function clickhouseQuery({
+ websiteId,
+ sessionId,
+ distinctId,
+ createdAt,
+}: SaveSessionLinkArgs) {
+ const { insert, getUTCString } = clickhouse;
+ const { sendMessage } = kafka;
+
+ const message = {
+ website_id: websiteId,
+ session_id: sessionId,
+ distinct_id: truncateString(distinctId, FIELD_LENGTH.distinctId),
+ created_at: getUTCString(createdAt),
+ };
+
+ if (kafka.enabled) {
+ await sendMessage('session_link', [message]);
+ } else {
+ await insert('session_link', [message]);
+ }
+}
diff --git a/src/queries/sql/sessions/updateSession.ts b/src/queries/sql/sessions/updateSession.ts
new file mode 100644
index 000000000..6780123d1
--- /dev/null
+++ b/src/queries/sql/sessions/updateSession.ts
@@ -0,0 +1,49 @@
+import clickhouse from '@/lib/clickhouse';
+import { FIELD_LENGTH } from '@/lib/constants';
+import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import { truncateString } from '@/lib/format';
+import prisma from '@/lib/prisma';
+
+const FUNCTION_NAME = 'updateSession';
+
+export interface UpdateSessionArgs {
+ websiteId: string;
+ sessionId: string;
+ distinctId: string;
+}
+
+export async function updateSession(data: UpdateSessionArgs) {
+ return runQuery({
+ [PRISMA]: () => relationalQuery(data),
+ [CLICKHOUSE]: () => clickhouseQuery(),
+ });
+}
+
+async function relationalQuery({ websiteId, sessionId, distinctId }: UpdateSessionArgs) {
+ const { writeRawQuery } = prisma;
+
+ await writeRawQuery(
+ `
+ update session
+ set distinct_id = {{distinctId}}
+ where website_id = {{websiteId}}
+ and session_id = {{sessionId}}
+ and coalesce(distinct_id, '') != {{distinctId}}
+ `,
+ {
+ websiteId,
+ sessionId,
+ distinctId: truncateString(distinctId, FIELD_LENGTH.distinctId),
+ },
+ FUNCTION_NAME,
+ );
+}
+
+async function clickhouseQuery() {
+ if (!clickhouse.enabled) {
+ return;
+ }
+
+ // ClickHouse session reads fall back to session_link because session rows are event-derived.
+ return;
+}
diff --git a/src/recorder/index.js b/src/recorder/index.js
index 3651793ca..7caa8600c 100644
--- a/src/recorder/index.js
+++ b/src/recorder/index.js
@@ -1,54 +1,119 @@
-import { record } from 'rrweb';
+import { addCustomEvent, record } from 'rrweb';
(window => {
const { document } = window;
const { currentScript } = document;
+
if (!currentScript) return;
const _data = 'data-';
const attr = currentScript.getAttribute.bind(currentScript);
const config = value => attr(`${_data}${value}`);
- const website = config(`website-id`);
- const hostUrl = config(`host-url`);
- const sampleRate = parseFloat(config(`sample-rate`) || '0.15');
- const maskLevel = config(`mask-level`) || 'moderate';
- const maxDuration = parseInt(config(`max-duration`) || '300000', 10);
- const blockSelector = config(`block-selector`) || '';
+ const website = config('website-id');
+ const hostUrl = config('host-url');
if (!website) return;
- // Sample rate check
- if (sampleRate < 1 && Math.random() > sampleRate) return;
-
const host =
hostUrl || '__COLLECT_API_HOST__' || currentScript.src.split('/').slice(0, -1).join('/');
- const endpoint = `${host.replace(/\/$/, '')}__COLLECT_REPLAY_ENDPOINT__`;
+ const hostBase = host.replace(/\/$/, '');
+ const endpoint = `${hostBase}/api/record`;
+ const configEndpoint = `${hostBase}/api/websites/${website}/recorder`;
- const FLUSH_EVENT_COUNT = 100;
- const FLUSH_INTERVAL = 10000;
+ const REPLAY_FLUSH_EVENT_COUNT = 100;
+ const REPLAY_FLUSH_INTERVAL = 2000;
+ const REPLAY_MAX_PAYLOAD_SIZE = 500000;
+ const REPLAY_FRAGMENT_TYPE = 'umami:rrweb-event-fragment';
+ const REPLAY_FRAGMENT_TOTAL_PLACEHOLDER = 999999999;
+ const RRWEB_EVENT_TYPE = {
+ FullSnapshot: 2,
+ };
+ const HEATMAP_FLUSH_EVENT_COUNT = 20;
+ const HEATMAP_FLUSH_INTERVAL = 5000;
- let eventBuffer = [];
- let stopFn = null;
- let flushTimer = null;
- let startTime = null;
- let stopped = false;
+ let replayEnabled = false;
+ let heatmapEnabled = false;
+ let sampleRate = 0.15;
+ let heatmapSampleRate = 0.15;
+ let maskLevel = 'moderate';
+ let maxDuration = 300000;
+ let blockSelector = '';
- const sendEvents = (events, useKeepalive = false) => {
- const session = window.umami?.getSession?.();
- if (!session?.cache) return;
+ let replayBuffer = [];
+ let heatmapBuffer = [];
+ let replayStopFn = null;
+ let replayFlushTimer = null;
+ let heatmapFlushTimer = null;
+ let replayStartTime = null;
+ let replayLastChunkIndex = 0;
+ let replayStopped = false;
+ let heatmapStarted = false;
- const body = JSON.stringify({
- type: 'record',
+ const getSessionCache = () => window.umami?.getSession?.()?.cache;
+
+ const getPayloadBody = (type, payload) =>
+ JSON.stringify({
+ type,
payload: {
website,
- events,
- timestamp: Math.floor(Date.now() / 1000),
+ ...payload,
},
});
- // keepalive has a 64KB body limit — only use it for small payloads on unload
- const keepalive = useKeepalive && body.length < 60000;
+ const getPayloadSize = body => {
+ try {
+ return new Blob([body]).size;
+ } catch {
+ return body.length;
+ }
+ };
+
+ const getReplayPayloadSize = (events, timestamp) =>
+ getPayloadSize(getPayloadBody('record', { events, timestamp }));
+
+ const isReplayPayloadTooLarge = (events, timestamp) =>
+ getReplayPayloadSize(events, timestamp) > REPLAY_MAX_PAYLOAD_SIZE;
+
+ const getReplayChunkIndex = () => {
+ const timestamp = Math.floor(Date.now() / 1000);
+ const chunkIndex = Math.max(timestamp, replayLastChunkIndex + 1);
+
+ replayLastChunkIndex = chunkIndex;
+
+ return chunkIndex;
+ };
+
+ const createReplayFragment = (id, index, total, timestamp, value) => ({
+ type: REPLAY_FRAGMENT_TYPE,
+ timestamp,
+ data: {
+ id,
+ index,
+ total,
+ value,
+ },
+ });
+
+ const getReplayEventTimestamp = event => {
+ const timestamp = Number(event?.timestamp);
+
+ return Number.isFinite(timestamp) ? timestamp : Date.now();
+ };
+
+ const getReplayFragmentId = () =>
+ `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
+
+ const isReplayFullSnapshot = event => event?.type === RRWEB_EVENT_TYPE.FullSnapshot;
+
+ const sendPayload = (type, payload, useKeepalive = false) => {
+ const cache = getSessionCache();
+
+ if (!cache) return;
+
+ const body = getPayloadBody(type, payload);
+
+ const keepalive = useKeepalive && getPayloadSize(body) < 60000;
return fetch(endpoint, {
keepalive,
@@ -56,32 +121,190 @@ import { record } from 'rrweb';
body,
headers: {
'Content-Type': 'application/json',
- 'x-umami-cache': session.cache,
+ 'x-umami-cache': cache,
},
credentials: 'omit',
}).catch(() => {});
};
- const flush = (useKeepalive = false) => {
- if (!eventBuffer.length) return;
+ const sendReplayChunk = (events, timestamp, useKeepalive = false) => {
+ replayLastChunkIndex = Math.max(replayLastChunkIndex, timestamp);
- const events = eventBuffer;
- eventBuffer = [];
-
- sendEvents(events, useKeepalive);
+ sendPayload(
+ 'record',
+ {
+ events,
+ timestamp,
+ },
+ useKeepalive,
+ );
};
- const scheduleFlush = () => {
- if (flushTimer) clearTimeout(flushTimer);
- flushTimer = setTimeout(flush, FLUSH_INTERVAL);
+ const getReplayEventFragments = (event, chunkTimestamp) => {
+ const value = JSON.stringify(event);
+ const id = getReplayFragmentId();
+ const eventTimestamp = getReplayEventTimestamp(event);
+ const fragments = [];
+ let start = 0;
+
+ while (start < value.length) {
+ let low = start + 1;
+ let high = value.length;
+ let end = start;
+
+ while (low <= high) {
+ const mid = Math.floor((low + high) / 2);
+ const fragmentChunkTimestamp = chunkTimestamp + fragments.length;
+ const fragment = createReplayFragment(
+ id,
+ fragments.length,
+ REPLAY_FRAGMENT_TOTAL_PLACEHOLDER,
+ eventTimestamp,
+ value.slice(start, mid),
+ );
+
+ if (isReplayPayloadTooLarge([fragment], fragmentChunkTimestamp)) {
+ high = mid - 1;
+ } else {
+ end = mid;
+ low = mid + 1;
+ }
+ }
+
+ if (end === start) {
+ end = start + 1;
+ }
+
+ fragments.push(value.slice(start, end));
+ start = end;
+ }
+
+ return fragments.map((fragment, index) =>
+ createReplayFragment(id, index, fragments.length, eventTimestamp, fragment),
+ );
};
- const stop = () => {
- if (stopped) return;
- stopped = true;
- if (flushTimer) clearTimeout(flushTimer);
- flush();
- if (stopFn) stopFn();
+ const sendReplayEventFragments = (event, timestamp, useKeepalive = false) => {
+ const fragments = getReplayEventFragments(event, timestamp);
+
+ fragments.forEach((fragment, index) => {
+ sendReplayChunk([fragment], timestamp + index, useKeepalive);
+ });
+
+ return fragments.length;
+ };
+
+ const sendReplayEvents = (events, timestamp, useKeepalive = false) => {
+ let chunk = [];
+ let chunkOffset = 0;
+
+ for (const event of events) {
+ const chunkTimestamp = timestamp + chunkOffset;
+
+ if (isReplayFullSnapshot(event)) {
+ if (chunk.length) {
+ sendReplayChunk(chunk, chunkTimestamp, useKeepalive);
+ chunk = [];
+ chunkOffset += 1;
+ }
+
+ const snapshotTimestamp = timestamp + chunkOffset;
+
+ if (isReplayPayloadTooLarge([event], snapshotTimestamp)) {
+ chunkOffset += sendReplayEventFragments(event, snapshotTimestamp, useKeepalive);
+ } else {
+ sendReplayChunk([event], snapshotTimestamp, useKeepalive);
+ chunkOffset += 1;
+ }
+
+ continue;
+ }
+
+ if (isReplayPayloadTooLarge([event], chunkTimestamp)) {
+ if (chunk.length) {
+ sendReplayChunk(chunk, chunkTimestamp, useKeepalive);
+ chunk = [];
+ chunkOffset += 1;
+ }
+
+ chunkOffset += sendReplayEventFragments(event, timestamp + chunkOffset, useKeepalive);
+ continue;
+ }
+
+ const candidate = [...chunk, event];
+
+ if (isReplayPayloadTooLarge(candidate, chunkTimestamp)) {
+ if (chunk.length) {
+ sendReplayChunk(chunk, chunkTimestamp, useKeepalive);
+ chunk = [];
+ chunkOffset += 1;
+ }
+ }
+
+ chunk.push(event);
+ }
+
+ if (chunk.length) {
+ sendReplayChunk(chunk, timestamp + chunkOffset, useKeepalive);
+ }
+ };
+
+ const flushReplay = (useKeepalive = false) => {
+ if (!replayBuffer.length) return;
+
+ const events = replayBuffer;
+ replayBuffer = [];
+
+ sendReplayEvents(events, getReplayChunkIndex(), useKeepalive);
+ };
+
+ const flushHeatmap = (useKeepalive = false) => {
+ if (!heatmapBuffer.length) return;
+
+ const events = heatmapBuffer;
+ heatmapBuffer = [];
+
+ sendPayload(
+ 'heatmap',
+ {
+ events,
+ timestamp: Math.floor(Date.now() / 1000),
+ },
+ useKeepalive,
+ );
+ };
+
+ const scheduleHeatmapFlush = () => {
+ if (heatmapFlushTimer) clearTimeout(heatmapFlushTimer);
+ heatmapFlushTimer = setTimeout(() => flushHeatmap(), HEATMAP_FLUSH_INTERVAL);
+ };
+
+ const queueHeatmapEvent = event => {
+ heatmapBuffer.push({
+ ...event,
+ timestamp: Date.now(),
+ });
+
+ if (heatmapBuffer.length >= HEATMAP_FLUSH_EVENT_COUNT) {
+ flushHeatmap();
+ return;
+ }
+
+ scheduleHeatmapFlush();
+ };
+
+ const stopReplay = () => {
+ if (replayStopped) return;
+
+ replayStopped = true;
+
+ if (replayFlushTimer) clearInterval(replayFlushTimer);
+ flushReplay();
+
+ if (replayStopFn) {
+ replayStopFn();
+ replayStopFn = null;
+ }
};
const getMaskConfig = level => {
@@ -91,47 +314,169 @@ import { record } from 'rrweb';
maskAllInputs: true,
maskTextSelector: '*',
};
- default: // moderate
+ default:
return {
maskAllInputs: true,
};
}
};
- const waitForSession = (attempts = 0) => {
- if (attempts > 50) return;
+ const shouldSample = value => {
+ if (value >= 1) return true;
+ if (value <= 0) return false;
- const session = window.umami?.getSession?.();
- if (session?.cache) {
- beginRecording();
- } else {
- setTimeout(() => waitForSession(attempts + 1), 100);
- }
+ return Math.random() <= value;
};
- const beginRecording = () => {
- startTime = Date.now();
+ const measureElementWidth = element => {
+ if (!element) return 0;
- const maskConfig = getMaskConfig(maskLevel);
+ const rect = element.getBoundingClientRect?.();
- stopFn = record({
+ return Math.max(
+ element.scrollWidth || 0,
+ element.offsetWidth || 0,
+ element.clientWidth || 0,
+ rect?.width || 0,
+ );
+ };
+
+ const measureElementHeight = element => {
+ if (!element) return 0;
+
+ const rect = element.getBoundingClientRect?.();
+
+ return Math.max(
+ element.scrollHeight || 0,
+ element.offsetHeight || 0,
+ element.clientHeight || 0,
+ rect?.height || 0,
+ );
+ };
+
+ const measureDocumentBounds = doc => {
+ const body = doc?.body;
+
+ if (!body) {
+ return { width: 0, height: 0 };
+ }
+
+ let maxRight = 0;
+ let maxBottom = 0;
+ const walker = doc.createTreeWalker(body, NodeFilter.SHOW_ELEMENT);
+ let node = walker.currentNode;
+
+ while (node) {
+ const rect = node.getBoundingClientRect?.();
+
+ if (rect && (rect.width > 0 || rect.height > 0)) {
+ maxRight = Math.max(maxRight, rect.right);
+ maxBottom = Math.max(maxBottom, rect.bottom);
+ }
+
+ node = walker.nextNode();
+ }
+
+ return {
+ width: Math.max(0, Math.round(maxRight)),
+ height: Math.max(0, Math.round(maxBottom)),
+ };
+ };
+
+ const createPageMetrics = () => {
+ const computePageMetrics = ({ includeBounds = false } = {}) => {
+ const scrollingElement = document.scrollingElement || document.documentElement;
+ const firstChild = document.body?.firstElementChild;
+ const bounds = includeBounds ? measureDocumentBounds(document) : null;
+ const pageW = Math.max(
+ bounds?.width || 0,
+ measureElementWidth(scrollingElement),
+ measureElementWidth(document.documentElement),
+ measureElementWidth(document.body),
+ measureElementWidth(firstChild),
+ );
+ const pageH = Math.max(
+ bounds?.height || 0,
+ measureElementHeight(scrollingElement),
+ measureElementHeight(document.documentElement),
+ measureElementHeight(document.body),
+ measureElementHeight(firstChild),
+ );
+ const scrollLeft = scrollingElement?.scrollLeft || window.scrollX || 0;
+ const scrollTop = scrollingElement?.scrollTop || window.scrollY || 0;
+
+ return {
+ pageW,
+ pageH,
+ scrollLeft,
+ scrollTop,
+ };
+ };
+
+ const computeScrollPct = () => {
+ const { pageH, scrollTop } = computePageMetrics();
+ const visible = scrollTop + window.innerHeight;
+
+ return {
+ pct: Math.max(0, Math.min(100, Math.round((visible / Math.max(1, pageH)) * 100))),
+ pageH,
+ };
+ };
+
+ return {
+ computePageMetrics,
+ computeScrollPct,
+ };
+ };
+
+ const beginReplayCapture = () => {
+ replayStartTime = Date.now();
+
+ replayFlushTimer = setInterval(() => flushReplay(), REPLAY_FLUSH_INTERVAL);
+
+ replayStopFn = record({
emit(event) {
- if (stopped) return;
+ if (replayStopped) return;
- if (Date.now() - startTime > maxDuration) {
- stop();
+ if (Date.now() - replayStartTime > maxDuration) {
+ stopReplay();
return;
}
- eventBuffer.push(event);
-
- if (eventBuffer.length >= FLUSH_EVENT_COUNT) {
- flush();
+ if (isReplayFullSnapshot(event)) {
+ flushReplay();
+ sendReplayEvents([event], getReplayChunkIndex());
+ return;
}
- scheduleFlush();
+ const timestamp = Math.floor(Date.now() / 1000);
+
+ if (isReplayPayloadTooLarge([event], timestamp)) {
+ if (replayBuffer.length) {
+ const events = replayBuffer;
+ replayBuffer = [];
+
+ sendReplayEvents(events, getReplayChunkIndex());
+ }
+
+ sendReplayEvents([event], getReplayChunkIndex());
+ return;
+ }
+
+ if (replayBuffer.length && isReplayPayloadTooLarge([...replayBuffer, event], timestamp)) {
+ flushReplay();
+ }
+
+ replayBuffer.push(event);
+
+ if (
+ replayBuffer.length >= REPLAY_FLUSH_EVENT_COUNT ||
+ isReplayPayloadTooLarge(replayBuffer, Math.floor(Date.now() / 1000))
+ ) {
+ flushReplay();
+ }
},
- ...maskConfig,
+ ...getMaskConfig(maskLevel),
inlineStylesheet: true,
slimDOMOptions: {
script: true,
@@ -149,18 +494,223 @@ import { record } from 'rrweb';
...(blockSelector && { blockSelector }),
});
- document.addEventListener('visibilitychange', () => {
- if (document.visibilityState === 'hidden') flush(true);
- });
-
- window.addEventListener('beforeunload', () => flush(true));
+ if (replayStopped && replayStopFn) {
+ replayStopFn();
+ replayStopFn = null;
+ }
};
- if (document.readyState === 'complete') {
- waitForSession();
- } else {
- document.addEventListener('readystatechange', () => {
- if (document.readyState === 'complete') waitForSession();
+ const beginHeatmapCapture = () => {
+ if (heatmapStarted) return;
+
+ heatmapStarted = true;
+
+ const { computePageMetrics, computeScrollPct } = createPageMetrics();
+ let scrollUrl = location.href;
+ let maxScrollPct = 0;
+ let lastFlushedScrollPct = 0;
+ let scrollTimer = null;
+
+ const flushScroll = () => {
+ if (maxScrollPct <= 0 || maxScrollPct <= lastFlushedScrollPct) return;
+
+ const { pageW, pageH } = computePageMetrics({ includeBounds: true });
+
+ queueHeatmapEvent({
+ type: 'scroll',
+ url: scrollUrl,
+ scrollPct: maxScrollPct,
+ viewportW: window.innerWidth,
+ viewportH: window.innerHeight,
+ pageW,
+ pageH,
+ });
+
+ lastFlushedScrollPct = maxScrollPct;
+ maxScrollPct = 0;
+ };
+
+ const onClick = event => {
+ if (!event.isTrusted || event.button !== 0) return;
+
+ const {
+ pageW: rawPageW,
+ pageH: rawPageH,
+ scrollLeft,
+ scrollTop,
+ } = computePageMetrics({
+ includeBounds: true,
+ });
+ const pageX = Number.isFinite(event.pageX) ? event.pageX : event.clientX + scrollLeft;
+ const pageY = Number.isFinite(event.pageY) ? event.pageY : event.clientY + scrollTop;
+ const target = event.target;
+ const targetRect =
+ target && typeof target.getBoundingClientRect === 'function'
+ ? target.getBoundingClientRect()
+ : null;
+ const targetRight = targetRect ? targetRect.right + scrollLeft : 0;
+ const targetBottom = targetRect ? targetRect.bottom + scrollTop : 0;
+ const pageW = Math.max(rawPageW, Math.ceil(pageX), Math.ceil(targetRight));
+ const pageH = Math.max(rawPageH, Math.ceil(pageY), Math.ceil(targetBottom));
+
+ queueHeatmapEvent({
+ type: 'click',
+ url: location.href,
+ x: Math.round(event.clientX),
+ y: Math.round(event.clientY),
+ pageX: Math.round(pageX),
+ pageY: Math.round(pageY),
+ pageW,
+ pageH,
+ viewportW: window.innerWidth,
+ viewportH: window.innerHeight,
+ });
+ };
+
+ const onScroll = () => {
+ if (scrollTimer) return;
+
+ scrollTimer = setTimeout(() => {
+ const { pct } = computeScrollPct();
+
+ if (pct > maxScrollPct) {
+ maxScrollPct = pct;
+ }
+
+ flushScroll();
+ scrollTimer = null;
+ }, 400);
+ };
+
+ const onUrlChange = () => {
+ if (location.href === scrollUrl) return;
+
+ flushScroll();
+ scrollUrl = location.href;
+ lastFlushedScrollPct = 0;
+
+ if (replayStopFn && !replayStopped) {
+ addCustomEvent('url-change', { url: scrollUrl });
+ }
+ };
+
+ const hookHistory = method => {
+ const original = history[method];
+
+ history[method] = function (...args) {
+ const result = original.apply(this, args);
+ onUrlChange();
+ return result;
+ };
+ };
+
+ hookHistory('pushState');
+ hookHistory('replaceState');
+ window.addEventListener('popstate', onUrlChange);
+ window.addEventListener('scroll', onScroll, { passive: true });
+ document.addEventListener('click', onClick, { capture: true, passive: true });
+
+ {
+ const { pct } = computeScrollPct();
+
+ if (pct > maxScrollPct) {
+ maxScrollPct = pct;
+ }
+
+ flushScroll();
+ }
+
+ document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState === 'hidden') {
+ flushScroll();
+ flushHeatmap(true);
+ }
});
- }
+
+ window.addEventListener('beforeunload', () => {
+ flushScroll();
+ flushHeatmap(true);
+ });
+ };
+
+ const waitForSession = (callback, attempts = 0) => {
+ if (attempts > 50) return;
+
+ if (getSessionCache()) {
+ callback();
+ return;
+ }
+
+ setTimeout(() => waitForSession(callback, attempts + 1), 100);
+ };
+
+ const startCaptures = () => {
+ const shouldRecordReplay = replayEnabled && shouldSample(sampleRate);
+ const shouldRecordHeatmap = heatmapEnabled && shouldSample(heatmapSampleRate);
+
+ if (shouldRecordHeatmap) {
+ beginHeatmapCapture();
+ }
+
+ if (shouldRecordReplay) {
+ beginReplayCapture();
+ }
+
+ if (!shouldRecordHeatmap && !shouldRecordReplay) {
+ return;
+ }
+
+ document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState === 'hidden') {
+ flushReplay(true);
+ }
+ });
+
+ window.addEventListener('pagehide', () => {
+ flushReplay(true);
+ });
+
+ window.addEventListener('beforeunload', () => {
+ flushReplay(true);
+ });
+ };
+
+ const bootstrap = async () => {
+ try {
+ const response = await fetch(configEndpoint, { credentials: 'omit' });
+
+ if (!response.ok) return;
+
+ const data = await response.json();
+
+ if (!data?.enabled) return;
+
+ replayEnabled = data.replayEnabled === true;
+ heatmapEnabled = data.heatmapEnabled === true;
+
+ if (typeof data.sampleRate === 'number') sampleRate = data.sampleRate;
+ if (typeof data.heatmapSampleRate === 'number') heatmapSampleRate = data.heatmapSampleRate;
+ if (typeof data.maskLevel === 'string') maskLevel = data.maskLevel;
+ if (typeof data.maxDuration === 'number') maxDuration = data.maxDuration;
+ if (typeof data.blockSelector === 'string') blockSelector = data.blockSelector;
+ } catch {
+ return;
+ }
+
+ if (!replayEnabled && !heatmapEnabled) {
+ return;
+ }
+
+ if (document.readyState === 'complete') {
+ waitForSession(startCaptures);
+ } else {
+ document.addEventListener('readystatechange', () => {
+ if (document.readyState === 'complete') {
+ waitForSession(startCaptures);
+ }
+ });
+ }
+ };
+
+ bootstrap();
})(window);
diff --git a/src/store/app.ts b/src/store/app.ts
index 0893c506a..851332e26 100644
--- a/src/store/app.ts
+++ b/src/store/app.ts
@@ -32,10 +32,7 @@ export function setLocale(locale: string) {
store.setState({ locale });
}
-export function setShareData(
- share: object | null,
- shareToken: { token?: string } | null,
-) {
+export function setShareData(share: object | null, shareToken: { token?: string } | null) {
store.setState({ share, shareToken });
}
diff --git a/src/test/README.md b/src/test/README.md
new file mode 100644
index 000000000..b43279854
--- /dev/null
+++ b/src/test/README.md
@@ -0,0 +1,11 @@
+# Test Convention
+
+Use Vitest for unit and component tests. Cypress remains the end-to-end test runner.
+
+- Place tests next to the code they cover as `*.test.ts` or `*.test.tsx`.
+- Import Vitest APIs explicitly: `import { describe, expect, test, vi } from 'vitest';`.
+- Use `test`, not `it`.
+- React component tests should import from `@/test/render`.
+- Prefer accessible Testing Library queries such as `getByRole`, `getByLabelText`, and `getByText`.
+- Use `getByTestId` only when there is no useful accessible query. The test id attribute is `data-test`.
+- Keep test doubles in the test file unless they are shared framework concerns, such as Next navigation.
diff --git a/src/test/msw/handlers.ts b/src/test/msw/handlers.ts
new file mode 100644
index 000000000..663b731dd
--- /dev/null
+++ b/src/test/msw/handlers.ts
@@ -0,0 +1,3 @@
+import type { RequestHandler } from 'msw';
+
+export const handlers: RequestHandler[] = [];
diff --git a/src/test/msw/server.ts b/src/test/msw/server.ts
new file mode 100644
index 000000000..1a5bd6432
--- /dev/null
+++ b/src/test/msw/server.ts
@@ -0,0 +1,5 @@
+import { setupServer } from 'msw/node';
+import { handlers } from './handlers';
+
+export const server = setupServer(...handlers);
+export { HttpResponse, http } from 'msw';
diff --git a/src/test/navigation.ts b/src/test/navigation.ts
new file mode 100644
index 000000000..d9f65dc2b
--- /dev/null
+++ b/src/test/navigation.ts
@@ -0,0 +1,43 @@
+import { vi } from 'vitest';
+
+const testNavigation = vi.hoisted(() => ({
+ pathname: '/',
+ searchParams: new URLSearchParams(),
+ router: {
+ back: vi.fn(),
+ forward: vi.fn(),
+ prefetch: vi.fn(),
+ push: vi.fn(),
+ refresh: vi.fn(),
+ replace: vi.fn(),
+ },
+}));
+
+export function setTestUrl(url: string) {
+ const nextUrl = new URL(url, 'http://localhost');
+
+ testNavigation.pathname = nextUrl.pathname;
+ testNavigation.searchParams = nextUrl.searchParams;
+
+ window.history.pushState({}, '', `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`);
+}
+
+export function getTestRouter() {
+ return testNavigation.router;
+}
+
+export function resetTestNavigation() {
+ setTestUrl('/');
+
+ Object.values(testNavigation.router).forEach(mock => {
+ mock.mockReset();
+ });
+}
+
+vi.mock('next/navigation', () => ({
+ notFound: vi.fn(),
+ redirect: vi.fn(),
+ usePathname: () => testNavigation.pathname,
+ useRouter: () => testNavigation.router,
+ useSearchParams: () => testNavigation.searchParams,
+}));
diff --git a/src/test/render.tsx b/src/test/render.tsx
new file mode 100644
index 000000000..6c1dea6b9
--- /dev/null
+++ b/src/test/render.tsx
@@ -0,0 +1,83 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import {
+ type RenderOptions,
+ screen,
+ render as testingLibraryRender,
+ waitFor,
+ within,
+} from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { RouterProvider, ZenProvider } from '@umami/react-zen';
+import { NextIntlClientProvider } from 'next-intl';
+import type { ReactElement, ReactNode } from 'react';
+import enUS from '../../public/intl/messages/en-US.json';
+import { setTestUrl } from './navigation';
+
+type TestRenderOptions = Omit & {
+ locale?: string;
+ messages?: Record;
+ queryClient?: QueryClient;
+ route?: string;
+};
+
+export function createTestQueryClient() {
+ return new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ refetchOnWindowFocus: false,
+ staleTime: 1000 * 60,
+ },
+ },
+ });
+}
+
+function TestProviders({
+ children,
+ locale = 'en-US',
+ messages = enUS,
+ queryClient = createTestQueryClient(),
+}: {
+ children: ReactNode;
+ locale?: string;
+ messages?: Record;
+ queryClient?: QueryClient;
+}) {
+ return (
+
+ window.history.pushState({}, '', url)}>
+ null}>
+ {children}
+
+
+
+ );
+}
+
+export function render(
+ ui: ReactElement,
+ {
+ locale = 'en-US',
+ messages = enUS,
+ queryClient = createTestQueryClient(),
+ route = '/',
+ ...options
+ }: TestRenderOptions = {},
+) {
+ setTestUrl(route);
+
+ return {
+ queryClient,
+ user: userEvent.setup(),
+ ...testingLibraryRender(ui, {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ ...options,
+ }),
+ };
+}
+
+export { screen, userEvent, waitFor, within };
diff --git a/src/test/setup.ts b/src/test/setup.ts
new file mode 100644
index 000000000..9570b9d04
--- /dev/null
+++ b/src/test/setup.ts
@@ -0,0 +1,59 @@
+import '@testing-library/jest-dom/vitest';
+import { cleanup, configure } from '@testing-library/react';
+import { afterEach, vi } from 'vitest';
+import { resetTestNavigation } from './navigation';
+
+configure({ testIdAttribute: 'data-test' });
+
+Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: vi.fn().mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+});
+
+Object.defineProperty(navigator, 'clipboard', {
+ configurable: true,
+ value: {
+ readText: vi.fn(),
+ writeText: vi.fn(),
+ },
+});
+
+window.scrollTo = vi.fn();
+
+class ResizeObserver {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+}
+
+class IntersectionObserver {
+ readonly root = null;
+ readonly rootMargin = '';
+ readonly scrollMargin = '';
+ readonly thresholds = [];
+
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ takeRecords() {
+ return [];
+ }
+}
+
+window.ResizeObserver = ResizeObserver;
+window.IntersectionObserver = IntersectionObserver;
+
+afterEach(() => {
+ cleanup();
+ resetTestNavigation();
+ vi.restoreAllMocks();
+});
diff --git a/src/test/test-utils.tsx b/src/test/test-utils.tsx
new file mode 100644
index 000000000..e0b715df3
--- /dev/null
+++ b/src/test/test-utils.tsx
@@ -0,0 +1,10 @@
+import { type RenderOptions, render } from '@testing-library/react';
+import type { ReactElement } from 'react';
+
+const customRender = (ui: ReactElement, options?: Omit) => {
+ return render(ui, options);
+};
+
+export * from '@testing-library/react';
+export { default as userEvent } from '@testing-library/user-event';
+export { customRender as render };
diff --git a/src/test/vitest.d.ts b/src/test/vitest.d.ts
new file mode 100644
index 000000000..edfa8b53f
--- /dev/null
+++ b/src/test/vitest.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/src/tracker/index.d.ts b/src/tracker/index.d.ts
index 32fbee97f..925e075cd 100644
--- a/src/tracker/index.d.ts
+++ b/src/tracker/index.d.ts
@@ -1,3 +1,4 @@
+/** Public types for the browser tracker. */
export type TrackedProperties = {
/**
* Hostname of server
@@ -5,48 +6,46 @@ export type TrackedProperties = {
* @description extracted from `window.location.hostname`
* @example 'analytics.umami.is'
*/
- hostname: string;
-
+ hostname?: string;
+ /** Distinct ID associated with the current visitor. */
+ id?: string;
/**
* Browser language
*
* @description extracted from `window.navigator.language`
* @example 'en-US', 'fr-FR'
*/
- language: string;
-
+ language?: string;
/**
* Page referrer
*
- * @description extracted from `window.navigator.language`
+ * @description extracted from `document.referrer`
* @example 'https://analytics.umami.is/docs/getting-started'
*/
- referrer: string;
-
+ referrer?: string;
/**
* Screen dimensions
*
* @description extracted from `window.screen.width` and `window.screen.height`
* @example '1920x1080', '2560x1440'
*/
- screen: string;
-
+ screen?: string;
+ /** Tag configured on the tracker script. */
+ tag?: string;
/**
* Page title
*
* @description extracted from `document.querySelector('head > title')`
* @example 'umami'
*/
- title: string;
-
+ title?: string;
/**
* Page url
*
- * @description built from `${window.location.pathname}${window.location.search}`
- * @example 'docs/getting-started'
+ * @description normalized from `window.location.href`
+ * @example 'https://analytics.umami.is/docs/getting-started'
*/
- url: string;
-
+ url?: string;
/**
* Website ID (required)
*
@@ -54,9 +53,10 @@ export type TrackedProperties = {
*/
website: string;
};
-
-export type WithRequired = T & { [P in K]-?: T[P] };
-
+export type WithRequired = T & {
+ [P in K]-?: T[P];
+};
+export type EventDataValue = boolean | number | string | null | EventData | EventDataValue[];
/**
*
* Event Data can work with any JSON data. There are a few rules in place to maintain performance.
@@ -66,9 +66,8 @@ export type WithRequired = T & { [P in K]-?: T[P] };
* - Objects have a max of 50 properties. Arrays are considered 1 property.
*/
export interface EventData {
- [key: string]: number | string | EventData | number[] | string[] | EventData[];
+ [key: string]: EventDataValue;
}
-
export type EventProperties = {
/**
* NOTE: event names will be truncated past 50 characters
@@ -80,7 +79,6 @@ export type PageViewProperties = WithRequired;
export type CustomEventFunction = (
props: PageViewProperties,
) => EventProperties | PageViewProperties;
-
export type UmamiTracker = {
track: {
/**
@@ -90,8 +88,7 @@ export type UmamiTracker = {
* umami.track();
* ```
*/
- (): Promise;
-
+ (): Promise;
/**
* Track an event with a given name
*
@@ -101,8 +98,7 @@ export type UmamiTracker = {
* umami.track('signup-button');
* ```
*/
- (eventName: string): Promise;
-
+ (eventName: string): Promise;
/**
* Tracks an event with dynamic data.
*
@@ -125,8 +121,7 @@ export type UmamiTracker = {
* umami.track('signup-button', { name: 'newsletter', id: 123 });
* ```
*/
- (eventName: string, obj: EventData): Promise;
-
+ (eventName: string, obj: EventData): Promise;
/**
* Tracks a page view with custom properties
*
@@ -134,8 +129,7 @@ export type UmamiTracker = {
* umami.track({ website: 'e676c9b4-11e4-4ef1-a4d7-87001773e9f2', url: '/home', title: 'Home page' });
* ```
*/
- (properties: PageViewProperties): Promise;
-
+ (properties: PageViewProperties): Promise;
/**
* Tracks an event with fully customizable dynamic data
* If you don't specify any `name` and/or `data`, it will be treated as a page view
@@ -144,10 +138,37 @@ export type UmamiTracker = {
* umami.track((props) => ({ ...props, url: path }));
* ```
*/
- (eventFunction: CustomEventFunction): Promise;
+ (eventFunction: CustomEventFunction): Promise;
+ };
+ identify: {
+ /**
+ * Identify a visitor with optional associated data.
+ *
+ * @example ```
+ * umami.identify('user-123', { plan: 'pro' });
+ * ```
+ */
+ (id: string, data?: EventData): Promise;
+ /**
+ * Associate data with the current visitor. An `id` string sets the Distinct ID.
+ *
+ * @example ```
+ * umami.identify({ id: 'user-123', plan: 'pro' });
+ * ```
+ */
+ (
+ data: EventData & {
+ id?: string;
+ },
+ ): Promise;
+ };
+ getSession: () => {
+ cache: string | undefined;
+ website: string | null;
};
};
-
-export interface Window {
- umami: UmamiTracker;
+declare global {
+ interface Window {
+ umami: UmamiTracker;
+ }
}
diff --git a/src/tracker/index.js b/src/tracker/index.js
deleted file mode 100644
index 85ad2ffea..000000000
--- a/src/tracker/index.js
+++ /dev/null
@@ -1,411 +0,0 @@
-(window => {
- const {
- screen: { width, height },
- navigator: { language, doNotTrack: ndnt, msDoNotTrack: msdnt },
- location,
- document,
- history,
- top,
- doNotTrack,
- } = window;
- const { currentScript, referrer } = document;
- if (!currentScript) return;
-
- const { hostname, href, origin } = location;
-
- let localStorage;
- try {
- localStorage = href.startsWith('data:') ? undefined : window.localStorage;
- } catch {
- /* (DOMException) SecurityError: Access is denied for this document. */
- }
-
- const _data = 'data-';
- const _false = 'false';
- const _true = 'true';
- const attr = currentScript.getAttribute.bind(currentScript);
- const config = value => attr(`${_data}${value}`);
-
- const website = config('website-id');
- const hostUrl = config('host-url');
- const beforeSend = config('before-send');
- const tag = config('tag') || undefined;
- const autoTrack = config('auto-track') !== _false;
- const dnt = config('do-not-track') === _true;
- const excludeSearch = config('exclude-search') === _true;
- const excludeHash = config('exclude-hash') === _true;
- const domain = config('domains') || '';
- const credentials = config('fetch-credentials') || 'omit';
- const perf = config('performance') === _true;
-
- const domains = domain.split(',').map(n => n.trim());
- const host =
- hostUrl || '__COLLECT_API_HOST__' || currentScript.src.split('/').slice(0, -1).join('/');
- const endpoint = `${host.replace(/\/$/, '')}__COLLECT_API_ENDPOINT__`;
- const screen = `${width}x${height}`;
- const eventRegex = /data-umami-event-([\w-_]+)/;
- const eventNameAttribute = `${_data}umami-event`;
- const delayDuration = 300;
-
- /* Helper functions */
-
- const normalize = raw => {
- if (!raw) return raw;
- try {
- const u = new URL(raw, location.href);
- if (excludeSearch) u.search = '';
- if (excludeHash) u.hash = '';
- return u.toString();
- } catch {
- return raw;
- }
- };
-
- const getPayload = () => ({
- website,
- screen,
- language,
- title: document.title,
- hostname,
- url: currentUrl,
- referrer: currentRef,
- tag,
- id: identity ? identity : undefined,
- });
-
- const hasDoNotTrack = () => {
- const dnt = doNotTrack || ndnt || msdnt;
- return dnt === 1 || dnt === '1' || dnt === 'yes';
- };
-
- /* Event handlers */
-
- const handlePush = (_state, _title, url) => {
- if (!url) return;
-
- if (typeof flushPerformance === 'function') {
- flushPerformance();
- }
-
- currentRef = currentUrl;
- currentUrl = normalize(new URL(url, location.href).toString());
-
- if (currentUrl !== currentRef) {
- setTimeout(track, delayDuration);
- }
- };
-
- const handlePathChanges = () => {
- const hook = (_this, method, callback) => {
- const orig = _this[method];
- return (...args) => {
- callback.apply(null, args);
- return orig.apply(_this, args);
- };
- };
-
- history.pushState = hook(history, 'pushState', handlePush);
- history.replaceState = hook(history, 'replaceState', handlePush);
- };
-
- const handleClicks = () => {
- const trackElement = async el => {
- const eventName = el.getAttribute(eventNameAttribute);
- if (eventName) {
- const eventData = {};
-
- el.getAttributeNames().forEach(name => {
- const match = name.match(eventRegex);
- if (match) eventData[match[1]] = el.getAttribute(name);
- });
-
- return track(eventName, eventData);
- }
- };
- const onClick = async e => {
- const el = e.target;
- const parentElement = el.closest('a,button');
- if (!parentElement) return trackElement(el);
-
- const { href, target } = parentElement;
- if (!parentElement.getAttribute(eventNameAttribute)) return;
-
- if (parentElement.tagName === 'BUTTON') {
- return trackElement(parentElement);
- }
- if (parentElement.tagName === 'A' && href) {
- const external =
- target === '_blank' ||
- e.ctrlKey ||
- e.shiftKey ||
- e.metaKey ||
- (e.button && e.button === 1);
- if (!external) e.preventDefault();
- return trackElement(parentElement).then(() => {
- if (!external) {
- (target === '_top' ? top.location : location).href = href;
- }
- });
- }
- };
- document.addEventListener('click', onClick, true);
- };
-
- /* Tracking functions */
-
- const trackingDisabled = () =>
- disabled ||
- !website ||
- localStorage?.getItem('umami.disabled') ||
- (domain && !domains.includes(hostname)) ||
- (dnt && hasDoNotTrack());
-
- const send = async (payload, type = 'event') => {
- if (trackingDisabled()) return;
-
- const callback = window[beforeSend];
-
- if (typeof callback === 'function') {
- payload = await Promise.resolve(callback(type, payload));
- }
-
- if (!payload) return;
-
- try {
- const res = await fetch(endpoint, {
- keepalive: true,
- method: 'POST',
- body: JSON.stringify({ type, payload }),
- headers: {
- 'Content-Type': 'application/json',
- ...(typeof cache !== 'undefined' && { 'x-umami-cache': cache }),
- },
- credentials,
- });
-
- const data = await res.json();
- if (data) {
- disabled = !!data.disabled;
- cache = data.cache;
- }
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- } catch (_e) {
- /* no-op */
- }
- };
-
- const init = () => {
- if (!initialized) {
- initialized = true;
- track();
- handlePathChanges();
- handleClicks();
- if (perf) initPerformance();
- }
- };
-
- const track = (name, data) => {
- if (typeof name === 'string') return send({ ...getPayload(), name, data });
- if (typeof name === 'object') return send({ ...name });
- if (typeof name === 'function') return send(name(getPayload()));
- return send(getPayload());
- };
-
- const identify = (id, data) => {
- if (typeof id === 'string') {
- identity = id;
- }
-
- cache = '';
- return send(
- {
- ...getPayload(),
- data: typeof id === 'object' ? id : data,
- },
- 'identify',
- );
- };
-
- /* Performance */
-
- const initPerformance = () => {
- const metrics = {};
- let sent = false;
- let timeoutId;
- let isInitialLoad = true;
- let activationStart = 0;
- let pageStartTime = 0;
-
- const observe = (type, callback) => {
- try {
- const observer = new PerformanceObserver(list => {
- list.getEntries().forEach(callback);
- });
- observer.observe({ type, buffered: true });
- } catch {
- /* not supported */
- }
- };
-
- // TTFB
- observe('navigation', entry => {
- activationStart = entry.activationStart || 0;
- metrics.ttfb = Math.max(entry.responseStart - activationStart, 0);
- });
-
- // FCP
- observe('paint', entry => {
- if (entry.name === 'first-contentful-paint') {
- metrics.fcp = Math.max(entry.startTime - activationStart, 0);
- }
- });
-
- // LCP
- observe('largest-contentful-paint', entry => {
- metrics.lcp = Math.max(entry.startTime - activationStart, 0);
- });
-
- // CLS - session windows algorithm (gap < 1s, max 5s duration; report worst window)
- let clsSessionValue = 0;
- let clsSessionEntries = [];
- observe('layout-shift', entry => {
- if (!entry.hadRecentInput) {
- const lastEntry = clsSessionEntries[clsSessionEntries.length - 1];
- const firstEntry = clsSessionEntries[0];
- if (
- lastEntry &&
- entry.startTime - lastEntry.startTime - lastEntry.duration < 1000 &&
- entry.startTime - firstEntry.startTime < 5000
- ) {
- clsSessionValue += entry.value;
- clsSessionEntries.push(entry);
- } else {
- clsSessionValue = entry.value;
- clsSessionEntries = [entry];
- }
- if (clsSessionValue > (metrics.cls || 0)) {
- metrics.cls = clsSessionValue;
- }
- }
- });
-
- // INP - group by interactionId, 98th percentile, 40ms threshold
- let interactions = {};
- try {
- const observer = new PerformanceObserver(list => {
- list.getEntries().forEach(entry => {
- if (entry.interactionId) {
- const existing = interactions[entry.interactionId];
- if (!existing || entry.duration > existing) {
- interactions[entry.interactionId] = entry.duration;
- }
- const values = Object.values(interactions).sort((a, b) => b - a);
- if (values.length) {
- const p98Index = Math.floor(Math.max(values.length, 10) * 0.02);
- metrics.inp = values[Math.min(p98Index, values.length - 1)];
- }
- }
- });
- });
- observer.observe({ type: 'event', buffered: true, durationThreshold: 40 });
- } catch {
- /* not supported */
- }
-
- const getEntriesByType = type => {
- try {
- return window.performance?.getEntriesByType?.(type) || [];
- } catch {
- return [];
- }
- };
-
- const applyFallbackMetrics = () => {
- if (!isInitialLoad) return;
-
- if (metrics.ttfb === undefined) {
- const navigation = getEntriesByType('navigation')?.[0];
- if (navigation) {
- metrics.ttfb = Math.max(navigation.responseStart - (navigation.activationStart || 0), 0);
- }
- }
-
- if (metrics.fcp === undefined) {
- const fcpEntry = getEntriesByType('paint')?.find(
- entry => entry.name === 'first-contentful-paint',
- );
- if (fcpEntry) {
- metrics.fcp = Math.max(fcpEntry.startTime - activationStart, 0);
- }
- }
-
- if (metrics.lcp === undefined) {
- const lcpEntries = getEntriesByType('largest-contentful-paint');
- const lcpEntry = lcpEntries?.[lcpEntries.length - 1];
- if (lcpEntry) {
- metrics.lcp = Math.max(lcpEntry.startTime - activationStart, 0);
- }
- }
- };
-
- const sendPerformance = () => {
- if (sent) return;
-
- applyFallbackMetrics();
- metrics.duration = Math.round(performance.now() - pageStartTime);
-
- sent = true;
- if (timeoutId) clearTimeout(timeoutId);
- send({ ...getPayload(), ...metrics }, 'performance');
- };
-
- flushPerformance = () => {
- sendPerformance();
- isInitialLoad = false;
- Object.keys(metrics).forEach(k => {
- delete metrics[k];
- });
- activationStart = 0;
- pageStartTime = performance.now();
- clsSessionValue = 0;
- clsSessionEntries = [];
- interactions = {};
- sent = false;
- if (timeoutId) clearTimeout(timeoutId);
- timeoutId = setTimeout(sendPerformance, 10000);
- };
- timeoutId = setTimeout(sendPerformance, 10000);
-
- document.addEventListener('visibilitychange', () => {
- if (document.visibilityState === 'hidden') sendPerformance();
- });
- window.addEventListener('pagehide', sendPerformance);
- };
-
- /* Start */
-
- if (!window.umami) {
- window.umami = {
- track,
- identify,
- getSession: () => ({ cache, website }),
- };
- }
-
- let currentUrl = normalize(href);
- let currentRef = normalize(referrer.startsWith(origin) ? '' : referrer);
-
- let initialized = false;
- let disabled = false;
- let cache;
- let identity;
- let flushPerformance;
-
- if (autoTrack && !trackingDisabled()) {
- if (document.readyState === 'complete') {
- init();
- } else {
- document.addEventListener('readystatechange', init, true);
- }
- }
-})(window);
diff --git a/src/tracker/index.ts b/src/tracker/index.ts
new file mode 100644
index 000000000..ab5d868e1
--- /dev/null
+++ b/src/tracker/index.ts
@@ -0,0 +1,649 @@
+/** Public types for the browser tracker. */
+export type TrackedProperties = {
+ /**
+ * Hostname of server
+ *
+ * @description extracted from `window.location.hostname`
+ * @example 'analytics.umami.is'
+ */
+ hostname?: string;
+
+ /** Distinct ID associated with the current visitor. */
+ id?: string;
+
+ /**
+ * Browser language
+ *
+ * @description extracted from `window.navigator.language`
+ * @example 'en-US', 'fr-FR'
+ */
+ language?: string;
+
+ /**
+ * Page referrer
+ *
+ * @description extracted from `document.referrer`
+ * @example 'https://analytics.umami.is/docs/getting-started'
+ */
+ referrer?: string;
+
+ /**
+ * Screen dimensions
+ *
+ * @description extracted from `window.screen.width` and `window.screen.height`
+ * @example '1920x1080', '2560x1440'
+ */
+ screen?: string;
+
+ /** Tag configured on the tracker script. */
+ tag?: string;
+
+ /**
+ * Page title
+ *
+ * @description extracted from `document.querySelector('head > title')`
+ * @example 'umami'
+ */
+ title?: string;
+
+ /**
+ * Page url
+ *
+ * @description normalized from `window.location.href`
+ * @example 'https://analytics.umami.is/docs/getting-started'
+ */
+ url?: string;
+
+ /**
+ * Website ID (required)
+ *
+ * @example 'b59e9c65-ae32-47f1-8400-119fcf4861c4'
+ */
+ website: string;
+};
+
+export type WithRequired = T & { [P in K]-?: T[P] };
+
+export type EventDataValue = boolean | number | string | null | EventData | EventDataValue[];
+
+/**
+ *
+ * Event Data can work with any JSON data. There are a few rules in place to maintain performance.
+ * - Numbers have a max precision of 4.
+ * - Strings have a max length of 500.
+ * - Arrays are converted to a String, with the same max length of 500.
+ * - Objects have a max of 50 properties. Arrays are considered 1 property.
+ */
+export interface EventData {
+ [key: string]: EventDataValue;
+}
+
+export type EventProperties = {
+ /**
+ * NOTE: event names will be truncated past 50 characters
+ */
+ name: string;
+ data?: EventData;
+} & WithRequired;
+export type PageViewProperties = WithRequired;
+export type CustomEventFunction = (
+ props: PageViewProperties,
+) => EventProperties | PageViewProperties;
+
+export type UmamiTracker = {
+ track: {
+ /**
+ * Track a page view
+ *
+ * @example ```
+ * umami.track();
+ * ```
+ */
+ (): Promise;
+
+ /**
+ * Track an event with a given name
+ *
+ * NOTE: event names will be truncated past 50 characters
+ *
+ * @example ```
+ * umami.track('signup-button');
+ * ```
+ */
+ (eventName: string): Promise;
+
+ /**
+ * Tracks an event with dynamic data.
+ *
+ * NOTE: event names will be truncated past 50 characters
+ *
+ * When tracking events, the default properties are included in the payload. This is equivalent to running:
+ *
+ * ```js
+ * umami.track(props => ({
+ * ...props,
+ * name: 'signup-button',
+ * data: {
+ * name: 'newsletter',
+ * id: 123
+ * }
+ * }));
+ * ```
+ *
+ * @example ```
+ * umami.track('signup-button', { name: 'newsletter', id: 123 });
+ * ```
+ */
+ (eventName: string, obj: EventData): Promise;
+
+ /**
+ * Tracks a page view with custom properties
+ *
+ * @example ```
+ * umami.track({ website: 'e676c9b4-11e4-4ef1-a4d7-87001773e9f2', url: '/home', title: 'Home page' });
+ * ```
+ */
+ (properties: PageViewProperties): Promise;
+
+ /**
+ * Tracks an event with fully customizable dynamic data
+ * If you don't specify any `name` and/or `data`, it will be treated as a page view
+ *
+ * @example ```
+ * umami.track((props) => ({ ...props, url: path }));
+ * ```
+ */
+ (eventFunction: CustomEventFunction): Promise;
+ };
+ identify: {
+ /**
+ * Identify a visitor with optional associated data.
+ *
+ * @example ```
+ * umami.identify('user-123', { plan: 'pro' });
+ * ```
+ */
+ (id: string, data?: EventData): Promise;
+
+ /**
+ * Associate data with the current visitor. An `id` string sets the Distinct ID.
+ *
+ * @example ```
+ * umami.identify({ id: 'user-123', plan: 'pro' });
+ * ```
+ */
+ (data: EventData & { id?: string }): Promise;
+ };
+ getSession: () => {
+ cache: string | undefined;
+ website: string | null;
+ };
+};
+
+declare global {
+ interface Window {
+ umami: UmamiTracker;
+ }
+}
+
+type Payload = Record;
+type BeforeSend = (
+ type: string,
+ payload: Payload,
+) => Payload | null | undefined | Promise;
+type TrackerWindow = Window &
+ typeof globalThis & {
+ doNotTrack?: string | number | null;
+ navigator: Navigator & {
+ msDoNotTrack?: string | number | null;
+ };
+ };
+type TrackerDocument = Document & {
+ currentScript: HTMLScriptElement | null;
+};
+type MetricEntry = PerformanceEntry & {
+ activationStart: number;
+ duration: number;
+ hadRecentInput: boolean;
+ interactionId: number;
+ responseStart: number;
+ startTime: number;
+ value: number;
+};
+(window => {
+ const {
+ screen: { width, height },
+ navigator: { language, doNotTrack: ndnt, msDoNotTrack: msdnt },
+ location,
+ document,
+ history,
+ top,
+ doNotTrack,
+ } = window;
+ const { currentScript, referrer } = document as TrackerDocument;
+ if (!currentScript) return;
+
+ const { hostname, href, origin } = location;
+
+ let localStorage: Storage | undefined;
+ try {
+ localStorage = href.startsWith('data:') ? undefined : window.localStorage;
+ } catch {
+ /* (DOMException) SecurityError: Access is denied for this document. */
+ }
+
+ const _data = 'data-';
+ const _false = 'false';
+ const _true = 'true';
+ const attr = currentScript.getAttribute.bind(currentScript);
+ const config = (value: string) => attr(`${_data}${value}`);
+
+ const website = config('website-id');
+ const hostUrl = config('host-url');
+ const beforeSend = config('before-send');
+ const tag = config('tag') || undefined;
+ const autoTrack = config('auto-track') !== _false;
+ const dnt = config('do-not-track') === _true;
+ const excludeSearch = config('exclude-search') === _true;
+ const excludeHash = config('exclude-hash') === _true;
+ const domain = config('domains') || '';
+ const credentials = (config('fetch-credentials') || 'omit') as RequestCredentials;
+ const perf = config('performance') === _true;
+ const autoPageview = config('auto-pageview') !== _false;
+
+ const domains = domain.split(',').map(n => n.trim());
+ const host =
+ hostUrl || '__COLLECT_API_HOST__' || currentScript.src.split('/').slice(0, -1).join('/');
+ const endpoint = `${host.replace(/\/$/, '')}__COLLECT_API_ENDPOINT__`;
+ const screen = `${width}x${height}`;
+ const eventRegex = /data-umami-event-([\w-_]+)/;
+ const eventNameAttribute = `${_data}umami-event`;
+ const delayDuration = 300;
+
+ /* Helper functions */
+
+ const normalize = (raw: string | URL): string => {
+ if (!raw) return raw as string;
+ try {
+ const u = new URL(raw, location.href);
+ if (excludeSearch) u.search = '';
+ if (excludeHash) u.hash = '';
+ return u.toString();
+ } catch {
+ return raw as string;
+ }
+ };
+
+ const getPayload = () => ({
+ website,
+ screen,
+ language,
+ title: document.title,
+ hostname,
+ url: currentUrl,
+ referrer: currentRef,
+ tag,
+ id: identity ? identity : undefined,
+ });
+
+ const hasDoNotTrack = () => {
+ const dnt = doNotTrack || ndnt || msdnt;
+ return dnt === 1 || dnt === '1' || dnt === 'yes';
+ };
+
+ /* Event handlers */
+
+ const handlePush = (_state: unknown, _title: string, url?: string | URL | null) => {
+ if (!url) return;
+
+ if (typeof flushPerformance === 'function') {
+ flushPerformance();
+ }
+
+ currentRef = currentUrl;
+ currentUrl = normalize(url);
+
+ if (currentUrl !== currentRef && autoPageview) {
+ setTimeout(track, delayDuration);
+ }
+ };
+
+ const handlePathChanges = () => {
+ const hook = (
+ _this: History,
+ method: 'pushState' | 'replaceState',
+ callback: typeof handlePush,
+ ) => {
+ const orig = _this[method];
+ return (...args: Parameters) => {
+ const result = orig.apply(_this, args);
+ callback.apply(null, args);
+ return result;
+ };
+ };
+
+ history.pushState = hook(history, 'pushState', handlePush);
+ history.replaceState = hook(history, 'replaceState', handlePush);
+ };
+
+ const handleClicks = () => {
+ const trackElement = async (el: Element) => {
+ const eventName = el.getAttribute(eventNameAttribute);
+ if (eventName) {
+ const eventData: EventData = {};
+
+ el.getAttributeNames().forEach(name => {
+ const match = name.match(eventRegex);
+ if (match) eventData[match[1]] = el.getAttribute(name) as string;
+ });
+
+ return track(eventName, eventData);
+ }
+ };
+ const onClick = (e: MouseEvent) => {
+ const el = e.target as Element;
+ const eventEl = el.closest(`[${eventNameAttribute}]`);
+ if (!eventEl) return;
+
+ if (eventEl.tagName === 'A' && (eventEl as HTMLAnchorElement).href) {
+ const { href, target } = eventEl as HTMLAnchorElement;
+ const external =
+ target === '_blank' ||
+ e.ctrlKey ||
+ e.shiftKey ||
+ e.metaKey ||
+ (e.button && e.button === 1);
+ if (!external) e.preventDefault();
+ return trackElement(eventEl).finally(() => {
+ if (!external) {
+ (target === '_top' ? (top as WindowProxy).location : location).href = href;
+ }
+ });
+ }
+
+ return trackElement(eventEl);
+ };
+ document.addEventListener('click', onClick, true);
+ };
+
+ /* Tracking functions */
+
+ const trackingDisabled = () =>
+ disabled ||
+ !website ||
+ localStorage?.getItem('umami.disabled') ||
+ (domain && !domains.includes(hostname)) ||
+ (dnt && hasDoNotTrack());
+
+ const send = async (payload: Payload | null | undefined, type = 'event'): Promise => {
+ if (trackingDisabled()) return;
+
+ const callback = (window as unknown as Record)[beforeSend as string] as
+ | BeforeSend
+ | undefined;
+
+ if (typeof callback === 'function') {
+ payload = await Promise.resolve(callback(type, payload as Payload));
+ }
+
+ if (!payload) return;
+
+ try {
+ const res = await fetch(endpoint, {
+ keepalive: true,
+ method: 'POST',
+ body: JSON.stringify({ type, payload }),
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-umami-website-id': website as string,
+ 'x-umami-hostname': hostname,
+ ...(typeof cache !== 'undefined' && { 'x-umami-cache': cache }),
+ },
+ credentials,
+ });
+
+ const data = (await res.json()) as { cache?: string; disabled?: boolean } | null;
+ if (data) {
+ disabled = !!data.disabled;
+ cache = data.cache;
+ }
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ } catch (_e) {
+ /* no-op */
+ }
+ };
+
+ const init = () => {
+ if (!initialized) {
+ initialized = true;
+ if (autoPageview) track();
+ handlePathChanges();
+ handleClicks();
+ if (perf) initPerformance();
+ }
+ };
+
+ const track = (
+ name?: string | Payload | ((payload: Payload) => Payload),
+ data?: EventData,
+ ): Promise => {
+ if (typeof name === 'string') return send({ ...getPayload(), name, data });
+ if (typeof name === 'object') return send({ ...name });
+ if (typeof name === 'function') return send(name(getPayload()));
+ return send(getPayload());
+ };
+
+ const identify = (
+ id: string | (EventData & { id?: string }),
+ data?: EventData,
+ ): Promise => {
+ const nextIdentity = typeof id === 'string' ? id : id.id;
+
+ if (nextIdentity !== undefined) {
+ identity = nextIdentity;
+ }
+
+ cache = '';
+ return send(
+ {
+ ...getPayload(),
+ data: typeof id === 'object' ? id : data,
+ },
+ 'identify',
+ );
+ };
+
+ /* Performance */
+
+ const initPerformance = () => {
+ const metrics: Record = {};
+ let sent = false;
+ let timeoutId: ReturnType | undefined;
+ let isInitialLoad = true;
+ let activationStart = 0;
+ let pageStartTime = 0;
+
+ const observe = (type: string, callback: (entry: MetricEntry) => void) => {
+ try {
+ const observer = new PerformanceObserver(list => {
+ (list.getEntries() as MetricEntry[]).forEach(callback);
+ });
+ observer.observe({ type, buffered: true });
+ } catch {
+ /* not supported */
+ }
+ };
+
+ // TTFB
+ observe('navigation', entry => {
+ activationStart = entry.activationStart || 0;
+ metrics.ttfb = Math.max(entry.responseStart - activationStart, 0);
+ });
+
+ // FCP
+ observe('paint', entry => {
+ if (entry.name === 'first-contentful-paint') {
+ metrics.fcp = Math.max(entry.startTime - activationStart, 0);
+ }
+ });
+
+ // LCP
+ observe('largest-contentful-paint', entry => {
+ metrics.lcp = Math.max(entry.startTime - activationStart, 0);
+ });
+
+ // CLS - session windows algorithm (gap < 1s, max 5s duration; report worst window)
+ let clsSessionValue = 0;
+ let clsSessionEntries: MetricEntry[] = [];
+ observe('layout-shift', entry => {
+ if (!entry.hadRecentInput) {
+ const lastEntry = clsSessionEntries[clsSessionEntries.length - 1];
+ const firstEntry = clsSessionEntries[0];
+ if (
+ lastEntry &&
+ entry.startTime - lastEntry.startTime - lastEntry.duration < 1000 &&
+ entry.startTime - firstEntry.startTime < 5000
+ ) {
+ clsSessionValue += entry.value;
+ clsSessionEntries.push(entry);
+ } else {
+ clsSessionValue = entry.value;
+ clsSessionEntries = [entry];
+ }
+ if (clsSessionValue > (metrics.cls || 0)) {
+ metrics.cls = clsSessionValue;
+ }
+ }
+ });
+
+ // INP - group by interactionId, 98th percentile, 40ms threshold
+ let interactions: Record = {};
+ let inpObserver: PerformanceObserver | undefined;
+ const recordInteractions = (entries: PerformanceEntryList) => {
+ (entries as MetricEntry[]).forEach(entry => {
+ if (entry.interactionId) {
+ const existing = interactions[entry.interactionId];
+ if (!existing || entry.duration > existing) {
+ interactions[entry.interactionId] = entry.duration;
+ }
+ }
+ });
+ };
+ try {
+ inpObserver = new PerformanceObserver(list => recordInteractions(list.getEntries()));
+ inpObserver.observe({
+ type: 'event',
+ buffered: true,
+ durationThreshold: 40,
+ } as PerformanceObserverInit);
+ } catch {
+ /* not supported */
+ }
+
+ const computeInp = () => {
+ if (inpObserver) recordInteractions(inpObserver.takeRecords());
+ const values = Object.values(interactions).sort((a, b) => b - a);
+ if (values.length) {
+ const p98Index = Math.floor(Math.max(values.length, 10) * 0.02);
+ metrics.inp = values[Math.min(p98Index, values.length - 1)];
+ }
+ };
+
+ const getEntriesByType = (type: string): MetricEntry[] => {
+ try {
+ return (window.performance?.getEntriesByType?.(type) as MetricEntry[]) || [];
+ } catch {
+ return [];
+ }
+ };
+
+ const applyFallbackMetrics = () => {
+ if (!isInitialLoad) return;
+
+ if (metrics.ttfb === undefined) {
+ const navigation = getEntriesByType('navigation')?.[0];
+ if (navigation) {
+ metrics.ttfb = Math.max(navigation.responseStart - (navigation.activationStart || 0), 0);
+ }
+ }
+
+ if (metrics.fcp === undefined) {
+ const fcpEntry = getEntriesByType('paint')?.find(
+ entry => entry.name === 'first-contentful-paint',
+ );
+ if (fcpEntry) {
+ metrics.fcp = Math.max(fcpEntry.startTime - activationStart, 0);
+ }
+ }
+
+ if (metrics.lcp === undefined) {
+ const lcpEntries = getEntriesByType('largest-contentful-paint');
+ const lcpEntry = lcpEntries?.[lcpEntries.length - 1];
+ if (lcpEntry) {
+ metrics.lcp = Math.max(lcpEntry.startTime - activationStart, 0);
+ }
+ }
+ };
+
+ const sendPerformance = () => {
+ if (sent) return;
+
+ applyFallbackMetrics();
+ computeInp();
+ metrics.duration = Math.round(performance.now() - pageStartTime);
+
+ sent = true;
+ if (timeoutId) clearTimeout(timeoutId);
+ send({ ...getPayload(), ...metrics }, 'performance');
+ };
+
+ flushPerformance = () => {
+ sendPerformance();
+ isInitialLoad = false;
+ Object.keys(metrics).forEach(k => {
+ delete metrics[k];
+ });
+ activationStart = 0;
+ pageStartTime = performance.now();
+ clsSessionValue = 0;
+ clsSessionEntries = [];
+ interactions = {};
+ sent = false;
+ if (timeoutId) clearTimeout(timeoutId);
+ timeoutId = setTimeout(sendPerformance, 10000);
+ };
+ timeoutId = setTimeout(sendPerformance, 10000);
+
+ document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState === 'hidden') sendPerformance();
+ });
+ window.addEventListener('pagehide', sendPerformance);
+ };
+
+ /* Start */
+
+ if (!window.umami) {
+ window.umami = {
+ track,
+ identify,
+ getSession: () => ({ cache, website }),
+ } as UmamiTracker;
+ }
+
+ let currentUrl = normalize(href);
+ let currentRef = normalize(referrer.startsWith(origin) ? '' : referrer);
+
+ let initialized = false;
+ let disabled = false;
+ let cache: string | undefined;
+ let identity: string | undefined;
+ let flushPerformance: (() => void) | undefined;
+
+ if (autoTrack && !trackingDisabled()) {
+ if (document.readyState === 'complete') {
+ init();
+ } else {
+ document.addEventListener('readystatechange', init, true);
+ }
+ }
+})(window as TrackerWindow);
diff --git a/tests/e2e/api-team.spec.ts b/tests/e2e/api-team.spec.ts
new file mode 100644
index 000000000..d5fe8b2fc
--- /dev/null
+++ b/tests/e2e/api-team.spec.ts
@@ -0,0 +1,153 @@
+import { expect, test } from '@playwright/test';
+import { teams, users } from './fixtures';
+import { type Auth, authHeaders, deleteUser, loginViaApi, umamiUser } from './helpers';
+
+test.describe('Team API tests', () => {
+ test.describe.configure({ mode: 'serial' });
+
+ let auth: Auth;
+ let teamId = '';
+ let userId = '';
+
+ test.beforeAll(async ({ request }) => {
+ auth = await loginViaApi(request);
+
+ const response = await request.post('/api/users', {
+ headers: authHeaders(auth),
+ data: users.userCreate,
+ });
+ const body = await response.json();
+
+ userId = body.id;
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('username', 'playwright1');
+ expect(body).toHaveProperty('role', 'user');
+ });
+
+ test.afterAll(async ({ request }) => {
+ if (userId) {
+ await deleteUser(request, auth, userId);
+ }
+ });
+
+ test('creates a team', async ({ request }) => {
+ const response = await request.post('/api/teams', {
+ headers: authHeaders(auth),
+ data: teams.teamCreate,
+ });
+ const body = await response.json();
+
+ teamId = body[0].id;
+
+ expect(response.status()).toBe(200);
+ expect(body[0]).toHaveProperty('name', 'playwright');
+ expect(body[1]).toHaveProperty('role', 'team-owner');
+ });
+
+ test('gets a team by ID', async ({ request }) => {
+ const response = await request.get(`/api/teams/${teamId}`, {
+ headers: authHeaders(auth),
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('id', teamId);
+ });
+
+ test('updates a team', async ({ request }) => {
+ const response = await request.post(`/api/teams/${teamId}`, {
+ headers: authHeaders(auth),
+ data: teams.teamUpdate,
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('id', teamId);
+ expect(body).toHaveProperty('name', 'playwrightUpdate');
+ });
+
+ test('gets all users that belong to a team', async ({ request }) => {
+ const response = await request.get(`/api/teams/${teamId}/users`, {
+ headers: authHeaders(auth),
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body.data[0]).toHaveProperty('id');
+ expect(body.data[0]).toHaveProperty('teamId');
+ expect(body.data[0]).toHaveProperty('userId');
+ expect(body.data[0]).toHaveProperty('user');
+ });
+
+ test('gets a user belonging to a team', async ({ request }) => {
+ const response = await request.get(`/api/teams/${teamId}/users/${umamiUser.id}`, {
+ headers: authHeaders(auth),
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('teamId');
+ expect(body).toHaveProperty('userId');
+ expect(body).toHaveProperty('role');
+ });
+
+ test('gets all websites belonging to a team', async ({ request }) => {
+ const response = await request.get(`/api/teams/${teamId}/websites`, {
+ headers: authHeaders(auth),
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('data');
+ });
+
+ test('adds a user to a team', async ({ request }) => {
+ const response = await request.post(`/api/teams/${teamId}/users`, {
+ headers: authHeaders(auth),
+ data: {
+ userId,
+ role: 'team-member',
+ },
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('userId', userId);
+ expect(body).toHaveProperty('role', 'team-member');
+ });
+
+ test('updates a user role on a team', async ({ request }) => {
+ const response = await request.post(`/api/teams/${teamId}/users/${userId}`, {
+ headers: authHeaders(auth),
+ data: {
+ role: 'team-view-only',
+ },
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('userId', userId);
+ expect(body).toHaveProperty('role', 'team-view-only');
+ });
+
+ test('removes a user from a team', async ({ request }) => {
+ const response = await request.delete(`/api/teams/${teamId}/users/${userId}`, {
+ headers: authHeaders(auth),
+ });
+
+ expect(response.status()).toBe(200);
+ });
+
+ test('deletes a team', async ({ request }) => {
+ const response = await request.delete(`/api/teams/${teamId}`, {
+ headers: authHeaders(auth),
+ });
+ const body = await response.json();
+
+ teamId = '';
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('ok', true);
+ });
+});
diff --git a/tests/e2e/api-user.spec.ts b/tests/e2e/api-user.spec.ts
new file mode 100644
index 000000000..cb94c769d
--- /dev/null
+++ b/tests/e2e/api-user.spec.ts
@@ -0,0 +1,98 @@
+import { expect, test } from '@playwright/test';
+import { users } from './fixtures';
+import { type Auth, authHeaders, loginViaApi } from './helpers';
+
+test.describe('User API tests', () => {
+ test.describe.configure({ mode: 'serial' });
+
+ let auth: Auth;
+ let userId = '';
+
+ test.beforeAll(async ({ request }) => {
+ auth = await loginViaApi(request);
+ });
+
+ test('creates a user', async ({ request }) => {
+ const response = await request.post('/api/users', {
+ headers: authHeaders(auth),
+ data: users.userCreate,
+ });
+ const body = await response.json();
+
+ userId = body.id;
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('username', 'playwright1');
+ expect(body).toHaveProperty('role', 'user');
+ });
+
+ test('returns all users when admin access is used', async ({ request }) => {
+ const response = await request.get('/api/admin/users', {
+ headers: authHeaders(auth),
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body.data[0]).toHaveProperty('id');
+ expect(body.data[0]).toHaveProperty('username');
+ expect(body.data[0]).toHaveProperty('password');
+ expect(body.data[0]).toHaveProperty('role');
+ });
+
+ test('updates a user', async ({ request }) => {
+ const response = await request.post(`/api/users/${userId}`, {
+ headers: authHeaders(auth),
+ data: users.userUpdate,
+ });
+ const body = await response.json();
+
+ userId = body.id;
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('id', userId);
+ expect(body).toHaveProperty('username', 'playwright1');
+ expect(body).toHaveProperty('role', 'view-only');
+ });
+
+ test('gets a user by ID', async ({ request }) => {
+ const response = await request.get(`/api/users/${userId}`, {
+ headers: authHeaders(auth),
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('id', userId);
+ expect(body).toHaveProperty('username', 'playwright1');
+ expect(body).toHaveProperty('role', 'view-only');
+ });
+
+ test('deletes a user', async ({ request }) => {
+ const response = await request.delete(`/api/users/${userId}`, {
+ headers: authHeaders(auth),
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('ok', true);
+ });
+
+ test('gets all websites that belong to a user', async ({ request }) => {
+ const response = await request.get(`/api/users/${userId}/websites`, {
+ headers: authHeaders(auth),
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('data');
+ });
+
+ test('gets all teams that belong to a user', async ({ request }) => {
+ const response = await request.get(`/api/users/${userId}/teams`, {
+ headers: authHeaders(auth),
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('data');
+ });
+});
diff --git a/tests/e2e/api-website.spec.ts b/tests/e2e/api-website.spec.ts
new file mode 100644
index 000000000..3fd585def
--- /dev/null
+++ b/tests/e2e/api-website.spec.ts
@@ -0,0 +1,152 @@
+import { expect, test } from '@playwright/test';
+import { uuid } from '../../src/lib/crypto';
+import { teams, websites } from './fixtures';
+import { type Auth, authHeaders, deleteTeam, loginViaApi } from './helpers';
+
+test.describe('Website API tests', () => {
+ test.describe.configure({ mode: 'serial' });
+
+ let auth: Auth;
+ let websiteId = '';
+ let teamId = '';
+
+ test.beforeAll(async ({ request }) => {
+ auth = await loginViaApi(request);
+
+ const response = await request.post('/api/teams', {
+ headers: authHeaders(auth),
+ data: teams.teamCreate,
+ });
+ const body = await response.json();
+
+ teamId = body[0].id;
+
+ expect(response.status()).toBe(200);
+ expect(body[0]).toHaveProperty('name', 'playwright');
+ expect(body[1]).toHaveProperty('role', 'team-owner');
+ });
+
+ test.afterAll(async ({ request }) => {
+ if (teamId) {
+ await deleteTeam(request, auth, teamId);
+ }
+ });
+
+ test('creates a website for user', async ({ request }) => {
+ const response = await request.post('/api/websites', {
+ headers: authHeaders(auth),
+ data: websites.websiteCreate,
+ });
+ const body = await response.json();
+
+ websiteId = body.id;
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('name', 'Playwright Website');
+ expect(body).toHaveProperty('domain', 'playwright.com');
+ });
+
+ test('creates a website for team', async ({ request }) => {
+ const response = await request.post('/api/websites', {
+ headers: authHeaders(auth),
+ data: {
+ name: 'Team Website',
+ domain: 'teamwebsite.com',
+ teamId,
+ },
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('name', 'Team Website');
+ expect(body).toHaveProperty('domain', 'teamwebsite.com');
+ });
+
+ test('creates a website with a fixed ID', async ({ request }) => {
+ const fixedId = uuid();
+ const response = await request.post('/api/websites', {
+ headers: authHeaders(auth),
+ data: { ...websites.websiteCreate, id: fixedId },
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('id', fixedId);
+ expect(body).toHaveProperty('name', 'Playwright Website');
+ expect(body).toHaveProperty('domain', 'playwright.com');
+
+ await request.delete(`/api/websites/${fixedId}`, {
+ headers: authHeaders(auth),
+ });
+ });
+
+ test('returns all tracked websites', async ({ request }) => {
+ const response = await request.get('/api/websites', {
+ headers: authHeaders(auth),
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body.data[0]).toHaveProperty('id');
+ expect(body.data[0]).toHaveProperty('name');
+ expect(body.data[0]).toHaveProperty('domain');
+ });
+
+ test('gets a website by ID', async ({ request }) => {
+ const response = await request.get(`/api/websites/${websiteId}`, {
+ headers: authHeaders(auth),
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('name', 'Playwright Website');
+ expect(body).toHaveProperty('domain', 'playwright.com');
+ });
+
+ test('updates a website', async ({ request }) => {
+ const response = await request.post(`/api/websites/${websiteId}`, {
+ headers: authHeaders(auth),
+ data: websites.websiteUpdate,
+ });
+ const body = await response.json();
+
+ websiteId = body.id;
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('name', 'Playwright Website Updated');
+ expect(body).toHaveProperty('domain', 'playwrightupdated.com');
+ });
+
+ test('updates a website with only shareId', async ({ request }) => {
+ const response = await request.post(`/api/websites/${websiteId}`, {
+ headers: authHeaders(auth),
+ data: { shareId: 'ABCDEF' },
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('shareId', 'ABCDEF');
+ });
+
+ test('resets a website by removing all data related to the website', async ({ request }) => {
+ const response = await request.post(`/api/websites/${websiteId}/reset`, {
+ headers: authHeaders(auth),
+ });
+ const body = await response.json();
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('ok', true);
+ });
+
+ test('deletes a website', async ({ request }) => {
+ const response = await request.delete(`/api/websites/${websiteId}`, {
+ headers: authHeaders(auth),
+ });
+ const body = await response.json();
+
+ websiteId = '';
+
+ expect(response.status()).toBe(200);
+ expect(body).toHaveProperty('ok', true);
+ });
+});
diff --git a/tests/e2e/fixtures.ts b/tests/e2e/fixtures.ts
new file mode 100644
index 000000000..860e15ffb
--- /dev/null
+++ b/tests/e2e/fixtures.ts
@@ -0,0 +1,31 @@
+export const users = {
+ userCreate: {
+ username: 'playwright1',
+ password: 'password',
+ role: 'user',
+ },
+ userUpdate: {
+ username: 'playwright1',
+ role: 'view-only',
+ },
+};
+
+export const teams = {
+ teamCreate: {
+ name: 'playwright',
+ },
+ teamUpdate: {
+ name: 'playwrightUpdate',
+ },
+};
+
+export const websites = {
+ websiteCreate: {
+ name: 'Playwright Website',
+ domain: 'playwright.com',
+ },
+ websiteUpdate: {
+ name: 'Playwright Website Updated',
+ domain: 'playwrightupdated.com',
+ },
+};
diff --git a/tests/e2e/helpers.ts b/tests/e2e/helpers.ts
new file mode 100644
index 000000000..8eae8dac1
--- /dev/null
+++ b/tests/e2e/helpers.ts
@@ -0,0 +1,120 @@
+import { type APIRequestContext, expect, type Page } from '@playwright/test';
+import { uuid } from '../../src/lib/crypto';
+
+export type Auth = {
+ token: string;
+ authorization: string;
+};
+
+export const umamiUser = {
+ username: process.env.UMAMI_USER ?? 'admin',
+ password: process.env.UMAMI_PASSWORD ?? 'umami',
+ id: process.env.UMAMI_USER_ID ?? '41e2b680-648e-4b09-bcd7-3e2b10c06264',
+};
+
+export const authHeaders = (auth: Auth) => ({
+ 'Content-Type': 'application/json',
+ Authorization: auth.authorization,
+});
+
+export async function loginViaApi(
+ request: APIRequestContext,
+ username = umamiUser.username,
+ password = umamiUser.password,
+): Promise {
+ const response = await request.post('/api/auth/login', {
+ data: { username, password },
+ });
+
+ expect(response.status()).toBe(200);
+
+ const body = await response.json();
+
+ return {
+ token: body.token,
+ authorization: `bearer ${body.token}`,
+ };
+}
+
+export async function loginPage(page: Page, request: APIRequestContext): Promise {
+ const auth = await loginViaApi(request);
+
+ await page.addInitScript(token => {
+ window.localStorage.setItem('umami.auth', JSON.stringify(token));
+ }, auth.token);
+
+ return auth;
+}
+
+export async function logout(page: Page) {
+ await page.getByTestId('button-profile').click();
+ await page.getByTestId('item-logout').click();
+ await expect(page).toHaveURL(/\/login$/);
+}
+
+export async function addWebsite(
+ request: APIRequestContext,
+ auth: Auth,
+ name: string,
+ domain: string,
+) {
+ const response = await request.post('/api/websites', {
+ headers: authHeaders(auth),
+ data: {
+ id: uuid(),
+ createdBy: umamiUser.id,
+ name,
+ domain,
+ },
+ });
+
+ expect(response.status()).toBe(200);
+}
+
+export async function deleteWebsite(request: APIRequestContext, auth: Auth, websiteId: string) {
+ const response = await request.delete(`/api/websites/${websiteId}`, {
+ headers: authHeaders(auth),
+ });
+
+ expect(response.status()).toBe(200);
+}
+
+export async function addUser(
+ request: APIRequestContext,
+ auth: Auth,
+ username: string,
+ password: string,
+ role: string,
+) {
+ const response = await request.post('/api/users', {
+ headers: authHeaders(auth),
+ data: { username, password, role },
+ });
+
+ expect(response.status()).toBe(200);
+}
+
+export async function deleteUser(request: APIRequestContext, auth: Auth, userId: string) {
+ const response = await request.delete(`/api/users/${userId}`, {
+ headers: authHeaders(auth),
+ });
+
+ expect(response.status()).toBe(200);
+}
+
+export async function addTeam(request: APIRequestContext, auth: Auth, name: string) {
+ const response = await request.post('/api/teams', {
+ headers: authHeaders(auth),
+ data: { name },
+ });
+
+ expect(response.status()).toBe(200);
+}
+
+export async function deleteTeam(request: APIRequestContext, auth: Auth, teamId: string) {
+ const response = await request.delete(`/api/teams/${teamId}`, {
+ headers: authHeaders(auth),
+ });
+
+ expect(response.status()).toBe(200);
+}
diff --git a/tests/e2e/login.spec.ts b/tests/e2e/login.spec.ts
new file mode 100644
index 000000000..c5b956b98
--- /dev/null
+++ b/tests/e2e/login.spec.ts
@@ -0,0 +1,29 @@
+import { expect, test } from '@playwright/test';
+import { logout, umamiUser } from './helpers';
+
+test.describe('Login tests', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/login');
+ });
+
+ test('logs user in with correct credentials and logs user out', async ({ page }) => {
+ await page.getByTestId('input-username').locator('input').fill(umamiUser.username);
+ await page.getByTestId('input-password').locator('input').fill(umamiUser.password);
+ await page.getByTestId('button-submit').click();
+
+ await expect(page).toHaveURL(/\/dashboard$/);
+
+ await logout(page);
+ });
+
+ test('shows validation for blank inputs or incorrect credentials', async ({ page }) => {
+ await page.getByTestId('button-submit').click();
+ await expect(page.getByText(/Required/i)).toBeVisible();
+
+ await page.getByTestId('input-username').locator('input').fill(umamiUser.username);
+ await page.getByTestId('input-password').locator('input').fill('wrongpassword');
+ await page.getByTestId('button-submit').click();
+
+ await expect(page.getByText(/Incorrect username and\/or password./i)).toBeVisible();
+ });
+});
diff --git a/tests/e2e/user.spec.ts b/tests/e2e/user.spec.ts
new file mode 100644
index 000000000..33023c204
--- /dev/null
+++ b/tests/e2e/user.spec.ts
@@ -0,0 +1,59 @@
+import { expect, test } from '@playwright/test';
+import { loginPage, logout } from './helpers';
+
+test.describe('User tests', () => {
+ test.describe.configure({ mode: 'serial' });
+
+ test.beforeEach(async ({ page, request }) => {
+ await loginPage(page, request);
+ await page.goto('/settings/users');
+ });
+
+ test('adds a user', async ({ page }) => {
+ await expect(page.getByText(/Create user/i)).toBeVisible();
+
+ await page.getByTestId('button-create-user').click();
+ await page.getByTestId('input-username').locator('input').fill('Test-user');
+ await page.getByTestId('input-password').locator('input').fill('testPasswordPlaywright');
+ await page.getByTestId('dropdown-role').click();
+ await page.getByTestId('dropdown-item-user').click();
+ await page.getByTestId('button-submit').click();
+
+ await expect(page.locator('td[label="Username"]')).toContainText('Test-user');
+ await expect(page.locator('td[label="Role"]')).toContainText('User');
+ });
+
+ test('edits a user role and password', async ({ page }) => {
+ const userRow = page.locator('table tbody tr').filter({
+ has: page.locator('td', { hasText: /Test-user/i }),
+ });
+
+ await userRow.getByTestId('link-button-edit').click();
+ await page.getByTestId('input-password').locator('input').fill('newPassword');
+ await page.getByTestId('dropdown-role').click();
+ await page.getByTestId('dropdown-item-viewOnly').click();
+ await page.getByTestId('button-submit').click();
+
+ await page.goto('/settings/users');
+ await expect(
+ page.locator('table tbody tr').filter({ has: page.locator('td', { hasText: /Test-user/i }) }),
+ ).toContainText('View only');
+
+ await logout(page);
+ await page.getByTestId('input-username').locator('input').fill('Test-user');
+ await page.getByTestId('input-password').locator('input').fill('newPassword');
+ await page.getByTestId('button-submit').click();
+
+ await expect(page).toHaveURL(/\/dashboard$/);
+ });
+
+ test('deletes a user', async ({ page }) => {
+ const userRow = page.locator('table tbody tr').filter({
+ has: page.locator('td', { hasText: /Test-user/i }),
+ });
+
+ await userRow.getByTestId('button-delete').click();
+ await expect(page.getByText(/Are you sure you want to delete Test-user?/i)).toBeVisible();
+ await page.getByTestId('button-confirm').click();
+ });
+});
diff --git a/tests/e2e/website.spec.ts b/tests/e2e/website.spec.ts
new file mode 100644
index 000000000..969dde97d
--- /dev/null
+++ b/tests/e2e/website.spec.ts
@@ -0,0 +1,73 @@
+import { expect, test } from '@playwright/test';
+import { addWebsite, deleteWebsite, loginPage } from './helpers';
+
+test.describe('Website tests', () => {
+ test('adds a website', async ({ page, request }) => {
+ const auth = await loginPage(page, request);
+
+ await page.goto('/settings/websites');
+ await page.getByTestId('button-website-add').click();
+ await expect(page.getByText(/Add website/i)).toBeVisible();
+ await page.getByTestId('input-name').locator('input').fill('Add test');
+ await page.getByTestId('input-domain').locator('input').fill('addtest.com');
+ await page.getByTestId('button-submit').click();
+
+ await expect(page.locator('td[label="Name"]')).toContainText('Add test');
+ await expect(page.locator('td[label="Domain"]')).toContainText('addtest.com');
+
+ await page.getByTestId('link-button-edit').first().click();
+ await expect(page.getByText(/Details/i)).toBeVisible();
+
+ const websiteId = await page.getByTestId('text-field-websiteId').locator('input').inputValue();
+
+ await deleteWebsite(request, auth, websiteId);
+ await page.goto('/settings/websites');
+ await expect(page.getByText(/Add test/i)).toHaveCount(0);
+ });
+
+ test('edits a website', async ({ page, request }) => {
+ const auth = await loginPage(page, request);
+
+ await addWebsite(request, auth, 'Update test', 'updatetest.com');
+ await page.goto('/settings/websites');
+
+ await page.getByTestId('link-button-edit').first().click();
+ await expect(page.getByText(/Details/i)).toBeVisible();
+ await page.getByTestId('input-name').locator('input').fill('Updated website');
+ await page.getByTestId('input-domain').locator('input').fill('updatedwebsite.com');
+ await page.getByTestId('button-submit').click();
+
+ await expect(page.getByTestId('input-name').locator('input')).toHaveValue('Updated website');
+ await expect(page.getByTestId('input-domain').locator('input')).toHaveValue(
+ 'updatedwebsite.com',
+ );
+
+ await page.getByText(/Tracking code/i).click();
+ await expect(page.locator('textarea')).toContainText('/script.js');
+
+ await page.getByText(/Details/i).click();
+ const websiteId = await page.getByTestId('text-field-websiteId').locator('input').inputValue();
+
+ await deleteWebsite(request, auth, websiteId);
+ await page.goto('/settings/websites');
+ await expect(page.getByText(/Update test/i)).toHaveCount(0);
+ });
+
+ test('deletes a website', async ({ page, request }) => {
+ const auth = await loginPage(page, request);
+
+ await addWebsite(request, auth, 'Delete test', 'deletetest.com');
+ await page.goto('/settings/websites');
+
+ await page.getByTestId('link-button-edit').first().click();
+ await expect(page.getByText(/Data/i)).toBeVisible();
+ await page.getByText(/Data/i).click();
+ await expect(page.getByText(/All website data will be deleted./i)).toBeVisible();
+ await page.getByTestId('button-delete').click();
+ await expect(page.getByText(/Type DELETE in the box below to confirm./i)).toBeVisible();
+ await page.locator('input[name="confirm"]').fill('DELETE');
+ await page.locator('button[type="submit"]').click();
+
+ await expect(page.getByText(/Delete test/i)).toHaveCount(0);
+ });
+});
diff --git a/tsconfig.json b/tsconfig.json
index 6fc745192..a876a46bb 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -39,5 +39,5 @@
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
- "exclude": ["node_modules", "./cypress.config.ts", "cypress"]
+ "exclude": ["node_modules", "./playwright.config.ts", "tests/e2e"]
}
diff --git a/tsconfig.tracker.json b/tsconfig.tracker.json
new file mode 100644
index 000000000..d0f38985f
--- /dev/null
+++ b/tsconfig.tracker.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "declaration": false,
+ "incremental": false,
+ "noEmit": true,
+ "noImplicitAny": true,
+ "outDir": "./public",
+ "rootDir": "./src/tracker",
+ "sourceMap": false,
+ "strict": true,
+ "strictNullChecks": true,
+ "target": "es2022"
+ },
+ "include": ["src/tracker/**/*.ts"],
+ "exclude": ["src/tracker/**/*.test.ts"]
+}
diff --git a/tsconfig.tracker.types.json b/tsconfig.tracker.types.json
new file mode 100644
index 000000000..c6a8f7707
--- /dev/null
+++ b/tsconfig.tracker.types.json
@@ -0,0 +1,13 @@
+{
+ "extends": "./tsconfig.tracker.json",
+ "compilerOptions": {
+ "declaration": true,
+ "emitDeclarationOnly": true,
+ "noEmit": false,
+ "outDir": "./src/tracker",
+ "removeComments": false,
+ "rootDir": "./src/tracker"
+ },
+ "include": ["src/tracker/index.ts"],
+ "exclude": []
+}
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 000000000..23a84a672
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,15 @@
+import { fileURLToPath } from 'node:url';
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
+ },
+ },
+ test: {
+ environment: 'jsdom',
+ include: ['src/**/*.test.{ts,tsx}'],
+ setupFiles: ['./src/test/setup.ts'],
+ },
+});