From 45af5c0e57955395054cfbdb90596bf2ec59e4d3 Mon Sep 17 00:00:00 2001 From: Pablo Date: Mon, 21 Sep 2026 12:19:53 -0500 Subject: [PATCH 1/4] feat(dashboard): dense app shell with header analyze and KPI strip Full-viewport grid shell (46px header, auto KPI row, 1fr main) with explicit grid-row placement. Header integrates compact AnalyzeForm, repo identity, drift pill and icon-only share/export. KpiStrip merges summary cards, metrics bar and elapsed time into one strip. --- frontend/src/App.tsx | 375 ++++++++++++++--------- frontend/src/components/AnalyzeForm.tsx | 27 +- frontend/src/components/ExportButton.tsx | 17 +- frontend/src/components/KpiStrip.tsx | 68 ++++ frontend/src/components/ShareButton.tsx | 13 +- frontend/src/index.css | 51 +++ 6 files changed, 386 insertions(+), 165 deletions(-) create mode 100644 frontend/src/components/KpiStrip.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c1a16b4..eb84edf 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,11 +1,10 @@ import { useState, useEffect, useRef, useCallback } from "react" -import { Sun, Moon, X, RotateCcw } from "lucide-react" +import { Sun, Moon, X, RotateCcw, History } from "lucide-react" import { motion } from "framer-motion" import { analyze } from "./api" import type { Violation, AnalysisResult } from "./api" import AnalyzeForm from "./components/AnalyzeForm" -import SummaryCards from "./components/SummaryCards" -import MetricsBar from "./components/MetricsBar" +import KpiStrip from "./components/KpiStrip" import ViolationsTab from "./components/ViolationsTab" import RemediationTab from "./components/RemediationTab" import ShareButton from "./components/ShareButton" @@ -16,13 +15,15 @@ import SkeletonTable from "./components/SkeletonTable" import FeatureCards from "./components/FeatureCards" import HowItWorks from "./components/HowItWorks" import ExampleRepos from "./components/ExampleRepos" +import HistoryRail from "./components/HistoryRail" +import FixInspector from "./components/FixInspector" +import { SampleOutput, MicroStats } from "./components/SampleOutput" import { usePrefersReducedMotion } from "./hooks/usePrefersReducedMotion" import { usePerformance } from "./hooks/usePerformance" import { useAnalytics } from "./hooks/useAnalytics" import { useToast } from "./components/Toast" import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts" import { useHistory } from "./hooks/useHistory" -import HistoryPanel from "./components/HistoryPanel" function useTheme() { const [dark, setDark] = useState(() => { @@ -57,6 +58,8 @@ export default function App() { const [severityFilter, setSeverityFilter] = useState("") const [kindFilter, setKindFilter] = useState("") const [search, setSearch] = useState("") + const [selectedIdx, setSelectedIdx] = useState(null) + const [railOpen, setRailOpen] = useState(false) const resultsRef = useRef(null) const tabRefs = useRef>({ @@ -68,6 +71,7 @@ export default function App() { setLoading(true) setError("") setResult(null) + setSelectedIdx(null) setLastAnalyzed({ url, branch: br }) startTimer() track("analysis_start", { repo: url, branch: br }) @@ -178,13 +182,19 @@ export default function App() { info: violations.filter((v) => v.severity === "info").length, } + const ruleGroups = new Set(violations.map((v) => v.rule)).size + const selectedViolation = + selectedIdx !== null && selectedIdx >= 0 && selectedIdx < violations.length + ? violations[selectedIdx] + : null + const MotionOrDiv = reducedMotion ? "div" : motion.div const animProps = reducedMotion ? {} : { initial: { opacity: 0 }, animate: { opacity: 1 } } return ( -
+
-
-
-

Sentinel

+
+
+ + S + +

Sentinel

+ +
+
+ {lastAnalyzed ? ( + <> + {lastAnalyzed.url} + + {lastAnalyzed.branch} + + {result && ( + + drift {result.drift_score.toFixed(2)} + + )} + + ) : ( + no repo analyzed yet + )} +
+ +
+ + {result && ( + <> + + + + )}
+
- - - {showHero && ( - <> - handleAnalyze(repo, "main")} loading={loading} /> - - - - )} + {result && !loading && ( + + )} - {!showHero && ( - handleAnalyze(url, branch)} - onClear={clearHistory} - loading={loading} - /> - )} - - {error && ( - -
- {error} -
- {lastAnalyzed && ( - - )} - +
+ {showHero ? ( +
+
+
+
+ Architecture erosion detector +
+

+ Ship features without rotting the architecture. +

+

+ Paste a GitHub repo. Sentinel maps the dependency graph, flags layering + violations and cycles, and scores drift — with a concrete fix for every finding. +

+
+
+ +
+ +
- - )} - - {loading && ( -
- - - -
- )} - - {result && !loading && ( -
-
- Analysis complete. {counts.total} violations found: {counts.errors} errors, {counts.warnings} warnings, {counts.info} info. +
+ +
+ + handleAnalyze(repo, "main")} loading={loading} /> +
+
- - + ) : ( + <> + handleAnalyze(url, branch)} + onClear={clearHistory} + loading={loading} + open={railOpen} /> - - {perfMetrics.analysisTimeMs !== null && ( -

- Analyzed in {(perfMetrics.analysisTimeMs / 1000).toFixed(1)}s -

- )} - -
- {metrics && } -
- - +
+
+ {result && !loading && ( + <>Analysis complete. {counts.total} violations found: {counts.errors} errors, {counts.warnings} warnings, {counts.info} info. + )}
-
-
- {TABS.map((tab) => ( - - ))} -
+
+ {error} +
+ {lastAnalyzed && ( + + )} + +
+
+ + )} - - {activeTab === "violations" ? ( - - ) : ( - + {loading && ( +
+ + + +
)} -
-
+ + {result && !loading && ( + <> +
+ {TABS.map((tab) => { + const n = tab === "violations" ? violations.length : ruleGroups + return ( + + ) + })} +
+ + + {activeTab === "violations" ? ( +
+ + setSelectedIdx((prev) => (idx === prev ? null : idx)) + } + /> +
+ ) : ( +
+ +
+ )} +
+ + )} + + + )} -
+
) } diff --git a/frontend/src/components/AnalyzeForm.tsx b/frontend/src/components/AnalyzeForm.tsx index 01a9de1..1c67bbb 100644 --- a/frontend/src/components/AnalyzeForm.tsx +++ b/frontend/src/components/AnalyzeForm.tsx @@ -5,6 +5,7 @@ import { motion } from "framer-motion" interface AnalyzeFormProps { onAnalyze: (url: string, branch: string) => void loading: boolean + compact?: boolean } function parseGithubInput(raw: string): { normalized: string; valid: boolean } { @@ -26,7 +27,7 @@ function parseGithubInput(raw: string): { normalized: string; valid: boolean } { return { normalized: trimmed, valid: false } } -export default function AnalyzeForm({ onAnalyze, loading }: AnalyzeFormProps) { +export default function AnalyzeForm({ onAnalyze, loading, compact = false }: AnalyzeFormProps) { const [url, setUrl] = useState("") const [branch, setBranch] = useState("main") @@ -46,49 +47,49 @@ export default function AnalyzeForm({ onAnalyze, loading }: AnalyzeFormProps) { -
-