From 6d4213a31c4aee92e3eac5c8ee484d7089cfaddd Mon Sep 17 00:00:00 2001 From: "Dennis (via Claude+Gemma)" Date: Sat, 23 May 2026 05:12:48 +0200 Subject: [PATCH] =?UTF-8?q?feat(admin-user-management):=20Admin-only=20CRU?= =?UTF-8?q?D=20/api/users=20+=20Settings-Page=20f=C3=BCr=20User-Verwaltu?= =?UTF-8?q?=20[tsc:ok]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .phase4-state.json | 5 + GENERATION_LOG.md | 16 ++ apps/api/src/routes/users.ts | 84 +++++- apps/web/package.json | 3 +- apps/web/src/pages/AdminUsers.tsx | 244 ++++++++++++++++ pnpm-lock.yaml | 271 +++++++++++++++++- .../phase3_features.cpython-312.pyc | Bin 0 -> 12973 bytes scripts/phase4_features.py | 208 ++++++++++++++ 8 files changed, 826 insertions(+), 5 deletions(-) create mode 100644 .phase4-state.json create mode 100644 apps/web/src/pages/AdminUsers.tsx create mode 100644 scripts/__pycache__/phase3_features.cpython-312.pyc create mode 100644 scripts/phase4_features.py diff --git a/.phase4-state.json b/.phase4-state.json new file mode 100644 index 0000000..e27e6fc --- /dev/null +++ b/.phase4-state.json @@ -0,0 +1,5 @@ +{ + "completed_features": [], + "current_feature": "admin-user-management", + "started_at": "2026-05-23T05:10:51.482879" +} \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index a774827..844b4a5 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -455,3 +455,19 @@ undefined ## Phase-3 Run beendet (2026-05-23 05:01:13) - `05:01:13` **INFO** OK: 4, Attempted: 3, Total: 7 + +## 🚀 Phase-4 Codegen-Run gestartet (2026-05-23 05:10:47) + +- `05:10:47` **INFO** Features: 6 +- `05:10:47` **INFO** Ensuring recharts dep… +- `05:10:51` **INFO** recharts install rc=0 + +## Phase-3 Feature: admin-user-management (2026-05-23 05:10:51) + +- `05:10:51` **INFO** Description: Admin-only CRUD /api/users + Settings-Page für User-Verwaltung +- `05:10:51` **INFO** Generating apps/api/src/routes/users.ts (ERWEITERTE users.ts — behalte GET /me + PATCH /me. Füge hinzu (alle nu…) +- `05:11:24` **INFO** wrote 3491 chars in 33.4s (attempt 1) +- `05:11:24` **INFO** Generating apps/web/src/pages/AdminUsers.tsx (Admin-User-Management Page. Liste aller User (TanStack Query). Inline-…) +- `05:12:47` **INFO** wrote 9929 chars in 82.5s (attempt 1) +- `05:12:47` **INFO** Running tsc --noEmit on api… +- `05:12:48` **INFO** tsc clean ✓ diff --git a/apps/api/src/routes/users.ts b/apps/api/src/routes/users.ts index 7e261e7..ffa46d7 100644 --- a/apps/api/src/routes/users.ts +++ b/apps/api/src/routes/users.ts @@ -3,9 +3,18 @@ import { db } from "../db" import { users } from "../db/schema" import { eq } from "drizzle-orm" import { z } from "zod" +import argon2 from "argon2" const UserUpdateSchema = z.object({ - name: z.string().min(1).optional() + name: z.string().min(1).optional(), + role: z.string().optional() +}) + +const UserCreateSchema = z.object({ + email: z.string().email(), + name: z.string().min(1), + role: z.string(), + password: z.string().min(8) }) export default async function userRoutes(fastify: FastifyInstance) { @@ -17,6 +26,10 @@ export default async function userRoutes(fastify: FastifyInstance) { } }) + const isAdmin = (request: any) => { + return (request.user as { sub: string, role: string })?.role === "admin" + } + fastify.get("/me", async (request, reply) => { const userId = (request.user as { sub: string } | undefined)?.sub @@ -57,4 +70,73 @@ export default async function userRoutes(fastify: FastifyInstance) { return user }) + + fastify.get("/", async (request, reply) => { + if (!isAdmin(request)) { + return reply.code(403).send({ message: "Forbidden: Admin role required" }) + } + + return await db.select().from(users) + }) + + fastify.post("/", async (request, reply) => { + if (!isAdmin(request)) { + return reply.code(403).send({ message: "Forbidden: Admin role required" }) + } + + const body = UserCreateSchema.parse(request.body) + const passwordHash = await argon2.hash(body.password) + + const [user] = await db + .insert(users) + .values({ + email: body.email, + name: body.name, + role: body.role, + passwordHash + }) + .returning() + + return reply.code(201).send(user) + }) + + fastify.patch("/:id", async (request, reply) => { + if (!isAdmin(request)) { + return reply.code(403).send({ message: "Forbidden: Admin role required" }) + } + + const { id } = request.params as { id: string } + const body = UserUpdateSchema.parse(request.body) + + const [user] = await db + .update(users) + .set(body) + .where(eq(users.id, id)) + .returning() + + if (!user) { + return reply.code(404).send({ message: "User not found" }) + } + + return user + }) + + fastify.delete("/:id", async (request, reply) => { + if (!isAdmin(request)) { + return reply.code(403).send({ message: "Forbidden: Admin role required" }) + } + + const { id } = request.params as { id: string } + + const result = await db + .delete(users) + .where(eq(users.id, id)) + .returning() + + if (result.length === 0) { + return reply.code(404).send({ message: "User not found" }) + } + + return reply.code(204).send() + }) } \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 64bdde3..df3025e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,7 +14,8 @@ "@tanstack/react-router": "^1.62.7", "lucide-react": "^1.16.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "recharts": "^3.8.1" }, "devDependencies": { "@types/react": "^18.3.10", diff --git a/apps/web/src/pages/AdminUsers.tsx b/apps/web/src/pages/AdminUsers.tsx new file mode 100644 index 0000000..354e787 --- /dev/null +++ b/apps/web/src/pages/AdminUsers.tsx @@ -0,0 +1,244 @@ +import { useState } from "react" +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { api } from "../lib/api" +import type { User } from "@emberclone/shared" + +export default function AdminUsers() { + const queryClient = useQueryClient() + const [me, setMe] = useState<{ role: string } | null>(null) + const [isMeLoading, setIsMeLoading] = useState(true) + + const [newUser, setNewUser] = useState({ email: "", name: "", role: "user", password: "" }) + const [editingUser, setEditingUser] = useState(null) + + // Check if current user is admin + const { data: currentUser } = useQuery({ + queryKey: ["me"], + queryFn: () => api.getMe(), + onSuccess: (data) => { + setMe(data) + setIsMeLoading(false) + } + }) + + const { data: users, isLoading, isError } = useQuery({ + queryKey: ["users"], + queryFn: () => api.listUsers(), + enabled: !!currentUser && currentUser.role === "admin" + }) + + const createMutation = useMutation({ + mutationFn: (data: any) => api.createUser(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }) + setNewUser({ email: "", name: "", role: "user", password: "" }) + } + }) + + const updateMutation = useMutation({ + mutationFn: ({ id, data }: { id: string; data: any }) => api.updateUser(id, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }) + setEditingUser(null) + } + }) + + const deleteMutation = useMutation({ + mutationFn: (id: string) => api.deleteUser(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }) + } + }) + + if (isMeLoading || !currentUser) { + return
Loading permissions...
+ } + + if (currentUser.role !== "admin") { + return ( +
+
+

Forbidden

+

You do not have administrative privileges to access this page.

+
+
+ ) + } + + const handleCreateSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (!newUser.email || !newUser.password) return + createMutation.mutate(newUser) + } + + const handleUpdateSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (!editingUser) return + updateMutation.mutate({ + id: editingUser.id, + data: { name: editingUser.name, role: editingUser.role } + }) + } + + if (isLoading) return
Loading users...
+ if (isError) return
Error loading users.
+ + return ( +
+
+

User Management

+

Manage system users and access levels

+
+ +
+

Create New User

+
+
+ + setNewUser({ ...newUser, email: e.target.value })} + /> +
+
+ + setNewUser({ ...newUser, name: e.target.value })} + /> +
+
+ + +
+
+ +
+ setNewUser({ ...newUser, password: e.target.value })} + /> + +
+
+
+
+ +
+ + + + + + + + + + {users && users.map((user: User) => ( + + + + + + ))} + +
UserRoleActions
+
{user.name}
+
{user.email}
+
+ + {user.role} + + + + +
+
+ + {editingUser && ( +
+
+

Edit User

+
+
+ + setEditingUser({ ...editingUser, name: e.target.value })} + /> +
+
+ + +
+
+ + +
+
+
+
+ )} +
+ ) +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98396dd..5f0f73e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,6 +81,9 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + recharts: + specifier: ^3.8.1 + version: 3.8.1(@types/react@18.3.29)(react-dom@18.3.1)(react-is@19.2.6)(react@18.3.1)(redux@5.0.1) devDependencies: '@types/react': specifier: ^18.3.10 @@ -1362,6 +1365,27 @@ packages: resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} dev: false + /@reduxjs/toolkit@2.12.0(react-redux@9.3.0)(react@18.3.1): + resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.8 + react: 18.3.1 + react-redux: 9.3.0(@types/react@18.3.29)(react@18.3.1)(redux@5.0.1) + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + dev: false + /@rolldown/pluginutils@1.0.0-beta.27: resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} dev: true @@ -1566,6 +1590,14 @@ packages: dev: true optional: true + /@standard-schema/spec@1.1.0: + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + dev: false + + /@standard-schema/utils@0.3.0: + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + dev: false + /@tanstack/history@1.162.0: resolution: {integrity: sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==} engines: {node: '>=20.19'} @@ -1654,6 +1686,48 @@ packages: '@babel/types': 7.29.0 dev: true + /@types/d3-array@3.2.2: + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + dev: false + + /@types/d3-color@3.1.3: + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + dev: false + + /@types/d3-ease@3.0.2: + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + dev: false + + /@types/d3-interpolate@3.0.4: + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + dependencies: + '@types/d3-color': 3.1.3 + dev: false + + /@types/d3-path@3.1.1: + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + dev: false + + /@types/d3-scale@4.0.9: + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + dependencies: + '@types/d3-time': 3.0.4 + dev: false + + /@types/d3-shape@3.1.8: + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + dependencies: + '@types/d3-path': 3.1.1 + dev: false + + /@types/d3-time@3.0.4: + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + dev: false + + /@types/d3-timer@3.0.2: + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + dev: false + /@types/estree@1.0.8: resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} dev: true @@ -1672,7 +1746,6 @@ packages: /@types/prop-types@15.7.15: resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - dev: true /@types/react-dom@18.3.7(@types/react@18.3.29): resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} @@ -1687,7 +1760,10 @@ packages: dependencies: '@types/prop-types': 15.7.15 csstype: 3.2.3 - dev: true + + /@types/use-sync-external-store@0.0.6: + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + dev: false /@vitejs/plugin-react@4.7.0(vite@5.4.21): resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} @@ -1894,6 +1970,11 @@ packages: wrap-ansi: 7.0.0 dev: true + /clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + dev: false + /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1949,7 +2030,77 @@ packages: /csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - dev: true + + /d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + dependencies: + internmap: 2.0.3 + dev: false + + /d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + dev: false + + /d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + dev: false + + /d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + dev: false + + /d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + dev: false + + /d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + dev: false + + /d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + dev: false + + /d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: false + + /d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + dependencies: + d3-time: 3.1.0 + dev: false + + /d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: false + + /d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + dev: false /debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -1963,6 +2114,10 @@ packages: ms: 2.1.3 dev: true + /decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + dev: false + /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true @@ -2098,6 +2253,10 @@ packages: engines: {node: '>= 0.4'} dev: true + /es-toolkit@1.46.1: + resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} + dev: false + /esbuild-register@3.6.0(esbuild@0.19.12): resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -2240,6 +2399,10 @@ packages: engines: {node: '>=6'} dev: true + /eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + dev: false + /fast-content-type-parse@1.1.0: resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} dev: false @@ -2441,10 +2604,23 @@ packages: function-bind: 1.1.2 dev: true + /immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + dev: false + + /immer@11.1.8: + resolution: {integrity: sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==} + dev: false + /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} dev: false + /internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + dev: false + /ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -2887,6 +3063,29 @@ packages: scheduler: 0.23.2 dev: false + /react-is@19.2.6: + resolution: {integrity: sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==} + dev: false + + /react-redux@9.3.0(@types/react@18.3.29)(react@18.3.1)(redux@5.0.1): + resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + dependencies: + '@types/react': 18.3.29 + '@types/use-sync-external-store': 0.0.6 + react: 18.3.1 + redux: 5.0.1 + use-sync-external-store: 1.6.0(react@18.3.1) + dev: false + /react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -2917,6 +3116,45 @@ packages: engines: {node: '>= 12.13.0'} dev: false + /recharts@3.8.1(@types/react@18.3.29)(react-dom@18.3.1)(react-is@19.2.6)(react@18.3.1)(redux@5.0.1): + resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + dependencies: + '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0)(react@18.3.1) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.46.1 + eventemitter3: 5.0.4 + immer: 10.2.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 19.2.6 + react-redux: 9.3.0(@types/react@18.3.29)(react@18.3.1)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@18.3.1) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + dev: false + + /redux-thunk@3.1.0(redux@5.0.1): + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + dependencies: + redux: 5.0.1 + dev: false + + /redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + dev: false + /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -2927,6 +3165,10 @@ packages: engines: {node: '>=0.10.0'} dev: false + /reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + dev: false + /resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} dev: true @@ -3203,6 +3445,10 @@ packages: real-require: 0.2.0 dev: false + /tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + dev: false + /tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} @@ -3278,6 +3524,25 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true + /victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + dev: false + /vite@5.4.21: resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} engines: {node: ^18.0.0 || >=20.0.0} diff --git a/scripts/__pycache__/phase3_features.cpython-312.pyc b/scripts/__pycache__/phase3_features.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e42aa168eff6ba0030fae66f3c40c5dcf0eefe8 GIT binary patch literal 12973 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 N*qXO;LJO1C{|mNC&tL!m literal 0 HcmV?d00001 diff --git a/scripts/phase4_features.py b/scripts/phase4_features.py new file mode 100644 index 0000000..785ee8e --- /dev/null +++ b/scripts/phase4_features.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +"""Phase-4: admin-page, CSV export, mobile-fix, error-boundary, dashboard-charts.""" + +from __future__ import annotations + +import asyncio +import datetime +import json +import sys +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from phase2_features import ( # noqa: E402 + Feature, FileGen, ROOT, log, log_section, git, gemma, + strip_codefence, load_existing, tsc_check, MAX_RETRIES, +) +from phase3_features import HARDENING_HINTS, generate_with_hints, run_feature_v2 # noqa: E402 + + +PHASE4_STATE = ROOT / ".phase4-state.json" + + +FEATURES: list[Feature] = [ + Feature( + name="admin-user-management", + description="Admin-only CRUD /api/users + Settings-Page für User-Verwaltung", + files=[ + FileGen( + path="apps/api/src/routes/users.ts", + purpose=( + "ERWEITERTE users.ts — behalte GET /me + PATCH /me. " + "Füge hinzu (alle nur für role='admin'): " + "GET / (list all users), POST / (create with email+name+role+password — argon2-hash), " + "PATCH /:id (update name, role), DELETE /:id (cascade time-entries). " + "Verwende `request.user as { sub: string, role: string }` für payload-access. " + "403 wenn role !== 'admin'." + ), + refs=["apps/api/src/routes/users.ts", "apps/api/src/db/schema.ts"], + ), + FileGen( + path="apps/web/src/pages/AdminUsers.tsx", + purpose=( + "Admin-User-Management Page. Liste aller User (TanStack Query). " + "Inline-Form zum Anlegen (email, name, role-select, password). " + "Edit-Button pro Row → Modal mit name/role-Update. Delete-Button mit confirm. " + "Verwende api.listUsers(), api.createUser(), api.updateUser(), api.deleteUser(). " + "Wenn current user role !== 'admin': zeige Forbidden-Message statt Inhalt." + ), + refs=["apps/web/src/pages/Customers.tsx", "apps/web/src/lib/api.ts"], + ), + ], + ), + Feature( + name="csv-export-time-entries", + description="CSV-Export-Endpoint + Button in TimeEntries-Page", + files=[ + FileGen( + path="apps/api/src/routes/time-entries.ts", + purpose=( + "ERWEITERTE time-entries.ts — behalte alle bestehenden Routes (CRUD + running/start/stop). " + "Füge hinzu: GET /export.csv?from=...&to=... → returnt text/csv content mit Spalten " + "id,description,projectId,startTime,endTime,durationMinutes. " + "Header content-type: text/csv, content-disposition: attachment; filename=time-entries.csv. " + "Auth erforderlich, user sieht nur eigene (außer admin)." + ), + refs=["apps/api/src/routes/time-entries.ts"], + ), + FileGen( + path="apps/web/src/pages/TimeEntries.tsx", + purpose=( + "ERWEITERT — behalte Form + Filter + Liste. Füge Export-Button im Filter-Bar: " + "ruft `window.location.href = \\'/api/time-entries/export.csv?from=...&to=...\\'` " + "mit aktuellen Filter-Werten. Stil: outline-Button rechts neben To-Date." + ), + refs=["apps/web/src/pages/TimeEntries.tsx"], + ), + ], + ), + Feature( + name="error-boundary", + description="React ErrorBoundary + global wrapping in App.tsx", + files=[ + FileGen( + path="apps/web/src/components/ErrorBoundary.tsx", + purpose=( + "React-ErrorBoundary class-component. Fängt unkaufgefangene Render-Errors. " + "Zeigt schöne Fehlerseite mit message + reload-button. " + "Verwende componentDidCatch + getDerivedStateFromError. " + "Tailwind, centered, max-w-md." + ), + ), + FileGen( + path="apps/web/src/App.tsx", + purpose=( + "ERWEITERT — wrap RouterProvider in . Behalte ToastProvider + alle Routes. " + "Importiere ErrorBoundary von ./components/ErrorBoundary." + ), + refs=["apps/web/src/App.tsx"], + ), + ], + ), + Feature( + name="dashboard-charts", + description="Dashboard mit Stunden-Chart (recharts)", + files=[ + FileGen( + path="apps/web/src/pages/Dashboard.tsx", + purpose=( + "ÜBERARBEITETER Dashboard. Behalte die 3 Karten (Heute/Woche/Aktive Projekte). " + "Füge unten ein BarChart hinzu (recharts BarChart + Bar) mit den letzten 7 Tagen — " + "x-axis: dayName, y-axis: hours. Daten aus listTimeEntries letzte 7 Tage, " + "group-by-day per useMemo. Card-Wrapper mit Tailwind." + ), + refs=["apps/web/src/pages/Dashboard.tsx"], + ), + ], + ), + Feature( + name="api-client-phase4", + description="API-Client um Admin-User + Export-URL ergänzt", + files=[ + FileGen( + path="apps/web/src/lib/api.ts", + purpose=( + "FINAL+ - behalte ALLES aus vorher. Füge hinzu: " + "listUsers(), createUser({email,name,role,password}), updateUser(id, {name,role}), deleteUser(id), " + "Alle Admin-Endpoints → /api/users." + ), + refs=["apps/web/src/lib/api.ts"], + ), + ], + ), + Feature( + name="router-with-admin", + description="App.tsx +/admin route + Nav admin-link bei admin-role", + files=[ + FileGen( + path="apps/web/src/App.tsx", + purpose=( + "ERWEITERT — füge Route /admin (AdminUsers component) hinzu. Auth-Check: zusätzlich role='admin' (sonst redirect /). " + "Behalte ErrorBoundary + ToastProvider + alle Routes." + ), + refs=["apps/web/src/App.tsx", "apps/web/src/pages/AdminUsers.tsx"], + ), + FileGen( + path="apps/web/src/components/Nav.tsx", + purpose=( + "ERWEITERT — Nav zeigt Admin-Link nur wenn current user role='admin'. " + "useQuery(['me'], api.getMe) → wenn data.role==='admin', render Admin. " + "Behalte alle anderen Links + Logout." + ), + refs=["apps/web/src/components/Nav.tsx"], + ), + ], + ), +] + + +def load_state() -> dict: + if PHASE4_STATE.exists(): + return json.loads(PHASE4_STATE.read_text()) + return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()} + + +def save_state(state: dict) -> None: + PHASE4_STATE.write_text(json.dumps(state, indent=2)) + + +async def main() -> int: + log_section(f"🚀 Phase-4 Codegen-Run gestartet") + log(f"Features: {len(FEATURES)}") + + # ensure recharts is installed for dashboard-charts + log("Ensuring recharts dep…") + import subprocess + r = subprocess.run(["pnpm", "--filter", "web", "add", "recharts"], + cwd=ROOT, capture_output=True, text=True, timeout=120) + log(f" recharts install rc={r.returncode}") + + state = load_state() + for feature in FEATURES: + if feature.name in state.get("completed_features", []): + log(f"⏭ Skip {feature.name}") + continue + state["current_feature"] = feature.name + save_state(state) + try: + success = await run_feature_v2(feature) + if success: + state.setdefault("completed_features", []).append(feature.name) + else: + state.setdefault("attempted_features", []).append(feature.name) + save_state(state) + except Exception as e: + log(f"❌ Feature {feature.name} crashed: {e}", level="ERROR") + state.setdefault("attempted_features", []).append(feature.name) + save_state(state) + + log_section("Phase-4 Run beendet") + log(f"OK: {len(state.get('completed_features', []))}, " + f"Attempted: {len(state.get('attempted_features', []))}, " + f"Total: {len(FEATURES)}") + return 0 + + +if __name__ == "__main__": + sys.exit(asyncio.run(main()))