refactor: form-core wiring, JWT sessions, slowapi, packaging, frontend rewrite, test declutter
This commit is contained in:
parent
06ab25fb47
commit
03af41e837
74 changed files with 6089 additions and 5183 deletions
795
frontend/package-lock.json
generated
795
frontend/package-lock.json
generated
|
|
@ -9,20 +9,28 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@mantine/core": "^9.6.1",
|
||||
"@mantine/dates": "^9.6.1",
|
||||
"@mantine/form": "^9.6.1",
|
||||
"@mantine/hooks": "^9.6.1",
|
||||
"@mantine/notifications": "^9.6.1",
|
||||
"@rjsf/core": "^6.10.0",
|
||||
"@rjsf/mantine": "^6.10.0",
|
||||
"@rjsf/utils": "^6.10.0",
|
||||
"@rjsf/validator-ajv8": "^6.10.0",
|
||||
"@tabler/icons-react": "^3.46.0",
|
||||
"@tanstack/react-query": "^5.62.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.18.3"
|
||||
"react-router-dom": "^7.18.3",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"playwright": "^1.63.0",
|
||||
"sharp": "^0.34.0",
|
||||
"typescript": "~5.6.0",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
|
|
@ -261,6 +269,15 @@
|
|||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||
|
|
@ -309,6 +326,17 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
|
||||
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||
|
|
@ -804,6 +832,496 @@
|
|||
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@img/colour": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
||||
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-ppc64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-riscv64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
||||
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
|
|
@ -873,22 +1391,35 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@mantine/dates": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-9.6.1.tgz",
|
||||
"integrity": "sha512-3NxfqrcwMuYB9wvxIhGhEhhazK3wcA2wG4AuNVFGJDd97ujWWAy+jGHqBF9jOusp/Mk4bn8u+3SuDICtLPDf5w==",
|
||||
"version": "9.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-9.6.3.tgz",
|
||||
"integrity": "sha512-sPDlPBKl0R8VR9mmTVaNvuvsczD79qMbZOpe+wjkQkrvE102G+iwk2cxW8UIA+o8jqc6jAP4Cf2aoy9EqJ/hiQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mantine/core": "9.6.1",
|
||||
"@mantine/hooks": "9.6.1",
|
||||
"@mantine/core": "9.6.3",
|
||||
"@mantine/hooks": "9.6.3",
|
||||
"dayjs": ">=1.0.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mantine/form": {
|
||||
"version": "9.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-9.6.3.tgz",
|
||||
"integrity": "sha512-u62dq7iwIPc5hs7hZ00sgAjlhsCgGs88J2NeCfUF7Ap4+65FoR1xi572xiIfLfzugFbDvORKFQir9WGfFJ00ew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"klona": "^2.0.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mantine/hooks": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-9.6.1.tgz",
|
||||
|
|
@ -898,6 +1429,31 @@
|
|||
"react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mantine/notifications": {
|
||||
"version": "9.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-9.6.3.tgz",
|
||||
"integrity": "sha512-1imtNfBwZ0JBDrg3ohVuZLnJvwRVhb7eWFbcfz5QFSy7hbM/MereURExWUGbZRBiJhQz850Aj6fVSEAviK6vdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mantine/store": "9.6.3",
|
||||
"react-transition-group": "4.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mantine/core": "9.6.3",
|
||||
"@mantine/hooks": "9.6.3",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mantine/store": {
|
||||
"version": "9.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/store/-/store-9.6.3.tgz",
|
||||
"integrity": "sha512-EdD0QAn5HpsAKbvZwz7cqzq4E+4UpXCWK3hK5SR/O/WFtSAm0tUIyKBcrQ9baPRoQknfhOBmN7A17K+PdOzGqg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
|
||||
|
|
@ -952,13 +1508,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rjsf/utils": {
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.10.0.tgz",
|
||||
"integrity": "sha512-GQa+dr28FgGOaRRhIOX3QEv1GkzoklrZYf4f1bTEDNuAWWWyO1oznw62ryZBj4FI/XKPaE/VyJQlssOSbD5KiQ==",
|
||||
"version": "6.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.10.1.tgz",
|
||||
"integrity": "sha512-dlbQUCoTl/+/TenyYs1zAKXf9sLTebtY+6pB+bUFsaN4xkdAtR8YBLwmpa1N5yNiRzf3fbxveISNuZmDrZE/yQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@x0k/json-schema-merge": "^1.0.3",
|
||||
"@x0k/json-schema-merge": "^1.0.6",
|
||||
"fast-equals": "^6.0.0",
|
||||
"fast-uri": "^4.1.4",
|
||||
"jsonpointer": "^5.0.1",
|
||||
|
|
@ -1344,6 +1899,12 @@
|
|||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tabler/icons": {
|
||||
"version": "3.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.46.0.tgz",
|
||||
|
|
@ -1370,6 +1931,32 @@
|
|||
"react": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.104.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.104.0.tgz",
|
||||
"integrity": "sha512-JrC2r/JQlXt7khBSdUpsgxNvybzOg+aITa+ARRMlP2AFo93Y8vIqz077rp+e61mJetSZ0T6AJbwW1JHer1vrPQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.104.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.104.0.tgz",
|
||||
"integrity": "sha512-e1TZmDCQnWIfiDVryIHeA6Idj+Lfx1hORLOhXS/l5wcgvtVD4yYqbTDl378sGQKIi2cSgb0TEMC9cKK8GGoJEw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.104.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
|
|
@ -1426,14 +2013,13 @@
|
|||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
|
||||
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
|
|
@ -1471,11 +2057,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@x0k/json-schema-merge": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@x0k/json-schema-merge/-/json-schema-merge-1.0.5.tgz",
|
||||
"integrity": "sha512-bPvwYpTkhBOXkvbC8KMNhkgzv+gWu6Yx6FVho3mL+vMf8Fy3gdn9YrlrBrXphznvOHhB85BudH/ywtVRZ/HG3A==",
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@x0k/json-schema-merge/-/json-schema-merge-1.0.6.tgz",
|
||||
"integrity": "sha512-SSdWAhirRj9UGTSjmgMnchC3Nytpbde46qPEXVQLm/S3opteW6E/VX4UzLlfmA9L2zdkmG3c69sfLfHbT0jl0w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
}
|
||||
|
|
@ -1630,7 +2215,6 @@
|
|||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
|
|
@ -1657,12 +2241,32 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-node-es": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.420",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz",
|
||||
|
|
@ -1729,19 +2333,18 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-equals": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-6.0.3.tgz",
|
||||
"integrity": "sha512-IECu4C0fFZFoUC2cA2rDaNUuVIfWIZMwh3tY+ng924vOuD9OaUFFrcIrVwCKbV7TJDkGmrHfhwZ7R0AnHfVhyw==",
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-6.0.4.tgz",
|
||||
"integrity": "sha512-JiBeR0k76FIahi9zAtQR7X1jgZDKLE+fs5S9ryjOGYZWZCOcm8QiwgeahkFvaboS3i6PkGBopRznHKuKhm9zpg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.4.tgz",
|
||||
"integrity": "sha512-dODXrIxlS9JSdgAnhIUKOosKV1oMtU2VtVw87QRaHzyl5jxO290Ii5tEZfCfzfWNHi3jKWwBSdQj0qIyshdZdQ==",
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.2.1.tgz",
|
||||
"integrity": "sha512-TmHQgewjHtMq1E5QKA0tOE0yeYGQs25KZC/ziJpubRtWI15W92e6vPFydWOeZBVROSHBYw/QhD9d5OefdD6LDg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
|
@ -1752,8 +2355,7 @@
|
|||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
|
|
@ -1811,7 +2413,6 @@
|
|||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
|
|
@ -1851,11 +2452,31 @@
|
|||
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz",
|
||||
"integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/klona": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz",
|
||||
"integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
|
|
@ -1930,6 +2551,15 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
|
@ -2008,6 +2638,23 @@
|
|||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||
|
|
@ -2033,8 +2680,7 @@
|
|||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-number-format": {
|
||||
"version": "5.4.5",
|
||||
|
|
@ -2163,6 +2809,22 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.6.0",
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
|
|
@ -2240,6 +2902,64 @@
|
|||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.0.0",
|
||||
"detect-libc": "^2.1.2",
|
||||
"semver": "^7.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.34.5",
|
||||
"@img/sharp-darwin-x64": "0.34.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||
"@img/sharp-linux-arm": "0.34.5",
|
||||
"@img/sharp-linux-arm64": "0.34.5",
|
||||
"@img/sharp-linux-ppc64": "0.34.5",
|
||||
"@img/sharp-linux-riscv64": "0.34.5",
|
||||
"@img/sharp-linux-s390x": "0.34.5",
|
||||
"@img/sharp-linux-x64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||
"@img/sharp-wasm32": "0.34.5",
|
||||
"@img/sharp-win32-arm64": "0.34.5",
|
||||
"@img/sharp-win32-ia32": "0.34.5",
|
||||
"@img/sharp-win32-x64": "0.34.5"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp/node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
|
|
@ -2475,6 +3195,15 @@
|
|||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,20 +12,28 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@mantine/core": "^9.6.1",
|
||||
"@mantine/dates": "^9.6.1",
|
||||
"@mantine/form": "^9.6.1",
|
||||
"@mantine/hooks": "^9.6.1",
|
||||
"@mantine/notifications": "^9.6.1",
|
||||
"@rjsf/core": "^6.10.0",
|
||||
"@rjsf/mantine": "^6.10.0",
|
||||
"@rjsf/utils": "^6.10.0",
|
||||
"@rjsf/validator-ajv8": "^6.10.0",
|
||||
"@tabler/icons-react": "^3.46.0",
|
||||
"@tanstack/react-query": "^5.62.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.18.3"
|
||||
"react-router-dom": "^7.18.3",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"playwright": "^1.63.0",
|
||||
"sharp": "^0.34.0",
|
||||
"typescript": "~5.6.0",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,4 @@ Dutch-language documents.
|
|||
| hero-church.webp | Stone church tower, blue sky | https://www.pexels.com/photo/historic-church-with-bell-tower-in-germany-31630127/ |
|
||||
| church-pews.webp | Bright empty pews, checkerboard| https://www.pexels.com/photo/church-pews-7566207/ |
|
||||
| paperwork-desk.webp | Desk with stamps and forms | https://www.pexels.com/photo/desk-with-documents-and-stamps-18687845/ |
|
||||
| team-laptop.webp | Team collaborating at laptop | https://www.pexels.com/photo/a-team-looking-at-a-laptop-7793702/ |
|
||||
| pilot-meeting.webp | Consultation meeting, notes | https://www.pexels.com/photo/two-people-shaking-hands-5668858/ |
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 115 KiB |
|
|
@ -50,12 +50,12 @@ try {
|
|||
const page = await context.newPage();
|
||||
for (const shot of shots) {
|
||||
await page.goto(base + shot.path, { waitUntil: "networkidle" });
|
||||
await page.waitForTimeout(1500);
|
||||
await page.waitForSelector("main, #root", { timeout: 15000 });
|
||||
if (shot.selectFlow) {
|
||||
// Show the generated RJSF form instead of the default first flow.
|
||||
await page.locator('[role="combobox"]').first().click();
|
||||
await page.getByRole("option", { name: shot.selectFlow }).click();
|
||||
await page.waitForTimeout(2000);
|
||||
await page.waitForSelector("form", { timeout: 15000 });
|
||||
}
|
||||
const file = join(outDir, `${shot.name}-${scheme}.png`);
|
||||
await page.screenshot({ path: file, fullPage: shot.fullPage });
|
||||
|
|
|
|||
|
|
@ -46,10 +46,6 @@ PHOTOS = {
|
|||
pexels(18687845),
|
||||
"https://www.pexels.com/photo/desk-with-documents-and-stamps-18687845/",
|
||||
),
|
||||
"team-laptop.webp": (
|
||||
pexels(7793702),
|
||||
"https://www.pexels.com/photo/a-team-looking-at-a-laptop-7793702/",
|
||||
),
|
||||
"pilot-meeting.webp": (
|
||||
pexels(5668858),
|
||||
"https://www.pexels.com/photo/two-people-shaking-hands-5668858/",
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
/* Generate favicon PNGs + manifest icons from favicon.svg (re-runnable).
|
||||
*
|
||||
* Usage (frontend container, Chromium already installed for screenshots):
|
||||
* Usage (frontend container):
|
||||
* npm run icons
|
||||
*
|
||||
* Renders the SVG at each target size (crisp native rasterization) into
|
||||
* public/. apple-touch-icon keeps the solid kirchenblau background (iOS
|
||||
* fills transparency black). The 512 icon doubles as maskable: the glyph
|
||||
* stays inside the inner 80% safe zone.
|
||||
* Renders the SVG at each target size with sharp (no browser needed).
|
||||
* apple-touch-icon keeps the solid kirchenblau background (iOS fills
|
||||
* transparency black). The 512 icon doubles as maskable: the glyph stays
|
||||
* inside the inner 80% safe zone.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { chromium } from "playwright";
|
||||
import sharp from "sharp";
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const svg = readFileSync(join(root, "public", "favicon.svg"), "utf-8");
|
||||
const dataUrl = `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
||||
const svg = readFileSync(join(root, "public", "favicon.svg"));
|
||||
|
||||
const targets = [
|
||||
["favicon-32x32.png", 32],
|
||||
|
|
@ -25,20 +24,11 @@ const targets = [
|
|||
["icon-512.png", 512],
|
||||
];
|
||||
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
for (const [file, size] of targets) {
|
||||
await page.setViewportSize({ width: size, height: size });
|
||||
await page.setContent(
|
||||
`<body style="margin:0"><img src="${dataUrl}" width="${size}" height="${size}"/></body>`,
|
||||
);
|
||||
await page.locator("img").screenshot({
|
||||
path: join(root, "public", file),
|
||||
omitBackground: true,
|
||||
});
|
||||
console.log(`wrote public/${file}`);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
for (const [file, size] of targets) {
|
||||
await sharp(svg, { density: 300 })
|
||||
.resize(size, size, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||
.flatten({ background: file === "apple-touch-icon.png" ? "#1d4a79" : "#ffffff" })
|
||||
.png()
|
||||
.toFile(join(root, "public", file));
|
||||
console.log(`wrote public/${file}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import {
|
|||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { Notifications } from "@mantine/notifications";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { ReactNode } from "react";
|
||||
import { AuthProvider, useAuth } from "./auth";
|
||||
import SchemeToggle from "./components/SchemeToggle";
|
||||
|
|
@ -67,15 +69,17 @@ function Nav() {
|
|||
);
|
||||
}
|
||||
|
||||
function Loading() {
|
||||
return (
|
||||
<Center mt="xl">
|
||||
<Loader />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
function RequireAuth({ children }: { children: ReactNode }) {
|
||||
const { me, loading } = useAuth();
|
||||
if (loading) {
|
||||
return (
|
||||
<Center mt="xl">
|
||||
<Loader />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
if (loading) return <Loading />;
|
||||
// Runs, run history and approvals are login-gated server-side;
|
||||
// anonymous visitors go to the magic-link login.
|
||||
if (!me) return <Navigate to="/login" replace />;
|
||||
|
|
@ -111,9 +115,7 @@ function Shell() {
|
|||
KI-gestützte Verwaltungsplattform für Kirche
|
||||
</Text>
|
||||
{loading ? (
|
||||
<Center mt="xl">
|
||||
<Loader />
|
||||
</Center>
|
||||
<Loading />
|
||||
) : (
|
||||
<Routes>
|
||||
<Route path="/" element={<StartPage />} />
|
||||
|
|
@ -143,7 +145,7 @@ function Shell() {
|
|||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<StartPage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
)}
|
||||
</Container>
|
||||
|
|
@ -152,10 +154,17 @@ function Shell() {
|
|||
);
|
||||
}
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { staleTime: 15_000, retry: 1 } },
|
||||
});
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<Shell />
|
||||
</AuthProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<Notifications position="top-right" />
|
||||
<Shell />
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,11 @@ export interface Decision {
|
|||
continuation_run: Run | null;
|
||||
}
|
||||
|
||||
export interface NamedRun {
|
||||
flow_name: string;
|
||||
run: Run;
|
||||
}
|
||||
|
||||
export interface SyncReport {
|
||||
added: string[];
|
||||
updated: string[];
|
||||
|
|
@ -153,6 +158,7 @@ export const api = {
|
|||
getInputSchema: (flowId: string) =>
|
||||
req<InputSchema>(`/api/flows/${flowId}/input-schema`),
|
||||
listRuns: (flowId: string) => req<Run[]>(`/api/flows/${flowId}/runs`),
|
||||
listAllRuns: () => req<NamedRun[]>("/api/runs"),
|
||||
listApprovals: () => req<ApprovalTask[]>("/api/approvals"),
|
||||
decide: (taskId: string, decision: "approve" | "reject", note: string) =>
|
||||
req<Decision>(`/api/approvals/${taskId}/decision`, {
|
||||
|
|
|
|||
|
|
@ -1,52 +1,55 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconAlertCircle,
|
||||
IconCheck,
|
||||
} from "@tabler/icons-react";
|
||||
import { ApiError, api, type AccessRequest } from "../api";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { api, type AccessRequest } from "../api";
|
||||
import { formatApiError } from "../utils";
|
||||
import { DecisionButtons } from "./DecisionButtons";
|
||||
import { ErrorAlert } from "./ErrorAlert";
|
||||
import { FormattedDate } from "./FormattedDate";
|
||||
import StatusBadge from "./StatusBadge";
|
||||
|
||||
export default function AccessQueue() {
|
||||
const [requests, setRequests] = useState<AccessRequest[]>([]);
|
||||
const [error, setError] = useState("");
|
||||
const [info, setInfo] = useState("");
|
||||
const queryClient = useQueryClient();
|
||||
const [busyId, setBusyId] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
setRequests(await api.listAccessRequests());
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const requestsQuery = useQuery({
|
||||
queryKey: ["access-requests"],
|
||||
queryFn: () => api.listAccessRequests(),
|
||||
});
|
||||
const requests: AccessRequest[] = requestsQuery.data ?? [];
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
if (requestsQuery.error) setError(formatApiError(requestsQuery.error));
|
||||
}, [requestsQuery.error]);
|
||||
|
||||
async function decide(id: string, approve: boolean) {
|
||||
setBusyId(id);
|
||||
setError("");
|
||||
setInfo("");
|
||||
try {
|
||||
const res = await api.decideAccess(id, approve);
|
||||
setRequests((prev) =>
|
||||
prev.map((r) => (r.id === id ? res.request : r)),
|
||||
);
|
||||
if (approve && !res.email_sent) {
|
||||
setInfo("Genehmigt — die E-Mail kam nicht an (SMTP prüfen).");
|
||||
notifications.show({
|
||||
title: "Genehmigt",
|
||||
message: "Die E-Mail kam nicht an (SMTP prüfen).",
|
||||
color: "yellow",
|
||||
});
|
||||
} else {
|
||||
notifications.show({
|
||||
title: approve ? "Genehmigt" : "Abgelehnt",
|
||||
message: approve ? "Zugang freigegeben." : "Anfrage abgelehnt.",
|
||||
color: approve ? "green" : "red",
|
||||
});
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: ["access-requests"] });
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
|
||||
setError(formatApiError(e));
|
||||
} finally {
|
||||
setBusyId("");
|
||||
}
|
||||
|
|
@ -60,16 +63,7 @@ export default function AccessQueue() {
|
|||
<Title order={3} mb="xs">
|
||||
Zugangsanfragen
|
||||
</Title>
|
||||
{error && (
|
||||
<Alert color="red" icon={<IconAlertCircle size={18} />} mb="md">
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
{info && (
|
||||
<Alert color="green" icon={<IconCheck size={18} />} mb="md">
|
||||
{info}
|
||||
</Alert>
|
||||
)}
|
||||
<ErrorAlert message={error} />
|
||||
{pending.length === 0 && (
|
||||
<Text c="dimmed" mb="md">
|
||||
Keine offenen Anfragen.
|
||||
|
|
@ -90,23 +84,13 @@ export default function AccessQueue() {
|
|||
{r.reason}
|
||||
</Text>
|
||||
)}
|
||||
<Group>
|
||||
<Button
|
||||
color="green"
|
||||
loading={busyId === r.id}
|
||||
onClick={() => void decide(r.id, true)}
|
||||
>
|
||||
Genehmigen
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
variant="light"
|
||||
loading={busyId === r.id}
|
||||
onClick={() => void decide(r.id, false)}
|
||||
>
|
||||
Ablehnen
|
||||
</Button>
|
||||
</Group>
|
||||
<DecisionButtons
|
||||
busy={busyId === r.id}
|
||||
onApprove={() => void decide(r.id, true)}
|
||||
onReject={() => void decide(r.id, false)}
|
||||
approveLabel="Genehmigen"
|
||||
rejectLabel="Ablehnen"
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
|
|
@ -119,7 +103,7 @@ export default function AccessQueue() {
|
|||
<Group key={r.id} gap="xs" mb={4}>
|
||||
<StatusBadge status={r.status} />
|
||||
<Text size="sm">
|
||||
{r.name} · {r.email}
|
||||
{r.name} · {r.email} · <FormattedDate iso={r.decided_at} />
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Anchor,
|
||||
Button,
|
||||
Card,
|
||||
|
|
@ -10,83 +9,111 @@ import {
|
|||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import {
|
||||
IconAlertCircle,
|
||||
IconCheck,
|
||||
IconExternalLink,
|
||||
IconRefresh,
|
||||
IconUpload,
|
||||
} from "@tabler/icons-react";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
ApiError,
|
||||
LANGFLOW_URL,
|
||||
api,
|
||||
type FlowDefinition,
|
||||
type FlowVersion,
|
||||
} from "../api";
|
||||
import { formatApiError } from "../utils";
|
||||
import FlowHelp from "./FlowHelp";
|
||||
import { ErrorAlert } from "./ErrorAlert";
|
||||
import StatusBadge from "./StatusBadge";
|
||||
|
||||
const importSchema = z.object({
|
||||
langflowId: z.string().min(1, "Flow-ID erforderlich"),
|
||||
importName: z.string(),
|
||||
});
|
||||
|
||||
export default function AdminTab() {
|
||||
const [flows, setFlows] = useState<FlowDefinition[]>([]);
|
||||
const [versions, setVersions] = useState<Record<string, FlowVersion[]>>({});
|
||||
const [langflowId, setLangflowId] = useState("");
|
||||
const [importName, setImportName] = useState("");
|
||||
const importForm = useForm({
|
||||
initialValues: { langflowId: "", importName: "" },
|
||||
validate: (values) => {
|
||||
const parsed = importSchema.safeParse(values);
|
||||
if (parsed.success) return {};
|
||||
return {
|
||||
langflowId:
|
||||
parsed.error.flatten().fieldErrors.langflowId?.[0] ?? null,
|
||||
};
|
||||
},
|
||||
});
|
||||
const [error, setError] = useState("");
|
||||
const [info, setInfo] = useState("");
|
||||
|
||||
const load = useCallback(async () => {
|
||||
function notifyInfo(message: string) {
|
||||
notifications.show({ message, color: "green" });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
const list = await api.listFlows();
|
||||
setFlows(list);
|
||||
const entries = await Promise.all(
|
||||
list.map(async (f) => [f.id, await api.listVersions(f.id)] as const),
|
||||
);
|
||||
setVersions(Object.fromEntries(entries));
|
||||
} catch (e) {
|
||||
setError(formatApiError(e));
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const list = await api.listFlows();
|
||||
setFlows(list);
|
||||
const all: Record<string, FlowVersion[]> = {};
|
||||
for (const f of list) {
|
||||
all[f.id] = await api.listVersions(f.id);
|
||||
}
|
||||
setVersions(all);
|
||||
const entries = await Promise.all(
|
||||
list.map(async (f) => [f.id, await api.listVersions(f.id)] as const),
|
||||
);
|
||||
setVersions(Object.fromEntries(entries));
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
|
||||
setError(formatApiError(e));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
}
|
||||
|
||||
async function onImport() {
|
||||
setError("");
|
||||
setInfo("");
|
||||
const validation = importForm.validate();
|
||||
if (validation.hasErrors) return;
|
||||
const { langflowId, importName } = importForm.values;
|
||||
try {
|
||||
const result = await api.importFlow(
|
||||
langflowId,
|
||||
importName.trim() ? importName.trim() : undefined,
|
||||
);
|
||||
setInfo(
|
||||
notifyInfo(
|
||||
`Importiert: ${result.definition.name} (Version ${result.version.version_no})`,
|
||||
);
|
||||
setLangflowId("");
|
||||
setImportName("");
|
||||
importForm.reset();
|
||||
await load();
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
|
||||
setError(formatApiError(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeploy(flowId: string, versionNo: number) {
|
||||
setError("");
|
||||
setInfo("");
|
||||
try {
|
||||
const version = await api.deployVersion(flowId, versionNo);
|
||||
setInfo(`Deployed: Version ${version.version_no} (${version.lfx_flow_id})`);
|
||||
notifyInfo(`Deployed: Version ${version.version_no} (${version.lfx_flow_id})`);
|
||||
await load();
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
|
||||
setError(formatApiError(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function onSync() {
|
||||
setError("");
|
||||
setInfo("");
|
||||
try {
|
||||
const r = await api.syncFlows();
|
||||
const parts = [
|
||||
|
|
@ -98,10 +125,10 @@ export default function AdminTab() {
|
|||
const errs = Object.entries(r.errors)
|
||||
.map(([f, e]) => `${f}: ${e}`)
|
||||
.join("; ");
|
||||
setInfo(`Sync ${parts}${errs ? ` — Fehler: ${errs}` : ""}`);
|
||||
notifyInfo(`Sync ${parts}${errs ? ` — Fehler: ${errs}` : ""}`);
|
||||
await load();
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
|
||||
setError(formatApiError(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,16 +137,7 @@ export default function AdminTab() {
|
|||
<Title order={3} mb="xs">
|
||||
Flows & Versionen
|
||||
</Title>
|
||||
{error && (
|
||||
<Alert color="red" icon={<IconAlertCircle size={18} />} mb="md">
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
{info && (
|
||||
<Alert color="green" icon={<IconCheck size={18} />} mb="md">
|
||||
{info}
|
||||
</Alert>
|
||||
)}
|
||||
<ErrorAlert message={error} />
|
||||
<Card withBorder padding="md" mb="md">
|
||||
<Text mb="xs">
|
||||
Flows werden visuell in Langflow entworfen:{" "}
|
||||
|
|
@ -130,20 +148,18 @@ export default function AdminTab() {
|
|||
<Group align="flex-end">
|
||||
<TextInput
|
||||
label="Langflow Flow-ID (UUID)"
|
||||
value={langflowId}
|
||||
onChange={(e) => setLangflowId(e.target.value)}
|
||||
style={{ flex: 1 }}
|
||||
styles={{ input: { fontFamily: "monospace" } }}
|
||||
{...importForm.getInputProps("langflowId")}
|
||||
/>
|
||||
<TextInput
|
||||
label="Name (optional)"
|
||||
value={importName}
|
||||
onChange={(e) => setImportName(e.target.value)}
|
||||
{...importForm.getInputProps("importName")}
|
||||
/>
|
||||
<Button
|
||||
leftSection={<IconUpload size={16} />}
|
||||
onClick={() => void onImport()}
|
||||
disabled={!langflowId.trim()}
|
||||
disabled={!importForm.values.langflowId.trim()}
|
||||
>
|
||||
Importieren + Deployen
|
||||
</Button>
|
||||
|
|
@ -200,9 +216,9 @@ export default function AdminTab() {
|
|||
</Table.Tr>
|
||||
);
|
||||
}
|
||||
const live = [...list]
|
||||
.reverse()
|
||||
.find((v) => v.deployed_at && v.id !== latest.id);
|
||||
const live = list
|
||||
.filter((v) => v.deployed_at && v.id !== latest.id)
|
||||
.at(-1);
|
||||
return (
|
||||
<Table.Tr key={latest.id}>
|
||||
<Table.Td>
|
||||
|
|
|
|||
|
|
@ -1,49 +1,55 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Code,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { IconAlertCircle, IconCheck, IconRefresh, IconX } from "@tabler/icons-react";
|
||||
import { ApiError, api, type ApprovalTask } from "../api";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { IconRefresh } from "@tabler/icons-react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { api, type ApprovalTask } from "../api";
|
||||
import { useAuth } from "../auth";
|
||||
import { formatApiError } from "../utils";
|
||||
import { DecisionButtons } from "./DecisionButtons";
|
||||
import { ErrorAlert } from "./ErrorAlert";
|
||||
import { JsonView } from "./JsonView";
|
||||
import { FormattedDate } from "./FormattedDate";
|
||||
import StatusBadge from "./StatusBadge";
|
||||
|
||||
export default function ApprovalsTab() {
|
||||
const { me } = useAuth();
|
||||
const [tasks, setTasks] = useState<ApprovalTask[]>([]);
|
||||
const queryClient = useQueryClient();
|
||||
const [note, setNote] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [busyId, setBusyId] = useState("");
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
setTasks(await api.listApprovals());
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const tasksQuery = useQuery({
|
||||
queryKey: ["approvals"],
|
||||
queryFn: api.listApprovals,
|
||||
});
|
||||
const tasks: ApprovalTask[] = tasksQuery.data ?? [];
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
if (tasksQuery.error) setError(formatApiError(tasksQuery.error));
|
||||
}, [tasksQuery.error]);
|
||||
|
||||
async function decide(taskId: string, decision: "approve" | "reject") {
|
||||
setBusyId(taskId);
|
||||
setError("");
|
||||
try {
|
||||
const result = await api.decide(taskId, decision, note);
|
||||
setTasks((prev) =>
|
||||
prev.map((t) => (t.id === taskId ? result.task : t)),
|
||||
);
|
||||
await api.decide(taskId, decision, note);
|
||||
notifications.show({
|
||||
title: decision === "approve" ? "Freigegeben" : "Abgelehnt",
|
||||
color: decision === "approve" ? "green" : "red",
|
||||
message: note || undefined,
|
||||
});
|
||||
await queryClient.invalidateQueries({ queryKey: ["approvals"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["all-runs"] });
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
|
||||
setError(formatApiError(e));
|
||||
} finally {
|
||||
setBusyId("");
|
||||
}
|
||||
|
|
@ -57,11 +63,7 @@ export default function ApprovalsTab() {
|
|||
<Title order={3} mb="xs">
|
||||
Freigaben
|
||||
</Title>
|
||||
{error && (
|
||||
<Alert color="red" icon={<IconAlertCircle size={18} />} mb="md">
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
<ErrorAlert message={error} />
|
||||
<Text size="sm" c="dimmed" mb="md">
|
||||
Angemeldet als {me?.email ?? "–"} — Entscheidungen werden diesem
|
||||
Konto zugeordnet.
|
||||
|
|
@ -76,7 +78,10 @@ export default function ApprovalsTab() {
|
|||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconRefresh size={16} />}
|
||||
onClick={() => void load()}
|
||||
loading={tasksQuery.isFetching}
|
||||
onClick={() =>
|
||||
void queryClient.invalidateQueries({ queryKey: ["approvals"] })
|
||||
}
|
||||
>
|
||||
Aktualisieren
|
||||
</Button>
|
||||
|
|
@ -99,27 +104,15 @@ export default function ApprovalsTab() {
|
|||
{t.tenant_id}
|
||||
</Text>
|
||||
</Group>
|
||||
<Code block mb="sm">
|
||||
{JSON.stringify(t.payload)}
|
||||
</Code>
|
||||
<JsonView value={t.payload} />
|
||||
<Group>
|
||||
<Button
|
||||
color="green"
|
||||
leftSection={<IconCheck size={16} />}
|
||||
loading={busyId === t.id}
|
||||
onClick={() => void decide(t.id, "approve")}
|
||||
>
|
||||
Freigeben
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
variant="light"
|
||||
leftSection={<IconX size={16} />}
|
||||
loading={busyId === t.id}
|
||||
onClick={() => void decide(t.id, "reject")}
|
||||
>
|
||||
Ablehnen
|
||||
</Button>
|
||||
<DecisionButtons
|
||||
busy={busyId === t.id}
|
||||
onApprove={() => void decide(t.id, "approve")}
|
||||
onReject={() => void decide(t.id, "reject")}
|
||||
approveLabel="Freigeben"
|
||||
rejectLabel="Ablehnen"
|
||||
/>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
|
|
@ -132,7 +125,8 @@ export default function ApprovalsTab() {
|
|||
<StatusBadge status={t.status} />
|
||||
<Text size="sm">
|
||||
{t.title || t.id} · {t.approver ?? "–"}
|
||||
{t.decision_note ? ` · „${t.decision_note}“` : ""}
|
||||
{t.decision_note ? ` · „${t.decision_note}“` : ""} ·{" "}
|
||||
<FormattedDate iso={t.decided_at} />
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
|
|
|
|||
38
frontend/src/components/DecisionButtons.tsx
Normal file
38
frontend/src/components/DecisionButtons.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { Button, Group } from "@mantine/core";
|
||||
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||
|
||||
export function DecisionButtons({
|
||||
busy,
|
||||
onApprove,
|
||||
onReject,
|
||||
approveLabel,
|
||||
rejectLabel,
|
||||
}: {
|
||||
busy: boolean;
|
||||
onApprove: () => void;
|
||||
onReject: () => void;
|
||||
approveLabel: string;
|
||||
rejectLabel: string;
|
||||
}) {
|
||||
return (
|
||||
<Group>
|
||||
<Button
|
||||
color="green"
|
||||
leftSection={<IconCheck size={16} />}
|
||||
loading={busy}
|
||||
onClick={onApprove}
|
||||
>
|
||||
{approveLabel}
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
variant="light"
|
||||
leftSection={<IconX size={16} />}
|
||||
loading={busy}
|
||||
onClick={onReject}
|
||||
>
|
||||
{rejectLabel}
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
10
frontend/src/components/FormattedDate.tsx
Normal file
10
frontend/src/components/FormattedDate.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/de";
|
||||
|
||||
dayjs.locale("de");
|
||||
|
||||
/** Localized date (was toLocaleString in one place, missing elsewhere). */
|
||||
export function FormattedDate({ iso }: { iso: string | null }) {
|
||||
if (!iso) return <>–</>;
|
||||
return <>{dayjs(iso).format("DD.MM.YYYY HH:mm")}</>;
|
||||
}
|
||||
12
frontend/src/components/JsonView.tsx
Normal file
12
frontend/src/components/JsonView.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Code } from "@mantine/core";
|
||||
|
||||
const MAX_LEN = 2000;
|
||||
|
||||
/** Pretty JSON view (was raw JSON.stringify / [object Object] in 3 places). */
|
||||
export function JsonView({ value }: { value: unknown }) {
|
||||
if (value === null || value === undefined) return <>–</>;
|
||||
const text =
|
||||
typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
||||
const shown = text.length > MAX_LEN ? text.slice(0, MAX_LEN) + "…" : text;
|
||||
return <Code block>{shown}</Code>;
|
||||
}
|
||||
71
frontend/src/components/Landing.tsx
Normal file
71
frontend/src/components/Landing.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import type { ReactNode } from "react";
|
||||
import { Card, Image, Text, ThemeIcon, Title } from "@mantine/core";
|
||||
|
||||
export const PHOTO_CLASS = "photo";
|
||||
|
||||
/** Shared landing building blocks (was copy-pasted Card/Icon/Title/Text x10). */
|
||||
export function SectionTitle({ children }: { children: string }) {
|
||||
return (
|
||||
<Title order={2} mb="xs" ta="center">
|
||||
{children}
|
||||
</Title>
|
||||
);
|
||||
}
|
||||
|
||||
export function SectionCard({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<Card withBorder padding="xl" radius="lg">
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function FeatureCard({
|
||||
icon,
|
||||
title,
|
||||
text,
|
||||
iconSize = 40,
|
||||
}: {
|
||||
icon: ReactNode;
|
||||
title: string;
|
||||
text: string;
|
||||
iconSize?: number;
|
||||
}) {
|
||||
return (
|
||||
<Card withBorder padding="lg" radius="lg">
|
||||
<ThemeIcon size={iconSize} radius="md" variant="light" mb="sm">
|
||||
{icon}
|
||||
</ThemeIcon>
|
||||
<Title order={4} mb="xs">
|
||||
{title}
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
{text}
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function PhotoImage({
|
||||
src,
|
||||
alt,
|
||||
h,
|
||||
eager = false,
|
||||
}: {
|
||||
src: string;
|
||||
alt: string;
|
||||
h?: number;
|
||||
eager?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
radius="lg"
|
||||
className={PHOTO_CLASS}
|
||||
loading={eager ? undefined : "lazy"}
|
||||
h={h}
|
||||
fit="cover"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import { Code, Table } from "@mantine/core";
|
||||
import { Table } from "@mantine/core";
|
||||
import StatusBadge from "./StatusBadge";
|
||||
import { FormattedDate } from "./FormattedDate";
|
||||
import { JsonView } from "./JsonView";
|
||||
import type { Run } from "../api";
|
||||
|
||||
export function RunsTable({ runs }: { runs: Run[] }) {
|
||||
|
|
@ -19,9 +21,11 @@ export function RunsTable({ runs }: { runs: Run[] }) {
|
|||
<StatusBadge status={r.status} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Code>{r.output?.result ?? r.output?.error ?? "–"}</Code>
|
||||
<JsonView value={r.output?.result ?? r.output?.error ?? "–"} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<FormattedDate iso={r.created_at} />
|
||||
</Table.Td>
|
||||
<Table.Td>{new Date(r.created_at).toLocaleString("de-DE")}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Stack,
|
||||
|
|
@ -10,31 +9,53 @@ import {
|
|||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { IconAlertCircle, IconCheck } from "@tabler/icons-react";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { z } from "zod";
|
||||
import { ApiError, api } from "../api";
|
||||
import { formatApiError } from "../utils";
|
||||
import { ErrorAlert } from "../components/ErrorAlert";
|
||||
|
||||
const accessSchema = z.object({
|
||||
name: z.string().min(1, "Name erforderlich").max(200),
|
||||
email: z.string().email("Gültige E-Mail erforderlich").max(320),
|
||||
reason: z.string().max(2000),
|
||||
});
|
||||
|
||||
export default function AccessPage() {
|
||||
const [params] = useSearchParams();
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState(params.get("email") ?? "");
|
||||
const [reason, setReason] = useState("");
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
name: "",
|
||||
email: params.get("email") ?? "",
|
||||
reason: "",
|
||||
},
|
||||
validate: (values) => {
|
||||
const parsed = accessSchema.safeParse(values);
|
||||
if (parsed.success) return {};
|
||||
const flat = parsed.error.flatten().fieldErrors;
|
||||
return {
|
||||
name: flat.name?.[0] ?? null,
|
||||
email: flat.email?.[0] ?? null,
|
||||
reason: flat.reason?.[0] ?? null,
|
||||
};
|
||||
},
|
||||
});
|
||||
const [done, setDone] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
async function onSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
async function onSubmit(values: typeof form.values) {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
await api.requestAccess(name.trim(), email.trim(), reason.trim());
|
||||
await api.requestAccess(values.name.trim(), values.email.trim(), values.reason.trim());
|
||||
setDone(true);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof ApiError
|
||||
? err.status === 429
|
||||
? "Zu viele Anfragen von dieser Adresse — bitte später erneut versuchen."
|
||||
: `${err.status}: ${err.message}`
|
||||
: formatApiError(err)
|
||||
: String(err),
|
||||
);
|
||||
} finally {
|
||||
|
|
@ -48,38 +69,25 @@ export default function AccessPage() {
|
|||
Zugang anfragen
|
||||
</Title>
|
||||
{done ? (
|
||||
<Alert color="green" icon={<IconCheck size={18} />} title="Angefragt">
|
||||
Danke! Ihre Anfrage ist eingegangen. Sie erhalten eine E-Mail, sobald
|
||||
sie freigegeben wurde.
|
||||
</Alert>
|
||||
<Text c="green">
|
||||
Danke! Ihre Anfrage ist eingegangen. Sie erhalten eine E-Mail,
|
||||
sobald sie freigegeben wurde.
|
||||
</Text>
|
||||
) : (
|
||||
<form onSubmit={(e) => void onSubmit(e)}>
|
||||
<form onSubmit={form.onSubmit((v) => void onSubmit(v))}>
|
||||
<Stack>
|
||||
{error && (
|
||||
<Alert color="red" icon={<IconAlertCircle size={18} />}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
<TextInput
|
||||
label="Name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
maxLength={200}
|
||||
/>
|
||||
<ErrorAlert message={error} />
|
||||
<TextInput label="Name" required {...form.getInputProps("name")} />
|
||||
<TextInput
|
||||
label="E-Mail"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
maxLength={320}
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
<Textarea
|
||||
label="Wofür brauchen Sie Zugang?"
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
rows={4}
|
||||
{...form.getInputProps("reason")}
|
||||
/>
|
||||
<Button type="submit" loading={busy}>
|
||||
Anfragen
|
||||
|
|
|
|||
|
|
@ -7,40 +7,29 @@ import { useAuth } from "../auth";
|
|||
|
||||
export default function AdminPage() {
|
||||
const { me } = useAuth();
|
||||
if (!me) {
|
||||
return (
|
||||
<div>
|
||||
<Title order={2} mb="md">
|
||||
Verwaltung
|
||||
</Title>
|
||||
<Alert color="blue">
|
||||
Bitte melden Sie sich an, um die Verwaltung zu nutzen.
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (me.role !== "admin") {
|
||||
return (
|
||||
<div>
|
||||
<Title order={2} mb="md">
|
||||
Verwaltung
|
||||
</Title>
|
||||
<Alert color="red" icon={<IconAlertCircle size={18} />}>
|
||||
Nur für Administratoren. Ihr Konto hat die Rolle „{me.role}“.
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const denied = !me || me.role !== "admin";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title order={2} mb="md">
|
||||
Verwaltung
|
||||
</Title>
|
||||
<Stack gap="xl">
|
||||
<AccessQueue />
|
||||
<ApprovalsTab />
|
||||
<AdminTab />
|
||||
</Stack>
|
||||
{denied ? (
|
||||
<Alert
|
||||
color={!me ? "blue" : "red"}
|
||||
icon={me ? <IconAlertCircle size={18} /> : undefined}
|
||||
>
|
||||
{!me
|
||||
? "Bitte melden Sie sich an, um die Verwaltung zu nutzen."
|
||||
: `Nur für Administratoren. Ihr Konto hat die Rolle „${me.role}“.`}
|
||||
</Alert>
|
||||
) : (
|
||||
<Stack gap="xl">
|
||||
<AccessQueue />
|
||||
<ApprovalsTab />
|
||||
<AdminTab />
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Link, Navigate, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
Alert,
|
||||
Anchor,
|
||||
Button,
|
||||
Card,
|
||||
|
|
@ -12,16 +11,31 @@ import {
|
|||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { IconAlertCircle, IconCheck, IconMail } from "@tabler/icons-react";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { IconMail } from "@tabler/icons-react";
|
||||
import { z } from "zod";
|
||||
import { ApiError, api } from "../api";
|
||||
import { useAuth } from "../auth";
|
||||
import { formatApiError } from "../utils";
|
||||
import { ErrorAlert } from "../components/ErrorAlert";
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email("Gültige E-Mail erforderlich").max(320),
|
||||
});
|
||||
|
||||
export default function LoginPage() {
|
||||
const [params] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const token = params.get("token") ?? "";
|
||||
const { me, refresh } = useAuth();
|
||||
const [email, setEmail] = useState("");
|
||||
const form = useForm({
|
||||
initialValues: { email: "" },
|
||||
validate: (values) => {
|
||||
const parsed = loginSchema.safeParse(values);
|
||||
if (parsed.success) return {};
|
||||
return { email: parsed.error.flatten().fieldErrors.email?.[0] ?? null };
|
||||
},
|
||||
});
|
||||
const [sent, setSent] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [busy, setBusy] = useState(!!token);
|
||||
|
|
@ -45,24 +59,23 @@ export default function LoginPage() {
|
|||
.finally(() => setBusy(false));
|
||||
}, [token, refresh]);
|
||||
|
||||
async function onSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
async function onSubmit(values: typeof form.values) {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
await api.requestMagicLink(email.trim());
|
||||
await api.requestMagicLink(values.email.trim());
|
||||
setSent(true);
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 403) {
|
||||
// Not allowed yet — forward to the access request form.
|
||||
navigate(`/access?email=${encodeURIComponent(email.trim())}`);
|
||||
navigate(`/access?email=${encodeURIComponent(values.email.trim())}`);
|
||||
return;
|
||||
}
|
||||
setError(
|
||||
err instanceof ApiError
|
||||
? err.status === 429
|
||||
? "Zu viele Links angefordert — bitte später erneut versuchen."
|
||||
: `${err.status}: ${err.message}`
|
||||
: formatApiError(err)
|
||||
: String(err),
|
||||
);
|
||||
} finally {
|
||||
|
|
@ -85,32 +98,22 @@ export default function LoginPage() {
|
|||
<Loader />
|
||||
</Center>
|
||||
)}
|
||||
{error && (
|
||||
<Alert color="red" icon={<IconAlertCircle size={18} />}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
<ErrorAlert message={error} />
|
||||
</>
|
||||
) : sent ? (
|
||||
<Alert color="green" icon={<IconCheck size={18} />} title="Unterwegs">
|
||||
<Text c="green">
|
||||
Falls die Adresse freigegeben ist, kommt der Login-Link per
|
||||
E-Mail. Bitte auch den Spam-Ordner prüfen.
|
||||
</Alert>
|
||||
</Text>
|
||||
) : (
|
||||
<form onSubmit={(e) => void onSubmit(e)}>
|
||||
<form onSubmit={form.onSubmit((v) => void onSubmit(v))}>
|
||||
<Stack>
|
||||
{error && (
|
||||
<Alert color="red" icon={<IconAlertCircle size={18} />}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
<ErrorAlert message={error} />
|
||||
<TextInput
|
||||
label="E-Mail"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
maxLength={320}
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
|
|
|
|||
|
|
@ -37,6 +37,12 @@ import {
|
|||
IconWand,
|
||||
} from "@tabler/icons-react";
|
||||
import { useAuth } from "../auth";
|
||||
import {
|
||||
FeatureCard,
|
||||
PhotoImage,
|
||||
SectionCard,
|
||||
SectionTitle,
|
||||
} from "../components/Landing";
|
||||
|
||||
const CTA_LABEL = "Pilotgespräch vereinbaren";
|
||||
const CTA_TO = "/access";
|
||||
|
|
@ -259,14 +265,6 @@ const pilotPoints = [
|
|||
"Persönlicher Ansprechpartner während der gesamten Pilotphase",
|
||||
];
|
||||
|
||||
function SectionTitle({ children }: { children: string }) {
|
||||
return (
|
||||
<Title order={2} mb="xs" ta="center">
|
||||
{children}
|
||||
</Title>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StartPage() {
|
||||
const { me } = useAuth();
|
||||
return (
|
||||
|
|
@ -318,13 +316,10 @@ export default function StartPage() {
|
|||
)}
|
||||
</Box>
|
||||
<Box style={{ flex: 5 }} w={{ base: "100%", md: "auto" }}>
|
||||
<Image
|
||||
<PhotoImage
|
||||
src="/photos/hero-church.webp"
|
||||
alt="Steinerner Kirchturm unter blauem Himmel"
|
||||
radius="lg"
|
||||
className="photo"
|
||||
h={420}
|
||||
fit="cover"
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
|
@ -354,14 +349,10 @@ export default function StartPage() {
|
|||
</Stack>
|
||||
</Box>
|
||||
<Box style={{ flex: 5 }} w={{ base: "100%", md: "auto" }}>
|
||||
<Image
|
||||
<PhotoImage
|
||||
src="/photos/paperwork-desk.webp"
|
||||
alt="Schreibtisch mit Stempeln und Formularen — so sieht es heute oft aus"
|
||||
radius="lg"
|
||||
className="photo"
|
||||
loading="lazy"
|
||||
h={280}
|
||||
fit="cover"
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
|
@ -432,17 +423,13 @@ export default function StartPage() {
|
|||
</Text>
|
||||
<SimpleGrid cols={{ base: 1, xs: 2, md: 3 }} spacing="lg">
|
||||
{platformParts.map((p) => (
|
||||
<Card key={p.title} withBorder padding="lg" radius="lg">
|
||||
<ThemeIcon size={44} radius="md" variant="light" mb="sm">
|
||||
{p.icon}
|
||||
</ThemeIcon>
|
||||
<Title order={4} mb="xs">
|
||||
{p.title}
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
{p.text}
|
||||
</Text>
|
||||
</Card>
|
||||
<FeatureCard
|
||||
key={p.title}
|
||||
icon={p.icon}
|
||||
title={p.title}
|
||||
text={p.text}
|
||||
iconSize={44}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
|
|
@ -486,23 +473,18 @@ export default function StartPage() {
|
|||
</Card>
|
||||
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="lg">
|
||||
{otherBenefits.map((b) => (
|
||||
<Card key={b.title} withBorder padding="lg" radius="lg">
|
||||
<ThemeIcon size={40} radius="md" variant="light" mb="sm">
|
||||
{b.icon}
|
||||
</ThemeIcon>
|
||||
<Title order={4} mb="xs">
|
||||
{b.title}
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
{b.text}
|
||||
</Text>
|
||||
</Card>
|
||||
<FeatureCard
|
||||
key={b.title}
|
||||
icon={b.icon}
|
||||
title={b.title}
|
||||
text={b.text}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
|
||||
{/* Integrations */}
|
||||
<Card withBorder padding="xl" radius="lg">
|
||||
<SectionCard>
|
||||
<Title order={2} mb="xs" ta="center">
|
||||
Passt zu Ihrer bestehenden IT
|
||||
</Title>
|
||||
|
|
@ -516,7 +498,7 @@ export default function StartPage() {
|
|||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
</Card>
|
||||
</SectionCard>
|
||||
|
||||
{/* Examples */}
|
||||
<div>
|
||||
|
|
@ -538,17 +520,12 @@ export default function StartPage() {
|
|||
<SectionTitle>Darauf können Sie sich verlassen</SectionTitle>
|
||||
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="lg">
|
||||
{trust.map((t) => (
|
||||
<Card key={t.title} withBorder padding="lg" radius="lg">
|
||||
<ThemeIcon size={40} radius="md" variant="light" mb="sm">
|
||||
{t.icon}
|
||||
</ThemeIcon>
|
||||
<Title order={4} mb="xs">
|
||||
{t.title}
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t.text}
|
||||
</Text>
|
||||
</Card>
|
||||
<FeatureCard
|
||||
key={t.title}
|
||||
icon={t.icon}
|
||||
title={t.title}
|
||||
text={t.text}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
|
|
@ -586,14 +563,10 @@ export default function StartPage() {
|
|||
</Button>
|
||||
</Box>
|
||||
<Box style={{ flex: 5 }} w={{ base: "100%", md: "auto" }}>
|
||||
<Image
|
||||
<PhotoImage
|
||||
src="/photos/pilot-meeting.webp"
|
||||
alt="Persönliches Beratungsgespräch mit Notizen am Tisch"
|
||||
radius="lg"
|
||||
className="photo"
|
||||
loading="lazy"
|
||||
h={300}
|
||||
fit="cover"
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
|
@ -631,14 +604,10 @@ export default function StartPage() {
|
|||
</Button>
|
||||
</Box>
|
||||
<Box style={{ flex: 5 }} w={{ base: "100%", md: "auto" }}>
|
||||
<Image
|
||||
<PhotoImage
|
||||
src="/photos/church-pews.webp"
|
||||
alt="Ruhiger Kirchenraum mit leeren Bänken"
|
||||
radius="lg"
|
||||
className="photo"
|
||||
loading="lazy"
|
||||
h={260}
|
||||
fit="cover"
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
|
|
|||
|
|
@ -1,43 +1,31 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Alert, Code, Table, Text, Title } from "@mantine/core";
|
||||
import { IconAlertCircle } from "@tabler/icons-react";
|
||||
import { ApiError, api, type Run } from "../api";
|
||||
import { Table, Text, Title } from "@mantine/core";
|
||||
import dayjs from "dayjs";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api } from "../api";
|
||||
import { formatApiError } from "../utils";
|
||||
import ApprovalsTab from "../components/ApprovalsTab";
|
||||
import { ErrorAlert } from "../components/ErrorAlert";
|
||||
import { JsonView } from "../components/JsonView";
|
||||
import StatusBadge from "../components/StatusBadge";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function TasksPage() {
|
||||
const [runs, setRuns] = useState<{ flow: string; run: Run }[]>([]);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const flows = await api.listFlows();
|
||||
const all: { flow: string; run: Run }[] = [];
|
||||
for (const f of flows) {
|
||||
const rs = await api.listRuns(f.id);
|
||||
for (const r of rs) all.push({ flow: f.name, run: r });
|
||||
}
|
||||
all.sort((a, b) => (a.run.created_at < b.run.created_at ? 1 : -1));
|
||||
setRuns(all);
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const runsQuery = useQuery({
|
||||
queryKey: ["all-runs"],
|
||||
queryFn: api.listAllRuns,
|
||||
});
|
||||
const runs = runsQuery.data ?? [];
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
if (runsQuery.error) setError(formatApiError(runsQuery.error));
|
||||
}, [runsQuery.error]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title order={2} mb="md">
|
||||
Aufgaben
|
||||
</Title>
|
||||
{error && (
|
||||
<Alert color="red" icon={<IconAlertCircle size={18} />} mb="md">
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
<ErrorAlert message={error} />
|
||||
<Title order={3} mb="xs">
|
||||
Eigene Läufe
|
||||
</Title>
|
||||
|
|
@ -52,17 +40,23 @@ export default function TasksPage() {
|
|||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Flow</Table.Th>
|
||||
<Table.Th>Ergebnis</Table.Th>
|
||||
<Table.Th>Erstellt</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{runs.map(({ flow, run }) => (
|
||||
{runs.map(({ flow_name, run }) => (
|
||||
<Table.Tr key={run.id}>
|
||||
<Table.Td>
|
||||
<StatusBadge status={run.status} />
|
||||
</Table.Td>
|
||||
<Table.Td>{flow}</Table.Td>
|
||||
<Table.Td>{flow_name}</Table.Td>
|
||||
<Table.Td>
|
||||
<Code>{run.output?.result ?? run.output?.error ?? "–"}</Code>
|
||||
<JsonView
|
||||
value={run.output?.result ?? run.output?.error ?? "–"}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{dayjs(run.created_at).format("DD.MM.YYYY HH:mm")}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import Form from "@rjsf/mantine";
|
||||
import validator from "@rjsf/validator-ajv8";
|
||||
import type { RJSFSchema, UiSchema } from "@rjsf/utils";
|
||||
|
|
@ -11,57 +11,67 @@ import {
|
|||
Textarea,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { IconPlayerPlay } from "@tabler/icons-react";
|
||||
import { ApiError, api, type FlowDefinition, type InputSchema, type Run } from "../api";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ApiError, api, type Run } from "../api";
|
||||
import { RunsTable } from "../components/RunsTable";
|
||||
import { ErrorAlert } from "../components/ErrorAlert";
|
||||
import { formatApiError } from "../utils";
|
||||
|
||||
export default function WorkflowsPage() {
|
||||
const [flows, setFlows] = useState<FlowDefinition[]>([]);
|
||||
const queryClient = useQueryClient();
|
||||
const [flowId, setFlowId] = useState("");
|
||||
const [schema, setSchema] = useState<InputSchema | null>(null);
|
||||
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
||||
const [freeText, setFreeText] = useState("");
|
||||
const [runs, setRuns] = useState<Run[]>([]);
|
||||
const [result, setResult] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const fail = useCallback((e: unknown) => {
|
||||
setError(formatApiError(e));
|
||||
}, []);
|
||||
|
||||
const flowsQuery = useQuery({
|
||||
queryKey: ["flows"],
|
||||
queryFn: api.listFlows,
|
||||
});
|
||||
const flows = flowsQuery.data ?? [];
|
||||
useEffect(() => {
|
||||
api
|
||||
.listFlows()
|
||||
.then((list) => {
|
||||
setFlows(list);
|
||||
const first = list[0];
|
||||
if (first) setFlowId((prev) => prev || first.id);
|
||||
})
|
||||
.catch(fail);
|
||||
}, [fail]);
|
||||
|
||||
const first = flows[0];
|
||||
if (first) setFlowId((prev) => prev || first.id);
|
||||
}, [flows]);
|
||||
useEffect(() => {
|
||||
if (!flowId) return;
|
||||
setError("");
|
||||
if (flowsQuery.error) setError(formatApiError(flowsQuery.error));
|
||||
}, [flowsQuery.error]);
|
||||
|
||||
const schemaQuery = useQuery({
|
||||
queryKey: ["input-schema", flowId],
|
||||
queryFn: () => api.getInputSchema(flowId),
|
||||
enabled: !!flowId,
|
||||
retry: false,
|
||||
});
|
||||
const runsQuery = useQuery({
|
||||
queryKey: ["runs", flowId],
|
||||
queryFn: () => api.listRuns(flowId),
|
||||
enabled: !!flowId,
|
||||
});
|
||||
const schema = schemaQuery.data ?? null;
|
||||
const runs = runsQuery.data ?? [];
|
||||
useEffect(() => {
|
||||
setFormData({});
|
||||
setResult("");
|
||||
Promise.all([api.getInputSchema(flowId), api.listRuns(flowId)])
|
||||
.then(([s, r]) => {
|
||||
setSchema(s);
|
||||
setFormData({});
|
||||
setRuns(r);
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
// Flows without a deployed version have no schema yet.
|
||||
if (e instanceof ApiError && e.status === 404) {
|
||||
setSchema(null);
|
||||
setRuns([]);
|
||||
setError("Für diesen Flow ist noch keine Version deployed.");
|
||||
} else fail(e);
|
||||
});
|
||||
}, [flowId, fail]);
|
||||
const e = schemaQuery.error;
|
||||
if (!e) {
|
||||
setError("");
|
||||
return;
|
||||
}
|
||||
// Flows without a deployed version have no schema yet.
|
||||
if (e instanceof ApiError && e.status === 404) {
|
||||
setError("Für diesen Flow ist noch keine Version deployed.");
|
||||
} else {
|
||||
setError(formatApiError(e));
|
||||
}
|
||||
}, [flowId, schemaQuery.error]);
|
||||
useEffect(() => {
|
||||
if (runsQuery.error) setError(formatApiError(runsQuery.error));
|
||||
}, [runsQuery.error]);
|
||||
|
||||
async function onRun(input: unknown) {
|
||||
if (!flowId) return;
|
||||
|
|
@ -69,11 +79,21 @@ export default function WorkflowsPage() {
|
|||
setError("");
|
||||
setResult("");
|
||||
try {
|
||||
const run = await api.runFlow(flowId, input);
|
||||
setRuns((prev) => [run, ...prev]);
|
||||
setResult(run.output?.result ?? run.output?.error ?? run.status);
|
||||
const run: Run = await api.runFlow(flowId, input);
|
||||
setResult(
|
||||
typeof run.output?.result === "string"
|
||||
? run.output.result
|
||||
: (run.output?.error ?? run.status),
|
||||
);
|
||||
notifications.show({
|
||||
title: "Lauf gestartet",
|
||||
message: `Status: ${run.status}`,
|
||||
color: "green",
|
||||
});
|
||||
await queryClient.invalidateQueries({ queryKey: ["runs", flowId] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["all-runs"] });
|
||||
} catch (e) {
|
||||
fail(e);
|
||||
setError(formatApiError(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue