From ef07bad9452d418a746370cc9e79353ed019b451 Mon Sep 17 00:00:00 2001 From: Ahmed Allam <49919286+0xallam@users.noreply.github.com> Date: Wed, 22 Jul 2026 08:34:52 -0700 Subject: [PATCH] Local viewer: UI polish and a Feedback & support tab (#847) --- strix/viewer/auth.py | 21 + strix/viewer/frontend/package-lock.json | 82 +-- strix/viewer/frontend/package.json | 1 + strix/viewer/frontend/src/App.tsx | 115 ++-- .../src/components/EmailReportView.tsx | 12 +- .../frontend/src/components/FeatureDetail.tsx | 96 --- .../frontend/src/components/FeedbackView.tsx | 134 ++++ .../frontend/src/components/PastRunsView.tsx | 2 +- .../viewer/frontend/src/components/ProCta.tsx | 118 +--- .../frontend/src/components/RunDetails.tsx | 8 +- .../frontend/src/components/Sidebar.tsx | 570 +++++++++++------- .../frontend/src/components/UpgradeModal.tsx | 150 +++++ .../src/components/live/AgentDetailModal.tsx | 84 ++- .../live/tool-renderers/LoadSkillRenderer.tsx | 9 +- .../vulnerability/VulnerabilityDetail.tsx | 68 +-- .../viewer/frontend/src/data/serverSource.ts | 22 +- strix/viewer/frontend/src/index.css | 159 ++++- strix/viewer/frontend/src/lib/pro-features.ts | 174 ------ strix/viewer/server.py | 29 + strix/viewer/static/assets/index-BU_tk5L-.js | 517 ---------------- strix/viewer/static/assets/index-BtBksasm.css | 10 + strix/viewer/static/assets/index-C0NveaV7.css | 10 - strix/viewer/static/assets/index-_2j_QfOq.js | 487 +++++++++++++++ strix/viewer/static/index.html | 4 +- 24 files changed, 1546 insertions(+), 1336 deletions(-) delete mode 100644 strix/viewer/frontend/src/components/FeatureDetail.tsx create mode 100644 strix/viewer/frontend/src/components/FeedbackView.tsx create mode 100644 strix/viewer/frontend/src/components/UpgradeModal.tsx delete mode 100644 strix/viewer/frontend/src/lib/pro-features.ts delete mode 100644 strix/viewer/static/assets/index-BU_tk5L-.js create mode 100644 strix/viewer/static/assets/index-BtBksasm.css delete mode 100644 strix/viewer/static/assets/index-C0NveaV7.css create mode 100644 strix/viewer/static/assets/index-_2j_QfOq.js diff --git a/strix/viewer/auth.py b/strix/viewer/auth.py index 1c7c5fef..c25d12b6 100644 --- a/strix/viewer/auth.py +++ b/strix/viewer/auth.py @@ -207,6 +207,26 @@ def otp_verify(email: str, code: str) -> dict[str, Any]: raise RelayError("unavailable") +def feedback_submit(email: str, message: str) -> None: + """Relay a feedback message + email to Strix. No verification is required; + the email is taken as given. Raises RelayError on failure.""" + status, data = _post_json( + "/api/oss/feedback", + {"email": email, "message": message}, + timeout=_OTP_TIMEOUT, + ) + if status == 200: + return + if status == 429: + raise RelayError("rate_limited") + if status == 400: + code = data.get("error") + if code in ("invalid_email", "invalid_message"): + raise RelayError(str(code)) + raise RelayError("invalid_message") + raise RelayError("unavailable") + + def report_send( token: str, pdf_bytes: bytes, @@ -241,6 +261,7 @@ def report_send( __all__ = [ "AUTH_PATH", "RelayError", + "feedback_submit", "forget", "is_verified", "otp_start", diff --git a/strix/viewer/frontend/package-lock.json b/strix/viewer/frontend/package-lock.json index 1c74cd0c..9f86e348 100644 --- a/strix/viewer/frontend/package-lock.json +++ b/strix/viewer/frontend/package-lock.json @@ -16,6 +16,7 @@ "lucide-react": "^0.563.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-icons": "^5.7.0", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", "tailwind-merge": "^3.4.0" @@ -62,6 +63,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -919,9 +921,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -936,9 +935,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -953,9 +949,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -970,9 +963,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -987,9 +977,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1004,9 +991,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1021,9 +1005,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1038,9 +1019,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1055,9 +1033,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1072,9 +1047,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1089,9 +1061,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1106,9 +1075,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1123,9 +1089,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1349,9 +1312,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1369,9 +1329,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1389,9 +1346,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1409,9 +1363,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1654,6 +1605,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1664,6 +1616,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -1786,6 +1739,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", @@ -1966,6 +1920,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -2543,9 +2498,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2567,9 +2519,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2591,9 +2540,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2615,9 +2561,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3628,6 +3571,7 @@ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3679,6 +3623,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3688,6 +3633,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3695,6 +3641,15 @@ "react": "^19.2.7" } }, + "node_modules/react-icons": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.7.0.tgz", + "integrity": "sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-markdown": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", @@ -4154,6 +4109,7 @@ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/strix/viewer/frontend/package.json b/strix/viewer/frontend/package.json index 37eaff55..e79db76f 100644 --- a/strix/viewer/frontend/package.json +++ b/strix/viewer/frontend/package.json @@ -17,6 +17,7 @@ "lucide-react": "^0.563.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-icons": "^5.7.0", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", "tailwind-merge": "^3.4.0" diff --git a/strix/viewer/frontend/src/App.tsx b/strix/viewer/frontend/src/App.tsx index 8afc025c..00c0b9c8 100644 --- a/strix/viewer/frontend/src/App.tsx +++ b/strix/viewer/frontend/src/App.tsx @@ -2,14 +2,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ArrowLeft, AlertCircle, - Waypoints, + Bot, Mail, ChevronDown, - Wrench, - FileCheck2, - CalendarClock, Radar, - GitPullRequest, Rocket, ArrowUpRight, History, @@ -45,11 +41,10 @@ import PastRunsView from "@/components/PastRunsView"; import EmailReportView from "@/components/EmailReportView"; import { RunDetails } from "@/components/RunDetails"; import { TrustToast } from "@/components/TrustToast"; -import FeatureDetail from "@/components/FeatureDetail"; -import { ProTile, ProInlineCta, type ProItem } from "@/components/ProCta"; -import { FEATURES } from "@/lib/pro-features"; +import FeedbackView from "@/components/FeedbackView"; +import { ProInlineCta } from "@/components/ProCta"; -export type View = "overview" | "issues" | "agents" | "history" | "feature" | "email"; +export type View = "overview" | "issues" | "agents" | "history" | "email" | "feedback"; const TRUST_BANNER = "Your findings stay on your machine. They're rendered here locally in your browser and never uploaded or stored by Strix."; @@ -57,25 +52,12 @@ const TRUST_BANNER = const SEVERITY_ORDER: VulnerabilitySeverity[] = ["critical", "high", "medium", "low"]; const POLL_MS = 500; -// Curated inline CTAs. Continuous-coverage row on Overview (the restyled upsell -// tiles), plus the recommendations pairing. -const RECOMMENDATION_CTAS: ProItem[] = [ - { title: "One-click autofix + open a fix PR", desc: "Fix it for you and open a PR, retested.", slug: "autofix", icon: Wrench }, - { title: "Export SOC 2 / ISO 27001 report", desc: "Share an auditor-ready report with your team.", slug: "compliance", icon: FileCheck2 }, -]; -const COVERAGE_CTAS: ProItem[] = [ - { title: "Scheduled pentesting", desc: "Continuous coverage for your whole org.", slug: "scheduled", icon: CalendarClock }, - { title: "Attack surface monitoring", desc: "Continuous coverage for your whole org.", slug: "asm", icon: Radar }, - { title: "PR reviews", desc: "Pentest every pull request your team opens.", slug: "pr_reviews", icon: GitPullRequest }, -]; - export default function App() { const [activeRun, setActiveRun] = useState(null); const [run, setRun] = useState(null); const [error, setError] = useState(null); const [selectedId, setSelectedId] = useState(null); const [view, setView] = useState("overview"); - const [activeFeature, setActiveFeature] = useState(null); const [auth, setAuth] = useState(null); const [runs, setRuns] = useState(null); const [emailPurpose, setEmailPurpose] = useState<"report" | "verify">("report"); @@ -249,12 +231,6 @@ export default function App() { await refreshRuns(); }, [refreshAuth, refreshRuns]); - const selectFeature = useCallback((slug: string) => { - trackCta(slug, "sidebar_nav"); - setActiveFeature(slug); - userSetView("feature"); - }, [userSetView]); - const onForget = useCallback(async () => { await forgetAuth(); await refreshAuth(); @@ -266,11 +242,13 @@ export default function App() { { + // Clicking a sidebar view always lands on that section's top level, + // so leaving a specific issue's detail view and clicking "Issues" + // returns to the full findings list. + setSelectedId(null); if (v === "history") openHistory(); else userSetView(v); }} - activeFeature={activeFeature} - onSelectFeature={selectFeature} issuesCount={run?.vulnerabilities.length ?? 0} agentCount={agentCount} runCount={runs?.count ?? 0} @@ -285,7 +263,7 @@ export default function App() {
{/* Top bar */}
-
+
Strix
- Local results {run && }
{verified && runs && !runs.locked && runs.runs.length > 0 && ( @@ -322,14 +299,20 @@ export default function App() {
-
- {error && !run && view !== "history" && view !== "email" && view !== "feature" && ( +
+ {error && !run && view !== "history" && view !== "email" && (
)} + {/* Keyed wrapper: re-mounts on every view / finding / run change so the + page-in transition replays. */} +
{view === "email" ? ( setView(dest === "history" ? "history" : "overview")} /> - ) : view === "feature" && activeFeature && FEATURES[activeFeature] ? ( - + ) : view === "feedback" ? ( + setView(dest)} + /> ) : view === "history" ? (
@@ -412,6 +398,7 @@ export default function App() { )} ) : null} +
@@ -573,7 +560,7 @@ function FindingsList({
- Email report + Export report to PDF
@@ -673,26 +660,32 @@ function OverviewTab({ return (
- +
+ +
{total > 0 && ( -
+
)} {/* Primary CTA: the one primary on Overview. Hidden until the run is finished, since a live scan would only email a partial report. */} - {finished && } + {finished && ( +
+ +
+ )} {sections.length > 0 ? ( -
+
{sections.map((s) => ( ))}
) : reportMarkdown ? ( -
+
) : ( @@ -701,22 +694,6 @@ function OverviewTab({ ) )} - {/* Near Recommendations: act on the fixes. */} -
- {RECOMMENDATION_CTAS.map((item) => ( - - ))} -
- - {/* Continuous coverage for your org (restyled upsell tiles). */} -
-

Continuous coverage for your org

-
- {COVERAGE_CTAS.map((item) => ( - - ))} -
-
); } @@ -746,8 +723,7 @@ function TabButton({ function AgentsTab({ run, canSteer }: { run: LoadedRun; canSteer: boolean }) { const { agents, events } = run.transcript; const graphAgents = useMemo(() => buildGraphAgents(agents, events), [agents, events]); - // Clicking a graph node opens the agent's transcript in a modal (matching the - // cloud app); no node selected means no modal. + // Clicking a graph node opens the agent's transcript in a modal; no node selected means no modal. const [selectedId, setSelectedId] = useState(null); const selectedAgent = selectedId ? (agents.find((a) => a.id === selectedId) ?? null) : null; @@ -758,7 +734,7 @@ function AgentsTab({ run, canSteer }: { run: LoadedRun; canSteer: boolean }) {
-
- {selectedAgent && ( - setSelectedId(null)} - /> - )} + setSelectedId(null)} + />
); } diff --git a/strix/viewer/frontend/src/components/EmailReportView.tsx b/strix/viewer/frontend/src/components/EmailReportView.tsx index 7f84cddf..7c95365f 100644 --- a/strix/viewer/frontend/src/components/EmailReportView.tsx +++ b/strix/viewer/frontend/src/components/EmailReportView.tsx @@ -180,7 +180,7 @@ export default function EmailReportView({ const confirmationEmail = sentTo || auth?.email || email.trim(); return ( -
+
@@ -269,7 +266,6 @@ export default function EmailReportView({ className="w-full rounded-lg bg-black px-3 py-2.5 text-sm text-white outline-none transition-colors focus:border-[#444]" style={{ border: "1px solid #2a2a2a" }} /> - Use your work email.
- ); -} diff --git a/strix/viewer/frontend/src/components/FeedbackView.tsx b/strix/viewer/frontend/src/components/FeedbackView.tsx new file mode 100644 index 00000000..f9d6985f --- /dev/null +++ b/strix/viewer/frontend/src/components/FeedbackView.tsx @@ -0,0 +1,134 @@ +import { useState } from "react"; +import { ArrowLeft, AlertCircle, CheckCircle2 } from "lucide-react"; +import { IoChatbubblesOutline } from "react-icons/io5"; +import { submitFeedback } from "@/data/serverSource"; +import type { View } from "@/App"; + +const MAX_MESSAGE = 5000; + +const ERROR_COPY: Record = { + invalid_email: "That email doesn't look right.", + invalid_message: "Please write a little more.", + unavailable: "Couldn't send that just now. Try again.", +}; + +/** + * Feedback & support form. Collects a message plus a work email (no + * verification — the email is taken as-is) and relays it to Strix via the local + * server. Mirrors EmailReportView's centered-card styling and palette. + */ +export default function FeedbackView({ + defaultEmail, + onExit, +}: { + defaultEmail: string | null; + onExit: (dest: View) => void; +}) { + const [message, setMessage] = useState(""); + const [email, setEmail] = useState(defaultEmail ?? ""); + const [step, setStep] = useState<"form" | "sending" | "sent">("form"); + const [error, setError] = useState(null); + + const canSend = message.trim().length > 0 && email.trim().length > 0 && step !== "sending"; + + const send = async () => { + if (!canSend) return; + setStep("sending"); + setError(null); + const result = await submitFeedback(message.trim(), email.trim()); + if (result.ok) { + setStep("sent"); + return; + } + setStep("form"); + setError(ERROR_COPY[result.error] ?? ERROR_COPY.unavailable); + }; + + return ( +
+ + +
+
+ +
+ {step === "sent" ? ( +
+
+ ) : ( + <> +

+ Bugs, feature requests, or anything else. Tell us what's on your mind. +

+ + {error && ( +
+
+ )} + +