Files
storkit/frontend/src/components/ReviewPanel.tsx
Dave 8f684a6ca4 Story 27: Coverage tracking (full-stack)
Add end-to-end coverage tracking: backend collects vitest coverage,
records metrics with threshold/baseline tracking, and blocks acceptance
on regression. Frontend displays coverage in gate/review panels with
a "Collect Coverage" button. Includes 20 Rust tests, 17 Vitest tests,
and 14 Playwright E2E tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 14:45:57 +00:00

341 lines
8.2 KiB
TypeScript

import type { ReviewStory } from "../api/workflow";
interface ReviewPanelProps {
reviewQueue: ReviewStory[];
isReviewLoading: boolean;
reviewError: string | null;
proceedingStoryId: string | null;
storyId: string;
isGateLoading: boolean;
proceedError: string | null;
proceedSuccess: string | null;
lastReviewRefresh: Date | null;
onRefresh: () => void;
onProceed: (storyId: string) => Promise<void>;
}
const formatTimestamp = (value: Date | null): string => {
if (!value) return "—";
return value.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
};
export function ReviewPanel({
reviewQueue,
isReviewLoading,
reviewError,
proceedingStoryId,
storyId,
isGateLoading,
proceedError,
proceedSuccess,
lastReviewRefresh,
onRefresh,
onProceed,
}: ReviewPanelProps) {
return (
<div
style={{
border: "1px solid #333",
borderRadius: "10px",
padding: "12px 16px",
background: "#1f1f1f",
display: "flex",
flexDirection: "column",
gap: "8px",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "12px",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
}}
>
<div style={{ fontWeight: 600 }}>Stories Awaiting Review</div>
<button
type="button"
onClick={onRefresh}
disabled={isReviewLoading}
style={{
padding: "4px 10px",
borderRadius: "999px",
border: "1px solid #333",
background: isReviewLoading ? "#2a2a2a" : "#2f2f2f",
color: isReviewLoading ? "#777" : "#aaa",
cursor: isReviewLoading ? "not-allowed" : "pointer",
fontSize: "0.75em",
fontWeight: 600,
}}
>
Refresh
</button>
</div>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-end",
gap: "2px",
fontSize: "0.85em",
color: "#aaa",
}}
>
<div>
{reviewQueue.filter((story) => story.can_accept).length} ready /{" "}
{reviewQueue.length} total
</div>
<div style={{ fontSize: "0.8em", color: "#777" }}>
Updated {formatTimestamp(lastReviewRefresh)}
</div>
</div>
</div>
{isReviewLoading ? (
<div style={{ fontSize: "0.85em", color: "#aaa" }}>
Loading review queue...
</div>
) : reviewError ? (
<div
style={{
fontSize: "0.85em",
color: "#ff7b72",
display: "flex",
alignItems: "center",
gap: "8px",
flexWrap: "wrap",
}}
>
<span>{reviewError} Use Refresh to try again.</span>
<button
type="button"
onClick={onRefresh}
disabled={isReviewLoading}
style={{
padding: "4px 10px",
borderRadius: "999px",
border: "1px solid #333",
background: isReviewLoading ? "#2a2a2a" : "#2f2f2f",
color: isReviewLoading ? "#777" : "#aaa",
cursor: isReviewLoading ? "not-allowed" : "pointer",
fontSize: "0.75em",
fontWeight: 600,
}}
>
Retry
</button>
</div>
) : reviewQueue.length === 0 ? (
<div style={{ fontSize: "0.85em", color: "#aaa" }}>
No stories waiting for review.
</div>
) : (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "8px",
}}
>
{reviewQueue.map((story) => (
<div
key={`review-${story.story_id}`}
style={{
border: "1px solid #2a2a2a",
borderRadius: "8px",
padding: "10px 12px",
background: "#191919",
display: "flex",
flexDirection: "column",
gap: "6px",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "12px",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
flexWrap: "wrap",
}}
>
<div style={{ fontWeight: 600 }}>{story.story_id}</div>
<span
style={{
padding: "2px 8px",
borderRadius: "999px",
fontSize: "0.7em",
fontWeight: 600,
background: story.can_accept ? "#7ee787" : "#ff7b72",
color: story.can_accept ? "#000" : "#1a1a1a",
}}
>
{story.can_accept ? "Ready" : "Blocked"}
</span>
{story.summary.failed > 0 && (
<span
style={{
padding: "2px 8px",
borderRadius: "999px",
fontSize: "0.7em",
fontWeight: 600,
background: "#ffb86c",
color: "#1a1a1a",
}}
>
Failing {story.summary.failed}
</span>
)}
{story.warning && (
<span
style={{
padding: "2px 8px",
borderRadius: "999px",
fontSize: "0.7em",
fontWeight: 600,
background: "#ffb86c",
color: "#1a1a1a",
}}
>
Warning
</span>
)}
{story.missing_categories.length > 0 && (
<span
style={{
padding: "2px 8px",
borderRadius: "999px",
fontSize: "0.7em",
fontWeight: 600,
background: "#3a2a1a",
color: "#ffb86c",
border: "1px solid #5a3a1a",
}}
>
Missing
</span>
)}
</div>
<button
type="button"
disabled={
proceedingStoryId === story.story_id ||
isReviewLoading ||
(story.story_id === storyId && isGateLoading) ||
!story.can_accept
}
onClick={() => onProceed(story.story_id)}
style={{
padding: "6px 12px",
borderRadius: "8px",
border: "none",
background:
proceedingStoryId === story.story_id
? "#444"
: story.can_accept
? "#7ee787"
: "#333",
color:
proceedingStoryId === story.story_id
? "#bbb"
: story.can_accept
? "#000"
: "#aaa",
cursor:
proceedingStoryId === story.story_id || !story.can_accept
? "not-allowed"
: "pointer",
fontSize: "0.85em",
fontWeight: 600,
}}
>
{proceedingStoryId === story.story_id
? "Proceeding..."
: story.can_accept
? "Proceed"
: "Blocked"}
</button>
</div>
<div style={{ fontSize: "0.85em", color: "#aaa" }}>
Summary: {story.summary.passed}/{story.summary.total} passing,{" "}
{` ${story.summary.failed}`} failing
</div>
{story.coverage_report && (
<div
style={{
fontSize: "0.85em",
color:
story.coverage_report.current_percent <
story.coverage_report.threshold_percent
? "#ff7b72"
: "#7ee787",
}}
>
Coverage: {story.coverage_report.current_percent.toFixed(1)}%
(threshold:{" "}
{story.coverage_report.threshold_percent.toFixed(1)}%)
</div>
)}
{story.missing_categories.length > 0 && (
<div style={{ fontSize: "0.85em", color: "#ffb86c" }}>
Missing: {story.missing_categories.join(", ")}
</div>
)}
{story.reasons.length > 0 && (
<ul
style={{
margin: "0 0 0 16px",
padding: 0,
fontSize: "0.85em",
color: "#ccc",
}}
>
{story.reasons.map((reason) => (
<li key={`review-reason-${story.story_id}-${reason}`}>
{reason}
</li>
))}
</ul>
)}
{story.warning && (
<div style={{ fontSize: "0.85em", color: "#ffb86c" }}>
{story.warning}
</div>
)}
</div>
))}
</div>
)}
{proceedError && (
<div style={{ fontSize: "0.85em", color: "#ff7b72" }}>
{proceedError}
</div>
)}
{proceedSuccess && (
<div style={{ fontSize: "0.85em", color: "#7ee787" }}>
{proceedSuccess}
</div>
)}
</div>
);
}