|
1 | 1 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; |
2 | 2 | import { invoke } from "@tauri-apps/api/core"; |
| 3 | +import { getCurrentWindow } from "@tauri-apps/api/window"; |
3 | 4 | import { useState, useEffect, useRef, useCallback } from "react"; |
| 5 | +import { Link } from "react-router-dom"; |
4 | 6 | import { toast } from "sonner"; |
| 7 | +import { AlertTriangle } from "lucide-react"; |
5 | 8 | import type { Finding } from "../types/finding"; |
6 | 9 | import type { AuditTokenUsage, PersistenceEntry, Snapshot } from "../types/snapshot"; |
7 | 10 | import type { ProviderConfig } from "../types/provider"; |
@@ -103,6 +106,43 @@ export default function Dashboard() { |
103 | 106 | .catch(() => {}); |
104 | 107 | }, []); |
105 | 108 |
|
| 109 | + const [providerReady, setProviderReady] = useState<{ |
| 110 | + ready: boolean; |
| 111 | + reason: string | null; |
| 112 | + active_provider: string; |
| 113 | + } | null>(null); |
| 114 | + |
| 115 | + function checkProviderReady() { |
| 116 | + invoke<{ ready: boolean; reason: string | null; active_provider: string }>( |
| 117 | + "is_provider_ready" |
| 118 | + ) |
| 119 | + .then(setProviderReady) |
| 120 | + .catch(() => {}); |
| 121 | + } |
| 122 | + |
| 123 | + useEffect(() => { |
| 124 | + checkProviderReady(); |
| 125 | + }, []); |
| 126 | + |
| 127 | + // Re-check readiness whenever the window regains focus (user may have updated Settings). |
| 128 | + useEffect(() => { |
| 129 | + let unlisten: (() => void) | null = null; |
| 130 | + let cancelled = false; |
| 131 | + getCurrentWindow() |
| 132 | + .onFocusChanged(({ payload: focused }) => { |
| 133 | + if (focused && !cancelled) checkProviderReady(); |
| 134 | + }) |
| 135 | + .then((fn) => { |
| 136 | + if (cancelled) fn(); |
| 137 | + else unlisten = fn; |
| 138 | + }) |
| 139 | + .catch(() => {}); |
| 140 | + return () => { |
| 141 | + cancelled = true; |
| 142 | + unlisten?.(); |
| 143 | + }; |
| 144 | + }, []); |
| 145 | + |
106 | 146 | const latestIdQuery = useQuery<number | null>({ |
107 | 147 | queryKey: ["latest_snapshot_id"], |
108 | 148 | queryFn: () => invoke<number | null>("latest_snapshot_id"), |
@@ -187,11 +227,19 @@ export default function Dashboard() { |
187 | 227 |
|
188 | 228 | const hasRunAutoSnapshot = useRef(false); |
189 | 229 | useEffect(() => { |
190 | | - if (!hasRunAutoSnapshot.current && sessionStorage.getItem("mscope_auto_snapshot") === "1") { |
191 | | - hasRunAutoSnapshot.current = true; |
192 | | - sessionStorage.removeItem("mscope_auto_snapshot"); |
193 | | - runFullScan.mutate(); |
194 | | - } |
| 230 | + if (hasRunAutoSnapshot.current) return; |
| 231 | + const flag = sessionStorage.getItem("mscope_auto_snapshot"); |
| 232 | + sessionStorage.removeItem("mscope_auto_snapshot"); |
| 233 | + if (!flag) return; |
| 234 | + const ts = parseInt(flag, 10); |
| 235 | + if (Number.isNaN(ts) || Date.now() - ts > 5000) return; |
| 236 | + invoke<{ ready: boolean }>("is_provider_ready") |
| 237 | + .then(({ ready }) => { |
| 238 | + if (!ready) return; |
| 239 | + hasRunAutoSnapshot.current = true; |
| 240 | + runFullScan.mutate(); |
| 241 | + }) |
| 242 | + .catch(() => {}); |
195 | 243 | // eslint-disable-next-line react-hooks/exhaustive-deps |
196 | 244 | }, []); |
197 | 245 |
|
@@ -339,9 +387,61 @@ export default function Dashboard() { |
339 | 387 | activeSnapshotId={activeSnapshotId} |
340 | 388 | findingCount={findings?.length ?? null} |
341 | 389 | isAnalyzing={isAnalyzing} |
| 390 | + snapshotBlocked={providerReady !== null && !providerReady.ready} |
342 | 391 | onTakeSnapshot={() => runFullScan.mutate()} |
343 | 392 | onReAnalyze={() => reAnalyze.mutate()} |
344 | 393 | /> |
| 394 | + {providerReady !== null && !providerReady.ready && ( |
| 395 | + <div |
| 396 | + style={{ |
| 397 | + display: "flex", |
| 398 | + alignItems: "center", |
| 399 | + gap: "10px", |
| 400 | + padding: "8px 20px", |
| 401 | + background: "var(--color-severity-medium-bg)", |
| 402 | + borderBottom: "1px solid var(--color-border-divider)", |
| 403 | + flexShrink: 0, |
| 404 | + }} |
| 405 | + > |
| 406 | + <AlertTriangle size={14} style={{ color: "var(--color-severity-medium-fg)", flexShrink: 0 }} /> |
| 407 | + <span |
| 408 | + style={{ |
| 409 | + flex: 1, |
| 410 | + fontSize: "var(--text-xs)", |
| 411 | + fontFamily: "var(--font-mono)", |
| 412 | + color: "var(--color-severity-medium-fg)", |
| 413 | + }} |
| 414 | + > |
| 415 | + {providerReady.reason} |
| 416 | + <span |
| 417 | + style={{ |
| 418 | + color: "var(--color-text-muted)", |
| 419 | + marginLeft: "8px", |
| 420 | + fontFamily: "var(--font-sans)", |
| 421 | + }} |
| 422 | + > |
| 423 | + Configure in Settings to enable analysis. |
| 424 | + </span> |
| 425 | + </span> |
| 426 | + <Link |
| 427 | + to="/settings" |
| 428 | + style={{ |
| 429 | + fontSize: "var(--text-xs)", |
| 430 | + fontFamily: "var(--font-sans)", |
| 431 | + fontWeight: 500, |
| 432 | + color: "var(--color-accent)", |
| 433 | + textDecoration: "none", |
| 434 | + padding: "3px 10px", |
| 435 | + background: "var(--color-accent-glow)", |
| 436 | + borderRadius: "var(--radius-sm)", |
| 437 | + border: "1px solid var(--color-accent-muted)", |
| 438 | + flexShrink: 0, |
| 439 | + }} |
| 440 | + > |
| 441 | + Configure |
| 442 | + </Link> |
| 443 | + </div> |
| 444 | + )} |
345 | 445 | <TabBar |
346 | 446 | active={active} |
347 | 447 | onChange={setActive} |
|
0 commit comments