Compare commits
4 Commits
v0.7.0
...
56f1edd613
| Author | SHA1 | Date | |
|---|---|---|---|
| 56f1edd613 | |||
| fdf2d741a1 | |||
| 9ceb11e7f9 | |||
| 6c92b6358d |
@@ -4,8 +4,8 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node ../../scripts/src/sync-release-docs.mjs && vite --config vite.config.ts --host 0.0.0.0",
|
||||
"build": "node ../../scripts/src/sync-release-docs.mjs && vite build --config vite.config.ts",
|
||||
"dev": "node ../../scripts/src/generate-docs.mjs && vite --config vite.config.ts --host 0.0.0.0",
|
||||
"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",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
|
||||
@@ -47,8 +47,7 @@ function Router() {
|
||||
<Route path="/admin" component={Admin} />
|
||||
<Route path="/admin/redundancy" component={Redundancy} />
|
||||
<Route path="/trash" component={Trash} />
|
||||
<Route path="/docs" component={Docs} />
|
||||
<Route path="/docs/:version" component={Docs} />
|
||||
<Route path="/docs/*?" component={Docs} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
@@ -37,10 +37,6 @@ function buildCrumbs(location: string, t: TFunction): Crumb[] {
|
||||
crumbs.push({ label: t("compare.title") });
|
||||
} else if (location.startsWith("/analytics")) {
|
||||
crumbs.push({ label: t("nav.analytics") });
|
||||
} else if (location.startsWith("/docs")) {
|
||||
crumbs.push({ href: "/docs", label: t("nav.docs") });
|
||||
const match = location.match(/^\/docs\/(.+)$/);
|
||||
if (match) crumbs.push({ label: match[1] });
|
||||
} else if (location.startsWith("/login")) {
|
||||
crumbs.push({ label: t("auth.signIn") });
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
"trash": "Papierkorb",
|
||||
"admin": "Admin",
|
||||
"redundancy": "Redundanz",
|
||||
"docs": "Doku",
|
||||
"docs": "Hilfe",
|
||||
"search": "Tools suchen…"
|
||||
},
|
||||
"auth": {
|
||||
@@ -183,10 +183,33 @@
|
||||
"backHome": "Zurück zur Startseite"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Versionsdokumentation",
|
||||
"subtitle": "Version-gebundene Dokumentation je Release — was ist neu, was hat sich geändert und was beim Upgrade zu beachten ist.",
|
||||
"title": "Dokumentation",
|
||||
"subtitle": "Version-gebundene Dokumentation — Release-Notes, Endpunkte und Datenfelder je Version.",
|
||||
"backToIndex": "Alle Releases",
|
||||
"noDocs": "Noch keine Release-Dokumentation verfügbar."
|
||||
"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": {
|
||||
"navigate": "Navigation",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"trash": "Trash",
|
||||
"admin": "Admin",
|
||||
"redundancy": "Redundancy",
|
||||
"docs": "Docs",
|
||||
"docs": "Help",
|
||||
"search": "Search tools…"
|
||||
},
|
||||
"auth": {
|
||||
@@ -183,10 +183,33 @@
|
||||
"backHome": "Back to Home"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Release Documentation",
|
||||
"subtitle": "Version-bound documentation for each release — what's new, what changed, and what to know when upgrading.",
|
||||
"title": "Documentation",
|
||||
"subtitle": "Version-bound documentation — release notes, endpoints and data fields per version.",
|
||||
"backToIndex": "All releases",
|
||||
"noDocs": "No release documentation available yet."
|
||||
"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": {
|
||||
"navigate": "Navigate",
|
||||
|
||||
@@ -265,6 +265,25 @@
|
||||
*/
|
||||
@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 */
|
||||
input[type="search"]::-webkit-search-cancel-button {
|
||||
@apply hidden;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -60,6 +60,7 @@ import {
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { customFetch } from "@workspace/api-client-react";
|
||||
import { recordRecentTool } from "@/lib/recent-tools";
|
||||
import { FieldHelp } from "@/components/field-help";
|
||||
|
||||
const ratingSchema = z.object({
|
||||
usefulness: z.number().min(1).max(5),
|
||||
@@ -776,7 +777,10 @@ export default function ToolDetail() {
|
||||
name="usefulness"
|
||||
render={({ field }) => (
|
||||
<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">
|
||||
<RatingStars
|
||||
value={field.value}
|
||||
@@ -794,7 +798,10 @@ export default function ToolDetail() {
|
||||
name="usability"
|
||||
render={({ field }) => (
|
||||
<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">
|
||||
<RatingStars
|
||||
value={field.value}
|
||||
@@ -814,7 +821,10 @@ export default function ToolDetail() {
|
||||
name="comment"
|
||||
render={({ field }) => (
|
||||
<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>
|
||||
<Textarea
|
||||
placeholder="What do you think about this tool?"
|
||||
@@ -832,7 +842,10 @@ export default function ToolDetail() {
|
||||
name="reviewerName"
|
||||
render={({ field }) => (
|
||||
<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>
|
||||
<Input placeholder="Anonymous" {...field} />
|
||||
</FormControl>
|
||||
|
||||
@@ -30,6 +30,7 @@ import { CategoryCombobox } from "@/components/category-combobox";
|
||||
import { FeatureInput } from "@/components/feature-input";
|
||||
import { TagInput } from "@/components/tag-input";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { FieldHelp } from "@/components/field-help";
|
||||
|
||||
const toolSchema = z.object({
|
||||
name: z.string().min(2, "Name must be at least 2 characters"),
|
||||
@@ -171,7 +172,10 @@ export default function ToolEdit() {
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel className="inline-flex items-center gap-1.5">
|
||||
Name
|
||||
<FieldHelp schema="ToolInput" field="name">Name</FieldHelp>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Tool name" {...field} />
|
||||
</FormControl>
|
||||
@@ -184,7 +188,10 @@ export default function ToolEdit() {
|
||||
name="category"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Category</FormLabel>
|
||||
<FormLabel className="inline-flex items-center gap-1.5">
|
||||
Category
|
||||
<FieldHelp schema="ToolInput" field="category">Category</FieldHelp>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<CategoryCombobox value={field.value} onChange={field.onChange} />
|
||||
</FormControl>
|
||||
@@ -198,8 +205,11 @@ export default function ToolEdit() {
|
||||
control={form.control}
|
||||
name="websiteUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Website URL (Optional)</FormLabel>
|
||||
<FormItem>
|
||||
<FormLabel className="inline-flex items-center gap-1.5">
|
||||
Website URL (Optional)
|
||||
<FieldHelp schema="ToolInput" field="websiteUrl">Website URL</FieldHelp>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://..." type="url" {...field} />
|
||||
</FormControl>
|
||||
@@ -212,8 +222,11 @@ export default function ToolEdit() {
|
||||
control={form.control}
|
||||
name="iconUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Icon / Logo URL (Optional)</FormLabel>
|
||||
<FormItem>
|
||||
<FormLabel className="inline-flex items-center gap-1.5">
|
||||
Icon / Logo URL (Optional)
|
||||
<FieldHelp schema="ToolInput" field="iconUrl">Icon / Logo URL</FieldHelp>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<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">
|
||||
@@ -245,8 +258,11 @@ export default function ToolEdit() {
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormItem>
|
||||
<FormLabel className="inline-flex items-center gap-1.5">
|
||||
Description
|
||||
<FieldHelp schema="ToolInput" field="description">Description</FieldHelp>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="What does this tool do?"
|
||||
@@ -262,7 +278,10 @@ export default function ToolEdit() {
|
||||
<div className="space-y-4 pt-4 border-t">
|
||||
<div className="flex justify-between items-center">
|
||||
<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>
|
||||
</div>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => appendFeature({ value: "" })}>
|
||||
@@ -306,7 +325,10 @@ export default function ToolEdit() {
|
||||
<div className="space-y-4 pt-4 border-t">
|
||||
<div className="flex justify-between items-center">
|
||||
<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>
|
||||
</div>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => appendTag({ value: "" })}>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { CategoryCombobox } from "@/components/category-combobox";
|
||||
import { FeatureInput } from "@/components/feature-input";
|
||||
import { TagInput } from "@/components/tag-input";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { FieldHelp } from "@/components/field-help";
|
||||
|
||||
const toolSchema = z.object({
|
||||
name: z.string().min(2, "Name must be at least 2 characters"),
|
||||
@@ -134,7 +135,10 @@ export default function ToolNew() {
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel className="inline-flex items-center gap-1.5">
|
||||
Name
|
||||
<FieldHelp schema="ToolInput" field="name">Name</FieldHelp>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="e.g. React, Next.js, Postgres" {...field} data-testid="input-tool-name" />
|
||||
</FormControl>
|
||||
@@ -148,7 +152,10 @@ export default function ToolNew() {
|
||||
name="category"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Category</FormLabel>
|
||||
<FormLabel className="inline-flex items-center gap-1.5">
|
||||
Category
|
||||
<FieldHelp schema="ToolInput" field="category">Category</FieldHelp>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<CategoryCombobox
|
||||
value={field.value}
|
||||
@@ -161,12 +168,15 @@ export default function ToolNew() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="websiteUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Website URL (Optional)</FormLabel>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="websiteUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="inline-flex items-center gap-1.5">
|
||||
Website URL (Optional)
|
||||
<FieldHelp schema="ToolInput" field="websiteUrl">Website URL</FieldHelp>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://..." type="url" {...field} data-testid="input-tool-url" />
|
||||
</FormControl>
|
||||
@@ -180,7 +190,10 @@ export default function ToolNew() {
|
||||
name="iconUrl"
|
||||
render={({ field }) => (
|
||||
<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>
|
||||
<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">
|
||||
@@ -214,7 +227,10 @@ export default function ToolNew() {
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormLabel className="inline-flex items-center gap-1.5">
|
||||
Description
|
||||
<FieldHelp schema="ToolInput" field="description">Description</FieldHelp>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="What does this tool do? Why do people use it?"
|
||||
@@ -231,7 +247,10 @@ export default function ToolNew() {
|
||||
<div className="space-y-4 pt-4 border-t">
|
||||
<div className="flex justify-between items-center">
|
||||
<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>
|
||||
</div>
|
||||
<Button
|
||||
@@ -284,7 +303,10 @@ export default function ToolNew() {
|
||||
<div className="space-y-4 pt-4 border-t">
|
||||
<div className="flex justify-between items-center">
|
||||
<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>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
+35
-22
@@ -1,38 +1,51 @@
|
||||
# Release-Dokumentation
|
||||
# Dokumentation (Handbuch, API-Referenz, Release-Notes)
|
||||
|
||||
Jeder Release hat eine version-gebundene Dokumentation unter
|
||||
`docs/releases/`. Die Dokumentation wird öffentlich in der App unter
|
||||
`/docs` (Index) und `/docs/<version>` (Detail) angezeigt.
|
||||
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` — Dokumentation pro Release (eine Datei je Version)
|
||||
- `docs/releases/vX.Y.Z.md` — Notes pro Release
|
||||
- `docs/releases/vX.Y.Z/reference.json` — API-Snapshot der jeweiligen Version
|
||||
|
||||
## Inhalt
|
||||
## Generator
|
||||
|
||||
Pro Release wird abgedeckt (kombiniert):
|
||||
`scripts/src/generate-docs.mjs` wird beim Frontend-Build (und `dev`) automatisch
|
||||
ausgeführt und schreibt die Artefakte nach `artifacts/toolrate/public/docs/`:
|
||||
|
||||
- **Changelog:** Neue Features, Fixes & Verbesserungen
|
||||
- **API-Änderungen:** Neue/geänderte/entfernte Endpunkte (Delta zur Vorversion)
|
||||
- **Betrieb / Upgrade:** Env-Vars, DB-Migrationen, Breaking Changes
|
||||
- `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 seit dem letzten Tag
|
||||
ableiten:
|
||||
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
|
||||
```
|
||||
(Funktions-/Fix-Commits in die passenden Abschnitte übernehmen, API-Delta
|
||||
anhand `lib/api-spec/openapi.yaml` prüfen.)
|
||||
3. **Committen & pushen.** Der Sync-Schritt (`scripts/sync-release-docs.mjs`)
|
||||
kopiert die Markdown-Dateien beim Frontend-Build automatisch nach
|
||||
`artifacts/toolrate/public/docs/` und generiert `index.json`. Dadurch sind
|
||||
die Releases im Deployment als `/docs/...` verfügbar.
|
||||
(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: `index.json` und die kopierten Dateien unter
|
||||
> `artifacts/toolrate/public/docs/` sind Build-Artefakte und werden bei jedem
|
||||
> Build neu generiert — nicht von Hand bearbeiten.
|
||||
> 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,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 @@
|
||||
# v0.7.0 — Release Notes
|
||||
|
||||
**Datum:** 2026-08-03 · **Tag:** [`v0.7.0`](https://git.kubebase.de/admin/tool-evaluator/tags/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/tags/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/tags/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/tags/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/tags/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/tags/v0.8.1)
|
||||
Generated
+13
@@ -78,6 +78,9 @@ catalogs:
|
||||
wouter:
|
||||
specifier: 3.10.0
|
||||
version: 3.10.0
|
||||
yaml:
|
||||
specifier: 2.9.0
|
||||
version: 2.9.0
|
||||
zod:
|
||||
specifier: 4.4.3
|
||||
version: 4.4.3
|
||||
@@ -718,6 +721,9 @@ importers:
|
||||
tsx:
|
||||
specifier: 'catalog:'
|
||||
version: 4.23.4
|
||||
yaml:
|
||||
specifier: 'catalog:'
|
||||
version: 2.9.0
|
||||
|
||||
packages:
|
||||
|
||||
@@ -3751,6 +3757,11 @@ packages:
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yaml@2.9.0:
|
||||
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yocto-queue@1.2.2:
|
||||
resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
|
||||
engines: {node: '>=12.20'}
|
||||
@@ -6596,6 +6607,8 @@ snapshots:
|
||||
|
||||
yaml@2.8.4: {}
|
||||
|
||||
yaml@2.9.0: {}
|
||||
|
||||
yocto-queue@1.2.2: {}
|
||||
|
||||
yoctocolors@2.1.2: {}
|
||||
|
||||
@@ -73,6 +73,7 @@ catalog:
|
||||
tsx: 4.23.4
|
||||
vite: 8.2.0
|
||||
wouter: 3.10.0
|
||||
yaml: 2.9.0
|
||||
zod: 4.4.3
|
||||
|
||||
autoInstallPeers: false
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"tsx": "catalog:"
|
||||
"tsx": "catalog:",
|
||||
"yaml": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
// Builds the /docs static content for the toolrate frontend.
|
||||
//
|
||||
// Sources:
|
||||
// - lib/api-spec/openapi.yaml -> reference.json (endpoints + schemas)
|
||||
// - docs/handbook/*.md -> handbook pages (current docs)
|
||||
// - docs/releases/*.md -> version-bound release notes
|
||||
// - docs/releases/<v>/reference.json -> per-version reference snapshots
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/src/generate-docs.mjs # build mode (run before vite build/dev)
|
||||
// node scripts/src/generate-docs.mjs --snapshot v0.8.0 # write docs/releases/<v>/reference.json
|
||||
//
|
||||
// Build mode copies committed snapshots and regenerates the CURRENT reference
|
||||
// from the live openapi.yaml. The --snapshot mode is run manually when
|
||||
// preparing a release so that older versions keep their own field reference.
|
||||
|
||||
import { readFile, readdir, copyFile, mkdir, rm, writeFile, stat } from "node:fs/promises";
|
||||
import { resolve, join, dirname, basename } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
|
||||
const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
||||
const openapiPath = resolve(root, "lib/api-spec/openapi.yaml");
|
||||
const handbookDir = resolve(root, "docs/handbook");
|
||||
const releasesDir = resolve(root, "docs/releases");
|
||||
const targetDir = resolve(root, "artifacts/toolrate/public/docs");
|
||||
|
||||
const isReleaseFile = (name) => /^v\d+\.\d+\.\d+\.md$/.test(name);
|
||||
const isHandbookFile = (name) => /\.md$/.test(name);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Version helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function parseVersion(name) {
|
||||
return name.replace(/\.md$/, "");
|
||||
}
|
||||
|
||||
function cmp(a, b) {
|
||||
const pa = parseVersion(a).slice(1).split(".").map(Number);
|
||||
const pb = parseVersion(b).slice(1).split(".").map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function extractTitle(content) {
|
||||
const m = content.match(/^#\s+(.+)$/m);
|
||||
return m ? m[1].trim() : null;
|
||||
}
|
||||
|
||||
function extractDate(content) {
|
||||
const m = content.match(/(?:Datum|Date)[^\d\n]{0,20}(\d{4}-\d{2}-\d{2})/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OpenAPI -> reference model
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function deref(schema) {
|
||||
return schema && typeof schema === "object" ? schema : {};
|
||||
}
|
||||
|
||||
function fieldType(schema) {
|
||||
const s = deref(schema);
|
||||
if (s.$ref) {
|
||||
return { kind: "ref", value: s.$ref.split("/").pop() };
|
||||
}
|
||||
if (Array.isArray(s.type)) {
|
||||
return { kind: "type", value: s.type.filter(Boolean).join(" | ") };
|
||||
}
|
||||
if (s.type === "array") {
|
||||
const item = deref(s.items);
|
||||
if (item.$ref) return { kind: "array", value: item.$ref.split("/").pop() };
|
||||
return { kind: "array", value: String(item.type ?? "any") };
|
||||
}
|
||||
return { kind: "type", value: String(s.type ?? "any") };
|
||||
}
|
||||
|
||||
function describeField(schema) {
|
||||
const s = deref(schema);
|
||||
const parts = [];
|
||||
if (s.format) parts.push(s.format);
|
||||
if (Array.isArray(s.enum) && s.enum.length > 0) parts.push(s.enum.join(", "));
|
||||
if (s.minLength != null) parts.push(`min ${s.minLength} chars`);
|
||||
if (s.minItems != null) parts.push(`min ${s.minItems} items`);
|
||||
if (s.maxItems != null) parts.push(`max ${s.maxItems} items`);
|
||||
if (s.minimum != null && s.maximum != null) parts.push(`${s.minimum}–${s.maximum}`);
|
||||
else if (s.minimum != null) parts.push(`>= ${s.minimum}`);
|
||||
else if (s.maximum != null) parts.push(`<= ${s.maximum}`);
|
||||
return parts.join(", ");
|
||||
}
|
||||
|
||||
function buildSchemaModel(name, schema) {
|
||||
const s = deref(schema);
|
||||
const required = new Set(Array.isArray(s.required) ? s.required : []);
|
||||
const fields = Object.entries(s.properties ?? {})
|
||||
.filter(([key]) => !key.startsWith("$"))
|
||||
.map(([key, prop]) => {
|
||||
const t = fieldType(prop);
|
||||
return {
|
||||
name: key,
|
||||
type: t,
|
||||
required: required.has(key),
|
||||
description: deref(prop).description ?? "",
|
||||
constraints: describeField(prop),
|
||||
};
|
||||
});
|
||||
return {
|
||||
name,
|
||||
description: s.description ?? "",
|
||||
fields,
|
||||
};
|
||||
}
|
||||
|
||||
function buildEndpointModel(path, pathItem) {
|
||||
const models = [];
|
||||
for (const [method, op] of Object.entries(pathItem)) {
|
||||
if (!["get", "post", "patch", "put", "delete"].includes(method)) continue;
|
||||
const o = deref(op);
|
||||
const parameters = (o.parameters ?? []).map((p) => {
|
||||
const s = deref(p.schema);
|
||||
const t = fieldType(p.schema);
|
||||
return {
|
||||
name: p.name,
|
||||
in: p.in,
|
||||
required: !!p.required,
|
||||
type: t,
|
||||
description: p.description ?? s.description ?? "",
|
||||
constraints: describeField(p.schema),
|
||||
};
|
||||
});
|
||||
const requestBody = o.requestBody
|
||||
? {
|
||||
required: !!o.requestBody.required,
|
||||
schema: fieldType(deref(o.requestBody).content?.["application/json"]?.schema),
|
||||
}
|
||||
: null;
|
||||
const responses = Object.entries(o.responses ?? {}).map(([status, r]) => ({
|
||||
status,
|
||||
description: deref(r).description ?? "",
|
||||
schema: fieldType(deref(r).content?.["application/json"]?.schema),
|
||||
}));
|
||||
models.push({
|
||||
operationId: o.operationId ?? `${method} ${path}`,
|
||||
method: method.toUpperCase(),
|
||||
path,
|
||||
summary: o.summary ?? "",
|
||||
description: o.description ?? "",
|
||||
parameters,
|
||||
requestBody,
|
||||
responses,
|
||||
});
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
function buildReference(api) {
|
||||
const tagNames = (api.tags ?? []).map((t) => t.name);
|
||||
const tags = tagNames.map((name) => {
|
||||
const meta = (api.tags ?? []).find((t) => t.name === name) ?? {};
|
||||
const endpoints = [];
|
||||
for (const [path, pathItem] of Object.entries(api.paths ?? {})) {
|
||||
for (const model of buildEndpointModel(path, pathItem)) {
|
||||
const rawOp = pathItem[model.method.toLowerCase()];
|
||||
if ((rawOp?.tags ?? []).includes(name)) endpoints.push(model);
|
||||
}
|
||||
}
|
||||
return { name, description: meta.description ?? "", endpoints };
|
||||
});
|
||||
const schemas = Object.entries(api.components?.schemas ?? {}).map(([name, s]) =>
|
||||
buildSchemaModel(name, s),
|
||||
);
|
||||
return { tags, schemas };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handbook parsing (frontmatter: title, order, icon)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function parseHandbook(content) {
|
||||
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
||||
if (!match) return { frontmatter: {}, body: content };
|
||||
let frontmatter = {};
|
||||
try {
|
||||
frontmatter = parseYaml(match[1]) ?? {};
|
||||
} catch {
|
||||
frontmatter = {};
|
||||
}
|
||||
return { frontmatter, body: content.slice(match[0].length) };
|
||||
}
|
||||
|
||||
function slugify(name) {
|
||||
return name.replace(/\.md$/, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
||||
}
|
||||
|
||||
async function buildHandbookIndex() {
|
||||
let files;
|
||||
try {
|
||||
files = (await readdir(handbookDir)).filter(isHandbookFile);
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") return [];
|
||||
throw err;
|
||||
}
|
||||
const pages = [];
|
||||
for (const file of files) {
|
||||
const content = await readFile(join(handbookDir, file), "utf8");
|
||||
const { frontmatter, body } = parseHandbook(content);
|
||||
pages.push({
|
||||
slug: slugify(basename(file)),
|
||||
file,
|
||||
title: frontmatter.title ?? extractTitle(body) ?? basename(file),
|
||||
order: typeof frontmatter.order === "number" ? frontmatter.order : 999,
|
||||
});
|
||||
await writeFile(join(targetDir, "handbook", file), body);
|
||||
}
|
||||
pages.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));
|
||||
return pages;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Search index
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function stripMarkdown(md) {
|
||||
return md
|
||||
.replace(/```[\s\S]*?```/g, " ")
|
||||
.replace(/[#>*`_\-\[\]()!]/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
async function buildSearchIndex(reference, handbookPages, releaseVersions) {
|
||||
const entries = [];
|
||||
|
||||
for (const page of handbookPages) {
|
||||
const file = join(handbookDir, page.file);
|
||||
const content = await readFile(file, "utf8");
|
||||
const { body } = parseHandbook(content);
|
||||
entries.push({
|
||||
title: page.title,
|
||||
href: `/docs/handbook/${page.slug}`,
|
||||
kind: "guide",
|
||||
text: stripMarkdown(body),
|
||||
});
|
||||
}
|
||||
|
||||
for (const tag of reference.tags) {
|
||||
for (const ep of tag.endpoints) {
|
||||
entries.push({
|
||||
title: `${ep.method} ${ep.path}`,
|
||||
href: `/docs/reference/endpoints/${tag.name}#${ep.operationId}`,
|
||||
kind: "endpoint",
|
||||
text: `${ep.summary} ${ep.description} ${ep.parameters
|
||||
.map((p) => `${p.name} ${p.description}`)
|
||||
.join(" ")}`.trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const schema of reference.schemas) {
|
||||
for (const field of schema.fields) {
|
||||
entries.push({
|
||||
title: `${schema.name}.${field.name}`,
|
||||
href: `/docs/reference/schemas/${schema.name}#${field.name}`,
|
||||
kind: "field",
|
||||
text: `${field.description} ${field.constraints} ${field.type.value ?? ""}`.trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const version of releaseVersions) {
|
||||
const file = join(releasesDir, `${version}.md`);
|
||||
const content = await readFile(file, "utf8");
|
||||
entries.push({
|
||||
title: version,
|
||||
href: `/docs/releases/${version}`,
|
||||
kind: "release",
|
||||
text: stripMarkdown(content),
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Snapshot mode
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function writeSnapshot(version) {
|
||||
const api = parseYaml(await readFile(openapiPath, "utf8"));
|
||||
const reference = buildReference(api);
|
||||
const dir = join(releasesDir, version);
|
||||
await mkdir(dir, { recursive: true });
|
||||
await writeFile(join(dir, "reference.json"), JSON.stringify(reference, null, 2));
|
||||
console.log(`[generate-docs] snapshot ${version} -> ${join(dir, "reference.json")}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Build mode
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function main() {
|
||||
const snapshotArg = process.argv.indexOf("--snapshot");
|
||||
if (snapshotArg >= 0) {
|
||||
const version = process.argv[snapshotArg + 1];
|
||||
if (!version) {
|
||||
console.error("[generate-docs] --snapshot requires a version, e.g. v0.8.0");
|
||||
process.exit(1);
|
||||
}
|
||||
await writeSnapshot(version);
|
||||
return;
|
||||
}
|
||||
|
||||
let releaseNames;
|
||||
try {
|
||||
releaseNames = (await readdir(releasesDir)).filter(isReleaseFile);
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
console.warn("[generate-docs] docs/releases not found; nothing to sync");
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
releaseNames.sort(cmp).reverse();
|
||||
|
||||
await rm(targetDir, { recursive: true, force: true });
|
||||
await mkdir(join(targetDir, "handbook"), { recursive: true });
|
||||
await mkdir(join(targetDir, "releases"), { recursive: true });
|
||||
await mkdir(join(targetDir, "versions"), { recursive: true });
|
||||
|
||||
// 1. Current reference from live openapi.yaml
|
||||
const api = parseYaml(await readFile(openapiPath, "utf8"));
|
||||
const reference = buildReference(api);
|
||||
await writeFile(join(targetDir, "reference.json"), JSON.stringify(reference, null, 2));
|
||||
|
||||
// 2. Handbook pages (current docs)
|
||||
const handbookPages = await buildHandbookIndex();
|
||||
await writeFile(
|
||||
join(targetDir, "handbook/index.json"),
|
||||
JSON.stringify(handbookPages, null, 2),
|
||||
);
|
||||
|
||||
// 3. Release notes + versioned reference snapshots
|
||||
const versions = [];
|
||||
for (const name of releaseNames) {
|
||||
const version = parseVersion(name);
|
||||
const content = await readFile(join(releasesDir, name), "utf8");
|
||||
await copyFile(join(releasesDir, name), join(targetDir, "releases", name));
|
||||
const hasSnapshot = await stat(join(releasesDir, version, "reference.json"))
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
if (hasSnapshot) {
|
||||
await copyFile(
|
||||
join(releasesDir, version, "reference.json"),
|
||||
join(targetDir, "versions", `${version}.json`),
|
||||
);
|
||||
}
|
||||
versions.push({
|
||||
version,
|
||||
file: `releases/${name}`,
|
||||
title: extractTitle(content) ?? version,
|
||||
date: extractDate(content) ?? null,
|
||||
hasReference: hasSnapshot,
|
||||
});
|
||||
}
|
||||
|
||||
await writeFile(join(targetDir, "index.json"), JSON.stringify(versions, null, 2));
|
||||
|
||||
// 4. Search index
|
||||
const searchIndex = await buildSearchIndex(reference, handbookPages, versions.map((v) => v.version));
|
||||
await writeFile(join(targetDir, "search.json"), JSON.stringify(searchIndex, null, 2));
|
||||
|
||||
console.log(
|
||||
`[generate-docs] synced ${versions.length} release(s), ${handbookPages.length} handbook page(s), ` +
|
||||
`${reference.schemas.length} schema(s), ${searchIndex.length} search entries`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("[generate-docs] failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
// Copies docs/releases/*.md into the toolrate frontend's static public dir
|
||||
// and generates index.json (version list) so the app can render /docs.
|
||||
//
|
||||
// Run before `vite build` (Vite copies public/ -> dist/public verbatim), and
|
||||
// before `vite dev` so the docs are available locally too.
|
||||
//
|
||||
// Usage: node scripts/src/sync-release-docs.mjs
|
||||
|
||||
import { readdir, readFile, copyFile, mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { resolve, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
||||
const sourceDir = resolve(root, "docs/releases");
|
||||
const targetDir = resolve(root, "artifacts/toolrate/public/docs");
|
||||
|
||||
const isReleaseFile = (name) => /^v\d+\.\d+\.\d+\.md$/.test(name);
|
||||
|
||||
function parseVersion(name) {
|
||||
return name.replace(/\.md$/, "");
|
||||
}
|
||||
|
||||
// Semantic-ish comparison: v0.6.0 > v0.5.0 > v0.4.2
|
||||
function cmp(a, b) {
|
||||
const pa = parseVersion(a).slice(1).split(".").map(Number);
|
||||
const pb = parseVersion(b).slice(1).split(".").map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function extractTitle(content) {
|
||||
const m = content.match(/^#\s+(.+)$/m);
|
||||
return m ? m[1].trim() : null;
|
||||
}
|
||||
|
||||
function extractDate(content) {
|
||||
// e.g. "**Datum:** 2026-08-03" or "**Date:** 2026-08-03" in the header block.
|
||||
// Match the first ISO date that follows a Datum/Date label, tolerating
|
||||
// bold markers/colons around it.
|
||||
const m = content.match(/(?:Datum|Date)[^\d\n]{0,20}(\d{4}-\d{2}-\d{2})/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let names;
|
||||
try {
|
||||
names = (await readdir(sourceDir)).filter(isReleaseFile);
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
console.warn(`[sync-release-docs] ${sourceDir} not found; nothing to sync`);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (names.length === 0) {
|
||||
console.warn("[sync-release-docs] no release docs found; clearing target dir");
|
||||
}
|
||||
|
||||
names.sort(cmp).reverse();
|
||||
|
||||
await rm(targetDir, { recursive: true, force: true });
|
||||
await mkdir(targetDir, { recursive: true });
|
||||
|
||||
const index = [];
|
||||
for (const name of names) {
|
||||
const content = await readFile(join(sourceDir, name), "utf8");
|
||||
await copyFile(join(sourceDir, name), join(targetDir, name));
|
||||
index.push({
|
||||
version: parseVersion(name),
|
||||
file: name,
|
||||
title: extractTitle(content) ?? parseVersion(name),
|
||||
date: extractDate(content) ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
await writeFile(join(targetDir, "index.json"), JSON.stringify(index, null, 2));
|
||||
|
||||
console.log(`[sync-release-docs] synced ${names.length} release doc(s) -> ${targetDir}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("[sync-release-docs] failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user