From d7fd22645c333c827cd01cd9d968ee2a18ed8d2c Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 28 Nov 2025 00:33:53 -0800 Subject: [PATCH 1/4] Fixed nav menus. --- pnpm-lock.yaml | 48 ++++++++++++++++++- src/app/(main)/admin/AdminLayout.tsx | 1 + .../admin/users/[userId]/UserEditForm.tsx | 2 +- src/app/(main)/settings/SettingsLayout.tsx | 1 + src/app/(main)/settings/layout.tsx | 2 +- src/components/common/SideMenu.tsx | 2 +- .../hooks/queries/useUpdateQuery.ts | 3 +- src/components/hooks/useMessages.ts | 5 +- src/lib/types.ts | 5 ++ 9 files changed, 62 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5db0535d..baa6fa61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -328,7 +328,44 @@ importers: specifier: ^5.9.3 version: 5.9.3 - dist: {} + dist: + dependencies: + chart.js: + specifier: ^4.5.0 + version: 4.5.1 + chartjs-adapter-date-fns: + specifier: ^3.0.0 + version: 3.0.0(chart.js@4.5.1)(date-fns@2.30.0) + colord: + specifier: ^2.9.2 + version: 2.9.3 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + lucide-react: + specifier: ^0.542.0 + version: 0.542.0(react@19.2.0) + pure-rand: + specifier: ^7.0.1 + version: 7.0.1 + react-simple-maps: + specifier: ^2.3.0 + version: 2.3.0(prop-types@15.8.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-use-measure: + specifier: ^2.0.4 + version: 2.1.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-window: + specifier: ^1.8.6 + version: 1.8.11(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + serialize-error: + specifier: ^12.0.0 + version: 12.0.0 + thenby: + specifier: ^1.3.4 + version: 1.3.4 + uuid: + specifier: ^11.1.0 + version: 11.1.0 packages: @@ -5152,6 +5189,11 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lucide-react@0.542.0: + resolution: {integrity: sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lucide-react@0.543.0: resolution: {integrity: sha512-fpVfuOQO0V3HBaOA1stIiP/A2fPCXHIleRZL16Mx3HmjTYwNSbimhnFBygs2CAfU1geexMX5ItUcWBGUaqw5CA==} peerDependencies: @@ -12921,6 +12963,10 @@ snapshots: dependencies: react: 19.2.0 + lucide-react@0.542.0(react@19.2.0): + dependencies: + react: 19.2.0 + lucide-react@0.543.0(react@19.2.0): dependencies: react: 19.2.0 diff --git a/src/app/(main)/admin/AdminLayout.tsx b/src/app/(main)/admin/AdminLayout.tsx index 56561a8e..3c8fa20a 100644 --- a/src/app/(main)/admin/AdminLayout.tsx +++ b/src/app/(main)/admin/AdminLayout.tsx @@ -21,6 +21,7 @@ export function AdminLayout({ children }: { children: ReactNode }) { border="right" backgroundColor marginRight="2" + padding="3" > diff --git a/src/app/(main)/admin/users/[userId]/UserEditForm.tsx b/src/app/(main)/admin/users/[userId]/UserEditForm.tsx index 30c86862..28bf030f 100644 --- a/src/app/(main)/admin/users/[userId]/UserEditForm.tsx +++ b/src/app/(main)/admin/users/[userId]/UserEditForm.tsx @@ -50,7 +50,7 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () = label={formatMessage(labels.role)} rules={{ required: formatMessage(labels.required) }} > - {formatMessage(labels.viewOnly)} diff --git a/src/app/(main)/settings/SettingsLayout.tsx b/src/app/(main)/settings/SettingsLayout.tsx index adfaaa4f..f6588721 100644 --- a/src/app/(main)/settings/SettingsLayout.tsx +++ b/src/app/(main)/settings/SettingsLayout.tsx @@ -14,6 +14,7 @@ export function SettingsLayout({ children }: { children: ReactNode }) { border="right" backgroundColor marginRight="2" + padding="3" > diff --git a/src/app/(main)/settings/layout.tsx b/src/app/(main)/settings/layout.tsx index e8dfb30d..4e773a37 100644 --- a/src/app/(main)/settings/layout.tsx +++ b/src/app/(main)/settings/layout.tsx @@ -3,7 +3,7 @@ import { SettingsLayout } from './SettingsLayout'; export default function ({ children }) { if (process.env.cloudMode) { - //return null; + return null; } return {children}; diff --git a/src/components/common/SideMenu.tsx b/src/components/common/SideMenu.tsx index bdd24952..92ff798a 100644 --- a/src/components/common/SideMenu.tsx +++ b/src/components/common/SideMenu.tsx @@ -51,7 +51,7 @@ export function SideMenu({ }; return ( - + {title && ( {title} diff --git a/src/components/hooks/queries/useUpdateQuery.ts b/src/components/hooks/queries/useUpdateQuery.ts index 9535c436..85a94425 100644 --- a/src/components/hooks/queries/useUpdateQuery.ts +++ b/src/components/hooks/queries/useUpdateQuery.ts @@ -1,10 +1,11 @@ import { useToast } from '@umami/react-zen'; +import type { ApiError } from '@/lib/types'; import { useApi } from '../useApi'; import { useModified } from '../useModified'; export function useUpdateQuery(path: string, params?: Record) { const { post, useMutation } = useApi(); - const query = useMutation({ + const query = useMutation>({ mutationFn: (data: Record) => post(path, { ...data, ...params }), }); const { touch } = useModified(); diff --git a/src/components/hooks/useMessages.ts b/src/components/hooks/useMessages.ts index 19f12d9a..d5bc2423 100644 --- a/src/components/hooks/useMessages.ts +++ b/src/components/hooks/useMessages.ts @@ -1,5 +1,6 @@ import { FormattedMessage, type MessageDescriptor, useIntl } from 'react-intl'; import { labels, messages } from '@/components/messages'; +import type { ApiError } from '@/lib/types'; type FormatMessage = ( descriptor: MessageDescriptor, @@ -12,7 +13,7 @@ interface UseMessages { messages: typeof messages; labels: typeof labels; getMessage: (id: string) => string; - getErrorMessage: (error: unknown) => string | undefined; + getErrorMessage: (error: ApiError) => string | undefined; FormattedMessage: typeof FormattedMessage; } @@ -25,7 +26,7 @@ export function useMessages(): UseMessages { return message ? formatMessage(message) : id; }; - const getErrorMessage = (error: unknown) => { + const getErrorMessage = (error: ApiError) => { if (!error) { return undefined; } diff --git a/src/lib/types.ts b/src/lib/types.ts index e727c87a..9c061979 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -136,3 +136,8 @@ export interface RealtimeData { urls: Record; visitors: any[]; } + +export interface ApiError extends Error { + code?: string; + message: string; +} From c427c6f54762ffb5a4fe92e495abf371ca254498 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 3 Dec 2025 17:05:14 -0800 Subject: [PATCH 2/4] Fixed replica logic. --- src/lib/prisma.ts | 60 ++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 94970584..06336d3b 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -206,6 +206,10 @@ async function rawQuery(sql: string, data: Record, name?: string): return `$${params.length}${type ?? ''}`; }); + if (process.env.DATABASE_REPLICA_URL && '$replica' in client) { + return client.$replica().$queryRawUnsafe(query, ...params); + } + return client.$queryRawUnsafe(query, ...params); } @@ -296,10 +300,6 @@ function getSchema() { } function getClient() { - if (!process.env.DATABASE_URL) { - return null; - } - const url = process.env.DATABASE_URL; const replicaUrl = process.env.DATABASE_REPLICA_URL; const logQuery = process.env.LOG_QUERY; @@ -307,43 +307,49 @@ function getClient() { const connectionUrl = new URL(url); const schema = connectionUrl.searchParams.get('schema') ?? undefined; - const adapter = new PrismaPg({ connectionString: url.toString() }, { schema }); + const baseAdapter = new PrismaPg({ connectionString: url }, { schema }); - const prisma = new PrismaClient({ - adapter, + const baseClient = new PrismaClient({ + adapter: baseAdapter, errorFormat: 'pretty', ...(logQuery ? PRISMA_LOG_OPTIONS : {}), }); - if (replicaUrl) { - const replicaAdapter = new PrismaPg({ connectionString: replicaUrl.toString() }, { schema }); - - const replicaClient = new PrismaClient({ - adapter: replicaAdapter, - ...(logQuery ? PRISMA_LOG_OPTIONS : {}), - }); - - prisma.$extends( - readReplicas({ - replicas: [replicaClient], - }), - ); + if (logQuery) { + baseClient.$on('query', log); } + if (!replicaUrl) { + log('Prisma initialized'); + globalThis[PRISMA] ??= baseClient; + return baseClient; + } + + const replicaAdapter = new PrismaPg({ connectionString: replicaUrl }, { schema }); + + const replicaClient = new PrismaClient({ + adapter: replicaAdapter, + errorFormat: 'pretty', + ...(logQuery ? PRISMA_LOG_OPTIONS : {}), + }); + if (logQuery) { - prisma.$on('query' as never, log); + replicaClient.$on('query', log); } - log('Prisma initialized'); + const extended = baseClient.$extends( + readReplicas({ + replicas: [replicaClient], + }), + ); - if (!globalThis[PRISMA]) { - globalThis[PRISMA] = prisma; - } + log('Prisma initialized (with replica)'); + globalThis[PRISMA] ??= extended; - return prisma; + return extended; } -const client: PrismaClient = globalThis[PRISMA] || getClient(); +const client = (globalThis[PRISMA] || getClient()) as ReturnType; export default { client, From 2993db14f09754a41aaec953bf7a93cd7ac3cb52 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 3 Dec 2025 19:37:51 -0800 Subject: [PATCH 3/4] Updated README. --- README.md | 29 ++++++++++++++--------------- pnpm-lock.yaml | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index dcc6865f..95bb4330 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ A detailed getting started guide can be found at [umami.is/docs](https://umami.i ### Requirements -- A server with Node.js version 18.18 or newer -- A database. Umami supports [PostgreSQL](https://www.postgresql.org/) (minimum v12.14) databases. +- A server with Node.js version 18.18+. +- A PostgreSQL database version v12.14+. -### Get the Source Code and Install Packages +### Get the source code and install packages ```bash git clone https://github.com/umami-software/umami.git @@ -58,7 +58,7 @@ postgresql://username:mypassword@localhost:5432/mydb pnpm run build ``` -_The build step will create tables in your database if you are installing for the first time. It will also create a login user with username **admin** and password **umami**._ +The build step will create tables in your database if you are installing for the first time. It will also create a login user with username **admin** and password **umami**. ### Start the Application @@ -66,37 +66,36 @@ _The build step will create tables in your database if you are installing for th pnpm run start ``` -_By default, this will launch the application on `http://localhost:3000`. You will need to either [proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) requests from your web server or change the [port](https://nextjs.org/docs/api-reference/cli#production) to serve the application directly._ +By default, this will launch the application on `http://localhost:3000`. You will need to either [proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) requests from your web server or change the [port](https://nextjs.org/docs/api-reference/cli#production) to serve the application directly. --- ## 🐳 Installing with Docker -To build the Umami container and start up a Postgres database, run: +Umami provides Docker images as well as a Docker compose file for easy deployment. -```bash -docker compose up -d -``` - -Alternatively, to pull just the Umami Docker image with PostgreSQL support: +Docker image: ```bash docker pull docker.umami.is/umami-software/umami:latest ``` +Docker compose to run Umami with a Postgres database, run: + +```bash +docker compose up -d +``` + --- ## 🔄 Getting Updates -> [!WARNING] -> If you are updating from Umami V2, image "postgresql-latest" is deprecated. You must change it to "latest". -> e.g., rename `docker.umami.is/umami-software/umami:postgresql-latest` to `docker.umami.is/umami-software/umami:latest`. To get the latest features, simply do a pull, install any new dependencies, and rebuild: ```bash git pull pnpm install -pnpm run build +pnpm build ``` To update the Docker image, simply pull the new images and rebuild: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10eed821..ef3eb1fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -331,7 +331,44 @@ importers: specifier: ^5.9.3 version: 5.9.3 - dist: {} + dist: + dependencies: + chart.js: + specifier: ^4.5.0 + version: 4.5.1 + chartjs-adapter-date-fns: + specifier: ^3.0.0 + version: 3.0.0(chart.js@4.5.1)(date-fns@2.30.0) + colord: + specifier: ^2.9.2 + version: 2.9.3 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + lucide-react: + specifier: ^0.542.0 + version: 0.542.0(react@19.2.1) + pure-rand: + specifier: ^7.0.1 + version: 7.0.1 + react-simple-maps: + specifier: ^2.3.0 + version: 2.3.0(prop-types@15.8.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + react-use-measure: + specifier: ^2.0.4 + version: 2.1.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + react-window: + specifier: ^1.8.6 + version: 1.8.11(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + serialize-error: + specifier: ^12.0.0 + version: 12.0.0 + thenby: + specifier: ^1.3.4 + version: 1.3.4 + uuid: + specifier: ^11.1.0 + version: 11.1.0 packages: @@ -5058,6 +5095,11 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lucide-react@0.542.0: + resolution: {integrity: sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lucide-react@0.543.0: resolution: {integrity: sha512-fpVfuOQO0V3HBaOA1stIiP/A2fPCXHIleRZL16Mx3HmjTYwNSbimhnFBygs2CAfU1geexMX5ItUcWBGUaqw5CA==} peerDependencies: @@ -12696,6 +12738,10 @@ snapshots: dependencies: yallist: 4.0.0 + lucide-react@0.542.0(react@19.2.1): + dependencies: + react: 19.2.1 + lucide-react@0.543.0(react@19.2.1): dependencies: react: 19.2.1 From 33e927ed1fe04a9b2c8253b5f57f0b64f6bdcb48 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 3 Dec 2025 23:01:22 -0800 Subject: [PATCH 4/4] Bump version 3.0.2. --- biome.json | 8 ++++++++ package.json | 2 +- pnpm-lock.yaml | 48 +---------------------------------------------- src/lib/prisma.ts | 4 +--- 4 files changed, 11 insertions(+), 51 deletions(-) diff --git a/biome.json b/biome.json index 0dec793b..61d094ca 100644 --- a/biome.json +++ b/biome.json @@ -46,6 +46,14 @@ "arrowParentheses": "asNeeded" } }, + "css": { + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf" + } + }, "assist": { "enabled": true, "actions": { diff --git a/package.json b/package.json index e20ae62d..09a69135 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "3.0.1", + "version": "3.0.2", "description": "A modern, privacy-focused alternative to Google Analytics.", "author": "Umami Software, Inc. ", "license": "MIT", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef3eb1fa..10eed821 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -331,44 +331,7 @@ importers: specifier: ^5.9.3 version: 5.9.3 - dist: - dependencies: - chart.js: - specifier: ^4.5.0 - version: 4.5.1 - chartjs-adapter-date-fns: - specifier: ^3.0.0 - version: 3.0.0(chart.js@4.5.1)(date-fns@2.30.0) - colord: - specifier: ^2.9.2 - version: 2.9.3 - jsonwebtoken: - specifier: ^9.0.2 - version: 9.0.2 - lucide-react: - specifier: ^0.542.0 - version: 0.542.0(react@19.2.1) - pure-rand: - specifier: ^7.0.1 - version: 7.0.1 - react-simple-maps: - specifier: ^2.3.0 - version: 2.3.0(prop-types@15.8.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) - react-use-measure: - specifier: ^2.0.4 - version: 2.1.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1) - react-window: - specifier: ^1.8.6 - version: 1.8.11(react-dom@19.2.1(react@19.2.1))(react@19.2.1) - serialize-error: - specifier: ^12.0.0 - version: 12.0.0 - thenby: - specifier: ^1.3.4 - version: 1.3.4 - uuid: - specifier: ^11.1.0 - version: 11.1.0 + dist: {} packages: @@ -5095,11 +5058,6 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - lucide-react@0.542.0: - resolution: {integrity: sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==} - peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - lucide-react@0.543.0: resolution: {integrity: sha512-fpVfuOQO0V3HBaOA1stIiP/A2fPCXHIleRZL16Mx3HmjTYwNSbimhnFBygs2CAfU1geexMX5ItUcWBGUaqw5CA==} peerDependencies: @@ -12738,10 +12696,6 @@ snapshots: dependencies: yallist: 4.0.0 - lucide-react@0.542.0(react@19.2.1): - dependencies: - react: 19.2.1 - lucide-react@0.543.0(react@19.2.1): dependencies: react: 19.2.1 diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 06336d3b..64cb870f 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -303,9 +303,7 @@ function getClient() { const url = process.env.DATABASE_URL; const replicaUrl = process.env.DATABASE_REPLICA_URL; const logQuery = process.env.LOG_QUERY; - - const connectionUrl = new URL(url); - const schema = connectionUrl.searchParams.get('schema') ?? undefined; + const schema = getSchema(); const baseAdapter = new PrismaPg({ connectionString: url }, { schema });