mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Board setup.
This commit is contained in:
parent
f97c840825
commit
e08907d998
9 changed files with 141 additions and 23 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -31,6 +31,9 @@ pm2.yml
|
||||||
*.log
|
*.log
|
||||||
.vscode
|
.vscode
|
||||||
.tool-versions
|
.tool-versions
|
||||||
|
.claude
|
||||||
|
tmpclaude*
|
||||||
|
nul
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
|
|
||||||
92
CLAUDE.md
Normal file
92
CLAUDE.md
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Umami is a privacy-focused web analytics platform built with Next.js 15, React 19, and TypeScript. It serves as an alternative to Google Analytics, storing data in PostgreSQL (primary) with optional ClickHouse for time-series analytics.
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
npm run dev # Start dev server on port 3001 with Turbopack
|
||||||
|
npm run build # Full production build (includes db setup, tracker, geo data)
|
||||||
|
npm run start # Start production server
|
||||||
|
|
||||||
|
# Database
|
||||||
|
npm run build-db # Generate Prisma client
|
||||||
|
npm run update-db # Run Prisma migrations
|
||||||
|
npm run check-db # Verify database connection
|
||||||
|
npm run seed-data # Seed test data
|
||||||
|
|
||||||
|
# Code Quality
|
||||||
|
npm run lint # Lint with Biome
|
||||||
|
npm run format # Format with Biome
|
||||||
|
npm run check # Format and lint with Biome
|
||||||
|
npm run test # Run Jest tests
|
||||||
|
|
||||||
|
# Building specific parts
|
||||||
|
npm run build-tracker # Build client-side tracking script (Rollup)
|
||||||
|
npm run build-geo # Build geolocation database
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
- `src/app/` - Next.js App Router (routes and API endpoints)
|
||||||
|
- `(main)/` - Authenticated app routes (dashboard, websites, teams, boards, etc.)
|
||||||
|
- `(collect)/` - Data collection routes
|
||||||
|
- `api/` - REST API endpoints
|
||||||
|
- `src/components/` - React components (charts, forms, common UI, hooks)
|
||||||
|
- `src/lib/` - Core utilities (auth, crypto, date, prisma helpers, redis)
|
||||||
|
- `src/queries/` - Data access layer (Prisma queries and raw SQL)
|
||||||
|
- `src/store/` - Zustand state stores (app, dashboard, websites, cache)
|
||||||
|
- `src/tracker/` - Client-side tracking script (lightweight IIFE)
|
||||||
|
- `prisma/` - Database schema and migrations
|
||||||
|
|
||||||
|
### Key Patterns
|
||||||
|
|
||||||
|
**API Request Handling** - All API endpoints use Zod validation with `parseRequest`:
|
||||||
|
```typescript
|
||||||
|
const schema = z.object({ /* fields */ });
|
||||||
|
const { body, error } = await parseRequest(request, schema);
|
||||||
|
if (error) return error();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authentication** - JWT tokens via Bearer header, share tokens via `x-umami-share-token` header for public dashboards. Role-based access: admin, manager, user.
|
||||||
|
|
||||||
|
**Data Fetching** - React Query with 60s stale time, no retry, no refetch on window focus.
|
||||||
|
|
||||||
|
**State Management** - Zustand for client state, localStorage for user preferences.
|
||||||
|
|
||||||
|
**Styling** - CSS Modules with CSS variables for theming (light/dark mode).
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
- **ORM**: Prisma 7.x with PostgreSQL adapter
|
||||||
|
- **Schema**: `prisma/schema.prisma` - 14 models (User, Team, Website, Session, WebsiteEvent, EventData, etc.)
|
||||||
|
- **Query helpers**: `src/lib/prisma.ts` has dynamic SQL generation functions (`getDateSQL`, `mapFilter`, `getSearchSQL`)
|
||||||
|
- **Raw SQL**: Complex analytics queries use `{{param}}` template placeholders for safe binding
|
||||||
|
|
||||||
|
### Tracker Script
|
||||||
|
|
||||||
|
The tracking script in `src/tracker/index.js` is a standalone IIFE (~3-4KB) built with Rollup. It sends events to `/api/send`. Alternative script names can be configured via `TRACKER_SCRIPT_NAME` env var.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Key variables in `.env`:
|
||||||
|
```
|
||||||
|
DATABASE_URL # PostgreSQL connection string (required)
|
||||||
|
APP_SECRET # Encryption/signing secret
|
||||||
|
CLICKHOUSE_URL # Optional ClickHouse for analytics
|
||||||
|
REDIS_URL # Optional Redis for caching/sessions
|
||||||
|
BASE_PATH # App base path (e.g., /analytics)
|
||||||
|
DEBUG # Debug namespaces (e.g., umami:*)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Node.js 18.18+
|
||||||
|
- PostgreSQL 12.14+
|
||||||
|
- pnpm (package manager)
|
||||||
|
|
@ -6,7 +6,6 @@ generator client {
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
url = env("DATABASE_URL")
|
|
||||||
relationMode = "prisma"
|
relationMode = "prisma"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,5 @@ export function BoardProvider({ boardId, children }: { boardId: string; children
|
||||||
return <Loading placement="absolute" />;
|
return <Loading placement="absolute" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!board) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <BoardContext.Provider value={board}>{children}</BoardContext.Provider>;
|
return <BoardContext.Provider value={board}>{children}</BoardContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,48 @@
|
||||||
import { Button, Column, Grid, Heading, Row, TextField } from '@umami/react-zen';
|
import { Column, Grid, Heading, LoadingButton, Row, TextField, useToast } from '@umami/react-zen';
|
||||||
import { useMessages } from '@/components/hooks';
|
import { useState } from 'react';
|
||||||
|
import { useApi, useBoard, useMessages, useModified, useNavigation } from '@/components/hooks';
|
||||||
|
|
||||||
export function BoardHeader() {
|
export function BoardHeader() {
|
||||||
const { formatMessage, labels } = useMessages();
|
const board = useBoard();
|
||||||
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
|
const { post, useMutation } = useApi();
|
||||||
|
const { touch } = useModified();
|
||||||
|
const { router, renderUrl } = useNavigation();
|
||||||
|
const { toast } = useToast();
|
||||||
const defaultName = formatMessage(labels.untitled);
|
const defaultName = formatMessage(labels.untitled);
|
||||||
const name = '';
|
|
||||||
const description = '';
|
|
||||||
|
|
||||||
const handleNameChange = (name: string) => {
|
const [name, setName] = useState(board?.name ?? '');
|
||||||
//updateReport({ name: name || defaultName });
|
const [description, setDescription] = useState(board?.description ?? '');
|
||||||
|
|
||||||
|
const { mutateAsync, isPending } = useMutation({
|
||||||
|
mutationFn: (data: { name: string; description: string }) => {
|
||||||
|
if (board) {
|
||||||
|
return post(`/boards/${board.id}`, data);
|
||||||
|
}
|
||||||
|
return post('/boards', { ...data, type: 'dashboard', slug: '' });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleNameChange = (value: string) => {
|
||||||
|
setName(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDescriptionChange = (description: string) => {
|
const handleDescriptionChange = (value: string) => {
|
||||||
//updateReport({ description });
|
setDescription(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <h1>asdgfviybiyu8oaero8g9873qrgb875qh0g8</h1>;
|
const handleSave = async () => {
|
||||||
|
const result = await mutateAsync({ name: name || defaultName, description });
|
||||||
|
|
||||||
|
toast(formatMessage(messages.saved));
|
||||||
|
touch('boards');
|
||||||
|
|
||||||
|
if (board) {
|
||||||
|
touch(`board:${board.id}`);
|
||||||
|
} else if (result?.id) {
|
||||||
|
router.push(renderUrl(`/boards/${result.id}`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
|
|
@ -25,14 +52,12 @@ export function BoardHeader() {
|
||||||
border="bottom"
|
border="bottom"
|
||||||
gapX="6"
|
gapX="6"
|
||||||
>
|
>
|
||||||
asdfasdfds
|
|
||||||
<Column>
|
<Column>
|
||||||
<Row>
|
<Row>
|
||||||
<TextField
|
<TextField
|
||||||
variant="quiet"
|
variant="quiet"
|
||||||
name="name"
|
name="name"
|
||||||
value={name}
|
value={name}
|
||||||
defaultValue={name}
|
|
||||||
placeholder={defaultName}
|
placeholder={defaultName}
|
||||||
onChange={handleNameChange}
|
onChange={handleNameChange}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
|
@ -46,7 +71,6 @@ export function BoardHeader() {
|
||||||
variant="quiet"
|
variant="quiet"
|
||||||
name="description"
|
name="description"
|
||||||
value={description}
|
value={description}
|
||||||
defaultValue={description}
|
|
||||||
placeholder={`+ ${formatMessage(labels.addDescription)}`}
|
placeholder={`+ ${formatMessage(labels.addDescription)}`}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onChange={handleDescriptionChange}
|
onChange={handleDescriptionChange}
|
||||||
|
|
@ -57,7 +81,9 @@ export function BoardHeader() {
|
||||||
</Row>
|
</Row>
|
||||||
</Column>
|
</Column>
|
||||||
<Column justifyContent="center" alignItems="flex-end">
|
<Column justifyContent="center" alignItems="flex-end">
|
||||||
<Button variant="primary">{formatMessage(labels.save)}</Button>
|
<LoadingButton variant="primary" onPress={handleSave} isLoading={isPending}>
|
||||||
|
{formatMessage(labels.save)}
|
||||||
|
</LoadingButton>
|
||||||
</Column>
|
</Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { Column } from '@umami/react-zen';
|
import { Column } from '@umami/react-zen';
|
||||||
|
import { BoardBody } from '@/app/(main)/boards/[boardId]/BoardBody';
|
||||||
import { BoardHeader } from '@/app/(main)/boards/[boardId]/BoardHeader';
|
import { BoardHeader } from '@/app/(main)/boards/[boardId]/BoardHeader';
|
||||||
import { BoardProvider } from '@/app/(main)/boards/BoardProvider';
|
import { BoardProvider } from '@/app/(main)/boards/BoardProvider';
|
||||||
import { PageBody } from '@/components/common/PageBody';
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
|
|
@ -10,6 +11,7 @@ export function BoardPage({ boardId }: { boardId: string }) {
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<Column>
|
<Column>
|
||||||
<BoardHeader />
|
<BoardHeader />
|
||||||
|
<BoardBody />
|
||||||
</Column>
|
</Column>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</BoardProvider>
|
</BoardProvider>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { BoardPage } from './BoardPage';
|
||||||
export default async function ({ params }: { params: Promise<{ boardId: string }> }) {
|
export default async function ({ params }: { params: Promise<{ boardId: string }> }) {
|
||||||
const { boardId } = await params;
|
const { boardId } = await params;
|
||||||
|
|
||||||
return <BoardPage boardId={boardId} />;
|
return <BoardPage boardId={boardId !== 'create' ? boardId : undefined} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ boar
|
||||||
export async function POST(request: Request, { params }: { params: Promise<{ boardId: string }> }) {
|
export async function POST(request: Request, { params }: { params: Promise<{ boardId: string }> }) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
domain: z.string().optional(),
|
description: z.string().optional(),
|
||||||
shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(),
|
shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -37,14 +37,14 @@ export async function POST(request: Request, { params }: { params: Promise<{ boa
|
||||||
}
|
}
|
||||||
|
|
||||||
const { boardId } = await params;
|
const { boardId } = await params;
|
||||||
const { name, domain, shareId } = body;
|
const { name, description, shareId } = body;
|
||||||
|
|
||||||
if (!(await canUpdateBoard(auth, boardId))) {
|
if (!(await canUpdateBoard(auth, boardId))) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const board = await updateBoard(boardId, { name, domain, shareId });
|
const board = await updateBoard(boardId, { name, description, shareId });
|
||||||
|
|
||||||
return Response.json(board);
|
return Response.json(board);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue