diff --git a/.dockerignore b/.dockerignore index 61cb85b98..71cdb8b9d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,5 +7,3 @@ node_modules .idea .env .env.* -scripts/seed -scripts/seed-data.ts diff --git a/package.json b/package.json index dc701dfba..077b74d3a 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "test": "jest", "cypress-open": "cypress open cypress run", "cypress-run": "cypress run cypress run", - "seed-data": "tsx scripts/seed-data.ts", "lint": "biome lint .", "format": "biome format --write .", "check": "biome check --write" @@ -73,7 +72,7 @@ "@react-spring/web": "^10.0.3", "@svgr/cli": "^8.1.0", "@tanstack/react-query": "^5.90.5", - "@umami/react-zen": "^0.211.0", + "@umami/react-zen": "^0.210.0", "@umami/redis-client": "^0.29.0", "bcryptjs": "^3.0.2", "chalk": "^5.6.2", @@ -168,7 +167,6 @@ "ts-jest": "^29.4.5", "ts-node": "^10.9.1", "tsup": "^8.5.0", - "tsx": "^4.19.0", "typescript": "^5.9.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be25301f5..e19b4029b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,8 +45,8 @@ importers: specifier: ^5.90.5 version: 5.90.10(react@19.2.0) '@umami/react-zen': - specifier: ^0.211.0 - version: 0.211.0(@babel/core@7.28.3)(@types/react@19.2.6)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.2.0)(use-sync-external-store@1.6.0(react@19.2.0)) + specifier: ^0.210.0 + version: 0.210.0(@babel/core@7.28.3)(@types/react@19.2.6)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.2.0)(use-sync-external-store@1.6.0(react@19.2.0)) '@umami/redis-client': specifier: ^0.29.0 version: 0.29.0 @@ -323,10 +323,7 @@ importers: version: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) tsup: specifier: ^8.5.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1) - tsx: - specifier: ^4.19.0 - version: 4.21.0 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -1696,8 +1693,8 @@ packages: '@next/env@15.5.3': resolution: {integrity: sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==} - '@next/env@16.0.6': - resolution: {integrity: sha512-PFTK/G/vM3UJwK5XDYMFOqt8QW42mmhSgdKDapOlCqBUAOfJN2dyOnASR/xUR/JRrro0pLohh/zOJ77xUQWQAg==} + '@next/env@15.5.6': + resolution: {integrity: sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q==} '@next/swc-darwin-arm64@15.5.3': resolution: {integrity: sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==} @@ -1705,8 +1702,8 @@ packages: cpu: [arm64] os: [darwin] - '@next/swc-darwin-arm64@16.0.6': - resolution: {integrity: sha512-AGzKiPlDiui+9JcPRHLI4V9WFTTcKukhJTfK9qu3e0tz+Y/88B7vo5yZoO7UaikplJEHORzG3QaBFQfkjhnL0Q==} + '@next/swc-darwin-arm64@15.5.6': + resolution: {integrity: sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -1717,8 +1714,8 @@ packages: cpu: [x64] os: [darwin] - '@next/swc-darwin-x64@16.0.6': - resolution: {integrity: sha512-LlLLNrK9WCIUkq2GciWDcquXYIf7vLxX8XE49gz7EncssZGL1vlHwgmURiJsUZAvk0HM1a8qb1ABDezsjAE/jw==} + '@next/swc-darwin-x64@15.5.6': + resolution: {integrity: sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -1729,8 +1726,8 @@ packages: cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-gnu@16.0.6': - resolution: {integrity: sha512-r04NzmLSGGfG8EPXKVK72N5zDNnq9pa9el78LhdtqIC3zqKh74QfKHnk24DoK4PEs6eY7sIK/CnNpt30oc59kg==} + '@next/swc-linux-arm64-gnu@15.5.6': + resolution: {integrity: sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1741,8 +1738,8 @@ packages: cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@16.0.6': - resolution: {integrity: sha512-hfB/QV0hA7lbD1OJxp52wVDlpffUMfyxUB5ysZbb/pBC5iuhyLcEKSVQo56PFUUmUQzbMsAtUu6k2Gh9bBtWXA==} + '@next/swc-linux-arm64-musl@15.5.6': + resolution: {integrity: sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1753,8 +1750,8 @@ packages: cpu: [x64] os: [linux] - '@next/swc-linux-x64-gnu@16.0.6': - resolution: {integrity: sha512-PZJushBgfvKhJBy01yXMdgL+l5XKr7uSn5jhOQXQXiH3iPT2M9iG64yHpPNGIKitKrHJInwmhPVGogZBAJOCPw==} + '@next/swc-linux-x64-gnu@15.5.6': + resolution: {integrity: sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1765,8 +1762,8 @@ packages: cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@16.0.6': - resolution: {integrity: sha512-LqY76IojrH9yS5fyATjLzlOIOgwyzBuNRqXwVxcGfZ58DWNQSyfnLGlfF6shAEqjwlDNLh4Z+P0rnOI87Y9jEw==} + '@next/swc-linux-x64-musl@15.5.6': + resolution: {integrity: sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1777,8 +1774,8 @@ packages: cpu: [arm64] os: [win32] - '@next/swc-win32-arm64-msvc@16.0.6': - resolution: {integrity: sha512-eIfSNNqAkj0tqKRf0u7BVjqylJCuabSrxnpSENY3YKApqwDMeAqYPmnOwmVe6DDl3Lvkbe7cJAyP6i9hQ5PmmQ==} + '@next/swc-win32-arm64-msvc@15.5.6': + resolution: {integrity: sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -1789,8 +1786,8 @@ packages: cpu: [x64] os: [win32] - '@next/swc-win32-x64-msvc@16.0.6': - resolution: {integrity: sha512-QGs18P4OKdK9y2F3Th42+KGnwsc2iaThOe6jxQgP62kslUU4W+g6AzI6bdIn/pslhSfxjAMU5SjakfT5Fyo/xA==} + '@next/swc-win32-x64-msvc@15.5.6': + resolution: {integrity: sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2950,8 +2947,8 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@umami/react-zen@0.211.0': - resolution: {integrity: sha512-e9dfsmMYpClYU/xQ+nwFo4ktAJc6eth4k6lpdD4j47FD5PaMfSY1FK1qJ7yq/JVN0Ydomc8cuWBDZbHpG4sQmQ==} + '@umami/react-zen@0.210.0': + resolution: {integrity: sha512-nQ8EfrSleuXMPBVabr6rDoH2VS0ca41A3V2OCQbG4HqgLJ5+Mj8gHT/aLqUz5EKNBAmMy0/XxPNAgsHwwoxrCQ==} '@umami/redis-client@0.29.0': resolution: {integrity: sha512-Jaqh++jskqDB7ny75pfC02OvKp1JTS4asGDsFrRL3qy8sxL3PAl9+/mybCJe4/6vWrXDJKqpgkSfUDJq2bFjyw==} @@ -3280,8 +3277,8 @@ packages: caniuse-lite@1.0.30001741: resolution: {integrity: sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==} - caniuse-lite@1.0.30001759: - resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==} + caniuse-lite@1.0.30001756: + resolution: {integrity: sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==} caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} @@ -4179,9 +4176,6 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-tsconfig@4.13.0: - resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} - getos@3.2.1: resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==} @@ -4200,10 +4194,6 @@ packages: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} hasBin: true - glob@13.0.0: - resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} - engines: {node: 20 || >=22} - glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -5047,10 +5037,6 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.4: - resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} - engines: {node: 20 || >=22} - lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -5058,13 +5044,13 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - lucide-react@0.543.0: - resolution: {integrity: sha512-fpVfuOQO0V3HBaOA1stIiP/A2fPCXHIleRZL16Mx3HmjTYwNSbimhnFBygs2CAfU1geexMX5ItUcWBGUaqw5CA==} + lucide-react@0.511.0: + resolution: {integrity: sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - lucide-react@0.555.0: - resolution: {integrity: sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==} + lucide-react@0.543.0: + resolution: {integrity: sha512-fpVfuOQO0V3HBaOA1stIiP/A2fPCXHIleRZL16Mx3HmjTYwNSbimhnFBygs2CAfU1geexMX5ItUcWBGUaqw5CA==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -5175,10 +5161,6 @@ packages: resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} engines: {node: 20 || >=22} - minimatch@10.1.1: - resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} - engines: {node: 20 || >=22} - minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -5267,9 +5249,9 @@ packages: sass: optional: true - next@16.0.6: - resolution: {integrity: sha512-2zOZ/4FdaAp5hfCU/RnzARlZzBsjaTZ/XjNQmuyYLluAPM7kcrbIkdeO2SL0Ysd1vnrSgU+GwugfeWX1cUCgCg==} - engines: {node: '>=20.9.0'} + next@15.5.6: + resolution: {integrity: sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -5469,10 +5451,6 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-scurry@2.0.1: - resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} - engines: {node: 20 || >=22} - path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} engines: {node: '>=4'} @@ -6141,8 +6119,8 @@ packages: peerDependencies: react: '>=16.13.1' - react-hook-form@7.67.0: - resolution: {integrity: sha512-E55EOwKJHHIT/I6J9DmQbCWToAYSw9nN5R57MZw9rMtjh+YQreMDxRLfdjfxQbiJ3/qbg3Z02wGzBX4M+5fMtQ==} + react-hook-form@7.66.1: + resolution: {integrity: sha512-2KnjpgG2Rhbi+CIiIBQQ9Df6sMGH5ExNyFl4Hw9qO7pIqMBR8Bvu9RQyjl3JM4vehzCh9soiNUM/xYMswb2EiA==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -6293,9 +6271,6 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve.exports@2.0.3: resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} engines: {node: '>=10'} @@ -6908,11 +6883,6 @@ packages: typescript: optional: true - tsx@4.21.0: - resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} - engines: {node: '>=18.0.0'} - hasBin: true - tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -8480,54 +8450,54 @@ snapshots: '@next/env@15.5.3': {} - '@next/env@16.0.6': {} + '@next/env@15.5.6': {} '@next/swc-darwin-arm64@15.5.3': optional: true - '@next/swc-darwin-arm64@16.0.6': + '@next/swc-darwin-arm64@15.5.6': optional: true '@next/swc-darwin-x64@15.5.3': optional: true - '@next/swc-darwin-x64@16.0.6': + '@next/swc-darwin-x64@15.5.6': optional: true '@next/swc-linux-arm64-gnu@15.5.3': optional: true - '@next/swc-linux-arm64-gnu@16.0.6': + '@next/swc-linux-arm64-gnu@15.5.6': optional: true '@next/swc-linux-arm64-musl@15.5.3': optional: true - '@next/swc-linux-arm64-musl@16.0.6': + '@next/swc-linux-arm64-musl@15.5.6': optional: true '@next/swc-linux-x64-gnu@15.5.3': optional: true - '@next/swc-linux-x64-gnu@16.0.6': + '@next/swc-linux-x64-gnu@15.5.6': optional: true '@next/swc-linux-x64-musl@15.5.3': optional: true - '@next/swc-linux-x64-musl@16.0.6': + '@next/swc-linux-x64-musl@15.5.6': optional: true '@next/swc-win32-arm64-msvc@15.5.3': optional: true - '@next/swc-win32-arm64-msvc@16.0.6': + '@next/swc-win32-arm64-msvc@15.5.6': optional: true '@next/swc-win32-x64-msvc@15.5.3': optional: true - '@next/swc-win32-x64-msvc@16.0.6': + '@next/swc-win32-x64-msvc@15.5.6': optional: true '@nodelib/fs.scandir@2.1.5': @@ -10117,21 +10087,21 @@ snapshots: '@types/node': 24.10.1 optional: true - '@umami/react-zen@0.211.0(@babel/core@7.28.3)(@types/react@19.2.6)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.2.0)(use-sync-external-store@1.6.0(react@19.2.0))': + '@umami/react-zen@0.210.0(@babel/core@7.28.3)(@types/react@19.2.6)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.2.0)(use-sync-external-store@1.6.0(react@19.2.0))': dependencies: '@fontsource/jetbrains-mono': 5.2.8 '@internationalized/date': 3.10.0 '@react-aria/focus': 3.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@react-spring/web': 9.7.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) classnames: 2.5.1 - glob: 13.0.0 + glob: 10.5.0 highlight.js: 11.11.1 - lucide-react: 0.555.0(react@19.2.0) - next: 16.0.6(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + lucide-react: 0.511.0(react@19.2.0) + next: 15.5.6(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 react-aria-components: 1.13.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-dom: 19.2.0(react@19.2.0) - react-hook-form: 7.67.0(react@19.2.0) + react-hook-form: 7.66.1(react@19.2.0) react-icons: 5.5.0(react@19.2.0) thenby: 1.3.4 zustand: 5.0.8(@types/react@19.2.6)(immer@10.2.0)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)) @@ -10537,7 +10507,7 @@ snapshots: caniuse-lite@1.0.30001741: {} - caniuse-lite@1.0.30001759: {} + caniuse-lite@1.0.30001756: {} caseless@0.12.0: {} @@ -11616,10 +11586,6 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 - get-tsconfig@4.13.0: - dependencies: - resolve-pkg-maps: 1.0.0 - getos@3.2.1: dependencies: async: 3.2.6 @@ -11650,12 +11616,6 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - glob@13.0.0: - dependencies: - minimatch: 10.1.1 - minipass: 7.1.2 - path-scurry: 2.0.1 - glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -12686,8 +12646,6 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.2.4: {} - lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -12696,11 +12654,11 @@ snapshots: dependencies: yallist: 4.0.0 - lucide-react@0.543.0(react@19.2.0): + lucide-react@0.511.0(react@19.2.0): dependencies: react: 19.2.0 - lucide-react@0.555.0(react@19.2.0): + lucide-react@0.543.0(react@19.2.0): dependencies: react: 19.2.0 @@ -12811,10 +12769,6 @@ snapshots: dependencies: '@isaacs/brace-expansion': 5.0.0 - minimatch@10.1.1: - dependencies: - '@isaacs/brace-expansion': 5.0.0 - minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -12899,24 +12853,24 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@16.0.6(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + next@15.5.6(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - '@next/env': 16.0.6 + '@next/env': 15.5.6 '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001759 + caniuse-lite: 1.0.30001756 postcss: 8.4.31 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) styled-jsx: 5.1.6(@babel/core@7.28.3)(react@19.2.0) optionalDependencies: - '@next/swc-darwin-arm64': 16.0.6 - '@next/swc-darwin-x64': 16.0.6 - '@next/swc-linux-arm64-gnu': 16.0.6 - '@next/swc-linux-arm64-musl': 16.0.6 - '@next/swc-linux-x64-gnu': 16.0.6 - '@next/swc-linux-x64-musl': 16.0.6 - '@next/swc-win32-arm64-msvc': 16.0.6 - '@next/swc-win32-x64-msvc': 16.0.6 + '@next/swc-darwin-arm64': 15.5.6 + '@next/swc-darwin-x64': 15.5.6 + '@next/swc-linux-arm64-gnu': 15.5.6 + '@next/swc-linux-arm64-musl': 15.5.6 + '@next/swc-linux-x64-gnu': 15.5.6 + '@next/swc-linux-x64-musl': 15.5.6 + '@next/swc-win32-arm64-msvc': 15.5.6 + '@next/swc-win32-x64-msvc': 15.5.6 babel-plugin-react-compiler: 19.1.0-rc.2 sharp: 0.34.5 transitivePeerDependencies: @@ -13101,11 +13055,6 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-scurry@2.0.1: - dependencies: - lru-cache: 11.2.4 - minipass: 7.1.2 - path-type@3.0.0: dependencies: pify: 3.0.0 @@ -13337,13 +13286,12 @@ snapshots: postcss: 8.5.6 ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.1): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(yaml@2.8.1): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 postcss: 8.5.6 - tsx: 4.21.0 yaml: 2.8.1 postcss-logical@5.0.4(postcss@8.5.6): @@ -13802,7 +13750,7 @@ snapshots: '@babel/runtime': 7.28.3 react: 19.2.0 - react-hook-form@7.67.0(react@19.2.0): + react-hook-form@7.66.1(react@19.2.0): dependencies: react: 19.2.0 @@ -14004,8 +13952,6 @@ snapshots: resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: {} - resolve.exports@2.0.3: {} resolve@1.22.10: @@ -14758,7 +14704,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1): + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1): dependencies: bundle-require: 5.1.0(esbuild@0.27.0) cac: 6.7.14 @@ -14769,7 +14715,7 @@ snapshots: 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.6)(tsx@4.21.0)(yaml@2.8.1) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(yaml@2.8.1) resolve-from: 5.0.0 rollup: 4.53.3 source-map: 0.7.6 @@ -14786,13 +14732,6 @@ snapshots: - tsx - yaml - tsx@4.21.0: - dependencies: - esbuild: 0.27.0 - get-tsconfig: 4.13.0 - optionalDependencies: - fsevents: 2.3.3 - tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 diff --git a/scripts/seed-data.ts b/scripts/seed-data.ts deleted file mode 100644 index 82a0564c3..000000000 --- a/scripts/seed-data.ts +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable no-console */ - -/** - * Umami Sample Data Generator - * - * Generates realistic analytics data for local development and testing. - * Creates two demo websites: - * - Demo Blog: Low traffic (~100 sessions/month) - * - Demo SaaS: Average traffic (~500 sessions/day) - * - * Usage: - * npm run seed-data # Generate 30 days of data - * npm run seed-data -- --days 90 # Generate 90 days of data - * npm run seed-data -- --clear # Clear existing demo data first - * npm run seed-data -- --verbose # Show detailed progress - */ - -import { seed, type SeedConfig } from './seed/index.js'; - -function parseArgs(): SeedConfig { - const args = process.argv.slice(2); - - const config: SeedConfig = { - days: 30, - clear: false, - verbose: false, - }; - - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - - if (arg === '--days' && args[i + 1]) { - config.days = parseInt(args[i + 1], 10); - if (isNaN(config.days) || config.days < 1) { - console.error('Error: --days must be a positive integer'); - process.exit(1); - } - i++; - } else if (arg === '--clear') { - config.clear = true; - } else if (arg === '--verbose' || arg === '-v') { - config.verbose = true; - } else if (arg === '--help' || arg === '-h') { - printHelp(); - process.exit(0); - } else if (arg.startsWith('--days=')) { - config.days = parseInt(arg.split('=')[1], 10); - if (isNaN(config.days) || config.days < 1) { - console.error('Error: --days must be a positive integer'); - process.exit(1); - } - } - } - - return config; -} - -function printHelp(): void { - console.log(` -Umami Sample Data Generator - -Generates realistic analytics data for local development and testing. - -Usage: - npm run seed-data [options] - -Options: - --days Number of days of data to generate (default: 30) - --clear Clear existing demo data before generating - --verbose, -v Show detailed progress - --help, -h Show this help message - -Examples: - npm run seed-data # Generate 30 days of data - npm run seed-data -- --days 90 # Generate 90 days of data - npm run seed-data -- --clear # Clear existing demo data first - npm run seed-data -- --days 7 -v # Generate 7 days with verbose output - -Generated Sites: - - Demo Blog: Low traffic (~90 sessions/month) - - Demo SaaS: Average traffic (~500 sessions/day) with revenue tracking - -Note: - This script is blocked from running in production environments - (NODE_ENV=production or cloud platforms like Vercel/Netlify/Railway). -`); -} - -function checkEnvironment(): void { - const nodeEnv = process.env.NODE_ENV; - - if (nodeEnv === 'production') { - console.error('\nError: seed-data cannot run in production environment.'); - console.error('This script is only for local development and testing.\n'); - process.exit(1); - } - - if (process.env.VERCEL || process.env.NETLIFY || process.env.RAILWAY_ENVIRONMENT) { - console.error('\nError: seed-data cannot run in cloud environments.'); - console.error('This script is only for local development and testing.\n'); - process.exit(1); - } -} - -async function main(): Promise { - console.log('\nUmami Sample Data Generator\n'); - - checkEnvironment(); - - const config = parseArgs(); - - try { - await seed(config); - } catch (error) { - console.error('\nError generating seed data:', error); - process.exit(1); - } -} - -main(); diff --git a/scripts/seed/distributions/devices.ts b/scripts/seed/distributions/devices.ts deleted file mode 100644 index 9d8b8c007..000000000 --- a/scripts/seed/distributions/devices.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { weightedRandom, pickRandom, type WeightedOption } from '../utils.js'; - -export type DeviceType = 'desktop' | 'mobile' | 'tablet'; - -const deviceWeights: WeightedOption[] = [ - { value: 'desktop', weight: 0.55 }, - { value: 'mobile', weight: 0.4 }, - { value: 'tablet', weight: 0.05 }, -]; - -const browsersByDevice: Record[]> = { - desktop: [ - { value: 'Chrome', weight: 0.65 }, - { value: 'Safari', weight: 0.12 }, - { value: 'Firefox', weight: 0.1 }, - { value: 'Edge', weight: 0.1 }, - { value: 'Opera', weight: 0.03 }, - ], - mobile: [ - { value: 'Chrome', weight: 0.55 }, - { value: 'Safari', weight: 0.35 }, - { value: 'Samsung', weight: 0.05 }, - { value: 'Firefox', weight: 0.03 }, - { value: 'Opera', weight: 0.02 }, - ], - tablet: [ - { value: 'Safari', weight: 0.6 }, - { value: 'Chrome', weight: 0.35 }, - { value: 'Firefox', weight: 0.05 }, - ], -}; - -const osByDevice: Record[]> = { - desktop: [ - { value: 'Windows 10', weight: 0.5 }, - { value: 'Mac OS', weight: 0.3 }, - { value: 'Linux', weight: 0.12 }, - { value: 'Chrome OS', weight: 0.05 }, - { value: 'Windows 11', weight: 0.03 }, - ], - mobile: [ - { value: 'iOS', weight: 0.45 }, - { value: 'Android', weight: 0.55 }, - ], - tablet: [ - { value: 'iOS', weight: 0.75 }, - { value: 'Android', weight: 0.25 }, - ], -}; - -const screensByDevice: Record = { - desktop: [ - '1920x1080', - '2560x1440', - '1366x768', - '1440x900', - '3840x2160', - '1536x864', - '1680x1050', - '2560x1080', - ], - mobile: ['390x844', '414x896', '375x812', '360x800', '428x926', '393x873', '412x915', '360x780'], - tablet: ['1024x768', '768x1024', '834x1194', '820x1180', '810x1080', '800x1280'], -}; - -export interface DeviceInfo { - device: DeviceType; - browser: string; - os: string; - screen: string; -} - -export function getRandomDevice(): DeviceInfo { - const device = weightedRandom(deviceWeights); - const browser = weightedRandom(browsersByDevice[device]); - const os = weightedRandom(osByDevice[device]); - const screen = pickRandom(screensByDevice[device]); - - return { device, browser, os, screen }; -} diff --git a/scripts/seed/distributions/geographic.ts b/scripts/seed/distributions/geographic.ts deleted file mode 100644 index ba6ebae3c..000000000 --- a/scripts/seed/distributions/geographic.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { weightedRandom, pickRandom, type WeightedOption } from '../utils.js'; - -interface GeoLocation { - country: string; - region: string; - city: string; -} - -const countryWeights: WeightedOption[] = [ - { value: 'US', weight: 0.35 }, - { value: 'GB', weight: 0.08 }, - { value: 'DE', weight: 0.06 }, - { value: 'FR', weight: 0.05 }, - { value: 'CA', weight: 0.04 }, - { value: 'AU', weight: 0.03 }, - { value: 'IN', weight: 0.08 }, - { value: 'BR', weight: 0.04 }, - { value: 'JP', weight: 0.03 }, - { value: 'NL', weight: 0.02 }, - { value: 'ES', weight: 0.02 }, - { value: 'IT', weight: 0.02 }, - { value: 'PL', weight: 0.02 }, - { value: 'SE', weight: 0.01 }, - { value: 'MX', weight: 0.02 }, - { value: 'KR', weight: 0.02 }, - { value: 'SG', weight: 0.01 }, - { value: 'ID', weight: 0.02 }, - { value: 'PH', weight: 0.01 }, - { value: 'TH', weight: 0.01 }, - { value: 'VN', weight: 0.01 }, - { value: 'RU', weight: 0.02 }, - { value: 'UA', weight: 0.01 }, - { value: 'ZA', weight: 0.01 }, - { value: 'NG', weight: 0.01 }, -]; - -const regionsByCountry: Record = { - US: [ - { region: 'CA', city: 'San Francisco' }, - { region: 'CA', city: 'Los Angeles' }, - { region: 'NY', city: 'New York' }, - { region: 'TX', city: 'Austin' }, - { region: 'TX', city: 'Houston' }, - { region: 'WA', city: 'Seattle' }, - { region: 'IL', city: 'Chicago' }, - { region: 'MA', city: 'Boston' }, - { region: 'CO', city: 'Denver' }, - { region: 'GA', city: 'Atlanta' }, - { region: 'FL', city: 'Miami' }, - { region: 'PA', city: 'Philadelphia' }, - ], - GB: [ - { region: 'ENG', city: 'London' }, - { region: 'ENG', city: 'Manchester' }, - { region: 'ENG', city: 'Birmingham' }, - { region: 'SCT', city: 'Edinburgh' }, - { region: 'ENG', city: 'Bristol' }, - ], - DE: [ - { region: 'BE', city: 'Berlin' }, - { region: 'BY', city: 'Munich' }, - { region: 'HH', city: 'Hamburg' }, - { region: 'HE', city: 'Frankfurt' }, - { region: 'NW', city: 'Cologne' }, - ], - FR: [ - { region: 'IDF', city: 'Paris' }, - { region: 'ARA', city: 'Lyon' }, - { region: 'PAC', city: 'Marseille' }, - { region: 'OCC', city: 'Toulouse' }, - ], - CA: [ - { region: 'ON', city: 'Toronto' }, - { region: 'BC', city: 'Vancouver' }, - { region: 'QC', city: 'Montreal' }, - { region: 'AB', city: 'Calgary' }, - ], - AU: [ - { region: 'NSW', city: 'Sydney' }, - { region: 'VIC', city: 'Melbourne' }, - { region: 'QLD', city: 'Brisbane' }, - { region: 'WA', city: 'Perth' }, - ], - IN: [ - { region: 'MH', city: 'Mumbai' }, - { region: 'KA', city: 'Bangalore' }, - { region: 'DL', city: 'New Delhi' }, - { region: 'TN', city: 'Chennai' }, - { region: 'TG', city: 'Hyderabad' }, - ], - BR: [ - { region: 'SP', city: 'Sao Paulo' }, - { region: 'RJ', city: 'Rio de Janeiro' }, - { region: 'MG', city: 'Belo Horizonte' }, - ], - JP: [ - { region: '13', city: 'Tokyo' }, - { region: '27', city: 'Osaka' }, - { region: '23', city: 'Nagoya' }, - ], - NL: [ - { region: 'NH', city: 'Amsterdam' }, - { region: 'ZH', city: 'Rotterdam' }, - { region: 'ZH', city: 'The Hague' }, - ], -}; - -const defaultRegions = [{ region: '', city: '' }]; - -export function getRandomGeo(): GeoLocation { - const country = weightedRandom(countryWeights); - const regions = regionsByCountry[country] || defaultRegions; - const { region, city } = pickRandom(regions); - - return { country, region, city }; -} - -const languages: WeightedOption[] = [ - { value: 'en-US', weight: 0.4 }, - { value: 'en-GB', weight: 0.08 }, - { value: 'de-DE', weight: 0.06 }, - { value: 'fr-FR', weight: 0.05 }, - { value: 'es-ES', weight: 0.05 }, - { value: 'pt-BR', weight: 0.04 }, - { value: 'ja-JP', weight: 0.03 }, - { value: 'zh-CN', weight: 0.05 }, - { value: 'ko-KR', weight: 0.02 }, - { value: 'ru-RU', weight: 0.02 }, - { value: 'it-IT', weight: 0.02 }, - { value: 'nl-NL', weight: 0.02 }, - { value: 'pl-PL', weight: 0.02 }, - { value: 'hi-IN', weight: 0.04 }, - { value: 'ar-SA', weight: 0.02 }, - { value: 'tr-TR', weight: 0.02 }, - { value: 'vi-VN', weight: 0.01 }, - { value: 'th-TH', weight: 0.01 }, - { value: 'id-ID', weight: 0.02 }, - { value: 'sv-SE', weight: 0.01 }, - { value: 'da-DK', weight: 0.01 }, -]; - -export function getRandomLanguage(): string { - return weightedRandom(languages); -} diff --git a/scripts/seed/distributions/referrers.ts b/scripts/seed/distributions/referrers.ts deleted file mode 100644 index 5b3f2c456..000000000 --- a/scripts/seed/distributions/referrers.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { weightedRandom, pickRandom, randomInt, type WeightedOption } from '../utils.js'; - -export type ReferrerType = 'direct' | 'organic' | 'social' | 'paid' | 'referral'; - -export interface ReferrerInfo { - type: ReferrerType; - domain: string | null; - path: string | null; - utmSource: string | null; - utmMedium: string | null; - utmCampaign: string | null; - utmContent: string | null; - utmTerm: string | null; - gclid: string | null; - fbclid: string | null; -} - -const referrerTypeWeights: WeightedOption[] = [ - { value: 'direct', weight: 0.4 }, - { value: 'organic', weight: 0.25 }, - { value: 'social', weight: 0.15 }, - { value: 'paid', weight: 0.1 }, - { value: 'referral', weight: 0.1 }, -]; - -const searchEngines = [ - { domain: 'google.com', path: '/search' }, - { domain: 'bing.com', path: '/search' }, - { domain: 'duckduckgo.com', path: '/' }, - { domain: 'yahoo.com', path: '/search' }, - { domain: 'baidu.com', path: '/s' }, -]; - -const socialPlatforms = [ - { domain: 'twitter.com', path: null }, - { domain: 'x.com', path: null }, - { domain: 'linkedin.com', path: '/feed' }, - { domain: 'facebook.com', path: null }, - { domain: 'reddit.com', path: '/r/programming' }, - { domain: 'news.ycombinator.com', path: '/item' }, - { domain: 'threads.net', path: null }, - { domain: 'bsky.app', path: null }, -]; - -const referralSites = [ - { domain: 'medium.com', path: '/@author/article' }, - { domain: 'dev.to', path: '/post' }, - { domain: 'hashnode.com', path: '/blog' }, - { domain: 'techcrunch.com', path: '/article' }, - { domain: 'producthunt.com', path: '/posts' }, - { domain: 'indiehackers.com', path: '/post' }, -]; - -interface PaidCampaign { - source: string; - medium: string; - campaign: string; - useGclid?: boolean; - useFbclid?: boolean; -} - -const paidCampaigns: PaidCampaign[] = [ - { source: 'google', medium: 'cpc', campaign: 'brand_search', useGclid: true }, - { source: 'google', medium: 'cpc', campaign: 'product_awareness', useGclid: true }, - { source: 'facebook', medium: 'paid_social', campaign: 'retargeting', useFbclid: true }, - { source: 'facebook', medium: 'paid_social', campaign: 'lookalike', useFbclid: true }, - { source: 'linkedin', medium: 'cpc', campaign: 'b2b_targeting' }, - { source: 'twitter', medium: 'paid_social', campaign: 'launch_promo' }, -]; - -const organicCampaigns = [ - { source: 'newsletter', medium: 'email', campaign: 'weekly_digest' }, - { source: 'newsletter', medium: 'email', campaign: 'product_update' }, - { source: 'partner', medium: 'referral', campaign: 'integration_launch' }, -]; - -function generateClickId(): string { - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - let result = ''; - for (let i = 0; i < 32; i++) { - result += chars.charAt(Math.floor(Math.random() * chars.length)); - } - return result; -} - -export function getRandomReferrer(): ReferrerInfo { - const type = weightedRandom(referrerTypeWeights); - - const result: ReferrerInfo = { - type, - domain: null, - path: null, - utmSource: null, - utmMedium: null, - utmCampaign: null, - utmContent: null, - utmTerm: null, - gclid: null, - fbclid: null, - }; - - switch (type) { - case 'direct': - // No referrer data - break; - - case 'organic': { - const engine = pickRandom(searchEngines); - result.domain = engine.domain; - result.path = engine.path; - break; - } - - case 'social': { - const platform = pickRandom(socialPlatforms); - result.domain = platform.domain; - result.path = platform.path; - - // Some social traffic has UTM params - if (Math.random() < 0.3) { - result.utmSource = platform.domain.replace('.com', '').replace('.net', ''); - result.utmMedium = 'social'; - } - break; - } - - case 'paid': { - const campaign = pickRandom(paidCampaigns); - result.utmSource = campaign.source; - result.utmMedium = campaign.medium; - result.utmCampaign = campaign.campaign; - result.utmContent = `ad_${randomInt(1, 5)}`; - - if (campaign.useGclid) { - result.gclid = generateClickId(); - result.domain = 'google.com'; - result.path = '/search'; - } else if (campaign.useFbclid) { - result.fbclid = generateClickId(); - result.domain = 'facebook.com'; - result.path = null; - } - break; - } - - case 'referral': { - // Mix of pure referrals and organic campaigns - if (Math.random() < 0.6) { - const site = pickRandom(referralSites); - result.domain = site.domain; - result.path = site.path; - } else { - const campaign = pickRandom(organicCampaigns); - result.utmSource = campaign.source; - result.utmMedium = campaign.medium; - result.utmCampaign = campaign.campaign; - } - break; - } - } - - return result; -} diff --git a/scripts/seed/distributions/temporal.ts b/scripts/seed/distributions/temporal.ts deleted file mode 100644 index da0409a9b..000000000 --- a/scripts/seed/distributions/temporal.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { weightedRandom, randomInt, type WeightedOption } from '../utils.js'; - -const hourlyWeights: WeightedOption[] = [ - { value: 0, weight: 0.02 }, - { value: 1, weight: 0.01 }, - { value: 2, weight: 0.01 }, - { value: 3, weight: 0.01 }, - { value: 4, weight: 0.01 }, - { value: 5, weight: 0.02 }, - { value: 6, weight: 0.03 }, - { value: 7, weight: 0.05 }, - { value: 8, weight: 0.07 }, - { value: 9, weight: 0.08 }, - { value: 10, weight: 0.09 }, - { value: 11, weight: 0.08 }, - { value: 12, weight: 0.07 }, - { value: 13, weight: 0.08 }, - { value: 14, weight: 0.09 }, - { value: 15, weight: 0.08 }, - { value: 16, weight: 0.07 }, - { value: 17, weight: 0.06 }, - { value: 18, weight: 0.05 }, - { value: 19, weight: 0.04 }, - { value: 20, weight: 0.03 }, - { value: 21, weight: 0.03 }, - { value: 22, weight: 0.02 }, - { value: 23, weight: 0.02 }, -]; - -const dayOfWeekWeights: WeightedOption[] = [ - { value: 0, weight: 0.08 }, // Sunday - { value: 1, weight: 0.16 }, // Monday - { value: 2, weight: 0.17 }, // Tuesday - { value: 3, weight: 0.17 }, // Wednesday - { value: 4, weight: 0.16 }, // Thursday - { value: 5, weight: 0.15 }, // Friday - { value: 6, weight: 0.11 }, // Saturday -]; - -export function getWeightedHour(): number { - return weightedRandom(hourlyWeights); -} - -export function getDayOfWeekMultiplier(dayOfWeek: number): number { - const weight = dayOfWeekWeights.find(d => d.value === dayOfWeek)?.weight ?? 0.14; - return weight / 0.14; // Normalize around 1.0 -} - -export function generateTimestampForDay(day: Date): Date { - const hour = getWeightedHour(); - const minute = randomInt(0, 59); - const second = randomInt(0, 59); - const millisecond = randomInt(0, 999); - - const timestamp = new Date(day); - timestamp.setHours(hour, minute, second, millisecond); - - return timestamp; -} - -export function getSessionCountForDay(baseCount: number, day: Date): number { - const dayOfWeek = day.getDay(); - const multiplier = getDayOfWeekMultiplier(dayOfWeek); - - // Add some random variance (±20%) - const variance = 0.8 + Math.random() * 0.4; - - return Math.round(baseCount * multiplier * variance); -} diff --git a/scripts/seed/generators/events.ts b/scripts/seed/generators/events.ts deleted file mode 100644 index 724290622..000000000 --- a/scripts/seed/generators/events.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { uuid, addSeconds, randomInt } from '../utils.js'; -import { getRandomReferrer } from '../distributions/referrers.js'; -import type { SessionData } from './sessions.js'; - -export const EVENT_TYPE = { - pageView: 1, - customEvent: 2, -} as const; - -export interface PageConfig { - path: string; - title: string; - weight: number; - avgTimeOnPage: number; -} - -export interface CustomEventConfig { - name: string; - weight: number; - pages?: string[]; - data?: Record; -} - -export interface JourneyConfig { - pages: string[]; - weight: number; -} - -export interface EventData { - id: string; - websiteId: string; - sessionId: string; - visitId: string; - eventType: number; - urlPath: string; - urlQuery: string | null; - pageTitle: string | null; - hostname: string; - referrerDomain: string | null; - referrerPath: string | null; - utmSource: string | null; - utmMedium: string | null; - utmCampaign: string | null; - utmContent: string | null; - utmTerm: string | null; - gclid: string | null; - fbclid: string | null; - eventName: string | null; - tag: string | null; - createdAt: Date; -} - -export interface EventDataEntry { - id: string; - websiteId: string; - websiteEventId: string; - dataKey: string; - stringValue: string | null; - numberValue: number | null; - dateValue: Date | null; - dataType: number; - createdAt: Date; -} - -export interface SiteConfig { - hostname: string; - pages: PageConfig[]; - journeys: JourneyConfig[]; - customEvents: CustomEventConfig[]; -} - -function getPageTitle(pages: PageConfig[], path: string): string | null { - const page = pages.find(p => p.path === path); - return page?.title ?? null; -} - -function getPageTimeOnPage(pages: PageConfig[], path: string): number { - const page = pages.find(p => p.path === path); - return page?.avgTimeOnPage ?? 30; -} - -export function generateEventsForSession( - session: SessionData, - siteConfig: SiteConfig, - journey: string[], -): { events: EventData[]; eventDataEntries: EventDataEntry[] } { - const events: EventData[] = []; - const eventDataEntries: EventDataEntry[] = []; - const visitId = uuid(); - - let currentTime = session.createdAt; - const referrer = getRandomReferrer(); - - for (let i = 0; i < journey.length; i++) { - const pagePath = journey[i]; - const isFirstPage = i === 0; - - const eventId = uuid(); - const pageTitle = getPageTitle(siteConfig.pages, pagePath); - - events.push({ - id: eventId, - websiteId: session.websiteId, - sessionId: session.id, - visitId, - eventType: EVENT_TYPE.pageView, - urlPath: pagePath, - urlQuery: null, - pageTitle, - hostname: siteConfig.hostname, - referrerDomain: isFirstPage ? referrer.domain : null, - referrerPath: isFirstPage ? referrer.path : null, - utmSource: isFirstPage ? referrer.utmSource : null, - utmMedium: isFirstPage ? referrer.utmMedium : null, - utmCampaign: isFirstPage ? referrer.utmCampaign : null, - utmContent: isFirstPage ? referrer.utmContent : null, - utmTerm: isFirstPage ? referrer.utmTerm : null, - gclid: isFirstPage ? referrer.gclid : null, - fbclid: isFirstPage ? referrer.fbclid : null, - eventName: null, - tag: null, - createdAt: currentTime, - }); - - // Check for custom events on this page - for (const customEvent of siteConfig.customEvents) { - // Check if this event can occur on this page - if (customEvent.pages && !customEvent.pages.includes(pagePath)) { - continue; - } - - // Random chance based on weight - if (Math.random() < customEvent.weight) { - currentTime = addSeconds(currentTime, randomInt(2, 15)); - - const customEventId = uuid(); - events.push({ - id: customEventId, - websiteId: session.websiteId, - sessionId: session.id, - visitId, - eventType: EVENT_TYPE.customEvent, - urlPath: pagePath, - urlQuery: null, - pageTitle, - hostname: siteConfig.hostname, - referrerDomain: null, - referrerPath: null, - utmSource: null, - utmMedium: null, - utmCampaign: null, - utmContent: null, - utmTerm: null, - gclid: null, - fbclid: null, - eventName: customEvent.name, - tag: null, - createdAt: currentTime, - }); - - // Generate event data if configured - if (customEvent.data) { - for (const [key, values] of Object.entries(customEvent.data)) { - const value = values[Math.floor(Math.random() * values.length)]; - const isNumber = typeof value === 'number'; - - eventDataEntries.push({ - id: uuid(), - websiteId: session.websiteId, - websiteEventId: customEventId, - dataKey: key, - stringValue: isNumber ? null : String(value), - numberValue: isNumber ? value : null, - dateValue: null, - dataType: isNumber ? 2 : 1, // 1 = string, 2 = number - createdAt: currentTime, - }); - } - } - } - } - - // Time spent on page before navigating - const timeOnPage = getPageTimeOnPage(siteConfig.pages, pagePath); - const variance = Math.floor(timeOnPage * 0.5); - const actualTime = timeOnPage + randomInt(-variance, variance); - currentTime = addSeconds(currentTime, Math.max(5, actualTime)); - } - - return { events, eventDataEntries }; -} diff --git a/scripts/seed/generators/revenue.ts b/scripts/seed/generators/revenue.ts deleted file mode 100644 index deea9e6b3..000000000 --- a/scripts/seed/generators/revenue.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { uuid, randomFloat } from '../utils.js'; -import type { EventData } from './events.js'; - -export interface RevenueConfig { - eventName: string; - minAmount: number; - maxAmount: number; - currency: string; - weight: number; -} - -export interface RevenueData { - id: string; - websiteId: string; - sessionId: string; - eventId: string; - eventName: string; - currency: string; - revenue: number; - createdAt: Date; -} - -export function generateRevenue(event: EventData, config: RevenueConfig): RevenueData | null { - if (event.eventName !== config.eventName) { - return null; - } - - if (Math.random() > config.weight) { - return null; - } - - const revenue = randomFloat(config.minAmount, config.maxAmount); - - return { - id: uuid(), - websiteId: event.websiteId, - sessionId: event.sessionId, - eventId: event.id, - eventName: event.eventName!, - currency: config.currency, - revenue: Math.round(revenue * 100) / 100, // Round to 2 decimal places - createdAt: event.createdAt, - }; -} - -export function generateRevenueForEvents( - events: EventData[], - configs: RevenueConfig[], -): RevenueData[] { - const revenueEntries: RevenueData[] = []; - - for (const event of events) { - if (!event.eventName) continue; - - for (const config of configs) { - const revenue = generateRevenue(event, config); - if (revenue) { - revenueEntries.push(revenue); - break; // Only one revenue per event - } - } - } - - return revenueEntries; -} diff --git a/scripts/seed/generators/sessions.ts b/scripts/seed/generators/sessions.ts deleted file mode 100644 index 1370511ff..000000000 --- a/scripts/seed/generators/sessions.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { uuid } from '../utils.js'; -import { getRandomDevice } from '../distributions/devices.js'; -import { getRandomGeo, getRandomLanguage } from '../distributions/geographic.js'; -import { generateTimestampForDay } from '../distributions/temporal.js'; - -export interface SessionData { - id: string; - websiteId: string; - browser: string; - os: string; - device: string; - screen: string; - language: string; - country: string; - region: string; - city: string; - createdAt: Date; -} - -export function createSession(websiteId: string, day: Date): SessionData { - const deviceInfo = getRandomDevice(); - const geo = getRandomGeo(); - const language = getRandomLanguage(); - const createdAt = generateTimestampForDay(day); - - return { - id: uuid(), - websiteId, - browser: deviceInfo.browser, - os: deviceInfo.os, - device: deviceInfo.device, - screen: deviceInfo.screen, - language, - country: geo.country, - region: geo.region, - city: geo.city, - createdAt, - }; -} - -export function createSessions(websiteId: string, day: Date, count: number): SessionData[] { - const sessions: SessionData[] = []; - - for (let i = 0; i < count; i++) { - sessions.push(createSession(websiteId, day)); - } - - // Sort by createdAt to maintain chronological order - sessions.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); - - return sessions; -} diff --git a/scripts/seed/index.ts b/scripts/seed/index.ts deleted file mode 100644 index 5b9de8de2..000000000 --- a/scripts/seed/index.ts +++ /dev/null @@ -1,378 +0,0 @@ -/* eslint-disable no-console */ -import 'dotenv/config'; -import { PrismaPg } from '@prisma/adapter-pg'; -import { PrismaClient, Prisma } from '../../src/generated/prisma/client.js'; -import { uuid, generateDatesBetween, subDays, formatNumber, progressBar } from './utils.js'; -import { createSessions, type SessionData } from './generators/sessions.js'; -import { - generateEventsForSession, - type EventData, - type EventDataEntry, -} from './generators/events.js'; -import { - generateRevenueForEvents, - type RevenueData, - type RevenueConfig, -} from './generators/revenue.js'; -import { getSessionCountForDay } from './distributions/temporal.js'; -import { - BLOG_WEBSITE_NAME, - BLOG_WEBSITE_DOMAIN, - BLOG_SESSIONS_PER_DAY, - getBlogSiteConfig, - getBlogJourney, -} from './sites/blog.js'; -import { - SAAS_WEBSITE_NAME, - SAAS_WEBSITE_DOMAIN, - SAAS_SESSIONS_PER_DAY, - getSaasSiteConfig, - getSaasJourney, - saasRevenueConfigs, -} from './sites/saas.js'; - -const BATCH_SIZE = 1000; - -type SessionCreateInput = Prisma.SessionCreateManyInput; -type WebsiteEventCreateInput = Prisma.WebsiteEventCreateManyInput; -type EventDataCreateInput = Prisma.EventDataCreateManyInput; -type RevenueCreateInput = Prisma.RevenueCreateManyInput; - -export interface SeedConfig { - days: number; - clear: boolean; - verbose: boolean; -} - -export interface SeedResult { - websites: number; - sessions: number; - events: number; - eventData: number; - revenue: number; -} - -async function batchInsertSessions( - prisma: PrismaClient, - data: SessionCreateInput[], - verbose: boolean, -): Promise { - for (let i = 0; i < data.length; i += BATCH_SIZE) { - const batch = data.slice(i, i + BATCH_SIZE); - await prisma.session.createMany({ data: batch, skipDuplicates: true }); - if (verbose) { - console.log( - ` Inserted ${Math.min(i + BATCH_SIZE, data.length)}/${data.length} session records`, - ); - } - } -} - -async function batchInsertEvents( - prisma: PrismaClient, - data: WebsiteEventCreateInput[], - verbose: boolean, -): Promise { - for (let i = 0; i < data.length; i += BATCH_SIZE) { - const batch = data.slice(i, i + BATCH_SIZE); - await prisma.websiteEvent.createMany({ data: batch, skipDuplicates: true }); - if (verbose) { - console.log( - ` Inserted ${Math.min(i + BATCH_SIZE, data.length)}/${data.length} event records`, - ); - } - } -} - -async function batchInsertEventData( - prisma: PrismaClient, - data: EventDataCreateInput[], - verbose: boolean, -): Promise { - for (let i = 0; i < data.length; i += BATCH_SIZE) { - const batch = data.slice(i, i + BATCH_SIZE); - await prisma.eventData.createMany({ data: batch, skipDuplicates: true }); - if (verbose) { - console.log( - ` Inserted ${Math.min(i + BATCH_SIZE, data.length)}/${data.length} eventData records`, - ); - } - } -} - -async function batchInsertRevenue( - prisma: PrismaClient, - data: RevenueCreateInput[], - verbose: boolean, -): Promise { - for (let i = 0; i < data.length; i += BATCH_SIZE) { - const batch = data.slice(i, i + BATCH_SIZE); - await prisma.revenue.createMany({ data: batch, skipDuplicates: true }); - if (verbose) { - console.log( - ` Inserted ${Math.min(i + BATCH_SIZE, data.length)}/${data.length} revenue records`, - ); - } - } -} - -async function findAdminUser(prisma: PrismaClient): Promise { - const adminUser = await prisma.user.findFirst({ - where: { role: 'admin' }, - select: { id: true }, - }); - - if (!adminUser) { - throw new Error( - 'No admin user found in the database.\n' + - 'Please ensure you have run the initial setup and created an admin user.\n' + - 'The default admin user is created during first build (username: admin, password: umami).', - ); - } - - return adminUser.id; -} - -async function createWebsite( - prisma: PrismaClient, - name: string, - domain: string, - adminUserId: string, -): Promise { - const websiteId = uuid(); - - await prisma.website.create({ - data: { - id: websiteId, - name, - domain, - userId: adminUserId, - createdBy: adminUserId, - }, - }); - - return websiteId; -} - -async function clearDemoData(prisma: PrismaClient): Promise { - console.log('Clearing existing demo data...'); - - const demoWebsites = await prisma.website.findMany({ - where: { - OR: [{ name: BLOG_WEBSITE_NAME }, { name: SAAS_WEBSITE_NAME }], - }, - select: { id: true }, - }); - - const websiteIds = demoWebsites.map(w => w.id); - - if (websiteIds.length === 0) { - console.log(' No existing demo websites found'); - return; - } - - console.log(` Found ${websiteIds.length} demo website(s)`); - - // Delete in correct order due to foreign key constraints - await prisma.revenue.deleteMany({ where: { websiteId: { in: websiteIds } } }); - await prisma.eventData.deleteMany({ where: { websiteId: { in: websiteIds } } }); - await prisma.sessionData.deleteMany({ where: { websiteId: { in: websiteIds } } }); - await prisma.websiteEvent.deleteMany({ where: { websiteId: { in: websiteIds } } }); - await prisma.session.deleteMany({ where: { websiteId: { in: websiteIds } } }); - await prisma.segment.deleteMany({ where: { websiteId: { in: websiteIds } } }); - await prisma.report.deleteMany({ where: { websiteId: { in: websiteIds } } }); - await prisma.website.deleteMany({ where: { id: { in: websiteIds } } }); - - console.log(' Cleared existing demo data'); -} - -interface SiteGeneratorConfig { - name: string; - domain: string; - sessionsPerDay: number; - getSiteConfig: () => ReturnType; - getJourney: () => string[]; - revenueConfigs?: RevenueConfig[]; -} - -async function generateSiteData( - prisma: PrismaClient, - config: SiteGeneratorConfig, - days: Date[], - adminUserId: string, - verbose: boolean, -): Promise<{ sessions: number; events: number; eventData: number; revenue: number }> { - console.log(`\nGenerating data for ${config.name}...`); - - const websiteId = await createWebsite(prisma, config.name, config.domain, adminUserId); - console.log(` Created website: ${config.name} (${websiteId})`); - - const siteConfig = config.getSiteConfig(); - - const allSessions: SessionData[] = []; - const allEvents: EventData[] = []; - const allEventData: EventDataEntry[] = []; - const allRevenue: RevenueData[] = []; - - for (let dayIndex = 0; dayIndex < days.length; dayIndex++) { - const day = days[dayIndex]; - const sessionCount = getSessionCountForDay(config.sessionsPerDay, day); - const sessions = createSessions(websiteId, day, sessionCount); - - for (const session of sessions) { - const journey = config.getJourney(); - const { events, eventDataEntries } = generateEventsForSession(session, siteConfig, journey); - - allSessions.push(session); - allEvents.push(...events); - allEventData.push(...eventDataEntries); - - if (config.revenueConfigs) { - const revenueEntries = generateRevenueForEvents(events, config.revenueConfigs); - allRevenue.push(...revenueEntries); - } - } - - // Show progress (every day in verbose mode, otherwise every 2 days) - const shouldShowProgress = verbose || dayIndex % 2 === 0 || dayIndex === days.length - 1; - if (shouldShowProgress) { - process.stdout.write( - `\r ${progressBar(dayIndex + 1, days.length)} Day ${dayIndex + 1}/${days.length}`, - ); - } - } - - console.log(''); // New line after progress bar - - // Batch insert all data - console.log(` Inserting ${formatNumber(allSessions.length)} sessions...`); - await batchInsertSessions(prisma, allSessions as SessionCreateInput[], verbose); - - console.log(` Inserting ${formatNumber(allEvents.length)} events...`); - await batchInsertEvents(prisma, allEvents as WebsiteEventCreateInput[], verbose); - - if (allEventData.length > 0) { - console.log(` Inserting ${formatNumber(allEventData.length)} event data entries...`); - await batchInsertEventData(prisma, allEventData as EventDataCreateInput[], verbose); - } - - if (allRevenue.length > 0) { - console.log(` Inserting ${formatNumber(allRevenue.length)} revenue entries...`); - await batchInsertRevenue(prisma, allRevenue as RevenueCreateInput[], verbose); - } - - return { - sessions: allSessions.length, - events: allEvents.length, - eventData: allEventData.length, - revenue: allRevenue.length, - }; -} - -function createPrismaClient(): PrismaClient { - const url = process.env.DATABASE_URL; - if (!url) { - throw new Error( - 'DATABASE_URL environment variable is not set.\n' + - 'Please set DATABASE_URL in your .env file or environment.\n' + - 'Example: DATABASE_URL=postgresql://user:password@localhost:5432/umami', - ); - } - - let schema: string | undefined; - try { - const connectionUrl = new URL(url); - schema = connectionUrl.searchParams.get('schema') ?? undefined; - } catch { - throw new Error( - 'DATABASE_URL is not a valid URL.\n' + - 'Expected format: postgresql://user:password@host:port/database\n' + - `Received: ${url.substring(0, 30)}...`, - ); - } - - const adapter = new PrismaPg({ connectionString: url }, { schema }); - - return new PrismaClient({ - adapter, - errorFormat: 'pretty', - }); -} - -export async function seed(config: SeedConfig): Promise { - const prisma = createPrismaClient(); - - try { - const endDate = new Date(); - const startDate = subDays(endDate, config.days); - const days = generateDatesBetween(startDate, endDate); - - console.log(`\nSeed Configuration:`); - console.log( - ` Date range: ${startDate.toISOString().split('T')[0]} to ${endDate.toISOString().split('T')[0]}`, - ); - console.log(` Days: ${days.length}`); - console.log(` Clear existing: ${config.clear}`); - - if (config.clear) { - await clearDemoData(prisma); - } - - // Find admin user to own the demo websites - const adminUserId = await findAdminUser(prisma); - console.log(` Using admin user: ${adminUserId}`); - - // Generate Blog site (low traffic) - const blogResults = await generateSiteData( - prisma, - { - name: BLOG_WEBSITE_NAME, - domain: BLOG_WEBSITE_DOMAIN, - sessionsPerDay: BLOG_SESSIONS_PER_DAY, - getSiteConfig: getBlogSiteConfig, - getJourney: getBlogJourney, - }, - days, - adminUserId, - config.verbose, - ); - - // Generate SaaS site (high traffic) - const saasResults = await generateSiteData( - prisma, - { - name: SAAS_WEBSITE_NAME, - domain: SAAS_WEBSITE_DOMAIN, - sessionsPerDay: SAAS_SESSIONS_PER_DAY, - getSiteConfig: getSaasSiteConfig, - getJourney: getSaasJourney, - revenueConfigs: saasRevenueConfigs, - }, - days, - adminUserId, - config.verbose, - ); - - const result: SeedResult = { - websites: 2, - sessions: blogResults.sessions + saasResults.sessions, - events: blogResults.events + saasResults.events, - eventData: blogResults.eventData + saasResults.eventData, - revenue: blogResults.revenue + saasResults.revenue, - }; - - console.log(`\n${'─'.repeat(50)}`); - console.log(`Seed Complete!`); - console.log(`${'─'.repeat(50)}`); - console.log(` Websites: ${formatNumber(result.websites)}`); - console.log(` Sessions: ${formatNumber(result.sessions)}`); - console.log(` Events: ${formatNumber(result.events)}`); - console.log(` Event Data: ${formatNumber(result.eventData)}`); - console.log(` Revenue: ${formatNumber(result.revenue)}`); - console.log(`${'─'.repeat(50)}\n`); - - return result; - } finally { - await prisma.$disconnect(); - } -} diff --git a/scripts/seed/sites/blog.ts b/scripts/seed/sites/blog.ts deleted file mode 100644 index e60b8b95e..000000000 --- a/scripts/seed/sites/blog.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { weightedRandom, type WeightedOption } from '../utils.js'; -import type { - SiteConfig, - JourneyConfig, - PageConfig, - CustomEventConfig, -} from '../generators/events.js'; - -export const BLOG_WEBSITE_NAME = 'Demo Blog'; -export const BLOG_WEBSITE_DOMAIN = 'blog.example.com'; - -const blogPosts = [ - 'getting-started-with-analytics', - 'privacy-first-tracking', - 'understanding-your-visitors', - 'improving-page-performance', - 'seo-best-practices', - 'content-marketing-guide', - 'building-audience-trust', - 'data-driven-decisions', -]; - -export const blogPages: PageConfig[] = [ - { path: '/', title: 'Demo Blog - Home', weight: 0.25, avgTimeOnPage: 30 }, - { path: '/blog', title: 'Blog Posts', weight: 0.2, avgTimeOnPage: 45 }, - { path: '/about', title: 'About Us', weight: 0.1, avgTimeOnPage: 60 }, - { path: '/contact', title: 'Contact', weight: 0.05, avgTimeOnPage: 45 }, - ...blogPosts.map(slug => ({ - path: `/blog/${slug}`, - title: slug - .split('-') - .map(w => w.charAt(0).toUpperCase() + w.slice(1)) - .join(' '), - weight: 0.05, - avgTimeOnPage: 180, - })), -]; - -export const blogJourneys: JourneyConfig[] = [ - // Direct to blog post (organic search) - { pages: ['/blog/getting-started-with-analytics'], weight: 0.15 }, - { pages: ['/blog/privacy-first-tracking'], weight: 0.12 }, - { pages: ['/blog/understanding-your-visitors'], weight: 0.1 }, - - // Homepage bounces - { pages: ['/'], weight: 0.15 }, - - // Homepage to blog listing - { pages: ['/', '/blog'], weight: 0.1 }, - - // Homepage to blog post - { pages: ['/', '/blog', '/blog/seo-best-practices'], weight: 0.08 }, - { pages: ['/', '/blog', '/blog/content-marketing-guide'], weight: 0.08 }, - - // About page visits - { pages: ['/', '/about'], weight: 0.07 }, - { pages: ['/', '/about', '/contact'], weight: 0.05 }, - - // Blog post to another - { pages: ['/blog/improving-page-performance', '/blog/data-driven-decisions'], weight: 0.05 }, - - // Longer sessions - { pages: ['/', '/blog', '/blog/building-audience-trust', '/about'], weight: 0.05 }, -]; - -export const blogCustomEvents: CustomEventConfig[] = [ - { - name: 'newsletter_signup', - weight: 0.03, - pages: ['/', '/blog'], - }, - { - name: 'share_click', - weight: 0.05, - pages: blogPosts.map(slug => `/blog/${slug}`), - data: { - platform: ['twitter', 'linkedin', 'facebook', 'copy_link'], - }, - }, - { - name: 'scroll_depth', - weight: 0.2, - pages: blogPosts.map(slug => `/blog/${slug}`), - data: { - depth: [25, 50, 75, 100], - }, - }, -]; - -export function getBlogSiteConfig(): SiteConfig { - return { - hostname: BLOG_WEBSITE_DOMAIN, - pages: blogPages, - journeys: blogJourneys, - customEvents: blogCustomEvents, - }; -} - -export function getBlogJourney(): string[] { - const journeyWeights: WeightedOption[] = blogJourneys.map(j => ({ - value: j.pages, - weight: j.weight, - })); - - return weightedRandom(journeyWeights); -} - -export const BLOG_SESSIONS_PER_DAY = 3; // ~90 sessions per month diff --git a/scripts/seed/sites/saas.ts b/scripts/seed/sites/saas.ts deleted file mode 100644 index 133895afd..000000000 --- a/scripts/seed/sites/saas.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { weightedRandom, type WeightedOption } from '../utils.js'; -import type { - SiteConfig, - JourneyConfig, - PageConfig, - CustomEventConfig, -} from '../generators/events.js'; -import type { RevenueConfig } from '../generators/revenue.js'; - -export const SAAS_WEBSITE_NAME = 'Demo SaaS'; -export const SAAS_WEBSITE_DOMAIN = 'app.example.com'; - -const docsSections = [ - 'getting-started', - 'installation', - 'configuration', - 'api-reference', - 'integrations', -]; - -const blogPosts = [ - 'announcing-v2', - 'customer-success-story', - 'product-roadmap', - 'security-best-practices', -]; - -export const saasPages: PageConfig[] = [ - { path: '/', title: 'Demo SaaS - Analytics Made Simple', weight: 0.2, avgTimeOnPage: 45 }, - { path: '/features', title: 'Features', weight: 0.15, avgTimeOnPage: 90 }, - { path: '/pricing', title: 'Pricing', weight: 0.15, avgTimeOnPage: 120 }, - { path: '/docs', title: 'Documentation', weight: 0.1, avgTimeOnPage: 60 }, - { path: '/blog', title: 'Blog', weight: 0.05, avgTimeOnPage: 45 }, - { path: '/signup', title: 'Sign Up', weight: 0.08, avgTimeOnPage: 90 }, - { path: '/login', title: 'Login', weight: 0.05, avgTimeOnPage: 30 }, - { path: '/demo', title: 'Request Demo', weight: 0.05, avgTimeOnPage: 60 }, - ...docsSections.map(slug => ({ - path: `/docs/${slug}`, - title: `Docs: ${slug - .split('-') - .map(w => w.charAt(0).toUpperCase() + w.slice(1)) - .join(' ')}`, - weight: 0.02, - avgTimeOnPage: 180, - })), - ...blogPosts.map(slug => ({ - path: `/blog/${slug}`, - title: slug - .split('-') - .map(w => w.charAt(0).toUpperCase() + w.slice(1)) - .join(' '), - weight: 0.02, - avgTimeOnPage: 150, - })), -]; - -export const saasJourneys: JourneyConfig[] = [ - // Conversion funnel - { pages: ['/', '/features', '/pricing', '/signup'], weight: 0.12 }, - { pages: ['/', '/pricing', '/signup'], weight: 0.1 }, - { pages: ['/pricing', '/signup'], weight: 0.08 }, - - // Feature exploration - { pages: ['/', '/features'], weight: 0.1 }, - { pages: ['/', '/features', '/pricing'], weight: 0.08 }, - - // Documentation users - { pages: ['/docs', '/docs/getting-started'], weight: 0.08 }, - { pages: ['/docs/getting-started', '/docs/installation', '/docs/configuration'], weight: 0.06 }, - { pages: ['/docs/api-reference'], weight: 0.05 }, - - // Blog readers - { pages: ['/blog/announcing-v2'], weight: 0.05 }, - { pages: ['/blog/customer-success-story'], weight: 0.04 }, - - // Returning users - { pages: ['/login'], weight: 0.08 }, - - // Bounces - { pages: ['/'], weight: 0.08 }, - { pages: ['/pricing'], weight: 0.05 }, - - // Demo requests - { pages: ['/', '/demo'], weight: 0.03 }, -]; - -export const saasCustomEvents: CustomEventConfig[] = [ - { - name: 'signup_started', - weight: 0.6, - pages: ['/signup'], - data: { - plan: ['free', 'pro', 'enterprise'], - }, - }, - { - name: 'signup_completed', - weight: 0.3, - pages: ['/signup'], - data: { - plan: ['free', 'pro', 'enterprise'], - method: ['email', 'google', 'github'], - }, - }, - { - name: 'purchase', - weight: 0.15, - pages: ['/signup', '/pricing'], - data: { - plan: ['pro', 'enterprise'], - billing: ['monthly', 'annual'], - revenue: [29, 49, 99, 299], - currency: ['USD'], - }, - }, - { - name: 'demo_requested', - weight: 0.5, - pages: ['/demo'], - data: { - company_size: ['1-10', '11-50', '51-200', '200+'], - }, - }, - { - name: 'feature_viewed', - weight: 0.3, - pages: ['/features'], - data: { - feature: ['analytics', 'reports', 'api', 'integrations', 'privacy'], - }, - }, - { - name: 'cta_click', - weight: 0.15, - pages: ['/', '/features', '/pricing'], - data: { - button: ['hero_signup', 'nav_signup', 'pricing_cta', 'footer_cta'], - }, - }, - { - name: 'docs_search', - weight: 0.2, - pages: ['/docs', ...docsSections.map(s => `/docs/${s}`)], - data: { - query_type: ['api', 'setup', 'integration', 'troubleshooting'], - }, - }, -]; - -export const saasRevenueConfigs: RevenueConfig[] = [ - { - eventName: 'purchase', - minAmount: 29, - maxAmount: 29, - currency: 'USD', - weight: 0.7, // 70% Pro plan - }, - { - eventName: 'purchase', - minAmount: 299, - maxAmount: 299, - currency: 'USD', - weight: 0.3, // 30% Enterprise - }, -]; - -export function getSaasSiteConfig(): SiteConfig { - return { - hostname: SAAS_WEBSITE_DOMAIN, - pages: saasPages, - journeys: saasJourneys, - customEvents: saasCustomEvents, - }; -} - -export function getSaasJourney(): string[] { - const journeyWeights: WeightedOption[] = saasJourneys.map(j => ({ - value: j.pages, - weight: j.weight, - })); - - return weightedRandom(journeyWeights); -} - -export const SAAS_SESSIONS_PER_DAY = 500; diff --git a/scripts/seed/utils.ts b/scripts/seed/utils.ts deleted file mode 100644 index 7b44261ed..000000000 --- a/scripts/seed/utils.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { v4 as uuidv4 } from 'uuid'; - -export interface WeightedOption { - value: T; - weight: number; -} - -export function weightedRandom(options: WeightedOption[]): T { - const totalWeight = options.reduce((sum, opt) => sum + opt.weight, 0); - let random = Math.random() * totalWeight; - - for (const option of options) { - random -= option.weight; - if (random <= 0) { - return option.value; - } - } - - return options[options.length - 1].value; -} - -export function randomInt(min: number, max: number): number { - return Math.floor(Math.random() * (max - min + 1)) + min; -} - -export function randomFloat(min: number, max: number): number { - return Math.random() * (max - min) + min; -} - -export function pickRandom(array: T[]): T { - return array[Math.floor(Math.random() * array.length)]; -} - -export function shuffleArray(array: T[]): T[] { - const result = [...array]; - for (let i = result.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [result[i], result[j]] = [result[j], result[i]]; - } - return result; -} - -export function uuid(): string { - return uuidv4(); -} - -export function generateDatesBetween(startDate: Date, endDate: Date): Date[] { - const dates: Date[] = []; - const current = new Date(startDate); - current.setHours(0, 0, 0, 0); - - while (current <= endDate) { - dates.push(new Date(current)); - current.setDate(current.getDate() + 1); - } - - return dates; -} - -export function addHours(date: Date, hours: number): Date { - return new Date(date.getTime() + hours * 60 * 60 * 1000); -} - -export function addMinutes(date: Date, minutes: number): Date { - return new Date(date.getTime() + minutes * 60 * 1000); -} - -export function addSeconds(date: Date, seconds: number): Date { - return new Date(date.getTime() + seconds * 1000); -} - -export function subDays(date: Date, days: number): Date { - return new Date(date.getTime() - days * 24 * 60 * 60 * 1000); -} - -export function formatNumber(num: number): string { - return num.toLocaleString(); -} - -export function progressBar(current: number, total: number, width = 30): string { - const percent = current / total; - const filled = Math.round(width * percent); - const empty = width - filled; - return `[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${Math.round(percent * 100)}%`; -} diff --git a/src/app/(main)/links/LinksTable.tsx b/src/app/(main)/links/LinksTable.tsx index a3b4a86ad..2ce4ba497 100644 --- a/src/app/(main)/links/LinksTable.tsx +++ b/src/app/(main)/links/LinksTable.tsx @@ -21,11 +21,7 @@ export function LinksTable(props: DataTableProps) { {({ slug }: any) => { const url = getSlugUrl(slug); - return ( - - {url} - - ); + return {url}; }} diff --git a/src/app/(main)/links/[linkId]/LinkHeader.tsx b/src/app/(main)/links/[linkId]/LinkHeader.tsx index 33f0c2426..b7c70f076 100644 --- a/src/app/(main)/links/[linkId]/LinkHeader.tsx +++ b/src/app/(main)/links/[linkId]/LinkHeader.tsx @@ -11,7 +11,7 @@ export function LinkHeader() { return ( } marginBottom="3"> - + diff --git a/src/app/(main)/pixels/PixelsTable.tsx b/src/app/(main)/pixels/PixelsTable.tsx index 48a845896..48f2121ba 100644 --- a/src/app/(main)/pixels/PixelsTable.tsx +++ b/src/app/(main)/pixels/PixelsTable.tsx @@ -21,11 +21,7 @@ export function PixelsTable(props: DataTableProps) { {({ slug }: any) => { const url = getSlugUrl(slug); - return ( - - {url} - - ); + return {url}; }} diff --git a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx index e7a925160..7db229126 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx @@ -13,19 +13,12 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) { const { renderUrl, pathname } = useNavigation(); const isSettings = pathname.endsWith('/settings'); - const { formatMessage, labels } = useMessages(); - if (isSettings) { return null; } return ( - } - marginBottom="3" - titleHref={renderUrl(`/websites/${website.id}`, false)} - > + } marginBottom="3"> @@ -36,7 +29,7 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) { - {formatMessage(labels.edit)} + Edit )} diff --git a/src/app/api/batch/route.ts b/src/app/api/batch/route.ts index 46e8b3c33..aae14c96e 100644 --- a/src/app/api/batch/route.ts +++ b/src/app/api/batch/route.ts @@ -17,7 +17,6 @@ export async function POST(request: Request) { const errors = []; let index = 0; - let cache = null; for (const data of body) { // Recreate a fresh Request since `new Request(request)` will have the following error: // > Cannot read private member #state from an object whose class did not declare it @@ -34,12 +33,9 @@ export async function POST(request: Request) { }); const response = await send.POST(newRequest); - const responseJson = await response.json(); if (!response.ok) { - errors.push({ index, response: responseJson }); - } else { - cache ??= responseJson.cache; + errors.push({ index, response: await response.json() }); } index++; @@ -50,7 +46,6 @@ export async function POST(request: Request) { processed: body.length - errors.length, errors: errors.length, details: errors, - cache, }); } catch (e) { return serverError(e); diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts index a0becc2ac..d1a7b90ba 100644 --- a/src/app/api/send/route.ts +++ b/src/app/api/send/route.ts @@ -41,9 +41,6 @@ const schema = z.object({ userAgent: z.string().optional(), timestamp: z.coerce.number().int().optional(), id: z.string().optional(), - browser: z.string().optional(), - os: z.string().optional(), - device: z.string().optional(), }) .refine( data => { diff --git a/src/components/common/ExternalLink.tsx b/src/components/common/ExternalLink.tsx index dec0d16fe..6b6dbb998 100644 --- a/src/components/common/ExternalLink.tsx +++ b/src/components/common/ExternalLink.tsx @@ -1,13 +1,8 @@ import { Icon, Row, Text } from '@umami/react-zen'; -import Link, { type LinkProps } from 'next/link'; -import type { ReactNode } from 'react'; +import Link from 'next/link'; import { ExternalLink as LinkIcon } from '@/components/icons'; -export function ExternalLink({ - href, - children, - ...props -}: LinkProps & { href: string; children: ReactNode }) { +export function ExternalLink({ href, children, ...props }) { return ( diff --git a/src/components/common/PageHeader.tsx b/src/components/common/PageHeader.tsx index 6d8c122a8..b46051508 100644 --- a/src/components/common/PageHeader.tsx +++ b/src/components/common/PageHeader.tsx @@ -1,6 +1,5 @@ import { Column, Grid, Heading, Icon, Row, Text } from '@umami/react-zen'; import type { ReactNode } from 'react'; -import { LinkButton } from './LinkButton'; export function PageHeader({ title, @@ -8,7 +7,6 @@ export function PageHeader({ label, icon, showBorder = true, - titleHref, children, }: { title: string; @@ -16,7 +14,6 @@ export function PageHeader({ label?: ReactNode; icon?: ReactNode; showBorder?: boolean; - titleHref?: string; allowEdit?: boolean; className?: string; children?: ReactNode; @@ -36,13 +33,7 @@ export function PageHeader({ {icon} )} - {title && titleHref ? ( - - {title} - - ) : ( - title && {title} - )} + {title && {title}} {description && ( diff --git a/src/components/metrics/ActiveUsers.tsx b/src/components/metrics/ActiveUsers.tsx index a4bc7da22..a7f6e6044 100644 --- a/src/components/metrics/ActiveUsers.tsx +++ b/src/components/metrics/ActiveUsers.tsx @@ -1,6 +1,5 @@ import { StatusLight, Text } from '@umami/react-zen'; import { useMemo } from 'react'; -import { LinkButton } from '@/components/common/LinkButton'; import { useActyiveUsersQuery, useMessages } from '@/components/hooks'; export function ActiveUsers({ @@ -28,12 +27,10 @@ export function ActiveUsers({ } return ( - - - - {count} {formatMessage(labels.online)} - - - + + + {count} {formatMessage(labels.online)} + + ); } diff --git a/src/lang/ar-SA.json b/src/lang/ar-SA.json index 5b5cfa9fb..6a60dd534 100644 --- a/src/lang/ar-SA.json +++ b/src/lang/ar-SA.json @@ -289,7 +289,6 @@ "label.websites": "المواقع", "label.window": "النافذة", "label.yesterday": "الأمس", - "label.behavior": "السلوك", "message.action-confirmation": "اكتب {confirmation} في المربع أدناه للتأكيد.", "message.active-users": "{x} حاليا {x, plural, one {زائر واحد} other {زوار}}", "message.bad-request": "Bad request", diff --git a/src/lang/be-BY.json b/src/lang/be-BY.json index 1a866a92e..932ea557c 100644 --- a/src/lang/be-BY.json +++ b/src/lang/be-BY.json @@ -20,7 +20,6 @@ "label.average": "Сярэдняе", "label.back": "Назад", "label.before": "Да", - "label.behavior": "Паводзіны", "label.boards": "Дошкі", "label.bounce-rate": "Паказчык адмоваў", "label.breakdown": "Разбіўка", diff --git a/src/lang/bg-BG.json b/src/lang/bg-BG.json index 4b0effc89..0487498d0 100644 --- a/src/lang/bg-BG.json +++ b/src/lang/bg-BG.json @@ -20,7 +20,6 @@ "label.average": "Средно", "label.back": "Назад", "label.before": "Преди", - "label.behavior": "Поведение", "label.boards": "Дъски", "label.bounce-rate": "Kоефициент на отказ", "label.breakdown": "Разбивка", diff --git a/src/lang/bn-BD.json b/src/lang/bn-BD.json index 9b9ad2f43..0260558be 100644 --- a/src/lang/bn-BD.json +++ b/src/lang/bn-BD.json @@ -20,7 +20,6 @@ "label.average": "গড়", "label.back": "পেছনে", "label.before": "পূর্বে", - "label.behavior": "আচরণ", "label.boards": "বোর্ডসমূহ", "label.bounce-rate": "উপরে উঠার হার", "label.breakdown": "ভাঙ্গন", diff --git a/src/lang/bs-BA.json b/src/lang/bs-BA.json index 56848771b..72d635d41 100644 --- a/src/lang/bs-BA.json +++ b/src/lang/bs-BA.json @@ -20,7 +20,6 @@ "label.average": "Prosjek", "label.back": "Nazad", "label.before": "Prije", - "label.behavior": "Ponašanje", "label.boards": "Ploče", "label.bounce-rate": "Stopa napuštanja", "label.breakdown": "Pregled po kategorijama", diff --git a/src/lang/ca-ES.json b/src/lang/ca-ES.json index ab5444ceb..7ff465557 100644 --- a/src/lang/ca-ES.json +++ b/src/lang/ca-ES.json @@ -20,7 +20,6 @@ "label.average": "Mitjana", "label.back": "Enrere", "label.before": "Abans", - "label.behavior": "Comportament", "label.boards": "Taulers", "label.bounce-rate": "Percentatge de rebot", "label.breakdown": "Desglossament", diff --git a/src/lang/cs-CZ.json b/src/lang/cs-CZ.json index 77d45a79f..a4dbc8654 100644 --- a/src/lang/cs-CZ.json +++ b/src/lang/cs-CZ.json @@ -20,7 +20,6 @@ "label.average": "Průměr", "label.back": "Zpět", "label.before": "Před", - "label.behavior": "Chování", "label.boards": "Nástěnky", "label.bounce-rate": "Okamžité opuštění", "label.breakdown": "Rozpis", diff --git a/src/lang/da-DK.json b/src/lang/da-DK.json index f6c447ff5..0078bcd31 100644 --- a/src/lang/da-DK.json +++ b/src/lang/da-DK.json @@ -20,7 +20,6 @@ "label.average": "Gennemsnit", "label.back": "Tilbage", "label.before": "Før", - "label.behavior": "Adfærd", "label.boards": "Tavler", "label.bounce-rate": "Afvisningsprocent", "label.breakdown": "Opdeling", diff --git a/src/lang/de-CH.json b/src/lang/de-CH.json index 55734ebdf..a770949aa 100644 --- a/src/lang/de-CH.json +++ b/src/lang/de-CH.json @@ -20,7 +20,6 @@ "label.average": "Durchschnitt", "label.back": "Zrugg", "label.before": "Vor", - "label.behavior": "Verhalte", "label.boards": "Boards", "label.bounce-rate": "Absprungsrate", "label.breakdown": "Uufschlüsselig", diff --git a/src/lang/de-DE.json b/src/lang/de-DE.json index 3436eb89e..9bbedbf90 100644 --- a/src/lang/de-DE.json +++ b/src/lang/de-DE.json @@ -20,7 +20,6 @@ "label.average": "Durchschnitt", "label.back": "Zurück", "label.before": "Vor", - "label.behavior": "Verhalten", "label.boards": "Boards", "label.bounce-rate": "Absprungrate", "label.breakdown": "Aufschlüsselung", diff --git a/src/lang/el-GR.json b/src/lang/el-GR.json index 720ff5ea1..df44a29d8 100644 --- a/src/lang/el-GR.json +++ b/src/lang/el-GR.json @@ -23,7 +23,6 @@ "label.boards": "Boards", "label.bounce-rate": "Ποσοστό αναπήδησης", "label.breakdown": "Breakdown", - "label.behavior": "Συμπεριφορά", "label.browser": "Browser", "label.browsers": "Προγράμματα περιήγησης", "label.campaigns": "Campaigns", diff --git a/src/lang/en-GB.json b/src/lang/en-GB.json index 7803dd68e..d22914953 100644 --- a/src/lang/en-GB.json +++ b/src/lang/en-GB.json @@ -20,7 +20,6 @@ "label.average": "Average", "label.back": "Back", "label.before": "Before", - "label.behavior": "Behavior", "label.boards": "Boards", "label.bounce-rate": "Bounce rate", "label.breakdown": "Breakdown", diff --git a/src/lang/en-US.json b/src/lang/en-US.json index 3e588f50d..60627ccad 100644 --- a/src/lang/en-US.json +++ b/src/lang/en-US.json @@ -289,7 +289,6 @@ "label.websites": "Websites", "label.window": "Window", "label.yesterday": "Yesterday", - "label.behavior": "Behavior", "message.action-confirmation": "Type {confirmation} in the box below to confirm.", "message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}", "message.bad-request": "Bad request", diff --git a/src/lang/es-ES.json b/src/lang/es-ES.json index e3a4d38d3..3e74ed353 100644 --- a/src/lang/es-ES.json +++ b/src/lang/es-ES.json @@ -290,7 +290,6 @@ "label.websites": "Sitios web", "label.window": "Ventana", "label.yesterday": "Ayer", - "label.behavior": "Comportamiento", "message.action-confirmation": "Escriba {confirmation} en el cuadro a continuación para confirmar.", "message.active-users": "{x} {x, plural, one {activo} other {activos}}", "message.bad-request": "Bad request", diff --git a/src/lang/fa-IR.json b/src/lang/fa-IR.json index 96b3da9bf..4e912fcea 100644 --- a/src/lang/fa-IR.json +++ b/src/lang/fa-IR.json @@ -20,7 +20,6 @@ "label.average": "میانگین", "label.back": "بازگشت", "label.before": "قبل از", - "label.behavior": "رفتار", "label.boards": "بردها", "label.bounce-rate": "نرخ ریزش", "label.breakdown": "تفکیک", diff --git a/src/lang/fi-FI.json b/src/lang/fi-FI.json index daaa62f0d..06f66d6c5 100644 --- a/src/lang/fi-FI.json +++ b/src/lang/fi-FI.json @@ -289,7 +289,6 @@ "label.websites": "Verkkosivut", "label.window": "Window", "label.yesterday": "Yesterday", - "label.behavior": "Behavior", "message.action-confirmation": "Type {confirmation} in the box below to confirm.", "message.active-users": "{x} {x, plural, one {vierailija} other {vierailijaa}}", "message.bad-request": "Bad request", diff --git a/src/lang/fo-FO.json b/src/lang/fo-FO.json index 6fca42586..e811ab5e3 100644 --- a/src/lang/fo-FO.json +++ b/src/lang/fo-FO.json @@ -20,7 +20,6 @@ "label.average": "Miðal", "label.back": "Aftur", "label.before": "Áðrenn", - "label.behavior": "Atferð", "label.boards": "Borð", "label.bounce-rate": "Bounce prosenttal", "label.breakdown": "Sundurgreining", diff --git a/src/lang/fr-FR.json b/src/lang/fr-FR.json index cd6a96bc3..517633afd 100644 --- a/src/lang/fr-FR.json +++ b/src/lang/fr-FR.json @@ -289,9 +289,6 @@ "label.websites": "Sites", "label.window": "Fenêtre", "label.yesterday": "Hier", - "label.behavior": "Comportement", - "label.traffic": "Trafic", - "label.segments": "Segments", "message.action-confirmation": "Taper {confirmation} ci-dessous pour confirmer.", "message.active-users": "{x} {x, plural, one {visiteur} other {visiteurs}} actuellement", "message.bad-request": "Bad request", @@ -318,13 +315,13 @@ "message.no-teams": "Vous n'avez pas créé d'équipe.", "message.no-users": "Aucun utilisateur.", "message.no-websites-configured": "Vous n'avez pas configuré de site.", - "message.not-found": "Non trouvé!", - "message.nothing-selected": "Rien n'est sélectionné.", + "message.not-found": "Not found", + "message.nothing-selected": "Nothing selected.", "message.page-not-found": "Page non trouvée.", "message.reset-website": "Pour réinitialiser ce site, taper {confirmation} ci-dessous pour confirmer.", "message.reset-website-warning": "Toutes les statistiques pour ce site seront supprimées, mais votre code de suivi restera intact.", "message.saved": "Enregistré.", - "message.sever-error": "Erreur serveur", + "message.sever-error": "Server error", "message.share-url": "Les statistiques de votre site sont accessibles publiquement sur cette URL :", "message.team-already-member": "Vous êtes déjà membre de cette équipe.", "message.team-not-found": "Équipe non trouvée.", @@ -334,7 +331,7 @@ "message.transfer-user-website-to-team": "Choisir l'équipe à laquelle transférer ce site.", "message.transfer-website": "Transférer la propriété du site sur votre compte ou à une autre équipe.", "message.triggered-event": "Évènement déclenché", - "message.unauthorized": "Non authorisé!", + "message.unauthorized": "Unauthorized", "message.user-deleted": "Utilisateur supprimé.", "message.viewed-page": "Page vue", "message.visitor-log": "Visiteur de {country} utilisant {browser} sur {os} {device}" diff --git a/src/lang/ga-ES.json b/src/lang/ga-ES.json index 208260059..bce46d3ba 100644 --- a/src/lang/ga-ES.json +++ b/src/lang/ga-ES.json @@ -289,7 +289,6 @@ "label.websites": "Sitios web", "label.window": "Ventá", "label.yesterday": "Onte", - "label.behavior": "Comportamento", "message.action-confirmation": "Escribe {confirmation} na caixa de embaixo para confirmar.", "message.active-users": "{x} actual {x, plural, one {visitante} other {visitantes}}", "message.bad-request": "Bad request", diff --git a/src/lang/he-IL.json b/src/lang/he-IL.json index 2d115c8d7..2345dc448 100644 --- a/src/lang/he-IL.json +++ b/src/lang/he-IL.json @@ -278,7 +278,6 @@ "label.value": "Value", "label.view": "View", "label.view-details": "פרטים נוספים", - "label.behavior": "התנהגות", "label.view-only": "View only", "label.views": "צפיות", "label.views-per-visit": "Views per visit", diff --git a/src/lang/hi-IN.json b/src/lang/hi-IN.json index 54cac3012..513d67428 100644 --- a/src/lang/hi-IN.json +++ b/src/lang/hi-IN.json @@ -20,7 +20,6 @@ "label.average": "औसत", "label.back": "पीछे", "label.before": "पहले", - "label.behavior": "व्यवहार", "label.boards": "बोर्ड्स", "label.bounce-rate": "उछाल दर", "label.breakdown": "विभाजन", diff --git a/src/lang/hr-HR.json b/src/lang/hr-HR.json index 141ad3fdd..c587e92ef 100644 --- a/src/lang/hr-HR.json +++ b/src/lang/hr-HR.json @@ -20,7 +20,6 @@ "label.average": "Prosjek", "label.back": "Natrag ", "label.before": "Prije", - "label.behavior": "Ponašanje", "label.boards": "Ploče", "label.bounce-rate": "Stopa napuštanja", "label.breakdown": "Raspad", diff --git a/src/lang/hu-HU.json b/src/lang/hu-HU.json index 1666b7a11..67bafa61f 100644 --- a/src/lang/hu-HU.json +++ b/src/lang/hu-HU.json @@ -20,7 +20,6 @@ "label.average": "Átlag", "label.back": "Vissza", "label.before": "Előtt", - "label.behavior": "Viselkedés", "label.boards": "Táblák", "label.bounce-rate": "Visszafordulási arány", "label.breakdown": "Bontás", diff --git a/src/lang/id-ID.json b/src/lang/id-ID.json index 30a64b6cb..e7a264701 100644 --- a/src/lang/id-ID.json +++ b/src/lang/id-ID.json @@ -20,7 +20,6 @@ "label.average": "Rata-rata", "label.back": "Kembali", "label.before": "Sebelum", - "label.behavior": "Perilaku", "label.boards": "Papan", "label.bounce-rate": "Rasio pentalan", "label.breakdown": "Rincian", diff --git a/src/lang/it-IT.json b/src/lang/it-IT.json index 40cb5ecda..019b4de58 100644 --- a/src/lang/it-IT.json +++ b/src/lang/it-IT.json @@ -20,7 +20,6 @@ "label.average": "Media", "label.back": "Indietro", "label.before": "Prima", - "label.behavior": "Comportamento", "label.boards": "Bacheche", "label.bounce-rate": "Frequenza di rimbalzo", "label.breakdown": "Dettaglio", diff --git a/src/lang/ja-JP.json b/src/lang/ja-JP.json index 7d2bf4030..2c6cc2f90 100644 --- a/src/lang/ja-JP.json +++ b/src/lang/ja-JP.json @@ -20,7 +20,6 @@ "label.average": "平均", "label.back": "戻る", "label.before": "直前", - "label.behavior": "行動", "label.boards": "ボード", "label.bounce-rate": "直帰率", "label.breakdown": "故障", diff --git a/src/lang/km-KH.json b/src/lang/km-KH.json index 087e24dc5..bae63986e 100644 --- a/src/lang/km-KH.json +++ b/src/lang/km-KH.json @@ -20,7 +20,6 @@ "label.average": "ជាមធ្យម", "label.back": "ថយក្រោយ", "label.before": "មុន", - "label.behavior": "អាកប្បកិរិយា", "label.boards": "ក្តារ", "label.bounce-rate": "ចំនួនវិលត្រឡប់", "label.breakdown": "បំបែកលម្អិត", diff --git a/src/lang/ko-KR.json b/src/lang/ko-KR.json index 977eea4e2..5826b1a46 100644 --- a/src/lang/ko-KR.json +++ b/src/lang/ko-KR.json @@ -20,7 +20,6 @@ "label.average": "평균", "label.back": "뒤로", "label.before": "이전", - "label.behavior": "행동", "label.boards": "보드", "label.bounce-rate": "이탈률", "label.breakdown": "세부 사항", diff --git a/src/lang/lt-LT.json b/src/lang/lt-LT.json index 772fa34e4..2fc89c2cd 100644 --- a/src/lang/lt-LT.json +++ b/src/lang/lt-LT.json @@ -20,7 +20,6 @@ "label.average": "Vidurkis", "label.back": "Atgal", "label.before": "Prieš", - "label.behavior": "Elgsena", "label.boards": "Lentos", "label.bounce-rate": "Atmetimo rodiklis", "label.breakdown": "Išskaidymas", diff --git a/src/lang/mn-MN.json b/src/lang/mn-MN.json index e9c649dbd..22e37bc42 100644 --- a/src/lang/mn-MN.json +++ b/src/lang/mn-MN.json @@ -20,7 +20,6 @@ "label.average": "Дундаж", "label.back": "Буцах", "label.before": "Өмнө", - "label.behavior": "Зан төлөв", "label.boards": "Самбарууд", "label.bounce-rate": "Нэг хуудас үзээд гарсан", "label.breakdown": "Задаргаа", diff --git a/src/lang/ms-MY.json b/src/lang/ms-MY.json index 32abd0850..eb5976bd5 100644 --- a/src/lang/ms-MY.json +++ b/src/lang/ms-MY.json @@ -20,7 +20,6 @@ "label.average": "Average", "label.back": "Kembali", "label.before": "Before", - "label.behavior": "Behavior", "label.boards": "Boards", "label.bounce-rate": "Kadar lantunan", "label.breakdown": "Breakdown", diff --git a/src/lang/my-MM.json b/src/lang/my-MM.json index 156b0c260..77f8e040a 100644 --- a/src/lang/my-MM.json +++ b/src/lang/my-MM.json @@ -20,7 +20,6 @@ "label.average": "ပျမ်းမျှ", "label.back": "နောက်သို့", "label.before": "မတိုင်မီ", - "label.behavior": "အပြုအမူ", "label.boards": "Boards", "label.bounce-rate": "Bounce နှုန်း", "label.breakdown": "ခွဲခြမ်းစိတ်ဖြာမှု", diff --git a/src/lang/nb-NO.json b/src/lang/nb-NO.json index adb4468e3..fc4222561 100644 --- a/src/lang/nb-NO.json +++ b/src/lang/nb-NO.json @@ -20,7 +20,6 @@ "label.average": "Gjennomsnnitt", "label.back": "Tilbake", "label.before": "Før", - "label.behavior": "Atferd", "label.boards": "Tavler", "label.bounce-rate": "Avvisningsfrekvens", "label.breakdown": "Nedbrytning", diff --git a/src/lang/nl-NL.json b/src/lang/nl-NL.json index 1ec5c0203..e48fa89e9 100644 --- a/src/lang/nl-NL.json +++ b/src/lang/nl-NL.json @@ -20,7 +20,6 @@ "label.average": "Gemiddelde", "label.back": "Terug", "label.before": "Voor", - "label.behavior": "Gedrag", "label.boards": "Borden", "label.bounce-rate": "Bouncepercentage", "label.breakdown": "Opsplitsen", diff --git a/src/lang/pl-PL.json b/src/lang/pl-PL.json index 0c8b00043..11c76763a 100644 --- a/src/lang/pl-PL.json +++ b/src/lang/pl-PL.json @@ -20,7 +20,6 @@ "label.average": "Średnia", "label.back": "Powrót", "label.before": "Przed", - "label.behavior": "Zachowanie", "label.boards": "Tablice", "label.bounce-rate": "Współczynnik odrzuceń", "label.breakdown": "Rozbicie", diff --git a/src/lang/pt-BR.json b/src/lang/pt-BR.json index c34c9ab05..5ccd48692 100644 --- a/src/lang/pt-BR.json +++ b/src/lang/pt-BR.json @@ -20,7 +20,6 @@ "label.average": "Média", "label.back": "Voltar", "label.before": "Antes", - "label.behavior": "Comportamento", "label.boards": "Quadros", "label.bounce-rate": "Taxa de rejeição", "label.breakdown": "Detalhamento", diff --git a/src/lang/pt-PT.json b/src/lang/pt-PT.json index 86734cb5d..ba5b3b08c 100644 --- a/src/lang/pt-PT.json +++ b/src/lang/pt-PT.json @@ -20,7 +20,6 @@ "label.average": "Média", "label.back": "Voltar", "label.before": "Antes", - "label.behavior": "Comportamento", "label.boards": "Quadros", "label.bounce-rate": "Taxa de rejeição", "label.breakdown": "Detalhamento", diff --git a/src/lang/ro-RO.json b/src/lang/ro-RO.json index 786333049..547070488 100644 --- a/src/lang/ro-RO.json +++ b/src/lang/ro-RO.json @@ -20,7 +20,6 @@ "label.average": "Mediu", "label.back": "Înapoi", "label.before": "Înainte", - "label.behavior": "Comportament", "label.boards": "Panouri", "label.bounce-rate": "Rata de respingere", "label.breakdown": "Detaliat", diff --git a/src/lang/ru-RU.json b/src/lang/ru-RU.json index 96d0538fb..e63e66ab5 100644 --- a/src/lang/ru-RU.json +++ b/src/lang/ru-RU.json @@ -20,7 +20,6 @@ "label.average": "Средний", "label.back": "Назад", "label.before": "До", - "label.behavior": "Поведение", "label.boards": "Доски", "label.bounce-rate": "Отказы", "label.breakdown": "Авария", diff --git a/src/lang/si-LK.json b/src/lang/si-LK.json index 3e6aff86c..34de41de2 100644 --- a/src/lang/si-LK.json +++ b/src/lang/si-LK.json @@ -20,7 +20,6 @@ "label.average": "Average", "label.back": "ආපසු", "label.before": "Before", - "label.behavior": "අචරණය", "label.boards": "Boards", "label.bounce-rate": "Bounce rate", "label.breakdown": "Breakdown", diff --git a/src/lang/sk-SK.json b/src/lang/sk-SK.json index 297d5e341..ed182331c 100644 --- a/src/lang/sk-SK.json +++ b/src/lang/sk-SK.json @@ -20,7 +20,6 @@ "label.average": "Priemer", "label.back": "Späť", "label.before": "Pred", - "label.behavior": "Správanie", "label.boards": "Tabule", "label.bounce-rate": "Okamžité opustenie", "label.breakdown": "Rozpis", diff --git a/src/lang/sl-SI.json b/src/lang/sl-SI.json index 3dd3226f6..5834881f5 100644 --- a/src/lang/sl-SI.json +++ b/src/lang/sl-SI.json @@ -20,7 +20,6 @@ "label.average": "Povprečno", "label.back": "Nazaj", "label.before": "Pred", - "label.behavior": "Obnašanje", "label.boards": "Table", "label.bounce-rate": "Odbojna stopnja", "label.breakdown": "Razčlenitev", diff --git a/src/lang/sv-SE.json b/src/lang/sv-SE.json index 1f456b0e7..18d216f20 100644 --- a/src/lang/sv-SE.json +++ b/src/lang/sv-SE.json @@ -20,7 +20,6 @@ "label.average": "Genomsnitt", "label.back": "Tillbaka", "label.before": "Före", - "label.behavior": "Beteende", "label.boards": "Anslagstavlor", "label.bounce-rate": "Avvisningsfrekvens", "label.breakdown": "Analys", diff --git a/src/lang/ta-IN.json b/src/lang/ta-IN.json index 9e33d7b6a..ad9926053 100644 --- a/src/lang/ta-IN.json +++ b/src/lang/ta-IN.json @@ -21,7 +21,6 @@ "label.back": "பின்னால்", "label.before": "Before", "label.boards": "Boards", - "label.behavior": "நடத்தை", "label.bounce-rate": "துள்ளல் விகிதம்", "label.breakdown": "Breakdown", "label.browser": "Browser", diff --git a/src/lang/th-TH.json b/src/lang/th-TH.json index b94ca9051..97df9fe3d 100644 --- a/src/lang/th-TH.json +++ b/src/lang/th-TH.json @@ -20,7 +20,6 @@ "label.average": "Average", "label.back": "ย้อนกลับ", "label.before": "Before", - "label.behavior": "พฤติกรรม", "label.boards": "Boards", "label.bounce-rate": "อัตราตีกลับ", "label.breakdown": "Breakdown", diff --git a/src/lang/tr-TR.json b/src/lang/tr-TR.json index 3a2dce4b8..4f8af0b32 100644 --- a/src/lang/tr-TR.json +++ b/src/lang/tr-TR.json @@ -20,7 +20,6 @@ "label.average": "Ortalama", "label.back": "Geri", "label.before": "Önce", - "label.behavior": "Davranış", "label.boards": "Panolar", "label.bounce-rate": "Tek sayfa ziyaret oranı", "label.breakdown": "Dağılım", diff --git a/src/lang/uk-UA.json b/src/lang/uk-UA.json index 768015bed..39c08db93 100644 --- a/src/lang/uk-UA.json +++ b/src/lang/uk-UA.json @@ -20,7 +20,6 @@ "label.average": "Середній", "label.back": "Назад", "label.before": "До", - "label.behavior": "Поведінка", "label.boards": "Дошки", "label.bounce-rate": "Показник відмов", "label.breakdown": "Розподіл", diff --git a/src/lang/ur-PK.json b/src/lang/ur-PK.json index 5cc31212c..66f33b9f5 100644 --- a/src/lang/ur-PK.json +++ b/src/lang/ur-PK.json @@ -20,7 +20,6 @@ "label.average": "Average", "label.back": "پیچھے", "label.before": "Before", - "label.behavior": "رویے", "label.boards": "Boards", "label.bounce-rate": "اچھال کی شرح", "label.breakdown": "Breakdown", diff --git a/src/lang/uz-UZ.json b/src/lang/uz-UZ.json index cf58945f4..e8a574c77 100644 --- a/src/lang/uz-UZ.json +++ b/src/lang/uz-UZ.json @@ -15,7 +15,6 @@ "label.average": "Oʻrtacha", "label.back": "Orqaga", "label.before": "Oldin", - "label.behavior": "Xulq-atvor", "label.bounce-rate": "Chiqib ketish darajasi", "label.breakdown": "Tahlil", "label.browser": "Brauzer", diff --git a/src/lang/vi-VN.json b/src/lang/vi-VN.json index fc0a8c139..5c9a78a24 100644 --- a/src/lang/vi-VN.json +++ b/src/lang/vi-VN.json @@ -15,7 +15,6 @@ "label.average": "Trung bình", "label.back": "Quay lại", "label.before": "Trước đó", - "label.behavior": "Hành vi", "label.bounce-rate": "Tỷ lệ thoát trang", "label.breakdown": "Phân tích chi tiết", "label.browser": "Trình duyệt", diff --git a/src/lang/zh-CN.json b/src/lang/zh-CN.json index c6f01dd5c..894ac35c0 100644 --- a/src/lang/zh-CN.json +++ b/src/lang/zh-CN.json @@ -20,7 +20,6 @@ "label.average": "平均", "label.back": "返回", "label.before": "之前", - "label.behavior": "行为", "label.boards": "看板", "label.bounce-rate": "跳出率", "label.breakdown": "故障", diff --git a/src/lang/zh-TW.json b/src/lang/zh-TW.json index 030d11dce..7b19cb25d 100644 --- a/src/lang/zh-TW.json +++ b/src/lang/zh-TW.json @@ -20,7 +20,6 @@ "label.average": "平均", "label.back": "返回", "label.before": "之前", - "label.behavior": "行為", "label.boards": "看板", "label.bounce-rate": "跳出率", "label.breakdown": "細項分析", diff --git a/src/lib/detect.ts b/src/lib/detect.ts index 68cb66726..f33012958 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -114,9 +114,9 @@ export async function getClientInfo(request: Request, payload: Record