From 7b4b5e1a148fd4b246aa0b40a33a7a4ee56732db Mon Sep 17 00:00:00 2001 From: Matthias G Date: Tue, 4 Aug 2026 11:56:27 +0200 Subject: [PATCH] Explain the point to whoever opens the admin The overview leads with what Logbuch is for, a new Anleitung page renders the editorial guide for humans, and the user list stops claiming accounts come from a shell command: they appear on first sign in. --- messages/de.json | 18 +++++++++--- messages/en.json | 18 +++++++++--- next.config.ts | 1 + src/app/admin/guide/page.tsx | 45 ++++++++++++++++++++++++++++++ src/app/admin/page.tsx | 7 +++++ src/components/layout/AdminNav.tsx | 4 ++- src/lib/admin-routes.ts | 1 + 7 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 src/app/admin/guide/page.tsx diff --git a/messages/de.json b/messages/de.json index f8bf6c3..7ebd62e 100644 --- a/messages/de.json +++ b/messages/de.json @@ -274,7 +274,8 @@ "manage": "Verwalten", "postTypes": "Beitragsarten", "api": "API", - "settings": "Einstellungen" + "settings": "Einstellungen", + "guide": "Anleitung" }, "messages": { "created": "Angelegt.", @@ -635,9 +636,9 @@ "back": "Zurück zur Liste", "saving": "Wird gespeichert", "emptyTitle": "Noch kein Konto", - "empty": "Es gibt noch kein Konto. Leg eins mit dem Befehl pnpm admin:create an.", + "empty": "Es gibt noch kein Konto. Sobald sich jemand zum ersten Mal anmeldet, erscheint er hier.", "newAccountTitle": "Neues Konto", - "newAccount": "Konten entstehen über den Befehl pnpm admin:create. Danach vergibst du hier die Rechte.", + "newAccount": "Konten entstehen bei der ersten Anmeldung. Wer eine Adresse auf einer erlaubten Domain hat, holt sich unter Anmelden selbst einen Link per E-Mail. Danach vergibst du hier die Rechte.", "adminSection": "Verwaltungsrecht", "isAdmin": "Dieses Konto verwaltet alles und hat Zugriff auf jedes Projekt.", "isNotAdmin": "Dieses Konto arbeitet nur in den Projekten, für die es freigeschaltet ist.", @@ -779,7 +780,16 @@ "neverSent": "Noch nichts verschickt. Der erste Bericht umfasst die letzten sieben Tage.", "resetWindow": "Zeitraum zurücksetzen", "resetting": "Wird zurückgesetzt" - } + }, + "guide": { + "eyebrow": "Verwaltung", + "title": "Anleitung", + "intro": "Wofür Logbuch da ist, wie ein Eintrag aufgebaut ist und wie er klingen soll. Derselbe Text, den auch ein KI-Zugang über die Schnittstelle liest.", + "missing": "Der Leitfaden ist nicht auffindbar." + }, + "welcomeTitle": "Worum es hier geht", + "welcome": "Logbuch hält fest, was in den Produkten passiert ist. Ein Eintrag sagt in ganzen Sätzen, was sich geändert hat und was der Leser davon hat, ohne Ticketnummern und ohne Fachjargon. Geschrieben wird pro Projekt, veröffentlicht wird von Hand.", + "welcomeAction": "Anleitung lesen" }, "filter": { "type": "Art", diff --git a/messages/en.json b/messages/en.json index 7e8a064..8b67cb9 100644 --- a/messages/en.json +++ b/messages/en.json @@ -274,7 +274,8 @@ "manage": "Manage", "postTypes": "Post types", "api": "API", - "settings": "Settings" + "settings": "Settings", + "guide": "Guide" }, "messages": { "created": "Created.", @@ -635,9 +636,9 @@ "back": "Back to the list", "saving": "Saving", "emptyTitle": "No account yet", - "empty": "There is no account yet. Create one with the command pnpm admin:create.", + "empty": "No account yet. As soon as someone signs in for the first time, they show up here.", "newAccountTitle": "New account", - "newAccount": "Accounts are created with the command pnpm admin:create. After that you hand out the rights here.", + "newAccount": "Accounts appear on the first sign in. Anyone with an address on an allowed domain requests a link themselves. You grant the rights here afterwards.", "adminSection": "Administration right", "isAdmin": "This account administers everything and reaches every project.", "isNotAdmin": "This account works only in the projects it has been released for.", @@ -779,7 +780,16 @@ "neverSent": "Nothing sent yet. The first digest covers the last seven days.", "resetWindow": "Reset the window", "resetting": "Resetting" - } + }, + "guide": { + "eyebrow": "Administration", + "title": "Guide", + "intro": "What Logbuch is for, how an entry is built and how it should sound. The same text an API client reads.", + "missing": "The guide could not be found." + }, + "welcomeTitle": "What this is for", + "welcome": "Logbuch records what happened in the products. An entry says in plain sentences what changed and what the reader gets from it, without ticket numbers or jargon. Writing happens per project, publishing by hand.", + "welcomeAction": "Read the guide" }, "filter": { "type": "Type", diff --git a/next.config.ts b/next.config.ts index 03585df..e09c052 100644 --- a/next.config.ts +++ b/next.config.ts @@ -14,6 +14,7 @@ const config: NextConfig = { '/api/v1/skill': ['./docs/skill.md'], '/admin/api/files/skill': ['./docs/skill.md'], '/admin/api/files/style-guide': ['./docs/style-guide.md'], + '/admin/guide': ['./docs/style-guide.md'], '/': ['./node_modules/drizzle-orm/**'], '/api/v1/media': sharpNative, '/admin/media': sharpNative, diff --git a/src/app/admin/guide/page.tsx b/src/app/admin/guide/page.tsx new file mode 100644 index 0000000..95be9ec --- /dev/null +++ b/src/app/admin/guide/page.tsx @@ -0,0 +1,45 @@ +import type { Metadata } from 'next' +import Markdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import { getTranslations } from 'next-intl/server' +import { AdminHeading } from '~/components/admin/AdminHeading' +import { AdminNotice } from '~/components/admin/AdminNotice' +import { readStyleGuide } from '~/lib/api-docs' +import { requireAdminArea } from '~/lib/auth-guards' + +export async function generateMetadata(): Promise { + const app = await getTranslations('app') + const t = await getTranslations('admin.guide') + + return { title: `${t('title')} - ${app('name')}` } +} + +export default async function AdminGuidePage() { + await requireAdminArea() + + const t = await getTranslations('admin.guide') + const text = await readStyleGuide() + + return ( +
+ + + {text === null ? ( + {t('missing')} + ) : ( +
+ null, + h2: props =>

, + a: props => , + }} + > + {text} + +

+ )} +
+ ) +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 59539bf..58c5fe7 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -19,6 +19,7 @@ import { adminNewEntryPath, adminEntriesPath, adminEntryPath, + adminGuidePath, adminNewProjectPath, adminProjectEntriesPath, adminProjectPath, @@ -92,6 +93,12 @@ export default async function AdminPage() { } /> + {t('welcomeAction')} + }> + {t('welcome')} + + {tiles.length > 0 ? (
    {tiles.map(tile => ( diff --git a/src/components/layout/AdminNav.tsx b/src/components/layout/AdminNav.tsx index ebcdcea..21e7892 100644 --- a/src/components/layout/AdminNav.tsx +++ b/src/components/layout/AdminNav.tsx @@ -1,6 +1,6 @@ import Link from 'next/link' import { getTranslations } from 'next-intl/server' -import { TbAddressBook, TbArrowNarrowLeft, TbCode, TbKey, TbLayoutGrid, TbNotebook, TbPhoto, TbSettings, TbStack2, TbTags, TbUsers } from 'react-icons/tb' +import { TbAddressBook, TbArrowNarrowLeft, TbBook, TbCode, TbKey, TbLayoutGrid, TbNotebook, TbPhoto, TbSettings, TbStack2, TbTags, TbUsers } from 'react-icons/tb' import { NavLink } from './NavLink' import { Wordmark } from './Wordmark' import { Scroller } from '~/components/ui/Scroller' @@ -11,6 +11,7 @@ import { canOpenAdmin, isAdmin, type Viewer } from '~/lib/auth-access' import { adminMediaPath, adminPath } from '~/lib/auth-routes' import { adminBrandsPath, + adminGuidePath, adminClientsPath, adminEntriesPath, adminPostTypesPath, @@ -44,6 +45,7 @@ export async function AdminNav({ viewer, number }: AdminNavProps) { own.push({ href: adminPath, label: t('nav.overview'), icon: , exact: true }) own.push({ href: adminEntriesPath, label: t('nav.posts'), icon: }) own.push({ href: adminMediaPath, label: t('nav.media'), icon: }) + own.push({ href: adminGuidePath, label: t('nav.guide'), icon: }) } if (isAdmin(viewer)) { diff --git a/src/lib/admin-routes.ts b/src/lib/admin-routes.ts index 9c1d7a6..c51daf8 100644 --- a/src/lib/admin-routes.ts +++ b/src/lib/admin-routes.ts @@ -11,6 +11,7 @@ export const adminUsersPath = '/admin/users' as Route export const adminClientsPath = '/admin/clients' as Route export const adminPostTypesPath = '/admin/post-types' as Route export const adminSettingsPath = '/admin/settings' as Route +export const adminGuidePath = '/admin/guide' as Route export const adminNewPostTypePath = '/admin/post-types/new' as Route export function adminApiFilePath(file: 'openapi' | 'style-guide' | 'skill'): Route {