Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a65e2eb92c | |||
| b886246808 | |||
| 3bb4598d04 | |||
| d23990e322 | |||
| 33cf261777 | |||
| 56f1edd613 | |||
| fdf2d741a1 | |||
| 9ceb11e7f9 | |||
| 6c92b6358d | |||
| 520f917723 | |||
| d1dd77bc1e | |||
| 0be45b6513 | |||
| 4b1274e34a | |||
| 2f66fff993 | |||
| bcae59626f | |||
| f851305d78 |
+15
-11
@@ -14,6 +14,14 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
run: npm install -g pnpm@11.18.0
|
||||||
|
|
||||||
|
- name: Security audit (fails on any prod or high/critical finding)
|
||||||
|
run: |
|
||||||
|
pnpm audit --prod
|
||||||
|
pnpm audit --audit-level high
|
||||||
|
|
||||||
- name: Install Docker CLI
|
- name: Install Docker CLI
|
||||||
run: |
|
run: |
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
@@ -30,33 +38,29 @@ jobs:
|
|||||||
FULL_SHA=$(git rev-parse HEAD)
|
FULL_SHA=$(git rev-parse HEAD)
|
||||||
IMAGE="git.kubebase.de/${{ gitea.repository }}"
|
IMAGE="git.kubebase.de/${{ gitea.repository }}"
|
||||||
DATE_STAMP=$(date -u +"%Y%m%d")
|
DATE_STAMP=$(date -u +"%Y%m%d")
|
||||||
|
VERSION="dev-$(date -u +"%Y%m%d-%H%M")"
|
||||||
|
TAGS="-t ${IMAGE}:latest"
|
||||||
if [ "${{ gitea.ref_type }}" = "tag" ]; then
|
if [ "${{ gitea.ref_type }}" = "tag" ]; then
|
||||||
VERSION="${{ gitea.ref_name }}"
|
VERSION="${{ gitea.ref_name }}"
|
||||||
VERSION_TAG="${{ gitea.ref_name }}"
|
TAGS="${TAGS} -t ${IMAGE}:${VERSION}"
|
||||||
else
|
|
||||||
VERSION="dev-$(date -u +"%Y%m%d-%H%M")"
|
|
||||||
VERSION_TAG="nightly-${DATE_STAMP}"
|
|
||||||
fi
|
fi
|
||||||
TAGS="-t ${IMAGE}:sha-${SHA} -t ${IMAGE}:latest -t ${IMAGE}:${VERSION_TAG}"
|
|
||||||
docker build --no-cache \
|
docker build --no-cache \
|
||||||
--build-arg COMMIT_SHA="$FULL_SHA" \
|
--build-arg COMMIT_SHA="$FULL_SHA" \
|
||||||
--build-arg BUILD_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
--build-arg BUILD_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
||||||
--build-arg VERSION="$VERSION" \
|
--build-arg VERSION="$VERSION" \
|
||||||
$TAGS .
|
$TAGS .
|
||||||
docker push "${IMAGE}:sha-${SHA}"
|
|
||||||
docker push "${IMAGE}:latest"
|
docker push "${IMAGE}:latest"
|
||||||
docker push "${IMAGE}:${VERSION_TAG}"
|
if [ "${{ gitea.ref_type }}" = "tag" ]; then
|
||||||
|
docker push "${IMAGE}:${VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Update k8s manifest in admin/apps
|
- name: Update k8s manifest in admin/apps
|
||||||
|
if: gitea.ref_type == 'tag'
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
SHA=$(git rev-parse --short HEAD)
|
SHA=$(git rev-parse --short HEAD)
|
||||||
if [ "${{ gitea.ref_type }}" = "tag" ]; then
|
|
||||||
NEWTAG="${{ gitea.ref_name }}"
|
NEWTAG="${{ gitea.ref_name }}"
|
||||||
else
|
|
||||||
NEWTAG="sha-${SHA}"
|
|
||||||
fi
|
|
||||||
git clone "https://admin:${GITEA_TOKEN}@git.kubebase.de/admin/apps.git" /tmp/apps
|
git clone "https://admin:${GITEA_TOKEN}@git.kubebase.de/admin/apps.git" /tmp/apps
|
||||||
cd /tmp/apps
|
cd /tmp/apps
|
||||||
cd apps/system/toolrate/overlays/k3s
|
cd apps/system/toolrate/overlays/k3s
|
||||||
|
|||||||
@@ -47,3 +47,6 @@ Thumbs.db
|
|||||||
# Replit
|
# Replit
|
||||||
.cache/
|
.cache/
|
||||||
.local/
|
.local/
|
||||||
|
|
||||||
|
# Generated release docs (produced by scripts/src/sync-release-docs.mjs during build)
|
||||||
|
/artifacts/toolrate/public/docs/
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
auto-install-peers=false
|
auto-install-peers=false
|
||||||
strict-peer-dependencies=false
|
strict-peer-dependencies=false
|
||||||
|
save-exact=true
|
||||||
|
|||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
FROM node:24-alpine AS builder
|
FROM node:24.18.1-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN npm install -g pnpm@10.26.1
|
RUN npm install -g pnpm@11.18.0
|
||||||
|
|
||||||
# 1. Alle Projektdateien in den Container bringen
|
# 1. Alle Projektdateien in den Container bringen
|
||||||
COPY . .
|
COPY . .
|
||||||
@@ -17,10 +17,10 @@ ENV PORT=8080
|
|||||||
|
|
||||||
RUN pnpm -r --if-present run build
|
RUN pnpm -r --if-present run build
|
||||||
|
|
||||||
FROM node:24-alpine AS runner
|
FROM node:24.18.1-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN npm install -g pnpm@10.26.1
|
RUN npm install -g pnpm@11.18.0
|
||||||
|
|
||||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||||
COPY lib/db/package.json lib/db/
|
COPY lib/db/package.json lib/db/
|
||||||
|
|||||||
@@ -12,30 +12,29 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@workspace/api-zod": "workspace:*",
|
"@workspace/api-zod": "workspace:*",
|
||||||
"@workspace/db": "workspace:*",
|
"@workspace/db": "workspace:*",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "3.0.3",
|
||||||
"connect-pg-simple": "^10.0.0",
|
"connect-pg-simple": "10.0.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "1.4.7",
|
||||||
"cors": "^2.8.6",
|
"cors": "2.8.6",
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"express": "^5.2.1",
|
"express": "5.2.1",
|
||||||
"express-rate-limit": "^8.6.1",
|
"express-rate-limit": "8.6.1",
|
||||||
"express-session": "^1.19.0",
|
"express-session": "1.19.0",
|
||||||
"openid-client": "^5.7.1",
|
"openid-client": "6.8.4",
|
||||||
"pino": "^9.14.0",
|
"pino": "10.3.1",
|
||||||
"pino-http": "^10.5.0",
|
"pino-http": "11.0.0",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^3.0.0",
|
"@types/connect-pg-simple": "7.0.3",
|
||||||
"@types/connect-pg-simple": "^7.0.3",
|
"@types/cookie-parser": "1.4.10",
|
||||||
"@types/cookie-parser": "^1.4.10",
|
"@types/cors": "2.8.19",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/express": "5.0.6",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express-session": "1.19.0",
|
||||||
"@types/express-session": "^1.19.0",
|
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"esbuild": "0.27.3",
|
"esbuild": "0.28.1",
|
||||||
"esbuild-plugin-pino": "^2.3.3",
|
"esbuild-plugin-pino": "2.3.3",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "13.1.3",
|
||||||
"thread-stream": "3.1.0"
|
"thread-stream": "4.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
import type { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
|
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
|
||||||
|
|
||||||
|
export function getCsrfToken(req: Request): string {
|
||||||
|
if (!req.session.csrfToken) {
|
||||||
|
req.session.csrfToken = crypto.randomBytes(24).toString("hex");
|
||||||
|
}
|
||||||
|
return req.session.csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function csrfProtection(req: Request, res: Response, next: NextFunction): void {
|
||||||
|
if (SAFE_METHODS.has(req.method.toUpperCase())) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const provided = req.headers["x-csrf-token"];
|
||||||
|
const token = getCsrfToken(req);
|
||||||
|
if (typeof provided === "string" && provided && provided === token) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(403).json({ error: "CSRF token missing or invalid" });
|
||||||
|
}
|
||||||
@@ -1,5 +1,16 @@
|
|||||||
import { Router, type IRouter, type Request } from "express";
|
import { Router, type IRouter, type Request } from "express";
|
||||||
import { Issuer, generators, type Client } from "openid-client";
|
import {
|
||||||
|
discovery,
|
||||||
|
randomPKCECodeVerifier,
|
||||||
|
calculatePKCECodeChallenge,
|
||||||
|
randomState,
|
||||||
|
buildAuthorizationUrl,
|
||||||
|
authorizationCodeGrant,
|
||||||
|
fetchUserInfo,
|
||||||
|
buildEndSessionUrl,
|
||||||
|
skipSubjectCheck,
|
||||||
|
type Configuration,
|
||||||
|
} from "openid-client";
|
||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
import { eq, and, inArray, isNull } from "drizzle-orm";
|
import { eq, and, inArray, isNull } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -8,10 +19,11 @@ import { logger } from "../lib/logger";
|
|||||||
import { writeAuditLog } from "../lib/audit";
|
import { writeAuditLog } from "../lib/audit";
|
||||||
import { loginRateLimit, passwordRateLimit } from "../lib/rate-limit";
|
import { loginRateLimit, passwordRateLimit } from "../lib/rate-limit";
|
||||||
import { getEntitlements, requireFeature } from "../middleware/feature";
|
import { getEntitlements, requireFeature } from "../middleware/feature";
|
||||||
|
import { getCsrfToken } from "../middleware/csrf";
|
||||||
|
|
||||||
const router: IRouter = Router();
|
const router: IRouter = Router();
|
||||||
|
|
||||||
let cachedClient: Client | null = null;
|
let cachedConfig: Configuration | null = null;
|
||||||
|
|
||||||
function isOidcConfigured(): boolean {
|
function isOidcConfigured(): boolean {
|
||||||
return !!(
|
return !!(
|
||||||
@@ -38,8 +50,8 @@ function isSafeReturnTo(value: string): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getClient(): Promise<Client | null> {
|
async function getClient(): Promise<Configuration | null> {
|
||||||
if (cachedClient) return cachedClient;
|
if (cachedConfig) return cachedConfig;
|
||||||
|
|
||||||
const keycloakUrl = process.env.KEYCLOAK_URL;
|
const keycloakUrl = process.env.KEYCLOAK_URL;
|
||||||
const realm = process.env.KEYCLOAK_REALM;
|
const realm = process.env.KEYCLOAK_REALM;
|
||||||
@@ -51,14 +63,9 @@ async function getClient(): Promise<Client | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const issuerUrl = `${keycloakUrl}/realms/${realm}`;
|
const issuerUrl = new URL(`${keycloakUrl}/realms/${realm}`);
|
||||||
const issuer = await Issuer.discover(issuerUrl);
|
cachedConfig = await discovery(issuerUrl, clientId, clientSecret);
|
||||||
cachedClient = new issuer.Client({
|
return cachedConfig;
|
||||||
client_id: clientId,
|
|
||||||
client_secret: clientSecret,
|
|
||||||
response_types: ["code"],
|
|
||||||
});
|
|
||||||
return cachedClient;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ err }, "Failed to discover Keycloak issuer");
|
logger.error({ err }, "Failed to discover Keycloak issuer");
|
||||||
return null;
|
return null;
|
||||||
@@ -109,6 +116,10 @@ router.get("/auth/mode", (_req, res): void => {
|
|||||||
res.json({ mode: isOidcConfigured() ? "oidc" : "local" });
|
res.json({ mode: isOidcConfigured() ? "oidc" : "local" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get("/auth/csrf", (req, res): void => {
|
||||||
|
res.json({ token: getCsrfToken(req) });
|
||||||
|
});
|
||||||
|
|
||||||
router.post("/auth/login", loginRateLimit, async (req, res): Promise<void> => {
|
router.post("/auth/login", loginRateLimit, async (req, res): Promise<void> => {
|
||||||
if (isOidcConfigured()) {
|
if (isOidcConfigured()) {
|
||||||
res.status(400).json({ error: "Use OIDC login when Keycloak is configured." });
|
res.status(400).json({ error: "Use OIDC login when Keycloak is configured." });
|
||||||
@@ -171,9 +182,9 @@ router.get("/auth/login", async (req, res): Promise<void> => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeVerifier = generators.codeVerifier();
|
const codeVerifier = randomPKCECodeVerifier();
|
||||||
const codeChallenge = generators.codeChallenge(codeVerifier);
|
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
||||||
const state = generators.state();
|
const state = randomState();
|
||||||
|
|
||||||
req.session.codeVerifier = codeVerifier;
|
req.session.codeVerifier = codeVerifier;
|
||||||
req.session.oidcState = state;
|
req.session.oidcState = state;
|
||||||
@@ -182,7 +193,7 @@ router.get("/auth/login", async (req, res): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const redirectUri = `${getBaseUrl(req)}/api/auth/callback`;
|
const redirectUri = `${getBaseUrl(req)}/api/auth/callback`;
|
||||||
const url = client.authorizationUrl({
|
const url = buildAuthorizationUrl(client, {
|
||||||
scope: "openid email profile",
|
scope: "openid email profile",
|
||||||
code_challenge: codeChallenge,
|
code_challenge: codeChallenge,
|
||||||
code_challenge_method: "S256",
|
code_challenge_method: "S256",
|
||||||
@@ -190,7 +201,7 @@ router.get("/auth/login", async (req, res): Promise<void> => {
|
|||||||
state,
|
state,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.redirect(url);
|
res.redirect(url.href);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/auth/callback", async (req, res): Promise<void> => {
|
router.get("/auth/callback", async (req, res): Promise<void> => {
|
||||||
@@ -216,13 +227,13 @@ router.get("/auth/callback", async (req, res): Promise<void> => {
|
|||||||
const redirectUri = `${getBaseUrl(req)}/api/auth/callback`;
|
const redirectUri = `${getBaseUrl(req)}/api/auth/callback`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params = client.callbackParams(req);
|
const tokens = await authorizationCodeGrant(
|
||||||
const tokenSet = await client.callback(redirectUri, params, {
|
client,
|
||||||
code_verifier: codeVerifier,
|
new URL(req.originalUrl ?? "/", getBaseUrl(req)),
|
||||||
state,
|
{ pkceCodeVerifier: codeVerifier, expectedState: state },
|
||||||
});
|
);
|
||||||
|
|
||||||
const userinfo = await client.userinfo(tokenSet.access_token!);
|
const userinfo = await fetchUserInfo(client, tokens.access_token, skipSubjectCheck);
|
||||||
const dbUser = await upsertUserFromOidc(userinfo);
|
const dbUser = await upsertUserFromOidc(userinfo);
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
@@ -254,9 +265,9 @@ router.get("/auth/logout", async (req, res): Promise<void> => {
|
|||||||
req.session.destroy(() => {});
|
req.session.destroy(() => {});
|
||||||
|
|
||||||
const client = await getClient();
|
const client = await getClient();
|
||||||
if (client && client.issuer.metadata.end_session_endpoint) {
|
if (client && client.serverMetadata().end_session_endpoint) {
|
||||||
const logoutUrl = client.endSessionUrl({ post_logout_redirect_uri: getBaseUrl(req) });
|
const logoutUrl = buildEndSessionUrl(client, { post_logout_redirect_uri: getBaseUrl(req) });
|
||||||
res.redirect(logoutUrl);
|
res.redirect(logoutUrl.href);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +298,7 @@ router.get("/auth/password-redirect", async (req, res): Promise<void> => {
|
|||||||
res.json({ url: null });
|
res.json({ url: null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const realm = client.issuer.metadata.issuer ?? "";
|
const realm = client.serverMetadata().issuer ?? "";
|
||||||
res.json({ url: `${realm}/account/password` });
|
res.json({ url: `${realm}/account/password` });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ import usersRouter from "./users";
|
|||||||
import auditRouter from "./audit";
|
import auditRouter from "./audit";
|
||||||
import costsRouter from "./costs";
|
import costsRouter from "./costs";
|
||||||
import adminRouter from "./admin";
|
import adminRouter from "./admin";
|
||||||
|
import { csrfProtection } from "../middleware/csrf";
|
||||||
|
|
||||||
const router: IRouter = Router();
|
const router: IRouter = Router();
|
||||||
|
|
||||||
|
router.use(csrfProtection);
|
||||||
|
|
||||||
router.use(authRouter);
|
router.use(authRouter);
|
||||||
router.use(healthRouter);
|
router.use(healthRouter);
|
||||||
router.use(toolsRouter);
|
router.use(toolsRouter);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Router, type IRouter } from "express";
|
import { Router, type IRouter } from "express";
|
||||||
import { eq, desc, asc, sql, and, not, isNull, inArray } from "drizzle-orm";
|
import { eq, desc, asc, sql, and, not, isNull, inArray, type SQL } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, toolsTable, ratingsTable, toolRelationsTable } from "@workspace/db";
|
import { db, toolsTable, ratingsTable, toolRelationsTable } from "@workspace/db";
|
||||||
import {
|
import {
|
||||||
@@ -57,21 +57,23 @@ router.get("/tools", async (req, res): Promise<void> => {
|
|||||||
const tagList = (tags ?? "").split(",").map((t) => t.trim()).filter(Boolean);
|
const tagList = (tags ?? "").split(",").map((t) => t.trim()).filter(Boolean);
|
||||||
const featureList = (features ?? "").split(",").map((f) => f.trim()).filter(Boolean);
|
const featureList = (features ?? "").split(",").map((f) => f.trim()).filter(Boolean);
|
||||||
|
|
||||||
let query = db.select().from(toolsTable).where(isNull(toolsTable.deletedAt)).$dynamic();
|
const conditions: SQL[] = [isNull(toolsTable.deletedAt)];
|
||||||
if (category) {
|
if (category) {
|
||||||
query = query.where(eq(toolsTable.category, category));
|
conditions.push(eq(toolsTable.category, category));
|
||||||
}
|
}
|
||||||
if (search) {
|
if (search) {
|
||||||
const escaped = search.replace(/[%_\\]/g, (m) => `\\${m}`);
|
const escaped = search.replace(/[%_\\]/g, (m) => `\\${m}`);
|
||||||
query = query.where(sql`${toolsTable.name} ilike ${`%${escaped}%`} escape '\\'`);
|
conditions.push(sql`${toolsTable.name} ilike ${`%${escaped}%`} escape '\\'`);
|
||||||
}
|
}
|
||||||
if (tagList.length > 0) {
|
if (tagList.length > 0) {
|
||||||
query = query.where(sql`${toolsTable.tags} @> ARRAY[${sql.join(tagList.map((t) => sql`${t}`), sql`, `)}]::text[]`);
|
conditions.push(sql`${toolsTable.tags} @> ARRAY[${sql.join(tagList.map((t) => sql`${t}`), sql`, `)}]::text[]`);
|
||||||
}
|
}
|
||||||
if (featureList.length > 0) {
|
if (featureList.length > 0) {
|
||||||
query = query.where(sql`${toolsTable.features} @> ARRAY[${sql.join(featureList.map((f) => sql`${f}`), sql`, `)}]::text[]`);
|
conditions.push(sql`${toolsTable.features} @> ARRAY[${sql.join(featureList.map((f) => sql`${f}`), sql`, `)}]::text[]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const query = db.select().from(toolsTable).where(and(...conditions));
|
||||||
|
|
||||||
const tools = await query.orderBy(desc(toolsTable.createdAt));
|
const tools = await query.orderBy(desc(toolsTable.createdAt));
|
||||||
|
|
||||||
const toolIds = tools.map((t) => t.id);
|
const toolIds = tools.map((t) => t.id);
|
||||||
|
|||||||
+1
@@ -14,5 +14,6 @@ declare module "express-session" {
|
|||||||
codeVerifier?: string;
|
codeVerifier?: string;
|
||||||
returnTo?: string;
|
returnTo?: string;
|
||||||
oidcState?: string;
|
oidcState?: string;
|
||||||
|
csrfToken?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,34 +10,34 @@
|
|||||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "5.7.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@radix-ui/react-accordion": "1.2.20",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "1.1.23",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
"@radix-ui/react-aspect-ratio": "1.1.15",
|
||||||
"@radix-ui/react-avatar": "^1.1.11",
|
"@radix-ui/react-avatar": "1.2.6",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "1.3.11",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "1.1.20",
|
||||||
"@radix-ui/react-context-menu": "^2.2.16",
|
"@radix-ui/react-context-menu": "2.3.7",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "1.1.23",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "2.1.24",
|
||||||
"@radix-ui/react-hover-card": "^1.1.15",
|
"@radix-ui/react-hover-card": "1.1.23",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "2.1.15",
|
||||||
"@radix-ui/react-menubar": "^1.1.16",
|
"@radix-ui/react-menubar": "1.1.24",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
"@radix-ui/react-navigation-menu": "1.2.22",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "1.1.23",
|
||||||
"@radix-ui/react-progress": "^1.1.8",
|
"@radix-ui/react-progress": "1.1.16",
|
||||||
"@radix-ui/react-radio-group": "^1.3.8",
|
"@radix-ui/react-radio-group": "1.4.7",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "1.2.18",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "2.3.7",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "1.1.15",
|
||||||
"@radix-ui/react-slider": "^1.3.6",
|
"@radix-ui/react-slider": "1.4.7",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "1.3.3",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "1.3.7",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "1.1.21",
|
||||||
"@radix-ui/react-toast": "^1.2.15",
|
"@radix-ui/react-toast": "1.2.23",
|
||||||
"@radix-ui/react-toggle": "^1.1.10",
|
"@radix-ui/react-toggle": "1.1.18",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
"@radix-ui/react-toggle-group": "1.1.19",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "1.2.16",
|
||||||
"@replit/vite-plugin-cartographer": "catalog:",
|
"@replit/vite-plugin-cartographer": "catalog:",
|
||||||
"@replit/vite-plugin-runtime-error-modal": "catalog:",
|
"@replit/vite-plugin-runtime-error-modal": "catalog:",
|
||||||
"@tailwindcss/vite": "catalog:",
|
"@tailwindcss/vite": "catalog:",
|
||||||
@@ -45,29 +45,30 @@
|
|||||||
"@types/react": "catalog:",
|
"@types/react": "catalog:",
|
||||||
"@types/react-dom": "catalog:",
|
"@types/react-dom": "catalog:",
|
||||||
"@vitejs/plugin-react": "catalog:",
|
"@vitejs/plugin-react": "catalog:",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "5.0.0",
|
||||||
"class-variance-authority": "catalog:",
|
"class-variance-authority": "catalog:",
|
||||||
"clsx": "catalog:",
|
"clsx": "catalog:",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "1.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "4.4.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "8.6.0",
|
||||||
"fast-glob": "^3.3.3",
|
"fast-glob": "3.3.3",
|
||||||
"framer-motion": "catalog:",
|
"framer-motion": "catalog:",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"lucide-react": "catalog:",
|
"lucide-react": "catalog:",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
"react-day-picker": "^9.14.0",
|
"react-day-picker": "10.0.1",
|
||||||
"react-dom": "catalog:",
|
"react-dom": "catalog:",
|
||||||
"react-hook-form": "^7.75.0",
|
"react-hook-form": "7.84.0",
|
||||||
"react-resizable-panels": "^2.1.9",
|
"react-is": "19.2.8",
|
||||||
"recharts": "^2.15.4",
|
"react-resizable-panels": "4.12.2",
|
||||||
"sonner": "^2.0.7",
|
"recharts": "3.10.1",
|
||||||
|
"sonner": "2.0.7",
|
||||||
"tailwind-merge": "catalog:",
|
"tailwind-merge": "catalog:",
|
||||||
"tailwindcss": "catalog:",
|
"tailwindcss": "catalog:",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "1.0.7",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "1.4.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "1.1.2",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ function Calendar({
|
|||||||
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
|
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
|
||||||
defaultClassNames.caption_label
|
defaultClassNames.caption_label
|
||||||
),
|
),
|
||||||
table: "w-full border-collapse",
|
month_grid: "w-full border-collapse",
|
||||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||||
weekday: cn(
|
weekday: cn(
|
||||||
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
|
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as RechartsPrimitive from "recharts"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
const THEMES = { light: "", dark: ".dark" } as const
|
const THEMES = { light: "", dark: ".dark" } as const
|
||||||
|
|
||||||
export type ChartConfig = {
|
export type ChartConfig = {
|
||||||
@@ -101,7 +102,7 @@ const ChartTooltip = RechartsPrimitive.Tooltip
|
|||||||
|
|
||||||
const ChartTooltipContent = React.forwardRef<
|
const ChartTooltipContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
RechartsPrimitive.TooltipContentProps &
|
||||||
React.ComponentProps<"div"> & {
|
React.ComponentProps<"div"> & {
|
||||||
hideLabel?: boolean
|
hideLabel?: boolean
|
||||||
hideIndicator?: boolean
|
hideIndicator?: boolean
|
||||||
@@ -191,7 +192,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.dataKey}
|
key={typeof item.dataKey === "string" || typeof item.dataKey === "number" ? item.dataKey : index}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||||
indicator === "dot" && "items-center"
|
indicator === "dot" && "items-center"
|
||||||
@@ -260,7 +261,7 @@ const ChartLegend = RechartsPrimitive.Legend
|
|||||||
const ChartLegendContent = React.forwardRef<
|
const ChartLegendContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<"div"> &
|
React.ComponentProps<"div"> &
|
||||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
Pick<RechartsPrimitive.DefaultLegendContentProps, "payload" | "verticalAlign"> & {
|
||||||
hideIcon?: boolean
|
hideIcon?: boolean
|
||||||
nameKey?: string
|
nameKey?: string
|
||||||
}
|
}
|
||||||
@@ -317,6 +318,7 @@ const ChartLegendContent = React.forwardRef<
|
|||||||
)
|
)
|
||||||
ChartLegendContent.displayName = "ChartLegend"
|
ChartLegendContent.displayName = "ChartLegend"
|
||||||
|
|
||||||
|
// Helper to extract item config from a payload.
|
||||||
function getPayloadConfigFromPayload(
|
function getPayloadConfigFromPayload(
|
||||||
config: ChartConfig,
|
config: ChartConfig,
|
||||||
payload: unknown,
|
payload: unknown,
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import { cn } from "@/lib/utils"
|
|||||||
const ResizablePanelGroup = ({
|
const ResizablePanelGroup = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
}: React.ComponentProps<typeof ResizablePrimitive.Group>) => (
|
||||||
<ResizablePrimitive.PanelGroup
|
<ResizablePrimitive.Group
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
"flex h-full w-full data-[group-orientation=vertical]:flex-col",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -24,12 +24,12 @@ const ResizableHandle = ({
|
|||||||
withHandle,
|
withHandle,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
}: React.ComponentProps<typeof ResizablePrimitive.Separator> & {
|
||||||
withHandle?: boolean
|
withHandle?: boolean
|
||||||
}) => (
|
}) => (
|
||||||
<ResizablePrimitive.PanelResizeHandle
|
<ResizablePrimitive.Separator
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[group-orientation=vertical]:h-px data-[group-orientation=vertical]:w-full data-[group-orientation=vertical]:after:left-0 data-[group-orientation=vertical]:after:h-1 data-[group-orientation=vertical]:after:w-full data-[group-orientation=vertical]:after:-translate-y-1/2 data-[group-orientation=vertical]:after:translate-x-0 [&[data-group-orientation=vertical]>div]:rotate-90",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -39,7 +39,7 @@ const ResizableHandle = ({
|
|||||||
<GripVertical className="h-2.5 w-2.5" />
|
<GripVertical className="h-2.5 w-2.5" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ResizablePrimitive.PanelResizeHandle>
|
</ResizablePrimitive.Separator>
|
||||||
)
|
)
|
||||||
|
|
||||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
||||||
|
|||||||
@@ -4,44 +4,44 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --config vite.config.ts --host 0.0.0.0",
|
"dev": "node ../../scripts/src/generate-docs.mjs && vite --config vite.config.ts --host 0.0.0.0",
|
||||||
"build": "vite build --config vite.config.ts",
|
"build": "node ../../scripts/src/generate-docs.mjs && vite build --config vite.config.ts",
|
||||||
"serve": "vite preview --config vite.config.ts --host 0.0.0.0",
|
"serve": "vite preview --config vite.config.ts --host 0.0.0.0",
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "5.7.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.4",
|
"@radix-ui/react-accordion": "1.2.20",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.7",
|
"@radix-ui/react-alert-dialog": "1.1.23",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.3",
|
"@radix-ui/react-aspect-ratio": "1.1.15",
|
||||||
"@radix-ui/react-avatar": "^1.1.4",
|
"@radix-ui/react-avatar": "1.2.6",
|
||||||
"@radix-ui/react-checkbox": "^1.1.5",
|
"@radix-ui/react-checkbox": "1.3.11",
|
||||||
"@radix-ui/react-collapsible": "^1.1.4",
|
"@radix-ui/react-collapsible": "1.1.20",
|
||||||
"@radix-ui/react-context-menu": "^2.2.7",
|
"@radix-ui/react-context-menu": "2.3.7",
|
||||||
"@radix-ui/react-dialog": "^1.1.7",
|
"@radix-ui/react-dialog": "1.1.23",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.7",
|
"@radix-ui/react-dropdown-menu": "2.1.24",
|
||||||
"@radix-ui/react-hover-card": "^1.1.7",
|
"@radix-ui/react-hover-card": "1.1.23",
|
||||||
"@radix-ui/react-label": "^2.1.3",
|
"@radix-ui/react-label": "2.1.15",
|
||||||
"@radix-ui/react-menubar": "^1.1.7",
|
"@radix-ui/react-menubar": "1.1.24",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.6",
|
"@radix-ui/react-navigation-menu": "1.2.22",
|
||||||
"@radix-ui/react-popover": "^1.1.7",
|
"@radix-ui/react-popover": "1.1.23",
|
||||||
"@radix-ui/react-progress": "^1.1.3",
|
"@radix-ui/react-progress": "1.1.16",
|
||||||
"@radix-ui/react-radio-group": "^1.2.4",
|
"@radix-ui/react-radio-group": "1.4.7",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.4",
|
"@radix-ui/react-scroll-area": "1.2.18",
|
||||||
"@radix-ui/react-select": "^2.1.7",
|
"@radix-ui/react-select": "2.3.7",
|
||||||
"@radix-ui/react-separator": "^1.1.3",
|
"@radix-ui/react-separator": "1.1.15",
|
||||||
"@radix-ui/react-slider": "^1.2.4",
|
"@radix-ui/react-slider": "1.4.7",
|
||||||
"@radix-ui/react-slot": "^1.2.0",
|
"@radix-ui/react-slot": "1.3.3",
|
||||||
"@radix-ui/react-switch": "^1.1.4",
|
"@radix-ui/react-switch": "1.3.7",
|
||||||
"@radix-ui/react-tabs": "^1.1.4",
|
"@radix-ui/react-tabs": "1.1.21",
|
||||||
"@radix-ui/react-toast": "^1.2.7",
|
"@radix-ui/react-toast": "1.2.23",
|
||||||
"@radix-ui/react-toggle": "^1.1.3",
|
"@radix-ui/react-toggle": "1.1.18",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.3",
|
"@radix-ui/react-toggle-group": "1.1.19",
|
||||||
"@radix-ui/react-tooltip": "^1.2.0",
|
"@radix-ui/react-tooltip": "1.2.16",
|
||||||
"@replit/vite-plugin-cartographer": "catalog:",
|
"@replit/vite-plugin-cartographer": "catalog:",
|
||||||
"@replit/vite-plugin-dev-banner": "catalog:",
|
"@replit/vite-plugin-dev-banner": "catalog:",
|
||||||
"@replit/vite-plugin-runtime-error-modal": "catalog:",
|
"@replit/vite-plugin-runtime-error-modal": "catalog:",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "0.5.20",
|
||||||
"@tailwindcss/vite": "catalog:",
|
"@tailwindcss/vite": "catalog:",
|
||||||
"@tanstack/react-query": "catalog:",
|
"@tanstack/react-query": "catalog:",
|
||||||
"@tanstack/react-virtual": "catalog:",
|
"@tanstack/react-virtual": "catalog:",
|
||||||
@@ -52,29 +52,32 @@
|
|||||||
"@workspace/api-client-react": "workspace:*",
|
"@workspace/api-client-react": "workspace:*",
|
||||||
"class-variance-authority": "catalog:",
|
"class-variance-authority": "catalog:",
|
||||||
"clsx": "catalog:",
|
"clsx": "catalog:",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "1.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "4.4.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"dompurify": "catalog:",
|
||||||
|
"embla-carousel-react": "8.6.0",
|
||||||
"framer-motion": "catalog:",
|
"framer-motion": "catalog:",
|
||||||
"i18next": "^26.3.6",
|
"i18next": "26.3.6",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"lucide-react": "catalog:",
|
"lucide-react": "catalog:",
|
||||||
"next-themes": "^0.4.6",
|
"marked": "catalog:",
|
||||||
|
"next-themes": "0.4.6",
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "10.0.1",
|
||||||
"react-dom": "catalog:",
|
"react-dom": "catalog:",
|
||||||
"react-hook-form": "^7.55.0",
|
"react-hook-form": "7.84.0",
|
||||||
"react-i18next": "^17.0.11",
|
"react-i18next": "17.0.11",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "5.7.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-is": "19.2.8",
|
||||||
"recharts": "^2.15.2",
|
"react-resizable-panels": "4.12.2",
|
||||||
"sonner": "^2.0.7",
|
"recharts": "3.10.1",
|
||||||
|
"sonner": "2.0.7",
|
||||||
"tailwind-merge": "catalog:",
|
"tailwind-merge": "catalog:",
|
||||||
"tailwindcss": "catalog:",
|
"tailwindcss": "catalog:",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "1.4.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "1.1.2",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
"wouter": "^3.3.5",
|
"wouter": "catalog:",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Switch, Route, Router as WouterRouter } from "wouter";
|
import { Switch, Route, Router as WouterRouter } from "wouter";
|
||||||
|
import { useEffect } from "react";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { I18nextProvider } from "react-i18next";
|
import { I18nextProvider } from "react-i18next";
|
||||||
import i18n from "@/i18n";
|
import i18n from "@/i18n";
|
||||||
|
import { loadCsrfToken } from "@/lib/csrf";
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
|
|
||||||
@@ -18,6 +20,7 @@ import Trash from "@/pages/trash";
|
|||||||
import Compare from "@/pages/compare";
|
import Compare from "@/pages/compare";
|
||||||
import Watchlist from "@/pages/watchlist";
|
import Watchlist from "@/pages/watchlist";
|
||||||
import Login from "@/pages/login";
|
import Login from "@/pages/login";
|
||||||
|
import Docs from "@/pages/docs";
|
||||||
import NotFound from "@/pages/not-found";
|
import NotFound from "@/pages/not-found";
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
@@ -44,12 +47,16 @@ function Router() {
|
|||||||
<Route path="/admin" component={Admin} />
|
<Route path="/admin" component={Admin} />
|
||||||
<Route path="/admin/redundancy" component={Redundancy} />
|
<Route path="/admin/redundancy" component={Redundancy} />
|
||||||
<Route path="/trash" component={Trash} />
|
<Route path="/trash" component={Trash} />
|
||||||
|
<Route path="/docs/*?" component={Docs} />
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
useEffect(() => {
|
||||||
|
void loadCsrfToken();
|
||||||
|
}, []);
|
||||||
return (
|
return (
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { HelpCircle } from "lucide-react";
|
||||||
|
import { Link } from "wouter";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
|
||||||
|
export function FieldHelp({
|
||||||
|
schema,
|
||||||
|
field,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
schema: string;
|
||||||
|
field: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const label = children ?? field;
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Link
|
||||||
|
href={`/docs/reference/schemas/${schema}#${field}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
aria-label={`Help: ${label}`}
|
||||||
|
data-testid={`help-${schema}-${field}`}
|
||||||
|
className="inline-flex shrink-0 text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<HelpCircle className="h-3.5 w-3.5" />
|
||||||
|
</Link>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{label} — Details in der Dokumentation</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { BookOpen } from "lucide-react";
|
||||||
|
import { Link } from "wouter";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
|
||||||
|
export function GuideHelp({
|
||||||
|
guide,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
guide: string;
|
||||||
|
label: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Link
|
||||||
|
href={`/docs/handbook/${guide}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
aria-label={`Help: ${label}`}
|
||||||
|
data-testid={`guide-${guide}`}
|
||||||
|
className="inline-flex shrink-0 text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<BookOpen className="h-3.5 w-3.5" />
|
||||||
|
</Link>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{label} — Anleitung in der Dokumentation</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Link, useLocation } from "wouter";
|
import { Link, useLocation } from "wouter";
|
||||||
import { LayoutDashboard, Wrench, BarChart3, LogIn, LogOut, ShieldCheck, AlertTriangle, Search } from "lucide-react";
|
import { LayoutDashboard, Wrench, BarChart3, FileText, LogIn, LogOut, ShieldCheck, AlertTriangle, Search } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { useGetVersion, getGetVersionQueryKey } from "@workspace/api-client-react";
|
import { useGetVersion, getGetVersionQueryKey } from "@workspace/api-client-react";
|
||||||
@@ -39,6 +39,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
{ href: "/", label: t("nav.home"), icon: LayoutDashboard },
|
{ href: "/", label: t("nav.home"), icon: LayoutDashboard },
|
||||||
{ href: "/tools", label: t("nav.browseTools"), icon: Wrench },
|
{ href: "/tools", label: t("nav.browseTools"), icon: Wrench },
|
||||||
{ href: "/analytics", label: t("nav.analytics"), icon: BarChart3 },
|
{ href: "/analytics", label: t("nav.analytics"), icon: BarChart3 },
|
||||||
|
{ href: "/docs", label: t("nav.docs"), icon: FileText },
|
||||||
];
|
];
|
||||||
|
|
||||||
const adminLinks = [
|
const adminLinks = [
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ function Calendar({
|
|||||||
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
|
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
|
||||||
defaultClassNames.caption_label
|
defaultClassNames.caption_label
|
||||||
),
|
),
|
||||||
table: "w-full border-collapse",
|
month_grid: "w-full border-collapse",
|
||||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||||
weekday: cn(
|
weekday: cn(
|
||||||
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
|
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ const ChartTooltip = RechartsPrimitive.Tooltip
|
|||||||
|
|
||||||
const ChartTooltipContent = React.forwardRef<
|
const ChartTooltipContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
RechartsPrimitive.TooltipContentProps &
|
||||||
React.ComponentProps<"div"> & {
|
React.ComponentProps<"div"> & {
|
||||||
hideLabel?: boolean
|
hideLabel?: boolean
|
||||||
hideIndicator?: boolean
|
hideIndicator?: boolean
|
||||||
@@ -192,7 +192,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.dataKey}
|
key={typeof item.dataKey === "string" || typeof item.dataKey === "number" ? item.dataKey : index}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||||
indicator === "dot" && "items-center"
|
indicator === "dot" && "items-center"
|
||||||
@@ -261,7 +261,7 @@ const ChartLegend = RechartsPrimitive.Legend
|
|||||||
const ChartLegendContent = React.forwardRef<
|
const ChartLegendContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<"div"> &
|
React.ComponentProps<"div"> &
|
||||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
Pick<RechartsPrimitive.DefaultLegendContentProps, "payload" | "verticalAlign"> & {
|
||||||
hideIcon?: boolean
|
hideIcon?: boolean
|
||||||
nameKey?: string
|
nameKey?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import { cn } from "@/lib/utils"
|
|||||||
const ResizablePanelGroup = ({
|
const ResizablePanelGroup = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
}: React.ComponentProps<typeof ResizablePrimitive.Group>) => (
|
||||||
<ResizablePrimitive.PanelGroup
|
<ResizablePrimitive.Group
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
"flex h-full w-full data-[group-orientation=vertical]:flex-col",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -24,12 +24,12 @@ const ResizableHandle = ({
|
|||||||
withHandle,
|
withHandle,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
}: React.ComponentProps<typeof ResizablePrimitive.Separator> & {
|
||||||
withHandle?: boolean
|
withHandle?: boolean
|
||||||
}) => (
|
}) => (
|
||||||
<ResizablePrimitive.PanelResizeHandle
|
<ResizablePrimitive.Separator
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[group-orientation=vertical]:h-px data-[group-orientation=vertical]:w-full data-[group-orientation=vertical]:after:left-0 data-[group-orientation=vertical]:after:h-1 data-[group-orientation=vertical]:after:w-full data-[group-orientation=vertical]:after:-translate-y-1/2 data-[group-orientation=vertical]:after:translate-x-0 [&[data-group-orientation=vertical]>div]:rotate-90",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -39,7 +39,7 @@ const ResizableHandle = ({
|
|||||||
<GripVertical className="h-2.5 w-2.5" />
|
<GripVertical className="h-2.5 w-2.5" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ResizablePrimitive.PanelResizeHandle>
|
</ResizablePrimitive.Separator>
|
||||||
)
|
)
|
||||||
|
|
||||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"trash": "Papierkorb",
|
"trash": "Papierkorb",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"redundancy": "Redundanz",
|
"redundancy": "Redundanz",
|
||||||
|
"docs": "Hilfe",
|
||||||
"search": "Tools suchen…"
|
"search": "Tools suchen…"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -127,7 +128,12 @@
|
|||||||
"relatedTools": "Ähnliche Tools",
|
"relatedTools": "Ähnliche Tools",
|
||||||
"costs": "Kosten",
|
"costs": "Kosten",
|
||||||
"addCost": "Kosten hinzufügen",
|
"addCost": "Kosten hinzufügen",
|
||||||
"recentRatings": "Letzte Bewertungen"
|
"recentRatings": "Letzte Bewertungen",
|
||||||
|
"deleteConfirmTitle": "Dieses Tool löschen?",
|
||||||
|
"deleteToTrash": "Dies verschiebt {{name}} in den Papierkorb. Es kann später wiederhergestellt werden.",
|
||||||
|
"deletePermanent": "Dies entfernt {{name}} dauerhaft inklusive aller Bewertungen. Das kann nicht rückgängig gemacht werden.",
|
||||||
|
"deleting": "Löschen…",
|
||||||
|
"deleteAction": "Löschen"
|
||||||
},
|
},
|
||||||
"compare": {
|
"compare": {
|
||||||
"title": "Tools vergleichen",
|
"title": "Tools vergleichen",
|
||||||
@@ -176,6 +182,35 @@
|
|||||||
"text": "Diese Seite existiert nicht.",
|
"text": "Diese Seite existiert nicht.",
|
||||||
"backHome": "Zurück zur Startseite"
|
"backHome": "Zurück zur Startseite"
|
||||||
},
|
},
|
||||||
|
"docs": {
|
||||||
|
"title": "Dokumentation",
|
||||||
|
"subtitle": "Version-gebundene Dokumentation — Release-Notes, Endpunkte und Datenfelder je Version.",
|
||||||
|
"backToIndex": "Alle Releases",
|
||||||
|
"noDocs": "Keine Dokumentation für diesen Pfad verfügbar.",
|
||||||
|
"version": "Version",
|
||||||
|
"latest": "Aktuell",
|
||||||
|
"repo": "Repository",
|
||||||
|
"nav": "Dokumentation",
|
||||||
|
"guides": "Handbuch",
|
||||||
|
"endpoints": "Endpunkte",
|
||||||
|
"schemas": "Datenmodelle",
|
||||||
|
"releases": "Release-Notes",
|
||||||
|
"reference": "API-Referenz",
|
||||||
|
"referenceIntro": "Automatisch aus der OpenAPI-Spezifikation generiert — alle Endpunkte und Datenfelder der aktuellen Version.",
|
||||||
|
"onThisPage": "Auf dieser Seite",
|
||||||
|
"searchPlaceholder": "Doku durchsuchen…",
|
||||||
|
"backToApp": "Zur App",
|
||||||
|
"field": "Feld",
|
||||||
|
"type": "Typ",
|
||||||
|
"required": "Pflicht",
|
||||||
|
"description": "Beschreibung",
|
||||||
|
"status": "Status",
|
||||||
|
"schema": "Schema",
|
||||||
|
"parameter": "Parameter",
|
||||||
|
"noResults": "Keine Treffer",
|
||||||
|
"fields": "Felder",
|
||||||
|
"fieldHelpHint": "Hinweis: Formular-Felder verlinken per ?-Icon direkt zu den jeweiligen Zeilen dieser Tabelle."
|
||||||
|
},
|
||||||
"command": {
|
"command": {
|
||||||
"navigate": "Navigation",
|
"navigate": "Navigation",
|
||||||
"recent": "Zuletzt besucht",
|
"recent": "Zuletzt besucht",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"trash": "Trash",
|
"trash": "Trash",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"redundancy": "Redundancy",
|
"redundancy": "Redundancy",
|
||||||
|
"docs": "Help",
|
||||||
"search": "Search tools…"
|
"search": "Search tools…"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -127,7 +128,12 @@
|
|||||||
"relatedTools": "Related Tools",
|
"relatedTools": "Related Tools",
|
||||||
"costs": "Costs",
|
"costs": "Costs",
|
||||||
"addCost": "Add Cost",
|
"addCost": "Add Cost",
|
||||||
"recentRatings": "Recent Ratings"
|
"recentRatings": "Recent Ratings",
|
||||||
|
"deleteConfirmTitle": "Delete this tool?",
|
||||||
|
"deleteToTrash": "This will move {{name}} to the trash. It can be restored later.",
|
||||||
|
"deletePermanent": "This will permanently remove {{name}} and all its ratings. This cannot be undone.",
|
||||||
|
"deleting": "Deleting…",
|
||||||
|
"deleteAction": "Delete"
|
||||||
},
|
},
|
||||||
"compare": {
|
"compare": {
|
||||||
"title": "Compare Tools",
|
"title": "Compare Tools",
|
||||||
@@ -176,6 +182,35 @@
|
|||||||
"text": "This page doesn't exist.",
|
"text": "This page doesn't exist.",
|
||||||
"backHome": "Back to Home"
|
"backHome": "Back to Home"
|
||||||
},
|
},
|
||||||
|
"docs": {
|
||||||
|
"title": "Documentation",
|
||||||
|
"subtitle": "Version-bound documentation — release notes, endpoints and data fields per version.",
|
||||||
|
"backToIndex": "All releases",
|
||||||
|
"noDocs": "No documentation available for this path.",
|
||||||
|
"version": "Version",
|
||||||
|
"latest": "Latest",
|
||||||
|
"repo": "Repository",
|
||||||
|
"nav": "Documentation",
|
||||||
|
"guides": "Guide",
|
||||||
|
"endpoints": "Endpoints",
|
||||||
|
"schemas": "Data models",
|
||||||
|
"releases": "Release notes",
|
||||||
|
"reference": "API reference",
|
||||||
|
"referenceIntro": "Generated automatically from the OpenAPI spec — all endpoints and data fields of the current version.",
|
||||||
|
"onThisPage": "On this page",
|
||||||
|
"searchPlaceholder": "Search docs…",
|
||||||
|
"backToApp": "Back to app",
|
||||||
|
"field": "Field",
|
||||||
|
"type": "Type",
|
||||||
|
"required": "Required",
|
||||||
|
"description": "Description",
|
||||||
|
"status": "Status",
|
||||||
|
"schema": "Schema",
|
||||||
|
"parameter": "Parameter",
|
||||||
|
"noResults": "No results",
|
||||||
|
"fields": "Fields",
|
||||||
|
"fieldHelpHint": "Note: form fields link via the ? icon directly to the respective rows of this table."
|
||||||
|
},
|
||||||
"command": {
|
"command": {
|
||||||
"navigate": "Navigate",
|
"navigate": "Navigate",
|
||||||
"recent": "Recent",
|
"recent": "Recent",
|
||||||
|
|||||||
@@ -265,6 +265,25 @@
|
|||||||
*/
|
*/
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
|
||||||
|
/* Documentation heading anchors (mkdocs style "¶" links) */
|
||||||
|
.docs-prose :is(h1, h2, h3, h4) {
|
||||||
|
scroll-margin-top: 6rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose .docs-anchor::after {
|
||||||
|
content: "¶";
|
||||||
|
margin-left: 0.35rem;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose :is(h1, h2, h3, h4):hover .docs-anchor::after {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
/* Hide ugly search cancel button in Chrome until we can style it properly */
|
/* Hide ugly search cancel button in Chrome until we can style it properly */
|
||||||
input[type="search"]::-webkit-search-cancel-button {
|
input[type="search"]::-webkit-search-cancel-button {
|
||||||
@apply hidden;
|
@apply hidden;
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { setCsrfTokenGetter } from "@workspace/api-client-react";
|
||||||
|
|
||||||
|
let token: string | null = null;
|
||||||
|
|
||||||
|
setCsrfTokenGetter(() => token);
|
||||||
|
|
||||||
|
export function getCsrfToken(): string | null {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadCsrfToken(): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/auth/csrf`, {
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
token = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const data = (await res.json()) as { token?: string };
|
||||||
|
token = data.token ?? null;
|
||||||
|
return token;
|
||||||
|
} catch {
|
||||||
|
token = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
GetTopToolsMetric
|
GetTopToolsMetric
|
||||||
} from "@workspace/api-client-react";
|
} from "@workspace/api-client-react";
|
||||||
import { Layout } from "@/components/layout";
|
import { Layout } from "@/components/layout";
|
||||||
|
import { GuideHelp } from "@/components/guide-help";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import {
|
import {
|
||||||
@@ -48,7 +49,10 @@ export default function Analytics() {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<div className="space-y-6 pb-10">
|
<div className="space-y-6 pb-10">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight mb-2">Platform Analytics</h1>
|
<h1 className="text-3xl font-bold tracking-tight mb-2 flex items-center gap-2">
|
||||||
|
Platform Analytics
|
||||||
|
<GuideHelp guide="analytics" label="Platform Analytics" />
|
||||||
|
</h1>
|
||||||
<p className="text-muted-foreground">Macro-level insights into tool performance and community engagement.</p>
|
<p className="text-muted-foreground">Macro-level insights into tool performance and community engagement.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Link, useSearch } from "wouter";
|
|||||||
import { Layout } from "@/components/layout";
|
import { Layout } from "@/components/layout";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { RatingStars } from "@/components/rating-stars";
|
import { RatingStars } from "@/components/rating-stars";
|
||||||
|
import { GuideHelp } from "@/components/guide-help";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
@@ -101,7 +102,10 @@ export default function Compare() {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<div className="space-y-6 pb-10">
|
<div className="space-y-6 pb-10">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight mb-2">{t("compare.title")}</h1>
|
<h1 className="text-3xl font-bold tracking-tight mb-2 flex items-center gap-2">
|
||||||
|
{t("compare.title")}
|
||||||
|
<GuideHelp guide="vergleichen" label={t("compare.title")} />
|
||||||
|
</h1>
|
||||||
<p className="text-muted-foreground">{t("compare.subtitle", { count: list.length })}</p>
|
<p className="text-muted-foreground">{t("compare.subtitle", { count: list.length })}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { PasswordInput } from "@/components/password-input";
|
import { PasswordInput } from "@/components/password-input";
|
||||||
|
import { loadCsrfToken } from "@/lib/csrf";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
import { Wrench, AlertCircle } from "lucide-react";
|
import { Wrench, AlertCircle } from "lucide-react";
|
||||||
@@ -33,6 +34,7 @@ export default function Login() {
|
|||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries();
|
queryClient.invalidateQueries();
|
||||||
|
void loadCsrfToken();
|
||||||
setLocation(returnTo);
|
setLocation(returnTo);
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { customFetch } from "@workspace/api-client-react";
|
import { customFetch } from "@workspace/api-client-react";
|
||||||
import { recordRecentTool } from "@/lib/recent-tools";
|
import { recordRecentTool } from "@/lib/recent-tools";
|
||||||
|
import { FieldHelp } from "@/components/field-help";
|
||||||
|
import { GuideHelp } from "@/components/guide-help";
|
||||||
|
|
||||||
const ratingSchema = z.object({
|
const ratingSchema = z.object({
|
||||||
usefulness: z.number().min(1).max(5),
|
usefulness: z.number().min(1).max(5),
|
||||||
@@ -614,7 +616,10 @@ export default function ToolDetail() {
|
|||||||
<Dialog open={costDialogOpen} onOpenChange={(o) => { if (!o) setCostDialogOpen(false); }}>
|
<Dialog open={costDialogOpen} onOpenChange={(o) => { if (!o) setCostDialogOpen(false); }}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{editCost ? t("detail.edit") + " " + t("detail.costs") : t("detail.addCost")}</DialogTitle>
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
{editCost ? t("detail.edit") + " " + t("detail.costs") : t("detail.addCost")}
|
||||||
|
<GuideHelp guide="kosten" label={t("detail.costs")} />
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription>Manage license cost information for this tool.</DialogDescription>
|
<DialogDescription>Manage license cost information for this tool.</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-2">
|
<div className="space-y-4 py-2">
|
||||||
@@ -764,7 +769,10 @@ export default function ToolDetail() {
|
|||||||
{isReviewFormOpen && (
|
{isReviewFormOpen && (
|
||||||
<Card className="border-primary shadow-sm">
|
<Card className="border-primary shadow-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t("detail.addReview")}</CardTitle>
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
{t("detail.addReview")}
|
||||||
|
<GuideHelp guide="bewerten" label={t("detail.addReview")} />
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>Share your experience with {tool.name}</CardDescription>
|
<CardDescription>Share your experience with {tool.name}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -776,7 +784,10 @@ export default function ToolDetail() {
|
|||||||
name="usefulness"
|
name="usefulness"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t("detail.usefulness")}</FormLabel>
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
|
{t("detail.usefulness")}
|
||||||
|
<FieldHelp schema="RatingInput" field="usefulness">{t("detail.usefulness")}</FieldHelp>
|
||||||
|
</FormLabel>
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<RatingStars
|
<RatingStars
|
||||||
value={field.value}
|
value={field.value}
|
||||||
@@ -794,7 +805,10 @@ export default function ToolDetail() {
|
|||||||
name="usability"
|
name="usability"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t("detail.usability")}</FormLabel>
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
|
{t("detail.usability")}
|
||||||
|
<FieldHelp schema="RatingInput" field="usability">{t("detail.usability")}</FieldHelp>
|
||||||
|
</FormLabel>
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<RatingStars
|
<RatingStars
|
||||||
value={field.value}
|
value={field.value}
|
||||||
@@ -814,7 +828,10 @@ export default function ToolDetail() {
|
|||||||
name="comment"
|
name="comment"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Comment (Optional)</FormLabel>
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
|
Comment (Optional)
|
||||||
|
<FieldHelp schema="RatingInput" field="comment">Comment</FieldHelp>
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="What do you think about this tool?"
|
placeholder="What do you think about this tool?"
|
||||||
@@ -832,7 +849,10 @@ export default function ToolDetail() {
|
|||||||
name="reviewerName"
|
name="reviewerName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name (Optional)</FormLabel>
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
|
Name (Optional)
|
||||||
|
<FieldHelp schema="RatingInput" field="reviewerName">Name</FieldHelp>
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Anonymous" {...field} />
|
<Input placeholder="Anonymous" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -914,23 +934,23 @@ export default function ToolDetail() {
|
|||||||
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Delete this tool?</AlertDialogTitle>
|
<AlertDialogTitle>{t("detail.deleteConfirmTitle")}</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
{hasTrash ? (
|
{hasTrash ? (
|
||||||
<>This will move <span className="font-medium">{tool?.name}</span> to the trash. It can be restored later.</>
|
<>{t("detail.deleteToTrash", { name: tool?.name })}</>
|
||||||
) : (
|
) : (
|
||||||
<>This will permanently remove <span className="font-medium">{tool?.name}</span> and all its ratings. This cannot be undone.</>
|
<>{t("detail.deletePermanent", { name: tool?.name })}</>
|
||||||
)}
|
)}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
disabled={deleteTool.isPending}
|
disabled={deleteTool.isPending}
|
||||||
>
|
>
|
||||||
{deleteTool.isPending ? "Deleting…" : "Delete"}
|
{deleteTool.isPending ? t("detail.deleting") : t("detail.deleteAction")}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import { CategoryCombobox } from "@/components/category-combobox";
|
|||||||
import { FeatureInput } from "@/components/feature-input";
|
import { FeatureInput } from "@/components/feature-input";
|
||||||
import { TagInput } from "@/components/tag-input";
|
import { TagInput } from "@/components/tag-input";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
import { FieldHelp } from "@/components/field-help";
|
||||||
|
import { GuideHelp } from "@/components/guide-help";
|
||||||
|
|
||||||
const toolSchema = z.object({
|
const toolSchema = z.object({
|
||||||
name: z.string().min(2, "Name must be at least 2 characters"),
|
name: z.string().min(2, "Name must be at least 2 characters"),
|
||||||
@@ -159,6 +161,7 @@ export default function ToolEdit() {
|
|||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Pencil className="w-5 h-5 text-primary" />
|
<Pencil className="w-5 h-5 text-primary" />
|
||||||
Tool Details
|
Tool Details
|
||||||
|
<GuideHelp guide="tool-bearbeiten" label="Tool Details" />
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Modify the tool information below.</CardDescription>
|
<CardDescription>Modify the tool information below.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -171,7 +174,10 @@ export default function ToolEdit() {
|
|||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
|
Name
|
||||||
|
<FieldHelp schema="ToolInput" field="name">Name</FieldHelp>
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Tool name" {...field} />
|
<Input placeholder="Tool name" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -184,7 +190,10 @@ export default function ToolEdit() {
|
|||||||
name="category"
|
name="category"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Category</FormLabel>
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
|
Category
|
||||||
|
<FieldHelp schema="ToolInput" field="category">Category</FieldHelp>
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<CategoryCombobox value={field.value} onChange={field.onChange} />
|
<CategoryCombobox value={field.value} onChange={field.onChange} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -199,7 +208,10 @@ export default function ToolEdit() {
|
|||||||
name="websiteUrl"
|
name="websiteUrl"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Website URL (Optional)</FormLabel>
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
|
Website URL (Optional)
|
||||||
|
<FieldHelp schema="ToolInput" field="websiteUrl">Website URL</FieldHelp>
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="https://..." type="url" {...field} />
|
<Input placeholder="https://..." type="url" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -213,7 +225,10 @@ export default function ToolEdit() {
|
|||||||
name="iconUrl"
|
name="iconUrl"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Icon / Logo URL (Optional)</FormLabel>
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
|
Icon / Logo URL (Optional)
|
||||||
|
<FieldHelp schema="ToolInput" field="iconUrl">Icon / Logo URL</FieldHelp>
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-9 h-9 rounded-md border bg-muted flex items-center justify-center overflow-hidden shrink-0">
|
<div className="w-9 h-9 rounded-md border bg-muted flex items-center justify-center overflow-hidden shrink-0">
|
||||||
@@ -246,7 +261,10 @@ export default function ToolEdit() {
|
|||||||
name="description"
|
name="description"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
|
Description
|
||||||
|
<FieldHelp schema="ToolInput" field="description">Description</FieldHelp>
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="What does this tool do?"
|
placeholder="What does this tool do?"
|
||||||
@@ -262,7 +280,10 @@ export default function ToolEdit() {
|
|||||||
<div className="space-y-4 pt-4 border-t">
|
<div className="space-y-4 pt-4 border-t">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium">Features</h3>
|
<h3 className="text-lg font-medium inline-flex items-center gap-1.5">
|
||||||
|
Features
|
||||||
|
<FieldHelp schema="ToolInput" field="features">Features</FieldHelp>
|
||||||
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">Key capabilities of this tool. Existing features from other tools are selectable.</p>
|
<p className="text-sm text-muted-foreground">Key capabilities of this tool. Existing features from other tools are selectable.</p>
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" variant="outline" size="sm" onClick={() => appendFeature({ value: "" })}>
|
<Button type="button" variant="outline" size="sm" onClick={() => appendFeature({ value: "" })}>
|
||||||
@@ -306,7 +327,10 @@ export default function ToolEdit() {
|
|||||||
<div className="space-y-4 pt-4 border-t">
|
<div className="space-y-4 pt-4 border-t">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium">Tags</h3>
|
<h3 className="text-lg font-medium inline-flex items-center gap-1.5">
|
||||||
|
Tags
|
||||||
|
<FieldHelp schema="ToolInput" field="tags">Tags</FieldHelp>
|
||||||
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">Keywords for this tool. Existing tags from other tools are selectable.</p>
|
<p className="text-sm text-muted-foreground">Keywords for this tool. Existing tags from other tools are selectable.</p>
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" variant="outline" size="sm" onClick={() => appendTag({ value: "" })}>
|
<Button type="button" variant="outline" size="sm" onClick={() => appendTag({ value: "" })}>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { CategoryCombobox } from "@/components/category-combobox";
|
|||||||
import { FeatureInput } from "@/components/feature-input";
|
import { FeatureInput } from "@/components/feature-input";
|
||||||
import { TagInput } from "@/components/tag-input";
|
import { TagInput } from "@/components/tag-input";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
import { FieldHelp } from "@/components/field-help";
|
||||||
|
import { GuideHelp } from "@/components/guide-help";
|
||||||
|
|
||||||
const toolSchema = z.object({
|
const toolSchema = z.object({
|
||||||
name: z.string().min(2, "Name must be at least 2 characters"),
|
name: z.string().min(2, "Name must be at least 2 characters"),
|
||||||
@@ -122,6 +124,7 @@ export default function ToolNew() {
|
|||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Wrench className="w-5 h-5 text-primary" />
|
<Wrench className="w-5 h-5 text-primary" />
|
||||||
Tool Details
|
Tool Details
|
||||||
|
<GuideHelp guide="tool-anlegen" label="Tool Details" />
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Provide the basic information about the tool.</CardDescription>
|
<CardDescription>Provide the basic information about the tool.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -134,7 +137,10 @@ export default function ToolNew() {
|
|||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
|
Name
|
||||||
|
<FieldHelp schema="ToolInput" field="name">Name</FieldHelp>
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="e.g. React, Next.js, Postgres" {...field} data-testid="input-tool-name" />
|
<Input placeholder="e.g. React, Next.js, Postgres" {...field} data-testid="input-tool-name" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -148,7 +154,10 @@ export default function ToolNew() {
|
|||||||
name="category"
|
name="category"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Category</FormLabel>
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
|
Category
|
||||||
|
<FieldHelp schema="ToolInput" field="category">Category</FieldHelp>
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<CategoryCombobox
|
<CategoryCombobox
|
||||||
value={field.value}
|
value={field.value}
|
||||||
@@ -166,7 +175,10 @@ export default function ToolNew() {
|
|||||||
name="websiteUrl"
|
name="websiteUrl"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Website URL (Optional)</FormLabel>
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
|
Website URL (Optional)
|
||||||
|
<FieldHelp schema="ToolInput" field="websiteUrl">Website URL</FieldHelp>
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="https://..." type="url" {...field} data-testid="input-tool-url" />
|
<Input placeholder="https://..." type="url" {...field} data-testid="input-tool-url" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -180,7 +192,10 @@ export default function ToolNew() {
|
|||||||
name="iconUrl"
|
name="iconUrl"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Icon / Logo URL (Optional)</FormLabel>
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
|
Icon / Logo URL (Optional)
|
||||||
|
<FieldHelp schema="ToolInput" field="iconUrl">Icon / Logo URL</FieldHelp>
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-9 h-9 rounded-md border bg-muted flex items-center justify-center overflow-hidden shrink-0">
|
<div className="w-9 h-9 rounded-md border bg-muted flex items-center justify-center overflow-hidden shrink-0">
|
||||||
@@ -214,7 +229,10 @@ export default function ToolNew() {
|
|||||||
name="description"
|
name="description"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
|
Description
|
||||||
|
<FieldHelp schema="ToolInput" field="description">Description</FieldHelp>
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="What does this tool do? Why do people use it?"
|
placeholder="What does this tool do? Why do people use it?"
|
||||||
@@ -231,7 +249,10 @@ export default function ToolNew() {
|
|||||||
<div className="space-y-4 pt-4 border-t">
|
<div className="space-y-4 pt-4 border-t">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium">Features</h3>
|
<h3 className="text-lg font-medium inline-flex items-center gap-1.5">
|
||||||
|
Features
|
||||||
|
<FieldHelp schema="ToolInput" field="features">Features</FieldHelp>
|
||||||
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">List key capabilities. Existing features from other tools are selectable.</p>
|
<p className="text-sm text-muted-foreground">List key capabilities. Existing features from other tools are selectable.</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -284,7 +305,10 @@ export default function ToolNew() {
|
|||||||
<div className="space-y-4 pt-4 border-t">
|
<div className="space-y-4 pt-4 border-t">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium">Tags</h3>
|
<h3 className="text-lg font-medium inline-flex items-center gap-1.5">
|
||||||
|
Tags
|
||||||
|
<FieldHelp schema="ToolInput" field="tags">Tags</FieldHelp>
|
||||||
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">Keywords to help find this tool. Existing tags from other tools are selectable.</p>
|
<p className="text-sm text-muted-foreground">Keywords to help find this tool. Existing tags from other tools are selectable.</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useAuth } from "@/hooks/use-auth";
|
|||||||
import { useWatchlist } from "@/hooks/use-watchlist";
|
import { useWatchlist } from "@/hooks/use-watchlist";
|
||||||
import { Layout } from "@/components/layout";
|
import { Layout } from "@/components/layout";
|
||||||
import { ToolCard } from "@/components/tool-card";
|
import { ToolCard } from "@/components/tool-card";
|
||||||
|
import { GuideHelp } from "@/components/guide-help";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { ShieldAlert, Bookmark } from "lucide-react";
|
import { ShieldAlert, Bookmark } from "lucide-react";
|
||||||
@@ -42,7 +43,10 @@ export default function Watchlist() {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<div className="space-y-6 pb-10">
|
<div className="space-y-6 pb-10">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight mb-2">{t("watchlist.title")}</h1>
|
<h1 className="text-3xl font-bold tracking-tight mb-2 flex items-center gap-2">
|
||||||
|
{t("watchlist.title")}
|
||||||
|
<GuideHelp guide="watchlist" label={t("watchlist.title")} />
|
||||||
|
</h1>
|
||||||
<p className="text-muted-foreground">{t("watchlist.subtitle")}</p>
|
<p className="text-muted-foreground">{t("watchlist.subtitle")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Dokumentation (Handbuch, API-Referenz, Release-Notes)
|
||||||
|
|
||||||
|
Die App zeigt unter `/docs` eine MkDocs-artige Doku-Seite mit drei Bereichen:
|
||||||
|
|
||||||
|
- **Handbuch** (`docs/handbook/*.md`) — von Hand gepflegte Anleitungen
|
||||||
|
- **API-Referenz** (`lib/api-spec/openapi.yaml`) — automatisch generierte
|
||||||
|
Endpunkte & Datenfelder (Schema-Detailseiten mit Feld-Ankern; die `?`-Icons
|
||||||
|
in Formularen verlinken auf diese Felder)
|
||||||
|
- **Release-Notes** (`docs/releases/vX.Y.Z.md`) — pro Release
|
||||||
|
|
||||||
|
## Struktur
|
||||||
|
|
||||||
|
- `docs/handbook/` — Handbuch-Seiten mit Frontmatter (`title`, `order`)
|
||||||
|
- `docs/releases/TEMPLATE.md` — Vorlage für neue Releases
|
||||||
|
- `docs/releases/vX.Y.Z.md` — Notes pro Release
|
||||||
|
- `docs/releases/vX.Y.Z/reference.json` — API-Snapshot der jeweiligen Version
|
||||||
|
|
||||||
|
## Generator
|
||||||
|
|
||||||
|
`scripts/src/generate-docs.mjs` wird beim Frontend-Build (und `dev`) automatisch
|
||||||
|
ausgeführt und schreibt die Artefakte nach `artifacts/toolrate/public/docs/`:
|
||||||
|
|
||||||
|
- `reference.json` (aktuelle API), `search.json` (Suchindex),
|
||||||
|
`index.json` (Releases), `handbook/*.md` + `handbook/index.json`
|
||||||
|
- `releases/vX.Y.Z.md` und `versions/vX.Y.Z.json` (API-Snapshots alter Versionen)
|
||||||
|
|
||||||
|
Manuell aufrufbar:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node scripts/src/generate-docs.mjs # Build-Modus
|
||||||
|
node scripts/src/generate-docs.mjs --snapshot v0.9.0 # Snapshot für neue Version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow beim Release
|
||||||
|
|
||||||
|
1. **Version taggen** wie bisher (`git tag vX.Y.Z`, CI baut und deployed).
|
||||||
|
2. **`docs/releases/vX.Y.Z.md` anlegen** — Vorlage aus `TEMPLATE.md` kopieren,
|
||||||
|
Entwurf aus der Git-Historie ableiten:
|
||||||
|
```sh
|
||||||
|
git log --oneline vX.Y.Z-1..vX.Y.Z
|
||||||
|
```
|
||||||
|
(API-Delta anhand `lib/api-spec/openapi.yaml` prüfen.)
|
||||||
|
3. **API-Snapshot erzeugen:** `node scripts/src/generate-docs.mjs --snapshot vX.Y.Z`
|
||||||
|
erzeugt `docs/releases/vX.Y.Z/reference.json`.
|
||||||
|
4. **Committen & pushen.** Der Build kopiert die Dokumentation automatisch nach
|
||||||
|
`artifacts/toolrate/public/docs/` und generiert `index.json`.
|
||||||
|
|
||||||
|
> Hinweis: Alle Dateien unter `artifacts/toolrate/public/docs/` sind
|
||||||
|
> Build-Artefakte und werden bei jedem Build neu generiert — nicht von Hand
|
||||||
|
> bearbeiten. Einzige Quellen sind `docs/handbook/`, `docs/releases/` und
|
||||||
|
> `lib/api-spec/openapi.yaml`.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Dependency Policy
|
||||||
|
|
||||||
|
How this workspace keeps npm dependencies current, safe and reproducible.
|
||||||
|
|
||||||
|
## Principles
|
||||||
|
|
||||||
|
1. **Exact pins** – Every direct dependency in `package.json` is pinned to an
|
||||||
|
exact version (`1.2.3`, never `^1.2.3`). `save-exact=true` is set in
|
||||||
|
`.npmrc` so `pnpm add` follows this rule automatically.
|
||||||
|
2. **Committed lockfile** – `pnpm-lock.yaml` is committed. CI and the Docker
|
||||||
|
build install with `--frozen-lockfile`, so builds are reproducible.
|
||||||
|
3. **Security gates** – CI runs `pnpm audit --prod` (fails on any finding) and
|
||||||
|
`pnpm audit --audit-level high` (fails on high/critical). A non-zero exit
|
||||||
|
blocks the release pipeline.
|
||||||
|
4. **Supply-chain protection** – `minimumReleaseAge: 1440` (1 day) in
|
||||||
|
`pnpm-workspace.yaml` blocks freshly published versions. Do not lower or
|
||||||
|
disable it; only allowlist trusted publishers via
|
||||||
|
`minimumReleaseAgeExclude` for urgent security fixes.
|
||||||
|
5. **Pinned critical packages** – `react`, `react-dom` and `esbuild` are
|
||||||
|
intentionally excluded from automated updates (see `renovate.json`). Bump
|
||||||
|
them deliberately, one release at a time, with a test pass.
|
||||||
|
|
||||||
|
## Update cadence
|
||||||
|
|
||||||
|
| Frequency | Scope | Who |
|
||||||
|
|-----------|-------|-----|
|
||||||
|
| Weekly | Patch + minor (grouped by Renovate) | Renovate PR, human merge after green CI |
|
||||||
|
| Monthly | One major version at a time | Human, own commit + release tag |
|
||||||
|
| As needed | Security advisories | Immediate fix + patch release |
|
||||||
|
| Yearly | Infrastructure review (Node LTS, Postgres major, k3s) | Human |
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Patch/minor**: merge freely once CI (typecheck + build + audit) is green.
|
||||||
|
- **Major**: never bundle multiple majors into one release. One major per
|
||||||
|
commit so regressions can be bisected to the responsible change.
|
||||||
|
- **"Safe intermediate"**: if a package has a newer major that is not yet
|
||||||
|
absorbed, stay on the latest patch/minor of the *current* major line. The
|
||||||
|
exact-pin guarantees we never float into a new major by accident.
|
||||||
|
- **Node/Infra**: Node base image and pnpm version in the `Dockerfile` are
|
||||||
|
pinned exactly. Update them together with a build + live smoke test.
|
||||||
|
- **Postgres**: image tag is managed in the `admin/apps` repository
|
||||||
|
(`apps/system/toolrate`). Major upgrades run through a backup/restore flow.
|
||||||
|
|
||||||
|
## Process for applying updates
|
||||||
|
|
||||||
|
1. `pnpm install` to refresh the lockfile.
|
||||||
|
2. Regenerate clients if the API changed: `pnpm --filter @workspace/api-spec run codegen`.
|
||||||
|
3. Run `pnpm run typecheck`, `pnpm run build` (with `PORT=8080 BASE_PATH=/`),
|
||||||
|
and `pnpm audit`.
|
||||||
|
4. Fix any code that the new majors require (the common ones are
|
||||||
|
`openid-client`, `recharts`, `react-day-picker`, `date-fns`,
|
||||||
|
`@hookform/resolvers`, `react-resizable-panels`).
|
||||||
|
5. Commit, tag `vX.Y.Z`, push. CI builds, audits and deploys to k3s.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
title: Administration
|
||||||
|
order: 13
|
||||||
|
---
|
||||||
|
|
||||||
|
# Administration
|
||||||
|
|
||||||
|
Der Bereich **Admin** (`/admin`) ist ausschließlich für Admins zugänglich.
|
||||||
|
Ohne Admin-Rolle erscheint eine Zugriffsverweigerung.
|
||||||
|
|
||||||
|
> Oben rechts führt die Schaltfläche **Redundanz-Dashboard** zur automatischen
|
||||||
|
> Doppelungs-Erkennung (siehe [Redundanz](/docs/handbook/redundanz)).
|
||||||
|
|
||||||
|
## Tab „Nutzer"
|
||||||
|
|
||||||
|
Verwaltung der lokalen Konten.
|
||||||
|
|
||||||
|
- **Nutzer hinzufügen:** Benutzername (Pflicht), Passwort (mind. 6 Zeichen),
|
||||||
|
E-Mail (optional), **Rolle** (User/Admin), **Tarif** (Free/Premium/Enterprise).
|
||||||
|
- **Nutzer bearbeiten:** Rolle, Tarif und (für lokale Konten) ein neues Passwort
|
||||||
|
setzen. Für OIDC-Konten wird die Passwortverwaltung im Identitätsanbieter
|
||||||
|
(z. B. Keycloak) angeboten.
|
||||||
|
- **Nutzer löschen:** Entfernt das Konto endgültig (nicht für das eigene Konto).
|
||||||
|
|
||||||
|
API-Referenz:
|
||||||
|
[`POST /users`](/docs/reference/endpoints/users#createUser),
|
||||||
|
[`PATCH /users/{id}`](/docs/reference/endpoints/users#updateUser),
|
||||||
|
[`DELETE /users/{id}`](/docs/reference/endpoints/users#deleteUser).
|
||||||
|
|
||||||
|
## Tab „Tools"
|
||||||
|
|
||||||
|
Zentraler Zugriff auf den Tool-Katalog.
|
||||||
|
|
||||||
|
- **Suchen** nach Tools.
|
||||||
|
- Tools einzeln ansehen, bearbeiten oder in den Papierkorb verschieben.
|
||||||
|
- **Massenaktion:** mehrere Tools auswählen und in den Papierkorb verschieben
|
||||||
|
(Bestätigungsdialog; soft gelöschte Tools sind aus allen öffentlichen Ansichten
|
||||||
|
entfernt und können wiederhergestellt oder endgültig gelöscht werden).
|
||||||
|
|
||||||
|
## Tab „Audit-Log"
|
||||||
|
|
||||||
|
Chronologisches Protokoll aller Anlage-, Änderungs- und Löschvorgänge
|
||||||
|
(max. 100 Einträge): Aktion, Entität + ID, Zeitstempel, ausführende Person und
|
||||||
|
geänderte Felder.
|
||||||
|
|
||||||
|
API-Referenz: [`GET /audit-logs`](/docs/reference/endpoints/audit#listAuditLogs).
|
||||||
|
|
||||||
|
## Tab „System"
|
||||||
|
|
||||||
|
Versionsinformationen der laufenden Instanz:
|
||||||
|
|
||||||
|
- **Version** (z. B. `v0.8.1`),
|
||||||
|
- **Commit** (7-stelliger SHA, verlinkt zum Repository),
|
||||||
|
- **Build-Datum**,
|
||||||
|
- **Papierkorb-Aufbewahrung** („N Tage" oder „Für immer").
|
||||||
|
|
||||||
|
## Tool-Verknüpfungen (Admin)
|
||||||
|
|
||||||
|
Auf der Detailseite eines Tools kannst du als Admin **Verknüpfungen**
|
||||||
|
(eigene/„manual" sowie automatisch erkannte) verwalten:
|
||||||
|
|
||||||
|
- **Tool verknüpfen:** Dialog mit Tool-ID, **Beziehungstyp**
|
||||||
|
(Ähnlich / Ersetzt / Abgelöst durch) und optionalen Notizen.
|
||||||
|
- Beziehungstypen werden als Badges auf der Detailseite angezeigt.
|
||||||
|
- Manuelle Verknüpfungen lassen sich per Papierkorb-Icon wieder entfernen.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
title: Analytics
|
||||||
|
order: 10
|
||||||
|
---
|
||||||
|
|
||||||
|
# Analytics
|
||||||
|
|
||||||
|
Der Bereich **Analytics** (`/analytics`) ist ein öffentliches Dashboard mit
|
||||||
|
Kennzahlen und Diagrammen auf Basis aller Tools und Bewertungen.
|
||||||
|
|
||||||
|
## Kennzahlen (KPI-Karten)
|
||||||
|
|
||||||
|
- **Anzahl Tools** — wie viele Tools sind im Katalog erfasst.
|
||||||
|
- **Anzahl Bewertungen** — wie viele Bewertungen wurden insgesamt abgegeben.
|
||||||
|
- **Aktive Kategorien** — wie viele Kategorien existieren.
|
||||||
|
- **Durchschnittliche Bewertung** — globaler kombinierter Wert.
|
||||||
|
|
||||||
|
## Diagramme
|
||||||
|
|
||||||
|
| Diagramm | Inhalt |
|
||||||
|
| --- | --- |
|
||||||
|
| **Top 8 Tools** | Balkendiagramm der Tools mit der höchsten kombinierten Punktzahl (0–5) |
|
||||||
|
| **Tools je Kategorie** | Radar-Diagramm der Tool-Anzahl pro Kategorie |
|
||||||
|
| **Punkteverteilung** | Zwei horizontale Balken-Diagramme (Nützlichkeit & Bedienbarkeit) pro Stern |
|
||||||
|
|
||||||
|
Die Diagramme sind interaktiv (Tooltips beim Überfahren).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- [`GET /analytics/summary`](/docs/reference/endpoints/analytics#getAnalyticsSummary)
|
||||||
|
- [`GET /analytics/top-tools`](/docs/reference/endpoints/analytics#getTopTools)
|
||||||
|
- [`GET /analytics/by-category`](/docs/reference/endpoints/analytics#getAnalyticsByCategory)
|
||||||
|
- [`GET /analytics/rating-distribution`](/docs/reference/endpoints/analytics#getRatingDistribution)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
title: Bewerten
|
||||||
|
order: 7
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bewerten
|
||||||
|
|
||||||
|
Auf der Detailseite eines Tools kannst du deine Erfahrung teilen. Klicke auf
|
||||||
|
**Bewertung abgeben** (erfordert ein Konto).
|
||||||
|
|
||||||
|
## Formularfelder
|
||||||
|
|
||||||
|
| Feld | Pflicht | Hinweise |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Nützlichkeit** | Ja | 1–5 Sterne |
|
||||||
|
| **Bedienbarkeit** | Ja | 1–5 Sterne |
|
||||||
|
| **Kommentar** | Nein | Freitext |
|
||||||
|
| **Name** | Nein | Standard „Anonym" |
|
||||||
|
|
||||||
|
Neben den Feldern führt das **?‑Icon** direkt zur zugehörigen Feldbeschreibung
|
||||||
|
in der [Datenmodell-Referenz](/docs/reference/schemas/ratinginput).
|
||||||
|
|
||||||
|
## Was passiert nach dem Abgeben?
|
||||||
|
|
||||||
|
- Deine Bewertung wird sofort gespeichert und erscheint in der
|
||||||
|
**Bewertungsliste** der Detailseite.
|
||||||
|
- Die **Durchschnittswerte** (Nützlichkeit, Bedienbarkeit, Kombiniert) und die
|
||||||
|
**Punkteverteilung** werden aktualisiert.
|
||||||
|
- Die **Statistiken** im Bereich [Analytics](/docs/handbook/analytics) werden
|
||||||
|
neu berechnet.
|
||||||
|
|
||||||
|
## Statistik-Bereiche auf der Detailseite
|
||||||
|
|
||||||
|
- **Bewertungsübersicht:** Nützlichkeit & Bedienbarkeit als Durchschnitt mit
|
||||||
|
Fortschrittsbalken.
|
||||||
|
- **Punkteverteilung:** Anzahl der Bewertungen pro Stern (1★–5★).
|
||||||
|
- **Verlauf:** Linienchart der kombinierten/Teilwerte über die Zeit
|
||||||
|
(erst ab mehreren Bewertungen sichtbar).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- [`POST /tools/{id}/ratings`](/docs/reference/endpoints/ratings#createRating) — Bewertung abgeben
|
||||||
|
- [`GET /tools/{id}/ratings`](/docs/reference/endpoints/ratings#listToolRatings) — Bewertungen eines Tools
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
title: Datenmodell
|
||||||
|
order: 17
|
||||||
|
---
|
||||||
|
|
||||||
|
# Datenmodell
|
||||||
|
|
||||||
|
Dieses Kapitel erklärt die zentralen Datenobjekte von toolr auf Ebene der
|
||||||
|
Anwendung. Die vollständige, automatisch generierte Referenz aller Felder,
|
||||||
|
Typen und Constraints findest du in der
|
||||||
|
[API-Referenz](/docs/reference/schemas/tool).
|
||||||
|
|
||||||
|
## Tool
|
||||||
|
|
||||||
|
Das Herzstück: ein im Katalog erfasstes Werkzeug.
|
||||||
|
|
||||||
|
| Eigenschaft | Beschreibung |
|
||||||
|
| --- | --- |
|
||||||
|
| `id` | Eindeutige Kennung |
|
||||||
|
| `name` | Anzeigename |
|
||||||
|
| `description` | Beschreibung (Was macht das Tool?) |
|
||||||
|
| `category` | Kategorie-Zuordnung |
|
||||||
|
| `websiteUrl` | Offizielle Website (optional) |
|
||||||
|
| `iconUrl` | Logo-/Icon-URL (optional) |
|
||||||
|
| `features` | Liste von Fähigkeiten |
|
||||||
|
| `tags` | Liste von Schlagwörtern |
|
||||||
|
| `createdAt` / `updatedAt` | Zeitstempel |
|
||||||
|
| `createdBy` | Erstellende Person |
|
||||||
|
| `deletedAt` / `deletedBy` | Soft-Löschung (Papierkorb) |
|
||||||
|
|
||||||
|
Eingabe-Formulare verwenden die abgeleiteten Schemas
|
||||||
|
[`ToolInput`](/docs/reference/schemas/toolinput) und
|
||||||
|
[`ToolUpdate`](/docs/reference/schemas/toolupdate).
|
||||||
|
Aggregierte Ansichten liefert [`ToolWithStats`](/docs/reference/schemas/toolwithstats)
|
||||||
|
(z. B. mit Durchschnittsbewertung).
|
||||||
|
|
||||||
|
## Rating (Bewertung)
|
||||||
|
|
||||||
|
Eine einzelne Bewertung zu einem Tool:
|
||||||
|
|
||||||
|
- `usefulness` (Nützlichkeit, 1–5) und `usability` (Bedienbarkeit, 1–5)
|
||||||
|
- optional `comment` und ein Anzeigename (`reviewerName`)
|
||||||
|
- Zeitstempel
|
||||||
|
|
||||||
|
Eingabe-Schema: [`RatingInput`](/docs/reference/schemas/ratinginput).
|
||||||
|
|
||||||
|
## User & Auth
|
||||||
|
|
||||||
|
- [`User`](/docs/reference/schemas/user) — Benutzerkonto mit Rolle (User/Admin)
|
||||||
|
und Tarif (Free/Premium/Enterprise).
|
||||||
|
- [`AuthUser`](/docs/reference/schemas/authuser) — das angemeldete Profil
|
||||||
|
inklusive `entitlements` (verfügbare Features).
|
||||||
|
- [`UserPreferences`](/docs/reference/schemas/userpreferences) — Ansichts- und
|
||||||
|
Dichte-Präferenzen sowie die `watchlist` (Liste von Tool-IDs).
|
||||||
|
|
||||||
|
## Analytics
|
||||||
|
|
||||||
|
Die Statistik-Endpunkte liefern aggregierte Daten:
|
||||||
|
|
||||||
|
- [`AnalyticsSummary`](/docs/reference/schemas/analyticssummary) — globale
|
||||||
|
Kennzahlen (Anzahl Tools/Bewertungen, Kategorien, Durchschnitt).
|
||||||
|
- [`TopToolEntry`](/docs/reference/schemas/TopToolEntry) — ein Eintrag der
|
||||||
|
Top-Tools.- [`CategoryStats`](/docs/reference/schemas/categorystats) — Tool-Anzahl je
|
||||||
|
Kategorie.
|
||||||
|
- [`RatingDistribution`](/docs/reference/schemas/ratingdistribution) —
|
||||||
|
Punkteverteilung (Nützlichkeit & Bedienbarkeit).
|
||||||
|
- [`ScoreBucket`](/docs/reference/schemas/scorebucket) — ein Werte-Bucket.
|
||||||
|
|
||||||
|
## Weitere
|
||||||
|
|
||||||
|
- [`VersionInfo`](/docs/reference/schemas/versioninfo) — Version, Commit-SHA,
|
||||||
|
Build-Datum und Papierkorb-Aufbewahrung der laufenden Instanz.
|
||||||
|
- [`AuditLog`](/docs/reference/schemas/auditlog) — ein Protokolleintrag
|
||||||
|
(Aktion, Entität, Zeitstempel, Akteur, Änderungen).
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
title: Erste Schritte
|
||||||
|
order: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
# Erste Schritte
|
||||||
|
|
||||||
|
Diese Seite führt dich durch die wichtigsten Abläufe in toolr — vom ersten
|
||||||
|
Besuch bis zum Anlegen und Bewerten eines Tools.
|
||||||
|
|
||||||
|
## 1. Anmelden
|
||||||
|
|
||||||
|
Die meisten Aktionen (Tool anlegen, bewerten, Watchlist, Vergleichen) erfordern
|
||||||
|
ein Konto. Klicke unten links auf **Anmelden**. Je nach Konfiguration der
|
||||||
|
Instanz hast du zwei Möglichkeiten:
|
||||||
|
|
||||||
|
- **Lokale Konten:** Benutzername + Passwort. Der Zugang wird von einem Admin
|
||||||
|
angelegt (siehe [Administration](/docs/handbook/administration)).
|
||||||
|
- **OIDC (SSO):** Anmelden mit dem konfigurierten Identitätsanbieter (z. B.
|
||||||
|
Keycloak).
|
||||||
|
|
||||||
|
Welcher Modus aktiv ist, steht im Endpunkt
|
||||||
|
[`GET /auth/mode`](/docs/reference/endpoints/auth#getAuthMode). Details findest
|
||||||
|
du im Abschnitt [Anmelden & Konto](/docs/handbook/konto).
|
||||||
|
|
||||||
|
## 2. Tools finden
|
||||||
|
|
||||||
|
Öffne den Bereich **Tools durchsuchen**:
|
||||||
|
|
||||||
|
- **Suchen** — Volltextsuche über Name & Beschreibung (Tastenkürzel `/`).
|
||||||
|
- **Filtern** — nach Kategorie, Tags, Features und Mindestbewertung
|
||||||
|
(`minRating`).
|
||||||
|
- **Sortieren** — nach Aktualität, Top-Bewertung, meistbewertet, Name
|
||||||
|
(auf-/absteigend) oder letztem Update.
|
||||||
|
|
||||||
|
Alle Optionen im Detail: [Tools finden & durchsuchen](/docs/handbook/tools-finden).
|
||||||
|
|
||||||
|
## 3. Tool anlegen
|
||||||
|
|
||||||
|
Gehe auf **Tool hinzufügen** und fülle das Formular aus. Details zu jedem Feld
|
||||||
|
findest du im Abschnitt [Tool anlegen](/docs/handbook/tool-anlegen) und in der
|
||||||
|
[Feld-Referenz](/docs/reference/schemas/toolinput).
|
||||||
|
|
||||||
|
## 4. Bewerten
|
||||||
|
|
||||||
|
Auf der Detailseite eines Tools kannst du **Nützlichkeit** und **Bedienbarkeit**
|
||||||
|
(jeweils 1–5) vergeben und optional einen Kommentar hinterlassen. Deine
|
||||||
|
Bewertung fließt sofort in die Statistiken ein.
|
||||||
|
Siehe [Bewerten](/docs/handbook/bewerten).
|
||||||
|
|
||||||
|
## 5. Weiterführend
|
||||||
|
|
||||||
|
- [Tools vergleichen](/docs/handbook/vergleichen)
|
||||||
|
- [Watchlist](/docs/handbook/watchlist)
|
||||||
|
- [Analytics](/docs/handbook/analytics)
|
||||||
|
- [Pläne & Berechtigungen](/docs/handbook/plaene)
|
||||||
|
- [Administration](/docs/handbook/administration)
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
title: Überblick
|
||||||
|
order: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
# Willkommen bei toolr
|
||||||
|
|
||||||
|
toolr ist eine Plattform zum **Entdecken, Bewerten und Vergleichen von
|
||||||
|
Entwicklungstools**. Nutzer:innen pflegen einen gemeinsamen Katalog von Tools,
|
||||||
|
vergeben Bewertungen (Nützlichkeit & Bedienbarkeit) und nutzen Statistiken, um
|
||||||
|
die richtige Wahl zu treffen.
|
||||||
|
|
||||||
|
## Was kannst du mit toolr tun?
|
||||||
|
|
||||||
|
| Funktion | Beschreibung | Sichtbarkeit |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Tools durchsuchen** | Katalog filtern, sortieren und durchsuchen | Alle |
|
||||||
|
| **Tool anlegen** | Neues Tool mit Beschreibung, Kategorie, Features & Tags eintragen | Angemeldet |
|
||||||
|
| **Tool bearbeiten/löschen** | Eigene Tools pflegen (Ersteller:in oder Admin) | Angemeldet |
|
||||||
|
| **Bewerten** | Nützlichkeit & Bedienbarkeit (1–5) plus Kommentar vergeben | Angemeldet |
|
||||||
|
| **Watchlist** | Tools als Favoriten speichern | Premium |
|
||||||
|
| **Vergleichen** | Tools nebeneinander gegenüberstellen | Premium |
|
||||||
|
| **Kosten erfassen** | Lizenz- und Kostenmodelle je Tool eintragen | Premium |
|
||||||
|
| **Analytics** | Statistiken, Top-Tools, Verteilungen | Alle |
|
||||||
|
| **Papierkorb** | Soft-gelöschte Tools wiederherstellen oder endgültig löschen | Premium |
|
||||||
|
| **Admin** | Nutzerverwaltung, Audit-Log, Systeminformationen | Admin |
|
||||||
|
| **Redundanz** | Automatische Doppelungs-Erkennung | Admin |
|
||||||
|
|
||||||
|
## Wie diese Doku aufgebaut ist
|
||||||
|
|
||||||
|
- **User Guide** (diese Seiten): Schritt-für-Schritt-Anleitungen für alle
|
||||||
|
Funktionen — von den [Ersten Schritten](/docs/handbook/getting-started) bis
|
||||||
|
zur [Administration](/docs/handbook/administration).
|
||||||
|
- **API-Referenz**: automatisch aus der OpenAPI-Spezifikation generiert — alle
|
||||||
|
[Endpunkte](/docs/reference/endpoints/tools) und
|
||||||
|
[Datenfelder](/docs/reference/schemas/toolinput) der aktuellen Version.
|
||||||
|
- **Release-Notes**: Was ist in welcher [Version](/docs/releases/v0.8.1) neu.
|
||||||
|
|
||||||
|
## Der Einstieg
|
||||||
|
|
||||||
|
Der schnellste Weg:
|
||||||
|
|
||||||
|
1. **Anmelden** — ohne Konto kannst du nur stöbern
|
||||||
|
(siehe [Erste Schritte](/docs/handbook/getting-started#1-anmelden)).
|
||||||
|
2. **Tools finden** — Suche, Filter und Sortierung im Bereich
|
||||||
|
[Tools durchsuchen](/docs/handbook/tools-finden).
|
||||||
|
3. **Tool anlegen** — über „Tool hinzufügen"
|
||||||
|
([Anleitung](/docs/handbook/tool-anlegen)).
|
||||||
|
4. **Bewerten** — auf der Detailseite eines Tools
|
||||||
|
([Anleitung](/docs/handbook/bewerten)).
|
||||||
|
|
||||||
|
## Kontakt & Quellcode
|
||||||
|
|
||||||
|
Der Quellcode liegt unter
|
||||||
|
[git.kubebase.de/admin/tool-evaluator](https://git.kubebase.de/admin/tool-evaluator) —
|
||||||
|
über das Repository-Icon oben rechts erreichst du ihn jederzeit.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
title: Anmelden & Konto
|
||||||
|
order: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
# Anmelden & Konto
|
||||||
|
|
||||||
|
## Anmelden
|
||||||
|
|
||||||
|
Klicke unten links in der Seitenleiste auf **Anmelden**. Je nach Konfiguration
|
||||||
|
der Instanz:
|
||||||
|
|
||||||
|
- **Lokale Konten:** Benutzername und Passwort eingeben. Die Konten werden von
|
||||||
|
einem Admin angelegt (siehe [Administration](/docs/handbook/administration)).
|
||||||
|
- **OIDC (SSO):** Du wirst an den konfigurierten Identitätsanbieter
|
||||||
|
weitergeleitet und meldest dich dort an.
|
||||||
|
|
||||||
|
Der aktive Modus steht im Endpunkt
|
||||||
|
[`GET /auth/mode`](/docs/reference/endpoints/auth#getAuthMode).
|
||||||
|
|
||||||
|
> Die Login-Seite erreichst du direkt unter `/login`. Nach erfolgreicher
|
||||||
|
> Anmeldung wirst du zur ursprünglich aufgerufenen Seite zurückgeleitet.
|
||||||
|
|
||||||
|
## Benutzerprofil
|
||||||
|
|
||||||
|
Dein Profil (Avatar, Name, E-Mail, Tarif) siehst du unten links im
|
||||||
|
Benutzermenü. Dort stehen dir folgende Aktionen zur Verfügung:
|
||||||
|
|
||||||
|
- **Watchlist** — deine gespeicherten Tools (nur mit dem entsprechenden Tarif).
|
||||||
|
- **Papierkorb** — wiederherstellbare, gelöschte Tools (Premium/Enterprise).
|
||||||
|
- **Passwort ändern** — für lokale Konten direkt in toolr; für OIDC-Konten wird
|
||||||
|
die Passwortverwaltung im Identitätsanbieter angeboten.
|
||||||
|
- **Abmelden** — beendet deine Sitzung.
|
||||||
|
|
||||||
|
## Passwort ändern (lokales Konto)
|
||||||
|
|
||||||
|
1. Öffne das Benutzermenü unten links.
|
||||||
|
2. Wähle **Passwort ändern**.
|
||||||
|
3. Gib das **aktuelle** sowie ein **neues** Passwort ein (mind. 6 Zeichen) und
|
||||||
|
bestätige es.
|
||||||
|
4. Speichern — das Passwort wird sofort übernommen.
|
||||||
|
|
||||||
|
API-Referenz: [`POST /auth/me/password`](/docs/reference/endpoints/auth#changeMyPassword).
|
||||||
|
|
||||||
|
## Anzeigeeinstellungen
|
||||||
|
|
||||||
|
Über die Schaltflächen oben rechts kannst du:
|
||||||
|
|
||||||
|
- **Sprache** wechseln (Deutsch / Englisch),
|
||||||
|
- **Theme** umschalten (Hell / Dunkel / System),
|
||||||
|
- die **Listenansicht** und **Dichte** im Bereich Tools durchsuchen anpassen
|
||||||
|
(siehe [Tools finden & durchsuchen](/docs/handbook/tools-finden)).
|
||||||
|
|
||||||
|
Deine Präferenzen (inkl. Watchlist) werden im Endpunkt
|
||||||
|
[`GET /auth/me/preferences`](/docs/reference/endpoints/auth#getMePreferences)
|
||||||
|
gespeichert und über [`PUT /auth/me/preferences`](/docs/reference/endpoints/auth#updateMePreferences)
|
||||||
|
aktualisiert.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
title: Kosten erfassen
|
||||||
|
order: 12
|
||||||
|
---
|
||||||
|
|
||||||
|
# Kosten erfassen
|
||||||
|
|
||||||
|
Auf der Detailseite eines Tools kannst du Kosten- und Lizenzmodelle eintragen,
|
||||||
|
damit die Gesamtkosten je Tool transparent werden.
|
||||||
|
|
||||||
|
> Kosten ist ein **Premium-Feature** (`costs`, Premium/Enterprise). Admins
|
||||||
|
> haben immer Zugriff.
|
||||||
|
|
||||||
|
## Kosten hinzufügen
|
||||||
|
|
||||||
|
Klicke auf **Kosten hinzufügen** im Kosten-Bereich der Detailseite und fülle
|
||||||
|
das Formular aus:
|
||||||
|
|
||||||
|
| Feld | Hinweise |
|
||||||
|
| --- | --- |
|
||||||
|
| **Lizenztyp** | Free / Subscription / One-Time / Usage-Based |
|
||||||
|
| **Abrechnungszeitraum** | Nur für „Subscription": Monatlich / Quartalsweise / Jährlich |
|
||||||
|
| **Kosten** | Betrag als Zahl |
|
||||||
|
| **Währung** | EUR / USD / GBP / CHF |
|
||||||
|
| **Notizen** | Optionaler Freitext |
|
||||||
|
|
||||||
|
Speichern legt den Eintrag an. Jeder Kosten-Eintrag wird als Karte mit
|
||||||
|
Lizenz-Badge, Abrechnungszeitraum, Betrag (`Betrag Währung` bzw. „Free") und
|
||||||
|
Notizen angezeigt.
|
||||||
|
|
||||||
|
## Kosten bearbeiten & löschen
|
||||||
|
|
||||||
|
Beim Überfahren einer Kosten-Karte erscheinen die Aktionen **Bearbeiten**
|
||||||
|
(Bleistift) und **Löschen** (Papierkorb).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Die Kosten-Daten werden über die Tool-Endpunkte verwaltet
|
||||||
|
(siehe [API-Referenz](/docs/reference/endpoints/tools)).
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
title: Papierkorb
|
||||||
|
order: 15
|
||||||
|
---
|
||||||
|
|
||||||
|
# Papierkorb
|
||||||
|
|
||||||
|
Der **Papierkorb** (`/trash`) enthält soft gelöschte Tools. Mit Papierkorb-Zugang
|
||||||
|
können sie wiederhergestellt werden; endgültiges Löschen ist Admins vorbehalten.
|
||||||
|
|
||||||
|
> Der Papierkorb ist ein **Premium-Feature** (`trash`, Premium/Enterprise).
|
||||||
|
> Admins haben immer Zugriff.
|
||||||
|
|
||||||
|
## Zugang
|
||||||
|
|
||||||
|
Der Papierkorb ist über das Benutzermenü oder die Seitenleiste erreichbar.
|
||||||
|
Ohne `trash`-Berechtigung erscheint ein Hinweis auf den Tarifwechsel.
|
||||||
|
|
||||||
|
## Wiederherstellen
|
||||||
|
|
||||||
|
- Markiere ein oder mehrere Tools (Checkboxen).
|
||||||
|
- Klicke auf **Wiederherstellen (N)** — die Tools erscheinen wieder in allen
|
||||||
|
öffentlichen Ansichten.
|
||||||
|
|
||||||
|
> Wiederherstellen steht jeder Person mit Papierkorb-Zugang zur Verfügung.
|
||||||
|
|
||||||
|
## Endgültig löschen (nur Admin)
|
||||||
|
|
||||||
|
- **Löschen (N)** entfernt die ausgewählten Tools **endgültig** — inklusive
|
||||||
|
aller Bewertungen, Kosten und Verknüpfungen. Das kann nicht rückgängig
|
||||||
|
gemacht werden.
|
||||||
|
- **Papierkorb leeren** entfernt alle soft gelöschten Tools endgültig.
|
||||||
|
|
||||||
|
## Tabelle
|
||||||
|
|
||||||
|
Der Papierkorb listet: Name, Kategorie, **Gelöscht am** (`tt.MM.jjjj HH:mm`),
|
||||||
|
**Gelöscht von** sowie Aktionen (Wiederherstellen; Löschen nur Admin). Die Suche
|
||||||
|
filtert nach Namen.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- [`GET /tools/trash`](/docs/reference/endpoints/tools#listTrashedTools) — Liste
|
||||||
|
- [`POST /tools/trash/restore`](/docs/reference/endpoints/tools#restoreTools) — Wiederherstellen
|
||||||
|
- [`DELETE /tools/trash`](/docs/reference/endpoints/tools#deleteTrashedTools) — Endgültig löschen (Admin)
|
||||||
|
- [`POST /tools/trash/empty`](/docs/reference/endpoints/tools#emptyTrash) — Papierkorb leeren (Admin)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
title: Pläne & Berechtigungen
|
||||||
|
order: 11
|
||||||
|
---
|
||||||
|
|
||||||
|
# Pläne & Berechtigungen
|
||||||
|
|
||||||
|
toolr unterscheidet **Tarife** (Tier) und **Rollen**. Admins umgehen alle
|
||||||
|
Feature-Beschränkungen.
|
||||||
|
|
||||||
|
## Tarife
|
||||||
|
|
||||||
|
| Tarif | Beschreibung |
|
||||||
|
| --- | --- |
|
||||||
|
| **Free** | Grundfunktionen: suchen, filtern, ansehen, Analytics |
|
||||||
|
| **Premium** | Zusätzlich Watchlist, Vergleichen, Papierkorb, Kosten |
|
||||||
|
| **Enterprise** | Alle Premium-Features + erweiterter Support |
|
||||||
|
|
||||||
|
### Feature-Berechtigungen
|
||||||
|
|
||||||
|
Premium/Enterprise schalten folgende Features frei:
|
||||||
|
|
||||||
|
| Feature | Funktion | Mehr erfahren |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `compare` | Tools vergleichen | [Vergleichen](/docs/handbook/vergleichen) |
|
||||||
|
| `watchlist` | Favoritenliste | [Watchlist](/docs/handbook/watchlist) |
|
||||||
|
| `trash` | Papierkorb (soft gelöschte Tools) | [Papierkorb](/docs/handbook/papierkorb) |
|
||||||
|
| `costs` | Kosten-/Lizenzmodelle erfassen | [Kosten erfassen](/docs/handbook/kosten) |
|
||||||
|
|
||||||
|
Fehlt dir ein Feature, zeigt die App einen **Upgrade-Hinweis** mit Link zur
|
||||||
|
Tarifverwaltung.
|
||||||
|
|
||||||
|
## Rollen
|
||||||
|
|
||||||
|
| Rolle | Berechtigungen |
|
||||||
|
| --- | --- |
|
||||||
|
| **User** | Standard-Konto: Tools anlegen/bewerten, eigene Tools bearbeiten |
|
||||||
|
| **Admin** | Alle User-Rechte + Verwaltung, Audit-Log, Redundanz, Papierkorb leeren, Tool-Verknüpfungen |
|
||||||
|
|
||||||
|
Admins passieren **alle** Feature-Checks — auch ohne Premium-Tarif.
|
||||||
|
|
||||||
|
## Tarif-/Rollenverwaltung
|
||||||
|
|
||||||
|
Die Zuordnung von Rolle und Tarif wird durch Admins im Bereich
|
||||||
|
[Administration](/docs/handbook/administration) (Tab „Nutzer") verwaltet.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
title: Redundanz-Dashboard
|
||||||
|
order: 14
|
||||||
|
---
|
||||||
|
|
||||||
|
# Redundanz-Dashboard
|
||||||
|
|
||||||
|
Das **Redundanz-Dashboard** (`/admin/redundancy`) ist ein Admin-Werkzeug zur
|
||||||
|
automatischen Erkennung doppelter oder stark überlappender Tools — jeweils
|
||||||
|
pro Kategorie — inklusive Kosten- und Bewertungsvergleich.
|
||||||
|
|
||||||
|
> Der Zugriff ist ausschließlich Admins vorbehalten (die API ist
|
||||||
|
> admin-geschützt).
|
||||||
|
|
||||||
|
## Aufbau
|
||||||
|
|
||||||
|
- **Pro Kategorie** wird eine Gruppe angezeigt: Name der Kategorie,
|
||||||
|
Anzahl Tools und Vergleiche sowie ggf. die **gesamten monatlichen Kosten**
|
||||||
|
(z. B. `€X.XX/mo gesamt`).
|
||||||
|
- Jedes Tool wird als Karte dargestellt: Name, monatliche Kosten, Anzahl der
|
||||||
|
Bewertungen, kombinierte Bewertung, Lizenz-Badges und Feature-Anzahl.
|
||||||
|
|
||||||
|
## Vergleiche & Empfehlungen
|
||||||
|
|
||||||
|
Für jedes Tool-Paar erscheint:
|
||||||
|
|
||||||
|
- Tool A vs. Tool B, jeweils mit Bewertung (`X.X ★`) und monatlichen Kosten.
|
||||||
|
- **Überlappung** in Prozent (Fortschrittsbalken in der Mitte).
|
||||||
|
- Eine **Empfehlung** mit Konfidenz-Farbe:
|
||||||
|
- **hoch** (grün), **mittel** (gelb), **niedrig** (grau)
|
||||||
|
- Das empfohlene, bessere Tool wird mit „Daumen hoch" markiert und begründet.
|
||||||
|
|
||||||
|
## Manuelle Bewertung
|
||||||
|
|
||||||
|
Du kannst ein Paar manuell bewerten: Klicke auf Tool A oder Tool B, um
|
||||||
|
festzuhalten, welches besser ist. Die Auswahl wird gespeichert und die
|
||||||
|
Darstellung aktualisiert.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- [`GET /api/admin/redundancy`](#) — Daten laden (admin-geschützt)
|
||||||
|
- [`POST /api/admin/redundancy/evaluate`](#) — manuelle Bewertung speichern
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
title: Tastenkürzel & Kommandopalette
|
||||||
|
order: 16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tastenkürzel & Kommandopalette
|
||||||
|
|
||||||
|
## Kommandopalette
|
||||||
|
|
||||||
|
Die Kommandopalette ist die zentrale Schnellnavigation:
|
||||||
|
|
||||||
|
- Öffnen mit **`⌘K`** (macOS) bzw. **`Ctrl+K`** (Windows/Linux).
|
||||||
|
- Alternativ über die Suchleiste oben rechts („Tools suchen… ⌘K") oder das
|
||||||
|
Such-Icon auf Mobilgeräten.
|
||||||
|
|
||||||
|
### Leerer Zustand
|
||||||
|
|
||||||
|
Ohne Eingabe zeigt die Palette:
|
||||||
|
|
||||||
|
- **Zuletzt angesehen** — die letzten 5 Tools, die du besucht hast.
|
||||||
|
- **Navigation** — Tools durchsuchen, Tool hinzufügen, Analytics sowie
|
||||||
|
(abhängig von Berechtigungen) Watchlist, Papierkorb und Admin.
|
||||||
|
|
||||||
|
### Suche
|
||||||
|
|
||||||
|
Tippe, um live nach Tools zu suchen (max. 10 Ergebnisse, inkl. Bewertung
|
||||||
|
`X.X★`).
|
||||||
|
|
||||||
|
## Tastenkürzel im Überblick
|
||||||
|
|
||||||
|
| Kürzel | Aktion |
|
||||||
|
| --- | --- |
|
||||||
|
| `⌘K` / `Ctrl+K` | Kommandopalette öffnen |
|
||||||
|
| `/` | Suche im Bereich „Tools durchsuchen" fokussieren |
|
||||||
|
|
||||||
|
## Weitere Hinweise
|
||||||
|
|
||||||
|
- **Zuletzt angesehen** wird lokal im Browser gespeichert (max. 5 Einträge).
|
||||||
|
- Die Seitenleiste (linke Navigation) ist auf Desktop einklappbar; der
|
||||||
|
Breadcrumb oben zeigt deinen aktuellen Ort.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
title: Tool anlegen
|
||||||
|
order: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tool anlegen
|
||||||
|
|
||||||
|
Um ein neues Tool zum Katalog hinzuzufügen, klicke auf **Tool hinzufügen**
|
||||||
|
(`/tools/new`). Das Anlegen erfordert ein Konto — ohne Anmeldung erscheint ein
|
||||||
|
Hinweis mit Login-Button.
|
||||||
|
|
||||||
|
## Formularfelder
|
||||||
|
|
||||||
|
| Feld | Pflicht | Hinweise |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Name** | Ja | Mind. 2 Zeichen |
|
||||||
|
| **Kategorie** | Ja | Auswahlliste; neue Kategorien lassen sich direkt anlegen |
|
||||||
|
| **Website URL** | Nein | Gültige URL (z. B. `https://...`) |
|
||||||
|
| **Icon / Logo URL** | Nein | Gültige URL; Vorschau wird live angezeigt |
|
||||||
|
| **Beschreibung** | Ja | Mind. 10 Zeichen; beschreibe, was das Tool tut |
|
||||||
|
| **Features** | Nein | Dynamische Liste mit Autovervollständigung (max. 6) |
|
||||||
|
| **Tags** | Nein | Dynamische Liste mit Autovervollständigung |
|
||||||
|
|
||||||
|
Neben jedem Feld führt das **?‑Icon** direkt zur zugehörigen Feldbeschreibung
|
||||||
|
in der [Datenmodell-Referenz](/docs/reference/schemas/toolinput).
|
||||||
|
|
||||||
|
### Kategorie
|
||||||
|
|
||||||
|
- Tippe, um nach bestehenden Kategorien zu suchen.
|
||||||
|
- Wähle **+ Erstelle „..."**, um eine neue Kategorie anzulegen.
|
||||||
|
|
||||||
|
### Features & Tags
|
||||||
|
|
||||||
|
- **Feature hinzufügen** / **Tag hinzufügen** hängt eine neue Zeile an.
|
||||||
|
- Die Eingabefelder schlagen bestehende Features/Tags vor
|
||||||
|
(Autovervollständigung, max. 6 Vorschläge).
|
||||||
|
- Mit dem **×**‑Button entfernst du einzelne Zeilen.
|
||||||
|
- Features und Tags helfen beim Filtern und Wiederfinden.
|
||||||
|
|
||||||
|
## Speichern
|
||||||
|
|
||||||
|
Klicke auf **Tool hinzufügen**. Nach erfolgreicher Anlage wirst du auf die
|
||||||
|
Detailseite des neuen Tools weitergeleitet.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- [`POST /tools`](/docs/reference/endpoints/tools#createTool) — Tool anlegen
|
||||||
|
- [`GET /categories`](/docs/reference/endpoints/tools#listCategories) — Kategorien
|
||||||
|
- [`GET /features/all`](/docs/reference/endpoints/tools#listAllFeatures) — Features
|
||||||
|
- [`GET /tags/all`](/docs/reference/endpoints/tools#listAllTags) — Tags
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
title: Tool bearbeiten & löschen
|
||||||
|
order: 6
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tool bearbeiten & löschen
|
||||||
|
|
||||||
|
## Bearbeiten
|
||||||
|
|
||||||
|
Auf der Detailseite eines Tools findest du die Schaltfläche **Bearbeiten**
|
||||||
|
(nur für die Person, die das Tool angelegt hat, sowie für Admins).
|
||||||
|
|
||||||
|
Die Bearbeitungsseite (`/tools/:id/edit`) enthält dieselben Felder wie beim
|
||||||
|
Anlegen (Name, Kategorie, Website/Icon-URL, Beschreibung, Features, Tags) —
|
||||||
|
bereits mit den aktuellen Werten befüllt.
|
||||||
|
|
||||||
|
- **Speichern** übernimmt die Änderungen.
|
||||||
|
- **Abbrechen** führt zurück zur Detailseite.
|
||||||
|
|
||||||
|
API-Referenz: [`PATCH /tools/{id}`](/docs/reference/endpoints/tools#updateTool).
|
||||||
|
|
||||||
|
## Löschen
|
||||||
|
|
||||||
|
Über **Löschen** auf der Detailseite wird das Tool entfernt. Das Verhalten
|
||||||
|
hängt von deinem Tarif ab:
|
||||||
|
|
||||||
|
- **Mit Papierkorb-Zugang** (Premium/Enterprise oder Admin): Das Tool wird
|
||||||
|
**soft gelöscht** — es verschwindet aus allen öffentlichen Ansichten, kann
|
||||||
|
aber im [Papierkorb](/docs/handbook/papierkorb) wiederhergestellt oder
|
||||||
|
endgültig gelöscht werden.
|
||||||
|
- **Ohne Papierkorb-Zugang:** Das Tool wird **endgültig** gelöscht und kann
|
||||||
|
nicht wiederhergestellt werden.
|
||||||
|
|
||||||
|
Die Löschung ist nur für die Person, die das Tool angelegt hat, sowie für
|
||||||
|
Admins möglich.
|
||||||
|
|
||||||
|
API-Referenz: [`DELETE /tools/{id}`](/docs/reference/endpoints/tools#deleteTool).
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
title: Tools finden & durchsuchen
|
||||||
|
order: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tools finden & durchsuchen
|
||||||
|
|
||||||
|
Der Bereich **Tools durchsuchen** (`/tools`) ist der Einstieg in den Katalog.
|
||||||
|
Hier kombinierst du Suche, Filter und Sortierung, um genau die Tools zu finden,
|
||||||
|
die dich interessieren.
|
||||||
|
|
||||||
|
## Suche
|
||||||
|
|
||||||
|
- Die **Suchleiste** durchsucht Name und Beschreibung (Volltext).
|
||||||
|
- Tastenkürzel: Drücke **`/`**, um die Suche zu fokussieren.
|
||||||
|
- Die Eingabe ist deaktiviert (Debounce), damit bei jedem Tastendruck sofort
|
||||||
|
nachgefiltert wird.
|
||||||
|
|
||||||
|
## Filtern
|
||||||
|
|
||||||
|
Über die Schaltfläche **Filter** (mit Badge für die Anzahl aktiver Filter)
|
||||||
|
öffnest du den Filter-Popover mit:
|
||||||
|
|
||||||
|
- **Tags** — Auswahl über Checkboxen (scrollbare Liste).
|
||||||
|
- **Features** — Auswahl über Checkboxen.
|
||||||
|
- **Mindestbewertung** — Schieberegler von 0 bis 5 (Schritte von 0,5); zeigt
|
||||||
|
z. B. „3.0+" an.
|
||||||
|
|
||||||
|
Aktive Filter erscheinen als **entfernbare Chips** über der Ergebnisliste.
|
||||||
|
Mit **Filter zurücksetzen** bzw. **Alle entfernen** räumst du sie wieder auf.
|
||||||
|
|
||||||
|
## Sortieren
|
||||||
|
|
||||||
|
Über das Dropdown **Sortieren** stehen folgende Optionen zur Verfügung:
|
||||||
|
|
||||||
|
| Sortierung | Beschreibung |
|
||||||
|
| --- | --- |
|
||||||
|
| Neueste | Neue Tools zuerst |
|
||||||
|
| Top bewertet | Nach kombinierter Bewertung |
|
||||||
|
| Meistbewertet | Nach Anzahl der Bewertungen |
|
||||||
|
| Name (A–Z) | Alphabetisch aufsteigend |
|
||||||
|
| Name (Z–A) | Alphabetisch absteigend |
|
||||||
|
| Zuletzt aktualisiert | Nach letztem Update |
|
||||||
|
|
||||||
|
## Ansicht & Dichte
|
||||||
|
|
||||||
|
- **Ansicht wechseln:** Raster / Tabelle / Zeilen.
|
||||||
|
- **Dichte:** gemütlich / kompakt (Schieberegler).
|
||||||
|
|
||||||
|
Deine Auswahl wird gespeichert — lokal im Browser und für angemeldete Nutzer:innen
|
||||||
|
zusätzlich serverseitig in den Präferenzen. Ansicht, Dichte, Suche, Filter und
|
||||||
|
Sortierung werden dabei in die URL übernommen, sodass du Ergebnisse teilen
|
||||||
|
kannst.
|
||||||
|
|
||||||
|
## Tabellenansicht
|
||||||
|
|
||||||
|
In der Tabellenansicht sind die Spalten **Tool**, **Bewertung** und **Anzahl
|
||||||
|
Bewertungen** sortierbar. Beim Überfahren einer Zeile erscheint eine Vorschau
|
||||||
|
mit Bewertungsdetails, Tags und Mini-Balken.
|
||||||
|
|
||||||
|
## Auswählen für Vergleich & Watchlist
|
||||||
|
|
||||||
|
- Auf jeder Karte/Zeile findest du ein **Vergleichs-Icon**, mit dem du Tools zur
|
||||||
|
[Vergleichsleiste](/docs/handbook/vergleichen) hinzufügst.
|
||||||
|
- Das **Lesezeichen-Icon** speichert Tools in deiner
|
||||||
|
[Watchlist](/docs/handbook/watchlist) (nur mit dem entsprechenden Tarif).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Alle Such-, Filter- und Sortierparameter entsprechen den Query-Parametern von
|
||||||
|
[`GET /tools`](/docs/reference/endpoints/tools#listTools).
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
title: Vergleichen
|
||||||
|
order: 9
|
||||||
|
---
|
||||||
|
|
||||||
|
# Vergleichen
|
||||||
|
|
||||||
|
Mit der Vergleichsfunktion stellst du mehrere Tools **nebeneinander** gegenüber —
|
||||||
|
ideal, um eine fundierte Entscheidung zu treffen.
|
||||||
|
|
||||||
|
> Vergleichen ist ein **Premium-Feature** (Premium/Enterprise) und steht Admins
|
||||||
|
> immer zur Verfügung.
|
||||||
|
|
||||||
|
## Tools auswählen
|
||||||
|
|
||||||
|
1. Im Bereich **Tools durchsuchen** klickst du auf jeder Karte/Zeile auf das
|
||||||
|
**Vergleichs-Icon** (Waage).
|
||||||
|
2. Unten erscheint die **Vergleichsleiste** mit den ausgewählten Tools als
|
||||||
|
Chips. Du kannst einzelne Tools entfernen (×) oder die Auswahl leeren.
|
||||||
|
3. Klicke auf **Vergleichen (N)**, um zur Vergleichsansicht zu gelangen.
|
||||||
|
|
||||||
|
> Ohne Premium-Tarif ist der Button gesperrt (Schloss-Icon). Über den
|
||||||
|
> Dialog gelangst du zum Tarifwechsel
|
||||||
|
> (siehe [Pläne & Berechtigungen](/docs/handbook/plaene)).
|
||||||
|
|
||||||
|
## Die Vergleichsansicht
|
||||||
|
|
||||||
|
Die Ansicht zeigt eine Tabelle mit einer Spalte pro Tool. Zeilen:
|
||||||
|
|
||||||
|
| Zeile | Inhalt |
|
||||||
|
| --- | --- |
|
||||||
|
| **Bewertung** | Sterne + Wert (z. B. `4.2/5`) |
|
||||||
|
| **Nützlichkeit** | Wert (X.X/5) |
|
||||||
|
| **Bedienbarkeit** | Wert (X.X/5) |
|
||||||
|
| **Anzahl Bewertungen** | Anzahl |
|
||||||
|
| **Beschreibung** | Text |
|
||||||
|
| **Features** | Badges |
|
||||||
|
| **Tags** | Badges |
|
||||||
|
| **Zuletzt aktualisiert** | Datum |
|
||||||
|
|
||||||
|
Der **beste Wert** pro Zeile wird hervorgehoben (mit Trophäen-Icon).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Die Vergleichsansicht liest die Daten über
|
||||||
|
[`GET /compare`](/docs/reference/endpoints/tools#listCompareTools).
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
title: Watchlist
|
||||||
|
order: 8
|
||||||
|
---
|
||||||
|
|
||||||
|
# Watchlist
|
||||||
|
|
||||||
|
Die **Watchlist** ist eine persönliche Favoritenliste. Tools darin kannst du
|
||||||
|
jederzeit per Klick wieder aufrufen und vergleichen.
|
||||||
|
|
||||||
|
> Die Watchlist ist ein **Premium-Feature** (Premium/Enterprise) und steht
|
||||||
|
> Admins immer zur Verfügung.
|
||||||
|
|
||||||
|
## Voraussetzung
|
||||||
|
|
||||||
|
Du benötigst einen Tarif mit `watchlist`-Berechtigung. Fehlt diese, erscheint
|
||||||
|
beim Lesezeichen ein Hinweis auf den Tarifwechsel
|
||||||
|
(siehe [Pläne & Berechtigungen](/docs/handbook/plaene)).
|
||||||
|
|
||||||
|
## Tool speichern
|
||||||
|
|
||||||
|
- Auf jeder Karte/Zeile im Bereich **Tools durchsuchen** findest du das
|
||||||
|
**Lesezeichen-Icon**.
|
||||||
|
- Ein Klick speichert das Tool in deiner Watchlist — das Icon wird gefüllt.
|
||||||
|
- Ein erneuter Klick entfernt es wieder.
|
||||||
|
|
||||||
|
## Watchlist ansehen
|
||||||
|
|
||||||
|
Öffne die Watchlist über das Benutzermenü oder die Seitenleiste. Sie zeigt alle
|
||||||
|
gespeicherten Tools als Karten. Das gefüllte Lesezeichen auf einer Karte
|
||||||
|
entfernt das Tool aus der Liste.
|
||||||
|
|
||||||
|
## Wo wird die Watchlist gespeichert?
|
||||||
|
|
||||||
|
Die Watchlist ist eine Liste von Tool-IDs in deinen **Benutzerpräferenzen**.
|
||||||
|
Damit ist sie geräteübergreifend mit deinem Konto verbunden.
|
||||||
|
|
||||||
|
API-Referenz: [`GET /auth/me/watchlist`](/docs/reference/endpoints/auth#getMeWatchlist).
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# vX.Y.Z — Release Notes
|
||||||
|
|
||||||
|
> Template für neue Release-Dokumentationen. Eine Kopie pro Release unter
|
||||||
|
> `docs/releases/vX.Y.Z.md` anlegen, Platzhalter ersetzen, Abschnitte die
|
||||||
|
> nicht zutreffen entfernen. Die Seite wird unter `/docs/vX.Y.Z` in der App
|
||||||
|
> angezeigt.
|
||||||
|
|
||||||
|
**Datum:** YYYY-MM-DD · **Tag:** [`vX.Y.Z`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/vX.Y.Z)
|
||||||
|
|
||||||
|
## Neue Features
|
||||||
|
|
||||||
|
- ...
|
||||||
|
|
||||||
|
## Fixes & Verbesserungen
|
||||||
|
|
||||||
|
- ...
|
||||||
|
|
||||||
|
## API-Änderungen
|
||||||
|
|
||||||
|
- ... (neue/geänderte/entfernte Endpunkte — siehe `lib/api-spec/openapi.yaml`)
|
||||||
|
|
||||||
|
## Betrieb / Upgrade
|
||||||
|
|
||||||
|
- **Env-Vars:** ... (neu/geändert/entfernt)
|
||||||
|
- **Migration:** ... (Datenbank-/Schema-Änderungen, Schritte für den Betreiber)
|
||||||
|
- **Breaking Changes:** ... (nur wenn vorhanden)
|
||||||
|
|
||||||
|
## Bekannte Einschränkungen
|
||||||
|
|
||||||
|
- ...
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Commit: [`<short-sha>`](https://git.kubebase.de/admin/tool-evaluator/commit/<short-sha>)
|
||||||
|
- Tag: [`vX.Y.Z`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/vX.Y.Z)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# v0.6.0 — Release Notes
|
||||||
|
|
||||||
|
**Datum:** 2026-08-03 · **Tag:** [`v0.6.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.6.0)
|
||||||
|
|
||||||
|
## Neue Features
|
||||||
|
|
||||||
|
- Vollständige Modernisierung aller Abhängigkeiten auf die aktuellen Hauptversionen
|
||||||
|
(TypeScript 7, Vite 8, React 19.2, openid-client 6, zod 4, recharts 3, react-day-picker 10).
|
||||||
|
|
||||||
|
## Fixes & Verbesserungen
|
||||||
|
|
||||||
|
- CI-Build durch `allowBuilds`-Konfiguration für pnpm 11 repariert
|
||||||
|
(Build-Scripts für esbuild & Co. werden nicht mehr blockiert).
|
||||||
|
- Image-Tagging vereinfacht: nur noch `latest` und `v*`-Tags, keine `nightly-*`/`sha-*`-Tags.
|
||||||
|
- Alle Dependencies exakt gepinnt; automatische Updates via Renovate vorbereitet
|
||||||
|
(`renovate.json`, `docs/dependency-policy.md`).
|
||||||
|
|
||||||
|
## API-Änderungen
|
||||||
|
|
||||||
|
- Keine Breaking Changes an der API. openid-client intern auf v6 migriert
|
||||||
|
(auth-Fluss verhält sich identisch).
|
||||||
|
|
||||||
|
## Betrieb / Upgrade
|
||||||
|
|
||||||
|
- **Env-Vars:** unverändert. Node-Image auf `node:24.18.1-alpine` gepinnt.
|
||||||
|
- **Migration:** keine Datenbank-Migration erforderlich.
|
||||||
|
- **Breaking Changes:** keine.
|
||||||
|
|
||||||
|
## Bekannte Einschränkungen
|
||||||
|
|
||||||
|
- `typedoc` (indirekte orval-Abhängigkeit) zeigt eine Peer-Dependency-Warnung
|
||||||
|
(erwartet TypeScript 5.x/6.x, installiert ist 7.x) — harmlos für Build & Laufzeit.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Commit: [`2f66fff`](https://git.kubebase.de/admin/tool-evaluator/commit/2f66fff)
|
||||||
|
- Tag: [`v0.6.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.6.0)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# v0.7.0 — Release Notes
|
||||||
|
|
||||||
|
**Datum:** 2026-08-03 · **Tag:** [`v0.7.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.7.0)
|
||||||
|
|
||||||
|
## Neue Features
|
||||||
|
|
||||||
|
- Version-gebundene **Release-Dokumentation** in der App unter `/docs`
|
||||||
|
(Index + Detailseite je Version, Markdown aus `docs/releases/`).
|
||||||
|
- Generiertes Release-Vorlage (`docs/releases/TEMPLATE.md`) und
|
||||||
|
Sync-Schritt für den Frontend-Build.
|
||||||
|
|
||||||
|
## Fixes & Verbesserungen
|
||||||
|
|
||||||
|
- `tsx` auf 4.23.4 angehoben — letzte veraltete Abhängigkeit im Workspace
|
||||||
|
(`pnpm outdated -r` ist jetzt leer).
|
||||||
|
|
||||||
|
## API-Änderungen
|
||||||
|
|
||||||
|
- Keine Breaking Changes an der API.
|
||||||
|
|
||||||
|
## Betrieb / Upgrade
|
||||||
|
|
||||||
|
- **Env-Vars:** unverändert.
|
||||||
|
- **Migration:** keine.
|
||||||
|
- **Breaking Changes:** keine.
|
||||||
|
|
||||||
|
## Bekannte Einschränkungen
|
||||||
|
|
||||||
|
- Die Doku ist bisher auf Release-Notes beschränkt; eine vollständige
|
||||||
|
API-/Feld-Referenz folgt in v0.8.0.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Commit: [`520f917`](https://git.kubebase.de/admin/tool-evaluator/commit/520f917)
|
||||||
|
- Tag: [`v0.7.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.7.0)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
|||||||
|
# v0.8.0 — Release Notes
|
||||||
|
|
||||||
|
**Datum:** 2026-08-03 · **Tag:** [`v0.8.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.0)
|
||||||
|
|
||||||
|
## Neue Features
|
||||||
|
|
||||||
|
- **Vollständige Dokumentations-Site** in mkdocs-Optik unter `/docs`:
|
||||||
|
- **Handbuch** mit verständlichen Erklärungen zu allen Features
|
||||||
|
(Erste Schritte, Tool anlegen, Bewertungen, Vergleichen, Watchlist,
|
||||||
|
Analytics, Administration, Datenmodell).
|
||||||
|
- **Automatisch generierte Referenz** aus `lib/api-spec/openapi.yaml`:
|
||||||
|
alle Endpunkte und Datenfelder (Typ, Pflichtstatus, Constraints) —
|
||||||
|
damit ist garantiert, dass *jedes* Feature dokumentiert ist.
|
||||||
|
- **Suche** über Handbuch, Endpunkte und Felder.
|
||||||
|
- **Versions-Dropdown**: ältere Releases behalten ihre vollständige
|
||||||
|
Feld-/Endpunkt-Referenz als Snapshot.
|
||||||
|
- **Repo-Link** oben rechts zur Quelle.
|
||||||
|
- **Hilfe-Buttons (?) in Formularen** (NetBox-Stil): neben jedem Feld
|
||||||
|
springt ein Icon direkt zur Feldbeschreibung in der Doku.
|
||||||
|
|
||||||
|
## Fixes & Verbesserungen
|
||||||
|
|
||||||
|
- Doku-Generator `scripts/src/generate-docs.mjs` ersetzt den bisherigen
|
||||||
|
`sync-release-docs.mjs` (OpenAPI-Parsing, Handbuch, Suchindex, Snapshots).
|
||||||
|
- Dokumentation für v0.7.0 nachgezogen.
|
||||||
|
|
||||||
|
## API-Änderungen
|
||||||
|
|
||||||
|
- Keine Breaking Changes an der API.
|
||||||
|
|
||||||
|
## Betrieb / Upgrade
|
||||||
|
|
||||||
|
- **Env-Vars:** unverändert.
|
||||||
|
- **Migration:** keine.
|
||||||
|
- **Breaking Changes:** keine.
|
||||||
|
|
||||||
|
## Bekannte Einschränkungen
|
||||||
|
|
||||||
|
- Handbuch & Referenz gelten für die aktuelle Version; ältere Versionen
|
||||||
|
zeigen ihre Release-Notes und einen Referenz-Snapshot, sofern beim Release
|
||||||
|
erzeugt (`node scripts/src/generate-docs.mjs --snapshot vX.Y.Z`).
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Commit: [`6c92b63`](https://git.kubebase.de/admin/tool-evaluator/commit/6c92b63)
|
||||||
|
- Tag: [`v0.8.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.0)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
|||||||
|
# v0.8.1 — Release Notes
|
||||||
|
|
||||||
|
**Datum:** 2026-08-03 · **Tag:** [`v0.8.1`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.1)
|
||||||
|
|
||||||
|
## Neue Features
|
||||||
|
|
||||||
|
- **Standalone-Doku-Seite**: Die Dokumentation steht jetzt als eigene,
|
||||||
|
mkdocs-artige Seite unter `toolr.kubebase.de/docs` — ohne die App-Shell
|
||||||
|
(eigene Kopfzeile mit Repo-Link, Versions-Dropdown, Suche, Theme-Umschalter
|
||||||
|
und Link zurück zur App).
|
||||||
|
- **Navigation umbenannt**: Der Seitenleisten-Eintrag heißt jetzt **„Hilfe"**
|
||||||
|
und führt zur Standalone-Doku.
|
||||||
|
- **User Guide komplett überarbeitet**: 17 Handbuch-Seiten mit
|
||||||
|
Schritt-für-Schritt-Anleitungen für alle Funktionen (Tools finden, Tool
|
||||||
|
anlegen, Bewerten, Watchlist, Vergleichen, Kosten, Analytics, Pläne,
|
||||||
|
Administration, Redundanz, Papierkorb, Tastenkürzel, Datenmodell).
|
||||||
|
|
||||||
|
## Fixes & Verbesserungen
|
||||||
|
|
||||||
|
- Referenz-Links sind jetzt unabhängig von Groß-/Kleinschreibung
|
||||||
|
(Schema-/Endpoint-Slugs wie `toolinput` und `ToolInput` funktionieren beide).
|
||||||
|
- Handbuch-Links auf Endpunkt-Anker korrigiert (PascalCase-OperationIds).
|
||||||
|
- Veraltete, kaputte Handbuch-Links (`vergleichen`, `watchlist` …) ersetzt.
|
||||||
|
- Dokumentations-Tabellenkopfzeilen und Hinweistexte in der Doku-Seite über
|
||||||
|
i18n internationalisiert (de/en).
|
||||||
|
|
||||||
|
## API-Änderungen
|
||||||
|
|
||||||
|
- Keine Änderungen an der API.
|
||||||
|
|
||||||
|
## Betrieb / Upgrade
|
||||||
|
|
||||||
|
- **Env-Vars:** unverändert.
|
||||||
|
- **Migration:** keine.
|
||||||
|
- **Breaking Changes:** keine. Die Doku-Seite ist unter `/docs` erreichbar wie
|
||||||
|
bisher; lediglich die Darstellung ist nun eigenständig.
|
||||||
|
|
||||||
|
## Bekannte Einschränkungen
|
||||||
|
|
||||||
|
- Handbuch & Referenz gelten für die aktuelle Version; ältere Versionen zeigen
|
||||||
|
ihre Release-Notes und einen Referenz-Snapshot, sofern beim Release erzeugt
|
||||||
|
(`node scripts/src/generate-docs.mjs --snapshot vX.Y.Z`).
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Commit: [`fdf2d74`](https://git.kubebase.de/admin/tool-evaluator/commit/fdf2d74)
|
||||||
|
- Tag: [`v0.8.1`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.1)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
|
||||||
|
# Administration
|
||||||
|
|
||||||
|
Der Bereich **Admin** (`/admin`) ist ausschließlich für Admins zugänglich.
|
||||||
|
Ohne Admin-Rolle erscheint eine Zugriffsverweigerung.
|
||||||
|
|
||||||
|
> Oben rechts führt die Schaltfläche **Redundanz-Dashboard** zur automatischen
|
||||||
|
> Doppelungs-Erkennung (siehe [Redundanz](/docs/handbook/redundanz)).
|
||||||
|
|
||||||
|
## Tab „Nutzer"
|
||||||
|
|
||||||
|
Verwaltung der lokalen Konten.
|
||||||
|
|
||||||
|
- **Nutzer hinzufügen:** Benutzername (Pflicht), Passwort (mind. 6 Zeichen),
|
||||||
|
E-Mail (optional), **Rolle** (User/Admin), **Tarif** (Free/Premium/Enterprise).
|
||||||
|
- **Nutzer bearbeiten:** Rolle, Tarif und (für lokale Konten) ein neues Passwort
|
||||||
|
setzen. Für OIDC-Konten wird die Passwortverwaltung im Identitätsanbieter
|
||||||
|
(z. B. Keycloak) angeboten.
|
||||||
|
- **Nutzer löschen:** Entfernt das Konto endgültig (nicht für das eigene Konto).
|
||||||
|
|
||||||
|
API-Referenz:
|
||||||
|
[`POST /users`](/docs/reference/endpoints/users#createUser),
|
||||||
|
[`PATCH /users/{id}`](/docs/reference/endpoints/users#updateUser),
|
||||||
|
[`DELETE /users/{id}`](/docs/reference/endpoints/users#deleteUser).
|
||||||
|
|
||||||
|
## Tab „Tools"
|
||||||
|
|
||||||
|
Zentraler Zugriff auf den Tool-Katalog.
|
||||||
|
|
||||||
|
- **Suchen** nach Tools.
|
||||||
|
- Tools einzeln ansehen, bearbeiten oder in den Papierkorb verschieben.
|
||||||
|
- **Massenaktion:** mehrere Tools auswählen und in den Papierkorb verschieben
|
||||||
|
(Bestätigungsdialog; soft gelöschte Tools sind aus allen öffentlichen Ansichten
|
||||||
|
entfernt und können wiederhergestellt oder endgültig gelöscht werden).
|
||||||
|
|
||||||
|
## Tab „Audit-Log"
|
||||||
|
|
||||||
|
Chronologisches Protokoll aller Anlage-, Änderungs- und Löschvorgänge
|
||||||
|
(max. 100 Einträge): Aktion, Entität + ID, Zeitstempel, ausführende Person und
|
||||||
|
geänderte Felder.
|
||||||
|
|
||||||
|
API-Referenz: [`GET /audit-logs`](/docs/reference/endpoints/audit#listAuditLogs).
|
||||||
|
|
||||||
|
## Tab „System"
|
||||||
|
|
||||||
|
Versionsinformationen der laufenden Instanz:
|
||||||
|
|
||||||
|
- **Version** (z. B. `v0.8.1`),
|
||||||
|
- **Commit** (7-stelliger SHA, verlinkt zum Repository),
|
||||||
|
- **Build-Datum**,
|
||||||
|
- **Papierkorb-Aufbewahrung** („N Tage" oder „Für immer").
|
||||||
|
|
||||||
|
## Tool-Verknüpfungen (Admin)
|
||||||
|
|
||||||
|
Auf der Detailseite eines Tools kannst du als Admin **Verknüpfungen**
|
||||||
|
(eigene/„manual" sowie automatisch erkannte) verwalten:
|
||||||
|
|
||||||
|
- **Tool verknüpfen:** Dialog mit Tool-ID, **Beziehungstyp**
|
||||||
|
(Ähnlich / Ersetzt / Abgelöst durch) und optionalen Notizen.
|
||||||
|
- Beziehungstypen werden als Badges auf der Detailseite angezeigt.
|
||||||
|
- Manuelle Verknüpfungen lassen sich per Papierkorb-Icon wieder entfernen.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
# Analytics
|
||||||
|
|
||||||
|
Der Bereich **Analytics** (`/analytics`) ist ein öffentliches Dashboard mit
|
||||||
|
Kennzahlen und Diagrammen auf Basis aller Tools und Bewertungen.
|
||||||
|
|
||||||
|
## Kennzahlen (KPI-Karten)
|
||||||
|
|
||||||
|
- **Anzahl Tools** — wie viele Tools sind im Katalog erfasst.
|
||||||
|
- **Anzahl Bewertungen** — wie viele Bewertungen wurden insgesamt abgegeben.
|
||||||
|
- **Aktive Kategorien** — wie viele Kategorien existieren.
|
||||||
|
- **Durchschnittliche Bewertung** — globaler kombinierter Wert.
|
||||||
|
|
||||||
|
## Diagramme
|
||||||
|
|
||||||
|
| Diagramm | Inhalt |
|
||||||
|
| --- | --- |
|
||||||
|
| **Top 8 Tools** | Balkendiagramm der Tools mit der höchsten kombinierten Punktzahl (0–5) |
|
||||||
|
| **Tools je Kategorie** | Radar-Diagramm der Tool-Anzahl pro Kategorie |
|
||||||
|
| **Punkteverteilung** | Zwei horizontale Balken-Diagramme (Nützlichkeit & Bedienbarkeit) pro Stern |
|
||||||
|
|
||||||
|
Die Diagramme sind interaktiv (Tooltips beim Überfahren).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- [`GET /analytics/summary`](/docs/reference/endpoints/analytics#getAnalyticsSummary)
|
||||||
|
- [`GET /analytics/top-tools`](/docs/reference/endpoints/analytics#getTopTools)
|
||||||
|
- [`GET /analytics/by-category`](/docs/reference/endpoints/analytics#getAnalyticsByCategory)
|
||||||
|
- [`GET /analytics/rating-distribution`](/docs/reference/endpoints/analytics#getRatingDistribution)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
# Bewerten
|
||||||
|
|
||||||
|
Auf der Detailseite eines Tools kannst du deine Erfahrung teilen. Klicke auf
|
||||||
|
**Bewertung abgeben** (erfordert ein Konto).
|
||||||
|
|
||||||
|
## Formularfelder
|
||||||
|
|
||||||
|
| Feld | Pflicht | Hinweise |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Nützlichkeit** | Ja | 1–5 Sterne |
|
||||||
|
| **Bedienbarkeit** | Ja | 1–5 Sterne |
|
||||||
|
| **Kommentar** | Nein | Freitext |
|
||||||
|
| **Name** | Nein | Standard „Anonym" |
|
||||||
|
|
||||||
|
Neben den Feldern führt das **?‑Icon** direkt zur zugehörigen Feldbeschreibung
|
||||||
|
in der [Datenmodell-Referenz](/docs/reference/schemas/ratinginput).
|
||||||
|
|
||||||
|
## Was passiert nach dem Abgeben?
|
||||||
|
|
||||||
|
- Deine Bewertung wird sofort gespeichert und erscheint in der
|
||||||
|
**Bewertungsliste** der Detailseite.
|
||||||
|
- Die **Durchschnittswerte** (Nützlichkeit, Bedienbarkeit, Kombiniert) und die
|
||||||
|
**Punkteverteilung** werden aktualisiert.
|
||||||
|
- Die **Statistiken** im Bereich [Analytics](/docs/handbook/analytics) werden
|
||||||
|
neu berechnet.
|
||||||
|
|
||||||
|
## Statistik-Bereiche auf der Detailseite
|
||||||
|
|
||||||
|
- **Bewertungsübersicht:** Nützlichkeit & Bedienbarkeit als Durchschnitt mit
|
||||||
|
Fortschrittsbalken.
|
||||||
|
- **Punkteverteilung:** Anzahl der Bewertungen pro Stern (1★–5★).
|
||||||
|
- **Verlauf:** Linienchart der kombinierten/Teilwerte über die Zeit
|
||||||
|
(erst ab mehreren Bewertungen sichtbar).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- [`POST /tools/{id}/ratings`](/docs/reference/endpoints/ratings#createRating) — Bewertung abgeben
|
||||||
|
- [`GET /tools/{id}/ratings`](/docs/reference/endpoints/ratings#listToolRatings) — Bewertungen eines Tools
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
|
||||||
|
# Datenmodell
|
||||||
|
|
||||||
|
Dieses Kapitel erklärt die zentralen Datenobjekte von toolr auf Ebene der
|
||||||
|
Anwendung. Die vollständige, automatisch generierte Referenz aller Felder,
|
||||||
|
Typen und Constraints findest du in der
|
||||||
|
[API-Referenz](/docs/reference/schemas/tool).
|
||||||
|
|
||||||
|
## Tool
|
||||||
|
|
||||||
|
Das Herzstück: ein im Katalog erfasstes Werkzeug.
|
||||||
|
|
||||||
|
| Eigenschaft | Beschreibung |
|
||||||
|
| --- | --- |
|
||||||
|
| `id` | Eindeutige Kennung |
|
||||||
|
| `name` | Anzeigename |
|
||||||
|
| `description` | Beschreibung (Was macht das Tool?) |
|
||||||
|
| `category` | Kategorie-Zuordnung |
|
||||||
|
| `websiteUrl` | Offizielle Website (optional) |
|
||||||
|
| `iconUrl` | Logo-/Icon-URL (optional) |
|
||||||
|
| `features` | Liste von Fähigkeiten |
|
||||||
|
| `tags` | Liste von Schlagwörtern |
|
||||||
|
| `createdAt` / `updatedAt` | Zeitstempel |
|
||||||
|
| `createdBy` | Erstellende Person |
|
||||||
|
| `deletedAt` / `deletedBy` | Soft-Löschung (Papierkorb) |
|
||||||
|
|
||||||
|
Eingabe-Formulare verwenden die abgeleiteten Schemas
|
||||||
|
[`ToolInput`](/docs/reference/schemas/toolinput) und
|
||||||
|
[`ToolUpdate`](/docs/reference/schemas/toolupdate).
|
||||||
|
Aggregierte Ansichten liefert [`ToolWithStats`](/docs/reference/schemas/toolwithstats)
|
||||||
|
(z. B. mit Durchschnittsbewertung).
|
||||||
|
|
||||||
|
## Rating (Bewertung)
|
||||||
|
|
||||||
|
Eine einzelne Bewertung zu einem Tool:
|
||||||
|
|
||||||
|
- `usefulness` (Nützlichkeit, 1–5) und `usability` (Bedienbarkeit, 1–5)
|
||||||
|
- optional `comment` und ein Anzeigename (`reviewerName`)
|
||||||
|
- Zeitstempel
|
||||||
|
|
||||||
|
Eingabe-Schema: [`RatingInput`](/docs/reference/schemas/ratinginput).
|
||||||
|
|
||||||
|
## User & Auth
|
||||||
|
|
||||||
|
- [`User`](/docs/reference/schemas/user) — Benutzerkonto mit Rolle (User/Admin)
|
||||||
|
und Tarif (Free/Premium/Enterprise).
|
||||||
|
- [`AuthUser`](/docs/reference/schemas/authuser) — das angemeldete Profil
|
||||||
|
inklusive `entitlements` (verfügbare Features).
|
||||||
|
- [`UserPreferences`](/docs/reference/schemas/userpreferences) — Ansichts- und
|
||||||
|
Dichte-Präferenzen sowie die `watchlist` (Liste von Tool-IDs).
|
||||||
|
|
||||||
|
## Analytics
|
||||||
|
|
||||||
|
Die Statistik-Endpunkte liefern aggregierte Daten:
|
||||||
|
|
||||||
|
- [`AnalyticsSummary`](/docs/reference/schemas/analyticssummary) — globale
|
||||||
|
Kennzahlen (Anzahl Tools/Bewertungen, Kategorien, Durchschnitt).
|
||||||
|
- [`TopToolEntry`](/docs/reference/schemas/TopToolEntry) — ein Eintrag der
|
||||||
|
Top-Tools.- [`CategoryStats`](/docs/reference/schemas/categorystats) — Tool-Anzahl je
|
||||||
|
Kategorie.
|
||||||
|
- [`RatingDistribution`](/docs/reference/schemas/ratingdistribution) —
|
||||||
|
Punkteverteilung (Nützlichkeit & Bedienbarkeit).
|
||||||
|
- [`ScoreBucket`](/docs/reference/schemas/scorebucket) — ein Werte-Bucket.
|
||||||
|
|
||||||
|
## Weitere
|
||||||
|
|
||||||
|
- [`VersionInfo`](/docs/reference/schemas/versioninfo) — Version, Commit-SHA,
|
||||||
|
Build-Datum und Papierkorb-Aufbewahrung der laufenden Instanz.
|
||||||
|
- [`AuditLog`](/docs/reference/schemas/auditlog) — ein Protokolleintrag
|
||||||
|
(Aktion, Entität, Zeitstempel, Akteur, Änderungen).
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
|
||||||
|
# Erste Schritte
|
||||||
|
|
||||||
|
Diese Seite führt dich durch die wichtigsten Abläufe in toolr — vom ersten
|
||||||
|
Besuch bis zum Anlegen und Bewerten eines Tools.
|
||||||
|
|
||||||
|
## 1. Anmelden
|
||||||
|
|
||||||
|
Die meisten Aktionen (Tool anlegen, bewerten, Watchlist, Vergleichen) erfordern
|
||||||
|
ein Konto. Klicke unten links auf **Anmelden**. Je nach Konfiguration der
|
||||||
|
Instanz hast du zwei Möglichkeiten:
|
||||||
|
|
||||||
|
- **Lokale Konten:** Benutzername + Passwort. Der Zugang wird von einem Admin
|
||||||
|
angelegt (siehe [Administration](/docs/handbook/administration)).
|
||||||
|
- **OIDC (SSO):** Anmelden mit dem konfigurierten Identitätsanbieter (z. B.
|
||||||
|
Keycloak).
|
||||||
|
|
||||||
|
Welcher Modus aktiv ist, steht im Endpunkt
|
||||||
|
[`GET /auth/mode`](/docs/reference/endpoints/auth#getAuthMode). Details findest
|
||||||
|
du im Abschnitt [Anmelden & Konto](/docs/handbook/konto).
|
||||||
|
|
||||||
|
## 2. Tools finden
|
||||||
|
|
||||||
|
Öffne den Bereich **Tools durchsuchen**:
|
||||||
|
|
||||||
|
- **Suchen** — Volltextsuche über Name & Beschreibung (Tastenkürzel `/`).
|
||||||
|
- **Filtern** — nach Kategorie, Tags, Features und Mindestbewertung
|
||||||
|
(`minRating`).
|
||||||
|
- **Sortieren** — nach Aktualität, Top-Bewertung, meistbewertet, Name
|
||||||
|
(auf-/absteigend) oder letztem Update.
|
||||||
|
|
||||||
|
Alle Optionen im Detail: [Tools finden & durchsuchen](/docs/handbook/tools-finden).
|
||||||
|
|
||||||
|
## 3. Tool anlegen
|
||||||
|
|
||||||
|
Gehe auf **Tool hinzufügen** und fülle das Formular aus. Details zu jedem Feld
|
||||||
|
findest du im Abschnitt [Tool anlegen](/docs/handbook/tool-anlegen) und in der
|
||||||
|
[Feld-Referenz](/docs/reference/schemas/toolinput).
|
||||||
|
|
||||||
|
## 4. Bewerten
|
||||||
|
|
||||||
|
Auf der Detailseite eines Tools kannst du **Nützlichkeit** und **Bedienbarkeit**
|
||||||
|
(jeweils 1–5) vergeben und optional einen Kommentar hinterlassen. Deine
|
||||||
|
Bewertung fließt sofort in die Statistiken ein.
|
||||||
|
Siehe [Bewerten](/docs/handbook/bewerten).
|
||||||
|
|
||||||
|
## 5. Weiterführend
|
||||||
|
|
||||||
|
- [Tools vergleichen](/docs/handbook/vergleichen)
|
||||||
|
- [Watchlist](/docs/handbook/watchlist)
|
||||||
|
- [Analytics](/docs/handbook/analytics)
|
||||||
|
- [Pläne & Berechtigungen](/docs/handbook/plaene)
|
||||||
|
- [Administration](/docs/handbook/administration)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"slug": "index",
|
||||||
|
"file": "index.md",
|
||||||
|
"title": "Überblick",
|
||||||
|
"order": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "getting-started",
|
||||||
|
"file": "getting-started.md",
|
||||||
|
"title": "Erste Schritte",
|
||||||
|
"order": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "konto",
|
||||||
|
"file": "konto.md",
|
||||||
|
"title": "Anmelden & Konto",
|
||||||
|
"order": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "tools-finden",
|
||||||
|
"file": "tools-finden.md",
|
||||||
|
"title": "Tools finden & durchsuchen",
|
||||||
|
"order": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "tool-anlegen",
|
||||||
|
"file": "tool-anlegen.md",
|
||||||
|
"title": "Tool anlegen",
|
||||||
|
"order": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "tool-bearbeiten",
|
||||||
|
"file": "tool-bearbeiten.md",
|
||||||
|
"title": "Tool bearbeiten & löschen",
|
||||||
|
"order": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "bewerten",
|
||||||
|
"file": "bewerten.md",
|
||||||
|
"title": "Bewerten",
|
||||||
|
"order": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "watchlist",
|
||||||
|
"file": "watchlist.md",
|
||||||
|
"title": "Watchlist",
|
||||||
|
"order": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "vergleichen",
|
||||||
|
"file": "vergleichen.md",
|
||||||
|
"title": "Vergleichen",
|
||||||
|
"order": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "analytics",
|
||||||
|
"file": "analytics.md",
|
||||||
|
"title": "Analytics",
|
||||||
|
"order": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "plaene",
|
||||||
|
"file": "plaene.md",
|
||||||
|
"title": "Pläne & Berechtigungen",
|
||||||
|
"order": 11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "kosten",
|
||||||
|
"file": "kosten.md",
|
||||||
|
"title": "Kosten erfassen",
|
||||||
|
"order": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "administration",
|
||||||
|
"file": "administration.md",
|
||||||
|
"title": "Administration",
|
||||||
|
"order": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "redundanz",
|
||||||
|
"file": "redundanz.md",
|
||||||
|
"title": "Redundanz-Dashboard",
|
||||||
|
"order": 14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "papierkorb",
|
||||||
|
"file": "papierkorb.md",
|
||||||
|
"title": "Papierkorb",
|
||||||
|
"order": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "tastatur",
|
||||||
|
"file": "tastatur.md",
|
||||||
|
"title": "Tastenkürzel & Kommandopalette",
|
||||||
|
"order": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "datenmodell",
|
||||||
|
"file": "datenmodell.md",
|
||||||
|
"title": "Datenmodell",
|
||||||
|
"order": 17
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
|
||||||
|
# Willkommen bei toolr
|
||||||
|
|
||||||
|
toolr ist eine Plattform zum **Entdecken, Bewerten und Vergleichen von
|
||||||
|
Entwicklungstools**. Nutzer:innen pflegen einen gemeinsamen Katalog von Tools,
|
||||||
|
vergeben Bewertungen (Nützlichkeit & Bedienbarkeit) und nutzen Statistiken, um
|
||||||
|
die richtige Wahl zu treffen.
|
||||||
|
|
||||||
|
## Was kannst du mit toolr tun?
|
||||||
|
|
||||||
|
| Funktion | Beschreibung | Sichtbarkeit |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Tools durchsuchen** | Katalog filtern, sortieren und durchsuchen | Alle |
|
||||||
|
| **Tool anlegen** | Neues Tool mit Beschreibung, Kategorie, Features & Tags eintragen | Angemeldet |
|
||||||
|
| **Tool bearbeiten/löschen** | Eigene Tools pflegen (Ersteller:in oder Admin) | Angemeldet |
|
||||||
|
| **Bewerten** | Nützlichkeit & Bedienbarkeit (1–5) plus Kommentar vergeben | Angemeldet |
|
||||||
|
| **Watchlist** | Tools als Favoriten speichern | Premium |
|
||||||
|
| **Vergleichen** | Tools nebeneinander gegenüberstellen | Premium |
|
||||||
|
| **Kosten erfassen** | Lizenz- und Kostenmodelle je Tool eintragen | Premium |
|
||||||
|
| **Analytics** | Statistiken, Top-Tools, Verteilungen | Alle |
|
||||||
|
| **Papierkorb** | Soft-gelöschte Tools wiederherstellen oder endgültig löschen | Premium |
|
||||||
|
| **Admin** | Nutzerverwaltung, Audit-Log, Systeminformationen | Admin |
|
||||||
|
| **Redundanz** | Automatische Doppelungs-Erkennung | Admin |
|
||||||
|
|
||||||
|
## Wie diese Doku aufgebaut ist
|
||||||
|
|
||||||
|
- **User Guide** (diese Seiten): Schritt-für-Schritt-Anleitungen für alle
|
||||||
|
Funktionen — von den [Ersten Schritten](/docs/handbook/getting-started) bis
|
||||||
|
zur [Administration](/docs/handbook/administration).
|
||||||
|
- **API-Referenz**: automatisch aus der OpenAPI-Spezifikation generiert — alle
|
||||||
|
[Endpunkte](/docs/reference/endpoints/tools) und
|
||||||
|
[Datenfelder](/docs/reference/schemas/toolinput) der aktuellen Version.
|
||||||
|
- **Release-Notes**: Was ist in welcher [Version](/docs/releases/v0.8.1) neu.
|
||||||
|
|
||||||
|
## Der Einstieg
|
||||||
|
|
||||||
|
Der schnellste Weg:
|
||||||
|
|
||||||
|
1. **Anmelden** — ohne Konto kannst du nur stöbern
|
||||||
|
(siehe [Erste Schritte](/docs/handbook/getting-started#1-anmelden)).
|
||||||
|
2. **Tools finden** — Suche, Filter und Sortierung im Bereich
|
||||||
|
[Tools durchsuchen](/docs/handbook/tools-finden).
|
||||||
|
3. **Tool anlegen** — über „Tool hinzufügen"
|
||||||
|
([Anleitung](/docs/handbook/tool-anlegen)).
|
||||||
|
4. **Bewerten** — auf der Detailseite eines Tools
|
||||||
|
([Anleitung](/docs/handbook/bewerten)).
|
||||||
|
|
||||||
|
## Kontakt & Quellcode
|
||||||
|
|
||||||
|
Der Quellcode liegt unter
|
||||||
|
[git.kubebase.de/admin/tool-evaluator](https://git.kubebase.de/admin/tool-evaluator) —
|
||||||
|
über das Repository-Icon oben rechts erreichst du ihn jederzeit.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
|
||||||
|
# Anmelden & Konto
|
||||||
|
|
||||||
|
## Anmelden
|
||||||
|
|
||||||
|
Klicke unten links in der Seitenleiste auf **Anmelden**. Je nach Konfiguration
|
||||||
|
der Instanz:
|
||||||
|
|
||||||
|
- **Lokale Konten:** Benutzername und Passwort eingeben. Die Konten werden von
|
||||||
|
einem Admin angelegt (siehe [Administration](/docs/handbook/administration)).
|
||||||
|
- **OIDC (SSO):** Du wirst an den konfigurierten Identitätsanbieter
|
||||||
|
weitergeleitet und meldest dich dort an.
|
||||||
|
|
||||||
|
Der aktive Modus steht im Endpunkt
|
||||||
|
[`GET /auth/mode`](/docs/reference/endpoints/auth#getAuthMode).
|
||||||
|
|
||||||
|
> Die Login-Seite erreichst du direkt unter `/login`. Nach erfolgreicher
|
||||||
|
> Anmeldung wirst du zur ursprünglich aufgerufenen Seite zurückgeleitet.
|
||||||
|
|
||||||
|
## Benutzerprofil
|
||||||
|
|
||||||
|
Dein Profil (Avatar, Name, E-Mail, Tarif) siehst du unten links im
|
||||||
|
Benutzermenü. Dort stehen dir folgende Aktionen zur Verfügung:
|
||||||
|
|
||||||
|
- **Watchlist** — deine gespeicherten Tools (nur mit dem entsprechenden Tarif).
|
||||||
|
- **Papierkorb** — wiederherstellbare, gelöschte Tools (Premium/Enterprise).
|
||||||
|
- **Passwort ändern** — für lokale Konten direkt in toolr; für OIDC-Konten wird
|
||||||
|
die Passwortverwaltung im Identitätsanbieter angeboten.
|
||||||
|
- **Abmelden** — beendet deine Sitzung.
|
||||||
|
|
||||||
|
## Passwort ändern (lokales Konto)
|
||||||
|
|
||||||
|
1. Öffne das Benutzermenü unten links.
|
||||||
|
2. Wähle **Passwort ändern**.
|
||||||
|
3. Gib das **aktuelle** sowie ein **neues** Passwort ein (mind. 6 Zeichen) und
|
||||||
|
bestätige es.
|
||||||
|
4. Speichern — das Passwort wird sofort übernommen.
|
||||||
|
|
||||||
|
API-Referenz: [`POST /auth/me/password`](/docs/reference/endpoints/auth#changeMyPassword).
|
||||||
|
|
||||||
|
## Anzeigeeinstellungen
|
||||||
|
|
||||||
|
Über die Schaltflächen oben rechts kannst du:
|
||||||
|
|
||||||
|
- **Sprache** wechseln (Deutsch / Englisch),
|
||||||
|
- **Theme** umschalten (Hell / Dunkel / System),
|
||||||
|
- die **Listenansicht** und **Dichte** im Bereich Tools durchsuchen anpassen
|
||||||
|
(siehe [Tools finden & durchsuchen](/docs/handbook/tools-finden)).
|
||||||
|
|
||||||
|
Deine Präferenzen (inkl. Watchlist) werden im Endpunkt
|
||||||
|
[`GET /auth/me/preferences`](/docs/reference/endpoints/auth#getMePreferences)
|
||||||
|
gespeichert und über [`PUT /auth/me/preferences`](/docs/reference/endpoints/auth#updateMePreferences)
|
||||||
|
aktualisiert.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
# Kosten erfassen
|
||||||
|
|
||||||
|
Auf der Detailseite eines Tools kannst du Kosten- und Lizenzmodelle eintragen,
|
||||||
|
damit die Gesamtkosten je Tool transparent werden.
|
||||||
|
|
||||||
|
> Kosten ist ein **Premium-Feature** (`costs`, Premium/Enterprise). Admins
|
||||||
|
> haben immer Zugriff.
|
||||||
|
|
||||||
|
## Kosten hinzufügen
|
||||||
|
|
||||||
|
Klicke auf **Kosten hinzufügen** im Kosten-Bereich der Detailseite und fülle
|
||||||
|
das Formular aus:
|
||||||
|
|
||||||
|
| Feld | Hinweise |
|
||||||
|
| --- | --- |
|
||||||
|
| **Lizenztyp** | Free / Subscription / One-Time / Usage-Based |
|
||||||
|
| **Abrechnungszeitraum** | Nur für „Subscription": Monatlich / Quartalsweise / Jährlich |
|
||||||
|
| **Kosten** | Betrag als Zahl |
|
||||||
|
| **Währung** | EUR / USD / GBP / CHF |
|
||||||
|
| **Notizen** | Optionaler Freitext |
|
||||||
|
|
||||||
|
Speichern legt den Eintrag an. Jeder Kosten-Eintrag wird als Karte mit
|
||||||
|
Lizenz-Badge, Abrechnungszeitraum, Betrag (`Betrag Währung` bzw. „Free") und
|
||||||
|
Notizen angezeigt.
|
||||||
|
|
||||||
|
## Kosten bearbeiten & löschen
|
||||||
|
|
||||||
|
Beim Überfahren einer Kosten-Karte erscheinen die Aktionen **Bearbeiten**
|
||||||
|
(Bleistift) und **Löschen** (Papierkorb).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Die Kosten-Daten werden über die Tool-Endpunkte verwaltet
|
||||||
|
(siehe [API-Referenz](/docs/reference/endpoints/tools)).
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
# Papierkorb
|
||||||
|
|
||||||
|
Der **Papierkorb** (`/trash`) enthält soft gelöschte Tools. Mit Papierkorb-Zugang
|
||||||
|
können sie wiederhergestellt werden; endgültiges Löschen ist Admins vorbehalten.
|
||||||
|
|
||||||
|
> Der Papierkorb ist ein **Premium-Feature** (`trash`, Premium/Enterprise).
|
||||||
|
> Admins haben immer Zugriff.
|
||||||
|
|
||||||
|
## Zugang
|
||||||
|
|
||||||
|
Der Papierkorb ist über das Benutzermenü oder die Seitenleiste erreichbar.
|
||||||
|
Ohne `trash`-Berechtigung erscheint ein Hinweis auf den Tarifwechsel.
|
||||||
|
|
||||||
|
## Wiederherstellen
|
||||||
|
|
||||||
|
- Markiere ein oder mehrere Tools (Checkboxen).
|
||||||
|
- Klicke auf **Wiederherstellen (N)** — die Tools erscheinen wieder in allen
|
||||||
|
öffentlichen Ansichten.
|
||||||
|
|
||||||
|
> Wiederherstellen steht jeder Person mit Papierkorb-Zugang zur Verfügung.
|
||||||
|
|
||||||
|
## Endgültig löschen (nur Admin)
|
||||||
|
|
||||||
|
- **Löschen (N)** entfernt die ausgewählten Tools **endgültig** — inklusive
|
||||||
|
aller Bewertungen, Kosten und Verknüpfungen. Das kann nicht rückgängig
|
||||||
|
gemacht werden.
|
||||||
|
- **Papierkorb leeren** entfernt alle soft gelöschten Tools endgültig.
|
||||||
|
|
||||||
|
## Tabelle
|
||||||
|
|
||||||
|
Der Papierkorb listet: Name, Kategorie, **Gelöscht am** (`tt.MM.jjjj HH:mm`),
|
||||||
|
**Gelöscht von** sowie Aktionen (Wiederherstellen; Löschen nur Admin). Die Suche
|
||||||
|
filtert nach Namen.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- [`GET /tools/trash`](/docs/reference/endpoints/tools#listTrashedTools) — Liste
|
||||||
|
- [`POST /tools/trash/restore`](/docs/reference/endpoints/tools#restoreTools) — Wiederherstellen
|
||||||
|
- [`DELETE /tools/trash`](/docs/reference/endpoints/tools#deleteTrashedTools) — Endgültig löschen (Admin)
|
||||||
|
- [`POST /tools/trash/empty`](/docs/reference/endpoints/tools#emptyTrash) — Papierkorb leeren (Admin)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
# Pläne & Berechtigungen
|
||||||
|
|
||||||
|
toolr unterscheidet **Tarife** (Tier) und **Rollen**. Admins umgehen alle
|
||||||
|
Feature-Beschränkungen.
|
||||||
|
|
||||||
|
## Tarife
|
||||||
|
|
||||||
|
| Tarif | Beschreibung |
|
||||||
|
| --- | --- |
|
||||||
|
| **Free** | Grundfunktionen: suchen, filtern, ansehen, Analytics |
|
||||||
|
| **Premium** | Zusätzlich Watchlist, Vergleichen, Papierkorb, Kosten |
|
||||||
|
| **Enterprise** | Alle Premium-Features + erweiterter Support |
|
||||||
|
|
||||||
|
### Feature-Berechtigungen
|
||||||
|
|
||||||
|
Premium/Enterprise schalten folgende Features frei:
|
||||||
|
|
||||||
|
| Feature | Funktion | Mehr erfahren |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `compare` | Tools vergleichen | [Vergleichen](/docs/handbook/vergleichen) |
|
||||||
|
| `watchlist` | Favoritenliste | [Watchlist](/docs/handbook/watchlist) |
|
||||||
|
| `trash` | Papierkorb (soft gelöschte Tools) | [Papierkorb](/docs/handbook/papierkorb) |
|
||||||
|
| `costs` | Kosten-/Lizenzmodelle erfassen | [Kosten erfassen](/docs/handbook/kosten) |
|
||||||
|
|
||||||
|
Fehlt dir ein Feature, zeigt die App einen **Upgrade-Hinweis** mit Link zur
|
||||||
|
Tarifverwaltung.
|
||||||
|
|
||||||
|
## Rollen
|
||||||
|
|
||||||
|
| Rolle | Berechtigungen |
|
||||||
|
| --- | --- |
|
||||||
|
| **User** | Standard-Konto: Tools anlegen/bewerten, eigene Tools bearbeiten |
|
||||||
|
| **Admin** | Alle User-Rechte + Verwaltung, Audit-Log, Redundanz, Papierkorb leeren, Tool-Verknüpfungen |
|
||||||
|
|
||||||
|
Admins passieren **alle** Feature-Checks — auch ohne Premium-Tarif.
|
||||||
|
|
||||||
|
## Tarif-/Rollenverwaltung
|
||||||
|
|
||||||
|
Die Zuordnung von Rolle und Tarif wird durch Admins im Bereich
|
||||||
|
[Administration](/docs/handbook/administration) (Tab „Nutzer") verwaltet.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
|
||||||
|
# Redundanz-Dashboard
|
||||||
|
|
||||||
|
Das **Redundanz-Dashboard** (`/admin/redundancy`) ist ein Admin-Werkzeug zur
|
||||||
|
automatischen Erkennung doppelter oder stark überlappender Tools — jeweils
|
||||||
|
pro Kategorie — inklusive Kosten- und Bewertungsvergleich.
|
||||||
|
|
||||||
|
> Der Zugriff ist ausschließlich Admins vorbehalten (die API ist
|
||||||
|
> admin-geschützt).
|
||||||
|
|
||||||
|
## Aufbau
|
||||||
|
|
||||||
|
- **Pro Kategorie** wird eine Gruppe angezeigt: Name der Kategorie,
|
||||||
|
Anzahl Tools und Vergleiche sowie ggf. die **gesamten monatlichen Kosten**
|
||||||
|
(z. B. `€X.XX/mo gesamt`).
|
||||||
|
- Jedes Tool wird als Karte dargestellt: Name, monatliche Kosten, Anzahl der
|
||||||
|
Bewertungen, kombinierte Bewertung, Lizenz-Badges und Feature-Anzahl.
|
||||||
|
|
||||||
|
## Vergleiche & Empfehlungen
|
||||||
|
|
||||||
|
Für jedes Tool-Paar erscheint:
|
||||||
|
|
||||||
|
- Tool A vs. Tool B, jeweils mit Bewertung (`X.X ★`) und monatlichen Kosten.
|
||||||
|
- **Überlappung** in Prozent (Fortschrittsbalken in der Mitte).
|
||||||
|
- Eine **Empfehlung** mit Konfidenz-Farbe:
|
||||||
|
- **hoch** (grün), **mittel** (gelb), **niedrig** (grau)
|
||||||
|
- Das empfohlene, bessere Tool wird mit „Daumen hoch" markiert und begründet.
|
||||||
|
|
||||||
|
## Manuelle Bewertung
|
||||||
|
|
||||||
|
Du kannst ein Paar manuell bewerten: Klicke auf Tool A oder Tool B, um
|
||||||
|
festzuhalten, welches besser ist. Die Auswahl wird gespeichert und die
|
||||||
|
Darstellung aktualisiert.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- [`GET /api/admin/redundancy`](#) — Daten laden (admin-geschützt)
|
||||||
|
- [`POST /api/admin/redundancy/evaluate`](#) — manuelle Bewertung speichern
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
# Tastenkürzel & Kommandopalette
|
||||||
|
|
||||||
|
## Kommandopalette
|
||||||
|
|
||||||
|
Die Kommandopalette ist die zentrale Schnellnavigation:
|
||||||
|
|
||||||
|
- Öffnen mit **`⌘K`** (macOS) bzw. **`Ctrl+K`** (Windows/Linux).
|
||||||
|
- Alternativ über die Suchleiste oben rechts („Tools suchen… ⌘K") oder das
|
||||||
|
Such-Icon auf Mobilgeräten.
|
||||||
|
|
||||||
|
### Leerer Zustand
|
||||||
|
|
||||||
|
Ohne Eingabe zeigt die Palette:
|
||||||
|
|
||||||
|
- **Zuletzt angesehen** — die letzten 5 Tools, die du besucht hast.
|
||||||
|
- **Navigation** — Tools durchsuchen, Tool hinzufügen, Analytics sowie
|
||||||
|
(abhängig von Berechtigungen) Watchlist, Papierkorb und Admin.
|
||||||
|
|
||||||
|
### Suche
|
||||||
|
|
||||||
|
Tippe, um live nach Tools zu suchen (max. 10 Ergebnisse, inkl. Bewertung
|
||||||
|
`X.X★`).
|
||||||
|
|
||||||
|
## Tastenkürzel im Überblick
|
||||||
|
|
||||||
|
| Kürzel | Aktion |
|
||||||
|
| --- | --- |
|
||||||
|
| `⌘K` / `Ctrl+K` | Kommandopalette öffnen |
|
||||||
|
| `/` | Suche im Bereich „Tools durchsuchen" fokussieren |
|
||||||
|
|
||||||
|
## Weitere Hinweise
|
||||||
|
|
||||||
|
- **Zuletzt angesehen** wird lokal im Browser gespeichert (max. 5 Einträge).
|
||||||
|
- Die Seitenleiste (linke Navigation) ist auf Desktop einklappbar; der
|
||||||
|
Breadcrumb oben zeigt deinen aktuellen Ort.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
# Tool anlegen
|
||||||
|
|
||||||
|
Um ein neues Tool zum Katalog hinzuzufügen, klicke auf **Tool hinzufügen**
|
||||||
|
(`/tools/new`). Das Anlegen erfordert ein Konto — ohne Anmeldung erscheint ein
|
||||||
|
Hinweis mit Login-Button.
|
||||||
|
|
||||||
|
## Formularfelder
|
||||||
|
|
||||||
|
| Feld | Pflicht | Hinweise |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Name** | Ja | Mind. 2 Zeichen |
|
||||||
|
| **Kategorie** | Ja | Auswahlliste; neue Kategorien lassen sich direkt anlegen |
|
||||||
|
| **Website URL** | Nein | Gültige URL (z. B. `https://...`) |
|
||||||
|
| **Icon / Logo URL** | Nein | Gültige URL; Vorschau wird live angezeigt |
|
||||||
|
| **Beschreibung** | Ja | Mind. 10 Zeichen; beschreibe, was das Tool tut |
|
||||||
|
| **Features** | Nein | Dynamische Liste mit Autovervollständigung (max. 6) |
|
||||||
|
| **Tags** | Nein | Dynamische Liste mit Autovervollständigung |
|
||||||
|
|
||||||
|
Neben jedem Feld führt das **?‑Icon** direkt zur zugehörigen Feldbeschreibung
|
||||||
|
in der [Datenmodell-Referenz](/docs/reference/schemas/toolinput).
|
||||||
|
|
||||||
|
### Kategorie
|
||||||
|
|
||||||
|
- Tippe, um nach bestehenden Kategorien zu suchen.
|
||||||
|
- Wähle **+ Erstelle „..."**, um eine neue Kategorie anzulegen.
|
||||||
|
|
||||||
|
### Features & Tags
|
||||||
|
|
||||||
|
- **Feature hinzufügen** / **Tag hinzufügen** hängt eine neue Zeile an.
|
||||||
|
- Die Eingabefelder schlagen bestehende Features/Tags vor
|
||||||
|
(Autovervollständigung, max. 6 Vorschläge).
|
||||||
|
- Mit dem **×**‑Button entfernst du einzelne Zeilen.
|
||||||
|
- Features und Tags helfen beim Filtern und Wiederfinden.
|
||||||
|
|
||||||
|
## Speichern
|
||||||
|
|
||||||
|
Klicke auf **Tool hinzufügen**. Nach erfolgreicher Anlage wirst du auf die
|
||||||
|
Detailseite des neuen Tools weitergeleitet.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- [`POST /tools`](/docs/reference/endpoints/tools#createTool) — Tool anlegen
|
||||||
|
- [`GET /categories`](/docs/reference/endpoints/tools#listCategories) — Kategorien
|
||||||
|
- [`GET /features/all`](/docs/reference/endpoints/tools#listAllFeatures) — Features
|
||||||
|
- [`GET /tags/all`](/docs/reference/endpoints/tools#listAllTags) — Tags
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
# Tool bearbeiten & löschen
|
||||||
|
|
||||||
|
## Bearbeiten
|
||||||
|
|
||||||
|
Auf der Detailseite eines Tools findest du die Schaltfläche **Bearbeiten**
|
||||||
|
(nur für die Person, die das Tool angelegt hat, sowie für Admins).
|
||||||
|
|
||||||
|
Die Bearbeitungsseite (`/tools/:id/edit`) enthält dieselben Felder wie beim
|
||||||
|
Anlegen (Name, Kategorie, Website/Icon-URL, Beschreibung, Features, Tags) —
|
||||||
|
bereits mit den aktuellen Werten befüllt.
|
||||||
|
|
||||||
|
- **Speichern** übernimmt die Änderungen.
|
||||||
|
- **Abbrechen** führt zurück zur Detailseite.
|
||||||
|
|
||||||
|
API-Referenz: [`PATCH /tools/{id}`](/docs/reference/endpoints/tools#updateTool).
|
||||||
|
|
||||||
|
## Löschen
|
||||||
|
|
||||||
|
Über **Löschen** auf der Detailseite wird das Tool entfernt. Das Verhalten
|
||||||
|
hängt von deinem Tarif ab:
|
||||||
|
|
||||||
|
- **Mit Papierkorb-Zugang** (Premium/Enterprise oder Admin): Das Tool wird
|
||||||
|
**soft gelöscht** — es verschwindet aus allen öffentlichen Ansichten, kann
|
||||||
|
aber im [Papierkorb](/docs/handbook/papierkorb) wiederhergestellt oder
|
||||||
|
endgültig gelöscht werden.
|
||||||
|
- **Ohne Papierkorb-Zugang:** Das Tool wird **endgültig** gelöscht und kann
|
||||||
|
nicht wiederhergestellt werden.
|
||||||
|
|
||||||
|
Die Löschung ist nur für die Person, die das Tool angelegt hat, sowie für
|
||||||
|
Admins möglich.
|
||||||
|
|
||||||
|
API-Referenz: [`DELETE /tools/{id}`](/docs/reference/endpoints/tools#deleteTool).
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
# Tools finden & durchsuchen
|
||||||
|
|
||||||
|
Der Bereich **Tools durchsuchen** (`/tools`) ist der Einstieg in den Katalog.
|
||||||
|
Hier kombinierst du Suche, Filter und Sortierung, um genau die Tools zu finden,
|
||||||
|
die dich interessieren.
|
||||||
|
|
||||||
|
## Suche
|
||||||
|
|
||||||
|
- Die **Suchleiste** durchsucht Name und Beschreibung (Volltext).
|
||||||
|
- Tastenkürzel: Drücke **`/`**, um die Suche zu fokussieren.
|
||||||
|
- Die Eingabe ist deaktiviert (Debounce), damit bei jedem Tastendruck sofort
|
||||||
|
nachgefiltert wird.
|
||||||
|
|
||||||
|
## Filtern
|
||||||
|
|
||||||
|
Über die Schaltfläche **Filter** (mit Badge für die Anzahl aktiver Filter)
|
||||||
|
öffnest du den Filter-Popover mit:
|
||||||
|
|
||||||
|
- **Tags** — Auswahl über Checkboxen (scrollbare Liste).
|
||||||
|
- **Features** — Auswahl über Checkboxen.
|
||||||
|
- **Mindestbewertung** — Schieberegler von 0 bis 5 (Schritte von 0,5); zeigt
|
||||||
|
z. B. „3.0+" an.
|
||||||
|
|
||||||
|
Aktive Filter erscheinen als **entfernbare Chips** über der Ergebnisliste.
|
||||||
|
Mit **Filter zurücksetzen** bzw. **Alle entfernen** räumst du sie wieder auf.
|
||||||
|
|
||||||
|
## Sortieren
|
||||||
|
|
||||||
|
Über das Dropdown **Sortieren** stehen folgende Optionen zur Verfügung:
|
||||||
|
|
||||||
|
| Sortierung | Beschreibung |
|
||||||
|
| --- | --- |
|
||||||
|
| Neueste | Neue Tools zuerst |
|
||||||
|
| Top bewertet | Nach kombinierter Bewertung |
|
||||||
|
| Meistbewertet | Nach Anzahl der Bewertungen |
|
||||||
|
| Name (A–Z) | Alphabetisch aufsteigend |
|
||||||
|
| Name (Z–A) | Alphabetisch absteigend |
|
||||||
|
| Zuletzt aktualisiert | Nach letztem Update |
|
||||||
|
|
||||||
|
## Ansicht & Dichte
|
||||||
|
|
||||||
|
- **Ansicht wechseln:** Raster / Tabelle / Zeilen.
|
||||||
|
- **Dichte:** gemütlich / kompakt (Schieberegler).
|
||||||
|
|
||||||
|
Deine Auswahl wird gespeichert — lokal im Browser und für angemeldete Nutzer:innen
|
||||||
|
zusätzlich serverseitig in den Präferenzen. Ansicht, Dichte, Suche, Filter und
|
||||||
|
Sortierung werden dabei in die URL übernommen, sodass du Ergebnisse teilen
|
||||||
|
kannst.
|
||||||
|
|
||||||
|
## Tabellenansicht
|
||||||
|
|
||||||
|
In der Tabellenansicht sind die Spalten **Tool**, **Bewertung** und **Anzahl
|
||||||
|
Bewertungen** sortierbar. Beim Überfahren einer Zeile erscheint eine Vorschau
|
||||||
|
mit Bewertungsdetails, Tags und Mini-Balken.
|
||||||
|
|
||||||
|
## Auswählen für Vergleich & Watchlist
|
||||||
|
|
||||||
|
- Auf jeder Karte/Zeile findest du ein **Vergleichs-Icon**, mit dem du Tools zur
|
||||||
|
[Vergleichsleiste](/docs/handbook/vergleichen) hinzufügst.
|
||||||
|
- Das **Lesezeichen-Icon** speichert Tools in deiner
|
||||||
|
[Watchlist](/docs/handbook/watchlist) (nur mit dem entsprechenden Tarif).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Alle Such-, Filter- und Sortierparameter entsprechen den Query-Parametern von
|
||||||
|
[`GET /tools`](/docs/reference/endpoints/tools#listTools).
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
# Vergleichen
|
||||||
|
|
||||||
|
Mit der Vergleichsfunktion stellst du mehrere Tools **nebeneinander** gegenüber —
|
||||||
|
ideal, um eine fundierte Entscheidung zu treffen.
|
||||||
|
|
||||||
|
> Vergleichen ist ein **Premium-Feature** (Premium/Enterprise) und steht Admins
|
||||||
|
> immer zur Verfügung.
|
||||||
|
|
||||||
|
## Tools auswählen
|
||||||
|
|
||||||
|
1. Im Bereich **Tools durchsuchen** klickst du auf jeder Karte/Zeile auf das
|
||||||
|
**Vergleichs-Icon** (Waage).
|
||||||
|
2. Unten erscheint die **Vergleichsleiste** mit den ausgewählten Tools als
|
||||||
|
Chips. Du kannst einzelne Tools entfernen (×) oder die Auswahl leeren.
|
||||||
|
3. Klicke auf **Vergleichen (N)**, um zur Vergleichsansicht zu gelangen.
|
||||||
|
|
||||||
|
> Ohne Premium-Tarif ist der Button gesperrt (Schloss-Icon). Über den
|
||||||
|
> Dialog gelangst du zum Tarifwechsel
|
||||||
|
> (siehe [Pläne & Berechtigungen](/docs/handbook/plaene)).
|
||||||
|
|
||||||
|
## Die Vergleichsansicht
|
||||||
|
|
||||||
|
Die Ansicht zeigt eine Tabelle mit einer Spalte pro Tool. Zeilen:
|
||||||
|
|
||||||
|
| Zeile | Inhalt |
|
||||||
|
| --- | --- |
|
||||||
|
| **Bewertung** | Sterne + Wert (z. B. `4.2/5`) |
|
||||||
|
| **Nützlichkeit** | Wert (X.X/5) |
|
||||||
|
| **Bedienbarkeit** | Wert (X.X/5) |
|
||||||
|
| **Anzahl Bewertungen** | Anzahl |
|
||||||
|
| **Beschreibung** | Text |
|
||||||
|
| **Features** | Badges |
|
||||||
|
| **Tags** | Badges |
|
||||||
|
| **Zuletzt aktualisiert** | Datum |
|
||||||
|
|
||||||
|
Der **beste Wert** pro Zeile wird hervorgehoben (mit Trophäen-Icon).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Die Vergleichsansicht liest die Daten über
|
||||||
|
[`GET /compare`](/docs/reference/endpoints/tools#listCompareTools).
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
# Watchlist
|
||||||
|
|
||||||
|
Die **Watchlist** ist eine persönliche Favoritenliste. Tools darin kannst du
|
||||||
|
jederzeit per Klick wieder aufrufen und vergleichen.
|
||||||
|
|
||||||
|
> Die Watchlist ist ein **Premium-Feature** (Premium/Enterprise) und steht
|
||||||
|
> Admins immer zur Verfügung.
|
||||||
|
|
||||||
|
## Voraussetzung
|
||||||
|
|
||||||
|
Du benötigst einen Tarif mit `watchlist`-Berechtigung. Fehlt diese, erscheint
|
||||||
|
beim Lesezeichen ein Hinweis auf den Tarifwechsel
|
||||||
|
(siehe [Pläne & Berechtigungen](/docs/handbook/plaene)).
|
||||||
|
|
||||||
|
## Tool speichern
|
||||||
|
|
||||||
|
- Auf jeder Karte/Zeile im Bereich **Tools durchsuchen** findest du das
|
||||||
|
**Lesezeichen-Icon**.
|
||||||
|
- Ein Klick speichert das Tool in deiner Watchlist — das Icon wird gefüllt.
|
||||||
|
- Ein erneuter Klick entfernt es wieder.
|
||||||
|
|
||||||
|
## Watchlist ansehen
|
||||||
|
|
||||||
|
Öffne die Watchlist über das Benutzermenü oder die Seitenleiste. Sie zeigt alle
|
||||||
|
gespeicherten Tools als Karten. Das gefüllte Lesezeichen auf einer Karte
|
||||||
|
entfernt das Tool aus der Liste.
|
||||||
|
|
||||||
|
## Wo wird die Watchlist gespeichert?
|
||||||
|
|
||||||
|
Die Watchlist ist eine Liste von Tool-IDs in deinen **Benutzerpräferenzen**.
|
||||||
|
Damit ist sie geräteübergreifend mit deinem Konto verbunden.
|
||||||
|
|
||||||
|
API-Referenz: [`GET /auth/me/watchlist`](/docs/reference/endpoints/auth#getMeWatchlist).
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
|||||||
|
# v0.8.2 — Release Notes
|
||||||
|
|
||||||
|
**Datum:** 2026-08-04 · **Tag:** [`v0.8.2`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.2)
|
||||||
|
|
||||||
|
## Fixes & Verbesserungen
|
||||||
|
|
||||||
|
- **Mobile Navigation in der Doku**: Die linke Navigationsleiste (Handbuch,
|
||||||
|
Endpunkte, Schemas) wurde unterhalb von `lg` (1024px) komplett ausgeblendet.
|
||||||
|
Es gibt jetzt einen Menü-Button im Kopfbereich, der ein seitliches
|
||||||
|
Navigations-Drawer mit demselben Inhalt öffnet — auf kleinen Bildschirmen
|
||||||
|
bleibt die Doku voll navigierbar.
|
||||||
|
- **Versions-Dropdown zeigt die gewählte Version an**: Beim Wechsel zu einem
|
||||||
|
anderen Release (`/docs/releases/vX.Y.Z`) blieb das Dropdown fälschlich auf
|
||||||
|
„aktuell". Die angezeigte Version wird jetzt auch aus der releases-Route
|
||||||
|
abgeleitet.
|
||||||
|
|
||||||
|
## API-Änderungen
|
||||||
|
|
||||||
|
- Keine Änderungen an der API.
|
||||||
|
|
||||||
|
## Betrieb / Upgrade
|
||||||
|
|
||||||
|
- **Env-Vars:** unverändert.
|
||||||
|
- **Migration:** keine.
|
||||||
|
- **Breaking Changes:** keine.
|
||||||
|
|
||||||
|
## Bekannte Einschränkungen
|
||||||
|
|
||||||
|
- Handbuch & Referenz gelten für die aktuelle Version; ältere Versionen zeigen
|
||||||
|
ihre Release-Notes und einen Referenz-Snapshot, sofern beim Release erzeugt
|
||||||
|
(`node scripts/src/generate-docs.mjs --snapshot vX.Y.Z`).
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Commit: [`33cf261`](https://git.kubebase.de/admin/tool-evaluator/commit/33cf261)
|
||||||
|
- Tag: [`v0.8.2`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.2)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
|
||||||
|
# Administration
|
||||||
|
|
||||||
|
Der Bereich **Admin** (`/admin`) ist ausschließlich für Admins zugänglich.
|
||||||
|
Ohne Admin-Rolle erscheint eine Zugriffsverweigerung.
|
||||||
|
|
||||||
|
> Oben rechts führt die Schaltfläche **Redundanz-Dashboard** zur automatischen
|
||||||
|
> Doppelungs-Erkennung (siehe [Redundanz](/docs/handbook/redundanz)).
|
||||||
|
|
||||||
|
## Tab „Nutzer"
|
||||||
|
|
||||||
|
Verwaltung der lokalen Konten.
|
||||||
|
|
||||||
|
- **Nutzer hinzufügen:** Benutzername (Pflicht), Passwort (mind. 6 Zeichen),
|
||||||
|
E-Mail (optional), **Rolle** (User/Admin), **Tarif** (Free/Premium/Enterprise).
|
||||||
|
- **Nutzer bearbeiten:** Rolle, Tarif und (für lokale Konten) ein neues Passwort
|
||||||
|
setzen. Für OIDC-Konten wird die Passwortverwaltung im Identitätsanbieter
|
||||||
|
(z. B. Keycloak) angeboten.
|
||||||
|
- **Nutzer löschen:** Entfernt das Konto endgültig (nicht für das eigene Konto).
|
||||||
|
|
||||||
|
API-Referenz:
|
||||||
|
[`POST /users`](/docs/reference/endpoints/users#createUser),
|
||||||
|
[`PATCH /users/{id}`](/docs/reference/endpoints/users#updateUser),
|
||||||
|
[`DELETE /users/{id}`](/docs/reference/endpoints/users#deleteUser).
|
||||||
|
|
||||||
|
## Tab „Tools"
|
||||||
|
|
||||||
|
Zentraler Zugriff auf den Tool-Katalog.
|
||||||
|
|
||||||
|
- **Suchen** nach Tools.
|
||||||
|
- Tools einzeln ansehen, bearbeiten oder in den Papierkorb verschieben.
|
||||||
|
- **Massenaktion:** mehrere Tools auswählen und in den Papierkorb verschieben
|
||||||
|
(Bestätigungsdialog; soft gelöschte Tools sind aus allen öffentlichen Ansichten
|
||||||
|
entfernt und können wiederhergestellt oder endgültig gelöscht werden).
|
||||||
|
|
||||||
|
## Tab „Audit-Log"
|
||||||
|
|
||||||
|
Chronologisches Protokoll aller Anlage-, Änderungs- und Löschvorgänge
|
||||||
|
(max. 100 Einträge): Aktion, Entität + ID, Zeitstempel, ausführende Person und
|
||||||
|
geänderte Felder.
|
||||||
|
|
||||||
|
API-Referenz: [`GET /audit-logs`](/docs/reference/endpoints/audit#listAuditLogs).
|
||||||
|
|
||||||
|
## Tab „System"
|
||||||
|
|
||||||
|
Versionsinformationen der laufenden Instanz:
|
||||||
|
|
||||||
|
- **Version** (z. B. `v0.8.1`),
|
||||||
|
- **Commit** (7-stelliger SHA, verlinkt zum Repository),
|
||||||
|
- **Build-Datum**,
|
||||||
|
- **Papierkorb-Aufbewahrung** („N Tage" oder „Für immer").
|
||||||
|
|
||||||
|
## Tool-Verknüpfungen (Admin)
|
||||||
|
|
||||||
|
Auf der Detailseite eines Tools kannst du als Admin **Verknüpfungen**
|
||||||
|
(eigene/„manual" sowie automatisch erkannte) verwalten:
|
||||||
|
|
||||||
|
- **Tool verknüpfen:** Dialog mit Tool-ID, **Beziehungstyp**
|
||||||
|
(Ähnlich / Ersetzt / Abgelöst durch) und optionalen Notizen.
|
||||||
|
- Beziehungstypen werden als Badges auf der Detailseite angezeigt.
|
||||||
|
- Manuelle Verknüpfungen lassen sich per Papierkorb-Icon wieder entfernen.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
# Analytics
|
||||||
|
|
||||||
|
Der Bereich **Analytics** (`/analytics`) ist ein öffentliches Dashboard mit
|
||||||
|
Kennzahlen und Diagrammen auf Basis aller Tools und Bewertungen.
|
||||||
|
|
||||||
|
## Kennzahlen (KPI-Karten)
|
||||||
|
|
||||||
|
- **Anzahl Tools** — wie viele Tools sind im Katalog erfasst.
|
||||||
|
- **Anzahl Bewertungen** — wie viele Bewertungen wurden insgesamt abgegeben.
|
||||||
|
- **Aktive Kategorien** — wie viele Kategorien existieren.
|
||||||
|
- **Durchschnittliche Bewertung** — globaler kombinierter Wert.
|
||||||
|
|
||||||
|
## Diagramme
|
||||||
|
|
||||||
|
| Diagramm | Inhalt |
|
||||||
|
| --- | --- |
|
||||||
|
| **Top 8 Tools** | Balkendiagramm der Tools mit der höchsten kombinierten Punktzahl (0–5) |
|
||||||
|
| **Tools je Kategorie** | Radar-Diagramm der Tool-Anzahl pro Kategorie |
|
||||||
|
| **Punkteverteilung** | Zwei horizontale Balken-Diagramme (Nützlichkeit & Bedienbarkeit) pro Stern |
|
||||||
|
|
||||||
|
Die Diagramme sind interaktiv (Tooltips beim Überfahren).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- [`GET /analytics/summary`](/docs/reference/endpoints/analytics#getAnalyticsSummary)
|
||||||
|
- [`GET /analytics/top-tools`](/docs/reference/endpoints/analytics#getTopTools)
|
||||||
|
- [`GET /analytics/by-category`](/docs/reference/endpoints/analytics#getAnalyticsByCategory)
|
||||||
|
- [`GET /analytics/rating-distribution`](/docs/reference/endpoints/analytics#getRatingDistribution)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
# Bewerten
|
||||||
|
|
||||||
|
Auf der Detailseite eines Tools kannst du deine Erfahrung teilen. Klicke auf
|
||||||
|
**Bewertung abgeben** (erfordert ein Konto).
|
||||||
|
|
||||||
|
## Formularfelder
|
||||||
|
|
||||||
|
| Feld | Pflicht | Hinweise |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Nützlichkeit** | Ja | 1–5 Sterne |
|
||||||
|
| **Bedienbarkeit** | Ja | 1–5 Sterne |
|
||||||
|
| **Kommentar** | Nein | Freitext |
|
||||||
|
| **Name** | Nein | Standard „Anonym" |
|
||||||
|
|
||||||
|
Neben den Feldern führt das **?‑Icon** direkt zur zugehörigen Feldbeschreibung
|
||||||
|
in der [Datenmodell-Referenz](/docs/reference/schemas/ratinginput).
|
||||||
|
|
||||||
|
## Was passiert nach dem Abgeben?
|
||||||
|
|
||||||
|
- Deine Bewertung wird sofort gespeichert und erscheint in der
|
||||||
|
**Bewertungsliste** der Detailseite.
|
||||||
|
- Die **Durchschnittswerte** (Nützlichkeit, Bedienbarkeit, Kombiniert) und die
|
||||||
|
**Punkteverteilung** werden aktualisiert.
|
||||||
|
- Die **Statistiken** im Bereich [Analytics](/docs/handbook/analytics) werden
|
||||||
|
neu berechnet.
|
||||||
|
|
||||||
|
## Statistik-Bereiche auf der Detailseite
|
||||||
|
|
||||||
|
- **Bewertungsübersicht:** Nützlichkeit & Bedienbarkeit als Durchschnitt mit
|
||||||
|
Fortschrittsbalken.
|
||||||
|
- **Punkteverteilung:** Anzahl der Bewertungen pro Stern (1★–5★).
|
||||||
|
- **Verlauf:** Linienchart der kombinierten/Teilwerte über die Zeit
|
||||||
|
(erst ab mehreren Bewertungen sichtbar).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- [`POST /tools/{id}/ratings`](/docs/reference/endpoints/ratings#createRating) — Bewertung abgeben
|
||||||
|
- [`GET /tools/{id}/ratings`](/docs/reference/endpoints/ratings#listToolRatings) — Bewertungen eines Tools
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
|
||||||
|
# Datenmodell
|
||||||
|
|
||||||
|
Dieses Kapitel erklärt die zentralen Datenobjekte von toolr auf Ebene der
|
||||||
|
Anwendung. Die vollständige, automatisch generierte Referenz aller Felder,
|
||||||
|
Typen und Constraints findest du in der
|
||||||
|
[API-Referenz](/docs/reference/schemas/tool).
|
||||||
|
|
||||||
|
## Tool
|
||||||
|
|
||||||
|
Das Herzstück: ein im Katalog erfasstes Werkzeug.
|
||||||
|
|
||||||
|
| Eigenschaft | Beschreibung |
|
||||||
|
| --- | --- |
|
||||||
|
| `id` | Eindeutige Kennung |
|
||||||
|
| `name` | Anzeigename |
|
||||||
|
| `description` | Beschreibung (Was macht das Tool?) |
|
||||||
|
| `category` | Kategorie-Zuordnung |
|
||||||
|
| `websiteUrl` | Offizielle Website (optional) |
|
||||||
|
| `iconUrl` | Logo-/Icon-URL (optional) |
|
||||||
|
| `features` | Liste von Fähigkeiten |
|
||||||
|
| `tags` | Liste von Schlagwörtern |
|
||||||
|
| `createdAt` / `updatedAt` | Zeitstempel |
|
||||||
|
| `createdBy` | Erstellende Person |
|
||||||
|
| `deletedAt` / `deletedBy` | Soft-Löschung (Papierkorb) |
|
||||||
|
|
||||||
|
Eingabe-Formulare verwenden die abgeleiteten Schemas
|
||||||
|
[`ToolInput`](/docs/reference/schemas/toolinput) und
|
||||||
|
[`ToolUpdate`](/docs/reference/schemas/toolupdate).
|
||||||
|
Aggregierte Ansichten liefert [`ToolWithStats`](/docs/reference/schemas/toolwithstats)
|
||||||
|
(z. B. mit Durchschnittsbewertung).
|
||||||
|
|
||||||
|
## Rating (Bewertung)
|
||||||
|
|
||||||
|
Eine einzelne Bewertung zu einem Tool:
|
||||||
|
|
||||||
|
- `usefulness` (Nützlichkeit, 1–5) und `usability` (Bedienbarkeit, 1–5)
|
||||||
|
- optional `comment` und ein Anzeigename (`reviewerName`)
|
||||||
|
- Zeitstempel
|
||||||
|
|
||||||
|
Eingabe-Schema: [`RatingInput`](/docs/reference/schemas/ratinginput).
|
||||||
|
|
||||||
|
## User & Auth
|
||||||
|
|
||||||
|
- [`User`](/docs/reference/schemas/user) — Benutzerkonto mit Rolle (User/Admin)
|
||||||
|
und Tarif (Free/Premium/Enterprise).
|
||||||
|
- [`AuthUser`](/docs/reference/schemas/authuser) — das angemeldete Profil
|
||||||
|
inklusive `entitlements` (verfügbare Features).
|
||||||
|
- [`UserPreferences`](/docs/reference/schemas/userpreferences) — Ansichts- und
|
||||||
|
Dichte-Präferenzen sowie die `watchlist` (Liste von Tool-IDs).
|
||||||
|
|
||||||
|
## Analytics
|
||||||
|
|
||||||
|
Die Statistik-Endpunkte liefern aggregierte Daten:
|
||||||
|
|
||||||
|
- [`AnalyticsSummary`](/docs/reference/schemas/analyticssummary) — globale
|
||||||
|
Kennzahlen (Anzahl Tools/Bewertungen, Kategorien, Durchschnitt).
|
||||||
|
- [`TopToolEntry`](/docs/reference/schemas/TopToolEntry) — ein Eintrag der
|
||||||
|
Top-Tools.- [`CategoryStats`](/docs/reference/schemas/categorystats) — Tool-Anzahl je
|
||||||
|
Kategorie.
|
||||||
|
- [`RatingDistribution`](/docs/reference/schemas/ratingdistribution) —
|
||||||
|
Punkteverteilung (Nützlichkeit & Bedienbarkeit).
|
||||||
|
- [`ScoreBucket`](/docs/reference/schemas/scorebucket) — ein Werte-Bucket.
|
||||||
|
|
||||||
|
## Weitere
|
||||||
|
|
||||||
|
- [`VersionInfo`](/docs/reference/schemas/versioninfo) — Version, Commit-SHA,
|
||||||
|
Build-Datum und Papierkorb-Aufbewahrung der laufenden Instanz.
|
||||||
|
- [`AuditLog`](/docs/reference/schemas/auditlog) — ein Protokolleintrag
|
||||||
|
(Aktion, Entität, Zeitstempel, Akteur, Änderungen).
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
|
||||||
|
# Erste Schritte
|
||||||
|
|
||||||
|
Diese Seite führt dich durch die wichtigsten Abläufe in toolr — vom ersten
|
||||||
|
Besuch bis zum Anlegen und Bewerten eines Tools.
|
||||||
|
|
||||||
|
## 1. Anmelden
|
||||||
|
|
||||||
|
Die meisten Aktionen (Tool anlegen, bewerten, Watchlist, Vergleichen) erfordern
|
||||||
|
ein Konto. Klicke unten links auf **Anmelden**. Je nach Konfiguration der
|
||||||
|
Instanz hast du zwei Möglichkeiten:
|
||||||
|
|
||||||
|
- **Lokale Konten:** Benutzername + Passwort. Der Zugang wird von einem Admin
|
||||||
|
angelegt (siehe [Administration](/docs/handbook/administration)).
|
||||||
|
- **OIDC (SSO):** Anmelden mit dem konfigurierten Identitätsanbieter (z. B.
|
||||||
|
Keycloak).
|
||||||
|
|
||||||
|
Welcher Modus aktiv ist, steht im Endpunkt
|
||||||
|
[`GET /auth/mode`](/docs/reference/endpoints/auth#getAuthMode). Details findest
|
||||||
|
du im Abschnitt [Anmelden & Konto](/docs/handbook/konto).
|
||||||
|
|
||||||
|
## 2. Tools finden
|
||||||
|
|
||||||
|
Öffne den Bereich **Tools durchsuchen**:
|
||||||
|
|
||||||
|
- **Suchen** — Volltextsuche über Name & Beschreibung (Tastenkürzel `/`).
|
||||||
|
- **Filtern** — nach Kategorie, Tags, Features und Mindestbewertung
|
||||||
|
(`minRating`).
|
||||||
|
- **Sortieren** — nach Aktualität, Top-Bewertung, meistbewertet, Name
|
||||||
|
(auf-/absteigend) oder letztem Update.
|
||||||
|
|
||||||
|
Alle Optionen im Detail: [Tools finden & durchsuchen](/docs/handbook/tools-finden).
|
||||||
|
|
||||||
|
## 3. Tool anlegen
|
||||||
|
|
||||||
|
Gehe auf **Tool hinzufügen** und fülle das Formular aus. Details zu jedem Feld
|
||||||
|
findest du im Abschnitt [Tool anlegen](/docs/handbook/tool-anlegen) und in der
|
||||||
|
[Feld-Referenz](/docs/reference/schemas/toolinput).
|
||||||
|
|
||||||
|
## 4. Bewerten
|
||||||
|
|
||||||
|
Auf der Detailseite eines Tools kannst du **Nützlichkeit** und **Bedienbarkeit**
|
||||||
|
(jeweils 1–5) vergeben und optional einen Kommentar hinterlassen. Deine
|
||||||
|
Bewertung fließt sofort in die Statistiken ein.
|
||||||
|
Siehe [Bewerten](/docs/handbook/bewerten).
|
||||||
|
|
||||||
|
## 5. Weiterführend
|
||||||
|
|
||||||
|
- [Tools vergleichen](/docs/handbook/vergleichen)
|
||||||
|
- [Watchlist](/docs/handbook/watchlist)
|
||||||
|
- [Analytics](/docs/handbook/analytics)
|
||||||
|
- [Pläne & Berechtigungen](/docs/handbook/plaene)
|
||||||
|
- [Administration](/docs/handbook/administration)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"slug": "index",
|
||||||
|
"file": "index.md",
|
||||||
|
"title": "Überblick",
|
||||||
|
"order": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "getting-started",
|
||||||
|
"file": "getting-started.md",
|
||||||
|
"title": "Erste Schritte",
|
||||||
|
"order": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "konto",
|
||||||
|
"file": "konto.md",
|
||||||
|
"title": "Anmelden & Konto",
|
||||||
|
"order": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "tools-finden",
|
||||||
|
"file": "tools-finden.md",
|
||||||
|
"title": "Tools finden & durchsuchen",
|
||||||
|
"order": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "tool-anlegen",
|
||||||
|
"file": "tool-anlegen.md",
|
||||||
|
"title": "Tool anlegen",
|
||||||
|
"order": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "tool-bearbeiten",
|
||||||
|
"file": "tool-bearbeiten.md",
|
||||||
|
"title": "Tool bearbeiten & löschen",
|
||||||
|
"order": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "bewerten",
|
||||||
|
"file": "bewerten.md",
|
||||||
|
"title": "Bewerten",
|
||||||
|
"order": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "watchlist",
|
||||||
|
"file": "watchlist.md",
|
||||||
|
"title": "Watchlist",
|
||||||
|
"order": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "vergleichen",
|
||||||
|
"file": "vergleichen.md",
|
||||||
|
"title": "Vergleichen",
|
||||||
|
"order": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "analytics",
|
||||||
|
"file": "analytics.md",
|
||||||
|
"title": "Analytics",
|
||||||
|
"order": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "plaene",
|
||||||
|
"file": "plaene.md",
|
||||||
|
"title": "Pläne & Berechtigungen",
|
||||||
|
"order": 11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "kosten",
|
||||||
|
"file": "kosten.md",
|
||||||
|
"title": "Kosten erfassen",
|
||||||
|
"order": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "administration",
|
||||||
|
"file": "administration.md",
|
||||||
|
"title": "Administration",
|
||||||
|
"order": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "redundanz",
|
||||||
|
"file": "redundanz.md",
|
||||||
|
"title": "Redundanz-Dashboard",
|
||||||
|
"order": 14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "papierkorb",
|
||||||
|
"file": "papierkorb.md",
|
||||||
|
"title": "Papierkorb",
|
||||||
|
"order": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "tastatur",
|
||||||
|
"file": "tastatur.md",
|
||||||
|
"title": "Tastenkürzel & Kommandopalette",
|
||||||
|
"order": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "datenmodell",
|
||||||
|
"file": "datenmodell.md",
|
||||||
|
"title": "Datenmodell",
|
||||||
|
"order": 17
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
|
||||||
|
# Willkommen bei toolr
|
||||||
|
|
||||||
|
toolr ist eine Plattform zum **Entdecken, Bewerten und Vergleichen von
|
||||||
|
Entwicklungstools**. Nutzer:innen pflegen einen gemeinsamen Katalog von Tools,
|
||||||
|
vergeben Bewertungen (Nützlichkeit & Bedienbarkeit) und nutzen Statistiken, um
|
||||||
|
die richtige Wahl zu treffen.
|
||||||
|
|
||||||
|
## Was kannst du mit toolr tun?
|
||||||
|
|
||||||
|
| Funktion | Beschreibung | Sichtbarkeit |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Tools durchsuchen** | Katalog filtern, sortieren und durchsuchen | Alle |
|
||||||
|
| **Tool anlegen** | Neues Tool mit Beschreibung, Kategorie, Features & Tags eintragen | Angemeldet |
|
||||||
|
| **Tool bearbeiten/löschen** | Eigene Tools pflegen (Ersteller:in oder Admin) | Angemeldet |
|
||||||
|
| **Bewerten** | Nützlichkeit & Bedienbarkeit (1–5) plus Kommentar vergeben | Angemeldet |
|
||||||
|
| **Watchlist** | Tools als Favoriten speichern | Premium |
|
||||||
|
| **Vergleichen** | Tools nebeneinander gegenüberstellen | Premium |
|
||||||
|
| **Kosten erfassen** | Lizenz- und Kostenmodelle je Tool eintragen | Premium |
|
||||||
|
| **Analytics** | Statistiken, Top-Tools, Verteilungen | Alle |
|
||||||
|
| **Papierkorb** | Soft-gelöschte Tools wiederherstellen oder endgültig löschen | Premium |
|
||||||
|
| **Admin** | Nutzerverwaltung, Audit-Log, Systeminformationen | Admin |
|
||||||
|
| **Redundanz** | Automatische Doppelungs-Erkennung | Admin |
|
||||||
|
|
||||||
|
## Wie diese Doku aufgebaut ist
|
||||||
|
|
||||||
|
- **User Guide** (diese Seiten): Schritt-für-Schritt-Anleitungen für alle
|
||||||
|
Funktionen — von den [Ersten Schritten](/docs/handbook/getting-started) bis
|
||||||
|
zur [Administration](/docs/handbook/administration).
|
||||||
|
- **API-Referenz**: automatisch aus der OpenAPI-Spezifikation generiert — alle
|
||||||
|
[Endpunkte](/docs/reference/endpoints/tools) und
|
||||||
|
[Datenfelder](/docs/reference/schemas/toolinput) der aktuellen Version.
|
||||||
|
- **Release-Notes**: Was ist in welcher [Version](/docs/releases/v0.8.1) neu.
|
||||||
|
|
||||||
|
## Der Einstieg
|
||||||
|
|
||||||
|
Der schnellste Weg:
|
||||||
|
|
||||||
|
1. **Anmelden** — ohne Konto kannst du nur stöbern
|
||||||
|
(siehe [Erste Schritte](/docs/handbook/getting-started#1-anmelden)).
|
||||||
|
2. **Tools finden** — Suche, Filter und Sortierung im Bereich
|
||||||
|
[Tools durchsuchen](/docs/handbook/tools-finden).
|
||||||
|
3. **Tool anlegen** — über „Tool hinzufügen"
|
||||||
|
([Anleitung](/docs/handbook/tool-anlegen)).
|
||||||
|
4. **Bewerten** — auf der Detailseite eines Tools
|
||||||
|
([Anleitung](/docs/handbook/bewerten)).
|
||||||
|
|
||||||
|
## Kontakt & Quellcode
|
||||||
|
|
||||||
|
Der Quellcode liegt unter
|
||||||
|
[git.kubebase.de/admin/tool-evaluator](https://git.kubebase.de/admin/tool-evaluator) —
|
||||||
|
über das Repository-Icon oben rechts erreichst du ihn jederzeit.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
|
||||||
|
# Anmelden & Konto
|
||||||
|
|
||||||
|
## Anmelden
|
||||||
|
|
||||||
|
Klicke unten links in der Seitenleiste auf **Anmelden**. Je nach Konfiguration
|
||||||
|
der Instanz:
|
||||||
|
|
||||||
|
- **Lokale Konten:** Benutzername und Passwort eingeben. Die Konten werden von
|
||||||
|
einem Admin angelegt (siehe [Administration](/docs/handbook/administration)).
|
||||||
|
- **OIDC (SSO):** Du wirst an den konfigurierten Identitätsanbieter
|
||||||
|
weitergeleitet und meldest dich dort an.
|
||||||
|
|
||||||
|
Der aktive Modus steht im Endpunkt
|
||||||
|
[`GET /auth/mode`](/docs/reference/endpoints/auth#getAuthMode).
|
||||||
|
|
||||||
|
> Die Login-Seite erreichst du direkt unter `/login`. Nach erfolgreicher
|
||||||
|
> Anmeldung wirst du zur ursprünglich aufgerufenen Seite zurückgeleitet.
|
||||||
|
|
||||||
|
## Benutzerprofil
|
||||||
|
|
||||||
|
Dein Profil (Avatar, Name, E-Mail, Tarif) siehst du unten links im
|
||||||
|
Benutzermenü. Dort stehen dir folgende Aktionen zur Verfügung:
|
||||||
|
|
||||||
|
- **Watchlist** — deine gespeicherten Tools (nur mit dem entsprechenden Tarif).
|
||||||
|
- **Papierkorb** — wiederherstellbare, gelöschte Tools (Premium/Enterprise).
|
||||||
|
- **Passwort ändern** — für lokale Konten direkt in toolr; für OIDC-Konten wird
|
||||||
|
die Passwortverwaltung im Identitätsanbieter angeboten.
|
||||||
|
- **Abmelden** — beendet deine Sitzung.
|
||||||
|
|
||||||
|
## Passwort ändern (lokales Konto)
|
||||||
|
|
||||||
|
1. Öffne das Benutzermenü unten links.
|
||||||
|
2. Wähle **Passwort ändern**.
|
||||||
|
3. Gib das **aktuelle** sowie ein **neues** Passwort ein (mind. 6 Zeichen) und
|
||||||
|
bestätige es.
|
||||||
|
4. Speichern — das Passwort wird sofort übernommen.
|
||||||
|
|
||||||
|
API-Referenz: [`POST /auth/me/password`](/docs/reference/endpoints/auth#changeMyPassword).
|
||||||
|
|
||||||
|
## Anzeigeeinstellungen
|
||||||
|
|
||||||
|
Über die Schaltflächen oben rechts kannst du:
|
||||||
|
|
||||||
|
- **Sprache** wechseln (Deutsch / Englisch),
|
||||||
|
- **Theme** umschalten (Hell / Dunkel / System),
|
||||||
|
- die **Listenansicht** und **Dichte** im Bereich Tools durchsuchen anpassen
|
||||||
|
(siehe [Tools finden & durchsuchen](/docs/handbook/tools-finden)).
|
||||||
|
|
||||||
|
Deine Präferenzen (inkl. Watchlist) werden im Endpunkt
|
||||||
|
[`GET /auth/me/preferences`](/docs/reference/endpoints/auth#getMePreferences)
|
||||||
|
gespeichert und über [`PUT /auth/me/preferences`](/docs/reference/endpoints/auth#updateMePreferences)
|
||||||
|
aktualisiert.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
# Kosten erfassen
|
||||||
|
|
||||||
|
Auf der Detailseite eines Tools kannst du Kosten- und Lizenzmodelle eintragen,
|
||||||
|
damit die Gesamtkosten je Tool transparent werden.
|
||||||
|
|
||||||
|
> Kosten ist ein **Premium-Feature** (`costs`, Premium/Enterprise). Admins
|
||||||
|
> haben immer Zugriff.
|
||||||
|
|
||||||
|
## Kosten hinzufügen
|
||||||
|
|
||||||
|
Klicke auf **Kosten hinzufügen** im Kosten-Bereich der Detailseite und fülle
|
||||||
|
das Formular aus:
|
||||||
|
|
||||||
|
| Feld | Hinweise |
|
||||||
|
| --- | --- |
|
||||||
|
| **Lizenztyp** | Free / Subscription / One-Time / Usage-Based |
|
||||||
|
| **Abrechnungszeitraum** | Nur für „Subscription": Monatlich / Quartalsweise / Jährlich |
|
||||||
|
| **Kosten** | Betrag als Zahl |
|
||||||
|
| **Währung** | EUR / USD / GBP / CHF |
|
||||||
|
| **Notizen** | Optionaler Freitext |
|
||||||
|
|
||||||
|
Speichern legt den Eintrag an. Jeder Kosten-Eintrag wird als Karte mit
|
||||||
|
Lizenz-Badge, Abrechnungszeitraum, Betrag (`Betrag Währung` bzw. „Free") und
|
||||||
|
Notizen angezeigt.
|
||||||
|
|
||||||
|
## Kosten bearbeiten & löschen
|
||||||
|
|
||||||
|
Beim Überfahren einer Kosten-Karte erscheinen die Aktionen **Bearbeiten**
|
||||||
|
(Bleistift) und **Löschen** (Papierkorb).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Die Kosten-Daten werden über die Tool-Endpunkte verwaltet
|
||||||
|
(siehe [API-Referenz](/docs/reference/endpoints/tools)).
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
# Papierkorb
|
||||||
|
|
||||||
|
Der **Papierkorb** (`/trash`) enthält soft gelöschte Tools. Mit Papierkorb-Zugang
|
||||||
|
können sie wiederhergestellt werden; endgültiges Löschen ist Admins vorbehalten.
|
||||||
|
|
||||||
|
> Der Papierkorb ist ein **Premium-Feature** (`trash`, Premium/Enterprise).
|
||||||
|
> Admins haben immer Zugriff.
|
||||||
|
|
||||||
|
## Zugang
|
||||||
|
|
||||||
|
Der Papierkorb ist über das Benutzermenü oder die Seitenleiste erreichbar.
|
||||||
|
Ohne `trash`-Berechtigung erscheint ein Hinweis auf den Tarifwechsel.
|
||||||
|
|
||||||
|
## Wiederherstellen
|
||||||
|
|
||||||
|
- Markiere ein oder mehrere Tools (Checkboxen).
|
||||||
|
- Klicke auf **Wiederherstellen (N)** — die Tools erscheinen wieder in allen
|
||||||
|
öffentlichen Ansichten.
|
||||||
|
|
||||||
|
> Wiederherstellen steht jeder Person mit Papierkorb-Zugang zur Verfügung.
|
||||||
|
|
||||||
|
## Endgültig löschen (nur Admin)
|
||||||
|
|
||||||
|
- **Löschen (N)** entfernt die ausgewählten Tools **endgültig** — inklusive
|
||||||
|
aller Bewertungen, Kosten und Verknüpfungen. Das kann nicht rückgängig
|
||||||
|
gemacht werden.
|
||||||
|
- **Papierkorb leeren** entfernt alle soft gelöschten Tools endgültig.
|
||||||
|
|
||||||
|
## Tabelle
|
||||||
|
|
||||||
|
Der Papierkorb listet: Name, Kategorie, **Gelöscht am** (`tt.MM.jjjj HH:mm`),
|
||||||
|
**Gelöscht von** sowie Aktionen (Wiederherstellen; Löschen nur Admin). Die Suche
|
||||||
|
filtert nach Namen.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- [`GET /tools/trash`](/docs/reference/endpoints/tools#listTrashedTools) — Liste
|
||||||
|
- [`POST /tools/trash/restore`](/docs/reference/endpoints/tools#restoreTools) — Wiederherstellen
|
||||||
|
- [`DELETE /tools/trash`](/docs/reference/endpoints/tools#deleteTrashedTools) — Endgültig löschen (Admin)
|
||||||
|
- [`POST /tools/trash/empty`](/docs/reference/endpoints/tools#emptyTrash) — Papierkorb leeren (Admin)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
# Pläne & Berechtigungen
|
||||||
|
|
||||||
|
toolr unterscheidet **Tarife** (Tier) und **Rollen**. Admins umgehen alle
|
||||||
|
Feature-Beschränkungen.
|
||||||
|
|
||||||
|
## Tarife
|
||||||
|
|
||||||
|
| Tarif | Beschreibung |
|
||||||
|
| --- | --- |
|
||||||
|
| **Free** | Grundfunktionen: suchen, filtern, ansehen, Analytics |
|
||||||
|
| **Premium** | Zusätzlich Watchlist, Vergleichen, Papierkorb, Kosten |
|
||||||
|
| **Enterprise** | Alle Premium-Features + erweiterter Support |
|
||||||
|
|
||||||
|
### Feature-Berechtigungen
|
||||||
|
|
||||||
|
Premium/Enterprise schalten folgende Features frei:
|
||||||
|
|
||||||
|
| Feature | Funktion | Mehr erfahren |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `compare` | Tools vergleichen | [Vergleichen](/docs/handbook/vergleichen) |
|
||||||
|
| `watchlist` | Favoritenliste | [Watchlist](/docs/handbook/watchlist) |
|
||||||
|
| `trash` | Papierkorb (soft gelöschte Tools) | [Papierkorb](/docs/handbook/papierkorb) |
|
||||||
|
| `costs` | Kosten-/Lizenzmodelle erfassen | [Kosten erfassen](/docs/handbook/kosten) |
|
||||||
|
|
||||||
|
Fehlt dir ein Feature, zeigt die App einen **Upgrade-Hinweis** mit Link zur
|
||||||
|
Tarifverwaltung.
|
||||||
|
|
||||||
|
## Rollen
|
||||||
|
|
||||||
|
| Rolle | Berechtigungen |
|
||||||
|
| --- | --- |
|
||||||
|
| **User** | Standard-Konto: Tools anlegen/bewerten, eigene Tools bearbeiten |
|
||||||
|
| **Admin** | Alle User-Rechte + Verwaltung, Audit-Log, Redundanz, Papierkorb leeren, Tool-Verknüpfungen |
|
||||||
|
|
||||||
|
Admins passieren **alle** Feature-Checks — auch ohne Premium-Tarif.
|
||||||
|
|
||||||
|
## Tarif-/Rollenverwaltung
|
||||||
|
|
||||||
|
Die Zuordnung von Rolle und Tarif wird durch Admins im Bereich
|
||||||
|
[Administration](/docs/handbook/administration) (Tab „Nutzer") verwaltet.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
|
||||||
|
# Redundanz-Dashboard
|
||||||
|
|
||||||
|
Das **Redundanz-Dashboard** (`/admin/redundancy`) ist ein Admin-Werkzeug zur
|
||||||
|
automatischen Erkennung doppelter oder stark überlappender Tools — jeweils
|
||||||
|
pro Kategorie — inklusive Kosten- und Bewertungsvergleich.
|
||||||
|
|
||||||
|
> Der Zugriff ist ausschließlich Admins vorbehalten (die API ist
|
||||||
|
> admin-geschützt).
|
||||||
|
|
||||||
|
## Aufbau
|
||||||
|
|
||||||
|
- **Pro Kategorie** wird eine Gruppe angezeigt: Name der Kategorie,
|
||||||
|
Anzahl Tools und Vergleiche sowie ggf. die **gesamten monatlichen Kosten**
|
||||||
|
(z. B. `€X.XX/mo gesamt`).
|
||||||
|
- Jedes Tool wird als Karte dargestellt: Name, monatliche Kosten, Anzahl der
|
||||||
|
Bewertungen, kombinierte Bewertung, Lizenz-Badges und Feature-Anzahl.
|
||||||
|
|
||||||
|
## Vergleiche & Empfehlungen
|
||||||
|
|
||||||
|
Für jedes Tool-Paar erscheint:
|
||||||
|
|
||||||
|
- Tool A vs. Tool B, jeweils mit Bewertung (`X.X ★`) und monatlichen Kosten.
|
||||||
|
- **Überlappung** in Prozent (Fortschrittsbalken in der Mitte).
|
||||||
|
- Eine **Empfehlung** mit Konfidenz-Farbe:
|
||||||
|
- **hoch** (grün), **mittel** (gelb), **niedrig** (grau)
|
||||||
|
- Das empfohlene, bessere Tool wird mit „Daumen hoch" markiert und begründet.
|
||||||
|
|
||||||
|
## Manuelle Bewertung
|
||||||
|
|
||||||
|
Du kannst ein Paar manuell bewerten: Klicke auf Tool A oder Tool B, um
|
||||||
|
festzuhalten, welches besser ist. Die Auswahl wird gespeichert und die
|
||||||
|
Darstellung aktualisiert.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- [`GET /api/admin/redundancy`](#) — Daten laden (admin-geschützt)
|
||||||
|
- [`POST /api/admin/redundancy/evaluate`](#) — manuelle Bewertung speichern
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
# Tastenkürzel & Kommandopalette
|
||||||
|
|
||||||
|
## Kommandopalette
|
||||||
|
|
||||||
|
Die Kommandopalette ist die zentrale Schnellnavigation:
|
||||||
|
|
||||||
|
- Öffnen mit **`⌘K`** (macOS) bzw. **`Ctrl+K`** (Windows/Linux).
|
||||||
|
- Alternativ über die Suchleiste oben rechts („Tools suchen… ⌘K") oder das
|
||||||
|
Such-Icon auf Mobilgeräten.
|
||||||
|
|
||||||
|
### Leerer Zustand
|
||||||
|
|
||||||
|
Ohne Eingabe zeigt die Palette:
|
||||||
|
|
||||||
|
- **Zuletzt angesehen** — die letzten 5 Tools, die du besucht hast.
|
||||||
|
- **Navigation** — Tools durchsuchen, Tool hinzufügen, Analytics sowie
|
||||||
|
(abhängig von Berechtigungen) Watchlist, Papierkorb und Admin.
|
||||||
|
|
||||||
|
### Suche
|
||||||
|
|
||||||
|
Tippe, um live nach Tools zu suchen (max. 10 Ergebnisse, inkl. Bewertung
|
||||||
|
`X.X★`).
|
||||||
|
|
||||||
|
## Tastenkürzel im Überblick
|
||||||
|
|
||||||
|
| Kürzel | Aktion |
|
||||||
|
| --- | --- |
|
||||||
|
| `⌘K` / `Ctrl+K` | Kommandopalette öffnen |
|
||||||
|
| `/` | Suche im Bereich „Tools durchsuchen" fokussieren |
|
||||||
|
|
||||||
|
## Weitere Hinweise
|
||||||
|
|
||||||
|
- **Zuletzt angesehen** wird lokal im Browser gespeichert (max. 5 Einträge).
|
||||||
|
- Die Seitenleiste (linke Navigation) ist auf Desktop einklappbar; der
|
||||||
|
Breadcrumb oben zeigt deinen aktuellen Ort.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
# Tool anlegen
|
||||||
|
|
||||||
|
Um ein neues Tool zum Katalog hinzuzufügen, klicke auf **Tool hinzufügen**
|
||||||
|
(`/tools/new`). Das Anlegen erfordert ein Konto — ohne Anmeldung erscheint ein
|
||||||
|
Hinweis mit Login-Button.
|
||||||
|
|
||||||
|
## Formularfelder
|
||||||
|
|
||||||
|
| Feld | Pflicht | Hinweise |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Name** | Ja | Mind. 2 Zeichen |
|
||||||
|
| **Kategorie** | Ja | Auswahlliste; neue Kategorien lassen sich direkt anlegen |
|
||||||
|
| **Website URL** | Nein | Gültige URL (z. B. `https://...`) |
|
||||||
|
| **Icon / Logo URL** | Nein | Gültige URL; Vorschau wird live angezeigt |
|
||||||
|
| **Beschreibung** | Ja | Mind. 10 Zeichen; beschreibe, was das Tool tut |
|
||||||
|
| **Features** | Nein | Dynamische Liste mit Autovervollständigung (max. 6) |
|
||||||
|
| **Tags** | Nein | Dynamische Liste mit Autovervollständigung |
|
||||||
|
|
||||||
|
Neben jedem Feld führt das **?‑Icon** direkt zur zugehörigen Feldbeschreibung
|
||||||
|
in der [Datenmodell-Referenz](/docs/reference/schemas/toolinput).
|
||||||
|
|
||||||
|
### Kategorie
|
||||||
|
|
||||||
|
- Tippe, um nach bestehenden Kategorien zu suchen.
|
||||||
|
- Wähle **+ Erstelle „..."**, um eine neue Kategorie anzulegen.
|
||||||
|
|
||||||
|
### Features & Tags
|
||||||
|
|
||||||
|
- **Feature hinzufügen** / **Tag hinzufügen** hängt eine neue Zeile an.
|
||||||
|
- Die Eingabefelder schlagen bestehende Features/Tags vor
|
||||||
|
(Autovervollständigung, max. 6 Vorschläge).
|
||||||
|
- Mit dem **×**‑Button entfernst du einzelne Zeilen.
|
||||||
|
- Features und Tags helfen beim Filtern und Wiederfinden.
|
||||||
|
|
||||||
|
## Speichern
|
||||||
|
|
||||||
|
Klicke auf **Tool hinzufügen**. Nach erfolgreicher Anlage wirst du auf die
|
||||||
|
Detailseite des neuen Tools weitergeleitet.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- [`POST /tools`](/docs/reference/endpoints/tools#createTool) — Tool anlegen
|
||||||
|
- [`GET /categories`](/docs/reference/endpoints/tools#listCategories) — Kategorien
|
||||||
|
- [`GET /features/all`](/docs/reference/endpoints/tools#listAllFeatures) — Features
|
||||||
|
- [`GET /tags/all`](/docs/reference/endpoints/tools#listAllTags) — Tags
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
# Tool bearbeiten & löschen
|
||||||
|
|
||||||
|
## Bearbeiten
|
||||||
|
|
||||||
|
Auf der Detailseite eines Tools findest du die Schaltfläche **Bearbeiten**
|
||||||
|
(nur für die Person, die das Tool angelegt hat, sowie für Admins).
|
||||||
|
|
||||||
|
Die Bearbeitungsseite (`/tools/:id/edit`) enthält dieselben Felder wie beim
|
||||||
|
Anlegen (Name, Kategorie, Website/Icon-URL, Beschreibung, Features, Tags) —
|
||||||
|
bereits mit den aktuellen Werten befüllt.
|
||||||
|
|
||||||
|
- **Speichern** übernimmt die Änderungen.
|
||||||
|
- **Abbrechen** führt zurück zur Detailseite.
|
||||||
|
|
||||||
|
API-Referenz: [`PATCH /tools/{id}`](/docs/reference/endpoints/tools#updateTool).
|
||||||
|
|
||||||
|
## Löschen
|
||||||
|
|
||||||
|
Über **Löschen** auf der Detailseite wird das Tool entfernt. Das Verhalten
|
||||||
|
hängt von deinem Tarif ab:
|
||||||
|
|
||||||
|
- **Mit Papierkorb-Zugang** (Premium/Enterprise oder Admin): Das Tool wird
|
||||||
|
**soft gelöscht** — es verschwindet aus allen öffentlichen Ansichten, kann
|
||||||
|
aber im [Papierkorb](/docs/handbook/papierkorb) wiederhergestellt oder
|
||||||
|
endgültig gelöscht werden.
|
||||||
|
- **Ohne Papierkorb-Zugang:** Das Tool wird **endgültig** gelöscht und kann
|
||||||
|
nicht wiederhergestellt werden.
|
||||||
|
|
||||||
|
Die Löschung ist nur für die Person, die das Tool angelegt hat, sowie für
|
||||||
|
Admins möglich.
|
||||||
|
|
||||||
|
API-Referenz: [`DELETE /tools/{id}`](/docs/reference/endpoints/tools#deleteTool).
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
# Tools finden & durchsuchen
|
||||||
|
|
||||||
|
Der Bereich **Tools durchsuchen** (`/tools`) ist der Einstieg in den Katalog.
|
||||||
|
Hier kombinierst du Suche, Filter und Sortierung, um genau die Tools zu finden,
|
||||||
|
die dich interessieren.
|
||||||
|
|
||||||
|
## Suche
|
||||||
|
|
||||||
|
- Die **Suchleiste** durchsucht Name und Beschreibung (Volltext).
|
||||||
|
- Tastenkürzel: Drücke **`/`**, um die Suche zu fokussieren.
|
||||||
|
- Die Eingabe ist deaktiviert (Debounce), damit bei jedem Tastendruck sofort
|
||||||
|
nachgefiltert wird.
|
||||||
|
|
||||||
|
## Filtern
|
||||||
|
|
||||||
|
Über die Schaltfläche **Filter** (mit Badge für die Anzahl aktiver Filter)
|
||||||
|
öffnest du den Filter-Popover mit:
|
||||||
|
|
||||||
|
- **Tags** — Auswahl über Checkboxen (scrollbare Liste).
|
||||||
|
- **Features** — Auswahl über Checkboxen.
|
||||||
|
- **Mindestbewertung** — Schieberegler von 0 bis 5 (Schritte von 0,5); zeigt
|
||||||
|
z. B. „3.0+" an.
|
||||||
|
|
||||||
|
Aktive Filter erscheinen als **entfernbare Chips** über der Ergebnisliste.
|
||||||
|
Mit **Filter zurücksetzen** bzw. **Alle entfernen** räumst du sie wieder auf.
|
||||||
|
|
||||||
|
## Sortieren
|
||||||
|
|
||||||
|
Über das Dropdown **Sortieren** stehen folgende Optionen zur Verfügung:
|
||||||
|
|
||||||
|
| Sortierung | Beschreibung |
|
||||||
|
| --- | --- |
|
||||||
|
| Neueste | Neue Tools zuerst |
|
||||||
|
| Top bewertet | Nach kombinierter Bewertung |
|
||||||
|
| Meistbewertet | Nach Anzahl der Bewertungen |
|
||||||
|
| Name (A–Z) | Alphabetisch aufsteigend |
|
||||||
|
| Name (Z–A) | Alphabetisch absteigend |
|
||||||
|
| Zuletzt aktualisiert | Nach letztem Update |
|
||||||
|
|
||||||
|
## Ansicht & Dichte
|
||||||
|
|
||||||
|
- **Ansicht wechseln:** Raster / Tabelle / Zeilen.
|
||||||
|
- **Dichte:** gemütlich / kompakt (Schieberegler).
|
||||||
|
|
||||||
|
Deine Auswahl wird gespeichert — lokal im Browser und für angemeldete Nutzer:innen
|
||||||
|
zusätzlich serverseitig in den Präferenzen. Ansicht, Dichte, Suche, Filter und
|
||||||
|
Sortierung werden dabei in die URL übernommen, sodass du Ergebnisse teilen
|
||||||
|
kannst.
|
||||||
|
|
||||||
|
## Tabellenansicht
|
||||||
|
|
||||||
|
In der Tabellenansicht sind die Spalten **Tool**, **Bewertung** und **Anzahl
|
||||||
|
Bewertungen** sortierbar. Beim Überfahren einer Zeile erscheint eine Vorschau
|
||||||
|
mit Bewertungsdetails, Tags und Mini-Balken.
|
||||||
|
|
||||||
|
## Auswählen für Vergleich & Watchlist
|
||||||
|
|
||||||
|
- Auf jeder Karte/Zeile findest du ein **Vergleichs-Icon**, mit dem du Tools zur
|
||||||
|
[Vergleichsleiste](/docs/handbook/vergleichen) hinzufügst.
|
||||||
|
- Das **Lesezeichen-Icon** speichert Tools in deiner
|
||||||
|
[Watchlist](/docs/handbook/watchlist) (nur mit dem entsprechenden Tarif).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Alle Such-, Filter- und Sortierparameter entsprechen den Query-Parametern von
|
||||||
|
[`GET /tools`](/docs/reference/endpoints/tools#listTools).
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
# Vergleichen
|
||||||
|
|
||||||
|
Mit der Vergleichsfunktion stellst du mehrere Tools **nebeneinander** gegenüber —
|
||||||
|
ideal, um eine fundierte Entscheidung zu treffen.
|
||||||
|
|
||||||
|
> Vergleichen ist ein **Premium-Feature** (Premium/Enterprise) und steht Admins
|
||||||
|
> immer zur Verfügung.
|
||||||
|
|
||||||
|
## Tools auswählen
|
||||||
|
|
||||||
|
1. Im Bereich **Tools durchsuchen** klickst du auf jeder Karte/Zeile auf das
|
||||||
|
**Vergleichs-Icon** (Waage).
|
||||||
|
2. Unten erscheint die **Vergleichsleiste** mit den ausgewählten Tools als
|
||||||
|
Chips. Du kannst einzelne Tools entfernen (×) oder die Auswahl leeren.
|
||||||
|
3. Klicke auf **Vergleichen (N)**, um zur Vergleichsansicht zu gelangen.
|
||||||
|
|
||||||
|
> Ohne Premium-Tarif ist der Button gesperrt (Schloss-Icon). Über den
|
||||||
|
> Dialog gelangst du zum Tarifwechsel
|
||||||
|
> (siehe [Pläne & Berechtigungen](/docs/handbook/plaene)).
|
||||||
|
|
||||||
|
## Die Vergleichsansicht
|
||||||
|
|
||||||
|
Die Ansicht zeigt eine Tabelle mit einer Spalte pro Tool. Zeilen:
|
||||||
|
|
||||||
|
| Zeile | Inhalt |
|
||||||
|
| --- | --- |
|
||||||
|
| **Bewertung** | Sterne + Wert (z. B. `4.2/5`) |
|
||||||
|
| **Nützlichkeit** | Wert (X.X/5) |
|
||||||
|
| **Bedienbarkeit** | Wert (X.X/5) |
|
||||||
|
| **Anzahl Bewertungen** | Anzahl |
|
||||||
|
| **Beschreibung** | Text |
|
||||||
|
| **Features** | Badges |
|
||||||
|
| **Tags** | Badges |
|
||||||
|
| **Zuletzt aktualisiert** | Datum |
|
||||||
|
|
||||||
|
Der **beste Wert** pro Zeile wird hervorgehoben (mit Trophäen-Icon).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Die Vergleichsansicht liest die Daten über
|
||||||
|
[`GET /compare`](/docs/reference/endpoints/tools#listCompareTools).
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
# Watchlist
|
||||||
|
|
||||||
|
Die **Watchlist** ist eine persönliche Favoritenliste. Tools darin kannst du
|
||||||
|
jederzeit per Klick wieder aufrufen und vergleichen.
|
||||||
|
|
||||||
|
> Die Watchlist ist ein **Premium-Feature** (Premium/Enterprise) und steht
|
||||||
|
> Admins immer zur Verfügung.
|
||||||
|
|
||||||
|
## Voraussetzung
|
||||||
|
|
||||||
|
Du benötigst einen Tarif mit `watchlist`-Berechtigung. Fehlt diese, erscheint
|
||||||
|
beim Lesezeichen ein Hinweis auf den Tarifwechsel
|
||||||
|
(siehe [Pläne & Berechtigungen](/docs/handbook/plaene)).
|
||||||
|
|
||||||
|
## Tool speichern
|
||||||
|
|
||||||
|
- Auf jeder Karte/Zeile im Bereich **Tools durchsuchen** findest du das
|
||||||
|
**Lesezeichen-Icon**.
|
||||||
|
- Ein Klick speichert das Tool in deiner Watchlist — das Icon wird gefüllt.
|
||||||
|
- Ein erneuter Klick entfernt es wieder.
|
||||||
|
|
||||||
|
## Watchlist ansehen
|
||||||
|
|
||||||
|
Öffne die Watchlist über das Benutzermenü oder die Seitenleiste. Sie zeigt alle
|
||||||
|
gespeicherten Tools als Karten. Das gefüllte Lesezeichen auf einer Karte
|
||||||
|
entfernt das Tool aus der Liste.
|
||||||
|
|
||||||
|
## Wo wird die Watchlist gespeichert?
|
||||||
|
|
||||||
|
Die Watchlist ist eine Liste von Tool-IDs in deinen **Benutzerpräferenzen**.
|
||||||
|
Damit ist sie geräteübergreifend mit deinem Konto verbunden.
|
||||||
|
|
||||||
|
API-Referenz: [`GET /auth/me/watchlist`](/docs/reference/endpoints/auth#getMeWatchlist).
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
|||||||
|
# v0.8.3 — Release Notes
|
||||||
|
|
||||||
|
**Datum:** 2026-08-04 · **Tag:** [`v0.8.3`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.3)
|
||||||
|
|
||||||
|
## Neue Features
|
||||||
|
|
||||||
|
- **Versionsbasierte Dokumentation**: Jedes Release trägt jetzt einen
|
||||||
|
vollständigen Doku-Snapshot (Handbuch + API-Referenz). Über das
|
||||||
|
Versions-Dropdown gelangst du zur Doku der jeweiligen Version
|
||||||
|
(`/docs/vX.Y.Z/…`) — inklusive Handbuch und Referenz, wie sie zum
|
||||||
|
Release-Zeitpunkt galten. Die letzten 7 Versionen bleiben verfügbar.
|
||||||
|
- **Hilfe-Buttons in Formularen (NetBox-Stil)**: Bei Bewertung, Tool
|
||||||
|
anlegen/bearbeiten, Kosten, Vergleichen, Watchlist und Analytics gibt es
|
||||||
|
jetzt ein Hilfe-Icon, das direkt zur passenden Anleitung im Handbuch führt.
|
||||||
|
|
||||||
|
## Fixes & Verbesserungen
|
||||||
|
|
||||||
|
- **Gitea-Tag-Links korrigiert**: `…/tags/vX.Y.Z` war eine 404-Seite; die
|
||||||
|
korrekte URL lautet `/releases/tag/vX.Y.Z` (in allen Release-Notes und der
|
||||||
|
Doku-Seite).
|
||||||
|
- Versions-Wechsel führt jetzt zur Doku der gewählten Version statt direkt
|
||||||
|
nur zur Release-Seite; alte `/docs/vX.Y.Z`-Redirects entfallen.
|
||||||
|
- Doku-Generator: `--snapshot` erzeugt Handbuch + Referenz pro Version; beim
|
||||||
|
Build werden die Snapshots der letzten 7 Versionen eingespielt.
|
||||||
|
|
||||||
|
## API-Änderungen
|
||||||
|
|
||||||
|
- Keine Änderungen an der API.
|
||||||
|
|
||||||
|
## Betrieb / Upgrade
|
||||||
|
|
||||||
|
- **Env-Vars:** unverändert.
|
||||||
|
- **Migration:** keine.
|
||||||
|
- **Breaking Changes:** keine. Neue Release-Notes müssen den korrekten
|
||||||
|
Tag-Link verwenden (siehe `docs/releases/TEMPLATE.md`). Für Versionen vor
|
||||||
|
diesem Release können Snapshots nachträglich erzeugt werden:
|
||||||
|
`node scripts/src/generate-docs.mjs --snapshot vX.Y.Z`.
|
||||||
|
|
||||||
|
## Bekannte Einschränkungen
|
||||||
|
|
||||||
|
- Ältere Releases ohne Snapshot (z. B. v0.6.0) zeigen nur ihre Release-Notes,
|
||||||
|
kein Handbuch/keine Referenz. Mit `--snapshot` nachpflegen.
|
||||||
|
- Die Doku-Version entspricht dem Doku-Stand zum Zeitpunkt der Erstellung des
|
||||||
|
Snapshots; Änderungen am Handbuch gelten ab dem jeweils nächsten Release.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Commit: [`3bb4598`](https://git.kubebase.de/admin/tool-evaluator/commit/3bb4598)
|
||||||
|
- Tag: [`v0.8.3`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.3)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user