Spezifikation v0.2 als lebendes Dokument + Ansicht in der App
Deploy App / deploy (push) Successful in 3m10s

Neue Funktionale/Technische Spezifikation (SPEZIFIKATION.md) mit
Aenderungshistorie; ersetzt die bisherigen FDD/TDD-Dokumente v1-v5.

In der App unter dem Menueintrag "SPEZIFIKATION" (Sidebar, unter den Plaenen)
lesbar: /api/spec liefert das Markdown, SpecView rendert es. Styling ueber die
bestehenden Farb-Tokens, folgt damit allen drei Schemata.

Die MD-Datei war ueber .dockerignore (*.md) vom Image ausgeschlossen -> Ausnahme
ergaenzt und Kopie in die Runner-Stage aufgenommen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 22:22:44 +02:00
parent dfebbeb397
commit 87e6a5f564
9 changed files with 3422 additions and 10 deletions
+39 -7
View File
@@ -2,6 +2,7 @@
import { useCallback, useEffect, useState } from "react";
import {
FileText,
FolderKanban,
LayoutDashboard,
Menu,
@@ -12,6 +13,7 @@ import {
} from "lucide-react";
import { PlanView } from "@/components/PlanView";
import { Dashboard } from "@/components/Dashboard";
import { SpecView } from "@/components/SpecView";
import { ProfileMenu } from "@/components/ProfileMenu";
import { PlanProfileFields, emptyProfileDraft, type ProfileDraft } from "@/components/PlanProfileFields";
import { api } from "@/lib/api-client";
@@ -34,6 +36,8 @@ export function AppShell({ username }: { username: string }) {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [showNewPlan, setShowNewPlan] = useState(false);
const [showScenario, setShowScenario] = useState(false);
// Die Spezifikation ist eine eigene Ansicht neben Uebersicht und Plan (schliessen sich aus).
const [showSpec, setShowSpec] = useState(false);
const loadPlans = useCallback(async (preferId?: string) => {
const data = await api.get<{ plans: PlanListItem[] }>("/api/plans");
@@ -95,10 +99,13 @@ export function AppShell({ username }: { username: string }) {
type="button"
onClick={() => {
setSelectedPlanId(null);
setShowSpec(false);
setSidebarOpen(false);
}}
className={`flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium ${
selectedPlanId === null ? "bg-accent-soft text-accent-soft-fg" : "text-muted hover:bg-surface-2"
selectedPlanId === null && !showSpec
? "bg-accent-soft text-accent-soft-fg"
: "text-muted hover:bg-surface-2"
}`}
>
<LayoutDashboard className="h-4 w-4" />
@@ -123,10 +130,13 @@ export function AppShell({ username }: { username: string }) {
type="button"
onClick={() => {
setSelectedPlanId(p.id);
setShowSpec(false);
setSidebarOpen(false);
}}
className={`flex items-center gap-2 rounded-lg px-3 py-2 text-left text-sm ${
selectedPlanId === p.id ? "bg-accent-soft font-medium text-accent-soft-fg" : "text-muted hover:bg-surface-2"
selectedPlanId === p.id && !showSpec
? "bg-accent-soft font-medium text-accent-soft-fg"
: "text-muted hover:bg-surface-2"
}`}
>
<FolderKanban className="h-4 w-4 shrink-0" />
@@ -134,6 +144,23 @@ export function AppShell({ username }: { username: string }) {
<span className="text-[11px] text-faint">{p.phases.length}</span>
</button>
))}
<div className="mt-4 border-t border-border pt-3">
<button
type="button"
onClick={() => {
setShowSpec(true);
setSelectedPlanId(null);
setSidebarOpen(false);
}}
className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm font-medium ${
showSpec ? "bg-accent-soft text-accent-soft-fg" : "text-muted hover:bg-surface-2"
}`}
>
<FileText className="h-4 w-4 shrink-0" />
SPEZIFIKATION
</button>
</div>
</nav>
</div>
);
@@ -173,25 +200,30 @@ export function AppShell({ username }: { username: string }) {
<Menu className="h-4 w-4" />
</button>
<h1 className="min-w-0 flex-1 truncate text-base font-semibold text-fg">
{activePlan ? activePlan.name : "Uebersicht"}
{showSpec ? "Spezifikation" : activePlan ? activePlan.name : "Uebersicht"}
</h1>
<ProfileMenu username={username} />
</header>
<main className="flex-1 px-4 py-6 lg:px-8">
{loading && <p className="text-sm text-muted">Laedt</p>}
{showSpec && <SpecView />}
{!loading && selectedPlanId === null && (
{!showSpec && loading && <p className="text-sm text-muted">Laedt</p>}
{!showSpec && !loading && selectedPlanId === null && (
<DashboardHome
username={username}
plans={plans}
onSelect={setSelectedPlanId}
onSelect={(id) => {
setSelectedPlanId(id);
setShowSpec(false);
}}
onCreate={() => setShowNewPlan(true)}
onDelete={handleDeletePlan}
/>
)}
{!loading && detail && selectedPlanId && (
{!showSpec && !loading && detail && selectedPlanId && (
<div className="flex flex-col gap-6">
<div className="flex flex-wrap items-center gap-2">
{detail.plan.phases.length > 0 && (
+50
View File
@@ -0,0 +1,50 @@
"use client";
import { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug";
import { FileText } from "lucide-react";
import { api } from "@/lib/api-client";
// Rendert SPEZIFIKATION.md (via /api/spec) als lesbares Dokument. Das Styling laeuft ueber
// die Klasse .md-doc in globals.css und folgt damit dem gewaehlten Farbschema.
export function SpecView() {
const [markdown, setMarkdown] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
api
.get<{ markdown: string }>("/api/spec")
.then((data) => setMarkdown(data.markdown))
.catch((e) => setError(e instanceof Error ? e.message : "Laden fehlgeschlagen."));
}, []);
if (error) return <p className="text-sm text-danger">{error}</p>;
if (markdown === null) return <p className="text-sm text-muted">Laedt</p>;
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<FileText className="h-5 w-5 text-accent" />
<h2 className="text-lg font-semibold text-fg">Spezifikation</h2>
</div>
<article className="md-doc rounded-xl border border-border bg-surface p-6 shadow-sm lg:p-8">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeSlug]}
components={{
// Breite Tabellen scrollen in ihrem eigenen Container statt die Seite zu dehnen.
table: ({ children, ...props }) => (
<div className="md-table-wrap">
<table {...props}>{children}</table>
</div>
),
}}
>
{markdown}
</ReactMarkdown>
</article>
</div>
);
}