From a04a13ef001f478d52d9849e43d6dc64db7737d5 Mon Sep 17 00:00:00 2001 From: Gunel Shukurova Date: Thu, 11 Jun 2026 13:30:02 +0400 Subject: [PATCH 1/6] feat: add mock dataset API with env switch --- apps/web/src/hooks/useDataset.ts | 19 +++++++++++++++---- apps/web/src/lib/api/dataset.ts | 24 ++++++++++++++++++++++++ bun.lock | 1 - 3 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/lib/api/dataset.ts diff --git a/apps/web/src/hooks/useDataset.ts b/apps/web/src/hooks/useDataset.ts index 84db13e..96c67db 100644 --- a/apps/web/src/hooks/useDataset.ts +++ b/apps/web/src/hooks/useDataset.ts @@ -1,10 +1,21 @@ -import { useMemo } from "react"; -import { getDataset } from "@/data"; +import { useEffect, useState } from "react"; +import { fetchDataset } from "@/lib/api/dataset"; import type { Dataset } from "@cleat/contracts"; import { useOrgStore } from "@/stores/useOrgStore"; -/** The memoized dataset for the currently active account. */ export function useDataset(): Dataset { const accountId = useOrgStore((s) => s.activeAccountId); - return useMemo(() => getDataset(accountId), [accountId]); + const [data, setData] = useState(null); + + useEffect(() => { + if (!accountId) return; + + fetchDataset(accountId).then(setData); + }, [accountId]); + + if (!data) { + throw new Promise(() => {}); + } + + return data; } diff --git a/apps/web/src/lib/api/dataset.ts b/apps/web/src/lib/api/dataset.ts new file mode 100644 index 0000000..b326d23 --- /dev/null +++ b/apps/web/src/lib/api/dataset.ts @@ -0,0 +1,24 @@ +import { getDataset } from "@/data"; +import type { Dataset } from "@cleat/contracts"; + +/** + * Mock API layer for dataset. + * Fully mimics real backend endpoint behavior. + */ +export async function fetchDataset(accountId: string): Promise { + // MOCK MODE + if (import.meta.env.VITE_USE_MOCK_API === "true") { + await new Promise((r) => setTimeout(r, 50)); + return getDataset(accountId); + } + + // REAL BACKEND + const res = await fetch(`/api/dataset?accountId=${accountId}`); + + if (!res.ok) { + throw new Error("Failed to fetch dataset"); + } + + const data: Dataset = await res.json(); + return data; +} diff --git a/bun.lock b/bun.lock index 649305c..f68112a 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,6 @@ "name": "cleat-web", "version": "1.7.0", "dependencies": { - "@cleat/contracts": "workspace:*", "@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/jetbrains-mono": "^5.2.8", "clsx": "^2.1.1", From ddabf913a9ef24e04d91841bb058d77968cf51e8 Mon Sep 17 00:00:00 2001 From: Gunel Shukurova Date: Sat, 13 Jun 2026 19:34:19 +0400 Subject: [PATCH 2/6] chore: implement mock dataset API and fix useDataset loading flow --- apps/web/.env.example | 1 + apps/web/package.json | 1 + apps/web/src/components/layout/Sidebar.tsx | 10 + apps/web/src/data/generate.ts | 2 +- apps/web/src/features/access/AccessPage.tsx | 8 + .../src/features/artifacts/ArtifactsPage.tsx | 29 +- .../dependencies/DependenciesPage.tsx | 29 +- .../src/features/overview/OverviewPage.tsx | 8 + .../features/repositories/RepoDetailPage.tsx | 8 + .../repositories/RepositoriesPage.tsx | 27 +- .../features/security/CodeScanningPage.tsx | 26 +- .../web/src/features/security/SecretsPage.tsx | 26 +- .../features/security/VulnerabilitiesPage.tsx | 25 +- .../features/supplyChain/SupplyChainPage.tsx | 27 +- apps/web/src/hooks/useDataset.ts | 23 +- apps/web/src/hooks/useNotifications.ts | 9 +- apps/web/src/lib/api/dataset.ts | 9 +- apps/web/vite.config.ts | 29 +- bun.lock | 21 + package-lock.json | 2773 +++++++++++++++++ 20 files changed, 3021 insertions(+), 70 deletions(-) create mode 100644 apps/web/.env.example create mode 100644 package-lock.json diff --git a/apps/web/.env.example b/apps/web/.env.example new file mode 100644 index 0000000..35d343a --- /dev/null +++ b/apps/web/.env.example @@ -0,0 +1 @@ +VITE_USE_MOCK_API=true \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index a90bf93..2d8b0d9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -33,6 +33,7 @@ "playwright": "^1.60.0", "react": "^19.2.6", "react-dom": "^19.2.6", + "react-loader-spinner": "^8.0.2", "react-router-dom": "^7.15.1", "recharts": "^3.8.1", "tailwind-merge": "^3.6.0", diff --git a/apps/web/src/components/layout/Sidebar.tsx b/apps/web/src/components/layout/Sidebar.tsx index 8ff4373..15c47f7 100644 --- a/apps/web/src/components/layout/Sidebar.tsx +++ b/apps/web/src/components/layout/Sidebar.tsx @@ -10,6 +10,7 @@ import { useUiStore } from "@/stores/useUiStore"; import { useDataset } from "@/hooks/useDataset"; import { useNotifications } from "@/hooks/useNotifications"; import { sidebarCounts } from "@/data/metrics"; +import { TailSpin } from "react-loader-spinner"; export function Sidebar({ forceExpanded = false }: { forceExpanded?: boolean }) { const storeCollapsed = useUiStore((s) => s.sidebarCollapsed); @@ -18,6 +19,15 @@ export function Sidebar({ forceExpanded = false }: { forceExpanded?: boolean }) const ds = useDataset(); const { unread } = useNotifications(); + if (!ds) { + return ( +
+
+ +
+
+ ); + } const base = sidebarCounts(ds); const counts: Record = { ...base, diff --git a/apps/web/src/data/generate.ts b/apps/web/src/data/generate.ts index 4892899..023d27e 100644 --- a/apps/web/src/data/generate.ts +++ b/apps/web/src/data/generate.ts @@ -1,4 +1,4 @@ -import { cvssToSeverity } from "@/lib/severity"; +import { cvssToSeverity } from "../lib/severity"; import type { Severity } from "@cleat/contracts"; import { Rng } from "./rng"; import { diff --git a/apps/web/src/features/access/AccessPage.tsx b/apps/web/src/features/access/AccessPage.tsx index d402d0a..781f58b 100644 --- a/apps/web/src/features/access/AccessPage.tsx +++ b/apps/web/src/features/access/AccessPage.tsx @@ -11,6 +11,7 @@ import { WebhooksTab } from "./WebhooksTab"; import { KeysTab } from "./KeysTab"; import { TokensTab } from "./TokensTab"; import { AuditLogTab } from "./AuditLogTab"; +import { TailSpin } from "react-loader-spinner"; type Tab = "members" | "apps" | "webhooks" | "keys" | "tokens" | "audit"; @@ -18,6 +19,13 @@ export function AccessPage() { const ds = useDataset(); const [tab, setTab] = useState("members"); + if (!ds) { + return ( +
+ +
+ ); + } const without2fa = membersWithout2fa(ds).length; const outside = ds.members.filter((m) => m.outsideCollaborator).length; diff --git a/apps/web/src/features/artifacts/ArtifactsPage.tsx b/apps/web/src/features/artifacts/ArtifactsPage.tsx index 53c308d..6ac7d1d 100644 --- a/apps/web/src/features/artifacts/ArtifactsPage.tsx +++ b/apps/web/src/features/artifacts/ArtifactsPage.tsx @@ -37,13 +37,32 @@ export function ArtifactsPage() { const [tab, setTab] = useState("artifacts"); const [deleted, setDeleted] = useState>(new Set()); const [selected, setSelected] = useState>(new Set()); - const artifacts = useMemo( - () => ds.artifacts.filter((a) => !deleted.has(a.id)), - [ds.artifacts, deleted], + () => ds?.artifacts.filter((a) => !deleted.has(a.id)) ?? [], + [ds, deleted], ); - const caches = useMemo(() => ds.caches.filter((c) => !deleted.has(c.id)), [ds.caches, deleted]); - const packages = ds.packages; + + const caches = useMemo(() => ds?.caches.filter((c) => !deleted.has(c.id)) ?? [], [ds, deleted]); + + const packages = ds?.packages ?? []; + if (!ds) { + return ( +
+ + +
+ {[1, 2, 3, 4].map((i) => ( +
+ ))} +
+ +
+
+ ); + } const totalStorageMb = artifacts.reduce((s, a) => s + a.sizeMb, 0) + diff --git a/apps/web/src/features/dependencies/DependenciesPage.tsx b/apps/web/src/features/dependencies/DependenciesPage.tsx index f482002..4a603ee 100644 --- a/apps/web/src/features/dependencies/DependenciesPage.tsx +++ b/apps/web/src/features/dependencies/DependenciesPage.tsx @@ -24,19 +24,15 @@ import { ecosystem } from "@/lib/ecosystems"; import { pluralize } from "@/lib/format"; import { useUiStore } from "@/stores/useUiStore"; import { cn } from "@/lib/cn"; - +import { TailSpin } from "react-loader-spinner"; const TABLE = "dependencies"; export function DependenciesPage() { const ds = useDataset(); const addToast = useUiStore((s) => s.addToast); const [format, setFormat] = useState("spdx"); - - const deps = useMemo(() => buildDependencies(ds), [ds]); - const dist = useMemo(() => licenseDistribution(deps), [deps]); - const vulnerable = deps.filter((d) => d.vulnerable).length; - const outdated = deps.filter((d) => d.outdated).length; - const copyleft = deps.filter((d) => COPYLEFT.has(d.license)).length; + const deps = useMemo(() => (ds ? buildDependencies(ds) : []), [ds]); + const dist = useMemo(() => (deps.length ? licenseDistribution(deps) : []), [deps]); const facets: FacetDef[] = [ { @@ -71,19 +67,36 @@ export function DependenciesPage() { facets, }); + if (!ds) { + return ( +
+
+ +
+
+ ); + } + + const vulnerable = deps.filter((d) => d.vulnerable).length; + const outdated = deps.filter((d) => d.outdated).length; + const copyleft = deps.filter((d) => COPYLEFT.has(d.license)).length; + function exportSbom() { + if (!ds) return; + const content = format === "spdx" ? buildSpdx(ds.account.login, deps) : buildCycloneDx(ds.account.login, deps); + downloadFile(`${ds.account.login}-sbom.${format}.json`, content); + addToast({ title: `${format === "spdx" ? "SPDX" : "CycloneDX"} SBOM exported`, description: `${deps.length} components`, variant: "success", }); } - const columns: Column[] = [ { id: "name", diff --git a/apps/web/src/features/overview/OverviewPage.tsx b/apps/web/src/features/overview/OverviewPage.tsx index fc057de..f1f5ea6 100644 --- a/apps/web/src/features/overview/OverviewPage.tsx +++ b/apps/web/src/features/overview/OverviewPage.tsx @@ -29,9 +29,17 @@ import { scoreToGrade, GRADE_COLOR, SEVERITY } from "@/lib/severity"; import { currency, fromMb, relativeTime, pluralize, percent } from "@/lib/format"; import { eventIcon } from "@/features/notifications/eventMeta"; import { cn } from "@/lib/cn"; +import { TailSpin } from "react-loader-spinner"; export function OverviewPage() { const ds = useDataset(); + if (!ds) { + return ( +
+ +
+ ); + } const sev = severityBreakdown(ds); const findings = totalOpenFindings(ds); const grade = scoreToGrade(ds.account.postureScore); diff --git a/apps/web/src/features/repositories/RepoDetailPage.tsx b/apps/web/src/features/repositories/RepoDetailPage.tsx index a4a8f95..6cd255c 100644 --- a/apps/web/src/features/repositories/RepoDetailPage.tsx +++ b/apps/web/src/features/repositories/RepoDetailPage.tsx @@ -34,6 +34,7 @@ import { languageColor } from "@/lib/ecosystems"; import { relativeTime, compactNumber, fromMb } from "@/lib/format"; import { cn } from "@/lib/cn"; import type { Visibility, ScorecardCheck } from "@cleat/contracts"; +import { TailSpin } from "react-loader-spinner"; const VIS: Record = { private: { icon: Lock, label: "Private" }, @@ -51,6 +52,13 @@ function scoreHex(score: number) { export function RepoDetailPage() { const { repoId } = useParams(); const ds = useDataset(); + if (!ds) { + return ( +
+ +
+ ); + } const repo = ds.repos.find((r) => r.id === repoId); if (!repo) { diff --git a/apps/web/src/features/repositories/RepositoriesPage.tsx b/apps/web/src/features/repositories/RepositoriesPage.tsx index 7c1886d..8a4bdbb 100644 --- a/apps/web/src/features/repositories/RepositoriesPage.tsx +++ b/apps/web/src/features/repositories/RepositoriesPage.tsx @@ -21,6 +21,7 @@ import { languageColor } from "@/lib/ecosystems"; import { relativeTime, compactNumber, fromMb } from "@/lib/format"; import { cn } from "@/lib/cn"; import type { Repo, Visibility } from "@cleat/contracts"; +import { TailSpin } from "react-loader-spinner"; const TABLE = "repositories"; @@ -33,13 +34,6 @@ const VIS_ICON: Record = { export function RepositoriesPage() { const ds = useDataset(); const navigate = useNavigate(); - - const protectedCount = ds.repos.filter((r) => r.branchProtected).length; - const archived = ds.repos.filter((r) => r.archived).length; - const avgHygiene = Math.round( - ds.repos.reduce((s, r) => s + r.hygieneScore, 0) / Math.max(1, ds.repos.length), - ); - const facets: FacetDef[] = [ { key: "visibility", @@ -55,7 +49,7 @@ export function RepositoriesPage() { key: "language", label: "Language", accessor: (r) => r.language, - options: [...new Set(ds.repos.map((r) => r.language))] + options: [...new Set(ds?.repos?.map((r) => r.language) ?? [])] .sort() .map((l) => ({ value: l, label: l })), }, @@ -76,11 +70,26 @@ export function RepositoriesPage() { }, ]; - const rows = useFilteredRows(TABLE, ds.repos, { + const rows = useFilteredRows(TABLE, ds?.repos ?? [], { search: (r) => `${r.name} ${r.language} ${r.topics.join(" ")}`, facets, }); + if (!ds) { + return ( +
+
+ +
+
+ ); + } + const protectedCount = ds.repos.filter((r) => r.branchProtected).length; + const archived = ds.repos.filter((r) => r.archived).length; + const avgHygiene = Math.round( + ds.repos.reduce((s, r) => s + r.hygieneScore, 0) / Math.max(1, ds.repos.length), + ); + const columns: Column[] = [ { id: "name", diff --git a/apps/web/src/features/security/CodeScanningPage.tsx b/apps/web/src/features/security/CodeScanningPage.tsx index 5a80e69..4e58a38 100644 --- a/apps/web/src/features/security/CodeScanningPage.tsx +++ b/apps/web/src/features/security/CodeScanningPage.tsx @@ -9,6 +9,7 @@ import { useFilteredRows, type FacetDef } from "@/hooks/useFilteredRows"; import { SEVERITY } from "@/lib/severity"; import { relativeTime } from "@/lib/format"; import type { CodeScanAlert } from "@cleat/contracts"; +import { TailSpin } from "react-loader-spinner"; const TABLE = "code-scanning"; @@ -23,10 +24,6 @@ const STATUS_META: Record< export function CodeScanningPage() { const ds = useDataset(); - const open = ds.codeAlerts.filter((a) => a.status === "open").length; - const fixed = ds.codeAlerts.filter((a) => a.status === "fixed").length; - const dismissed = ds.codeAlerts.filter((a) => a.status === "dismissed").length; - const rules = new Set(ds.codeAlerts.map((a) => a.ruleId)).size; const facets: FacetDef[] = [ { @@ -52,15 +49,32 @@ export function CodeScanningPage() { key: "tool", label: "Tool", accessor: (r) => r.tool, - options: [...new Set(ds.codeAlerts.map((a) => a.tool))].map((t) => ({ value: t, label: t })), + options: [...new Set(ds?.codeAlerts?.map((a) => a.tool) ?? [])].map((t) => ({ + value: t, + label: t, + })), }, ]; - const rows = useFilteredRows(TABLE, ds.codeAlerts, { + const rows = useFilteredRows(TABLE, ds?.codeAlerts ?? [], { search: (r) => `${r.rule} ${r.ruleId} ${r.repo} ${r.file}`, facets, }); + if (!ds) { + return ( +
+
+ +
+
+ ); + } + const open = ds.codeAlerts.filter((a) => a.status === "open").length; + const fixed = ds.codeAlerts.filter((a) => a.status === "fixed").length; + const dismissed = ds.codeAlerts.filter((a) => a.status === "dismissed").length; + const rules = new Set(ds.codeAlerts.map((a) => a.ruleId)).size; + const columns: Column[] = [ { id: "rule", diff --git a/apps/web/src/features/security/SecretsPage.tsx b/apps/web/src/features/security/SecretsPage.tsx index db1cc26..ba6ff29 100644 --- a/apps/web/src/features/security/SecretsPage.tsx +++ b/apps/web/src/features/security/SecretsPage.tsx @@ -14,18 +14,14 @@ import { provider } from "@/lib/ecosystems"; import { SEVERITY } from "@/lib/severity"; import { relativeTime } from "@/lib/format"; import type { SecretFinding } from "@cleat/contracts"; +import { TailSpin } from "react-loader-spinner"; const TABLE = "secrets"; export function SecretsPage() { const ds = useDataset(); - const [selected, setSelected] = useState(null); - - const active = ds.secrets.filter((s) => s.validity === "active").length; - const revoked = ds.secrets.filter((s) => s.validity === "revoked").length; - const blocked = ds.secrets.filter((s) => s.pushProtectionBlocked).length; - const repos = new Set(ds.secrets.map((s) => s.repo)).size; + const [selected, setSelected] = useState(null); const facets: FacetDef[] = [ { key: "validity", @@ -50,18 +46,32 @@ export function SecretsPage() { key: "provider", label: "Provider", accessor: (r) => r.provider, - options: [...new Set(ds.secrets.map((s) => s.provider))].map((p) => ({ + options: [...new Set(ds?.secrets?.map((s) => s.provider) ?? [])].map((p) => ({ value: p, label: provider(p).label, })), }, ]; - const rows = useFilteredRows(TABLE, ds.secrets, { + const rows = useFilteredRows(TABLE, ds?.secrets ?? [], { search: (r) => `${r.secretType} ${r.repo} ${r.file} ${r.author} ${provider(r.provider).label}`, facets, }); + if (!ds) { + return ( +
+
+ +
+
+ ); + } + const active = ds.secrets.filter((s) => s.validity === "active").length; + const revoked = ds.secrets.filter((s) => s.validity === "revoked").length; + const blocked = ds.secrets.filter((s) => s.pushProtectionBlocked).length; + const repos = new Set(ds.secrets.map((s) => s.repo)).size; + const columns: Column[] = [ { id: "secretType", diff --git a/apps/web/src/features/security/VulnerabilitiesPage.tsx b/apps/web/src/features/security/VulnerabilitiesPage.tsx index 4666b1b..835a731 100644 --- a/apps/web/src/features/security/VulnerabilitiesPage.tsx +++ b/apps/web/src/features/security/VulnerabilitiesPage.tsx @@ -16,17 +16,14 @@ import { SEVERITY, cvssToSeverity } from "@/lib/severity"; import { percent, pluralize } from "@/lib/format"; import { cn } from "@/lib/cn"; import type { Vulnerability } from "@cleat/contracts"; +import { TailSpin } from "react-loader-spinner"; const TABLE = "vulnerabilities"; export function VulnerabilitiesPage() { const ds = useDataset(); - const [selected, setSelected] = useState(null); - - const critical = ds.vulnerabilities.filter((v) => v.severity === "critical").length; - const kev = ds.vulnerabilities.filter((v) => v.kev).length; - const fixable = ds.vulnerabilities.filter((v) => v.fixedVersion).length; + const [selected, setSelected] = useState(null); const facets: FacetDef[] = [ { key: "severity", @@ -41,7 +38,7 @@ export function VulnerabilitiesPage() { key: "ecosystem", label: "Ecosystem", accessor: (r) => r.ecosystem, - options: [...new Set(ds.vulnerabilities.map((v) => v.ecosystem))].map((e) => ({ + options: [...new Set(ds?.vulnerabilities?.map((v) => v.ecosystem) ?? [])].map((e) => ({ value: e, label: ecosystem(e).label, })), @@ -66,8 +63,7 @@ export function VulnerabilitiesPage() { ], }, ]; - - const filtered = useFilteredRows(TABLE, ds.vulnerabilities, { + const filtered = useFilteredRows(TABLE, ds?.vulnerabilities ?? [], { search: (r) => `${r.package} ${r.title} ${r.advisoryId} ${r.cwe}`, facets, }); @@ -77,6 +73,19 @@ export function VulnerabilitiesPage() { [filtered], ); + if (!ds) { + return ( +
+
+ +
+
+ ); + } + const critical = ds.vulnerabilities.filter((v) => v.severity === "critical").length; + const kev = ds.vulnerabilities.filter((v) => v.kev).length; + const fixable = ds.vulnerabilities.filter((v) => v.fixedVersion).length; + const columns: Column[] = [ { id: "priority", diff --git a/apps/web/src/features/supplyChain/SupplyChainPage.tsx b/apps/web/src/features/supplyChain/SupplyChainPage.tsx index c89f48c..0608b2e 100644 --- a/apps/web/src/features/supplyChain/SupplyChainPage.tsx +++ b/apps/web/src/features/supplyChain/SupplyChainPage.tsx @@ -21,6 +21,7 @@ import { unpinnedActionCount } from "@/data/metrics"; import { percent } from "@/lib/format"; import { useUiStore } from "@/stores/useUiStore"; import type { WorkflowAudit, SupplyChainIncident } from "@cleat/contracts"; +import { TailSpin } from "react-loader-spinner"; const TABLE = "workflows"; @@ -33,14 +34,8 @@ function riskTone(score: number) { export function SupplyChainPage() { const ds = useDataset(); - const [selected, setSelected] = useState(null); - - const unpinned = unpinnedActionCount(ds); - const broad = ds.workflows.filter((w) => w.permissions === "broad").length; - const oidc = ds.workflows.length - ? ds.workflows.filter((w) => w.usesOidc).length / ds.workflows.length - : 0; + const [selected, setSelected] = useState(null); const facets: FacetDef[] = [ { key: "permissions", @@ -71,10 +66,26 @@ export function SupplyChainPage() { }, ]; - const rows = useFilteredRows(TABLE, ds.workflows, { + const rows = useFilteredRows(TABLE, ds?.workflows ?? [], { search: (r) => `${r.repo} ${r.workflow} ${r.actions.map((a) => a.name).join(" ")}`, facets, }); + if (!ds) { + return ( +
+ + +
+ +
+
+ ); + } + const unpinned = unpinnedActionCount(ds); + const broad = ds.workflows.filter((w) => w.permissions === "broad").length; + const oidc = ds.workflows.length + ? ds.workflows.filter((w) => w.usesOidc).length / ds.workflows.length + : 0; const columns: Column[] = [ { diff --git a/apps/web/src/hooks/useDataset.ts b/apps/web/src/hooks/useDataset.ts index 96c67db..0999909 100644 --- a/apps/web/src/hooks/useDataset.ts +++ b/apps/web/src/hooks/useDataset.ts @@ -3,19 +3,28 @@ import { fetchDataset } from "@/lib/api/dataset"; import type { Dataset } from "@cleat/contracts"; import { useOrgStore } from "@/stores/useOrgStore"; -export function useDataset(): Dataset { +export function useDataset(): Dataset | null { const accountId = useOrgStore((s) => s.activeAccountId); const [data, setData] = useState(null); useEffect(() => { - if (!accountId) return; + let cancelled = false; - fetchDataset(accountId).then(setData); - }, [accountId]); + if (!accountId) { + setData(null); + return; + } + + setData(null); - if (!data) { - throw new Promise(() => {}); - } + fetchDataset(accountId).then((d) => { + if (!cancelled) setData(d); + }); + + return () => { + cancelled = true; + }; + }, [accountId]); return data; } diff --git a/apps/web/src/hooks/useNotifications.ts b/apps/web/src/hooks/useNotifications.ts index beb3fdf..d089a05 100644 --- a/apps/web/src/hooks/useNotifications.ts +++ b/apps/web/src/hooks/useNotifications.ts @@ -10,14 +10,21 @@ export interface NotificationItem extends ActivityEvent { /** Activity events for the active account, merged with persisted read-state. */ export function useNotifications() { const ds = useDataset(); + const readIds = useNotificationStore((s) => s.readIds); return useMemo(() => { + if (!ds) { + return { items: [], unread: 0 }; + } + const items: NotificationItem[] = ds.events.map((e) => ({ ...e, read: readIds.includes(e.id), })); + const unread = items.filter((i) => !i.read).length; + return { items, unread }; - }, [ds.events, readIds]); + }, [ds, readIds]); } diff --git a/apps/web/src/lib/api/dataset.ts b/apps/web/src/lib/api/dataset.ts index b326d23..3dc4b64 100644 --- a/apps/web/src/lib/api/dataset.ts +++ b/apps/web/src/lib/api/dataset.ts @@ -6,19 +6,16 @@ import type { Dataset } from "@cleat/contracts"; * Fully mimics real backend endpoint behavior. */ export async function fetchDataset(accountId: string): Promise { - // MOCK MODE if (import.meta.env.VITE_USE_MOCK_API === "true") { await new Promise((r) => setTimeout(r, 50)); return getDataset(accountId); } - // REAL BACKEND - const res = await fetch(`/api/dataset?accountId=${accountId}`); + const res = await fetch(`/api/dataset?accountId=${encodeURIComponent(accountId)}`); if (!res.ok) { - throw new Error("Failed to fetch dataset"); + throw new Error(`Failed to fetch dataset: ${res.status} ${res.statusText}`); } - const data: Dataset = await res.json(); - return data; + return (await res.json()) as Dataset; } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 47d5e18..3d967a5 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -1,15 +1,38 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; -import { fileURLToPath, URL } from "node:url"; +import type { ViteDevServer } from "vite"; +import path from "node:path"; + +import { getDataset } from "./src/data/index"; + +function mockApiPlugin() { + return { + name: "mock-api", + apply: "serve", + configureServer(server: ViteDevServer) { + server.middlewares.use("/api/dataset", (req, res) => { + const url = new URL(req.originalUrl || req.url || "", "http://localhost"); + const accountId = url.searchParams.get("accountId") ?? "demo"; + + const dataset = getDataset(accountId); + + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify(dataset)); + }); + }, + }; +} export default defineConfig({ - plugins: [react(), tailwindcss()], + plugins: [react(), tailwindcss(), mockApiPlugin()], + resolve: { alias: { - "@": fileURLToPath(new URL("./src", import.meta.url)), + "@": path.resolve(__dirname, "./src"), }, }, + server: { port: 5173, host: true, diff --git a/bun.lock b/bun.lock index f68112a..762aa71 100644 --- a/bun.lock +++ b/bun.lock @@ -18,6 +18,7 @@ "playwright": "^1.60.0", "react": "^19.2.6", "react-dom": "^19.2.6", + "react-loader-spinner": "^8.0.2", "react-router-dom": "^7.15.1", "recharts": "^3.8.1", "tailwind-merge": "^3.6.0", @@ -90,6 +91,10 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.4.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0" } }, "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw=="], + + "@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], @@ -276,6 +281,8 @@ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001797", "", {}, "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w=="], "cleat-web": ["cleat-web@workspace:apps/web"], @@ -288,6 +295,10 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="], + + "css-to-react-native": ["css-to-react-native@3.2.0", "", { "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^4.0.2" } }, "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], @@ -488,6 +499,8 @@ "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "prettier": ["prettier@3.8.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q=="], @@ -500,6 +513,8 @@ "react-is": ["react-is@19.2.7", "", {}, "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A=="], + "react-loader-spinner": ["react-loader-spinner@8.0.2", "", { "dependencies": { "styled-components": "^6.3.6", "tinycolor2": "^1.6.0" }, "peerDependencies": { "react": ">=17.0.0 <20.0.0", "react-dom": ">=17.0.0 <20.0.0" } }, "sha512-N2gHEy88VKRaijzEFoRp/aHEPE6Lr6upkmPBPu9hfqZqdxftxsXd1B+lzOmVnn3BYHyXwvYrifWzMbR64lwQlw=="], + "react-redux": ["react-redux@9.3.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g=="], "react-router": ["react-router@7.17.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ=="], @@ -528,6 +543,10 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "styled-components": ["styled-components@6.4.2", "", { "dependencies": { "@emotion/is-prop-valid": "1.4.0", "css-to-react-native": "3.2.0", "csstype": "3.2.3", "stylis": "4.3.6" }, "peerDependencies": { "react": ">= 16.8.0", "react-dom": ">= 16.8.0", "react-native": ">= 0.68.0" }, "optionalPeers": ["react-dom", "react-native"] }, "sha512-xZBhBJsMtGqb+aKcwKgaT+BtuFums9VynX2JRvXJGTx5UfZzN12rk5r4nVdhXYvRw+hE7yiYxVrOqJZaK2+Txg=="], + + "stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], + "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], @@ -536,6 +555,8 @@ "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..955b943 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2773 @@ +{ + "name": "cleat", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cleat", + "workspaces": [ + "apps/*", + "packages/*" + ] + }, + "apps/web": { + "name": "cleat-web", + "version": "1.7.0", + "dependencies": { + "@fontsource-variable/inter": "^5.2.8", + "@fontsource-variable/jetbrains-mono": "^5.2.8", + "clsx": "^2.1.1", + "date-fns": "^4.3.0", + "lucide-react": "^1.16.0", + "motion": "^12.40.0", + "playwright": "^1.60.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-loader-spinner": "^8.0.2", + "react-router-dom": "^7.15.1", + "recharts": "^3.8.1", + "tailwind-merge": "^3.6.0", + "zustand": "^5.0.13" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "^4.3.0", + "@types/bun": "latest", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.2", + "eslint": "^10.4.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "prettier": "^3.8.3", + "tailwindcss": "^4.3.0", + "typescript-eslint": "^8.60.0", + "vite": "^8.0.14" + }, + "peerDependencies": { + "typescript": "^5" + } + }, + "apps/web/node_modules/@babel/code-frame": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "apps/web/node_modules/@babel/compat-data": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "apps/web/node_modules/@babel/core": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "apps/web/node_modules/@babel/generator": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "apps/web/node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "apps/web/node_modules/@babel/helper-globals": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "apps/web/node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "apps/web/node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "apps/web/node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "apps/web/node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "apps/web/node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "apps/web/node_modules/@babel/helpers": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "apps/web/node_modules/@babel/parser": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "apps/web/node_modules/@babel/template": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "apps/web/node_modules/@babel/traverse": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "apps/web/node_modules/@babel/types": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "apps/web/node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "apps/web/node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "apps/web/node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "apps/web/node_modules/@eslint/config-array": { + "version": "0.23.5", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "apps/web/node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "apps/web/node_modules/@eslint/core": { + "version": "1.2.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "apps/web/node_modules/@eslint/js": { + "version": "10.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "apps/web/node_modules/@eslint/object-schema": { + "version": "3.0.5", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "apps/web/node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "apps/web/node_modules/@fontsource-variable/inter": { + "version": "5.2.8", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "apps/web/node_modules/@fontsource-variable/jetbrains-mono": { + "version": "5.2.8", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "apps/web/node_modules/@humanfs/core": { + "version": "0.19.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "apps/web/node_modules/@humanfs/node": { + "version": "0.16.8", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "apps/web/node_modules/@humanfs/types": { + "version": "0.15.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "apps/web/node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "apps/web/node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "apps/web/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "apps/web/node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "apps/web/node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "apps/web/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "apps/web/node_modules/@oxc-project/types": { + "version": "0.132.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "apps/web/node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "apps/web/node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.8", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "apps/web/node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.2", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "apps/web/node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/@standard-schema/spec": { + "version": "1.1.0", + "license": "MIT" + }, + "apps/web/node_modules/@standard-schema/utils": { + "version": "0.3.0", + "license": "MIT" + }, + "apps/web/node_modules/@tailwindcss/node": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "apps/web/node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "apps/web/node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "apps/web/node_modules/@tailwindcss/vite": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "apps/web/node_modules/@types/d3-array": { + "version": "3.2.2", + "license": "MIT" + }, + "apps/web/node_modules/@types/d3-color": { + "version": "3.1.3", + "license": "MIT" + }, + "apps/web/node_modules/@types/d3-ease": { + "version": "3.0.2", + "license": "MIT" + }, + "apps/web/node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "apps/web/node_modules/@types/d3-path": { + "version": "3.1.1", + "license": "MIT" + }, + "apps/web/node_modules/@types/d3-scale": { + "version": "4.0.9", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "apps/web/node_modules/@types/d3-shape": { + "version": "3.1.8", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "apps/web/node_modules/@types/d3-time": { + "version": "3.0.4", + "license": "MIT" + }, + "apps/web/node_modules/@types/d3-timer": { + "version": "3.0.2", + "license": "MIT" + }, + "apps/web/node_modules/@types/esrecurse": { + "version": "4.3.1", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/@types/estree": { + "version": "1.0.9", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/@types/react": { + "version": "19.2.15", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "apps/web/node_modules/@types/react-dom": { + "version": "19.2.3", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "apps/web/node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "license": "MIT" + }, + "apps/web/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.60.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/type-utils": "8.60.0", + "@typescript-eslint/utils": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.60.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "apps/web/node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "apps/web/node_modules/@typescript-eslint/parser": { + "version": "8.60.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "apps/web/node_modules/@typescript-eslint/project-service": { + "version": "8.60.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.60.0", + "@typescript-eslint/types": "^8.60.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "apps/web/node_modules/@typescript-eslint/scope-manager": { + "version": "8.60.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "apps/web/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.60.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "apps/web/node_modules/@typescript-eslint/type-utils": { + "version": "8.60.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/utils": "8.60.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "apps/web/node_modules/@typescript-eslint/types": { + "version": "8.60.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "apps/web/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.60.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.60.0", + "@typescript-eslint/tsconfig-utils": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "apps/web/node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "apps/web/node_modules/@typescript-eslint/utils": { + "version": "8.60.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "apps/web/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.60.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "apps/web/node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "apps/web/node_modules/acorn": { + "version": "8.16.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "apps/web/node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "apps/web/node_modules/ajv": { + "version": "6.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "apps/web/node_modules/balanced-match": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "apps/web/node_modules/baseline-browser-mapping": { + "version": "2.10.33", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "apps/web/node_modules/brace-expansion": { + "version": "5.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "apps/web/node_modules/browserslist": { + "version": "4.28.2", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "apps/web/node_modules/caniuse-lite": { + "version": "1.0.30001793", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "apps/web/node_modules/clsx": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "apps/web/node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/cookie": { + "version": "1.1.1", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "apps/web/node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "apps/web/node_modules/d3-array": { + "version": "3.2.4", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "apps/web/node_modules/d3-color": { + "version": "3.1.0", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "apps/web/node_modules/d3-ease": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "apps/web/node_modules/d3-format": { + "version": "3.1.2", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "apps/web/node_modules/d3-interpolate": { + "version": "3.0.1", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "apps/web/node_modules/d3-path": { + "version": "3.1.0", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "apps/web/node_modules/d3-scale": { + "version": "4.0.2", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "apps/web/node_modules/d3-shape": { + "version": "3.2.0", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "apps/web/node_modules/d3-time": { + "version": "3.1.0", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "apps/web/node_modules/d3-time-format": { + "version": "4.1.0", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "apps/web/node_modules/d3-timer": { + "version": "3.0.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "apps/web/node_modules/date-fns": { + "version": "4.3.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "apps/web/node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "apps/web/node_modules/decimal.js-light": { + "version": "2.5.1", + "license": "MIT" + }, + "apps/web/node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/detect-libc": { + "version": "2.1.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "apps/web/node_modules/electron-to-chromium": { + "version": "1.5.364", + "dev": true, + "license": "ISC" + }, + "apps/web/node_modules/enhanced-resolve": { + "version": "5.22.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "apps/web/node_modules/es-toolkit": { + "version": "1.46.1", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "apps/web/node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "apps/web/node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/web/node_modules/eslint": { + "version": "10.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "apps/web/node_modules/eslint-config-prettier": { + "version": "10.1.8", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "apps/web/node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "apps/web/node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "apps/web/node_modules/eslint-scope": { + "version": "9.1.2", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "apps/web/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "apps/web/node_modules/espree": { + "version": "11.2.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "apps/web/node_modules/esquery": { + "version": "1.7.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "apps/web/node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "apps/web/node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "apps/web/node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "apps/web/node_modules/eventemitter3": { + "version": "5.0.4", + "license": "MIT" + }, + "apps/web/node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "apps/web/node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "apps/web/node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/web/node_modules/flat-cache": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "apps/web/node_modules/flatted": { + "version": "3.4.2", + "dev": true, + "license": "ISC" + }, + "apps/web/node_modules/framer-motion": { + "version": "12.40.0", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.40.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "apps/web/node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "apps/web/node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "apps/web/node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "apps/web/node_modules/globals": { + "version": "17.6.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/web/node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "license": "ISC" + }, + "apps/web/node_modules/hermes-estree": { + "version": "0.25.1", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/hermes-parser": { + "version": "0.25.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "apps/web/node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "apps/web/node_modules/immer": { + "version": "10.2.0", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "apps/web/node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "apps/web/node_modules/internmap": { + "version": "2.0.3", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "apps/web/node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "apps/web/node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "apps/web/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "apps/web/node_modules/jiti": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "apps/web/node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/jsesc": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "apps/web/node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "apps/web/node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "apps/web/node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "apps/web/node_modules/lightningcss": { + "version": "1.32.0", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "apps/web/node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "apps/web/node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/web/node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "apps/web/node_modules/lucide-react": { + "version": "1.16.0", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "apps/web/node_modules/magic-string": { + "version": "0.30.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "apps/web/node_modules/minimatch": { + "version": "10.2.5", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "apps/web/node_modules/motion": { + "version": "12.40.0", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.40.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "apps/web/node_modules/motion-dom": { + "version": "12.40.0", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "apps/web/node_modules/motion-utils": { + "version": "12.39.0", + "license": "MIT" + }, + "apps/web/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/nanoid": { + "version": "3.3.12", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "apps/web/node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/node-releases": { + "version": "2.0.46", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "apps/web/node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "apps/web/node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/web/node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/web/node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "apps/web/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "apps/web/node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "apps/web/node_modules/picomatch": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "apps/web/node_modules/playwright": { + "version": "1.60.0", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "apps/web/node_modules/playwright-core": { + "version": "1.60.0", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "apps/web/node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "apps/web/node_modules/postcss": { + "version": "8.5.15", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "apps/web/node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "apps/web/node_modules/prettier": { + "version": "3.8.3", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "apps/web/node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "apps/web/node_modules/react-is": { + "version": "19.2.6", + "license": "MIT", + "peer": true + }, + "apps/web/node_modules/react-redux": { + "version": "9.3.0", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "apps/web/node_modules/react-router": { + "version": "7.15.1", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "apps/web/node_modules/react-router-dom": { + "version": "7.15.1", + "license": "MIT", + "dependencies": { + "react-router": "7.15.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "apps/web/node_modules/recharts": { + "version": "3.8.1", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "apps/web/node_modules/redux": { + "version": "5.0.1", + "license": "MIT" + }, + "apps/web/node_modules/redux-thunk": { + "version": "3.1.0", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "apps/web/node_modules/reselect": { + "version": "5.1.1", + "license": "MIT" + }, + "apps/web/node_modules/rolldown": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.132.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" + } + }, + "apps/web/node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "apps/web/node_modules/set-cookie-parser": { + "version": "2.7.2", + "license": "MIT" + }, + "apps/web/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "apps/web/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "apps/web/node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "apps/web/node_modules/tailwind-merge": { + "version": "3.6.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "apps/web/node_modules/tailwindcss": { + "version": "4.3.0", + "dev": true, + "license": "MIT" + }, + "apps/web/node_modules/tapable": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "apps/web/node_modules/tiny-invariant": { + "version": "1.3.3", + "license": "MIT" + }, + "apps/web/node_modules/tinyglobby": { + "version": "0.2.16", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "apps/web/node_modules/ts-api-utils": { + "version": "2.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "apps/web/node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "apps/web/node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "apps/web/node_modules/typescript": { + "version": "5.9.3", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "apps/web/node_modules/typescript-eslint": { + "version": "8.60.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.60.0", + "@typescript-eslint/parser": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/utils": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "apps/web/node_modules/update-browserslist-db": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "apps/web/node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "apps/web/node_modules/use-sync-external-store": { + "version": "1.6.0", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "apps/web/node_modules/victory-vendor": { + "version": "37.3.6", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "apps/web/node_modules/vite": { + "version": "8.0.14", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.2", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "apps/web/node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "apps/web/node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "apps/web/node_modules/yallist": { + "version": "3.1.1", + "dev": true, + "license": "ISC" + }, + "apps/web/node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/web/node_modules/zod": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "apps/web/node_modules/zod-validation-error": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "apps/web/node_modules/zustand": { + "version": "5.0.13", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, + "node_modules/@cleat/contracts": { + "resolved": "packages/contracts", + "link": true + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@types/bun": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.14.tgz", + "integrity": "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.3.14" + } + }, + "node_modules/@types/node": { + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/bun-types": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.14.tgz", + "integrity": "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cleat-web": { + "resolved": "apps/web", + "link": true + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-loader-spinner": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-8.0.2.tgz", + "integrity": "sha512-N2gHEy88VKRaijzEFoRp/aHEPE6Lr6upkmPBPu9hfqZqdxftxsXd1B+lzOmVnn3BYHyXwvYrifWzMbR64lwQlw==", + "license": "MIT", + "dependencies": { + "styled-components": "^6.3.6", + "tinycolor2": "^1.6.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": ">=17.0.0 <20.0.0", + "react-dom": ">=17.0.0 <20.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/styled-components": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.4.2.tgz", + "integrity": "sha512-xZBhBJsMtGqb+aKcwKgaT+BtuFums9VynX2JRvXJGTx5UfZzN12rk5r4nVdhXYvRw+hE7yiYxVrOqJZaK2+Txg==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.4.0", + "css-to-react-native": "3.2.0", + "csstype": "3.2.3", + "stylis": "4.3.6" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "css-to-react-native": ">= 3.2.0", + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0", + "react-native": ">= 0.68.0" + }, + "peerDependenciesMeta": { + "css-to-react-native": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "packages/contracts": { + "name": "@cleat/contracts", + "version": "0.1.0" + } + } +} From f81ba973f6797fc5edf66bba550bef91f3c0a8b6 Mon Sep 17 00:00:00 2001 From: Gunel Shukurova Date: Thu, 18 Jun 2026 14:40:19 +0400 Subject: [PATCH 3/6] fix: stabilize dataset loading and mock api fallback --- apps/web/package.json | 1 - apps/web/src/components/layout/Sidebar.tsx | 16 ++----- apps/web/src/features/access/AccessPage.tsx | 28 ++++++++++-- .../src/features/artifacts/ArtifactsPage.tsx | 19 +++++++- .../dependencies/DependenciesPage.tsx | 31 ++++++++++--- .../src/features/overview/OverviewPage.tsx | 26 +++++++++-- .../features/repositories/RepoDetailPage.tsx | 31 +++++++++++-- .../repositories/RepositoriesPage.tsx | 29 +++++++++--- .../features/security/CodeScanningPage.tsx | 29 +++++++++--- .../web/src/features/security/SecretsPage.tsx | 27 ++++++++--- .../features/security/VulnerabilitiesPage.tsx | 28 +++++++++--- .../features/supplyChain/SupplyChainPage.tsx | 26 ++++++++--- apps/web/src/hooks/useDataset.ts | 36 ++++++++++++--- apps/web/src/hooks/useNotifications.ts | 2 +- apps/web/src/lib/api/dataset.ts | 45 +++++++++++++------ 15 files changed, 291 insertions(+), 83 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 2d8b0d9..a90bf93 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -33,7 +33,6 @@ "playwright": "^1.60.0", "react": "^19.2.6", "react-dom": "^19.2.6", - "react-loader-spinner": "^8.0.2", "react-router-dom": "^7.15.1", "recharts": "^3.8.1", "tailwind-merge": "^3.6.0", diff --git a/apps/web/src/components/layout/Sidebar.tsx b/apps/web/src/components/layout/Sidebar.tsx index 15c47f7..5b65f25 100644 --- a/apps/web/src/components/layout/Sidebar.tsx +++ b/apps/web/src/components/layout/Sidebar.tsx @@ -10,25 +10,17 @@ import { useUiStore } from "@/stores/useUiStore"; import { useDataset } from "@/hooks/useDataset"; import { useNotifications } from "@/hooks/useNotifications"; import { sidebarCounts } from "@/data/metrics"; -import { TailSpin } from "react-loader-spinner"; export function Sidebar({ forceExpanded = false }: { forceExpanded?: boolean }) { const storeCollapsed = useUiStore((s) => s.sidebarCollapsed); const toggle = useUiStore((s) => s.toggleSidebar); const collapsed = forceExpanded ? false : storeCollapsed; - const ds = useDataset(); + const { data: ds } = useDataset(); const { unread } = useNotifications(); - if (!ds) { - return ( -
-
- -
-
- ); - } - const base = sidebarCounts(ds); + + const base = ds ? sidebarCounts(ds) : ({} as ReturnType); + const counts: Record = { ...base, artifactsReclaimable: 0, diff --git a/apps/web/src/features/access/AccessPage.tsx b/apps/web/src/features/access/AccessPage.tsx index 781f58b..db06f52 100644 --- a/apps/web/src/features/access/AccessPage.tsx +++ b/apps/web/src/features/access/AccessPage.tsx @@ -11,18 +11,38 @@ import { WebhooksTab } from "./WebhooksTab"; import { KeysTab } from "./KeysTab"; import { TokensTab } from "./TokensTab"; import { AuditLogTab } from "./AuditLogTab"; -import { TailSpin } from "react-loader-spinner"; type Tab = "members" | "apps" | "webhooks" | "keys" | "tokens" | "audit"; export function AccessPage() { - const ds = useDataset(); + const { data: ds, error, loading } = useDataset(); const [tab, setTab] = useState("members"); - if (!ds) { + if (loading) { return (
- +
+
+ ); + } + + if (error) { + return ( +
+ Failed to load access data. +
+ ); + } + + if (!ds) { + return ( +
+
+

No access data available

+
); } diff --git a/apps/web/src/features/artifacts/ArtifactsPage.tsx b/apps/web/src/features/artifacts/ArtifactsPage.tsx index 6ac7d1d..6e09a4b 100644 --- a/apps/web/src/features/artifacts/ArtifactsPage.tsx +++ b/apps/web/src/features/artifacts/ArtifactsPage.tsx @@ -32,7 +32,7 @@ function isReclaimable(a: Artifact) { } export function ArtifactsPage() { - const ds = useDataset(); + const { data: ds, error, loading } = useDataset(); const addToast = useUiStore((s) => s.addToast); const [tab, setTab] = useState("artifacts"); const [deleted, setDeleted] = useState>(new Set()); @@ -45,7 +45,7 @@ export function ArtifactsPage() { const caches = useMemo(() => ds?.caches.filter((c) => !deleted.has(c.id)) ?? [], [ds, deleted]); const packages = ds?.packages ?? []; - if (!ds) { + if (loading) { return (
@@ -64,6 +64,21 @@ export function ArtifactsPage() { ); } + if (error) { + return ( +
+ Failed to load artifacts data. +
+ ); + } + + if (!ds) { + return ( +
+ No dataset available. +
+ ); + } const totalStorageMb = artifacts.reduce((s, a) => s + a.sizeMb, 0) + caches.reduce((s, c) => s + c.sizeMb, 0) + diff --git a/apps/web/src/features/dependencies/DependenciesPage.tsx b/apps/web/src/features/dependencies/DependenciesPage.tsx index 4a603ee..44f8d72 100644 --- a/apps/web/src/features/dependencies/DependenciesPage.tsx +++ b/apps/web/src/features/dependencies/DependenciesPage.tsx @@ -24,11 +24,11 @@ import { ecosystem } from "@/lib/ecosystems"; import { pluralize } from "@/lib/format"; import { useUiStore } from "@/stores/useUiStore"; import { cn } from "@/lib/cn"; -import { TailSpin } from "react-loader-spinner"; + const TABLE = "dependencies"; export function DependenciesPage() { - const ds = useDataset(); + const { data: ds, error, loading } = useDataset(); const addToast = useUiStore((s) => s.addToast); const [format, setFormat] = useState("spdx"); const deps = useMemo(() => (ds ? buildDependencies(ds) : []), [ds]); @@ -67,16 +67,33 @@ export function DependenciesPage() { facets, }); - if (!ds) { + if (loading) { return ( -
-
- -
+
+
); } + if (error) { + return ( +
+ Failed to load dependencies. +
+ ); + } + + if (!ds) { + return ( +
+ + No dependency data found. +
+ ); + } const vulnerable = deps.filter((d) => d.vulnerable).length; const outdated = deps.filter((d) => d.outdated).length; const copyleft = deps.filter((d) => COPYLEFT.has(d.license)).length; diff --git a/apps/web/src/features/overview/OverviewPage.tsx b/apps/web/src/features/overview/OverviewPage.tsx index f1f5ea6..1282097 100644 --- a/apps/web/src/features/overview/OverviewPage.tsx +++ b/apps/web/src/features/overview/OverviewPage.tsx @@ -29,14 +29,32 @@ import { scoreToGrade, GRADE_COLOR, SEVERITY } from "@/lib/severity"; import { currency, fromMb, relativeTime, pluralize, percent } from "@/lib/format"; import { eventIcon } from "@/features/notifications/eventMeta"; import { cn } from "@/lib/cn"; -import { TailSpin } from "react-loader-spinner"; export function OverviewPage() { - const ds = useDataset(); + const { data: ds, error, loading } = useDataset(); + + if (loading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+ Failed to load overview data. +
+ ); + } if (!ds) { return ( -
- +
+ No account selected.
); } diff --git a/apps/web/src/features/repositories/RepoDetailPage.tsx b/apps/web/src/features/repositories/RepoDetailPage.tsx index 6cd255c..444b9ef 100644 --- a/apps/web/src/features/repositories/RepoDetailPage.tsx +++ b/apps/web/src/features/repositories/RepoDetailPage.tsx @@ -34,7 +34,6 @@ import { languageColor } from "@/lib/ecosystems"; import { relativeTime, compactNumber, fromMb } from "@/lib/format"; import { cn } from "@/lib/cn"; import type { Visibility, ScorecardCheck } from "@cleat/contracts"; -import { TailSpin } from "react-loader-spinner"; const VIS: Record = { private: { icon: Lock, label: "Private" }, @@ -51,11 +50,35 @@ function scoreHex(score: number) { export function RepoDetailPage() { const { repoId } = useParams(); - const ds = useDataset(); + const { data: ds, error, loading } = useDataset(); + if (loading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+ Failed to load repository data. +
+ ); + } + if (!ds) { return ( -
- +
+
+

No repository data available

+

+ Select an account to view repository details. +

+
); } diff --git a/apps/web/src/features/repositories/RepositoriesPage.tsx b/apps/web/src/features/repositories/RepositoriesPage.tsx index 8a4bdbb..02e7ba7 100644 --- a/apps/web/src/features/repositories/RepositoriesPage.tsx +++ b/apps/web/src/features/repositories/RepositoriesPage.tsx @@ -21,7 +21,6 @@ import { languageColor } from "@/lib/ecosystems"; import { relativeTime, compactNumber, fromMb } from "@/lib/format"; import { cn } from "@/lib/cn"; import type { Repo, Visibility } from "@cleat/contracts"; -import { TailSpin } from "react-loader-spinner"; const TABLE = "repositories"; @@ -32,8 +31,9 @@ const VIS_ICON: Record = { }; export function RepositoriesPage() { - const ds = useDataset(); + const { data: ds, error, loading } = useDataset(); const navigate = useNavigate(); + const facets: FacetDef[] = [ { key: "visibility", @@ -74,16 +74,33 @@ export function RepositoriesPage() { search: (r) => `${r.name} ${r.language} ${r.topics.join(" ")}`, facets, }); + if (loading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+ Failed to load repositories data. +
+ ); + } if (!ds) { return ( -
-
- -
+
+ No data available.
); } + const protectedCount = ds.repos.filter((r) => r.branchProtected).length; const archived = ds.repos.filter((r) => r.archived).length; const avgHygiene = Math.round( diff --git a/apps/web/src/features/security/CodeScanningPage.tsx b/apps/web/src/features/security/CodeScanningPage.tsx index 4e58a38..cfd7cc9 100644 --- a/apps/web/src/features/security/CodeScanningPage.tsx +++ b/apps/web/src/features/security/CodeScanningPage.tsx @@ -9,7 +9,6 @@ import { useFilteredRows, type FacetDef } from "@/hooks/useFilteredRows"; import { SEVERITY } from "@/lib/severity"; import { relativeTime } from "@/lib/format"; import type { CodeScanAlert } from "@cleat/contracts"; -import { TailSpin } from "react-loader-spinner"; const TABLE = "code-scanning"; @@ -23,7 +22,7 @@ const STATUS_META: Record< }; export function CodeScanningPage() { - const ds = useDataset(); + const { data: ds, error, loading } = useDataset(); const facets: FacetDef[] = [ { @@ -61,15 +60,33 @@ export function CodeScanningPage() { facets, }); + if (loading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+ Failed to load code scanning data. +
+ ); + } + if (!ds) { return ( -
-
- -
+
+ No data available.
); } + const open = ds.codeAlerts.filter((a) => a.status === "open").length; const fixed = ds.codeAlerts.filter((a) => a.status === "fixed").length; const dismissed = ds.codeAlerts.filter((a) => a.status === "dismissed").length; diff --git a/apps/web/src/features/security/SecretsPage.tsx b/apps/web/src/features/security/SecretsPage.tsx index ba6ff29..a6897eb 100644 --- a/apps/web/src/features/security/SecretsPage.tsx +++ b/apps/web/src/features/security/SecretsPage.tsx @@ -19,7 +19,7 @@ import { TailSpin } from "react-loader-spinner"; const TABLE = "secrets"; export function SecretsPage() { - const ds = useDataset(); + const { data: ds, error, loading } = useDataset(); const [selected, setSelected] = useState(null); const facets: FacetDef[] = [ @@ -58,12 +58,29 @@ export function SecretsPage() { facets, }); + if (loading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+ Failed to load secrets data. +
+ ); + } + if (!ds) { return ( -
-
- -
+
+ No data available.
); } diff --git a/apps/web/src/features/security/VulnerabilitiesPage.tsx b/apps/web/src/features/security/VulnerabilitiesPage.tsx index 835a731..8e23a9a 100644 --- a/apps/web/src/features/security/VulnerabilitiesPage.tsx +++ b/apps/web/src/features/security/VulnerabilitiesPage.tsx @@ -16,12 +16,11 @@ import { SEVERITY, cvssToSeverity } from "@/lib/severity"; import { percent, pluralize } from "@/lib/format"; import { cn } from "@/lib/cn"; import type { Vulnerability } from "@cleat/contracts"; -import { TailSpin } from "react-loader-spinner"; const TABLE = "vulnerabilities"; export function VulnerabilitiesPage() { - const ds = useDataset(); + const { data: ds, error, loading } = useDataset(); const [selected, setSelected] = useState(null); const facets: FacetDef[] = [ @@ -73,12 +72,29 @@ export function VulnerabilitiesPage() { [filtered], ); + if (loading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+ Failed to load vulnerability data. +
+ ); + } + if (!ds) { return ( -
-
- -
+
+ No vulnerability data available.
); } diff --git a/apps/web/src/features/supplyChain/SupplyChainPage.tsx b/apps/web/src/features/supplyChain/SupplyChainPage.tsx index 0608b2e..c277388 100644 --- a/apps/web/src/features/supplyChain/SupplyChainPage.tsx +++ b/apps/web/src/features/supplyChain/SupplyChainPage.tsx @@ -21,7 +21,6 @@ import { unpinnedActionCount } from "@/data/metrics"; import { percent } from "@/lib/format"; import { useUiStore } from "@/stores/useUiStore"; import type { WorkflowAudit, SupplyChainIncident } from "@cleat/contracts"; -import { TailSpin } from "react-loader-spinner"; const TABLE = "workflows"; @@ -33,7 +32,7 @@ function riskTone(score: number) { } export function SupplyChainPage() { - const ds = useDataset(); + const { data: ds, error, loading } = useDataset(); const [selected, setSelected] = useState(null); const facets: FacetDef[] = [ @@ -70,17 +69,34 @@ export function SupplyChainPage() { search: (r) => `${r.repo} ${r.workflow} ${r.actions.map((a) => a.name).join(" ")}`, facets, }); - if (!ds) { + + if (loading) { return (
-
- +
+ Loading workflow audit...
); } + + if (error) { + return ( +
+ Failed to load supply chain data. +
+ ); + } + + if (!ds) { + return ( +
+ No workflow audit data available. +
+ ); + } const unpinned = unpinnedActionCount(ds); const broad = ds.workflows.filter((w) => w.permissions === "broad").length; const oidc = ds.workflows.length diff --git a/apps/web/src/hooks/useDataset.ts b/apps/web/src/hooks/useDataset.ts index 0999909..6d47a85 100644 --- a/apps/web/src/hooks/useDataset.ts +++ b/apps/web/src/hooks/useDataset.ts @@ -3,28 +3,52 @@ import { fetchDataset } from "@/lib/api/dataset"; import type { Dataset } from "@cleat/contracts"; import { useOrgStore } from "@/stores/useOrgStore"; -export function useDataset(): Dataset | null { +export function useDataset() { const accountId = useOrgStore((s) => s.activeAccountId); + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); useEffect(() => { let cancelled = false; if (!accountId) { setData(null); + setError(null); + setLoading(false); return; } - setData(null); + setError(null); + setLoading(true); - fetchDataset(accountId).then((d) => { - if (!cancelled) setData(d); - }); + fetchDataset(accountId) + .then((d) => { + if (!cancelled) { + setData(d); + } + }) + .catch((err) => { + if (!cancelled) { + setData(null); + setError(err instanceof Error ? err : new Error("Unexpected error")); + } + }) + .finally(() => { + if (!cancelled) { + setLoading(false); + } + }); return () => { cancelled = true; }; }, [accountId]); - return data; + return { + data, + error, + loading, + }; } diff --git a/apps/web/src/hooks/useNotifications.ts b/apps/web/src/hooks/useNotifications.ts index d089a05..223b8f3 100644 --- a/apps/web/src/hooks/useNotifications.ts +++ b/apps/web/src/hooks/useNotifications.ts @@ -9,7 +9,7 @@ export interface NotificationItem extends ActivityEvent { /** Activity events for the active account, merged with persisted read-state. */ export function useNotifications() { - const ds = useDataset(); + const { data: ds } = useDataset(); const readIds = useNotificationStore((s) => s.readIds); diff --git a/apps/web/src/lib/api/dataset.ts b/apps/web/src/lib/api/dataset.ts index 3dc4b64..44732d1 100644 --- a/apps/web/src/lib/api/dataset.ts +++ b/apps/web/src/lib/api/dataset.ts @@ -1,21 +1,38 @@ -import { getDataset } from "@/data"; import type { Dataset } from "@cleat/contracts"; +import { getDataset } from "@/data"; -/** - * Mock API layer for dataset. - * Fully mimics real backend endpoint behavior. - */ -export async function fetchDataset(accountId: string): Promise { - if (import.meta.env.VITE_USE_MOCK_API === "true") { - await new Promise((r) => setTimeout(r, 50)); - return getDataset(accountId); - } +const requestCache = new Map>(); - const res = await fetch(`/api/dataset?accountId=${encodeURIComponent(accountId)}`); +export function fetchDataset(accountId: string): Promise { + const cached = requestCache.get(accountId); - if (!res.ok) { - throw new Error(`Failed to fetch dataset: ${res.status} ${res.statusText}`); + if (cached) { + return cached; } - return (await res.json()) as Dataset; + const request = (async () => { + const useMockApi = import.meta.env.VITE_USE_MOCK_API !== "false"; + + if (useMockApi) { + await new Promise((r) => setTimeout(r, 50)); + + return getDataset(accountId); + } + + const res = await fetch(`/api/dataset?accountId=${encodeURIComponent(accountId)}`); + + if (!res.ok) { + throw new Error(`Failed to fetch dataset: ${res.status} ${res.statusText}`); + } + + return (await res.json()) as Dataset; + })(); + + requestCache.set(accountId, request); + + request.catch(() => { + requestCache.delete(accountId); + }); + + return request; } From 920d37b4c575035cdff23cc78e280306df6aaf1c Mon Sep 17 00:00:00 2001 From: Gunel Shukurova Date: Thu, 18 Jun 2026 14:42:49 +0400 Subject: [PATCH 4/6] chore: bump web version to 1.7.1 --- apps/web/package.json | 2 +- bun.lock | 17 ----------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index a90bf93..e82e874 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -48,5 +48,5 @@ "lint:fix": "eslint . --fix", "format:check": "prettier --check ." }, - "version": "1.7.0" + "version": "1.7.1" } diff --git a/bun.lock b/bun.lock index 762aa71..68af86b 100644 --- a/bun.lock +++ b/bun.lock @@ -18,7 +18,6 @@ "playwright": "^1.60.0", "react": "^19.2.6", "react-dom": "^19.2.6", - "react-loader-spinner": "^8.0.2", "react-router-dom": "^7.15.1", "recharts": "^3.8.1", "tailwind-merge": "^3.6.0", @@ -281,8 +280,6 @@ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], - "camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001797", "", {}, "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w=="], "cleat-web": ["cleat-web@workspace:apps/web"], @@ -295,10 +292,6 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="], - - "css-to-react-native": ["css-to-react-native@3.2.0", "", { "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^4.0.2" } }, "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ=="], - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], @@ -499,8 +492,6 @@ "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], - "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "prettier": ["prettier@3.8.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q=="], @@ -513,8 +504,6 @@ "react-is": ["react-is@19.2.7", "", {}, "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A=="], - "react-loader-spinner": ["react-loader-spinner@8.0.2", "", { "dependencies": { "styled-components": "^6.3.6", "tinycolor2": "^1.6.0" }, "peerDependencies": { "react": ">=17.0.0 <20.0.0", "react-dom": ">=17.0.0 <20.0.0" } }, "sha512-N2gHEy88VKRaijzEFoRp/aHEPE6Lr6upkmPBPu9hfqZqdxftxsXd1B+lzOmVnn3BYHyXwvYrifWzMbR64lwQlw=="], - "react-redux": ["react-redux@9.3.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g=="], "react-router": ["react-router@7.17.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ=="], @@ -543,10 +532,6 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "styled-components": ["styled-components@6.4.2", "", { "dependencies": { "@emotion/is-prop-valid": "1.4.0", "css-to-react-native": "3.2.0", "csstype": "3.2.3", "stylis": "4.3.6" }, "peerDependencies": { "react": ">= 16.8.0", "react-dom": ">= 16.8.0", "react-native": ">= 0.68.0" }, "optionalPeers": ["react-dom", "react-native"] }, "sha512-xZBhBJsMtGqb+aKcwKgaT+BtuFums9VynX2JRvXJGTx5UfZzN12rk5r4nVdhXYvRw+hE7yiYxVrOqJZaK2+Txg=="], - - "stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], - "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], @@ -555,8 +540,6 @@ "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], - "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], - "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], From cd7daa012d1338466819390ccbf3e5e5686659b8 Mon Sep 17 00:00:00 2001 From: Gunel Shukurova Date: Thu, 18 Jun 2026 15:05:09 +0400 Subject: [PATCH 5/6] remove react-loader-spinner dependency --- apps/web/src/features/security/SecretsPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/src/features/security/SecretsPage.tsx b/apps/web/src/features/security/SecretsPage.tsx index a6897eb..789590b 100644 --- a/apps/web/src/features/security/SecretsPage.tsx +++ b/apps/web/src/features/security/SecretsPage.tsx @@ -14,7 +14,6 @@ import { provider } from "@/lib/ecosystems"; import { SEVERITY } from "@/lib/severity"; import { relativeTime } from "@/lib/format"; import type { SecretFinding } from "@cleat/contracts"; -import { TailSpin } from "react-loader-spinner"; const TABLE = "secrets"; From 08d7b40df74e6044f1ecded9afc6e1bd1cb78d16 Mon Sep 17 00:00:00 2001 From: Gunel Shukurova Date: Thu, 18 Jun 2026 17:12:04 +0400 Subject: [PATCH 6/6] fix dataset loading error handling and retry flow --- apps/web/.env.example | 1 - apps/web/src/features/access/AccessPage.tsx | 13 +++++++++---- .../web/src/features/artifacts/ArtifactsPage.tsx | 13 +++++++++---- .../features/dependencies/DependenciesPage.tsx | 13 +++++++++---- apps/web/src/features/overview/OverviewPage.tsx | 14 +++++++++++--- .../src/features/repositories/RepoDetailPage.tsx | 16 ++++++++++++++-- .../features/repositories/RepositoriesPage.tsx | 13 +++++++++---- .../src/features/security/CodeScanningPage.tsx | 13 +++++++++---- apps/web/src/features/security/SecretsPage.tsx | 13 +++++++++---- .../features/security/VulnerabilitiesPage.tsx | 13 +++++++++---- .../src/features/supplyChain/SupplyChainPage.tsx | 13 +++++++++---- apps/web/src/hooks/useDataset.ts | 4 +++- 12 files changed, 100 insertions(+), 39 deletions(-) delete mode 100644 apps/web/.env.example diff --git a/apps/web/.env.example b/apps/web/.env.example deleted file mode 100644 index 35d343a..0000000 --- a/apps/web/.env.example +++ /dev/null @@ -1 +0,0 @@ -VITE_USE_MOCK_API=true \ No newline at end of file diff --git a/apps/web/src/features/access/AccessPage.tsx b/apps/web/src/features/access/AccessPage.tsx index db06f52..346989f 100644 --- a/apps/web/src/features/access/AccessPage.tsx +++ b/apps/web/src/features/access/AccessPage.tsx @@ -15,7 +15,7 @@ import { AuditLogTab } from "./AuditLogTab"; type Tab = "members" | "apps" | "webhooks" | "keys" | "tokens" | "audit"; export function AccessPage() { - const { data: ds, error, loading } = useDataset(); + const { data: ds, error, loading, retry } = useDataset(); const [tab, setTab] = useState("members"); if (loading) { @@ -31,12 +31,17 @@ export function AccessPage() { if (error) { return ( -
- Failed to load access data. +
+

Failed to load access data.

+
); } - if (!ds) { return (
diff --git a/apps/web/src/features/artifacts/ArtifactsPage.tsx b/apps/web/src/features/artifacts/ArtifactsPage.tsx index 6e09a4b..4b407b5 100644 --- a/apps/web/src/features/artifacts/ArtifactsPage.tsx +++ b/apps/web/src/features/artifacts/ArtifactsPage.tsx @@ -32,7 +32,7 @@ function isReclaimable(a: Artifact) { } export function ArtifactsPage() { - const { data: ds, error, loading } = useDataset(); + const { data: ds, error, loading, retry } = useDataset(); const addToast = useUiStore((s) => s.addToast); const [tab, setTab] = useState("artifacts"); const [deleted, setDeleted] = useState>(new Set()); @@ -66,12 +66,17 @@ export function ArtifactsPage() { if (error) { return ( -
- Failed to load artifacts data. +
+

Failed to load artifacts data.

+
); } - if (!ds) { return (
diff --git a/apps/web/src/features/dependencies/DependenciesPage.tsx b/apps/web/src/features/dependencies/DependenciesPage.tsx index 44f8d72..90f6d40 100644 --- a/apps/web/src/features/dependencies/DependenciesPage.tsx +++ b/apps/web/src/features/dependencies/DependenciesPage.tsx @@ -28,7 +28,7 @@ import { cn } from "@/lib/cn"; const TABLE = "dependencies"; export function DependenciesPage() { - const { data: ds, error, loading } = useDataset(); + const { data: ds, error, loading, retry } = useDataset(); const addToast = useUiStore((s) => s.addToast); const [format, setFormat] = useState("spdx"); const deps = useMemo(() => (ds ? buildDependencies(ds) : []), [ds]); @@ -80,12 +80,17 @@ export function DependenciesPage() { if (error) { return ( -
- Failed to load dependencies. +
+

Failed to load dependencies.

+
); } - if (!ds) { return (
diff --git a/apps/web/src/features/overview/OverviewPage.tsx b/apps/web/src/features/overview/OverviewPage.tsx index 1282097..3375357 100644 --- a/apps/web/src/features/overview/OverviewPage.tsx +++ b/apps/web/src/features/overview/OverviewPage.tsx @@ -31,7 +31,7 @@ import { eventIcon } from "@/features/notifications/eventMeta"; import { cn } from "@/lib/cn"; export function OverviewPage() { - const { data: ds, error, loading } = useDataset(); + const { data: ds, error, loading, retry } = useDataset(); if (loading) { return ( @@ -46,8 +46,16 @@ export function OverviewPage() { if (error) { return ( -
- Failed to load overview data. +
+

Failed to load overview data.

+
); } diff --git a/apps/web/src/features/repositories/RepoDetailPage.tsx b/apps/web/src/features/repositories/RepoDetailPage.tsx index 444b9ef..504929c 100644 --- a/apps/web/src/features/repositories/RepoDetailPage.tsx +++ b/apps/web/src/features/repositories/RepoDetailPage.tsx @@ -50,7 +50,7 @@ function scoreHex(score: number) { export function RepoDetailPage() { const { repoId } = useParams(); - const { data: ds, error, loading } = useDataset(); + const { data: ds, error, loading, retry } = useDataset(); if (loading) { return (
@@ -69,7 +69,19 @@ export function RepoDetailPage() {
); } - + if (error) { + return ( +
+

Failed to load repository data.

+ +
+ ); + } if (!ds) { return (
diff --git a/apps/web/src/features/repositories/RepositoriesPage.tsx b/apps/web/src/features/repositories/RepositoriesPage.tsx index 02e7ba7..2007be7 100644 --- a/apps/web/src/features/repositories/RepositoriesPage.tsx +++ b/apps/web/src/features/repositories/RepositoriesPage.tsx @@ -31,7 +31,7 @@ const VIS_ICON: Record = { }; export function RepositoriesPage() { - const { data: ds, error, loading } = useDataset(); + const { data: ds, error, loading, retry } = useDataset(); const navigate = useNavigate(); const facets: FacetDef[] = [ @@ -87,12 +87,17 @@ export function RepositoriesPage() { if (error) { return ( -
- Failed to load repositories data. +
+

Failed to load repositories data.

+
); } - if (!ds) { return (
diff --git a/apps/web/src/features/security/CodeScanningPage.tsx b/apps/web/src/features/security/CodeScanningPage.tsx index cfd7cc9..688d1af 100644 --- a/apps/web/src/features/security/CodeScanningPage.tsx +++ b/apps/web/src/features/security/CodeScanningPage.tsx @@ -22,7 +22,7 @@ const STATUS_META: Record< }; export function CodeScanningPage() { - const { data: ds, error, loading } = useDataset(); + const { data: ds, error, loading, retry } = useDataset(); const facets: FacetDef[] = [ { @@ -73,12 +73,17 @@ export function CodeScanningPage() { if (error) { return ( -
- Failed to load code scanning data. +
+

Failed to load code scanning data.

+
); } - if (!ds) { return (
diff --git a/apps/web/src/features/security/SecretsPage.tsx b/apps/web/src/features/security/SecretsPage.tsx index 789590b..5fc1245 100644 --- a/apps/web/src/features/security/SecretsPage.tsx +++ b/apps/web/src/features/security/SecretsPage.tsx @@ -18,7 +18,7 @@ import type { SecretFinding } from "@cleat/contracts"; const TABLE = "secrets"; export function SecretsPage() { - const { data: ds, error, loading } = useDataset(); + const { data: ds, error, loading, retry } = useDataset(); const [selected, setSelected] = useState(null); const facets: FacetDef[] = [ @@ -70,12 +70,17 @@ export function SecretsPage() { if (error) { return ( -
- Failed to load secrets data. +
+

Failed to load secrets data.

+
); } - if (!ds) { return (
diff --git a/apps/web/src/features/security/VulnerabilitiesPage.tsx b/apps/web/src/features/security/VulnerabilitiesPage.tsx index 8e23a9a..c582853 100644 --- a/apps/web/src/features/security/VulnerabilitiesPage.tsx +++ b/apps/web/src/features/security/VulnerabilitiesPage.tsx @@ -20,7 +20,7 @@ import type { Vulnerability } from "@cleat/contracts"; const TABLE = "vulnerabilities"; export function VulnerabilitiesPage() { - const { data: ds, error, loading } = useDataset(); + const { data: ds, error, loading, retry } = useDataset(); const [selected, setSelected] = useState(null); const facets: FacetDef[] = [ @@ -85,12 +85,17 @@ export function VulnerabilitiesPage() { if (error) { return ( -
- Failed to load vulnerability data. +
+

Failed to load vulnerability data.

+
); } - if (!ds) { return (
diff --git a/apps/web/src/features/supplyChain/SupplyChainPage.tsx b/apps/web/src/features/supplyChain/SupplyChainPage.tsx index c277388..5acf21e 100644 --- a/apps/web/src/features/supplyChain/SupplyChainPage.tsx +++ b/apps/web/src/features/supplyChain/SupplyChainPage.tsx @@ -32,7 +32,7 @@ function riskTone(score: number) { } export function SupplyChainPage() { - const { data: ds, error, loading } = useDataset(); + const { data: ds, error, loading, retry } = useDataset(); const [selected, setSelected] = useState(null); const facets: FacetDef[] = [ @@ -84,12 +84,17 @@ export function SupplyChainPage() { if (error) { return ( -
- Failed to load supply chain data. +
+

Failed to load supply chain data.

+
); } - if (!ds) { return (
diff --git a/apps/web/src/hooks/useDataset.ts b/apps/web/src/hooks/useDataset.ts index 6d47a85..7a659eb 100644 --- a/apps/web/src/hooks/useDataset.ts +++ b/apps/web/src/hooks/useDataset.ts @@ -9,6 +9,7 @@ export function useDataset() { const [data, setData] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); + const [retry, setRetry] = useState(0); useEffect(() => { let cancelled = false; @@ -44,11 +45,12 @@ export function useDataset() { return () => { cancelled = true; }; - }, [accountId]); + }, [accountId, retry]); return { data, error, loading, + retry: () => setRetry((v) => v + 1), }; }