Rework the viewer nav, add in-app feature pages, and verify-only past runs

- Collapse the sidebar into one ordered list of uniform two-line rows (icon +
  label + short one-liner), no tier sections.
- Sidebar Pro/Enterprise rows now open an in-app FeatureDetail upsell page
  instead of linking out; the page's primary CTA is the sign-up link.
- Past runs "View runs" runs a verify-only flow that never sends a report.
- Ask for a work email: helper text, an instant common-domain check, and a
  friendly message when the relay rejects a personal domain.
This commit is contained in:
Jonathan Singer
2026-07-20 13:31:13 -04:00
parent 2cd953b54e
commit d73f319be5
7 changed files with 436 additions and 102 deletions
+25 -4
View File
@@ -42,9 +42,11 @@ import { SIGNUP_URL, trackCta } from "@/lib/cta";
import Sidebar from "@/components/Sidebar";
import PastRunsView from "@/components/PastRunsView";
import EmailReportDialog from "@/components/EmailReportDialog";
import FeatureDetail from "@/components/FeatureDetail";
import { ProTile, ProInlineCta, type ProItem } from "@/components/ProCta";
import { FEATURES } from "@/lib/pro-features";
export type View = "overview" | "issues" | "agents" | "history";
export type View = "overview" | "issues" | "agents" | "history" | "feature";
const TRUST_BANNER =
"Your findings stay on your machine. They're rendered here locally in your browser and never uploaded or stored by Strix. Emailing a report is an explicit opt-in that sends an encrypted copy only you can open.";
@@ -70,9 +72,11 @@ export default function App() {
const [error, setError] = useState<string | null>(null);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [view, setView] = useState<View>("overview");
const [activeFeature, setActiveFeature] = useState<string | null>(null);
const [auth, setAuth] = useState<AuthStatus | null>(null);
const [runs, setRuns] = useState<RunsPayload | null>(null);
const [emailOpen, setEmailOpen] = useState(false);
const [emailPurpose, setEmailPurpose] = useState<"report" | "verify">("report");
const refreshAuth = useCallback(async () => {
try {
@@ -181,15 +185,27 @@ export default function App() {
const openEmail = useCallback(() => {
trackCta("email_report");
setEmailPurpose("report");
setEmailOpen(true);
}, []);
const openVerify = useCallback(() => {
trackCta("history_unlock");
setEmailPurpose("verify");
setEmailOpen(true);
}, []);
const openHistory = useCallback(() => {
trackCta("history_unlock");
void refreshRuns();
setView("history");
}, [refreshRuns]);
const selectFeature = useCallback((slug: string) => {
trackCta(`nav_${slug}`);
setActiveFeature(slug);
setView("feature");
}, []);
const onForget = useCallback(async () => {
await forgetAuth();
await refreshAuth();
@@ -204,6 +220,8 @@ export default function App() {
if (v === "history") openHistory();
else setView(v);
}}
activeFeature={activeFeature}
onSelectFeature={selectFeature}
issuesCount={run?.vulnerabilities.length ?? 0}
agentCount={agentCount}
runCount={runs?.count ?? 0}
@@ -268,7 +286,9 @@ export default function App() {
</div>
)}
{view === "history" ? (
{view === "feature" && activeFeature && FEATURES[activeFeature] ? (
<FeatureDetail feature={FEATURES[activeFeature]} />
) : view === "history" ? (
<div className="space-y-4">
<div className="flex items-center gap-2">
<History className="w-5 h-5 text-[#888]" aria-hidden="true" />
@@ -278,7 +298,7 @@ export default function App() {
runs={runs}
activeRun={activeRun}
onSelectRun={selectRun}
onVerifyClick={openEmail}
onVerifyClick={openVerify}
/>
</div>
) : !run && !error ? (
@@ -342,6 +362,7 @@ export default function App() {
onClose={() => setEmailOpen(false)}
activeRun={activeRun}
auth={auth}
purpose={emailPurpose}
onVerified={() => {
void refreshAuth();
void refreshRuns();
@@ -9,11 +9,19 @@ import {
type Step = "disclosure" | "email" | "code" | "sending" | "password";
/**
* "report" runs the full disclosure -> verify -> encrypted send flow.
* "verify" is a verify-only flow (view past runs): it starts at the email step
* and finishes as soon as the code is confirmed, without sending a report.
*/
type Purpose = "report" | "verify";
interface EmailReportDialogProps {
open: boolean;
onClose: () => void;
activeRun: string | null;
auth: AuthStatus | null;
purpose: Purpose;
/** Re-fetch auth status after a successful verify (lifts state to App). */
onVerified: () => void;
}
@@ -21,9 +29,34 @@ interface EmailReportDialogProps {
const OTP_START_ERRORS: Record<string, string> = {
rate_limited: "Too many requests. Wait a minute and try again.",
invalid_email: "That email does not look right. Check it and try again.",
work_email_required: "Please use your work email, not a personal one.",
unavailable: "The email service is unavailable right now. Try again shortly.",
};
// Small common set for instant UX only; the relay is authoritative on the full
// free/personal domain list.
const COMMON_FREE_DOMAINS = new Set([
"gmail.com",
"googlemail.com",
"yahoo.com",
"ymail.com",
"outlook.com",
"hotmail.com",
"live.com",
"icloud.com",
"me.com",
"aol.com",
"proton.me",
"protonmail.com",
"gmx.com",
"mail.com",
]);
function isCommonFreeEmail(email: string): boolean {
const domain = email.split("@")[1]?.trim().toLowerCase();
return domain != null && COMMON_FREE_DOMAINS.has(domain);
}
const SEND_ERRORS: Record<string, string> = {
forbidden: "This email was unsubscribed from Strix, so we cannot send to it.",
too_large: "This report is too large to email. Try a smaller run.",
@@ -35,6 +68,7 @@ export default function EmailReportDialog({
onClose,
activeRun,
auth,
purpose,
onVerified,
}: EmailReportDialogProps) {
const verified = auth?.verified === true;
@@ -49,11 +83,12 @@ export default function EmailReportDialog({
const [copied, setCopied] = useState(false);
const sentTo = useRef<string>("");
// Reset to the disclosure step each time the dialog opens; prefill the email
// when already verified so the flow can skip straight to sending.
// Reset each time the dialog opens. The report flow opens on the disclosure
// step; the verify-only flow skips it and starts straight at the email step.
// Prefill the email when already verified so the flow can skip ahead.
useEffect(() => {
if (!open) return;
setStep("disclosure");
setStep(purpose === "verify" ? "email" : "disclosure");
setCode("");
setBusy(false);
setError(null);
@@ -62,7 +97,7 @@ export default function EmailReportDialog({
setFilename("");
setCopied(false);
setEmail(auth?.email ?? "");
}, [open, auth?.email]);
}, [open, auth?.email, purpose]);
// Close on Escape (but never while a password is on screen: it must not be
// dismissed by accident).
@@ -113,6 +148,12 @@ export default function EmailReportDialog({
setError("Enter your email to continue.");
return;
}
// Snappy client-side guard for the obvious personal domains; the relay is
// still authoritative on the full list.
if (isCommonFreeEmail(value)) {
setError(OTP_START_ERRORS.work_email_required);
return;
}
setBusy(true);
setError(null);
const result = await otpStart(value);
@@ -138,6 +179,11 @@ export default function EmailReportDialog({
if (result.verified) {
sentTo.current = result.email;
onVerified();
// Verify-only flow (viewing past runs) stops here; no report is sent.
if (purpose === "verify") {
onClose();
return;
}
void doSend();
} else {
setError("That code did not match. Check it and try again.");
@@ -187,8 +233,16 @@ export default function EmailReportDialog({
<Mail className="h-4 w-4 text-emerald-400" aria-hidden="true" />
</div>
<div>
<h2 className="text-base font-semibold text-white">Email an encrypted PDF report of this run</h2>
<p className="text-xs text-[#666]">Verified by a one-time code sent to your email</p>
<h2 className="text-base font-semibold text-white">
{purpose === "verify"
? "Verify your email to view your runs"
: "Email an encrypted PDF report of this run"}
</h2>
<p className="text-xs text-[#666]">
{purpose === "verify"
? "We send a one-time code to confirm it is you."
: "Verified by a one-time code sent to your email"}
</p>
</div>
</div>
@@ -255,6 +309,7 @@ export default function EmailReportDialog({
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" }}
/>
<span className="mt-1.5 block text-xs text-[#666]">Use your work email.</span>
</label>
<button
type="submit"
@@ -293,7 +348,7 @@ export default function EmailReportDialog({
className="flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90 disabled:opacity-60"
>
{busy && <Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" />}
Verify and send
{purpose === "verify" ? "Verify" : "Verify and send"}
</button>
<button
type="button"
@@ -0,0 +1,96 @@
import {
CalendarClock,
WandSparkles,
Puzzle,
Users,
ArrowUpRight,
} from "lucide-react";
import { SIGNUP_URL, PRICING_URL, trackCta } from "@/lib/cta";
import type { ProFeature } from "@/lib/pro-features";
import { ProTag } from "@/components/ProCta";
/**
* In-app upsell page for a single platform feature. Modeled on the cloud app's
* Networks upsell: a centered bordered card with an icon medallion, tier pill,
* headline, one-line description, a shared "Included in Strix Pro" bullet list,
* then a primary sign-up CTA and a secondary link to all plans.
*/
const INCLUDED = [
{
icon: CalendarClock,
text: "Continuous coverage: scheduled pentests and attack surface monitoring",
},
{ icon: WandSparkles, text: "One-click autofix that opens a retested pull request" },
{ icon: Puzzle, text: "Two-way sync to Jira, Linear, and Slack" },
{ icon: Users, text: "Your whole team, with roles and shared history" },
];
export default function FeatureDetail({ feature }: { feature: ProFeature }) {
const Icon = feature.icon;
return (
<div className="mx-auto w-full max-w-lg">
<div className="rounded-2xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-8 text-center">
<div
className="mx-auto flex h-12 w-12 items-center justify-center rounded-xl"
style={{ border: "1px solid #2a2a2a", background: "rgba(255,255,255,0.04)" }}
>
<Icon className="h-5 w-5 text-[#888]" aria-hidden="true" />
</div>
<div className="mt-4 flex justify-center">
<ProTag label={feature.tier} />
</div>
<h2 className="mt-3 text-2xl font-semibold text-white">{feature.headline}</h2>
<p className="mx-auto mt-2 max-w-md text-sm text-[#888]">{feature.description}</p>
<div
className="mt-6 rounded-xl p-4 text-left"
style={{ border: "1px solid #222", background: "rgba(255,255,255,0.02)" }}
>
<p className="mb-3 text-xs font-semibold uppercase tracking-wide text-[#666]">
Included in Strix Pro
</p>
<ul className="space-y-2.5">
{INCLUDED.map((item) => {
const BulletIcon = item.icon;
return (
<li key={item.text} className="flex items-start gap-2.5">
<BulletIcon
className="mt-0.5 h-4 w-4 flex-shrink-0 text-[#888]"
aria-hidden="true"
/>
<span className="text-sm text-[#aaa]">{item.text}</span>
</li>
);
})}
</ul>
</div>
<div className="mt-6 flex flex-col items-center gap-3">
<a
href={SIGNUP_URL}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta(feature.slug)}
className="inline-flex w-full items-center justify-center gap-1.5 rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90"
>
Start free
<ArrowUpRight className="h-3.5 w-3.5" aria-hidden="true" />
</a>
<a
href={PRICING_URL}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta(`${feature.slug}_pricing`)}
className="inline-flex items-center gap-1 text-xs text-[#888] transition-colors hover:text-white"
>
View all plans
<ArrowUpRight className="h-3 w-3" aria-hidden="true" />
</a>
</div>
</div>
</div>
);
}
@@ -71,14 +71,13 @@ export default function PastRunsView({
</div>
<h2 className="text-base font-semibold text-white">Browse every run on this machine</h2>
<p className="mx-auto mt-1.5 max-w-md text-sm text-[#888]">
You have {count} past {count === 1 ? "run" : "runs"} on this machine. Verify your
email to browse them here.
You have {count} past {count === 1 ? "run" : "runs"} on this machine.
</p>
<button
onClick={onVerifyClick}
className="mt-4 cursor-pointer rounded-lg bg-white px-4 py-2 text-sm font-semibold text-black transition-opacity hover:opacity-90"
>
Verify email to unlock
View runs
</button>
<p className="mt-4 flex items-center justify-center gap-1.5 text-xs text-[#555]">
<Terminal className="h-3.5 w-3.5" aria-hidden="true" />
+31 -21
View File
@@ -1,6 +1,7 @@
import React, { useState } from "react";
import { ArrowUpRight } from "lucide-react";
import { SIGNUP_URL, trackCta } from "@/lib/cta";
import type { ProFeature } from "@/lib/pro-features";
/**
* Shared Pro CTA primitives. Every Pro item is a direct link-out to the cloud
@@ -9,14 +10,14 @@ import { SIGNUP_URL, trackCta } from "@/lib/cta";
* row, and the inline CTAs in the tabs.
*/
/** Small "Pro" pill. Deliberately not a padlock. */
export function ProTag({ className = "" }: { className?: string }) {
/** Small tier pill ("Pro" or "Enterprise"). Deliberately not a padlock. */
export function ProTag({ label = "Pro", className = "" }: { label?: string; className?: string }) {
return (
<span
className={`inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-[#aaa] ${className}`}
style={{ border: "1px solid #2a2a2a", background: "rgba(255,255,255,0.04)" }}
>
Pro
{label}
</span>
);
}
@@ -93,31 +94,40 @@ export function ProTile({ item }: { item: ProItem }) {
}
/**
* Sidebar-row Pro item: icon + label + Pro tag, with the one-liner shown as
* always-visible secondary text under the title. Link-out to sign-up in a new
* tab.
* Sidebar-row Pro item: a two-line row (icon + label + short one-liner
* underneath) with a small right-aligned tier tag. Opens the in-app
* FeatureDetail view via onClick (no link-out) so it sits uniformly beside the
* run/local rows in the themed nav list.
*/
export function ProNavItem({ item }: { item: ProItem }) {
const Icon = item.icon;
export function ProNavItem({
feature,
active,
onClick,
}: {
feature: ProFeature;
active?: boolean;
onClick: () => void;
}) {
const Icon = feature.icon;
return (
<a
href={SIGNUP_URL}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta(item.slug)}
className="group flex w-full items-start gap-2.5 rounded-md px-2.5 py-2 transition-colors hover:bg-[rgba(255,255,255,0.06)]"
<button
onClick={onClick}
className={`group flex w-full cursor-pointer items-start gap-2.5 rounded-md px-2.5 py-1.5 text-left transition-colors ${
active
? "text-white"
: "text-[#888] hover:bg-[rgba(255,255,255,0.06)] hover:text-white"
}`}
style={active ? { background: "rgba(255,255,255,0.12)" } : undefined}
>
<Icon className="mt-0.5 h-4 w-4 flex-shrink-0 text-[#666] transition-colors group-hover:text-[#aaa]" aria-hidden="true" />
<Icon className="mt-0.5 h-4 w-4 flex-shrink-0" aria-hidden="true" />
<span className="min-w-0 flex-1">
<span className="flex items-center gap-1.5">
<span className="flex-1 truncate text-sm text-[#aaa] transition-colors group-hover:text-white">
{item.title}
</span>
<ProTag />
<span className="flex-1 truncate text-sm">{feature.title}</span>
<ProTag label={feature.tier} />
</span>
<span className="mt-0.5 block text-[11px] leading-snug text-[#666]">{item.desc}</span>
<span className="mt-0.5 block text-[11px] leading-snug text-[#666]">{feature.navDesc}</span>
</span>
</a>
</button>
);
}
+46 -67
View File
@@ -5,50 +5,28 @@ import {
Waypoints,
History,
Mail,
GitPullRequest,
Search,
Layers,
Globe,
Puzzle,
Users,
LayoutDashboard,
AlertTriangle,
MessageSquare,
Network,
Database,
ArrowUpRight,
LogOut,
ShieldCheck,
} from "lucide-react";
import { SIGNUP_URL, trackCta } from "@/lib/cta";
import { ProNavItem, type ProItem } from "@/components/ProCta";
import { ProNavItem } from "@/components/ProCta";
import { FEATURES, PLATFORM_ORDER } from "@/lib/pro-features";
import type { View } from "@/App";
/**
* Persistent left rail. Mirrors the cloud app's nav: real in-page content
* ("This run"), local email-gated features ("Your runs"), and org-oriented
* Pro link-outs ("Platform"). Matches App.tsx's dark palette.
* Persistent left rail. A single, ungrouped, ordered list of uniform two-line
* rows (icon + label + short one-liner): the current run's views, the local
* run-history + email-report actions, then the platform features. No section
* headers. Tier is shown only by the inline Pro/Enterprise tag on platform
* rows. Matches App.tsx's dark palette.
*/
// Org-signal items lead the list (PR Reviews, Repositories, Domains,
// Integrations, Members), then the rest of the platform surface.
const PLATFORM_ITEMS: ProItem[] = [
{ title: "PR Reviews", desc: "Pentest every pull request your team opens.", slug: "pr_reviews", icon: GitPullRequest },
{ title: "Repositories", desc: "Connect your org's repositories.", slug: "repositories", icon: Layers },
{ title: "Domains", desc: "Add the domains your team owns.", slug: "domains", icon: Globe },
{ title: "Integrations", desc: "Two-way sync findings to Jira, Linear, and Slack.", slug: "integrations", icon: Puzzle },
{ title: "Members", desc: "Invite your team and set roles.", slug: "members", icon: Users },
{ title: "Pentests", desc: "Launch deeper pentests on managed infra.", slug: "pentests", icon: Search },
{ title: "Dashboard", desc: "See every project and finding in one place.", slug: "dashboard", icon: LayoutDashboard },
{ title: "Issues", desc: "Track and triage findings across your org.", slug: "platform_issues", icon: AlertTriangle },
{ title: "Chat", desc: "Ask the agents about any finding.", slug: "chat", icon: MessageSquare },
{ title: "Networks", desc: "Map and monitor your network exposure.", slug: "networks", icon: Network },
{ title: "Knowledge", desc: "Give agents context about your systems.", slug: "knowledge", icon: Database },
];
interface SidebarProps {
view: View;
onSelectView: (view: View) => void;
activeFeature: string | null;
onSelectFeature: (slug: string) => void;
issuesCount: number;
agentCount: number;
runCount: number;
@@ -62,6 +40,8 @@ interface SidebarProps {
export default function Sidebar({
view,
onSelectView,
activeFeature,
onSelectFeature,
issuesCount,
agentCount,
runCount,
@@ -119,17 +99,19 @@ export default function Sidebar({
)}
</div>
{/* This run */}
<Section label="This run">
{/* One single ordered list, no section headers. */}
<div className="mt-6 space-y-0.5">
<NavItem
icon={FileText}
label="Overview"
desc="This run's executive report"
active={view === "overview"}
onClick={() => onSelectView("overview")}
/>
<NavItem
icon={Bug}
label="Issues"
desc="Findings from this run"
count={issuesCount > 0 ? issuesCount : undefined}
active={view === "issues"}
onClick={() => onSelectView("issues")}
@@ -138,64 +120,56 @@ export default function Sidebar({
<NavItem
icon={Waypoints}
label="Agents"
desc="What each agent did"
count={agentCount}
active={view === "agents"}
onClick={() => onSelectView("agents")}
/>
)}
</Section>
{/* Your runs (local, unlock with email) */}
<Section label="Your runs">
<NavItem
icon={History}
label="Past runs"
desc="Every run on this machine"
count={runCount > 0 ? runCount : undefined}
active={view === "history"}
onClick={onOpenHistory}
/>
<NavItem icon={Mail} label="Email report" onClick={onOpenEmail} />
</Section>
<NavItem
icon={Mail}
label="Email report"
desc="Get an encrypted PDF by email"
onClick={onOpenEmail}
/>
{/* Platform (Pro link-outs) */}
<div className="mt-6">
<p className="px-2.5 pb-1 text-[11px] font-semibold uppercase tracking-wide text-[#555]">
Platform
</p>
<p className="px-2.5 pb-2 text-[11px] leading-snug text-[#666]">
Get the most out of Strix for your team.
</p>
<div className="space-y-0.5">
{PLATFORM_ITEMS.map((item) => (
<ProNavItem key={item.slug} item={item} />
))}
</div>
{PLATFORM_ORDER.map((slug) => {
const feature = FEATURES[slug];
if (!feature) return null;
return (
<ProNavItem
key={slug}
feature={feature}
active={view === "feature" && activeFeature === slug}
onClick={() => onSelectFeature(slug)}
/>
);
})}
</div>
</div>
</aside>
);
}
function Section({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="mt-6">
<p className="px-2.5 pb-1.5 text-[11px] font-semibold uppercase tracking-wide text-[#555]">
{label}
</p>
<div className="space-y-0.5">{children}</div>
</div>
);
}
function NavItem({
icon: Icon,
label,
desc,
count,
active,
onClick,
}: {
icon: React.ElementType;
label: string;
desc: string;
count?: number;
active?: boolean;
onClick: () => void;
@@ -203,16 +177,21 @@ function NavItem({
return (
<button
onClick={onClick}
className={`flex w-full cursor-pointer items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors ${
className={`flex w-full cursor-pointer items-start gap-2.5 rounded-md px-2.5 py-1.5 text-left transition-colors ${
active
? "text-white"
: "text-[#888] hover:bg-[rgba(255,255,255,0.06)] hover:text-white"
}`}
style={active ? { background: "rgba(255,255,255,0.12)" } : undefined}
>
<Icon className="h-4 w-4 flex-shrink-0" aria-hidden="true" />
<span className="flex-1 text-left">{label}</span>
{count != null && <span className="text-xs text-[#666] tabular-nums">{count}</span>}
<Icon className="mt-0.5 h-4 w-4 flex-shrink-0" aria-hidden="true" />
<span className="min-w-0 flex-1">
<span className="flex items-center gap-1.5">
<span className="flex-1 truncate text-sm">{label}</span>
{count != null && <span className="text-xs text-[#666] tabular-nums">{count}</span>}
</span>
<span className="mt-0.5 block text-[11px] leading-snug text-[#666]">{desc}</span>
</span>
</button>
);
}
+174
View File
@@ -0,0 +1,174 @@
import type React from "react";
import {
GitPullRequest,
Layers,
Globe,
Puzzle,
Users,
Search,
LayoutDashboard,
AlertTriangle,
MessageSquare,
Network,
Database,
} from "lucide-react";
/**
* Platform (Pro / Enterprise) feature catalog. Powers both the unified sidebar
* nav rows and the in-app FeatureDetail upsell view. Everything is "Pro" except
* Networks, which is "Enterprise". No lock icons anywhere.
*/
export type FeatureTier = "Pro" | "Enterprise";
export interface ProFeature {
slug: string;
title: string;
icon: React.ElementType;
tier: FeatureTier;
/** Short one-liner for the sidebar nav row (two-line layout). */
navDesc: string;
/** Headline shown on the FeatureDetail upsell page. */
headline: string;
/** Longer sentence shown on the FeatureDetail upsell page. */
description: string;
}
// Flat catalog of every platform feature, keyed by slug for routing. The
// sidebar groups these into capability themes (see PLATFORM_THEMES); nothing
// here implies a tier ordering.
export const PLATFORM_FEATURES: ProFeature[] = [
{
slug: "pr_reviews",
title: "PR Reviews",
icon: GitPullRequest,
tier: "Pro",
navDesc: "Pentest every pull request",
headline: "Pentest every pull request",
description:
"Strix reviews every pull request your team opens and catches exploitable changes before they merge.",
},
{
slug: "repositories",
title: "Repositories",
icon: Layers,
tier: "Pro",
navDesc: "Connect your team's repos",
headline: "Connect your team's repositories",
description:
"Link your org's repositories so Strix can scan them continuously and track findings over time.",
},
{
slug: "domains",
title: "Domains",
icon: Globe,
tier: "Pro",
navDesc: "Cover the domains you own",
headline: "Cover every domain you own",
description:
"Add the domains your team owns and let Strix watch them for newly exposed paths and drift.",
},
{
slug: "integrations",
title: "Integrations",
icon: Puzzle,
tier: "Pro",
navDesc: "Sync to Jira, Linear, Slack",
headline: "Sync findings to your tools",
description:
"Two-way sync findings to Jira, Linear, and Slack so fixes happen where your team already works.",
},
{
slug: "members",
title: "Members",
icon: Users,
tier: "Pro",
navDesc: "Invite your team, set roles",
headline: "Bring your whole team",
description:
"Invite your team, set roles, and share findings and run history across your org.",
},
{
slug: "pentests",
title: "Pentests",
icon: Search,
tier: "Pro",
navDesc: "Deeper scans on managed infra",
headline: "Launch deeper pentests",
description:
"Run deeper, longer pentests on managed infrastructure whenever you need them.",
},
{
slug: "dashboard",
title: "Dashboard",
icon: LayoutDashboard,
tier: "Pro",
navDesc: "Everything in one place",
headline: "See everything in one place",
description:
"Track every project, run, and finding across your org from a single dashboard.",
},
{
slug: "platform_issues",
title: "Issues",
icon: AlertTriangle,
tier: "Pro",
navDesc: "Triage across your org",
headline: "Triage findings across your org",
description:
"Manage and triage findings across every project and repository in one queue.",
},
{
slug: "chat",
title: "Chat",
icon: MessageSquare,
tier: "Pro",
navDesc: "Ask about any finding",
headline: "Ask Strix anything",
description:
"Ask the agents about any finding, run, or part of your app in natural language.",
},
{
slug: "networks",
title: "Networks",
icon: Network,
tier: "Enterprise",
navDesc: "Reach internal, VPN-only targets",
headline: "Scan internal networks",
description:
"Connect private networks to scan internal applications, VPN-only services, and RFC1918 targets.",
},
{
slug: "knowledge",
title: "Knowledge",
icon: Database,
tier: "Pro",
navDesc: "Give agents context",
headline: "Give agents context",
description:
"Teach Strix about your systems and business logic so every run gets smarter.",
},
];
export const FEATURES: Record<string, ProFeature> = Object.fromEntries(
PLATFORM_FEATURES.map((f) => [f.slug, f])
);
/**
* Order the platform rows appear in the sidebar's single, ungrouped nav list
* (after the run/local rows). No section headers; tier is shown only by each
* row's inline tag.
*/
export const PLATFORM_ORDER: string[] = [
"pr_reviews",
"repositories",
"domains",
"integrations",
"members",
"pentests",
"networks",
"chat",
"dashboard",
"platform_issues",
"knowledge",
];