From 6aaddc256a2735b3dfd44305b97c4d8e032aa0c2 Mon Sep 17 00:00:00 2001 From: sfun <15905889+ssfun@users.noreply.github.com> Date: Wed, 22 Jul 2026 23:04:40 +0800 Subject: [PATCH] refactor(management): decompose account inspection page --- .../account-inspection-styles/_analytics.scss | 716 ++++ .../account-inspection-styles/_base.scss | 507 +++ .../_responsive.scss | 379 ++ .../account-inspection-styles/_settings.scss | 381 ++ .../_tables-dialogs.scss | 609 +++ .../account-inspection-styles/_workbench.scss | 890 +++++ .../monitoring/accountInspection.module.scss | 6 + .../monitoring/accountInspectionPageModel.tsx | 1372 +++++++ .../pages/AccountInspectionPage.module.scss | 3487 ----------------- .../src/pages/AccountInspectionPage.tsx | 1418 +------ .../tests/accountInspectionPageModel.test.ts | 62 + ...st_account_inspection_module_boundaries.py | 33 + docs/code-review/2026-07.md | 19 +- 13 files changed, 5041 insertions(+), 4838 deletions(-) create mode 100644 cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_analytics.scss create mode 100644 cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_base.scss create mode 100644 cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_responsive.scss create mode 100644 cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_settings.scss create mode 100644 cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_tables-dialogs.scss create mode 100644 cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_workbench.scss create mode 100644 cliproxyapi-pro-management/overlay/src/features/monitoring/accountInspection.module.scss create mode 100644 cliproxyapi-pro-management/overlay/src/features/monitoring/accountInspectionPageModel.tsx delete mode 100644 cliproxyapi-pro-management/overlay/src/pages/AccountInspectionPage.module.scss create mode 100644 cliproxyapi-pro-management/overlay/tests/accountInspectionPageModel.test.ts create mode 100644 cliproxyapi-pro-management/tests/test_account_inspection_module_boundaries.py diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_analytics.scss b/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_analytics.scss new file mode 100644 index 0000000..abb40df --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_analytics.scss @@ -0,0 +1,716 @@ +.assetIntelligenceLayout, +.actionStudioLayout { + display: grid; + align-items: stretch; + gap: 16px; + min-width: 0; +} + +.assetIntelligenceLayout { + grid-template-columns: minmax(260px, 0.82fr) minmax(0, 1.18fr); +} + +.actionStudioLayout { + grid-template-columns: minmax(260px, 0.72fr) minmax(0, 1.28fr); +} + +.providerRankingPanel, +.actionMetricStack { + display: grid; + align-content: start; + gap: 14px; + min-width: 0; + padding: 16px; + border: 1px solid color-mix(in srgb, var(--inspect-line) 76%, transparent); + border-radius: 12px; + background: color-mix(in srgb, var(--inspect-surface-muted) 58%, var(--inspect-surface)); +} + +.providerRankingPanel .chartBarList { + max-height: 260px; + overflow: auto; +} + +.actionMetricStack { + background: transparent; + border: 0; + padding: 0; +} + +.summaryGrid, +.summaryGridCompact, +.riskGrid { + display: grid; + gap: 12px; +} + +.assetOverviewLayout { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(360px, 0.8fr); + gap: 16px; +} + +.assetInsightsMain, +.inspectionStatsMain { + display: grid; + gap: 12px; + min-width: 0; +} + +.assetMetricGrid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.assetMetricCard { + min-height: 112px; +} + +.assetMetricCard strong { + font-size: 26px; +} + +.chartGrid, +.inspectionStatsLayout { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + min-width: 0; +} + +.inspectionStatsLayout { + grid-template-columns: minmax(0, 1fr) minmax(340px, 0.42fr); + align-items: stretch; +} + +.chartPanel { + display: grid; + min-width: 0; + min-height: 260px; + padding: 20px; + border: 1px solid var(--inspect-line); + border-radius: 12px; + background: var(--inspect-surface); + box-shadow: var(--shadow); + overflow: visible; +} + +.chartDonutPanel { + display: grid; + gap: 16px; + min-width: 0; +} + +.chartDonutHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + min-width: 0; +} + +.chartDonutHeader strong { + font-size: 15px; + line-height: 1.25; +} + +.chartDonutHeader span { + color: var(--inspect-muted); + font-size: 12px; +} + +.chartDonutLayout { + display: grid; + grid-template-columns: minmax(128px, 0.8fr) minmax(0, 1fr); + align-items: center; + gap: 18px; + min-width: 0; +} + +.donutChart { + position: relative; + display: grid; + place-items: center; + width: min(168px, 100%); + aspect-ratio: 1; + border-radius: 50%; + background: var(--donut-bg); +} + +.donutChart::after { + content: ''; + position: absolute; + inset: 22%; + border-radius: 50%; + background: var(--inspect-surface); + box-shadow: inset 0 0 0 1px var(--inspect-line); +} + +.donutCenter { + position: relative; + z-index: 1; + display: grid; + gap: 5px; + justify-items: center; + max-width: 78%; + text-align: center; +} + +.donutCenter span { + color: var(--inspect-muted); + font-size: 12px; +} + +.donutCenter strong { + max-width: 100%; + overflow: hidden; + font-size: 18px; + font-weight: 800; + text-overflow: ellipsis; + white-space: nowrap; +} + +.donutTooltip { + position: absolute; + left: calc(100% + 14px); + top: 50%; + z-index: 5; + width: min(300px, calc(100vw - 48px)); + max-height: 260px; + overflow: auto; + padding: 12px; + border-radius: 12px; + border: 1px solid var(--inspect-line); + background: color-mix(in srgb, var(--inspect-surface-elevated) 96%, var(--inspect-bg)); + box-shadow: 0 18px 42px -26px rgba(15, 23, 42, 0.55); + opacity: 0; + pointer-events: none; + transform: translate(0, -50%); + transition: opacity 0.16s ease; +} + +.donutChart:hover .donutTooltip, +.donutChart:focus-within .donutTooltip { + opacity: 1; +} + +.donutTooltip strong { + display: block; + margin-bottom: 8px; + color: var(--inspect-ink); + font-size: 12px; +} + +.donutTooltip div { + display: grid; + gap: 7px; +} + +.donutTooltip span { + display: grid; + grid-template-columns: 8px minmax(0, 1fr) auto; + align-items: center; + gap: 8px; + min-width: 0; + color: var(--inspect-muted); + font-size: 12px; +} + +.donutTooltip i, +.chartLegendDot { + width: 8px; + height: 8px; + border-radius: 999px; +} + +.donutTooltip em { + min-width: 0; + overflow: hidden; + font-style: normal; + text-overflow: ellipsis; + white-space: nowrap; +} + +.donutTooltip b { + color: var(--inspect-ink); + font-size: 12px; +} + +.chartLegend, +.chartBarList { + display: grid; + gap: 8px; + min-width: 0; +} + +.chartLegendItem { + display: grid; + grid-template-columns: 10px minmax(0, 1fr) auto; + align-items: center; + gap: 8px; + min-width: 0; + color: var(--inspect-muted); + font-size: 12px; +} + +.chartLegendDot { + background: var(--legend-color, var(--inspect-accent)); +} + +.chartLegendItem span:nth-child(2) { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chartLegendItem strong { + color: var(--inspect-ink); + font-size: 12px; +} + +.chartBarRow { + display: grid; + gap: 8px; + min-width: 0; + padding: 10px 0; + border-bottom: 1px solid color-mix(in srgb, var(--inspect-line) 58%, transparent); +} + +.chartBarRow:last-child { + border-bottom: 0; +} + +.chartBarRow div { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-width: 0; +} + +.chartBarRow div span { + min-width: 0; + overflow: hidden; + color: var(--inspect-muted); + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chartBarRow div strong { + color: var(--inspect-ink); + font-size: 13px; +} + +.chartBarRow > span { + display: block; + height: 7px; + overflow: hidden; + border-radius: 999px; + background: color-mix(in srgb, var(--inspect-line) 58%, transparent); +} + +.chartBarRow > span::before { + content: ''; + display: block; + width: var(--bar-width, 0%); + height: 100%; + border-radius: inherit; + background: var(--bar-color, var(--inspect-accent)); +} + +.providerPanel { + display: grid; + gap: 14px; + padding: 18px; + border: 1px solid var(--inspect-line); + border-radius: 12px; + background: var(--inspect-surface); +} + +.providerPanelHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.providerPanelHeader strong { + font-size: 15px; +} + +.providerPanelHeader span { + color: var(--inspect-muted); + font-size: 12px; +} + +.providerList { + display: grid; + gap: 8px; + max-height: 260px; + overflow: auto; +} + +.providerRow { + display: grid; + grid-template-columns: minmax(120px, 0.8fr) minmax(0, 1.2fr); + gap: 12px; + padding: 12px; + border: 1px solid color-mix(in srgb, var(--inspect-line) 70%, transparent); + border-radius: 10px; + background: color-mix(in srgb, var(--inspect-surface-muted) 50%, transparent); +} + +.providerNameCell { + display: grid; + gap: 4px; +} + +.providerNameCell strong { + font-size: 14px; +} + +.providerNameCell span { + color: var(--inspect-muted); + font-size: 12px; +} + +.providerStatusGrid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 6px 10px; + align-content: center; +} + +.providerStatusGrid span { + color: var(--inspect-muted); + font-size: 12px; + line-height: 1.4; +} + +.actionBreakdownGrid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.actionBreakdownCard { + display: grid; + gap: 10px; + padding: 16px; + border: 1px solid var(--inspect-line); + border-radius: 12px; + background: color-mix(in srgb, var(--inspect-surface-muted) 58%, var(--inspect-surface)); +} + +.actionBreakdownCard strong { + font-size: 14px; +} + +.actionBreakdownCard div { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.actionBreakdownCard span { + display: inline-flex; + min-height: 28px; + align-items: center; + padding: 0 10px; + border-radius: 999px; + background: var(--inspect-surface); + color: var(--inspect-muted); + font-size: 12px; +} + +.riskGrid { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.summaryGrid { + grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); +} + +.summaryGridCompact { + grid-template-columns: repeat(5, minmax(120px, 1fr)); +} + + +.summaryCard { + display: grid; + gap: 4px; + padding: 16px; + border-radius: 8px; +} + +.summaryCard span { + color: var(--inspect-muted); + font-size: 14px; + font-weight: 500; +} + +.summaryCard strong { + font-size: 20px; + font-weight: 800; + line-height: 1.1; + letter-spacing: 0; +} + +.summaryGood { + border-color: color-mix(in srgb, var(--inspect-green) 28%, var(--inspect-line)); + background: color-mix(in srgb, var(--inspect-green) 7%, var(--inspect-surface)); +} + +.summaryWarn { + border-color: color-mix(in srgb, var(--inspect-amber) 30%, var(--inspect-line)); + background: color-mix(in srgb, var(--inspect-amber) 7%, var(--inspect-surface)); +} + +.summaryBad { + border-color: color-mix(in srgb, var(--inspect-red) 30%, var(--inspect-line)); + background: color-mix(in srgb, var(--inspect-red) 7%, var(--inspect-surface)); +} + +.riskCard { + min-height: 112px; +} + +.riskCard strong { + font-size: 24px; +} + +.controlPanel { + border-color: color-mix(in srgb, var(--inspect-accent) 16%, var(--inspect-line)); + background: + linear-gradient( + 180deg, + color-mix(in srgb, var(--inspect-accent) 3%, var(--inspect-surface)), + var(--inspect-surface) + ); +} + +.policyBanner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + padding: 14px 16px; + border-radius: 12px; + border: 1px solid color-mix(in srgb, var(--inspect-accent) 18%, var(--inspect-line)); + background: color-mix(in srgb, var(--inspect-accent) 7%, var(--inspect-surface-muted)); +} + +.policyBanner strong { + color: var(--inspect-ink); + font-size: 14px; +} + +.policyBanner span { + color: var(--inspect-muted); + font-size: 13px; + line-height: 1.5; + text-align: right; +} + +.panel { + display: grid; + gap: 16px; +} + +.settingsModal :global(.modal-body) { + display: grid; + gap: 0; + max-height: min(78vh, 820px); + overflow: hidden; + padding: 0; +} + +.settingsWorkbench { + display: grid; + grid-template-columns: 260px minmax(0, 1fr); + min-height: 620px; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--inspect-line) 74%, transparent); + border-radius: 18px; + background: var(--inspect-surface); +} + +.settingsSidebar { + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + gap: 16px; + min-width: 0; + padding: 18px; + border-right: 1px solid color-mix(in srgb, var(--inspect-line) 74%, transparent); + background: + radial-gradient(circle at top left, color-mix(in srgb, var(--inspect-accent) 12%, transparent), transparent 34%), + color-mix(in srgb, var(--inspect-surface-muted) 64%, var(--inspect-surface)); +} + +.settingsSidebarIntro { + display: grid; + gap: 8px; +} + +.settingsSidebarIntro strong { + color: var(--inspect-ink); + font-size: 16px; + font-weight: 780; + line-height: 1.35; +} + +.settingsSidebarIntro span { + color: var(--inspect-muted); + font-size: 12px; + line-height: 1.55; +} + +.settingsSidebarNav { + display: grid; + align-content: start; + gap: 8px; + min-height: 0; + overflow: auto; + padding-right: 2px; +} + +.settingsSidebarNav button { + display: grid; + gap: 5px; + width: 100%; + padding: 12px; + border: 1px solid transparent; + border-radius: 12px; + background: transparent; + color: inherit; + text-align: left; + cursor: pointer; + transition: background 0.16s ease, border-color 0.16s ease, transform 0.16s ease; +} + +.settingsSidebarNav button:hover { + border-color: color-mix(in srgb, var(--inspect-accent) 18%, var(--inspect-line)); + background: color-mix(in srgb, var(--inspect-accent) 7%, var(--inspect-surface)); + transform: translateX(2px); +} + +.settingsSidebarNav strong { + color: var(--inspect-ink); + font-size: 13px; + font-weight: 730; + line-height: 1.35; +} + +.settingsSidebarNav span { + min-width: 0; + overflow: hidden; + color: var(--inspect-muted); + font-size: 11px; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; +} + +.settingsSidebarStatus { + display: grid; + gap: 8px; + padding-top: 14px; + border-top: 1px solid color-mix(in srgb, var(--inspect-line) 72%, transparent); +} + +.settingsSidebarStatus span, +.settingsSummaryGrid span { + display: grid; + gap: 6px; + min-width: 0; + padding: 11px 12px; + border: 1px solid color-mix(in srgb, var(--inspect-line) 72%, transparent); + border-radius: 12px; + background: color-mix(in srgb, var(--inspect-surface) 86%, var(--inspect-surface-muted)); +} + +.settingsSidebarStatus small, +.settingsSummaryGrid small { + min-width: 0; + overflow: hidden; + color: var(--inspect-muted); + font-size: 11px; + font-weight: 650; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; +} + +.settingsSidebarStatus strong, +.settingsSummaryGrid strong { + min-width: 0; + overflow: hidden; + color: var(--inspect-ink); + font-size: 13px; + font-weight: 760; + line-height: 1.35; + text-overflow: ellipsis; +} + +.settingsWorkbenchMain { + display: grid; + align-content: start; + gap: 14px; + min-width: 0; + max-height: 620px; + overflow: auto; + padding: 18px; + scroll-behavior: smooth; +} + +.settingsHeroPanel, +.settingsWorkbenchSection { + display: grid; + gap: 16px; + min-width: 0; + border: 1px solid color-mix(in srgb, var(--inspect-line) 78%, transparent); + border-radius: 16px; + background: var(--inspect-surface-elevated); + box-shadow: 0 10px 28px color-mix(in srgb, var(--inspect-shadow) 6%, transparent); +} + +.settingsHeroPanel { + padding: 16px; + background: + linear-gradient(135deg, color-mix(in srgb, var(--inspect-accent) 8%, var(--inspect-surface-elevated)), var(--inspect-surface-elevated) 52%), + var(--inspect-surface-elevated); +} + +.settingsHeroPanel > div:first-child, +.settingsWorkbenchHeader > div { + display: grid; + gap: 5px; + min-width: 0; +} + +.settingsHeroPanel > div:first-child strong { + color: var(--inspect-ink); + font-size: 17px; + font-weight: 780; + line-height: 1.35; +} + +.settingsHeroPanel > div:first-child span { + color: var(--inspect-muted); + font-size: 13px; + line-height: 1.55; +} + +.settingsSummaryGrid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.settingsWorkbenchSection { + scroll-margin-top: 14px; + padding: 16px; +} diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_base.scss b/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_base.scss new file mode 100644 index 0000000..e3e1c13 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_base.scss @@ -0,0 +1,507 @@ +.page, +.settingsModal, +.errorModal { + --inspect-bg: var(--bg-secondary); + --inspect-surface: var(--bg-primary); + --inspect-surface-elevated: color-mix(in srgb, var(--bg-primary) 88%, var(--bg-secondary)); + --inspect-surface-muted: var(--bg-secondary); + --inspect-line: var(--border-color); + --inspect-line-strong: var(--border-primary); + --inspect-ink: var(--text-primary); + --inspect-muted: var(--text-secondary); + --inspect-accent: var(--primary-color); + --inspect-accent-soft: rgba(37, 99, 235, 0.12); + --inspect-green: var(--success-color); + --inspect-amber: #d97706; + --inspect-red: var(--error-color); + color: var(--inspect-ink); +} + +.page { + display: grid; + gap: 24px; + min-width: 0; +} + +.restoredSnapshotNotice { + display: grid; + gap: 5px; + padding: 13px 16px; + border: 1px solid color-mix(in srgb, var(--inspect-amber) 32%, var(--inspect-line)); + border-radius: 12px; + background: color-mix(in srgb, var(--inspect-amber) 8%, var(--inspect-surface)); + color: var(--inspect-ink); +} + +.restoredSnapshotNotice strong { + color: var(--inspect-amber); + font-size: 13px; +} + +.restoredSnapshotNotice span { + color: var(--inspect-muted); + font-size: 13px; + line-height: 1.55; +} + +.heroCard:global(.card), +.heroCard { + display: grid; + gap: 18px; + min-width: 0; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; +} + +.summaryCard, +.panel { + border: 1px solid var(--inspect-line); + border-radius: 12px; + background: var(--inspect-surface); + box-shadow: var(--shadow); +} + +.panel { + padding: 18px; +} + +.resultsSection .panel, +.logsSection .panel { + display: grid; + gap: 14px; + min-width: 0; + padding: 18px; + border: 1px solid var(--inspect-line); + border-radius: 12px; + background: var(--inspect-surface); + box-shadow: var(--shadow); +} + +.heroHeader, +.panelHeader { + display: flex; + justify-content: space-between; + gap: 16px; +} + +.heroHeader { + display: grid; + grid-template-columns: minmax(0, 1fr); + align-items: start; + gap: 20px; + min-width: 0; +} + +.heroCopy { + max-width: 760px; +} + +.heroTitle { + font-size: 28px; + font-weight: 700; + line-height: 1.15; + letter-spacing: 0; + color: var(--inspect-ink); +} + +.heroSubtitle { + font-size: 14px; +} + +.heroCopy, +.heroActions, +.panelHeader > div:first-child { + min-width: 0; +} + +.heroCopy { + display: grid; + gap: 10px; +} + +.heroTitle, +.panelTitle { + margin: 0; +} + +.heroTitle { + line-height: 1.15; + letter-spacing: 0; +} + +.heroSubtitle, +.panelSubtitle, +.mutedText, +.logTime, +.primaryCell small { + color: var(--inspect-muted); +} + +.heroSubtitle, +.panelSubtitle { + margin: 0; + line-height: 1.6; +} + +.heroSubtitle { + font-size: 14px; +} + +.heroActions, +.panelActions, +.panelMeta, +.metaRow { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; +} + +.heroActions { + justify-content: flex-end; + max-width: 100%; + gap: 8px; + padding: 4px; + border: 1px solid color-mix(in srgb, var(--inspect-line) 72%, transparent); + border-radius: 14px; + background: color-mix(in srgb, var(--inspect-surface) 74%, transparent); + box-shadow: 0 12px 28px -26px rgba(15, 23, 42, 0.5); +} + +.heroActionButton:global(.btn) { + flex: 0 0 auto; + align-self: center; + min-height: 32px; + padding: 0 10px; + border-radius: 8px; + border-color: transparent; + background: transparent; + color: var(--inspect-muted); + font: inherit; + line-height: 1; + box-shadow: none; +} + +.heroActionButton:global(.btn) > span { + line-height: 1; +} + +.heroActionButton:global(.btn):hover:not(:disabled) { + transform: translateY(-1px); + border-color: color-mix(in srgb, var(--inspect-accent) 35%, var(--inspect-line-strong)); + background: var(--inspect-surface-muted); + color: var(--inspect-ink); + box-shadow: none; +} + +.backLink, +.foldButton { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 36px; + padding: 0 14px; + border-radius: 12px; + border: 1px solid color-mix(in srgb, var(--inspect-line) 80%, transparent); + background: color-mix(in srgb, var(--inspect-surface-muted) 78%, var(--inspect-surface)); + color: var(--inspect-muted); + font-size: 13px; + font-weight: 700; + text-decoration: none; + cursor: pointer; + transition: + border-color 0.18s ease, + background-color 0.18s ease, + color 0.18s ease; +} + +.backLink:hover, +.foldButton:hover { + border-color: color-mix(in srgb, var(--inspect-accent) 28%, var(--inspect-line-strong)); + background: color-mix(in srgb, var(--inspect-accent) 6%, var(--inspect-surface-muted)); + color: var(--inspect-ink); +} + +.foldButton:disabled { + cursor: not-allowed; + opacity: 0.6; + transform: none; +} + +.controlLayout { + display: grid; + gap: 12px; +} + +.metaRow { + margin-top: 0; +} + +.metaPill, +.panelMeta span { + display: inline-flex; + align-items: center; + min-height: 30px; + padding: 0 10px; + border-radius: 999px; + background: var(--inspect-surface-muted); + color: var(--inspect-muted); + font-size: 12px; + font-weight: 500; +} + +.progressSection { + display: grid; + gap: 8px; + margin-top: 0; + padding: 12px 14px; + border-radius: 10px; + border: 1px solid color-mix(in srgb, var(--inspect-line) 74%, transparent); + background: color-mix(in srgb, var(--inspect-surface-muted) 62%, transparent); +} + +.progressHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.progressFooter { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.progressMeta { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.progressHeader strong, +.progressHeader span { + font-size: 13px; + line-height: 1.3; +} + +.progressHeader span { + color: var(--inspect-ink); + font-weight: 700; +} + +.progressTrack { + position: relative; + overflow: hidden; + height: 8px; + border-radius: 999px; + background: color-mix(in srgb, var(--inspect-line) 68%, transparent); +} + +.progressBar { + position: absolute; + inset: 0 auto 0 0; + border-radius: inherit; + background: linear-gradient(135deg, var(--inspect-accent), #0ea5e9); +} + +.progressActions { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; + flex-shrink: 0; +} + +.progressActions :global(.btn) { + padding: 8px 12px; +} + +.progressMeta { + color: var(--inspect-muted); + font-size: 12px; + line-height: 1.5; +} + +.progressMeta strong { + color: var(--inspect-amber); +} + +.summarySection { + display: grid; + gap: 10px; +} + +.summarySectionHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.summarySectionHeader > div { + display: grid; + gap: 4px; + min-width: 0; +} + +.summarySectionHeader h2 { + margin: 0; + font-size: 16px; + line-height: 1.4; +} + +.summarySectionHeader p { + margin: 0; + color: var(--inspect-muted); + font-size: 13px; + line-height: 1.5; +} + +.cockpitSection, +.workbenchSection, +.operationSection, +.resultsSection, +.logsSection { + display: grid; + gap: 16px; + min-width: 0; +} + +.operationModuleHeader { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 18px; + min-width: 0; + padding: 0; + border: 0; + background: transparent; +} + +.operationModuleHeader > div:first-child { + display: grid; + gap: 8px; + min-width: 0; +} + +.operationModuleHeader > .foldButton { + justify-self: end; +} + +.resultToolbar { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + align-items: center; + gap: 8px; + min-width: 0; +} + +.resultProviderSelect { + flex: 0 0 172px; + width: 172px; + min-width: 0; +} + +.resultProviderSelectTrigger { + height: 36px; + min-height: 36px; + padding: 0 12px; + border-color: color-mix(in srgb, var(--inspect-line) 80%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--inspect-surface-muted) 78%, var(--inspect-surface)); + color: var(--inspect-ink); + font-size: 12px; + font-weight: 700; + box-shadow: none; +} + +.resultProviderSelectTrigger:hover:not(:disabled), +.resultProviderSelectTrigger[aria-expanded='true'] { + border-color: color-mix(in srgb, var(--inspect-accent) 36%, var(--inspect-line)); + background: color-mix(in srgb, var(--inspect-accent) 7%, var(--inspect-surface)); + box-shadow: none; +} + +.resultProviderSelectDropdown { + border-radius: 12px; +} + +.operationModuleHeader h2 { + margin: 0; + color: var(--inspect-ink); + font-size: 22px; + font-weight: 800; + letter-spacing: -0.03em; + line-height: 1.2; +} + +.operationModuleHeader p { + margin: 0; + max-width: 760px; + color: var(--inspect-muted); + font-size: 13px; + line-height: 1.55; +} + +.resultFilterControl { + display: inline-flex; + flex: 0 0 auto; + max-width: 100%; + gap: 3px; + overflow-x: auto; + padding: 3px; + border: 1px solid color-mix(in srgb, var(--inspect-line) 80%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--inspect-surface-muted) 78%, var(--inspect-surface)); + scrollbar-width: none; +} + +.resultFilterControl::-webkit-scrollbar { + display: none; +} + +.resultFilterButton { + display: inline-flex; + align-items: center; + gap: 6px; + flex: 0 0 auto; + min-height: 28px; + padding: 0 10px; + border: 0; + border-radius: 999px; + background: transparent; + color: var(--inspect-muted); + font-size: 12px; + font-weight: 700; + cursor: pointer; + transition: + background-color 0.18s ease, + color 0.18s ease, + box-shadow 0.18s ease; +} + +.resultFilterButton:hover { + color: var(--inspect-ink); +} + +.resultFilterButtonActive { + background: var(--inspect-accent); + color: var(--primary-contrast, #fff); + box-shadow: 0 8px 18px -14px rgba(0, 0, 0, 0.45); +} + +.logHeaderActions { + display: inline-flex; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + gap: 10px; + min-width: 0; +} diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_responsive.scss b/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_responsive.scss new file mode 100644 index 0000000..442153b --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_responsive.scss @@ -0,0 +1,379 @@ +@media (max-width: 1280px) { + .assetOverviewLayout { + grid-template-columns: 1fr; + } + + .assetMetricGrid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .assetOverviewGrid { + grid-template-columns: 1fr; + } + + .providerDistributionCard { + order: -1; + } + + .assetKpiGrid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .cockpitGrid, + .workbenchGrid, + .inspectionOperationGrid, + .runControlCard, + .assetIntelligenceLayout, + .actionStudioLayout, + .chartGrid, + .inspectionStatsLayout { + grid-template-columns: 1fr; + } +} + +@media (max-width: 900px) { + .heroHeader, + .panelHeader, + .cockpitHeader { + flex-direction: column; + align-items: stretch; + } + + .heroHeader, + .operationModuleHeader { + grid-template-columns: 1fr; + } + + .operationModuleHeader > .foldButton { + justify-self: start; + width: fit-content; + } + + .heroActions { + justify-content: flex-start; + width: fit-content; + } + + .resultsHeaderActions { + width: fit-content; + max-width: 100%; + } + + .resultFilterControl { + width: fit-content; + max-width: 100%; + } + + .resultToolbar { + justify-content: flex-start; + } + + .resultProviderSelect { + flex-basis: 160px; + width: 160px; + } + + .heroActions, + .panelActions, + .resultsHeaderActions, + .logHeaderActions { + justify-content: flex-start; + } + + .progressFooter { + flex-direction: column; + align-items: stretch; + } + + .progressActions { + justify-content: flex-start; + } + + .runControlCard .cockpitCardHeader { + grid-row: auto; + } + + .providerDistributionHeader { + display: flex; + align-items: flex-start; + grid-template-columns: none; + } + + .providerDistributionSelection:global(.btn) { + max-width: min(58%, 220px); + } + + .providerSelectorRow { + display: grid; + grid-template-columns: 1fr; + } + + .providerSelectorRow > span { + width: 100%; + } + + .runStatusGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .runStatusPrimary { + grid-column: 1 / -1; + } + + .runExecutionGrid { + grid-template-columns: 1fr; + } + + .summaryGrid, + .summaryGridCompact, + .riskGrid, + .assetMetricGrid, + .assetKpiGrid, + .actionBreakdownGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .chartGrid, + .inspectionStatsLayout { + grid-template-columns: 1fr; + } + + .policyBanner { + flex-direction: column; + align-items: flex-start; + } + + .policyBanner span { + text-align: left; + } + + .logRow { + grid-template-columns: 1fr; + } + + .settingsWorkbench { + grid-template-columns: 1fr; + min-height: 0; + } + + .settingsSidebar { + grid-template-rows: auto auto auto; + border-right: 0; + border-bottom: 1px solid color-mix(in srgb, var(--inspect-line) 74%, transparent); + } + + .settingsSidebarNav { + display: flex; + overflow-x: auto; + padding-bottom: 2px; + } + + .settingsSidebarNav button { + min-width: 170px; + } + + .settingsSidebarStatus, + .settingsSummaryGrid, + .settingsThreeGrid, + .settingsMatrixGrid, + .settingsFocusGrid, + .settingsAutomationGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .settingsSplitGrid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + .panel, + .assetIntelligenceCard, + .runControlCard, + .actionStudioCard, + .executionTaskCard, + .providerPanel, + .providerDistributionCard { + padding: 16px; + border-radius: 12px; + } + + .professionalMetricCard { + min-height: 112px; + padding: 16px; + } + + .cockpitHeader h2 { + font-size: 20px; + } + + .cockpitCardHeader { + flex-direction: column; + } + + .inspectionConfigGrid, + .resultOverviewGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .inspectionControlActions { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .inspectionControlActions :global(.btn:first-child) { + grid-column: 1 / -1; + } + + .strategyTotalsRow { + grid-template-columns: repeat(4, minmax(0, 1fr)); + overflow-x: auto; + scrollbar-width: none; + } + + .strategyTotalsRow::-webkit-scrollbar { + display: none; + } + + .strategyActivityRow, + .inspectionOperationGrid .actionBreakdownCard div { + grid-template-columns: 1fr; + } + + .strategyActivityRow { + justify-items: start; + } + + .executionTaskGrid, + .runStatusGrid, + .summaryGrid, + .summaryGridCompact, + .riskGrid, + .assetMetricGrid, + .actionBreakdownGrid, + .runControlCard, + .chartGrid, + .inspectionStatsLayout, + .providerRow, + .providerStatusGrid { + grid-template-columns: 1fr; + } + + .assetKpiGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .chartPanel { + padding: 16px; + } + + .heroTitle { + font-size: 28px; + } + + .progressActions :global(.btn) { + flex: 1 1 140px; + } + + .heroActionButton:global(.btn) { + flex: 0 0 auto; + } + + .table { + min-width: 980px; + } + + .confirmationStats, + .confirmationPreviewRow { + grid-template-columns: 1fr; + } + + .confirmationStats span, + .confirmationPreviewRow strong { + text-align: left; + } + + .emptyState { + padding: 22px 16px; + } + + .settingsSummaryGrid, + .settingsSidebarStatus, + .settingsThreeGrid, + .settingsMatrixGrid, + .settingsFocusGrid, + .settingsAutomationGrid { + grid-template-columns: 1fr; + } + + .settingsModal :global(.modal-body) { + display: block; + max-height: min(72vh, calc(100vh - 132px)); + overflow-y: auto; + padding: 0; + -webkit-overflow-scrolling: touch; + } + + @supports (height: 100dvh) { + .settingsModal :global(.modal-body) { + max-height: min(72dvh, calc(100dvh - 132px)); + } + } + + .settingsWorkbench { + display: block; + max-height: none; + min-height: 0; + overflow: visible; + border-radius: 14px 14px 0 0; + } + + .settingsSidebar { + display: none; + } + + .settingsWorkbenchMain { + display: grid; + max-height: none; + overflow: visible; + padding: 14px; + } + + .settingsWorkbenchSection, + .settingsHeroPanel { + padding: 14px; + border-radius: 14px; + } + + .settingsWorkbenchHeader { + flex-direction: column; + align-items: flex-start; + } + + .settingsActionsBar { + position: sticky; + bottom: 0; + display: grid; + grid-template-columns: minmax(96px, auto) minmax(0, 1fr); + align-items: stretch; + gap: 8px; + padding: 10px 14px 14px; + border-radius: 0 0 14px 14px; + background: var(--inspect-surface-elevated); + } + + .settingsActionsBar > :global(button) { + width: auto; + min-width: 96px; + } + + .settingsActionsRight { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + width: auto; + gap: 8px; + } + + .settingsActionsRight :global(button) { + width: 100%; + } +} diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_settings.scss b/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_settings.scss new file mode 100644 index 0000000..e96c2a3 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_settings.scss @@ -0,0 +1,381 @@ +.settingsWorkbenchHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + padding-bottom: 12px; + border-bottom: 1px solid color-mix(in srgb, var(--inspect-line) 72%, transparent); +} + +.settingsWorkbenchHeader small { + width: fit-content; + padding: 3px 7px; + border-radius: 999px; + background: color-mix(in srgb, var(--inspect-accent) 10%, transparent); + color: var(--inspect-accent); + font-size: 11px; + font-weight: 760; + line-height: 1.2; +} + +.settingsWorkbenchHeader strong { + color: var(--inspect-ink); + font-size: 15px; + font-weight: 760; + line-height: 1.35; +} + +.settingsWorkbenchHeader span { + color: var(--inspect-muted); + font-size: 12px; + line-height: 1.55; +} + +.settingsSplitGrid, +.settingsThreeGrid, +.settingsMatrixGrid, +.settingsFocusGrid, +.settingsAutomationGrid { + display: grid; + gap: 12px; + min-width: 0; +} + +.settingsSplitGrid { + grid-template-columns: minmax(0, 1.35fr) minmax(220px, 0.65fr); +} + +.settingsThreeGrid { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.settingsMatrixGrid { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.settingsFocusGrid, +.settingsAutomationGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.settingsFormPanel, +.settingsFocusCard, +.settingsAutomationCard, +.settingsRiskPanel, +.settingsInsightPanel { + display: grid; + gap: 10px; + align-content: start; + min-width: 0; + padding: 14px; + border: 1px solid color-mix(in srgb, var(--inspect-line) 72%, transparent); + border-radius: 14px; + background: color-mix(in srgb, var(--inspect-surface-muted) 44%, var(--inspect-surface)); +} + +.settingsInsightPanel { + align-content: center; + background: color-mix(in srgb, var(--inspect-accent) 6%, var(--inspect-surface)); +} + +.settingsInsightPanel small, +.settingsFocusCard span, +.settingsAutomationCard span, +.settingsRiskPanel span { + color: var(--inspect-muted); + font-size: 12px; + line-height: 1.55; +} + +.settingsInsightPanel strong, +.settingsFocusCard strong { + min-width: 0; + overflow: hidden; + color: var(--inspect-ink); + font-size: 15px; + font-weight: 760; + line-height: 1.35; + text-overflow: ellipsis; +} + +.settingsInsightPanel span { + color: var(--inspect-accent); + font-size: 12px; + font-weight: 700; + line-height: 1.4; +} + +.settingsAutomationStack { + display: grid; + gap: 12px; +} + +.settingsRiskPanel { + border-color: color-mix(in srgb, var(--inspect-amber) 22%, var(--inspect-line)); + background: color-mix(in srgb, var(--inspect-amber) 4%, var(--inspect-surface)); +} + +.settingsPolicyControl { + display: flex; + align-items: center; + min-height: 38px; +} + +.settingsLabel { + display: block; + margin-bottom: 6px; + color: var(--inspect-text); + font-size: 13px; + font-weight: 650; +} + +.settingsHint { + color: var(--inspect-muted); + font-size: 12px; + line-height: 1.5; +} + +.settingsDangerNote { + padding: 9px 10px; + border: 1px solid color-mix(in srgb, var(--inspect-red) 32%, transparent); + border-radius: 10px; + background: color-mix(in srgb, var(--inspect-red) 8%, transparent); + color: var(--inspect-red); + font-size: 12px; + font-weight: 650; + line-height: 1.5; +} + +.settingsMutedField { + opacity: 0.58; +} + +.settingsFormPanel :global(.form-group), +.settingsFocusCard :global(.form-group), +.settingsRiskPanel :global(.form-group), +.settingsMatrixGrid :global(.form-group) { + margin-bottom: 0; +} + +.settingsFormPanel :global(.input), +.settingsFocusCard :global(.input), +.settingsRiskPanel :global(.input), +.settingsMatrixGrid :global(.input), +.settingsFormPanel input, +.settingsFocusCard input, +.settingsRiskPanel input, +.settingsMatrixGrid input { + min-height: 42px; +} + +.settingsActionsBar { + position: sticky; + bottom: 0; + z-index: 2; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; + margin: 0; + padding: 14px 18px; + border: 1px solid color-mix(in srgb, var(--inspect-line) 74%, transparent); + border-top: 0; + border-radius: 0 0 18px 18px; + background: + linear-gradient( + 180deg, + color-mix(in srgb, var(--inspect-surface-elevated) 78%, transparent), + var(--inspect-surface-elevated) 38% + ); +} + +.settingsActionsRight { + display: flex; + justify-content: flex-end; + flex-wrap: wrap; + gap: 10px; +} + +.resultsHeaderActions { + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 10px; +} + +.panelTitle { + font-size: 20px; +} + +.panelSubtitle { + margin-top: 4px; + font-size: 13px; +} + +.logList { + display: grid; + gap: 8px; + max-height: 360px; + overflow: auto; + padding-right: 4px; +} + +.logRow { + display: grid; + grid-template-columns: 180px minmax(0, 1fr); + gap: 12px; + align-items: start; + padding: 12px 14px; + border: 1px solid var(--inspect-line); + border-radius: 16px; + background: var(--inspect-surface-muted); + font-size: 12px; +} + +.logInfo { + color: var(--inspect-ink); +} + +.logSuccess { + color: var(--inspect-green); +} + +.logWarning { + color: var(--inspect-amber); + background: color-mix(in srgb, var(--inspect-amber) 10%, transparent); +} + +.logError { + color: var(--inspect-red); + background: color-mix(in srgb, var(--inspect-red) 10%, transparent); +} + +.logTime { + font-size: 12px; + font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); +} + +.logMessage { + word-break: break-word; + line-height: 1.6; +} + +.logCollapsedBar { + padding: 14px 16px; + border-radius: 16px; + background: var(--inspect-surface-muted); + color: var(--inspect-muted); +} + +.renderLimitHint { + padding: 10px 12px; + border: 1px solid var(--inspect-line); + border-radius: 8px; + background: var(--inspect-surface-muted); + color: var(--inspect-muted); + font-size: 12px; + line-height: 1.45; +} + +.previewSection { + display: grid; + gap: 12px; + padding: 18px; + border-radius: 18px; + background: color-mix(in srgb, var(--inspect-accent) 4%, var(--inspect-surface-muted)); + border: 1px solid var(--inspect-line); +} + +.previewHeader { + display: flex; + justify-content: space-between; + gap: 12px; + align-items: center; +} + +.previewHeader strong { + font-size: 15px; +} + +.previewHint { + color: var(--inspect-muted); + font-size: 12px; +} + +.previewList { + margin: 0; + padding-left: 18px; + display: grid; + gap: 8px; +} + +.previewList li { + line-height: 1.6; +} + +.healthCell { + display: inline-flex; + align-items: flex-start; + max-width: 100%; +} + +.healthBadge { + display: inline-flex; + align-items: center; + min-height: 24px; + padding: 0 8px; + border-radius: 999px; + border: 1px solid transparent; + font-size: 12px; + font-weight: 600; + line-height: 1.35; + text-align: left; + white-space: normal; + overflow-wrap: anywhere; +} + +.healthErrorButton { + display: inline-flex; + align-items: center; + min-width: 0; + padding: 0; + border: 0; + border-radius: 999px; + background: transparent; + color: inherit; + cursor: pointer; +} + +.healthErrorButton:hover { + filter: brightness(0.97); +} + +.healthErrorButton:focus-visible { + outline: 2px solid color-mix(in srgb, var(--inspect-red) 42%, var(--inspect-line)); + outline-offset: 2px; +} + +.healthHealthy { + background: color-mix(in srgb, var(--inspect-green) 12%, var(--inspect-surface)); + color: var(--inspect-green); +} + +.healthDisabled, +.healthQuota { + background: color-mix(in srgb, var(--inspect-amber) 12%, var(--inspect-surface)); + color: var(--inspect-amber); +} + +.healthAuthInvalid, +.healthError { + background: color-mix(in srgb, var(--inspect-red) 12%, var(--inspect-surface)); + color: var(--inspect-red); +} + +.healthRecoverable, +.healthProcessed { + background: color-mix(in srgb, var(--inspect-accent) 12%, var(--inspect-surface)); + color: var(--inspect-accent); +} diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_tables-dialogs.scss b/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_tables-dialogs.scss new file mode 100644 index 0000000..f8b2c37 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_tables-dialogs.scss @@ -0,0 +1,609 @@ +.tableWrap { + max-height: min(620px, 68vh); + overflow: auto; + overscroll-behavior: contain; + border: 1px solid var(--inspect-line); + border-radius: 8px; + background: var(--inspect-surface); + scrollbar-gutter: stable; +} + +.tableWrap::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +.tableWrap::-webkit-scrollbar-thumb { + border: 3px solid transparent; + border-radius: 999px; + background: color-mix(in srgb, var(--inspect-muted) 34%, transparent); + background-clip: padding-box; +} + +.tableWrap::-webkit-scrollbar-track { + background: transparent; +} + +.table { + min-width: 980px; + width: 100%; + border-collapse: separate; + border-spacing: 0; + table-layout: fixed; +} + +.accountColumn { + width: 26%; +} + +.healthColumn { + width: 15%; +} + +.enabledColumn { + width: 9%; +} + +.quotaColumn { + width: 9%; +} + +.tokenColumn { + width: 9%; +} + +.verdictColumn { + width: 14%; +} + +.operationColumn { + width: 18%; +} + +.table th, +.table td { + padding: 11px 12px; + border-bottom: 1px solid var(--inspect-line); + text-align: left; + vertical-align: middle; + font-size: 13px; + line-height: 1.45; +} + +.table th { + position: sticky; + top: 0; + z-index: 2; + background: var(--inspect-surface-muted); + color: var(--inspect-ink); + font-size: 12px; + font-weight: 700; + line-height: 1.35; + white-space: normal; +} + +.table tbody tr:nth-child(2n) td { + background: color-mix(in srgb, var(--inspect-surface-muted) 72%, var(--inspect-surface)); +} + +.table tbody tr:last-child td { + border-bottom: 0; +} + +.table tbody tr:hover td { + background: color-mix(in srgb, var(--inspect-accent) 5%, var(--inspect-surface)); +} + +.primaryCell { + display: grid; + gap: 4px; + min-width: 0; +} + +.primaryCell span { + font-weight: 650; + line-height: 1.35; + overflow-wrap: anywhere; +} + +.primaryCell small { + font-size: 12px; + line-height: 1.35; + overflow-wrap: anywhere; +} + +.statusCell, +.probeCell, +.diagnosisCell, +.quotaCell, +.tokenRefreshCell, +.verdictCell { + display: grid; + gap: 5px; + min-width: 0; +} + +.statusCell small, +.probeCell small, +.diagnosisCell small, +.quotaCell small, +.tokenRefreshCell small, +.verdictCell small { + color: var(--inspect-muted); + font-size: 12px; + line-height: 1.35; + overflow-wrap: anywhere; +} + +.quotaCell span, +.probeCell span { + color: var(--inspect-ink); + font-weight: 650; +} + +.quotaCell small, +.probeCell span { + font-variant-numeric: tabular-nums; +} + +.verdictCell strong { + color: var(--inspect-ink); + font-size: 13px; + font-weight: 650; + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.errorDetailsPanel { + display: grid; + gap: 14px; + padding: 2px 0 4px; +} + +.errorModalActions { + display: flex; + justify-content: flex-end; +} + +.errorOverview { + display: grid; + justify-items: start; + gap: 10px; + min-width: 0; + padding: 14px 16px; + border: 1px solid color-mix(in srgb, var(--inspect-red) 24%, var(--inspect-line)); + border-radius: 8px; + background: color-mix(in srgb, var(--inspect-red) 7%, var(--inspect-surface)); +} + +.errorOverview strong { + color: var(--inspect-ink); + font-size: 15px; + font-weight: 650; + line-height: 1.45; + overflow-wrap: anywhere; +} + +.errorDetailsGrid { + display: grid; + grid-template-columns: minmax(0, 1fr); + border: 1px solid color-mix(in srgb, var(--inspect-line) 82%, transparent); + border-radius: 8px; + overflow: hidden; +} + +.errorDetailItem { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 16px; + min-width: 0; + padding: 9px 12px; + background: var(--inspect-surface); +} + +.errorDetailItem + .errorDetailItem { + border-top: 1px solid color-mix(in srgb, var(--inspect-line) 72%, transparent); +} + +.errorDetailItem span, +.errorMessageBlock > span { + color: var(--inspect-muted); + font-size: 12px; + line-height: 1.35; +} + +.errorDetailItem strong { + min-width: 0; + max-width: 72%; + text-align: right; + overflow-wrap: anywhere; + color: var(--inspect-ink); + font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace); + font-size: 12px; + font-weight: 600; + line-height: 1.35; +} + +.errorMessageBlock { + display: grid; + gap: 6px; +} + +.errorMessage { + max-height: min(440px, 48vh); + overflow: auto; + margin: 0; + padding: 12px 14px; + border: 1px solid color-mix(in srgb, var(--inspect-line) 86%, transparent); + border-radius: 8px; + background: color-mix(in srgb, var(--inspect-surface-muted) 58%, var(--inspect-surface)); + color: var(--inspect-ink); + font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace); + font-size: 12px; + line-height: 1.5; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.stateText, +.stateTextGood, +.stateTextMuted, +.stateTextWarn, +.stateTextBad { + font-size: 12px; + font-weight: 650; + line-height: 1.35; + white-space: nowrap; +} + +.stateText, +.stateTextGood { + color: var(--inspect-green); +} + +.stateTextMuted { + color: var(--inspect-muted); +} + +.stateTextWarn { + color: var(--inspect-amber); +} + +.stateTextBad { + color: var(--inspect-red); +} + +.statePill { + display: inline-flex; + align-items: center; + width: fit-content; + min-height: 24px; + padding: 0 8px; + border-radius: 999px; + font-size: 12px; + font-weight: 650; + white-space: nowrap; +} + +.statePillGood { + background: color-mix(in srgb, var(--inspect-green) 12%, var(--inspect-surface)); + color: var(--inspect-green); +} + +.statePillMuted { + background: var(--inspect-surface-muted); + color: var(--inspect-muted); +} + +.statePillWarn { + background: color-mix(in srgb, var(--inspect-amber) 12%, var(--inspect-surface)); + color: var(--inspect-amber); +} + +.statePillBad { + background: color-mix(in srgb, var(--inspect-red) 12%, var(--inspect-surface)); + color: var(--inspect-red); +} + +.diagnosisCell span { + overflow-wrap: anywhere; +} + +.compactCell, +.numericCell { + color: var(--inspect-ink); + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.numericCell { + text-align: right; +} + +.reasonCell, +.errorCell { + max-width: 0; + overflow-wrap: anywhere; + word-break: break-word; +} + +.errorCell { + font-size: 12px; +} + +.operationCell { + vertical-align: middle; +} + +.operationCell .operationActions { + display: flex; + flex-wrap: nowrap; + align-items: center; + justify-content: flex-start; + gap: 4px; +} + +.operationCell .operationActions :global(.btn) { + flex: 0 0 auto; + width: auto; + min-width: 34px; + min-height: 26px; + padding: 0 5px; + border-radius: 7px; + font-size: 12px; + line-height: 1.2; +} + +.operationCell .operationActions :global(.btn + .btn) { + margin-left: 0; +} + +.monoCell { + font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); + word-break: break-all; +} + +.operationActions { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: flex-start; + gap: 6px; +} + +.operationActions :global(.btn) { + flex: 0 0 auto; + min-height: 30px; + padding: 0 9px; + border-radius: 8px; +} + +.emptyState { + display: grid; + justify-items: center; + gap: 12px; + min-height: 220px; + padding: 28px; + border: 1px dashed color-mix(in srgb, var(--inspect-line-strong) 72%, transparent); + border-radius: 14px; + background: color-mix(in srgb, var(--inspect-surface-muted) 68%, transparent); + text-align: center; +} + +.emptyState strong { + max-width: 620px; + font-size: 15px; + line-height: 1.6; +} + +.emptyState span { + max-width: 560px; + color: var(--inspect-muted); + font-size: 13px; + line-height: 1.6; +} + +.confirmationBody { + display: grid; + gap: 12px; +} + +.confirmationBody p { + margin: 0; + color: var(--inspect-muted); + font-size: 13px; + line-height: 1.6; +} + +.confirmationLead { + display: grid; + gap: 6px; + padding: 14px; + border: 1px solid color-mix(in srgb, var(--inspect-accent) 18%, var(--inspect-line)); + border-radius: 12px; + background: linear-gradient(180deg, color-mix(in srgb, var(--inspect-accent) 7%, var(--inspect-surface)), var(--inspect-surface)); +} + +.confirmationLead strong { + color: var(--inspect-ink); + font-size: 14px; + font-weight: 750; + line-height: 1.35; +} + +.confirmationLead span { + color: var(--inspect-muted); + font-size: 13px; + line-height: 1.55; + overflow-wrap: anywhere; +} + +.confirmationLeadDanger { + border-color: color-mix(in srgb, var(--inspect-red) 28%, var(--inspect-line)); + background: linear-gradient(180deg, color-mix(in srgb, var(--inspect-red) 8%, var(--inspect-surface)), var(--inspect-surface)); +} + +.confirmationAccountCard { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + align-items: center; + padding: 12px 14px; + border: 1px solid var(--inspect-line); + border-radius: 12px; + background: var(--inspect-surface-muted); +} + +.confirmationAccountCard div { + display: grid; + gap: 4px; + min-width: 0; +} + +.confirmationAccountCard span { + color: var(--inspect-ink); + font-size: 13px; + font-weight: 700; + line-height: 1.35; + overflow-wrap: anywhere; +} + +.confirmationAccountCard small { + color: var(--inspect-muted); + font-size: 12px; + line-height: 1.35; +} + +.confirmationAccountCard strong { + display: inline-flex; + align-items: center; + min-height: 24px; + padding: 0 8px; + border-radius: 999px; + background: color-mix(in srgb, var(--inspect-accent) 10%, var(--inspect-surface)); + color: var(--inspect-accent); + font-size: 12px; + font-weight: 700; + white-space: nowrap; +} + +.confirmationNotice { + padding: 10px 12px; + border: 1px solid color-mix(in srgb, var(--inspect-amber) 24%, var(--inspect-line)); + border-radius: 10px; + background: color-mix(in srgb, var(--inspect-amber) 9%, var(--inspect-surface)); + color: var(--inspect-amber); + font-size: 12px; + font-weight: 650; + line-height: 1.5; +} + +.confirmationNoticeDanger { + border-color: color-mix(in srgb, var(--inspect-red) 28%, var(--inspect-line)); + background: color-mix(in srgb, var(--inspect-red) 9%, var(--inspect-surface)); + color: var(--inspect-red); +} + +.confirmationStats { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.confirmationStats span { + padding: 10px 12px; + border-radius: 10px; + background: var(--inspect-surface-muted); + color: var(--inspect-ink); + font-size: 13px; + font-weight: 700; + text-align: center; +} + +.confirmationDangerStat { + background: color-mix(in srgb, var(--inspect-red) 12%, var(--inspect-surface)) !important; + color: var(--inspect-red) !important; +} + +.confirmationPreview { + display: grid; + gap: 8px; + padding: 12px; + border: 1px solid var(--inspect-line); + border-radius: 12px; + background: var(--inspect-surface-muted); +} + +.confirmationPreview > strong { + font-size: 13px; +} + +.confirmationPreviewRow { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(76px, 0.5fr) minmax(76px, 0.5fr) minmax(0, 1.6fr); + gap: 8px; + align-items: start; + font-size: 12px; + line-height: 1.5; +} + +.confirmationCompactRow { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + align-items: center; + font-size: 13px; + line-height: 1.5; +} + +.confirmationCompactRow small { + color: var(--inspect-muted); +} + +.confirmationPreviewRow span, +.confirmationPreviewRow em, +.confirmationCompactRow span { + overflow-wrap: anywhere; +} + +.confirmationPreviewRow small { + color: var(--inspect-muted); +} + +.confirmationPreviewRow em { + color: var(--inspect-muted); + font-style: normal; +} + +.warningText { + color: var(--inspect-amber); +} + +.dangerText { + color: var(--inspect-red); + font-weight: 700; +} + +.errorText { + color: var(--inspect-red); + word-break: break-word; +} + +.emptyBlock, +.emptyBlockSmall { + display: grid; + place-items: center; + color: var(--inspect-muted); + text-align: center; +} + +.emptyBlock { + min-height: 140px; +} + +.emptyBlockSmall { + min-height: 70px; +} diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_workbench.scss b/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_workbench.scss new file mode 100644 index 0000000..a0e9ca3 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/account-inspection-styles/_workbench.scss @@ -0,0 +1,890 @@ +.inspectionOperationGrid { + display: grid; + grid-template-columns: minmax(0, 1.24fr) minmax(360px, 0.76fr); + align-items: stretch; + gap: 16px; + min-width: 0; +} + +.operationMainColumn { + display: grid; + min-width: 0; +} + + +.cockpitHeader { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 18px; + min-width: 0; +} + +.cockpitHeader > div:first-child { + display: grid; + gap: 8px; + min-width: 0; +} + +.cockpitHeader h2 { + margin: 0; + color: var(--inspect-ink); + font-size: 22px; + font-weight: 800; + letter-spacing: -0.03em; + line-height: 1.2; +} + +.cockpitHeader p { + margin: 0; + max-width: 760px; + color: var(--inspect-muted); + font-size: 13px; + line-height: 1.55; +} + +.assetOverviewGrid { + display: grid; + grid-template-columns: minmax(0, 1.08fr) minmax(360px, 0.92fr); + align-items: stretch; + gap: 16px; + min-width: 0; +} + +.assetKpiGrid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; + min-width: 0; +} + +.professionalMetricCard { + --metric-accent: var(--inspect-accent); + --metric-accent-soft: rgba(37, 99, 235, 0.12); + display: grid; + gap: 16px; + min-width: 0; + min-height: 128px; + padding: 18px; + border: 1px solid color-mix(in srgb, var(--metric-accent) 18%, var(--inspect-line)); + border-radius: 12px; + background: + linear-gradient(180deg, color-mix(in srgb, var(--metric-accent) 5%, var(--inspect-surface)), var(--inspect-surface)); + box-shadow: var(--shadow); + transition: + transform 0.18s ease, + border-color 0.18s ease, + box-shadow 0.18s ease; +} + +.professionalMetricCard:hover { + transform: translateY(-1px); + border-color: color-mix(in srgb, var(--metric-accent) 32%, var(--inspect-line)); + box-shadow: 0 16px 34px -28px var(--metric-accent); +} + +.professionalMetricCard1, +.professionalMetricCard4 { + --metric-accent: #2563eb; + --metric-accent-soft: rgba(37, 99, 235, 0.12); +} + +.professionalMetricCard2, +.professionalMetricCard5 { + --metric-accent: #16a34a; + --metric-accent-soft: rgba(22, 163, 74, 0.12); +} + +.professionalMetricCard3, +.professionalMetricCard6 { + --metric-accent: #d97706; + --metric-accent-soft: rgba(217, 119, 6, 0.12); +} + +.professionalMetricHeader { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.professionalMetricHeader strong { + min-width: 0; + overflow: hidden; + color: var(--inspect-ink); + font-size: 14px; + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; +} + +.professionalMetricIcon { + position: relative; + flex: 0 0 auto; + width: 30px; + height: 30px; + border-radius: 10px; + background: var(--metric-accent-soft); + color: var(--metric-accent); +} + +.professionalMetricIcon::before, +.professionalMetricIcon::after { + content: ''; + position: absolute; + border-radius: 999px; + background: currentColor; +} + +.professionalMetricIcon::before { + left: 8px; + right: 8px; + bottom: 7px; + height: 4px; +} + +.professionalMetricIcon::after { + width: 5px; + height: 12px; + right: 9px; + bottom: 10px; + box-shadow: -7px 4px 0 currentColor; +} + +.professionalMetricBody { + display: grid; + gap: 8px; +} + +.professionalMetricBody span { + color: var(--inspect-muted); + font-size: 12px; + line-height: 1.35; +} + +.professionalMetricBody strong { + min-width: 0; + overflow: hidden; + color: var(--inspect-ink); + font-size: 26px; + font-weight: 850; + letter-spacing: -0.035em; + line-height: 1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.providerDistributionCard { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + align-content: stretch; + gap: 16px; + min-width: 0; + height: 360px; + padding: 20px; + border: 1px solid var(--inspect-line); + border-radius: 12px; + background: var(--inspect-surface); + box-shadow: var(--shadow); +} + +.providerDistributionHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + min-width: 0; +} + +.providerDistributionHeader > div { + display: grid; + gap: 6px; + min-width: 0; +} + +.providerDistributionHeader h3 { + margin: 0; + color: var(--inspect-ink); + font-size: 18px; + font-weight: 750; + letter-spacing: -0.02em; + line-height: 1.25; +} + +.providerDistributionHeader p { + margin: 0; + color: var(--inspect-muted); + font-size: 13px; + line-height: 1.5; +} + +.providerDistributionSelection:global(.btn) { + display: inline-flex; + align-items: center; + flex: 0 1 auto; + min-width: 0; + min-height: 34px; + max-width: 220px; + padding: 0 12px; + border: 1px solid color-mix(in srgb, var(--inspect-accent) 18%, var(--inspect-line)); + border-radius: 8px; + background: color-mix(in srgb, var(--inspect-accent) 7%, var(--inspect-surface)); + color: var(--inspect-accent); + font-size: 12px; + font-weight: 700; + box-shadow: none; +} + +.providerDistributionSelection:global(.btn) > span { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + overflow: hidden; + line-height: 1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.providerDistributionSelection:global(.btn) svg { + flex: 0 0 auto; +} + +.providerDistributionSelection:global(.btn):hover:not(:disabled) { + transform: translateY(-1px); + border-color: color-mix(in srgb, var(--inspect-accent) 38%, var(--inspect-line)); + background: color-mix(in srgb, var(--inspect-accent) 12%, var(--inspect-surface)); + box-shadow: none; +} + +.providerSelectorList { + display: grid; + align-content: start; + gap: 10px; + min-width: 0; + min-height: 0; + overflow: auto; + padding-right: 2px; +} + +.providerSelectorRow { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(120px, 0.45fr); + align-items: center; + gap: 14px; + min-width: 0; + padding: 12px 14px; + border: 1px solid color-mix(in srgb, var(--inspect-line) 78%, transparent); + border-radius: 12px; + background: color-mix(in srgb, var(--inspect-surface-muted) 45%, var(--inspect-surface)); + color: var(--inspect-ink); + text-align: left; + cursor: pointer; + transition: + transform 0.18s ease, + border-color 0.18s ease, + background-color 0.18s ease, + box-shadow 0.18s ease; +} + +.providerSelectorRow:hover { + transform: translateY(-1px); + border-color: color-mix(in srgb, var(--inspect-accent) 28%, var(--inspect-line)); + background: color-mix(in srgb, var(--inspect-accent) 5%, var(--inspect-surface)); +} + +.providerSelectorRowActive { + border-color: color-mix(in srgb, var(--inspect-accent) 44%, var(--inspect-line)); + background: color-mix(in srgb, var(--inspect-accent) 9%, var(--inspect-surface)); + box-shadow: 0 14px 28px -26px var(--inspect-accent); +} + +.providerSelectorRow div { + display: grid; + gap: 4px; + min-width: 0; +} + +.providerSelectorRow strong, +.providerSelectorRow span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.providerSelectorTitle { + display: inline-flex !important; + align-items: center; + gap: 8px; + min-width: 0; +} + +.providerSelectorTitle img, +.providerLogoFallback { + flex: 0 0 auto; + width: 24px; + height: 24px; + border-radius: 8px; +} + +.providerSelectorTitle img { + display: block; + object-fit: contain; +} + +.providerLogoFallback { + display: inline-grid; + place-items: center; + background: color-mix(in srgb, var(--inspect-accent) 12%, var(--inspect-surface)); + color: var(--inspect-accent); + font-size: 11px; + font-weight: 800; +} + +.providerSelectorTitle strong { + color: var(--inspect-ink); + font-size: 14px; + font-weight: 750; +} + + +.providerSelectorRow > span { + display: block; + height: 8px; + border-radius: 999px; + background: color-mix(in srgb, var(--inspect-line) 62%, transparent); +} + +.providerSelectorRow > span i { + display: block; + width: var(--bar-width, 0%); + height: 100%; + border-radius: inherit; + background: var(--bar-color, var(--inspect-accent)); + transition: width 0.24s ease, background-color 0.24s ease; +} + +.assetWorkbenchGrid, +.actionWorkbenchGrid, +.cockpitGrid, +.workbenchGrid { + display: grid; + align-items: stretch; + gap: 16px; + min-width: 0; +} + +.assetWorkbenchGrid, +.actionWorkbenchGrid { + grid-template-columns: 1fr; +} + +.cockpitGrid { + grid-template-columns: minmax(0, 1.2fr) minmax(360px, 0.8fr); +} + +.workbenchGrid { + grid-template-columns: minmax(0, 1.18fr) minmax(360px, 0.82fr); +} + +.assetIntelligenceCard, +.inspectionStatusCard, +.inspectionControlCard, +.actionStudioCard { + display: grid; + gap: 14px; + min-width: 0; + padding: 18px; + border: 1px solid color-mix(in srgb, var(--inspect-line) 82%, transparent); + border-radius: 14px; + background: var(--inspect-surface); + box-shadow: 0 14px 32px -30px rgba(15, 23, 42, 0.55); +} + +.operationMainStack, +.actionStudioCard { + display: grid; + align-content: stretch; + gap: 12px; + min-width: 0; + min-height: 100%; +} + +.operationMainStack { + grid-template-rows: minmax(0, 1fr) auto; +} + +.inspectionStatusCard { + gap: 14px; + border-color: color-mix(in srgb, var(--inspect-accent) 14%, var(--inspect-line)); + background: + linear-gradient(180deg, color-mix(in srgb, var(--inspect-accent) 3%, var(--inspect-surface)), var(--inspect-surface)); +} + +.inspectionProgressHero { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 16px; + min-width: 0; +} + +.progressRing { + --ring-track: color-mix(in srgb, var(--inspect-green) 14%, var(--inspect-line)); + --ring-value: var(--inspect-green); + display: grid; + place-items: center; + width: 104px; + aspect-ratio: 1; + border-radius: 50%; + background: conic-gradient(var(--ring-value) var(--progress, 0%), var(--ring-track) 0); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--inspect-green) 14%, transparent); +} + +.progressRing::before { + content: ''; + grid-area: 1 / 1; + width: 74%; + aspect-ratio: 1; + border-radius: 50%; + background: var(--inspect-surface); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--inspect-line) 70%, transparent); +} + +.progressRing strong { + grid-area: 1 / 1; + z-index: 1; + color: var(--inspect-ink); + font-size: 20px; + font-weight: 800; + letter-spacing: -0.03em; +} + +.inspectionStatusCopy { + display: grid; + gap: 6px; + min-width: 0; +} + +.inspectionStatusCopy strong { + color: var(--inspect-ink); + font-size: 20px; + font-weight: 750; + line-height: 1.2; +} + +.inspectionStatusCopy span, +.inspectionStatusCopy small { + color: var(--inspect-muted); + font-size: 13px; + line-height: 1.45; +} + +.inspectionConfigGrid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; + min-width: 0; +} + +.inspectionConfigGrid span { + display: grid; + gap: 6px; + min-width: 0; + min-height: 68px; + padding: 12px; + border: 1px solid color-mix(in srgb, var(--inspect-line) 66%, transparent); + border-radius: 10px; + background: color-mix(in srgb, var(--inspect-surface-muted) 28%, var(--inspect-surface)); +} + +.inspectionConfigGrid small { + min-width: 0; + overflow: hidden; + color: var(--inspect-muted); + font-size: 11px; + font-weight: 650; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; +} + +.inspectionConfigGrid strong { + min-width: 0; + overflow: hidden; + color: var(--inspect-ink); + font-size: 15px; + font-weight: 750; + line-height: 1.25; + text-overflow: ellipsis; +} + +.inspectionNextRunText { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 6px; + min-width: 0; + color: var(--inspect-muted); + font-size: 12px; + line-height: 1.35; + text-align: right; +} + +.inspectionNextRunText strong { + color: var(--inspect-ink); + font-weight: 750; +} + +.inspectionControlCard { + gap: 12px; + border-color: color-mix(in srgb, var(--inspect-line-strong) 84%, transparent); + background: var(--inspect-surface); +} + +.inspectionControlCard h3, +.resultOverviewSection h3, +.strategySectionHeader h3, +.manualPendingSection h3 { + margin: 0; + color: var(--inspect-ink); + font-size: 15px; + font-weight: 750; + letter-spacing: 0; + line-height: 1.3; +} + +.inspectionControlActions { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.inspectionControlActions :global(.btn) { + min-height: 40px; + border-radius: 10px; + font-size: 13px; + font-weight: 750; + box-shadow: none; +} + +.inspectionControlActions :global(.btn:nth-child(1)) { + border-color: color-mix(in srgb, var(--inspect-green) 54%, var(--inspect-line-strong)); + background: color-mix(in srgb, var(--inspect-green) 14%, var(--inspect-surface)); + color: var(--inspect-green); +} + +.inspectionControlActions :global(.btn:nth-child(1):hover:not(:disabled)) { + transform: translateY(-1px); + border-color: color-mix(in srgb, var(--inspect-green) 68%, var(--inspect-line-strong)); + background: color-mix(in srgb, var(--inspect-green) 20%, var(--inspect-surface)); +} + +.inspectionControlActions :global(.btn:nth-child(2)) { + border-color: color-mix(in srgb, var(--inspect-amber) 48%, var(--inspect-line-strong)); + background: color-mix(in srgb, var(--inspect-amber) 12%, var(--inspect-surface)); + color: var(--inspect-amber); +} + +.inspectionControlActions :global(.btn:nth-child(2):hover:not(:disabled)) { + transform: translateY(-1px); + border-color: color-mix(in srgb, var(--inspect-amber) 64%, var(--inspect-line-strong)); + background: color-mix(in srgb, var(--inspect-amber) 18%, var(--inspect-surface)); +} + +.inspectionControlActions :global(.btn:nth-child(3)) { + border-color: color-mix(in srgb, var(--inspect-red) 46%, var(--inspect-line-strong)); + background: color-mix(in srgb, var(--inspect-red) 11%, var(--inspect-surface)); + color: var(--inspect-red); +} + +.inspectionControlActions :global(.btn:nth-child(3):hover:not(:disabled)) { + transform: translateY(-1px); + border-color: color-mix(in srgb, var(--inspect-red) 62%, var(--inspect-line-strong)); + background: color-mix(in srgb, var(--inspect-red) 17%, var(--inspect-surface)); +} + +.actionStudioCard { + grid-template-rows: auto minmax(0, 1fr); +} + +.resultOverviewSection, +.strategyResultSection, +.manualPendingSection { + display: grid; + gap: 12px; + min-width: 0; + padding: 14px; + border: 1px solid color-mix(in srgb, var(--inspect-line) 70%, transparent); + border-radius: 12px; + background: color-mix(in srgb, var(--inspect-surface-muted) 22%, var(--inspect-surface)); +} + +.resultOverviewGrid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.resultOverviewGrid span { + position: relative; + display: grid; + gap: 6px; + min-width: 0; + min-height: 68px; + padding: 12px; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--inspect-accent) 14%, var(--inspect-line)); + border-radius: 10px; + background: + linear-gradient(180deg, color-mix(in srgb, var(--inspect-accent) 4%, var(--inspect-surface)), var(--inspect-surface)); +} + +.resultOverviewGrid span::before { + content: ''; + position: absolute; + inset: 0 auto 0 0; + width: 3px; + background: var(--inspect-accent); + opacity: 0.58; +} + +.resultOverviewGrid small { + color: var(--inspect-muted); + font-size: 11px; + font-weight: 650; + line-height: 1.35; +} + +.resultOverviewGrid strong { + color: var(--inspect-ink); + font-size: 22px; + font-weight: 800; + letter-spacing: -0.03em; + line-height: 1; +} + +.resultOverviewGood { + border-color: color-mix(in srgb, var(--inspect-green) 38%, var(--inspect-line-strong)) !important; + background: + linear-gradient(180deg, color-mix(in srgb, var(--inspect-green) 9%, var(--inspect-surface)), var(--inspect-surface)) !important; +} + +.resultOverviewGood::before { + background: var(--inspect-green) !important; +} + +.resultOverviewGood strong { + color: var(--inspect-green) !important; +} + +.resultOverviewWarn { + border-color: color-mix(in srgb, var(--inspect-amber) 38%, var(--inspect-line-strong)) !important; + background: + linear-gradient(180deg, color-mix(in srgb, var(--inspect-amber) 9%, var(--inspect-surface)), var(--inspect-surface)) !important; +} + +.resultOverviewWarn::before { + background: var(--inspect-amber) !important; +} + +.resultOverviewWarn strong { + color: var(--inspect-amber) !important; +} + +.resultOverviewBad { + border-color: color-mix(in srgb, var(--inspect-red) 38%, var(--inspect-line-strong)) !important; + background: + linear-gradient(180deg, color-mix(in srgb, var(--inspect-red) 9%, var(--inspect-surface)), var(--inspect-surface)) !important; +} + +.resultOverviewBad::before { + background: var(--inspect-red) !important; +} + +.resultOverviewBad strong { + color: var(--inspect-red) !important; +} + +.strategySectionHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.strategySectionHeader button, +.strategyActivityRow button { + border: 0; + background: transparent; + color: var(--inspect-accent); + font-size: 12px; + font-weight: 700; + cursor: pointer; +} + +.strategyTotalsRow { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 6px; + padding-top: 10px; + border-top: 1px solid color-mix(in srgb, var(--inspect-line) 70%, transparent); +} + +.strategyTotalsRow span { + min-width: 0; + overflow: hidden; + padding: 7px 6px; + border-radius: 8px; + background: var(--inspect-surface); + color: var(--inspect-muted); + font-size: 11px; + font-weight: 650; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; +} + +.strategyActivityRow { + display: grid; + grid-template-columns: auto auto minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + min-width: 0; + padding: 10px; + border: 1px solid color-mix(in srgb, var(--inspect-line) 66%, transparent); + border-radius: 10px; + background: var(--inspect-surface); +} + +.strategyActivityIcon { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--inspect-green); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--inspect-green) 12%, transparent); +} + +.strategyActivityRow span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.strategyActivityRow strong { + min-width: 0; +} + +.strategyActivityRow span { + color: var(--inspect-muted); + font-size: 12px; +} + +.strategyActivityRow strong { + color: var(--inspect-ink); + font-size: 13px; + font-weight: 700; + line-height: 1.45; + overflow-wrap: anywhere; +} + +.manualPendingSection { + align-content: start; +} + +.manualPendingList { + display: grid; + gap: 8px; +} + +.manualPendingList span { + padding: 9px 10px; + border: 1px solid color-mix(in srgb, var(--inspect-line) 62%, transparent); + border-radius: 9px; + background: var(--inspect-surface); + color: var(--inspect-muted); + font-size: 12px; +} + +.manualPendingActions { + display: flex; + justify-content: center; + margin-top: 4px; + padding: 14px; + border-radius: 9px; + background: color-mix(in srgb, var(--inspect-line-strong) 14%, transparent); +} + +.manualPendingActions :global(.btn) { + min-height: 36px; + border-radius: 9px; + padding-inline: 18px; +} + +.manualPendingEmpty { + display: grid; + place-items: center; + align-content: center; + gap: 8px; + min-height: 136px; + padding: 16px; + border: 1px dashed color-mix(in srgb, var(--inspect-line-strong) 46%, transparent); + border-radius: 10px; + background: color-mix(in srgb, var(--inspect-surface) 78%, var(--inspect-surface-muted)); + color: var(--inspect-muted); + text-align: center; +} + +.manualPendingEmpty span { + display: grid; + place-items: center; + width: 28px; + height: 28px; + border-radius: 50%; + background: color-mix(in srgb, var(--inspect-green) 10%, var(--inspect-surface)); + color: var(--inspect-green); + font-size: 16px; + font-weight: 800; +} + +.manualPendingEmpty strong { + color: var(--inspect-ink); + font-size: 13px; +} + +.manualPendingEmpty small { + max-width: 260px; + font-size: 12px; + line-height: 1.5; +} + + +.cockpitCardHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + min-width: 0; +} + +.cockpitCardHeader > div:first-child { + min-width: 0; +} + +.cockpitCardHeader h3 { + margin: 0; + color: var(--inspect-ink); + font-size: 18px; + font-weight: 750; + letter-spacing: -0.02em; + line-height: 1.25; +} + +.cockpitCardHeader p { + margin: 6px 0 0; + color: var(--inspect-muted); + font-size: 13px; + line-height: 1.5; +} diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/accountInspection.module.scss b/cliproxyapi-pro-management/overlay/src/features/monitoring/accountInspection.module.scss new file mode 100644 index 0000000..9af4cf3 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/accountInspection.module.scss @@ -0,0 +1,6 @@ +@use './account-inspection-styles/base'; +@use './account-inspection-styles/workbench'; +@use './account-inspection-styles/analytics'; +@use './account-inspection-styles/settings'; +@use './account-inspection-styles/tables-dialogs'; +@use './account-inspection-styles/responsive'; diff --git a/cliproxyapi-pro-management/overlay/src/features/monitoring/accountInspectionPageModel.tsx b/cliproxyapi-pro-management/overlay/src/features/monitoring/accountInspectionPageModel.tsx new file mode 100644 index 0000000..24b9b35 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/src/features/monitoring/accountInspectionPageModel.tsx @@ -0,0 +1,1372 @@ +/* eslint-disable react-refresh/only-export-components -- This module owns shared page models and JSX confirmation builders. */ +import { startTransition, type CSSProperties } from 'react'; +import type { TFunction } from 'i18next'; +import { + ACCOUNT_INSPECTION_ALL_PROVIDER_TYPE, + ACCOUNT_INSPECTION_SUPPORTED_PROVIDERS, + ACCOUNT_INSPECTION_SETTING_LIMITS, + buildAccountInspectionBackendViewState, + createIdleAccountInspectionProgressSnapshot, + isSuggestedAction, + type AccountInspectionAction, + type AccountInspectionAntigravityQuotaMode, + type AccountInspectionAutoErrorAction, + type AccountInspectionConfigurableSettings, + type AccountInspectionLogLevel, + type AccountInspectionPageInfo, + type AccountInspectionProgressSnapshot, + type AccountInspectionResultItem, + type AccountInspectionRunResult, +} from './accountInspection'; +import type { + AccountInspectionInspectOneItem, + AccountInspectionScheduleResponse, +} from '@/services/api'; +import { useQuotaStore } from '@/stores'; +import type { AuthFileItem } from '@/types'; +import { + isDisabledAuthFile, + isQuotaLowState, + isRecordValue, + normalizeNumberValue, + readBooleanValue, + readStringValue, + resolveAuthProvider, +} from '@/utils/quota'; +import { resolveProviderDisplayLabel } from '@/utils/sourceResolver'; +import styles from './accountInspection.module.scss'; + +export type RunStatus = 'idle' | 'running' | 'paused' | 'success' | 'error'; + +export type ResultHealthStatus = 'healthy' | 'disabled' | 'authInvalid' | 'quotaExhausted' | 'inspectionError' | 'recoverable'; + +export type ResultFilter = 'pending' | 'accountInvalid' | 'requestError' | 'quotaExhausted' | 'recoverable' | 'highAvailable'; + +export type SettingsSectionKey = 'plan' | 'scope' | 'runtime' | 'antigravity' | 'auto'; + +export type ManualAccountInspectionAction = Exclude; + +export type QuotaAccountStatsState = Pick< + ReturnType, + 'antigravityQuota' | 'claudeQuota' | 'codexQuota' | 'kimiQuota' | 'xaiQuota' +>; + +export type HealthCounts = { + total: number; + healthy: number; + disabled: number; + authInvalid: number; + quotaExhausted: number; + inspectionError: number; + recoverable: number; +}; + +export type InspectionLogEntry = { + id: string; + level: AccountInspectionLogLevel; + message: string; + timestamp: number; +}; + +export type SummaryCard = { + key: string; + label: string; + value: string; + description?: string; + tone?: 'neutral' | 'good' | 'warn' | 'bad'; +}; + +export type InspectionSettingsDraft = { + targetType: string; + workers: string; + deleteWorkers: string; + timeout: string; + retries: string; + usedPercentThreshold: string; + sampleSize: string; + antigravityDeepProbeEnabled: boolean; + antigravityDeepProbeModel: string; + antigravityQuotaMode: AccountInspectionAntigravityQuotaMode; + xaiDeepProbeEnabled: boolean; + xaiDeepProbeModel: string; + autoExecuteQuotaLimitDisable: boolean; + autoExecuteQuotaRecoveryEnable: boolean; + autoExecuteAccountInvalidAction: AccountInspectionAutoErrorAction; + autoExecuteRequestErrorAction: AccountInspectionAutoErrorAction; +}; + +export type InspectionSettingsDraftField = Exclude< + keyof InspectionSettingsDraft, + 'antigravityDeepProbeEnabled' | 'antigravityQuotaMode' | 'xaiDeepProbeEnabled' | 'autoExecuteQuotaLimitDisable' | 'autoExecuteQuotaRecoveryEnable' | 'autoExecuteAccountInvalidAction' | 'autoExecuteRequestErrorAction' +>; + +export type ScheduleDraft = { + enabled: boolean; + intervalMinutes: string; +}; + +export type ProviderAccountStats = { + provider: string; + total: number; + enabled: number; + highAvailable: number; + disabled: number; + quotaLow: number; + accountInvalid: number; + requestError: number; +}; + +export type ResolvedTheme = 'light' | 'dark'; + +export type AuthFileAccountStats = { + total: number; + providerCount: number; + enabled: number; + highAvailable: number; + disabled: number; + quotaLow: number; + accountInvalid: number; + requestError: number; + providers: ProviderAccountStats[]; +}; + +export type AutoExecutionCounts = { + delete: number; + disable: number; + enable: number; +}; + +export type InspectionResultViewRow = { + item: AccountInspectionResultItem; + healthStatus: ResultHealthStatus; + manualActions: ManualAccountInspectionAction[]; +}; + +export type InspectionResultsViewState = { + rows: InspectionResultViewRow[]; + healthCounts: HealthCounts; + actionableActionCounts: AutoExecutionCounts; + filterRows: Record; + filterRowCounts: Record; +}; + +export const ACCOUNT_INSPECTION_LOG_LIMIT = 200; +export const ACCOUNT_INSPECTION_RESULT_PAGE_SIZE = 100; +export const ACCOUNT_INSPECTION_LOG_PAGE_SIZE = 100; +export const ACCOUNT_INSPECTION_ACTION_PAGE_SIZE = 500; +export const ACCOUNT_INSPECTION_DETAILS_IDLE_DELAY_MS = 250; +export const ACCOUNT_INSPECTION_AUTH_FILES_IDLE_DELAY_MS = 300; +export const ACCOUNT_INSPECTION_ASSET_STATS_CHUNK_SIZE = 500; +export const ACCOUNT_INSPECTION_EXPORT_DOWNLOAD_CONCURRENCY = 4; + +export type AccountInspectionIdleCallback = (deadline: { didTimeout: boolean; timeRemaining: () => number }) => void; + +export const appendInspectionLogEntry = (entries: InspectionLogEntry[], entry: InspectionLogEntry) => + [...entries, entry].slice(-ACCOUNT_INSPECTION_LOG_LIMIT); + +export const getPaginationRange = (pageInfo: AccountInspectionPageInfo | null, visibleCount: number) => { + if (!pageInfo || pageInfo.total <= 0 || visibleCount <= 0) { + return { + page: pageInfo?.page ?? 1, + pageSize: pageInfo?.pageSize ?? visibleCount, + total: pageInfo?.total ?? visibleCount, + totalPages: pageInfo?.totalPages ?? (visibleCount > 0 ? 1 : 0), + from: visibleCount > 0 ? 1 : 0, + to: visibleCount, + hasPrevious: (pageInfo?.page ?? 1) > 1, + hasNext: Boolean(pageInfo?.hasMore), + }; + } + + const from = (pageInfo.page - 1) * pageInfo.pageSize + 1; + return { + page: pageInfo.page, + pageSize: pageInfo.pageSize, + total: pageInfo.total, + totalPages: pageInfo.totalPages, + from, + to: Math.min(pageInfo.total, from + visibleCount - 1), + hasPrevious: pageInfo.page > 1, + hasNext: pageInfo.hasMore, + }; +}; + +export const getProviderInitial = (label: string) => label.trim().charAt(0).toUpperCase() || '?'; + +export const getDocumentTheme = (): ResolvedTheme => { + if (typeof document === 'undefined') return 'light'; + const root = document.documentElement; + const theme = root.dataset.theme || root.getAttribute('data-theme') || root.className; + return String(theme).toLowerCase().includes('dark') ? 'dark' : 'light'; +}; + +export const emptyAutoExecutionCounts = (): AutoExecutionCounts => ({ + delete: 0, + disable: 0, + enable: 0, +}); + +export const createEmptyFilterRows = (): Record => ({ + pending: [], + accountInvalid: [], + requestError: [], + quotaExhausted: [], + recoverable: [], + highAvailable: [], +}); + +export const levelClassMap: Record = { + info: styles.logInfo, + success: styles.logSuccess, + warning: styles.logWarning, + error: styles.logError, +}; + +export const healthToneClass: Record = { + healthy: styles.healthHealthy, + disabled: styles.healthDisabled, + authInvalid: styles.healthAuthInvalid, + quotaExhausted: styles.healthQuota, + inspectionError: styles.healthError, + recoverable: styles.healthRecoverable, +}; + +export const healthLabelKey: Record = { + healthy: 'monitoring.account_inspection_health_healthy', + disabled: 'monitoring.account_inspection_health_disabled', + authInvalid: 'monitoring.account_inspection_account_invalid', + quotaExhausted: 'monitoring.account_inspection_health_quota_exhausted', + inspectionError: 'monitoring.account_inspection_account_request_error', + recoverable: 'monitoring.account_inspection_health_recoverable', +}; + +export const extractHealthHttpStatusCode = (item: AccountInspectionResultItem) => { + if (item.statusCode !== null && item.statusCode >= 400) return item.statusCode; + const errorText = [item.error, item.deepProbeError, item.tokenRefreshError].filter(Boolean).join(' '); + const match = errorText.match(/\bHTTP\s+(\d{3})\b/i) ?? errorText.match(/\bstatus(?:\s+code)?\s*[:=]?\s*(\d{3})\b/i); + return match ? Number(match[1]) : null; +}; + +export const buildHealthStatusCodeText = (item: AccountInspectionResultItem) => { + const httpStatusCode = extractHealthHttpStatusCode(item); + return httpStatusCode !== null ? String(httpStatusCode) : ''; +}; + +export const buildHealthStatusLabel = ( + item: AccountInspectionResultItem, + healthStatus: ResultHealthStatus, + t: TFunction +) => { + const label = t(healthLabelKey[healthStatus]); + const code = buildHealthStatusCodeText(item); + return code ? `${label} · ${code}` : label; +}; + +export const hasInspectionErrorDetails = (item: AccountInspectionResultItem) => Boolean( + item.error + || item.errorDetail?.trim() + || item.errorCode?.trim() + || item.deepProbeError + || item.tokenRefreshError + || extractHealthHttpStatusCode(item) !== null +); + +export const parseInspectionErrorPayload = (value: string): unknown => { + const text = value.trim(); + if (!text || (!text.startsWith('{') && !text.startsWith('['))) return null; + try { + return JSON.parse(text); + } catch { + return null; + } +}; + +export const readInspectionErrorMessage = (value: unknown): string => { + if (typeof value === 'string') return value.trim(); + if (!value || typeof value !== 'object' || Array.isArray(value)) return ''; + const record = value as Record; + const directMessage = readInspectionErrorMessage(record.message); + if (directMessage) return directMessage; + const nestedError = readInspectionErrorMessage(record.error); + if (nestedError) return nestedError; + return ''; +}; + +export const buildInspectionErrorPresentation = (item: AccountInspectionResultItem) => { + const candidates = [item.error, item.deepProbeError || '', item.tokenRefreshError || ''] + .map((value) => value.trim()) + .filter((value, index, values) => Boolean(value) && values.indexOf(value) === index) + .sort((left, right) => right.length - left.length); + const detailText = item.errorDetail?.trim() || candidates[0] || ''; + const detailPayload = parseInspectionErrorPayload(detailText); + const parsedMessage = readInspectionErrorMessage(detailPayload); + const httpStatusCode = extractHealthHttpStatusCode(item); + const fallbackSummary = candidates.find((value) => parseInspectionErrorPayload(value) === null) || ''; + const summary = parsedMessage || fallbackSummary; + const normalizedSummary = summary.replace(/\s+/g, ' ').trim(); + const statusOnlySummary = httpStatusCode !== null && normalizedSummary.toLowerCase() === `http ${httpStatusCode}`.toLowerCase(); + return { + summary: statusOnlySummary ? '' : normalizedSummary, + detail: detailPayload === null + ? (item.errorDetail?.trim() ? detailText : '') + : JSON.stringify(detailPayload, null, 2), + }; +}; + +export const resolveResultHealthStatus = (item: AccountInspectionResultItem): ResultHealthStatus => { + if (item.action === 'delete' || (item.statusCode !== null && ACCOUNT_INVALID_ERROR_STATUSES.has(item.statusCode))) { + return 'authInvalid'; + } + if (item.error) return 'inspectionError'; + if (item.isQuota || item.action === 'disable') return 'quotaExhausted'; + if (item.action === 'enable') return 'recoverable'; + if (item.disabled) return 'disabled'; + return 'healthy'; +}; + +export function InspectionErrorDetailsPanel({ + item, + t, +}: { + item: AccountInspectionResultItem; + t: TFunction; +}) { + const healthStatus = resolveResultHealthStatus(item); + const httpStatusCode = extractHealthHttpStatusCode(item); + const errorPresentation = buildInspectionErrorPresentation(item); + const detailItems = [ + { label: t('monitoring.account_label'), value: item.fileName }, + { label: t('monitoring.filter_provider'), value: item.provider }, + { label: t('monitoring.account_inspection_http_status'), value: httpStatusCode !== null ? String(httpStatusCode) : '' }, + { label: t('monitoring.account_inspection_error_code'), value: item.errorCode?.trim() || '' }, + ].filter((detail) => detail.value); + + return ( +
+
+ + {buildHealthStatusLabel(item, healthStatus, t)} + + {errorPresentation.summary ? {errorPresentation.summary} : null} +
+
+ {detailItems.map((detail) => ( +
+ {detail.label} + {detail.value} +
+ ))} +
+ {errorPresentation.detail ? ( +
+ {t('monitoring.account_inspection_raw_error_response')} +
{errorPresentation.detail}
+
+ ) : null} +
+ ); +} + +export const ACCOUNT_INVALID_ERROR_STATUSES = new Set([400, 401, 403, 404]); +export const ACCOUNT_INSPECTION_SUPPORTED_PROVIDER_SET = new Set(ACCOUNT_INSPECTION_SUPPORTED_PROVIDERS); + +export const readAuthFileField = (file: AuthFileItem, key: string) => + readStringValue((file as unknown as Record)[key]); + +export const isAccountInspectionApiKeyAuthFile = (file: AuthFileItem) => { + const label = readAuthFileField(file, 'label').toLowerCase(); + const source = readAuthFileField(file, 'source').toLowerCase(); + const apiKey = readAuthFileField(file, 'api_key') || readAuthFileField(file, 'apiKey'); + const path = readAuthFileField(file, 'path'); + + return label.includes('apikey') || + label.includes('api-key') || + (source.startsWith('config:') && Boolean(apiKey)) || + (Boolean(apiKey) && !path); +}; + +export const isInspectableAccountInspectionAuthFile = (file: AuthFileItem) => { + const provider = resolveAuthProvider(file); + return ACCOUNT_INSPECTION_SUPPORTED_PROVIDER_SET.has(provider) && !isAccountInspectionApiKeyAuthFile(file); +}; + +export const readAuthFileStatusMessage = (file: AuthFileItem) => { + const raw = file['status_message'] ?? file.statusMessage; + if (raw === undefined || raw === null) return ''; + return String(raw).trim(); +}; + +export const readAuthFileLastError = (file: AuthFileItem) => { + const raw = file['last_error'] ?? file.lastError; + return isRecordValue(raw) ? raw : null; +}; + +export const readAuthFileLastErrorCode = (file: AuthFileItem) => readStringValue(readAuthFileLastError(file)?.code); + +export const readAuthFileLastErrorStatus = (file: AuthFileItem) => { + const error = readAuthFileLastError(file); + return error ? normalizeNumberValue(error.http_status ?? error.httpStatus ?? error.status) : null; +}; + +export const isAuthFileAccountInvalid = (file: AuthFileItem) => + readAuthFileLastErrorCode(file) === 'inspection_http_error' && + ACCOUNT_INVALID_ERROR_STATUSES.has(readAuthFileLastErrorStatus(file) ?? 0); + +export const isAuthFileRequestError = (file: AuthFileItem) => { + const code = readAuthFileLastErrorCode(file); + if (code === 'inspection_probe_error' || code === 'antigravity_deep_probe_error') return true; + if (isAuthFileAccountInvalid(file)) return false; + if (readAuthFileLastError(file)) return true; + if (readBooleanValue(file.unavailable ?? file['unavailable'])) return true; + const status = String(file.status ?? file.state ?? '').trim().toLowerCase(); + return status === 'error' || readAuthFileStatusMessage(file).length > 0; +}; + +export const incrementProviderStats = (stats: ProviderAccountStats, disabled: boolean, highAvailable: boolean, quotaLow: boolean, accountInvalid: boolean, requestError: boolean) => { + stats.total += 1; + if (disabled) { + stats.disabled += 1; + } else { + stats.enabled += 1; + } + if (highAvailable) stats.highAvailable += 1; + if (quotaLow) stats.quotaLow += 1; + if (accountInvalid) stats.accountInvalid += 1; + if (requestError) stats.requestError += 1; +}; + +export const emptyProviderAccountStats = (provider: string): ProviderAccountStats => ({ + provider, + total: 0, + enabled: 0, + highAvailable: 0, + disabled: 0, + quotaLow: 0, + accountInvalid: 0, + requestError: 0, +}); + +export const createEmptyAuthFileAccountStats = (): AuthFileAccountStats => ({ + total: 0, + providerCount: 0, + enabled: 0, + highAvailable: 0, + disabled: 0, + quotaLow: 0, + accountInvalid: 0, + requestError: 0, + providers: [], +}); + +export const finalizeAuthFileAccountStats = ( + stats: AuthFileAccountStats, + providerStats: Map +) => ({ + ...stats, + providerCount: providerStats.size, + providers: [...providerStats.values()].sort((left, right) => right.total - left.total || left.provider.localeCompare(right.provider)), +}); + +export const quotaUsedPercentFromRemaining = (item: unknown): number | null => { + if (!isRecordValue(item)) return null; + const usedPercent = normalizeNumberValue(item.usedPercent ?? item.used_percent); + if (usedPercent !== null) return Math.max(0, Math.min(100, usedPercent)); + const remainingFraction = normalizeNumberValue(item.remainingFraction ?? item.remaining_fraction); + if (remainingFraction === null) return null; + const normalized = remainingFraction > 1 && remainingFraction <= 100 ? remainingFraction / 100 : remainingFraction; + return Math.max(0, Math.min(100, (1 - Math.max(0, Math.min(1, normalized))) * 100)); +}; + +export const maxQuotaUsedPercent = (items: unknown): number | null => { + if (!Array.isArray(items)) return null; + const values = items + .map(quotaUsedPercentFromRemaining) + .filter((value): value is number => value !== null); + if (values.length === 0) return null; + return Math.max(...values); +}; + +export const antigravityGroupUsedPercent = (group: unknown): number | null => { + if (!isRecordValue(group)) return null; + return maxQuotaUsedPercent(group.buckets); +}; + +export const maxAntigravityGroupUsedPercent = (groups: unknown[]): number | null => { + const values = groups + .map(antigravityGroupUsedPercent) + .filter((value): value is number => value !== null); + if (values.length === 0) return null; + return Math.max(...values); +}; + +export const isAntigravityClaudeGptGroup = (group: unknown): boolean => { + if (!isRecordValue(group)) return false; + const normalize = (value: unknown) => + typeof value === 'string' + ? value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') + : ''; + const id = normalize(group.id); + const label = normalize(group.label); + if (id === 'claude-gpt' || label === 'claude-gpt') return true; + const combined = `${id}-${label}`; + return combined.includes('claude') && (combined.includes('gpt') || combined.includes('openai')); +}; + +export const isAntigravityQuotaLow = ( + quota: unknown, + usedPercentThreshold: number, + quotaMode: AccountInspectionAntigravityQuotaMode +) => { + if (!isRecordValue(quota) || quota.status !== 'success') return false; + const groups = Array.isArray(quota.groups) ? quota.groups : []; + const used = quotaMode === 'max-used' + ? maxAntigravityGroupUsedPercent(groups) + : (maxAntigravityGroupUsedPercent(groups.filter(isAntigravityClaudeGptGroup)) ?? maxAntigravityGroupUsedPercent(groups)); + return used !== null && used >= usedPercentThreshold; +}; + +export const isXaiQuotaLow = (quota: unknown, usedPercentThreshold: number) => { + if (!isRecordValue(quota) || quota.status !== 'success') return false; + if (!isRecordValue(quota.billing)) return false; + const used = + normalizeNumberValue(quota.billing.usagePercent ?? quota.billing.usage_percent) + ?? normalizeNumberValue(quota.billing.usedPercent ?? quota.billing.used_percent) + ?? maxAntigravityGroupUsedPercent(Array.isArray(quota.billing.productUsage) ? quota.billing.productUsage : []); + return used !== null && used >= usedPercentThreshold; +}; + +export const isProviderQuotaLow = ( + provider: string, + quotaStore: QuotaAccountStatsState, + fileName: string, + usedPercentThreshold: number, + antigravityQuotaMode: AccountInspectionAntigravityQuotaMode +) => { + switch (provider) { + case 'antigravity': + return isAntigravityQuotaLow(quotaStore.antigravityQuota[fileName], usedPercentThreshold, antigravityQuotaMode); + case 'claude': + return isQuotaLowState(quotaStore.claudeQuota[fileName], usedPercentThreshold); + case 'codex': + return isQuotaLowState(quotaStore.codexQuota[fileName], usedPercentThreshold); + case 'kimi': + return isQuotaLowState(quotaStore.kimiQuota[fileName], usedPercentThreshold); + case 'xai': + return isXaiQuotaLow(quotaStore.xaiQuota[fileName], usedPercentThreshold); + default: + return false; + } +}; + +export const accumulateAuthFileAccountStats = ( + stats: AuthFileAccountStats, + providerStats: Map, + file: AuthFileItem, + quotaStore: QuotaAccountStatsState, + usedPercentThreshold: number, + antigravityQuotaMode: AccountInspectionAntigravityQuotaMode +) => { + if (!isInspectableAccountInspectionAuthFile(file)) return; + + const provider = resolveAuthProvider(file) || 'unknown'; + const disabled = isDisabledAuthFile(file); + const quotaLow = isProviderQuotaLow( + provider, + quotaStore, + file.name, + usedPercentThreshold, + antigravityQuotaMode + ); + const accountInvalid = isAuthFileAccountInvalid(file); + const requestError = isAuthFileRequestError(file); + const highAvailable = !disabled && !quotaLow && !accountInvalid && !requestError; + + stats.total += 1; + if (disabled) { + stats.disabled += 1; + } else { + stats.enabled += 1; + } + if (highAvailable) stats.highAvailable += 1; + if (accountInvalid) stats.accountInvalid += 1; + if (requestError) stats.requestError += 1; + if (quotaLow) stats.quotaLow += 1; + + const providerEntry = providerStats.get(provider) ?? emptyProviderAccountStats(provider); + incrementProviderStats(providerEntry, disabled, highAvailable, quotaLow, accountInvalid, requestError); + providerStats.set(provider, providerEntry); +}; + +export type AuthFileAccountStatsJob = { + cancelled: boolean; + handle: number | null; + cancelHandle: ((handle: number) => void) | null; +}; + +export const scheduleAuthFileAccountStats = ( + job: AuthFileAccountStatsJob, + files: AuthFileItem[], + quotaStore: QuotaAccountStatsState, + usedPercentThreshold: number, + antigravityQuotaMode: AccountInspectionAntigravityQuotaMode, + onComplete: (stats: AuthFileAccountStats) => void +) => { + const providerStats = new Map(); + const stats = createEmptyAuthFileAccountStats(); + let index = 0; + const windowWithIdleCallback = window as Window & { + requestIdleCallback?: (callback: AccountInspectionIdleCallback, options?: { timeout?: number }) => number; + cancelIdleCallback?: (handle: number) => void; + }; + + const finish = () => { + if (job.cancelled) return; + const nextStats = finalizeAuthFileAccountStats(stats, providerStats); + startTransition(() => { + if (!job.cancelled) onComplete(nextStats); + }); + }; + + const scheduleNextChunk = () => { + if (windowWithIdleCallback.requestIdleCallback && windowWithIdleCallback.cancelIdleCallback) { + job.cancelHandle = windowWithIdleCallback.cancelIdleCallback.bind(windowWithIdleCallback); + job.handle = windowWithIdleCallback.requestIdleCallback(runChunk, { timeout: 250 }); + return; + } + job.cancelHandle = window.clearTimeout.bind(window); + job.handle = window.setTimeout(() => runChunk(), 0); + }; + + const runChunk = (deadline?: { timeRemaining: () => number }) => { + if (job.cancelled) return; + do { + const chunkEnd = Math.min(index + ACCOUNT_INSPECTION_ASSET_STATS_CHUNK_SIZE, files.length); + while (index < chunkEnd) { + accumulateAuthFileAccountStats( + stats, + providerStats, + files[index], + quotaStore, + usedPercentThreshold, + antigravityQuotaMode + ); + index += 1; + } + } while (index < files.length && deadline && deadline.timeRemaining() > 8); + + if (index >= files.length) { + finish(); + return; + } + + scheduleNextChunk(); + }; + + runChunk(); +}; + +export const emptyHealthCounts = (): HealthCounts => ({ + total: 0, + healthy: 0, + disabled: 0, + authInvalid: 0, + quotaExhausted: 0, + inspectionError: 0, + recoverable: 0, +}); + +export const getManualActionsByHealthStatus = ( + item: AccountInspectionResultItem, + healthStatus: ResultHealthStatus +): ManualAccountInspectionAction[] => { + if (healthStatus === 'healthy') return []; + return [item.disabled ? 'enable' : 'disable', 'delete']; +}; + +export const buildInspectionResultsViewState = (items: AccountInspectionResultItem[]): InspectionResultsViewState => { + const healthCounts = emptyHealthCounts(); + const actionableActionCounts = emptyAutoExecutionCounts(); + const filterRows = createEmptyFilterRows(); + const filterRowCounts: Record = { + highAvailable: 0, + accountInvalid: 0, + quotaExhausted: 0, + requestError: 0, + recoverable: 0, + pending: 0, + }; + const rows: InspectionResultViewRow[] = []; + + const pushResultRow = ( + target: InspectionResultViewRow[], + item: AccountInspectionResultItem, + healthStatus: ResultHealthStatus, + existingRow: InspectionResultViewRow | null + ) => { + if (existingRow) { + target.push(existingRow); + return existingRow; + } + const row = { + item, + healthStatus, + manualActions: getManualActionsByHealthStatus(item, healthStatus), + }; + target.push(row); + return row; + }; + + healthCounts.total = items.length; + items.forEach((item) => { + const healthStatus = resolveResultHealthStatus(item); + let row: InspectionResultViewRow | null = null; + row = pushResultRow(rows, item, healthStatus, row); + + switch (healthStatus) { + case 'healthy': + healthCounts.healthy += 1; + filterRowCounts.highAvailable += 1; + row = pushResultRow(filterRows.highAvailable, item, healthStatus, row); + break; + case 'disabled': + healthCounts.disabled += 1; + break; + case 'authInvalid': + healthCounts.authInvalid += 1; + filterRowCounts.accountInvalid += 1; + row = pushResultRow(filterRows.accountInvalid, item, healthStatus, row); + break; + case 'quotaExhausted': + healthCounts.quotaExhausted += 1; + filterRowCounts.quotaExhausted += 1; + row = pushResultRow(filterRows.quotaExhausted, item, healthStatus, row); + break; + case 'inspectionError': + healthCounts.inspectionError += 1; + filterRowCounts.requestError += 1; + row = pushResultRow(filterRows.requestError, item, healthStatus, row); + break; + case 'recoverable': + healthCounts.recoverable += 1; + filterRowCounts.recoverable += 1; + row = pushResultRow(filterRows.recoverable, item, healthStatus, row); + break; + } + + if (isSuggestedAction(item) && !item.executed) { + filterRowCounts.pending += 1; + pushResultRow(filterRows.pending, item, healthStatus, row); + if (item.action === 'delete') actionableActionCounts.delete += 1; + if (item.action === 'disable') actionableActionCounts.disable += 1; + if (item.action === 'enable') actionableActionCounts.enable += 1; + } + }); + + return { + rows, + healthCounts, + actionableActionCounts, + filterRows, + filterRowCounts, + }; +}; + +export const collectActionableInspectionResults = (items: AccountInspectionResultItem[]) => { + const targets: AccountInspectionResultItem[] = []; + items.forEach((item) => { + if (isSuggestedAction(item) && !item.executed) { + targets.push(item); + } + }); + return targets; +}; + +export const buildManualActionItem = ( + item: AccountInspectionResultItem, + action: ManualAccountInspectionAction +): AccountInspectionResultItem => ({ + ...item, + action, + actionReason: item.actionReason || action, +}); + +export const summaryToneClass: Record, string> = { + neutral: '', + good: styles.summaryGood, + warn: styles.summaryWarn, + bad: styles.summaryBad, +}; + +export const INSPECTION_TARGET_OPTIONS = [ + { value: ACCOUNT_INSPECTION_ALL_PROVIDER_TYPE, label: 'All' }, + ...ACCOUNT_INSPECTION_SUPPORTED_PROVIDERS.map((provider) => ({ + value: provider, + label: resolveProviderDisplayLabel(provider), + })), +] as const; + +export const AUTO_ERROR_ACTION_OPTIONS: Array<{ value: AccountInspectionAutoErrorAction; labelKey: string }> = [ + { value: 'none', labelKey: 'monitoring.account_inspection_settings_account_error_action_none' }, + { value: 'disable', labelKey: 'monitoring.account_inspection_settings_account_error_action_disable' }, + { value: 'delete', labelKey: 'monitoring.account_inspection_settings_account_error_action_delete' }, +]; + +export const ANTIGRAVITY_QUOTA_MODE_OPTIONS: Array<{ value: AccountInspectionAntigravityQuotaMode; labelKey: string }> = [ + { value: 'claude-gpt', labelKey: 'monitoring.account_inspection_settings_antigravity_quota_mode_claude_gpt' }, + { value: 'max-used', labelKey: 'monitoring.account_inspection_settings_antigravity_quota_mode_max_used' }, +]; + +export const WORKER_LIMITS = ACCOUNT_INSPECTION_SETTING_LIMITS.workers; +export const DELETE_WORKER_LIMITS = ACCOUNT_INSPECTION_SETTING_LIMITS.deleteWorkers; +export const TIMEOUT_LIMITS = ACCOUNT_INSPECTION_SETTING_LIMITS.timeout; +export const RETRY_LIMITS = ACCOUNT_INSPECTION_SETTING_LIMITS.retries; +export const THRESHOLD_LIMITS = ACCOUNT_INSPECTION_SETTING_LIMITS.usedPercentThreshold; +export const SAMPLE_SIZE_LIMITS = ACCOUNT_INSPECTION_SETTING_LIMITS.sampleSize; +export const SCHEDULE_INTERVAL_LIMITS = ACCOUNT_INSPECTION_SETTING_LIMITS.scheduleIntervalMinutes; + +export const formatTimestamp = (value: number, locale: string) => new Date(value).toLocaleString(locale); + +export const formatInspectionInterval = (minutes: number, locale: string) => + new Intl.NumberFormat(locale, { style: 'unit', unit: 'minute', unitDisplay: 'short' }).format(minutes); + +export const buildHighAvailabilityBarStyle = (highAvailable: number, total: number): CSSProperties => { + const share = total > 0 ? Math.min(Math.max(highAvailable / total, 0), 1) : 0; + return { + '--bar-width': `${share * 100}%`, + '--bar-color': `hsl(${Math.round(share * 120)}, 72%, 44%)`, + } as CSSProperties; +}; + +export const toSettingsDraft = (settings: AccountInspectionConfigurableSettings): InspectionSettingsDraft => ({ + targetType: settings.targetType, + workers: String(settings.workers), + deleteWorkers: String(settings.deleteWorkers), + timeout: String(settings.timeout), + retries: String(settings.retries), + usedPercentThreshold: String(settings.usedPercentThreshold), + sampleSize: String(settings.sampleSize), + antigravityDeepProbeEnabled: settings.antigravityDeepProbeEnabled, + antigravityDeepProbeModel: settings.antigravityDeepProbeModel, + antigravityQuotaMode: settings.antigravityQuotaMode, + xaiDeepProbeEnabled: settings.xaiDeepProbeEnabled, + xaiDeepProbeModel: settings.xaiDeepProbeModel, + autoExecuteQuotaLimitDisable: settings.autoExecuteQuotaLimitDisable, + autoExecuteQuotaRecoveryEnable: settings.autoExecuteQuotaRecoveryEnable, + autoExecuteAccountInvalidAction: settings.autoExecuteAccountInvalidAction, + autoExecuteRequestErrorAction: settings.autoExecuteRequestErrorAction, +}); + +export const formatActionLabel = (action: AccountInspectionAction, t: TFunction) => { + switch (action) { + case 'delete': + return t('monitoring.account_inspection_action_delete'); + case 'disable': + return t('monitoring.account_inspection_action_disable'); + case 'enable': + return t('monitoring.account_inspection_action_enable'); + case 'keep': + default: + return t('monitoring.account_inspection_action_keep'); + } +}; + +export const formatQuotaRemainingLabel = (value: number | null) => { + if (value === null) return '--'; + return `${Math.max(0, 100 - value).toFixed(1)}%`; +}; + +export const formatTokenRefreshLabel = ( + item: AccountInspectionResultItem, + t: TFunction +) => { + if (item.tokenRefreshStatus === 'success') return t('monitoring.account_inspection_token_refresh_success'); + if (item.tokenRefreshStatus === 'failed') return t('monitoring.account_inspection_token_refresh_failed'); + if (item.nextRefreshAt && item.nextRefreshAt > Date.now()) return t('monitoring.account_inspection_token_refresh_pending'); + return t('monitoring.account_inspection_token_refresh_not_triggered'); +}; + +export const formatTokenRefreshDetail = ( + item: AccountInspectionResultItem, + locale: string, + t: TFunction +) => { + if (item.tokenRefreshStatus === 'failed') return item.tokenRefreshError || ''; + if (item.nextRefreshAt && item.nextRefreshAt > 0) { + return t('monitoring.account_inspection_token_next_refresh_at', { + time: formatTimestamp(item.nextRefreshAt, locale), + }); + } + return ''; +}; + +export const tokenRefreshToneClass = (item: AccountInspectionResultItem) => { + if (item.tokenRefreshStatus === 'success') return styles.stateTextGood; + if (item.tokenRefreshStatus === 'failed') return styles.stateTextBad; + if (item.nextRefreshAt && item.nextRefreshAt > Date.now()) return styles.stateTextWarn; + return styles.stateTextMuted; +}; + +export const formatInspectionVerdictPrimary = ( + item: AccountInspectionResultItem, + healthStatus: ResultHealthStatus, + t: TFunction +) => { + if (item.tokenRefreshStatus === 'failed') return t('monitoring.account_inspection_verdict_token_refresh_failed'); + + switch (healthStatus) { + case 'inspectionError': + return t('monitoring.account_inspection_verdict_probe_error'); + case 'authInvalid': + return t('monitoring.account_inspection_verdict_auth_invalid'); + case 'quotaExhausted': + return item.disabled + ? t('monitoring.account_inspection_verdict_quota_limited_disabled') + : t('monitoring.account_inspection_verdict_quota_limited'); + case 'recoverable': + return t('monitoring.account_inspection_verdict_quota_recovered'); + case 'disabled': + return t('monitoring.account_inspection_verdict_disabled'); + case 'healthy': + default: + return item.disabled + ? t('monitoring.account_inspection_verdict_healthy_disabled') + : t('monitoring.account_inspection_verdict_healthy'); + } +}; + +export const inspectionToastTone = (healthStatus: ResultHealthStatus): 'success' | 'warning' | 'error' => { + if (healthStatus === 'healthy' || healthStatus === 'recoverable') return 'success'; + if (healthStatus === 'inspectionError' || healthStatus === 'authInvalid') return 'error'; + return 'warning'; +}; + +export const formatInspectionResultToast = ( + item: AccountInspectionResultItem, + t: TFunction +) => { + const healthStatus = resolveResultHealthStatus(item); + const primary = formatInspectionVerdictPrimary(item, healthStatus, t); + return { + message: `${item.fileName}: ${primary}`, + tone: inspectionToastTone(healthStatus), + }; +}; + +export const formatTokenRefreshToast = ( + item: AccountInspectionResultItem, + fallbackError: string | undefined, + locale: string, + t: TFunction +) => { + const detail = item.tokenRefreshError || fallbackError || formatTokenRefreshDetail(item, locale, t); + if (item.tokenRefreshStatus === 'success') { + return { + message: detail + ? `${item.fileName}: ${t('monitoring.account_inspection_token_refresh_success')} · ${detail}` + : `${item.fileName}: ${t('monitoring.account_inspection_token_refresh_success')}`, + tone: 'success' as const, + }; + } + if (item.tokenRefreshStatus === 'failed' || fallbackError) { + return { + message: detail + ? `${item.fileName}: ${t('monitoring.account_inspection_token_refresh_failed')} · ${detail}` + : `${item.fileName}: ${t('monitoring.account_inspection_token_refresh_failed')}`, + tone: 'error' as const, + }; + } + return { + message: `${item.fileName}: ${t('monitoring.account_inspection_token_refresh_not_triggered')}`, + tone: 'warning' as const, + }; +}; + +export const formatCurrentStateLabel = (item: AccountInspectionResultItem, t: TFunction) => { + if (item.disabled) return t('monitoring.account_inspection_state_disabled'); + return t('monitoring.account_inspection_state_enabled'); +}; + +export const formatRunInspectionButtonLabel = (status: RunStatus, t: TFunction) => { + if (status === 'paused') return t('monitoring.account_inspection_resume'); + if (status === 'running') return t('monitoring.account_inspection_running'); + return t('monitoring.account_inspection_run'); +}; + +export const countActions = (items: AccountInspectionResultItem[]) => { + const summary = { + delete: 0, + disable: 0, + enable: 0, + }; + + items.forEach((item) => { + if (item.action === 'delete') summary.delete += 1; + if (item.action === 'disable') summary.disable += 1; + if (item.action === 'enable') summary.enable += 1; + }); + + return summary; +}; + +export const toAccountInspectionApiItem = (item: AccountInspectionResultItem): AccountInspectionInspectOneItem => ({ + key: item.key, + provider: item.provider, + fileName: item.fileName, + displayName: item.displayAccount, + email: item.email, + name: item.name, + authIndex: item.authIndex, + disabled: item.disabled, +}); + +export const buildActionRiskPreview = (items: AccountInspectionResultItem[], t: TFunction) => + items + .filter((item) => item.action === 'delete' || item.action === 'disable') + .slice(0, 5) + .map((item) => ({ + key: item.key, + account: item.fileName, + provider: item.provider, + action: formatActionLabel(item.action, t), + reason: item.actionReason || item.error || '-', + dangerous: item.action === 'delete', + })); + +export const buildExecuteConfirmationMessage = ( + items: AccountInspectionResultItem[], + t: TFunction, + hasAutoExecutePolicy: boolean +) => { + const counts = countActions(items); + const preview = buildActionRiskPreview(items, t); + const hasDelete = counts.delete > 0; + + return ( +
+

+ {t('monitoring.account_inspection_execute_confirm_body', { + total: items.length, + delete: counts.delete, + disable: counts.disable, + enable: counts.enable, + })} +

+
+ {`${t('monitoring.account_inspection_action_delete')}: ${counts.delete}`} + {`${t('monitoring.account_inspection_action_disable')}: ${counts.disable}`} + {`${t('monitoring.account_inspection_action_enable')}: ${counts.enable}`} +
+ {preview.length > 0 ? ( +
+ {t('monitoring.account_inspection_preview_title')} + {preview.map((item) => ( +
+ {item.account} + {item.provider} + {item.action} + {item.reason} +
+ ))} +
+ ) : null} + {hasAutoExecutePolicy ? ( +

+ {t('monitoring.account_inspection_settings_auto_section_desc')} +

+ ) : null} + {hasDelete ? ( +

+ {t('monitoring.account_inspection_delete_irreversible_warning')} +

+ ) : null} +
+ ); +}; + +export const buildConfirmationAccountCard = ( + item: AccountInspectionResultItem, + t: TFunction +) => ( +
+
+ {item.fileName} + {item.provider} +
+ {item.disabled ? t('monitoring.account_inspection_state_disabled') : t('monitoring.account_inspection_state_enabled')} +
+); + +export const buildDeleteConfirmationMessage = ( + item: AccountInspectionResultItem, + t: TFunction +) => ( +
+
+ {t('monitoring.account_inspection_delete_single_title')} + + {t('monitoring.account_inspection_delete_single_confirm_body', { + account: item.fileName, + })} + +
+ {buildConfirmationAccountCard(item, t)} +
+ {t('monitoring.account_inspection_delete_single_warning')} +
+
+); + +export const buildRefreshTokenConfirmationMessage = ( + item: AccountInspectionResultItem, + t: TFunction +) => ( +
+
+ {t('monitoring.account_inspection_refresh_token_confirm_title')} + + {t('monitoring.account_inspection_refresh_token_confirm_body', { + account: item.fileName, + })} + +
+ {buildConfirmationAccountCard(item, t)} +
+ {t('monitoring.account_inspection_refresh_token_confirm_hint')} +
+
+); + +export const withChanged = ( + state: S, + key: K, + next: S[K], + isEqual: (left: S[K], right: S[K]) => boolean +): S => { + if (isEqual(next, state[key])) return state; + return { ...state, [key]: next }; +}; + +export const sameProgressSnapshot = (left: AccountInspectionProgressSnapshot, right: AccountInspectionProgressSnapshot) => + left.total === right.total && + left.completed === right.completed && + left.inFlight === right.inFlight && + left.pending === right.pending && + left.percent === right.percent && + left.status === right.status && + left.startedAt === right.startedAt && + left.summary.totalFiles === right.summary.totalFiles && + left.summary.probeSetCount === right.summary.probeSetCount && + left.summary.sampledCount === right.summary.sampledCount && + left.summary.disabledCount === right.summary.disabledCount && + left.summary.enabledCount === right.summary.enabledCount && + left.summary.deleteCount === right.summary.deleteCount && + left.summary.disableCount === right.summary.disableCount && + left.summary.enableCount === right.summary.enableCount && + left.summary.keepCount === right.summary.keepCount && + left.summary.errorCount === right.summary.errorCount; + +export const sameInspectionSettings = (left: AccountInspectionConfigurableSettings, right: AccountInspectionConfigurableSettings) => + left.targetType === right.targetType && + left.workers === right.workers && + left.deleteWorkers === right.deleteWorkers && + left.timeout === right.timeout && + left.retries === right.retries && + left.usedPercentThreshold === right.usedPercentThreshold && + left.sampleSize === right.sampleSize && + left.antigravityDeepProbeEnabled === right.antigravityDeepProbeEnabled && + left.antigravityDeepProbeModel === right.antigravityDeepProbeModel && + left.antigravityQuotaMode === right.antigravityQuotaMode && + left.xaiDeepProbeEnabled === right.xaiDeepProbeEnabled && + left.xaiDeepProbeModel === right.xaiDeepProbeModel && + left.autoExecuteQuotaLimitDisable === right.autoExecuteQuotaLimitDisable && + left.autoExecuteQuotaRecoveryEnable === right.autoExecuteQuotaRecoveryEnable && + left.autoExecuteAccountInvalidAction === right.autoExecuteAccountInvalidAction && + left.autoExecuteRequestErrorAction === right.autoExecuteRequestErrorAction && + left.autoExecuteConfirmations === right.autoExecuteConfirmations; + +export const sameSettingsDraft = (left: InspectionSettingsDraft, right: InspectionSettingsDraft) => + left.targetType === right.targetType && + left.workers === right.workers && + left.deleteWorkers === right.deleteWorkers && + left.timeout === right.timeout && + left.retries === right.retries && + left.usedPercentThreshold === right.usedPercentThreshold && + left.sampleSize === right.sampleSize && + left.antigravityDeepProbeEnabled === right.antigravityDeepProbeEnabled && + left.antigravityDeepProbeModel === right.antigravityDeepProbeModel && + left.antigravityQuotaMode === right.antigravityQuotaMode && + left.xaiDeepProbeEnabled === right.xaiDeepProbeEnabled && + left.xaiDeepProbeModel === right.xaiDeepProbeModel && + left.autoExecuteQuotaLimitDisable === right.autoExecuteQuotaLimitDisable && + left.autoExecuteQuotaRecoveryEnable === right.autoExecuteQuotaRecoveryEnable && + left.autoExecuteAccountInvalidAction === right.autoExecuteAccountInvalidAction && + left.autoExecuteRequestErrorAction === right.autoExecuteRequestErrorAction; + +export const sameScheduleDraft = (left: ScheduleDraft, right: ScheduleDraft) => + left.enabled === right.enabled && left.intervalMinutes === right.intervalMinutes; + +export type InspectionScheduleSnapshot = AccountInspectionScheduleResponse['schedule']; + +export const sameScheduleSnapshot = ( + left: InspectionScheduleSnapshot | null, + right: InspectionScheduleSnapshot | null +) => { + if (left === right) return true; + if (!left || !right) return false; + return left.enabled === right.enabled && + left.intervalMinutes === right.intervalMinutes && + left.nextRunAt === right.nextRunAt && + sameInspectionSettings(left.settings, right.settings); +}; + +export const sameAutoExecutionCounts = (left: AutoExecutionCounts, right: AutoExecutionCounts) => + left.delete === right.delete && left.disable === right.disable && left.enable === right.enable; + +export const sameRunStatus = (left: RunStatus, right: RunStatus) => left === right; + +export const handleAccountInspectionControlError = ( + error: unknown, + appendLog: (level: AccountInspectionLogLevel, message: string) => void, + showNotification: (message: string, type?: 'info' | 'success' | 'warning' | 'error') => void, + fallbackMessage: string +) => { + const message = error instanceof Error ? error.message : String(error || fallbackMessage); + appendLog('error', message); + showNotification(message, 'error'); +}; + +export type BackendInspectionViewState = ReturnType; + +export type InspectionBackendState = { + inspectionSettings: AccountInspectionConfigurableSettings; + settingsDraft: InspectionSettingsDraft; + scheduleDraft: ScheduleDraft; + schedule: InspectionScheduleSnapshot | null; + logs: InspectionLogEntry[]; + logsPage: AccountInspectionPageInfo | null; + runStatus: RunStatus; + progress: AccountInspectionProgressSnapshot; + result: AccountInspectionRunResult | null; + autoExecutionCounts: AutoExecutionCounts; + restoredSnapshot: boolean; +}; + +export type InspectionBackendAction = + | { type: 'configChanged'; settings: AccountInspectionConfigurableSettings; syncDraft: boolean } + | { type: 'backendResponseReceived'; response: AccountInspectionScheduleResponse } + | { type: 'clearSchedule' } + | { type: 'appendLog'; level: AccountInspectionLogLevel; message: string; timestamp: number } + | { type: 'clearLogs' } + | { type: 'startRun'; timestamp: number } + | { type: 'runFailed' } + | { type: 'clearAutoExecutionCounts' } + | { type: 'setResult'; result: AccountInspectionRunResult | null } + | { type: 'resetSettings'; settings: AccountInspectionConfigurableSettings } + | { type: 'setSettingsDraft'; draft: InspectionSettingsDraft } + | { type: 'updateSettingsDraft'; values: Partial } + | { type: 'updateScheduleDraft'; values: Partial }; + +export const createInspectionBackendState = (settings: AccountInspectionConfigurableSettings): InspectionBackendState => ({ + inspectionSettings: settings, + settingsDraft: toSettingsDraft(settings), + scheduleDraft: { enabled: false, intervalMinutes: '360' }, + schedule: null, + logs: [], + logsPage: null, + runStatus: 'idle', + progress: createIdleAccountInspectionProgressSnapshot(), + result: null, + autoExecutionCounts: emptyAutoExecutionCounts(), + restoredSnapshot: false, +}); + +export const applyBackendViewState = ( + state: InspectionBackendState, + response: AccountInspectionScheduleResponse, + viewState: BackendInspectionViewState +) => { + let nextState = state; + nextState = withChanged(nextState, 'inspectionSettings', viewState.settings, sameInspectionSettings); + nextState = withChanged(nextState, 'settingsDraft', toSettingsDraft(viewState.settings), sameSettingsDraft); + nextState = withChanged(nextState, 'scheduleDraft', viewState.scheduleDraft, sameScheduleDraft); + nextState = withChanged(nextState, 'schedule', response.schedule, sameScheduleSnapshot); + nextState = withChanged(nextState, 'autoExecutionCounts', viewState.autoExecutionCounts, sameAutoExecutionCounts); + nextState = withChanged(nextState, 'progress', viewState.progress, sameProgressSnapshot); + nextState = withChanged(nextState, 'runStatus', viewState.runStatus, sameRunStatus); + nextState = withChanged(nextState, 'restoredSnapshot', viewState.restoredSnapshot, Object.is); + if (viewState.logs) { + nextState = withChanged(nextState, 'logs', viewState.logs, Object.is); + } + if (viewState.logsPage !== undefined) { + nextState = withChanged(nextState, 'logsPage', viewState.logsPage ?? null, Object.is); + } + if (viewState.result !== undefined) { + nextState = withChanged(nextState, 'result', viewState.result, Object.is); + } + return nextState; +}; + +export const inspectionBackendReducer = ( + state: InspectionBackendState, + action: InspectionBackendAction +): InspectionBackendState => { + switch (action.type) { + case 'configChanged': { + let nextState = withChanged(state, 'inspectionSettings', action.settings, sameInspectionSettings); + if (action.syncDraft) { + nextState = withChanged(nextState, 'settingsDraft', toSettingsDraft(action.settings), sameSettingsDraft); + } + return nextState; + } + case 'backendResponseReceived': + return applyBackendViewState(state, action.response, buildAccountInspectionBackendViewState(action.response)); + case 'clearSchedule': + return state.schedule === null ? state : { ...state, schedule: null }; + case 'appendLog': + return { + ...state, + logs: appendInspectionLogEntry(state.logs, { + id: `${action.timestamp}-${state.logs.length}`, + level: action.level, + message: action.message, + timestamp: action.timestamp, + }), + }; + case 'clearLogs': + return state.logs.length === 0 ? state : { ...state, logs: [] }; + case 'startRun': + return { + ...state, + result: null, + runStatus: 'running', + restoredSnapshot: false, + autoExecutionCounts: emptyAutoExecutionCounts(), + progress: { + ...createIdleAccountInspectionProgressSnapshot(), + status: 'running', + startedAt: action.timestamp, + updatedAt: action.timestamp, + }, + }; + case 'runFailed': + return state.runStatus === 'error' ? state : { ...state, runStatus: 'error' }; + case 'clearAutoExecutionCounts': + return withChanged(state, 'autoExecutionCounts', emptyAutoExecutionCounts(), sameAutoExecutionCounts); + case 'setResult': + return state.result === action.result ? state : { ...state, result: action.result }; + case 'resetSettings': + return { + ...state, + inspectionSettings: action.settings, + settingsDraft: toSettingsDraft(action.settings), + }; + case 'setSettingsDraft': + return withChanged(state, 'settingsDraft', action.draft, sameSettingsDraft); + case 'updateSettingsDraft': + return { ...state, settingsDraft: { ...state.settingsDraft, ...action.values } }; + case 'updateScheduleDraft': + return { ...state, scheduleDraft: { ...state.scheduleDraft, ...action.values } }; + default: + return state; + } +}; diff --git a/cliproxyapi-pro-management/overlay/src/pages/AccountInspectionPage.module.scss b/cliproxyapi-pro-management/overlay/src/pages/AccountInspectionPage.module.scss deleted file mode 100644 index 0569903..0000000 --- a/cliproxyapi-pro-management/overlay/src/pages/AccountInspectionPage.module.scss +++ /dev/null @@ -1,3487 +0,0 @@ -.page, -.settingsModal, -.errorModal { - --inspect-bg: var(--bg-secondary); - --inspect-surface: var(--bg-primary); - --inspect-surface-elevated: color-mix(in srgb, var(--bg-primary) 88%, var(--bg-secondary)); - --inspect-surface-muted: var(--bg-secondary); - --inspect-line: var(--border-color); - --inspect-line-strong: var(--border-primary); - --inspect-ink: var(--text-primary); - --inspect-muted: var(--text-secondary); - --inspect-accent: var(--primary-color); - --inspect-accent-soft: rgba(37, 99, 235, 0.12); - --inspect-green: var(--success-color); - --inspect-amber: #d97706; - --inspect-red: var(--error-color); - color: var(--inspect-ink); -} - -.page { - display: grid; - gap: 24px; - min-width: 0; -} - -.restoredSnapshotNotice { - display: grid; - gap: 5px; - padding: 13px 16px; - border: 1px solid color-mix(in srgb, var(--inspect-amber) 32%, var(--inspect-line)); - border-radius: 12px; - background: color-mix(in srgb, var(--inspect-amber) 8%, var(--inspect-surface)); - color: var(--inspect-ink); -} - -.restoredSnapshotNotice strong { - color: var(--inspect-amber); - font-size: 13px; -} - -.restoredSnapshotNotice span { - color: var(--inspect-muted); - font-size: 13px; - line-height: 1.55; -} - -.heroCard:global(.card), -.heroCard { - display: grid; - gap: 18px; - min-width: 0; - padding: 0; - border: 0; - border-radius: 0; - background: transparent; - box-shadow: none; -} - -.summaryCard, -.panel { - border: 1px solid var(--inspect-line); - border-radius: 12px; - background: var(--inspect-surface); - box-shadow: var(--shadow); -} - -.panel { - padding: 18px; -} - -.resultsSection .panel, -.logsSection .panel { - display: grid; - gap: 14px; - min-width: 0; - padding: 18px; - border: 1px solid var(--inspect-line); - border-radius: 12px; - background: var(--inspect-surface); - box-shadow: var(--shadow); -} - -.heroHeader, -.panelHeader { - display: flex; - justify-content: space-between; - gap: 16px; -} - -.heroHeader { - display: grid; - grid-template-columns: minmax(0, 1fr); - align-items: start; - gap: 20px; - min-width: 0; -} - -.heroCopy { - max-width: 760px; -} - -.heroTitle { - font-size: 28px; - font-weight: 700; - line-height: 1.15; - letter-spacing: 0; - color: var(--inspect-ink); -} - -.heroSubtitle { - font-size: 14px; -} - -.heroCopy, -.heroActions, -.panelHeader > div:first-child { - min-width: 0; -} - -.heroCopy { - display: grid; - gap: 10px; -} - -.heroTitle, -.panelTitle { - margin: 0; -} - -.heroTitle { - line-height: 1.15; - letter-spacing: 0; -} - -.heroSubtitle, -.panelSubtitle, -.mutedText, -.logTime, -.primaryCell small { - color: var(--inspect-muted); -} - -.heroSubtitle, -.panelSubtitle { - margin: 0; - line-height: 1.6; -} - -.heroSubtitle { - font-size: 14px; -} - -.heroActions, -.panelActions, -.panelMeta, -.metaRow { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 10px; -} - -.heroActions { - justify-content: flex-end; - max-width: 100%; - gap: 8px; - padding: 4px; - border: 1px solid color-mix(in srgb, var(--inspect-line) 72%, transparent); - border-radius: 14px; - background: color-mix(in srgb, var(--inspect-surface) 74%, transparent); - box-shadow: 0 12px 28px -26px rgba(15, 23, 42, 0.5); -} - -.heroActionButton:global(.btn) { - flex: 0 0 auto; - align-self: center; - min-height: 32px; - padding: 0 10px; - border-radius: 8px; - border-color: transparent; - background: transparent; - color: var(--inspect-muted); - font: inherit; - line-height: 1; - box-shadow: none; -} - -.heroActionButton:global(.btn) > span { - line-height: 1; -} - -.heroActionButton:global(.btn):hover:not(:disabled) { - transform: translateY(-1px); - border-color: color-mix(in srgb, var(--inspect-accent) 35%, var(--inspect-line-strong)); - background: var(--inspect-surface-muted); - color: var(--inspect-ink); - box-shadow: none; -} - -.backLink, -.foldButton { - display: inline-flex; - align-items: center; - gap: 8px; - min-height: 36px; - padding: 0 14px; - border-radius: 12px; - border: 1px solid color-mix(in srgb, var(--inspect-line) 80%, transparent); - background: color-mix(in srgb, var(--inspect-surface-muted) 78%, var(--inspect-surface)); - color: var(--inspect-muted); - font-size: 13px; - font-weight: 700; - text-decoration: none; - cursor: pointer; - transition: - border-color 0.18s ease, - background-color 0.18s ease, - color 0.18s ease; -} - -.backLink:hover, -.foldButton:hover { - border-color: color-mix(in srgb, var(--inspect-accent) 28%, var(--inspect-line-strong)); - background: color-mix(in srgb, var(--inspect-accent) 6%, var(--inspect-surface-muted)); - color: var(--inspect-ink); -} - -.foldButton:disabled { - cursor: not-allowed; - opacity: 0.6; - transform: none; -} - -.controlLayout { - display: grid; - gap: 12px; -} - -.metaRow { - margin-top: 0; -} - -.metaPill, -.panelMeta span { - display: inline-flex; - align-items: center; - min-height: 30px; - padding: 0 10px; - border-radius: 999px; - background: var(--inspect-surface-muted); - color: var(--inspect-muted); - font-size: 12px; - font-weight: 500; -} - -.progressSection { - display: grid; - gap: 8px; - margin-top: 0; - padding: 12px 14px; - border-radius: 10px; - border: 1px solid color-mix(in srgb, var(--inspect-line) 74%, transparent); - background: color-mix(in srgb, var(--inspect-surface-muted) 62%, transparent); -} - -.progressHeader { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.progressFooter { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.progressMeta { - display: flex; - align-items: center; - gap: 12px; - min-width: 0; -} - -.progressHeader strong, -.progressHeader span { - font-size: 13px; - line-height: 1.3; -} - -.progressHeader span { - color: var(--inspect-ink); - font-weight: 700; -} - -.progressTrack { - position: relative; - overflow: hidden; - height: 8px; - border-radius: 999px; - background: color-mix(in srgb, var(--inspect-line) 68%, transparent); -} - -.progressBar { - position: absolute; - inset: 0 auto 0 0; - border-radius: inherit; - background: linear-gradient(135deg, var(--inspect-accent), #0ea5e9); -} - -.progressActions { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; - gap: 8px; - flex-shrink: 0; -} - -.progressActions :global(.btn) { - padding: 8px 12px; -} - -.progressMeta { - color: var(--inspect-muted); - font-size: 12px; - line-height: 1.5; -} - -.progressMeta strong { - color: var(--inspect-amber); -} - -.summarySection { - display: grid; - gap: 10px; -} - -.summarySectionHeader { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.summarySectionHeader > div { - display: grid; - gap: 4px; - min-width: 0; -} - -.summarySectionHeader h2 { - margin: 0; - font-size: 16px; - line-height: 1.4; -} - -.summarySectionHeader p { - margin: 0; - color: var(--inspect-muted); - font-size: 13px; - line-height: 1.5; -} - -.cockpitSection, -.workbenchSection, -.operationSection, -.resultsSection, -.logsSection { - display: grid; - gap: 16px; - min-width: 0; -} - -.operationModuleHeader { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: center; - gap: 18px; - min-width: 0; - padding: 0; - border: 0; - background: transparent; -} - -.operationModuleHeader > div:first-child { - display: grid; - gap: 8px; - min-width: 0; -} - -.operationModuleHeader > .foldButton { - justify-self: end; -} - -.resultToolbar { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; - align-items: center; - gap: 8px; - min-width: 0; -} - -.resultProviderSelect { - flex: 0 0 172px; - width: 172px; - min-width: 0; -} - -.resultProviderSelectTrigger { - height: 36px; - min-height: 36px; - padding: 0 12px; - border-color: color-mix(in srgb, var(--inspect-line) 80%, transparent); - border-radius: 999px; - background: color-mix(in srgb, var(--inspect-surface-muted) 78%, var(--inspect-surface)); - color: var(--inspect-ink); - font-size: 12px; - font-weight: 700; - box-shadow: none; -} - -.resultProviderSelectTrigger:hover:not(:disabled), -.resultProviderSelectTrigger[aria-expanded='true'] { - border-color: color-mix(in srgb, var(--inspect-accent) 36%, var(--inspect-line)); - background: color-mix(in srgb, var(--inspect-accent) 7%, var(--inspect-surface)); - box-shadow: none; -} - -.resultProviderSelectDropdown { - border-radius: 12px; -} - -.operationModuleHeader h2 { - margin: 0; - color: var(--inspect-ink); - font-size: 22px; - font-weight: 800; - letter-spacing: -0.03em; - line-height: 1.2; -} - -.operationModuleHeader p { - margin: 0; - max-width: 760px; - color: var(--inspect-muted); - font-size: 13px; - line-height: 1.55; -} - -.resultFilterControl { - display: inline-flex; - flex: 0 0 auto; - max-width: 100%; - gap: 3px; - overflow-x: auto; - padding: 3px; - border: 1px solid color-mix(in srgb, var(--inspect-line) 80%, transparent); - border-radius: 999px; - background: color-mix(in srgb, var(--inspect-surface-muted) 78%, var(--inspect-surface)); - scrollbar-width: none; -} - -.resultFilterControl::-webkit-scrollbar { - display: none; -} - -.resultFilterButton { - display: inline-flex; - align-items: center; - gap: 6px; - flex: 0 0 auto; - min-height: 28px; - padding: 0 10px; - border: 0; - border-radius: 999px; - background: transparent; - color: var(--inspect-muted); - font-size: 12px; - font-weight: 700; - cursor: pointer; - transition: - background-color 0.18s ease, - color 0.18s ease, - box-shadow 0.18s ease; -} - -.resultFilterButton:hover { - color: var(--inspect-ink); -} - -.resultFilterButtonActive { - background: var(--inspect-accent); - color: var(--primary-contrast, #fff); - box-shadow: 0 8px 18px -14px rgba(0, 0, 0, 0.45); -} - -.logHeaderActions { - display: inline-flex; - flex-wrap: wrap; - align-items: center; - justify-content: flex-end; - gap: 10px; - min-width: 0; -} - -.inspectionOperationGrid { - display: grid; - grid-template-columns: minmax(0, 1.24fr) minmax(360px, 0.76fr); - align-items: stretch; - gap: 16px; - min-width: 0; -} - -.operationMainColumn { - display: grid; - min-width: 0; -} - - -.cockpitHeader { - display: flex; - align-items: flex-end; - justify-content: space-between; - gap: 18px; - min-width: 0; -} - -.cockpitHeader > div:first-child { - display: grid; - gap: 8px; - min-width: 0; -} - -.cockpitHeader h2 { - margin: 0; - color: var(--inspect-ink); - font-size: 22px; - font-weight: 800; - letter-spacing: -0.03em; - line-height: 1.2; -} - -.cockpitHeader p { - margin: 0; - max-width: 760px; - color: var(--inspect-muted); - font-size: 13px; - line-height: 1.55; -} - -.assetOverviewGrid { - display: grid; - grid-template-columns: minmax(0, 1.08fr) minmax(360px, 0.92fr); - align-items: stretch; - gap: 16px; - min-width: 0; -} - -.assetKpiGrid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; - min-width: 0; -} - -.professionalMetricCard { - --metric-accent: var(--inspect-accent); - --metric-accent-soft: rgba(37, 99, 235, 0.12); - display: grid; - gap: 16px; - min-width: 0; - min-height: 128px; - padding: 18px; - border: 1px solid color-mix(in srgb, var(--metric-accent) 18%, var(--inspect-line)); - border-radius: 12px; - background: - linear-gradient(180deg, color-mix(in srgb, var(--metric-accent) 5%, var(--inspect-surface)), var(--inspect-surface)); - box-shadow: var(--shadow); - transition: - transform 0.18s ease, - border-color 0.18s ease, - box-shadow 0.18s ease; -} - -.professionalMetricCard:hover { - transform: translateY(-1px); - border-color: color-mix(in srgb, var(--metric-accent) 32%, var(--inspect-line)); - box-shadow: 0 16px 34px -28px var(--metric-accent); -} - -.professionalMetricCard1, -.professionalMetricCard4 { - --metric-accent: #2563eb; - --metric-accent-soft: rgba(37, 99, 235, 0.12); -} - -.professionalMetricCard2, -.professionalMetricCard5 { - --metric-accent: #16a34a; - --metric-accent-soft: rgba(22, 163, 74, 0.12); -} - -.professionalMetricCard3, -.professionalMetricCard6 { - --metric-accent: #d97706; - --metric-accent-soft: rgba(217, 119, 6, 0.12); -} - -.professionalMetricHeader { - display: flex; - align-items: center; - gap: 10px; - min-width: 0; -} - -.professionalMetricHeader strong { - min-width: 0; - overflow: hidden; - color: var(--inspect-ink); - font-size: 14px; - font-weight: 700; - text-overflow: ellipsis; - white-space: nowrap; -} - -.professionalMetricIcon { - position: relative; - flex: 0 0 auto; - width: 30px; - height: 30px; - border-radius: 10px; - background: var(--metric-accent-soft); - color: var(--metric-accent); -} - -.professionalMetricIcon::before, -.professionalMetricIcon::after { - content: ''; - position: absolute; - border-radius: 999px; - background: currentColor; -} - -.professionalMetricIcon::before { - left: 8px; - right: 8px; - bottom: 7px; - height: 4px; -} - -.professionalMetricIcon::after { - width: 5px; - height: 12px; - right: 9px; - bottom: 10px; - box-shadow: -7px 4px 0 currentColor; -} - -.professionalMetricBody { - display: grid; - gap: 8px; -} - -.professionalMetricBody span { - color: var(--inspect-muted); - font-size: 12px; - line-height: 1.35; -} - -.professionalMetricBody strong { - min-width: 0; - overflow: hidden; - color: var(--inspect-ink); - font-size: 26px; - font-weight: 850; - letter-spacing: -0.035em; - line-height: 1; - text-overflow: ellipsis; - white-space: nowrap; -} - -.providerDistributionCard { - display: grid; - grid-template-rows: auto minmax(0, 1fr); - align-content: stretch; - gap: 16px; - min-width: 0; - height: 360px; - padding: 20px; - border: 1px solid var(--inspect-line); - border-radius: 12px; - background: var(--inspect-surface); - box-shadow: var(--shadow); -} - -.providerDistributionHeader { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; - min-width: 0; -} - -.providerDistributionHeader > div { - display: grid; - gap: 6px; - min-width: 0; -} - -.providerDistributionHeader h3 { - margin: 0; - color: var(--inspect-ink); - font-size: 18px; - font-weight: 750; - letter-spacing: -0.02em; - line-height: 1.25; -} - -.providerDistributionHeader p { - margin: 0; - color: var(--inspect-muted); - font-size: 13px; - line-height: 1.5; -} - -.providerDistributionSelection:global(.btn) { - display: inline-flex; - align-items: center; - flex: 0 1 auto; - min-width: 0; - min-height: 34px; - max-width: 220px; - padding: 0 12px; - border: 1px solid color-mix(in srgb, var(--inspect-accent) 18%, var(--inspect-line)); - border-radius: 8px; - background: color-mix(in srgb, var(--inspect-accent) 7%, var(--inspect-surface)); - color: var(--inspect-accent); - font-size: 12px; - font-weight: 700; - box-shadow: none; -} - -.providerDistributionSelection:global(.btn) > span { - display: inline-flex; - align-items: center; - gap: 6px; - min-width: 0; - overflow: hidden; - line-height: 1; - text-overflow: ellipsis; - white-space: nowrap; -} - -.providerDistributionSelection:global(.btn) svg { - flex: 0 0 auto; -} - -.providerDistributionSelection:global(.btn):hover:not(:disabled) { - transform: translateY(-1px); - border-color: color-mix(in srgb, var(--inspect-accent) 38%, var(--inspect-line)); - background: color-mix(in srgb, var(--inspect-accent) 12%, var(--inspect-surface)); - box-shadow: none; -} - -.providerSelectorList { - display: grid; - align-content: start; - gap: 10px; - min-width: 0; - min-height: 0; - overflow: auto; - padding-right: 2px; -} - -.providerSelectorRow { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(120px, 0.45fr); - align-items: center; - gap: 14px; - min-width: 0; - padding: 12px 14px; - border: 1px solid color-mix(in srgb, var(--inspect-line) 78%, transparent); - border-radius: 12px; - background: color-mix(in srgb, var(--inspect-surface-muted) 45%, var(--inspect-surface)); - color: var(--inspect-ink); - text-align: left; - cursor: pointer; - transition: - transform 0.18s ease, - border-color 0.18s ease, - background-color 0.18s ease, - box-shadow 0.18s ease; -} - -.providerSelectorRow:hover { - transform: translateY(-1px); - border-color: color-mix(in srgb, var(--inspect-accent) 28%, var(--inspect-line)); - background: color-mix(in srgb, var(--inspect-accent) 5%, var(--inspect-surface)); -} - -.providerSelectorRowActive { - border-color: color-mix(in srgb, var(--inspect-accent) 44%, var(--inspect-line)); - background: color-mix(in srgb, var(--inspect-accent) 9%, var(--inspect-surface)); - box-shadow: 0 14px 28px -26px var(--inspect-accent); -} - -.providerSelectorRow div { - display: grid; - gap: 4px; - min-width: 0; -} - -.providerSelectorRow strong, -.providerSelectorRow span { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.providerSelectorTitle { - display: inline-flex !important; - align-items: center; - gap: 8px; - min-width: 0; -} - -.providerSelectorTitle img, -.providerLogoFallback { - flex: 0 0 auto; - width: 24px; - height: 24px; - border-radius: 8px; -} - -.providerSelectorTitle img { - display: block; - object-fit: contain; -} - -.providerLogoFallback { - display: inline-grid; - place-items: center; - background: color-mix(in srgb, var(--inspect-accent) 12%, var(--inspect-surface)); - color: var(--inspect-accent); - font-size: 11px; - font-weight: 800; -} - -.providerSelectorTitle strong { - color: var(--inspect-ink); - font-size: 14px; - font-weight: 750; -} - - -.providerSelectorRow > span { - display: block; - height: 8px; - border-radius: 999px; - background: color-mix(in srgb, var(--inspect-line) 62%, transparent); -} - -.providerSelectorRow > span i { - display: block; - width: var(--bar-width, 0%); - height: 100%; - border-radius: inherit; - background: var(--bar-color, var(--inspect-accent)); - transition: width 0.24s ease, background-color 0.24s ease; -} - -.assetWorkbenchGrid, -.actionWorkbenchGrid, -.cockpitGrid, -.workbenchGrid { - display: grid; - align-items: stretch; - gap: 16px; - min-width: 0; -} - -.assetWorkbenchGrid, -.actionWorkbenchGrid { - grid-template-columns: 1fr; -} - -.cockpitGrid { - grid-template-columns: minmax(0, 1.2fr) minmax(360px, 0.8fr); -} - -.workbenchGrid { - grid-template-columns: minmax(0, 1.18fr) minmax(360px, 0.82fr); -} - -.assetIntelligenceCard, -.inspectionStatusCard, -.inspectionControlCard, -.actionStudioCard { - display: grid; - gap: 14px; - min-width: 0; - padding: 18px; - border: 1px solid color-mix(in srgb, var(--inspect-line) 82%, transparent); - border-radius: 14px; - background: var(--inspect-surface); - box-shadow: 0 14px 32px -30px rgba(15, 23, 42, 0.55); -} - -.operationMainStack, -.actionStudioCard { - display: grid; - align-content: stretch; - gap: 12px; - min-width: 0; - min-height: 100%; -} - -.operationMainStack { - grid-template-rows: minmax(0, 1fr) auto; -} - -.inspectionStatusCard { - gap: 14px; - border-color: color-mix(in srgb, var(--inspect-accent) 14%, var(--inspect-line)); - background: - linear-gradient(180deg, color-mix(in srgb, var(--inspect-accent) 3%, var(--inspect-surface)), var(--inspect-surface)); -} - -.inspectionProgressHero { - display: grid; - grid-template-columns: auto minmax(0, 1fr) auto; - align-items: center; - gap: 16px; - min-width: 0; -} - -.progressRing { - --ring-track: color-mix(in srgb, var(--inspect-green) 14%, var(--inspect-line)); - --ring-value: var(--inspect-green); - display: grid; - place-items: center; - width: 104px; - aspect-ratio: 1; - border-radius: 50%; - background: conic-gradient(var(--ring-value) var(--progress, 0%), var(--ring-track) 0); - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--inspect-green) 14%, transparent); -} - -.progressRing::before { - content: ''; - grid-area: 1 / 1; - width: 74%; - aspect-ratio: 1; - border-radius: 50%; - background: var(--inspect-surface); - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--inspect-line) 70%, transparent); -} - -.progressRing strong { - grid-area: 1 / 1; - z-index: 1; - color: var(--inspect-ink); - font-size: 20px; - font-weight: 800; - letter-spacing: -0.03em; -} - -.inspectionStatusCopy { - display: grid; - gap: 6px; - min-width: 0; -} - -.inspectionStatusCopy strong { - color: var(--inspect-ink); - font-size: 20px; - font-weight: 750; - line-height: 1.2; -} - -.inspectionStatusCopy span, -.inspectionStatusCopy small { - color: var(--inspect-muted); - font-size: 13px; - line-height: 1.45; -} - -.inspectionConfigGrid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 8px; - min-width: 0; -} - -.inspectionConfigGrid span { - display: grid; - gap: 6px; - min-width: 0; - min-height: 68px; - padding: 12px; - border: 1px solid color-mix(in srgb, var(--inspect-line) 66%, transparent); - border-radius: 10px; - background: color-mix(in srgb, var(--inspect-surface-muted) 28%, var(--inspect-surface)); -} - -.inspectionConfigGrid small { - min-width: 0; - overflow: hidden; - color: var(--inspect-muted); - font-size: 11px; - font-weight: 650; - line-height: 1.35; - text-overflow: ellipsis; - white-space: nowrap; -} - -.inspectionConfigGrid strong { - min-width: 0; - overflow: hidden; - color: var(--inspect-ink); - font-size: 15px; - font-weight: 750; - line-height: 1.25; - text-overflow: ellipsis; -} - -.inspectionNextRunText { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; - gap: 6px; - min-width: 0; - color: var(--inspect-muted); - font-size: 12px; - line-height: 1.35; - text-align: right; -} - -.inspectionNextRunText strong { - color: var(--inspect-ink); - font-weight: 750; -} - -.inspectionControlCard { - gap: 12px; - border-color: color-mix(in srgb, var(--inspect-line-strong) 84%, transparent); - background: var(--inspect-surface); -} - -.inspectionControlCard h3, -.resultOverviewSection h3, -.strategySectionHeader h3, -.manualPendingSection h3 { - margin: 0; - color: var(--inspect-ink); - font-size: 15px; - font-weight: 750; - letter-spacing: 0; - line-height: 1.3; -} - -.inspectionControlActions { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 8px; -} - -.inspectionControlActions :global(.btn) { - min-height: 40px; - border-radius: 10px; - font-size: 13px; - font-weight: 750; - box-shadow: none; -} - -.inspectionControlActions :global(.btn:nth-child(1)) { - border-color: color-mix(in srgb, var(--inspect-green) 54%, var(--inspect-line-strong)); - background: color-mix(in srgb, var(--inspect-green) 14%, var(--inspect-surface)); - color: var(--inspect-green); -} - -.inspectionControlActions :global(.btn:nth-child(1):hover:not(:disabled)) { - transform: translateY(-1px); - border-color: color-mix(in srgb, var(--inspect-green) 68%, var(--inspect-line-strong)); - background: color-mix(in srgb, var(--inspect-green) 20%, var(--inspect-surface)); -} - -.inspectionControlActions :global(.btn:nth-child(2)) { - border-color: color-mix(in srgb, var(--inspect-amber) 48%, var(--inspect-line-strong)); - background: color-mix(in srgb, var(--inspect-amber) 12%, var(--inspect-surface)); - color: var(--inspect-amber); -} - -.inspectionControlActions :global(.btn:nth-child(2):hover:not(:disabled)) { - transform: translateY(-1px); - border-color: color-mix(in srgb, var(--inspect-amber) 64%, var(--inspect-line-strong)); - background: color-mix(in srgb, var(--inspect-amber) 18%, var(--inspect-surface)); -} - -.inspectionControlActions :global(.btn:nth-child(3)) { - border-color: color-mix(in srgb, var(--inspect-red) 46%, var(--inspect-line-strong)); - background: color-mix(in srgb, var(--inspect-red) 11%, var(--inspect-surface)); - color: var(--inspect-red); -} - -.inspectionControlActions :global(.btn:nth-child(3):hover:not(:disabled)) { - transform: translateY(-1px); - border-color: color-mix(in srgb, var(--inspect-red) 62%, var(--inspect-line-strong)); - background: color-mix(in srgb, var(--inspect-red) 17%, var(--inspect-surface)); -} - -.actionStudioCard { - grid-template-rows: auto minmax(0, 1fr); -} - -.resultOverviewSection, -.strategyResultSection, -.manualPendingSection { - display: grid; - gap: 12px; - min-width: 0; - padding: 14px; - border: 1px solid color-mix(in srgb, var(--inspect-line) 70%, transparent); - border-radius: 12px; - background: color-mix(in srgb, var(--inspect-surface-muted) 22%, var(--inspect-surface)); -} - -.resultOverviewGrid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 8px; -} - -.resultOverviewGrid span { - position: relative; - display: grid; - gap: 6px; - min-width: 0; - min-height: 68px; - padding: 12px; - overflow: hidden; - border: 1px solid color-mix(in srgb, var(--inspect-accent) 14%, var(--inspect-line)); - border-radius: 10px; - background: - linear-gradient(180deg, color-mix(in srgb, var(--inspect-accent) 4%, var(--inspect-surface)), var(--inspect-surface)); -} - -.resultOverviewGrid span::before { - content: ''; - position: absolute; - inset: 0 auto 0 0; - width: 3px; - background: var(--inspect-accent); - opacity: 0.58; -} - -.resultOverviewGrid small { - color: var(--inspect-muted); - font-size: 11px; - font-weight: 650; - line-height: 1.35; -} - -.resultOverviewGrid strong { - color: var(--inspect-ink); - font-size: 22px; - font-weight: 800; - letter-spacing: -0.03em; - line-height: 1; -} - -.resultOverviewGood { - border-color: color-mix(in srgb, var(--inspect-green) 38%, var(--inspect-line-strong)) !important; - background: - linear-gradient(180deg, color-mix(in srgb, var(--inspect-green) 9%, var(--inspect-surface)), var(--inspect-surface)) !important; -} - -.resultOverviewGood::before { - background: var(--inspect-green) !important; -} - -.resultOverviewGood strong { - color: var(--inspect-green) !important; -} - -.resultOverviewWarn { - border-color: color-mix(in srgb, var(--inspect-amber) 38%, var(--inspect-line-strong)) !important; - background: - linear-gradient(180deg, color-mix(in srgb, var(--inspect-amber) 9%, var(--inspect-surface)), var(--inspect-surface)) !important; -} - -.resultOverviewWarn::before { - background: var(--inspect-amber) !important; -} - -.resultOverviewWarn strong { - color: var(--inspect-amber) !important; -} - -.resultOverviewBad { - border-color: color-mix(in srgb, var(--inspect-red) 38%, var(--inspect-line-strong)) !important; - background: - linear-gradient(180deg, color-mix(in srgb, var(--inspect-red) 9%, var(--inspect-surface)), var(--inspect-surface)) !important; -} - -.resultOverviewBad::before { - background: var(--inspect-red) !important; -} - -.resultOverviewBad strong { - color: var(--inspect-red) !important; -} - -.strategySectionHeader { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.strategySectionHeader button, -.strategyActivityRow button { - border: 0; - background: transparent; - color: var(--inspect-accent); - font-size: 12px; - font-weight: 700; - cursor: pointer; -} - -.strategyTotalsRow { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 6px; - padding-top: 10px; - border-top: 1px solid color-mix(in srgb, var(--inspect-line) 70%, transparent); -} - -.strategyTotalsRow span { - min-width: 0; - overflow: hidden; - padding: 7px 6px; - border-radius: 8px; - background: var(--inspect-surface); - color: var(--inspect-muted); - font-size: 11px; - font-weight: 650; - text-align: center; - text-overflow: ellipsis; - white-space: nowrap; -} - -.strategyActivityRow { - display: grid; - grid-template-columns: auto auto minmax(0, 1fr) auto; - align-items: center; - gap: 10px; - min-width: 0; - padding: 10px; - border: 1px solid color-mix(in srgb, var(--inspect-line) 66%, transparent); - border-radius: 10px; - background: var(--inspect-surface); -} - -.strategyActivityIcon { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--inspect-green); - box-shadow: 0 0 0 4px color-mix(in srgb, var(--inspect-green) 12%, transparent); -} - -.strategyActivityRow span { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.strategyActivityRow strong { - min-width: 0; -} - -.strategyActivityRow span { - color: var(--inspect-muted); - font-size: 12px; -} - -.strategyActivityRow strong { - color: var(--inspect-ink); - font-size: 13px; - font-weight: 700; - line-height: 1.45; - overflow-wrap: anywhere; -} - -.manualPendingSection { - align-content: start; -} - -.manualPendingList { - display: grid; - gap: 8px; -} - -.manualPendingList span { - padding: 9px 10px; - border: 1px solid color-mix(in srgb, var(--inspect-line) 62%, transparent); - border-radius: 9px; - background: var(--inspect-surface); - color: var(--inspect-muted); - font-size: 12px; -} - -.manualPendingActions { - display: flex; - justify-content: center; - margin-top: 4px; - padding: 14px; - border-radius: 9px; - background: color-mix(in srgb, var(--inspect-line-strong) 14%, transparent); -} - -.manualPendingActions :global(.btn) { - min-height: 36px; - border-radius: 9px; - padding-inline: 18px; -} - -.manualPendingEmpty { - display: grid; - place-items: center; - align-content: center; - gap: 8px; - min-height: 136px; - padding: 16px; - border: 1px dashed color-mix(in srgb, var(--inspect-line-strong) 46%, transparent); - border-radius: 10px; - background: color-mix(in srgb, var(--inspect-surface) 78%, var(--inspect-surface-muted)); - color: var(--inspect-muted); - text-align: center; -} - -.manualPendingEmpty span { - display: grid; - place-items: center; - width: 28px; - height: 28px; - border-radius: 50%; - background: color-mix(in srgb, var(--inspect-green) 10%, var(--inspect-surface)); - color: var(--inspect-green); - font-size: 16px; - font-weight: 800; -} - -.manualPendingEmpty strong { - color: var(--inspect-ink); - font-size: 13px; -} - -.manualPendingEmpty small { - max-width: 260px; - font-size: 12px; - line-height: 1.5; -} - - -.cockpitCardHeader { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; - min-width: 0; -} - -.cockpitCardHeader > div:first-child { - min-width: 0; -} - -.cockpitCardHeader h3 { - margin: 0; - color: var(--inspect-ink); - font-size: 18px; - font-weight: 750; - letter-spacing: -0.02em; - line-height: 1.25; -} - -.cockpitCardHeader p { - margin: 6px 0 0; - color: var(--inspect-muted); - font-size: 13px; - line-height: 1.5; -} - -.assetIntelligenceLayout, -.actionStudioLayout { - display: grid; - align-items: stretch; - gap: 16px; - min-width: 0; -} - -.assetIntelligenceLayout { - grid-template-columns: minmax(260px, 0.82fr) minmax(0, 1.18fr); -} - -.actionStudioLayout { - grid-template-columns: minmax(260px, 0.72fr) minmax(0, 1.28fr); -} - -.providerRankingPanel, -.actionMetricStack { - display: grid; - align-content: start; - gap: 14px; - min-width: 0; - padding: 16px; - border: 1px solid color-mix(in srgb, var(--inspect-line) 76%, transparent); - border-radius: 12px; - background: color-mix(in srgb, var(--inspect-surface-muted) 58%, var(--inspect-surface)); -} - -.providerRankingPanel .chartBarList { - max-height: 260px; - overflow: auto; -} - -.actionMetricStack { - background: transparent; - border: 0; - padding: 0; -} - -.summaryGrid, -.summaryGridCompact, -.riskGrid { - display: grid; - gap: 12px; -} - -.assetOverviewLayout { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(360px, 0.8fr); - gap: 16px; -} - -.assetInsightsMain, -.inspectionStatsMain { - display: grid; - gap: 12px; - min-width: 0; -} - -.assetMetricGrid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; -} - -.assetMetricCard { - min-height: 112px; -} - -.assetMetricCard strong { - font-size: 26px; -} - -.chartGrid, -.inspectionStatsLayout { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; - min-width: 0; -} - -.inspectionStatsLayout { - grid-template-columns: minmax(0, 1fr) minmax(340px, 0.42fr); - align-items: stretch; -} - -.chartPanel { - display: grid; - min-width: 0; - min-height: 260px; - padding: 20px; - border: 1px solid var(--inspect-line); - border-radius: 12px; - background: var(--inspect-surface); - box-shadow: var(--shadow); - overflow: visible; -} - -.chartDonutPanel { - display: grid; - gap: 16px; - min-width: 0; -} - -.chartDonutHeader { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; - min-width: 0; -} - -.chartDonutHeader strong { - font-size: 15px; - line-height: 1.25; -} - -.chartDonutHeader span { - color: var(--inspect-muted); - font-size: 12px; -} - -.chartDonutLayout { - display: grid; - grid-template-columns: minmax(128px, 0.8fr) minmax(0, 1fr); - align-items: center; - gap: 18px; - min-width: 0; -} - -.donutChart { - position: relative; - display: grid; - place-items: center; - width: min(168px, 100%); - aspect-ratio: 1; - border-radius: 50%; - background: var(--donut-bg); -} - -.donutChart::after { - content: ''; - position: absolute; - inset: 22%; - border-radius: 50%; - background: var(--inspect-surface); - box-shadow: inset 0 0 0 1px var(--inspect-line); -} - -.donutCenter { - position: relative; - z-index: 1; - display: grid; - gap: 5px; - justify-items: center; - max-width: 78%; - text-align: center; -} - -.donutCenter span { - color: var(--inspect-muted); - font-size: 12px; -} - -.donutCenter strong { - max-width: 100%; - overflow: hidden; - font-size: 18px; - font-weight: 800; - text-overflow: ellipsis; - white-space: nowrap; -} - -.donutTooltip { - position: absolute; - left: calc(100% + 14px); - top: 50%; - z-index: 5; - width: min(300px, calc(100vw - 48px)); - max-height: 260px; - overflow: auto; - padding: 12px; - border-radius: 12px; - border: 1px solid var(--inspect-line); - background: color-mix(in srgb, var(--inspect-surface-elevated) 96%, var(--inspect-bg)); - box-shadow: 0 18px 42px -26px rgba(15, 23, 42, 0.55); - opacity: 0; - pointer-events: none; - transform: translate(0, -50%); - transition: opacity 0.16s ease; -} - -.donutChart:hover .donutTooltip, -.donutChart:focus-within .donutTooltip { - opacity: 1; -} - -.donutTooltip strong { - display: block; - margin-bottom: 8px; - color: var(--inspect-ink); - font-size: 12px; -} - -.donutTooltip div { - display: grid; - gap: 7px; -} - -.donutTooltip span { - display: grid; - grid-template-columns: 8px minmax(0, 1fr) auto; - align-items: center; - gap: 8px; - min-width: 0; - color: var(--inspect-muted); - font-size: 12px; -} - -.donutTooltip i, -.chartLegendDot { - width: 8px; - height: 8px; - border-radius: 999px; -} - -.donutTooltip em { - min-width: 0; - overflow: hidden; - font-style: normal; - text-overflow: ellipsis; - white-space: nowrap; -} - -.donutTooltip b { - color: var(--inspect-ink); - font-size: 12px; -} - -.chartLegend, -.chartBarList { - display: grid; - gap: 8px; - min-width: 0; -} - -.chartLegendItem { - display: grid; - grid-template-columns: 10px minmax(0, 1fr) auto; - align-items: center; - gap: 8px; - min-width: 0; - color: var(--inspect-muted); - font-size: 12px; -} - -.chartLegendDot { - background: var(--legend-color, var(--inspect-accent)); -} - -.chartLegendItem span:nth-child(2) { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.chartLegendItem strong { - color: var(--inspect-ink); - font-size: 12px; -} - -.chartBarRow { - display: grid; - gap: 8px; - min-width: 0; - padding: 10px 0; - border-bottom: 1px solid color-mix(in srgb, var(--inspect-line) 58%, transparent); -} - -.chartBarRow:last-child { - border-bottom: 0; -} - -.chartBarRow div { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - min-width: 0; -} - -.chartBarRow div span { - min-width: 0; - overflow: hidden; - color: var(--inspect-muted); - font-size: 12px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.chartBarRow div strong { - color: var(--inspect-ink); - font-size: 13px; -} - -.chartBarRow > span { - display: block; - height: 7px; - overflow: hidden; - border-radius: 999px; - background: color-mix(in srgb, var(--inspect-line) 58%, transparent); -} - -.chartBarRow > span::before { - content: ''; - display: block; - width: var(--bar-width, 0%); - height: 100%; - border-radius: inherit; - background: var(--bar-color, var(--inspect-accent)); -} - -.providerPanel { - display: grid; - gap: 14px; - padding: 18px; - border: 1px solid var(--inspect-line); - border-radius: 12px; - background: var(--inspect-surface); -} - -.providerPanelHeader { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 12px; -} - -.providerPanelHeader strong { - font-size: 15px; -} - -.providerPanelHeader span { - color: var(--inspect-muted); - font-size: 12px; -} - -.providerList { - display: grid; - gap: 8px; - max-height: 260px; - overflow: auto; -} - -.providerRow { - display: grid; - grid-template-columns: minmax(120px, 0.8fr) minmax(0, 1.2fr); - gap: 12px; - padding: 12px; - border: 1px solid color-mix(in srgb, var(--inspect-line) 70%, transparent); - border-radius: 10px; - background: color-mix(in srgb, var(--inspect-surface-muted) 50%, transparent); -} - -.providerNameCell { - display: grid; - gap: 4px; -} - -.providerNameCell strong { - font-size: 14px; -} - -.providerNameCell span { - color: var(--inspect-muted); - font-size: 12px; -} - -.providerStatusGrid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 6px 10px; - align-content: center; -} - -.providerStatusGrid span { - color: var(--inspect-muted); - font-size: 12px; - line-height: 1.4; -} - -.actionBreakdownGrid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; -} - -.actionBreakdownCard { - display: grid; - gap: 10px; - padding: 16px; - border: 1px solid var(--inspect-line); - border-radius: 12px; - background: color-mix(in srgb, var(--inspect-surface-muted) 58%, var(--inspect-surface)); -} - -.actionBreakdownCard strong { - font-size: 14px; -} - -.actionBreakdownCard div { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.actionBreakdownCard span { - display: inline-flex; - min-height: 28px; - align-items: center; - padding: 0 10px; - border-radius: 999px; - background: var(--inspect-surface); - color: var(--inspect-muted); - font-size: 12px; -} - -.riskGrid { - grid-template-columns: repeat(4, minmax(0, 1fr)); -} - -.summaryGrid { - grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); -} - -.summaryGridCompact { - grid-template-columns: repeat(5, minmax(120px, 1fr)); -} - - -.summaryCard { - display: grid; - gap: 4px; - padding: 16px; - border-radius: 8px; -} - -.summaryCard span { - color: var(--inspect-muted); - font-size: 14px; - font-weight: 500; -} - -.summaryCard strong { - font-size: 20px; - font-weight: 800; - line-height: 1.1; - letter-spacing: 0; -} - -.summaryGood { - border-color: color-mix(in srgb, var(--inspect-green) 28%, var(--inspect-line)); - background: color-mix(in srgb, var(--inspect-green) 7%, var(--inspect-surface)); -} - -.summaryWarn { - border-color: color-mix(in srgb, var(--inspect-amber) 30%, var(--inspect-line)); - background: color-mix(in srgb, var(--inspect-amber) 7%, var(--inspect-surface)); -} - -.summaryBad { - border-color: color-mix(in srgb, var(--inspect-red) 30%, var(--inspect-line)); - background: color-mix(in srgb, var(--inspect-red) 7%, var(--inspect-surface)); -} - -.riskCard { - min-height: 112px; -} - -.riskCard strong { - font-size: 24px; -} - -.controlPanel { - border-color: color-mix(in srgb, var(--inspect-accent) 16%, var(--inspect-line)); - background: - linear-gradient( - 180deg, - color-mix(in srgb, var(--inspect-accent) 3%, var(--inspect-surface)), - var(--inspect-surface) - ); -} - -.policyBanner { - display: flex; - align-items: center; - justify-content: space-between; - gap: 14px; - padding: 14px 16px; - border-radius: 12px; - border: 1px solid color-mix(in srgb, var(--inspect-accent) 18%, var(--inspect-line)); - background: color-mix(in srgb, var(--inspect-accent) 7%, var(--inspect-surface-muted)); -} - -.policyBanner strong { - color: var(--inspect-ink); - font-size: 14px; -} - -.policyBanner span { - color: var(--inspect-muted); - font-size: 13px; - line-height: 1.5; - text-align: right; -} - -.panel { - display: grid; - gap: 16px; -} - -.settingsModal :global(.modal-body) { - display: grid; - gap: 0; - max-height: min(78vh, 820px); - overflow: hidden; - padding: 0; -} - -.settingsWorkbench { - display: grid; - grid-template-columns: 260px minmax(0, 1fr); - min-height: 620px; - overflow: hidden; - border: 1px solid color-mix(in srgb, var(--inspect-line) 74%, transparent); - border-radius: 18px; - background: var(--inspect-surface); -} - -.settingsSidebar { - display: grid; - grid-template-rows: auto minmax(0, 1fr) auto; - gap: 16px; - min-width: 0; - padding: 18px; - border-right: 1px solid color-mix(in srgb, var(--inspect-line) 74%, transparent); - background: - radial-gradient(circle at top left, color-mix(in srgb, var(--inspect-accent) 12%, transparent), transparent 34%), - color-mix(in srgb, var(--inspect-surface-muted) 64%, var(--inspect-surface)); -} - -.settingsSidebarIntro { - display: grid; - gap: 8px; -} - -.settingsSidebarIntro strong { - color: var(--inspect-ink); - font-size: 16px; - font-weight: 780; - line-height: 1.35; -} - -.settingsSidebarIntro span { - color: var(--inspect-muted); - font-size: 12px; - line-height: 1.55; -} - -.settingsSidebarNav { - display: grid; - align-content: start; - gap: 8px; - min-height: 0; - overflow: auto; - padding-right: 2px; -} - -.settingsSidebarNav button { - display: grid; - gap: 5px; - width: 100%; - padding: 12px; - border: 1px solid transparent; - border-radius: 12px; - background: transparent; - color: inherit; - text-align: left; - cursor: pointer; - transition: background 0.16s ease, border-color 0.16s ease, transform 0.16s ease; -} - -.settingsSidebarNav button:hover { - border-color: color-mix(in srgb, var(--inspect-accent) 18%, var(--inspect-line)); - background: color-mix(in srgb, var(--inspect-accent) 7%, var(--inspect-surface)); - transform: translateX(2px); -} - -.settingsSidebarNav strong { - color: var(--inspect-ink); - font-size: 13px; - font-weight: 730; - line-height: 1.35; -} - -.settingsSidebarNav span { - min-width: 0; - overflow: hidden; - color: var(--inspect-muted); - font-size: 11px; - line-height: 1.35; - text-overflow: ellipsis; - white-space: nowrap; -} - -.settingsSidebarStatus { - display: grid; - gap: 8px; - padding-top: 14px; - border-top: 1px solid color-mix(in srgb, var(--inspect-line) 72%, transparent); -} - -.settingsSidebarStatus span, -.settingsSummaryGrid span { - display: grid; - gap: 6px; - min-width: 0; - padding: 11px 12px; - border: 1px solid color-mix(in srgb, var(--inspect-line) 72%, transparent); - border-radius: 12px; - background: color-mix(in srgb, var(--inspect-surface) 86%, var(--inspect-surface-muted)); -} - -.settingsSidebarStatus small, -.settingsSummaryGrid small { - min-width: 0; - overflow: hidden; - color: var(--inspect-muted); - font-size: 11px; - font-weight: 650; - line-height: 1.35; - text-overflow: ellipsis; - white-space: nowrap; -} - -.settingsSidebarStatus strong, -.settingsSummaryGrid strong { - min-width: 0; - overflow: hidden; - color: var(--inspect-ink); - font-size: 13px; - font-weight: 760; - line-height: 1.35; - text-overflow: ellipsis; -} - -.settingsWorkbenchMain { - display: grid; - align-content: start; - gap: 14px; - min-width: 0; - max-height: 620px; - overflow: auto; - padding: 18px; - scroll-behavior: smooth; -} - -.settingsHeroPanel, -.settingsWorkbenchSection { - display: grid; - gap: 16px; - min-width: 0; - border: 1px solid color-mix(in srgb, var(--inspect-line) 78%, transparent); - border-radius: 16px; - background: var(--inspect-surface-elevated); - box-shadow: 0 10px 28px color-mix(in srgb, var(--inspect-shadow) 6%, transparent); -} - -.settingsHeroPanel { - padding: 16px; - background: - linear-gradient(135deg, color-mix(in srgb, var(--inspect-accent) 8%, var(--inspect-surface-elevated)), var(--inspect-surface-elevated) 52%), - var(--inspect-surface-elevated); -} - -.settingsHeroPanel > div:first-child, -.settingsWorkbenchHeader > div { - display: grid; - gap: 5px; - min-width: 0; -} - -.settingsHeroPanel > div:first-child strong { - color: var(--inspect-ink); - font-size: 17px; - font-weight: 780; - line-height: 1.35; -} - -.settingsHeroPanel > div:first-child span { - color: var(--inspect-muted); - font-size: 13px; - line-height: 1.55; -} - -.settingsSummaryGrid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 10px; -} - -.settingsWorkbenchSection { - scroll-margin-top: 14px; - padding: 16px; -} - -.settingsWorkbenchHeader { - display: flex; - align-items: center; - justify-content: space-between; - gap: 14px; - padding-bottom: 12px; - border-bottom: 1px solid color-mix(in srgb, var(--inspect-line) 72%, transparent); -} - -.settingsWorkbenchHeader small { - width: fit-content; - padding: 3px 7px; - border-radius: 999px; - background: color-mix(in srgb, var(--inspect-accent) 10%, transparent); - color: var(--inspect-accent); - font-size: 11px; - font-weight: 760; - line-height: 1.2; -} - -.settingsWorkbenchHeader strong { - color: var(--inspect-ink); - font-size: 15px; - font-weight: 760; - line-height: 1.35; -} - -.settingsWorkbenchHeader span { - color: var(--inspect-muted); - font-size: 12px; - line-height: 1.55; -} - -.settingsSplitGrid, -.settingsThreeGrid, -.settingsMatrixGrid, -.settingsFocusGrid, -.settingsAutomationGrid { - display: grid; - gap: 12px; - min-width: 0; -} - -.settingsSplitGrid { - grid-template-columns: minmax(0, 1.35fr) minmax(220px, 0.65fr); -} - -.settingsThreeGrid { - grid-template-columns: repeat(3, minmax(0, 1fr)); -} - -.settingsMatrixGrid { - grid-template-columns: repeat(4, minmax(0, 1fr)); -} - -.settingsFocusGrid, -.settingsAutomationGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.settingsFormPanel, -.settingsFocusCard, -.settingsAutomationCard, -.settingsRiskPanel, -.settingsInsightPanel { - display: grid; - gap: 10px; - align-content: start; - min-width: 0; - padding: 14px; - border: 1px solid color-mix(in srgb, var(--inspect-line) 72%, transparent); - border-radius: 14px; - background: color-mix(in srgb, var(--inspect-surface-muted) 44%, var(--inspect-surface)); -} - -.settingsInsightPanel { - align-content: center; - background: color-mix(in srgb, var(--inspect-accent) 6%, var(--inspect-surface)); -} - -.settingsInsightPanel small, -.settingsFocusCard span, -.settingsAutomationCard span, -.settingsRiskPanel span { - color: var(--inspect-muted); - font-size: 12px; - line-height: 1.55; -} - -.settingsInsightPanel strong, -.settingsFocusCard strong { - min-width: 0; - overflow: hidden; - color: var(--inspect-ink); - font-size: 15px; - font-weight: 760; - line-height: 1.35; - text-overflow: ellipsis; -} - -.settingsInsightPanel span { - color: var(--inspect-accent); - font-size: 12px; - font-weight: 700; - line-height: 1.4; -} - -.settingsAutomationStack { - display: grid; - gap: 12px; -} - -.settingsRiskPanel { - border-color: color-mix(in srgb, var(--inspect-amber) 22%, var(--inspect-line)); - background: color-mix(in srgb, var(--inspect-amber) 4%, var(--inspect-surface)); -} - -.settingsPolicyControl { - display: flex; - align-items: center; - min-height: 38px; -} - -.settingsLabel { - display: block; - margin-bottom: 6px; - color: var(--inspect-text); - font-size: 13px; - font-weight: 650; -} - -.settingsHint { - color: var(--inspect-muted); - font-size: 12px; - line-height: 1.5; -} - -.settingsDangerNote { - padding: 9px 10px; - border: 1px solid color-mix(in srgb, var(--inspect-red) 32%, transparent); - border-radius: 10px; - background: color-mix(in srgb, var(--inspect-red) 8%, transparent); - color: var(--inspect-red); - font-size: 12px; - font-weight: 650; - line-height: 1.5; -} - -.settingsMutedField { - opacity: 0.58; -} - -.settingsFormPanel :global(.form-group), -.settingsFocusCard :global(.form-group), -.settingsRiskPanel :global(.form-group), -.settingsMatrixGrid :global(.form-group) { - margin-bottom: 0; -} - -.settingsFormPanel :global(.input), -.settingsFocusCard :global(.input), -.settingsRiskPanel :global(.input), -.settingsMatrixGrid :global(.input), -.settingsFormPanel input, -.settingsFocusCard input, -.settingsRiskPanel input, -.settingsMatrixGrid input { - min-height: 42px; -} - -.settingsActionsBar { - position: sticky; - bottom: 0; - z-index: 2; - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: 10px; - margin: 0; - padding: 14px 18px; - border: 1px solid color-mix(in srgb, var(--inspect-line) 74%, transparent); - border-top: 0; - border-radius: 0 0 18px 18px; - background: - linear-gradient( - 180deg, - color-mix(in srgb, var(--inspect-surface-elevated) 78%, transparent), - var(--inspect-surface-elevated) 38% - ); -} - -.settingsActionsRight { - display: flex; - justify-content: flex-end; - flex-wrap: wrap; - gap: 10px; -} - -.resultsHeaderActions { - display: flex; - align-items: center; - justify-content: flex-end; - flex-wrap: wrap; - gap: 10px; -} - -.panelTitle { - font-size: 20px; -} - -.panelSubtitle { - margin-top: 4px; - font-size: 13px; -} - -.logList { - display: grid; - gap: 8px; - max-height: 360px; - overflow: auto; - padding-right: 4px; -} - -.logRow { - display: grid; - grid-template-columns: 180px minmax(0, 1fr); - gap: 12px; - align-items: start; - padding: 12px 14px; - border: 1px solid var(--inspect-line); - border-radius: 16px; - background: var(--inspect-surface-muted); - font-size: 12px; -} - -.logInfo { - color: var(--inspect-ink); -} - -.logSuccess { - color: var(--inspect-green); -} - -.logWarning { - color: var(--inspect-amber); - background: color-mix(in srgb, var(--inspect-amber) 10%, transparent); -} - -.logError { - color: var(--inspect-red); - background: color-mix(in srgb, var(--inspect-red) 10%, transparent); -} - -.logTime { - font-size: 12px; - font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); -} - -.logMessage { - word-break: break-word; - line-height: 1.6; -} - -.logCollapsedBar { - padding: 14px 16px; - border-radius: 16px; - background: var(--inspect-surface-muted); - color: var(--inspect-muted); -} - -.renderLimitHint { - padding: 10px 12px; - border: 1px solid var(--inspect-line); - border-radius: 8px; - background: var(--inspect-surface-muted); - color: var(--inspect-muted); - font-size: 12px; - line-height: 1.45; -} - -.previewSection { - display: grid; - gap: 12px; - padding: 18px; - border-radius: 18px; - background: color-mix(in srgb, var(--inspect-accent) 4%, var(--inspect-surface-muted)); - border: 1px solid var(--inspect-line); -} - -.previewHeader { - display: flex; - justify-content: space-between; - gap: 12px; - align-items: center; -} - -.previewHeader strong { - font-size: 15px; -} - -.previewHint { - color: var(--inspect-muted); - font-size: 12px; -} - -.previewList { - margin: 0; - padding-left: 18px; - display: grid; - gap: 8px; -} - -.previewList li { - line-height: 1.6; -} - -.healthCell { - display: inline-flex; - align-items: flex-start; - max-width: 100%; -} - -.healthBadge { - display: inline-flex; - align-items: center; - min-height: 24px; - padding: 0 8px; - border-radius: 999px; - border: 1px solid transparent; - font-size: 12px; - font-weight: 600; - line-height: 1.35; - text-align: left; - white-space: normal; - overflow-wrap: anywhere; -} - -.healthErrorButton { - display: inline-flex; - align-items: center; - min-width: 0; - padding: 0; - border: 0; - border-radius: 999px; - background: transparent; - color: inherit; - cursor: pointer; -} - -.healthErrorButton:hover { - filter: brightness(0.97); -} - -.healthErrorButton:focus-visible { - outline: 2px solid color-mix(in srgb, var(--inspect-red) 42%, var(--inspect-line)); - outline-offset: 2px; -} - -.healthHealthy { - background: color-mix(in srgb, var(--inspect-green) 12%, var(--inspect-surface)); - color: var(--inspect-green); -} - -.healthDisabled, -.healthQuota { - background: color-mix(in srgb, var(--inspect-amber) 12%, var(--inspect-surface)); - color: var(--inspect-amber); -} - -.healthAuthInvalid, -.healthError { - background: color-mix(in srgb, var(--inspect-red) 12%, var(--inspect-surface)); - color: var(--inspect-red); -} - -.healthRecoverable, -.healthProcessed { - background: color-mix(in srgb, var(--inspect-accent) 12%, var(--inspect-surface)); - color: var(--inspect-accent); -} - -.tableWrap { - max-height: min(620px, 68vh); - overflow: auto; - overscroll-behavior: contain; - border: 1px solid var(--inspect-line); - border-radius: 8px; - background: var(--inspect-surface); - scrollbar-gutter: stable; -} - -.tableWrap::-webkit-scrollbar { - width: 10px; - height: 10px; -} - -.tableWrap::-webkit-scrollbar-thumb { - border: 3px solid transparent; - border-radius: 999px; - background: color-mix(in srgb, var(--inspect-muted) 34%, transparent); - background-clip: padding-box; -} - -.tableWrap::-webkit-scrollbar-track { - background: transparent; -} - -.table { - min-width: 980px; - width: 100%; - border-collapse: separate; - border-spacing: 0; - table-layout: fixed; -} - -.accountColumn { - width: 26%; -} - -.healthColumn { - width: 15%; -} - -.enabledColumn { - width: 9%; -} - -.quotaColumn { - width: 9%; -} - -.tokenColumn { - width: 9%; -} - -.verdictColumn { - width: 14%; -} - -.operationColumn { - width: 18%; -} - -.table th, -.table td { - padding: 11px 12px; - border-bottom: 1px solid var(--inspect-line); - text-align: left; - vertical-align: middle; - font-size: 13px; - line-height: 1.45; -} - -.table th { - position: sticky; - top: 0; - z-index: 2; - background: var(--inspect-surface-muted); - color: var(--inspect-ink); - font-size: 12px; - font-weight: 700; - line-height: 1.35; - white-space: normal; -} - -.table tbody tr:nth-child(2n) td { - background: color-mix(in srgb, var(--inspect-surface-muted) 72%, var(--inspect-surface)); -} - -.table tbody tr:last-child td { - border-bottom: 0; -} - -.table tbody tr:hover td { - background: color-mix(in srgb, var(--inspect-accent) 5%, var(--inspect-surface)); -} - -.primaryCell { - display: grid; - gap: 4px; - min-width: 0; -} - -.primaryCell span { - font-weight: 650; - line-height: 1.35; - overflow-wrap: anywhere; -} - -.primaryCell small { - font-size: 12px; - line-height: 1.35; - overflow-wrap: anywhere; -} - -.statusCell, -.probeCell, -.diagnosisCell, -.quotaCell, -.tokenRefreshCell, -.verdictCell { - display: grid; - gap: 5px; - min-width: 0; -} - -.statusCell small, -.probeCell small, -.diagnosisCell small, -.quotaCell small, -.tokenRefreshCell small, -.verdictCell small { - color: var(--inspect-muted); - font-size: 12px; - line-height: 1.35; - overflow-wrap: anywhere; -} - -.quotaCell span, -.probeCell span { - color: var(--inspect-ink); - font-weight: 650; -} - -.quotaCell small, -.probeCell span { - font-variant-numeric: tabular-nums; -} - -.verdictCell strong { - color: var(--inspect-ink); - font-size: 13px; - font-weight: 650; - line-height: 1.4; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.errorDetailsPanel { - display: grid; - gap: 14px; - padding: 2px 0 4px; -} - -.errorModalActions { - display: flex; - justify-content: flex-end; -} - -.errorOverview { - display: grid; - justify-items: start; - gap: 10px; - min-width: 0; - padding: 14px 16px; - border: 1px solid color-mix(in srgb, var(--inspect-red) 24%, var(--inspect-line)); - border-radius: 8px; - background: color-mix(in srgb, var(--inspect-red) 7%, var(--inspect-surface)); -} - -.errorOverview strong { - color: var(--inspect-ink); - font-size: 15px; - font-weight: 650; - line-height: 1.45; - overflow-wrap: anywhere; -} - -.errorDetailsGrid { - display: grid; - grid-template-columns: minmax(0, 1fr); - border: 1px solid color-mix(in srgb, var(--inspect-line) 82%, transparent); - border-radius: 8px; - overflow: hidden; -} - -.errorDetailItem { - display: flex; - align-items: baseline; - justify-content: space-between; - gap: 16px; - min-width: 0; - padding: 9px 12px; - background: var(--inspect-surface); -} - -.errorDetailItem + .errorDetailItem { - border-top: 1px solid color-mix(in srgb, var(--inspect-line) 72%, transparent); -} - -.errorDetailItem span, -.errorMessageBlock > span { - color: var(--inspect-muted); - font-size: 12px; - line-height: 1.35; -} - -.errorDetailItem strong { - min-width: 0; - max-width: 72%; - text-align: right; - overflow-wrap: anywhere; - color: var(--inspect-ink); - font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace); - font-size: 12px; - font-weight: 600; - line-height: 1.35; -} - -.errorMessageBlock { - display: grid; - gap: 6px; -} - -.errorMessage { - max-height: min(440px, 48vh); - overflow: auto; - margin: 0; - padding: 12px 14px; - border: 1px solid color-mix(in srgb, var(--inspect-line) 86%, transparent); - border-radius: 8px; - background: color-mix(in srgb, var(--inspect-surface-muted) 58%, var(--inspect-surface)); - color: var(--inspect-ink); - font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace); - font-size: 12px; - line-height: 1.5; - white-space: pre-wrap; - overflow-wrap: anywhere; -} - -.stateText, -.stateTextGood, -.stateTextMuted, -.stateTextWarn, -.stateTextBad { - font-size: 12px; - font-weight: 650; - line-height: 1.35; - white-space: nowrap; -} - -.stateText, -.stateTextGood { - color: var(--inspect-green); -} - -.stateTextMuted { - color: var(--inspect-muted); -} - -.stateTextWarn { - color: var(--inspect-amber); -} - -.stateTextBad { - color: var(--inspect-red); -} - -.statePill { - display: inline-flex; - align-items: center; - width: fit-content; - min-height: 24px; - padding: 0 8px; - border-radius: 999px; - font-size: 12px; - font-weight: 650; - white-space: nowrap; -} - -.statePillGood { - background: color-mix(in srgb, var(--inspect-green) 12%, var(--inspect-surface)); - color: var(--inspect-green); -} - -.statePillMuted { - background: var(--inspect-surface-muted); - color: var(--inspect-muted); -} - -.statePillWarn { - background: color-mix(in srgb, var(--inspect-amber) 12%, var(--inspect-surface)); - color: var(--inspect-amber); -} - -.statePillBad { - background: color-mix(in srgb, var(--inspect-red) 12%, var(--inspect-surface)); - color: var(--inspect-red); -} - -.diagnosisCell span { - overflow-wrap: anywhere; -} - -.compactCell, -.numericCell { - color: var(--inspect-ink); - font-variant-numeric: tabular-nums; - white-space: nowrap; -} - -.numericCell { - text-align: right; -} - -.reasonCell, -.errorCell { - max-width: 0; - overflow-wrap: anywhere; - word-break: break-word; -} - -.errorCell { - font-size: 12px; -} - -.operationCell { - vertical-align: middle; -} - -.operationCell .operationActions { - display: flex; - flex-wrap: nowrap; - align-items: center; - justify-content: flex-start; - gap: 4px; -} - -.operationCell .operationActions :global(.btn) { - flex: 0 0 auto; - width: auto; - min-width: 34px; - min-height: 26px; - padding: 0 5px; - border-radius: 7px; - font-size: 12px; - line-height: 1.2; -} - -.operationCell .operationActions :global(.btn + .btn) { - margin-left: 0; -} - -.monoCell { - font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); - word-break: break-all; -} - -.operationActions { - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: flex-start; - gap: 6px; -} - -.operationActions :global(.btn) { - flex: 0 0 auto; - min-height: 30px; - padding: 0 9px; - border-radius: 8px; -} - -.emptyState { - display: grid; - justify-items: center; - gap: 12px; - min-height: 220px; - padding: 28px; - border: 1px dashed color-mix(in srgb, var(--inspect-line-strong) 72%, transparent); - border-radius: 14px; - background: color-mix(in srgb, var(--inspect-surface-muted) 68%, transparent); - text-align: center; -} - -.emptyState strong { - max-width: 620px; - font-size: 15px; - line-height: 1.6; -} - -.emptyState span { - max-width: 560px; - color: var(--inspect-muted); - font-size: 13px; - line-height: 1.6; -} - -.confirmationBody { - display: grid; - gap: 12px; -} - -.confirmationBody p { - margin: 0; - color: var(--inspect-muted); - font-size: 13px; - line-height: 1.6; -} - -.confirmationLead { - display: grid; - gap: 6px; - padding: 14px; - border: 1px solid color-mix(in srgb, var(--inspect-accent) 18%, var(--inspect-line)); - border-radius: 12px; - background: linear-gradient(180deg, color-mix(in srgb, var(--inspect-accent) 7%, var(--inspect-surface)), var(--inspect-surface)); -} - -.confirmationLead strong { - color: var(--inspect-ink); - font-size: 14px; - font-weight: 750; - line-height: 1.35; -} - -.confirmationLead span { - color: var(--inspect-muted); - font-size: 13px; - line-height: 1.55; - overflow-wrap: anywhere; -} - -.confirmationLeadDanger { - border-color: color-mix(in srgb, var(--inspect-red) 28%, var(--inspect-line)); - background: linear-gradient(180deg, color-mix(in srgb, var(--inspect-red) 8%, var(--inspect-surface)), var(--inspect-surface)); -} - -.confirmationAccountCard { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 12px; - align-items: center; - padding: 12px 14px; - border: 1px solid var(--inspect-line); - border-radius: 12px; - background: var(--inspect-surface-muted); -} - -.confirmationAccountCard div { - display: grid; - gap: 4px; - min-width: 0; -} - -.confirmationAccountCard span { - color: var(--inspect-ink); - font-size: 13px; - font-weight: 700; - line-height: 1.35; - overflow-wrap: anywhere; -} - -.confirmationAccountCard small { - color: var(--inspect-muted); - font-size: 12px; - line-height: 1.35; -} - -.confirmationAccountCard strong { - display: inline-flex; - align-items: center; - min-height: 24px; - padding: 0 8px; - border-radius: 999px; - background: color-mix(in srgb, var(--inspect-accent) 10%, var(--inspect-surface)); - color: var(--inspect-accent); - font-size: 12px; - font-weight: 700; - white-space: nowrap; -} - -.confirmationNotice { - padding: 10px 12px; - border: 1px solid color-mix(in srgb, var(--inspect-amber) 24%, var(--inspect-line)); - border-radius: 10px; - background: color-mix(in srgb, var(--inspect-amber) 9%, var(--inspect-surface)); - color: var(--inspect-amber); - font-size: 12px; - font-weight: 650; - line-height: 1.5; -} - -.confirmationNoticeDanger { - border-color: color-mix(in srgb, var(--inspect-red) 28%, var(--inspect-line)); - background: color-mix(in srgb, var(--inspect-red) 9%, var(--inspect-surface)); - color: var(--inspect-red); -} - -.confirmationStats { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 8px; -} - -.confirmationStats span { - padding: 10px 12px; - border-radius: 10px; - background: var(--inspect-surface-muted); - color: var(--inspect-ink); - font-size: 13px; - font-weight: 700; - text-align: center; -} - -.confirmationDangerStat { - background: color-mix(in srgb, var(--inspect-red) 12%, var(--inspect-surface)) !important; - color: var(--inspect-red) !important; -} - -.confirmationPreview { - display: grid; - gap: 8px; - padding: 12px; - border: 1px solid var(--inspect-line); - border-radius: 12px; - background: var(--inspect-surface-muted); -} - -.confirmationPreview > strong { - font-size: 13px; -} - -.confirmationPreviewRow { - display: grid; - grid-template-columns: minmax(0, 1.2fr) minmax(76px, 0.5fr) minmax(76px, 0.5fr) minmax(0, 1.6fr); - gap: 8px; - align-items: start; - font-size: 12px; - line-height: 1.5; -} - -.confirmationCompactRow { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 10px; - align-items: center; - font-size: 13px; - line-height: 1.5; -} - -.confirmationCompactRow small { - color: var(--inspect-muted); -} - -.confirmationPreviewRow span, -.confirmationPreviewRow em, -.confirmationCompactRow span { - overflow-wrap: anywhere; -} - -.confirmationPreviewRow small { - color: var(--inspect-muted); -} - -.confirmationPreviewRow em { - color: var(--inspect-muted); - font-style: normal; -} - -.warningText { - color: var(--inspect-amber); -} - -.dangerText { - color: var(--inspect-red); - font-weight: 700; -} - -.errorText { - color: var(--inspect-red); - word-break: break-word; -} - -.emptyBlock, -.emptyBlockSmall { - display: grid; - place-items: center; - color: var(--inspect-muted); - text-align: center; -} - -.emptyBlock { - min-height: 140px; -} - -.emptyBlockSmall { - min-height: 70px; -} - -@media (max-width: 1280px) { - .assetOverviewLayout { - grid-template-columns: 1fr; - } - - .assetMetricGrid { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - - .assetOverviewGrid { - grid-template-columns: 1fr; - } - - .providerDistributionCard { - order: -1; - } - - .assetKpiGrid { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - - .cockpitGrid, - .workbenchGrid, - .inspectionOperationGrid, - .runControlCard, - .assetIntelligenceLayout, - .actionStudioLayout, - .chartGrid, - .inspectionStatsLayout { - grid-template-columns: 1fr; - } -} - -@media (max-width: 900px) { - .heroHeader, - .panelHeader, - .cockpitHeader { - flex-direction: column; - align-items: stretch; - } - - .heroHeader, - .operationModuleHeader { - grid-template-columns: 1fr; - } - - .operationModuleHeader > .foldButton { - justify-self: start; - width: fit-content; - } - - .heroActions { - justify-content: flex-start; - width: fit-content; - } - - .resultsHeaderActions { - width: fit-content; - max-width: 100%; - } - - .resultFilterControl { - width: fit-content; - max-width: 100%; - } - - .resultToolbar { - justify-content: flex-start; - } - - .resultProviderSelect { - flex-basis: 160px; - width: 160px; - } - - .heroActions, - .panelActions, - .resultsHeaderActions, - .logHeaderActions { - justify-content: flex-start; - } - - .progressFooter { - flex-direction: column; - align-items: stretch; - } - - .progressActions { - justify-content: flex-start; - } - - .runControlCard .cockpitCardHeader { - grid-row: auto; - } - - .providerDistributionHeader { - display: flex; - align-items: flex-start; - grid-template-columns: none; - } - - .providerDistributionSelection:global(.btn) { - max-width: min(58%, 220px); - } - - .providerSelectorRow { - display: grid; - grid-template-columns: 1fr; - } - - .providerSelectorRow > span { - width: 100%; - } - - .runStatusGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .runStatusPrimary { - grid-column: 1 / -1; - } - - .runExecutionGrid { - grid-template-columns: 1fr; - } - - .summaryGrid, - .summaryGridCompact, - .riskGrid, - .assetMetricGrid, - .assetKpiGrid, - .actionBreakdownGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .chartGrid, - .inspectionStatsLayout { - grid-template-columns: 1fr; - } - - .policyBanner { - flex-direction: column; - align-items: flex-start; - } - - .policyBanner span { - text-align: left; - } - - .logRow { - grid-template-columns: 1fr; - } - - .settingsWorkbench { - grid-template-columns: 1fr; - min-height: 0; - } - - .settingsSidebar { - grid-template-rows: auto auto auto; - border-right: 0; - border-bottom: 1px solid color-mix(in srgb, var(--inspect-line) 74%, transparent); - } - - .settingsSidebarNav { - display: flex; - overflow-x: auto; - padding-bottom: 2px; - } - - .settingsSidebarNav button { - min-width: 170px; - } - - .settingsSidebarStatus, - .settingsSummaryGrid, - .settingsThreeGrid, - .settingsMatrixGrid, - .settingsFocusGrid, - .settingsAutomationGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .settingsSplitGrid { - grid-template-columns: 1fr; - } -} - -@media (max-width: 640px) { - .panel, - .assetIntelligenceCard, - .runControlCard, - .actionStudioCard, - .executionTaskCard, - .providerPanel, - .providerDistributionCard { - padding: 16px; - border-radius: 12px; - } - - .professionalMetricCard { - min-height: 112px; - padding: 16px; - } - - .cockpitHeader h2 { - font-size: 20px; - } - - .cockpitCardHeader { - flex-direction: column; - } - - .inspectionConfigGrid, - .resultOverviewGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .inspectionControlActions { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .inspectionControlActions :global(.btn:first-child) { - grid-column: 1 / -1; - } - - .strategyTotalsRow { - grid-template-columns: repeat(4, minmax(0, 1fr)); - overflow-x: auto; - scrollbar-width: none; - } - - .strategyTotalsRow::-webkit-scrollbar { - display: none; - } - - .strategyActivityRow, - .inspectionOperationGrid .actionBreakdownCard div { - grid-template-columns: 1fr; - } - - .strategyActivityRow { - justify-items: start; - } - - .executionTaskGrid, - .runStatusGrid, - .summaryGrid, - .summaryGridCompact, - .riskGrid, - .assetMetricGrid, - .actionBreakdownGrid, - .runControlCard, - .chartGrid, - .inspectionStatsLayout, - .providerRow, - .providerStatusGrid { - grid-template-columns: 1fr; - } - - .assetKpiGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .chartPanel { - padding: 16px; - } - - .heroTitle { - font-size: 28px; - } - - .progressActions :global(.btn) { - flex: 1 1 140px; - } - - .heroActionButton:global(.btn) { - flex: 0 0 auto; - } - - .table { - min-width: 980px; - } - - .confirmationStats, - .confirmationPreviewRow { - grid-template-columns: 1fr; - } - - .confirmationStats span, - .confirmationPreviewRow strong { - text-align: left; - } - - .emptyState { - padding: 22px 16px; - } - - .settingsSummaryGrid, - .settingsSidebarStatus, - .settingsThreeGrid, - .settingsMatrixGrid, - .settingsFocusGrid, - .settingsAutomationGrid { - grid-template-columns: 1fr; - } - - .settingsModal :global(.modal-body) { - display: block; - max-height: min(72vh, calc(100vh - 132px)); - overflow-y: auto; - padding: 0; - -webkit-overflow-scrolling: touch; - } - - @supports (height: 100dvh) { - .settingsModal :global(.modal-body) { - max-height: min(72dvh, calc(100dvh - 132px)); - } - } - - .settingsWorkbench { - display: block; - max-height: none; - min-height: 0; - overflow: visible; - border-radius: 14px 14px 0 0; - } - - .settingsSidebar { - display: none; - } - - .settingsWorkbenchMain { - display: grid; - max-height: none; - overflow: visible; - padding: 14px; - } - - .settingsWorkbenchSection, - .settingsHeroPanel { - padding: 14px; - border-radius: 14px; - } - - .settingsWorkbenchHeader { - flex-direction: column; - align-items: flex-start; - } - - .settingsActionsBar { - position: sticky; - bottom: 0; - display: grid; - grid-template-columns: minmax(96px, auto) minmax(0, 1fr); - align-items: stretch; - gap: 8px; - padding: 10px 14px 14px; - border-radius: 0 0 14px 14px; - background: var(--inspect-surface-elevated); - } - - .settingsActionsBar > :global(button) { - width: auto; - min-width: 96px; - } - - .settingsActionsRight { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - width: auto; - gap: 8px; - } - - .settingsActionsRight :global(button) { - width: 100%; - } -} diff --git a/cliproxyapi-pro-management/overlay/src/pages/AccountInspectionPage.tsx b/cliproxyapi-pro-management/overlay/src/pages/AccountInspectionPage.tsx index 71d3b7b..94c665b 100644 --- a/cliproxyapi-pro-management/overlay/src/pages/AccountInspectionPage.tsx +++ b/cliproxyapi-pro-management/overlay/src/pages/AccountInspectionPage.tsx @@ -14,13 +14,10 @@ import { import { getAuthFileIcon } from '@/features/authFiles/constants'; import { ACCOUNT_INSPECTION_ALL_PROVIDER_TYPE, - ACCOUNT_INSPECTION_SUPPORTED_PROVIDERS, - ACCOUNT_INSPECTION_SETTING_LIMITS, accountInspectionBackendResultToItem, buildAccountInspectionBackendViewState, buildExecutionFailureMessage, clearAccountInspectionConfigurableSettings, - createIdleAccountInspectionProgressSnapshot, DEFAULT_ACCOUNT_INSPECTION_SETTINGS, hasAccountInspectionAutoExecutePolicies, isSuggestedAction, @@ -28,16 +25,75 @@ import { normalizeAntigravityQuotaMode, normalizeAutoErrorAction, saveAccountInspectionConfigurableSettings, - type AccountInspectionAction, - type AccountInspectionAntigravityQuotaMode, - type AccountInspectionAutoErrorAction, - type AccountInspectionConfigurableSettings, type AccountInspectionLogLevel, - type AccountInspectionPageInfo, - type AccountInspectionProgressSnapshot, type AccountInspectionResultItem, - type AccountInspectionRunResult, } from '@/features/monitoring/accountInspection'; +import { + ACCOUNT_INSPECTION_ACTION_PAGE_SIZE, + ACCOUNT_INSPECTION_AUTH_FILES_IDLE_DELAY_MS, + ACCOUNT_INSPECTION_DETAILS_IDLE_DELAY_MS, + ACCOUNT_INSPECTION_EXPORT_DOWNLOAD_CONCURRENCY, + ACCOUNT_INSPECTION_LOG_PAGE_SIZE, + ACCOUNT_INSPECTION_RESULT_PAGE_SIZE, + ACCOUNT_INSPECTION_SUPPORTED_PROVIDER_SET, + ANTIGRAVITY_QUOTA_MODE_OPTIONS, + AUTO_ERROR_ACTION_OPTIONS, + DELETE_WORKER_LIMITS, + INSPECTION_TARGET_OPTIONS, + InspectionErrorDetailsPanel, + RETRY_LIMITS, + SAMPLE_SIZE_LIMITS, + SCHEDULE_INTERVAL_LIMITS, + THRESHOLD_LIMITS, + TIMEOUT_LIMITS, + WORKER_LIMITS, + buildDeleteConfirmationMessage, + buildExecuteConfirmationMessage, + buildHealthStatusLabel, + buildHighAvailabilityBarStyle, + buildInspectionResultsViewState, + buildManualActionItem, + buildRefreshTokenConfirmationMessage, + collectActionableInspectionResults, + countActions, + createEmptyAuthFileAccountStats, + createInspectionBackendState, + formatActionLabel, + formatCurrentStateLabel, + formatInspectionInterval, + formatInspectionResultToast, + formatInspectionVerdictPrimary, + formatQuotaRemainingLabel, + formatRunInspectionButtonLabel, + formatTimestamp, + formatTokenRefreshDetail, + formatTokenRefreshLabel, + formatTokenRefreshToast, + getDocumentTheme, + getPaginationRange, + getProviderInitial, + handleAccountInspectionControlError, + hasInspectionErrorDetails, + healthToneClass, + inspectionBackendReducer, + isInspectableAccountInspectionAuthFile, + levelClassMap, + scheduleAuthFileAccountStats, + summaryToneClass, + toAccountInspectionApiItem, + toSettingsDraft, + tokenRefreshToneClass, + type AccountInspectionIdleCallback, + type AuthFileAccountStats, + type AuthFileAccountStatsJob, + type InspectionSettingsDraftField, + type ManualAccountInspectionAction, + type ProviderAccountStats, + type ResolvedTheme, + type ResultFilter, + type SettingsSectionKey, + type SummaryCard, +} from '@/features/monitoring/accountInspectionPageModel'; import { buildZipArchive, downloadBlobFile, @@ -50,7 +106,6 @@ import { buildAccountInspectionLogsWebSocketUrl, nextAccountInspectionReconnectDelay, refreshAccountInspectionAfterReconnect, - type AccountInspectionInspectOneItem, type AccountInspectionLogStreamMessage, type AccountInspectionScheduleResponse, } from '@/services/api'; @@ -58,1347 +113,10 @@ import { authFilesApi } from '@/services/api/authFiles'; import { quotaPersistenceMiddleware } from '@/extensions/quota/persistenceMiddleware'; import { useAuthStore, useConfigStore, useNotificationStore, useQuotaStore } from '@/stores'; import type { AuthFileItem } from '@/types'; -import { isDisabledAuthFile, isQuotaLowState, isRecordValue, normalizeNumberValue, readBooleanValue, readStringValue, resolveAuthProvider } from '@/utils/quota'; +import { resolveAuthProvider } from '@/utils/quota'; import { resolveProviderDisplayLabel } from '@/utils/sourceResolver'; import quotaStyles from '@/pages/QuotaPage.module.scss'; -import styles from './AccountInspectionPage.module.scss'; - -type RunStatus = 'idle' | 'running' | 'paused' | 'success' | 'error'; - -type ResultHealthStatus = 'healthy' | 'disabled' | 'authInvalid' | 'quotaExhausted' | 'inspectionError' | 'recoverable'; - -type ResultFilter = 'pending' | 'accountInvalid' | 'requestError' | 'quotaExhausted' | 'recoverable' | 'highAvailable'; - -type SettingsSectionKey = 'plan' | 'scope' | 'runtime' | 'antigravity' | 'auto'; - -type ManualAccountInspectionAction = Exclude; - -type QuotaAccountStatsState = Pick< - ReturnType, - 'antigravityQuota' | 'claudeQuota' | 'codexQuota' | 'kimiQuota' | 'xaiQuota' ->; - -type HealthCounts = { - total: number; - healthy: number; - disabled: number; - authInvalid: number; - quotaExhausted: number; - inspectionError: number; - recoverable: number; -}; - -type InspectionLogEntry = { - id: string; - level: AccountInspectionLogLevel; - message: string; - timestamp: number; -}; - -type SummaryCard = { - key: string; - label: string; - value: string; - description?: string; - tone?: 'neutral' | 'good' | 'warn' | 'bad'; -}; - -type InspectionSettingsDraft = { - targetType: string; - workers: string; - deleteWorkers: string; - timeout: string; - retries: string; - usedPercentThreshold: string; - sampleSize: string; - antigravityDeepProbeEnabled: boolean; - antigravityDeepProbeModel: string; - antigravityQuotaMode: AccountInspectionAntigravityQuotaMode; - xaiDeepProbeEnabled: boolean; - xaiDeepProbeModel: string; - autoExecuteQuotaLimitDisable: boolean; - autoExecuteQuotaRecoveryEnable: boolean; - autoExecuteAccountInvalidAction: AccountInspectionAutoErrorAction; - autoExecuteRequestErrorAction: AccountInspectionAutoErrorAction; -}; - -type InspectionSettingsDraftField = Exclude< - keyof InspectionSettingsDraft, - 'antigravityDeepProbeEnabled' | 'antigravityQuotaMode' | 'xaiDeepProbeEnabled' | 'autoExecuteQuotaLimitDisable' | 'autoExecuteQuotaRecoveryEnable' | 'autoExecuteAccountInvalidAction' | 'autoExecuteRequestErrorAction' ->; - -type ScheduleDraft = { - enabled: boolean; - intervalMinutes: string; -}; - -type ProviderAccountStats = { - provider: string; - total: number; - enabled: number; - highAvailable: number; - disabled: number; - quotaLow: number; - accountInvalid: number; - requestError: number; -}; - -type ResolvedTheme = 'light' | 'dark'; - -type AuthFileAccountStats = { - total: number; - providerCount: number; - enabled: number; - highAvailable: number; - disabled: number; - quotaLow: number; - accountInvalid: number; - requestError: number; - providers: ProviderAccountStats[]; -}; - -type AutoExecutionCounts = { - delete: number; - disable: number; - enable: number; -}; - -type InspectionResultViewRow = { - item: AccountInspectionResultItem; - healthStatus: ResultHealthStatus; - manualActions: ManualAccountInspectionAction[]; -}; - -type InspectionResultsViewState = { - rows: InspectionResultViewRow[]; - healthCounts: HealthCounts; - actionableActionCounts: AutoExecutionCounts; - filterRows: Record; - filterRowCounts: Record; -}; - -const ACCOUNT_INSPECTION_LOG_LIMIT = 200; -const ACCOUNT_INSPECTION_RESULT_PAGE_SIZE = 100; -const ACCOUNT_INSPECTION_LOG_PAGE_SIZE = 100; -const ACCOUNT_INSPECTION_ACTION_PAGE_SIZE = 500; -const ACCOUNT_INSPECTION_DETAILS_IDLE_DELAY_MS = 250; -const ACCOUNT_INSPECTION_AUTH_FILES_IDLE_DELAY_MS = 300; -const ACCOUNT_INSPECTION_ASSET_STATS_CHUNK_SIZE = 500; -const ACCOUNT_INSPECTION_EXPORT_DOWNLOAD_CONCURRENCY = 4; - -type AccountInspectionIdleCallback = (deadline: { didTimeout: boolean; timeRemaining: () => number }) => void; - -const appendInspectionLogEntry = (entries: InspectionLogEntry[], entry: InspectionLogEntry) => - [...entries, entry].slice(-ACCOUNT_INSPECTION_LOG_LIMIT); - -const getPaginationRange = (pageInfo: AccountInspectionPageInfo | null, visibleCount: number) => { - if (!pageInfo || pageInfo.total <= 0 || visibleCount <= 0) { - return { - page: pageInfo?.page ?? 1, - pageSize: pageInfo?.pageSize ?? visibleCount, - total: pageInfo?.total ?? visibleCount, - totalPages: pageInfo?.totalPages ?? (visibleCount > 0 ? 1 : 0), - from: visibleCount > 0 ? 1 : 0, - to: visibleCount, - hasPrevious: (pageInfo?.page ?? 1) > 1, - hasNext: Boolean(pageInfo?.hasMore), - }; - } - - const from = (pageInfo.page - 1) * pageInfo.pageSize + 1; - return { - page: pageInfo.page, - pageSize: pageInfo.pageSize, - total: pageInfo.total, - totalPages: pageInfo.totalPages, - from, - to: Math.min(pageInfo.total, from + visibleCount - 1), - hasPrevious: pageInfo.page > 1, - hasNext: pageInfo.hasMore, - }; -}; - -const getProviderInitial = (label: string) => label.trim().charAt(0).toUpperCase() || '?'; - -const getDocumentTheme = (): ResolvedTheme => { - if (typeof document === 'undefined') return 'light'; - const root = document.documentElement; - const theme = root.dataset.theme || root.getAttribute('data-theme') || root.className; - return String(theme).toLowerCase().includes('dark') ? 'dark' : 'light'; -}; - -const emptyAutoExecutionCounts = (): AutoExecutionCounts => ({ - delete: 0, - disable: 0, - enable: 0, -}); - -const createEmptyFilterRows = (): Record => ({ - pending: [], - accountInvalid: [], - requestError: [], - quotaExhausted: [], - recoverable: [], - highAvailable: [], -}); - -const levelClassMap: Record = { - info: styles.logInfo, - success: styles.logSuccess, - warning: styles.logWarning, - error: styles.logError, -}; - -const healthToneClass: Record = { - healthy: styles.healthHealthy, - disabled: styles.healthDisabled, - authInvalid: styles.healthAuthInvalid, - quotaExhausted: styles.healthQuota, - inspectionError: styles.healthError, - recoverable: styles.healthRecoverable, -}; - -const healthLabelKey: Record = { - healthy: 'monitoring.account_inspection_health_healthy', - disabled: 'monitoring.account_inspection_health_disabled', - authInvalid: 'monitoring.account_inspection_account_invalid', - quotaExhausted: 'monitoring.account_inspection_health_quota_exhausted', - inspectionError: 'monitoring.account_inspection_account_request_error', - recoverable: 'monitoring.account_inspection_health_recoverable', -}; - -const extractHealthHttpStatusCode = (item: AccountInspectionResultItem) => { - if (item.statusCode !== null && item.statusCode >= 400) return item.statusCode; - const errorText = [item.error, item.deepProbeError, item.tokenRefreshError].filter(Boolean).join(' '); - const match = errorText.match(/\bHTTP\s+(\d{3})\b/i) ?? errorText.match(/\bstatus(?:\s+code)?\s*[:=]?\s*(\d{3})\b/i); - return match ? Number(match[1]) : null; -}; - -const buildHealthStatusCodeText = (item: AccountInspectionResultItem) => { - const httpStatusCode = extractHealthHttpStatusCode(item); - return httpStatusCode !== null ? String(httpStatusCode) : ''; -}; - -const buildHealthStatusLabel = ( - item: AccountInspectionResultItem, - healthStatus: ResultHealthStatus, - t: ReturnType['t'] -) => { - const label = t(healthLabelKey[healthStatus]); - const code = buildHealthStatusCodeText(item); - return code ? `${label} · ${code}` : label; -}; - -const hasInspectionErrorDetails = (item: AccountInspectionResultItem) => Boolean( - item.error - || item.errorDetail?.trim() - || item.errorCode?.trim() - || item.deepProbeError - || item.tokenRefreshError - || extractHealthHttpStatusCode(item) !== null -); - -const parseInspectionErrorPayload = (value: string): unknown => { - const text = value.trim(); - if (!text || (!text.startsWith('{') && !text.startsWith('['))) return null; - try { - return JSON.parse(text); - } catch { - return null; - } -}; - -const readInspectionErrorMessage = (value: unknown): string => { - if (typeof value === 'string') return value.trim(); - if (!value || typeof value !== 'object' || Array.isArray(value)) return ''; - const record = value as Record; - const directMessage = readInspectionErrorMessage(record.message); - if (directMessage) return directMessage; - const nestedError = readInspectionErrorMessage(record.error); - if (nestedError) return nestedError; - return ''; -}; - -const buildInspectionErrorPresentation = (item: AccountInspectionResultItem) => { - const candidates = [item.error, item.deepProbeError || '', item.tokenRefreshError || ''] - .map((value) => value.trim()) - .filter((value, index, values) => Boolean(value) && values.indexOf(value) === index) - .sort((left, right) => right.length - left.length); - const detailText = item.errorDetail?.trim() || candidates[0] || ''; - const detailPayload = parseInspectionErrorPayload(detailText); - const parsedMessage = readInspectionErrorMessage(detailPayload); - const httpStatusCode = extractHealthHttpStatusCode(item); - const fallbackSummary = candidates.find((value) => parseInspectionErrorPayload(value) === null) || ''; - const summary = parsedMessage || fallbackSummary; - const normalizedSummary = summary.replace(/\s+/g, ' ').trim(); - const statusOnlySummary = httpStatusCode !== null && normalizedSummary.toLowerCase() === `http ${httpStatusCode}`.toLowerCase(); - return { - summary: statusOnlySummary ? '' : normalizedSummary, - detail: detailPayload === null - ? (item.errorDetail?.trim() ? detailText : '') - : JSON.stringify(detailPayload, null, 2), - }; -}; - -const resolveResultHealthStatus = (item: AccountInspectionResultItem): ResultHealthStatus => { - if (item.action === 'delete' || (item.statusCode !== null && ACCOUNT_INVALID_ERROR_STATUSES.has(item.statusCode))) { - return 'authInvalid'; - } - if (item.error) return 'inspectionError'; - if (item.isQuota || item.action === 'disable') return 'quotaExhausted'; - if (item.action === 'enable') return 'recoverable'; - if (item.disabled) return 'disabled'; - return 'healthy'; -}; - -function InspectionErrorDetailsPanel({ - item, - t, -}: { - item: AccountInspectionResultItem; - t: ReturnType['t']; -}) { - const healthStatus = resolveResultHealthStatus(item); - const httpStatusCode = extractHealthHttpStatusCode(item); - const errorPresentation = buildInspectionErrorPresentation(item); - const detailItems = [ - { label: t('monitoring.account_label'), value: item.fileName }, - { label: t('monitoring.filter_provider'), value: item.provider }, - { label: t('monitoring.account_inspection_http_status'), value: httpStatusCode !== null ? String(httpStatusCode) : '' }, - { label: t('monitoring.account_inspection_error_code'), value: item.errorCode?.trim() || '' }, - ].filter((detail) => detail.value); - - return ( -
-
- - {buildHealthStatusLabel(item, healthStatus, t)} - - {errorPresentation.summary ? {errorPresentation.summary} : null} -
-
- {detailItems.map((detail) => ( -
- {detail.label} - {detail.value} -
- ))} -
- {errorPresentation.detail ? ( -
- {t('monitoring.account_inspection_raw_error_response')} -
{errorPresentation.detail}
-
- ) : null} -
- ); -} - -const ACCOUNT_INVALID_ERROR_STATUSES = new Set([400, 401, 403, 404]); -const ACCOUNT_INSPECTION_SUPPORTED_PROVIDER_SET = new Set(ACCOUNT_INSPECTION_SUPPORTED_PROVIDERS); - -const readAuthFileField = (file: AuthFileItem, key: string) => - readStringValue((file as unknown as Record)[key]); - -const isAccountInspectionApiKeyAuthFile = (file: AuthFileItem) => { - const label = readAuthFileField(file, 'label').toLowerCase(); - const source = readAuthFileField(file, 'source').toLowerCase(); - const apiKey = readAuthFileField(file, 'api_key') || readAuthFileField(file, 'apiKey'); - const path = readAuthFileField(file, 'path'); - - return label.includes('apikey') || - label.includes('api-key') || - (source.startsWith('config:') && Boolean(apiKey)) || - (Boolean(apiKey) && !path); -}; - -const isInspectableAccountInspectionAuthFile = (file: AuthFileItem) => { - const provider = resolveAuthProvider(file); - return ACCOUNT_INSPECTION_SUPPORTED_PROVIDER_SET.has(provider) && !isAccountInspectionApiKeyAuthFile(file); -}; - -const readAuthFileStatusMessage = (file: AuthFileItem) => { - const raw = file['status_message'] ?? file.statusMessage; - if (raw === undefined || raw === null) return ''; - return String(raw).trim(); -}; - -const readAuthFileLastError = (file: AuthFileItem) => { - const raw = file['last_error'] ?? file.lastError; - return isRecordValue(raw) ? raw : null; -}; - -const readAuthFileLastErrorCode = (file: AuthFileItem) => readStringValue(readAuthFileLastError(file)?.code); - -const readAuthFileLastErrorStatus = (file: AuthFileItem) => { - const error = readAuthFileLastError(file); - return error ? normalizeNumberValue(error.http_status ?? error.httpStatus ?? error.status) : null; -}; - -const isAuthFileAccountInvalid = (file: AuthFileItem) => - readAuthFileLastErrorCode(file) === 'inspection_http_error' && - ACCOUNT_INVALID_ERROR_STATUSES.has(readAuthFileLastErrorStatus(file) ?? 0); - -const isAuthFileRequestError = (file: AuthFileItem) => { - const code = readAuthFileLastErrorCode(file); - if (code === 'inspection_probe_error' || code === 'antigravity_deep_probe_error') return true; - if (isAuthFileAccountInvalid(file)) return false; - if (readAuthFileLastError(file)) return true; - if (readBooleanValue(file.unavailable ?? file['unavailable'])) return true; - const status = String(file.status ?? file.state ?? '').trim().toLowerCase(); - return status === 'error' || readAuthFileStatusMessage(file).length > 0; -}; - -const incrementProviderStats = (stats: ProviderAccountStats, disabled: boolean, highAvailable: boolean, quotaLow: boolean, accountInvalid: boolean, requestError: boolean) => { - stats.total += 1; - if (disabled) { - stats.disabled += 1; - } else { - stats.enabled += 1; - } - if (highAvailable) stats.highAvailable += 1; - if (quotaLow) stats.quotaLow += 1; - if (accountInvalid) stats.accountInvalid += 1; - if (requestError) stats.requestError += 1; -}; - -const emptyProviderAccountStats = (provider: string): ProviderAccountStats => ({ - provider, - total: 0, - enabled: 0, - highAvailable: 0, - disabled: 0, - quotaLow: 0, - accountInvalid: 0, - requestError: 0, -}); - -const createEmptyAuthFileAccountStats = (): AuthFileAccountStats => ({ - total: 0, - providerCount: 0, - enabled: 0, - highAvailable: 0, - disabled: 0, - quotaLow: 0, - accountInvalid: 0, - requestError: 0, - providers: [], -}); - -const finalizeAuthFileAccountStats = ( - stats: AuthFileAccountStats, - providerStats: Map -) => ({ - ...stats, - providerCount: providerStats.size, - providers: [...providerStats.values()].sort((left, right) => right.total - left.total || left.provider.localeCompare(right.provider)), -}); - -const quotaUsedPercentFromRemaining = (item: unknown): number | null => { - if (!isRecordValue(item)) return null; - const usedPercent = normalizeNumberValue(item.usedPercent ?? item.used_percent); - if (usedPercent !== null) return Math.max(0, Math.min(100, usedPercent)); - const remainingFraction = normalizeNumberValue(item.remainingFraction ?? item.remaining_fraction); - if (remainingFraction === null) return null; - const normalized = remainingFraction > 1 && remainingFraction <= 100 ? remainingFraction / 100 : remainingFraction; - return Math.max(0, Math.min(100, (1 - Math.max(0, Math.min(1, normalized))) * 100)); -}; - -const maxQuotaUsedPercent = (items: unknown): number | null => { - if (!Array.isArray(items)) return null; - const values = items - .map(quotaUsedPercentFromRemaining) - .filter((value): value is number => value !== null); - if (values.length === 0) return null; - return Math.max(...values); -}; - -const antigravityGroupUsedPercent = (group: unknown): number | null => { - if (!isRecordValue(group)) return null; - return maxQuotaUsedPercent(group.buckets); -}; - -const maxAntigravityGroupUsedPercent = (groups: unknown[]): number | null => { - const values = groups - .map(antigravityGroupUsedPercent) - .filter((value): value is number => value !== null); - if (values.length === 0) return null; - return Math.max(...values); -}; - -const isAntigravityClaudeGptGroup = (group: unknown): boolean => { - if (!isRecordValue(group)) return false; - const normalize = (value: unknown) => - typeof value === 'string' - ? value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') - : ''; - const id = normalize(group.id); - const label = normalize(group.label); - if (id === 'claude-gpt' || label === 'claude-gpt') return true; - const combined = `${id}-${label}`; - return combined.includes('claude') && (combined.includes('gpt') || combined.includes('openai')); -}; - -const isAntigravityQuotaLow = ( - quota: unknown, - usedPercentThreshold: number, - quotaMode: AccountInspectionAntigravityQuotaMode -) => { - if (!isRecordValue(quota) || quota.status !== 'success') return false; - const groups = Array.isArray(quota.groups) ? quota.groups : []; - const used = quotaMode === 'max-used' - ? maxAntigravityGroupUsedPercent(groups) - : (maxAntigravityGroupUsedPercent(groups.filter(isAntigravityClaudeGptGroup)) ?? maxAntigravityGroupUsedPercent(groups)); - return used !== null && used >= usedPercentThreshold; -}; - -const isXaiQuotaLow = (quota: unknown, usedPercentThreshold: number) => { - if (!isRecordValue(quota) || quota.status !== 'success') return false; - if (!isRecordValue(quota.billing)) return false; - const used = - normalizeNumberValue(quota.billing.usagePercent ?? quota.billing.usage_percent) - ?? normalizeNumberValue(quota.billing.usedPercent ?? quota.billing.used_percent) - ?? maxAntigravityGroupUsedPercent(Array.isArray(quota.billing.productUsage) ? quota.billing.productUsage : []); - return used !== null && used >= usedPercentThreshold; -}; - -const isProviderQuotaLow = ( - provider: string, - quotaStore: QuotaAccountStatsState, - fileName: string, - usedPercentThreshold: number, - antigravityQuotaMode: AccountInspectionAntigravityQuotaMode -) => { - switch (provider) { - case 'antigravity': - return isAntigravityQuotaLow(quotaStore.antigravityQuota[fileName], usedPercentThreshold, antigravityQuotaMode); - case 'claude': - return isQuotaLowState(quotaStore.claudeQuota[fileName], usedPercentThreshold); - case 'codex': - return isQuotaLowState(quotaStore.codexQuota[fileName], usedPercentThreshold); - case 'kimi': - return isQuotaLowState(quotaStore.kimiQuota[fileName], usedPercentThreshold); - case 'xai': - return isXaiQuotaLow(quotaStore.xaiQuota[fileName], usedPercentThreshold); - default: - return false; - } -}; - -const accumulateAuthFileAccountStats = ( - stats: AuthFileAccountStats, - providerStats: Map, - file: AuthFileItem, - quotaStore: QuotaAccountStatsState, - usedPercentThreshold: number, - antigravityQuotaMode: AccountInspectionAntigravityQuotaMode -) => { - if (!isInspectableAccountInspectionAuthFile(file)) return; - - const provider = resolveAuthProvider(file) || 'unknown'; - const disabled = isDisabledAuthFile(file); - const quotaLow = isProviderQuotaLow( - provider, - quotaStore, - file.name, - usedPercentThreshold, - antigravityQuotaMode - ); - const accountInvalid = isAuthFileAccountInvalid(file); - const requestError = isAuthFileRequestError(file); - const highAvailable = !disabled && !quotaLow && !accountInvalid && !requestError; - - stats.total += 1; - if (disabled) { - stats.disabled += 1; - } else { - stats.enabled += 1; - } - if (highAvailable) stats.highAvailable += 1; - if (accountInvalid) stats.accountInvalid += 1; - if (requestError) stats.requestError += 1; - if (quotaLow) stats.quotaLow += 1; - - const providerEntry = providerStats.get(provider) ?? emptyProviderAccountStats(provider); - incrementProviderStats(providerEntry, disabled, highAvailable, quotaLow, accountInvalid, requestError); - providerStats.set(provider, providerEntry); -}; - -type AuthFileAccountStatsJob = { - cancelled: boolean; - handle: number | null; - cancelHandle: ((handle: number) => void) | null; -}; - -const scheduleAuthFileAccountStats = ( - job: AuthFileAccountStatsJob, - files: AuthFileItem[], - quotaStore: QuotaAccountStatsState, - usedPercentThreshold: number, - antigravityQuotaMode: AccountInspectionAntigravityQuotaMode, - onComplete: (stats: AuthFileAccountStats) => void -) => { - const providerStats = new Map(); - const stats = createEmptyAuthFileAccountStats(); - let index = 0; - const windowWithIdleCallback = window as Window & { - requestIdleCallback?: (callback: AccountInspectionIdleCallback, options?: { timeout?: number }) => number; - cancelIdleCallback?: (handle: number) => void; - }; - - const finish = () => { - if (job.cancelled) return; - const nextStats = finalizeAuthFileAccountStats(stats, providerStats); - startTransition(() => { - if (!job.cancelled) onComplete(nextStats); - }); - }; - - const scheduleNextChunk = () => { - if (windowWithIdleCallback.requestIdleCallback && windowWithIdleCallback.cancelIdleCallback) { - job.cancelHandle = windowWithIdleCallback.cancelIdleCallback.bind(windowWithIdleCallback); - job.handle = windowWithIdleCallback.requestIdleCallback(runChunk, { timeout: 250 }); - return; - } - job.cancelHandle = window.clearTimeout.bind(window); - job.handle = window.setTimeout(() => runChunk(), 0); - }; - - const runChunk = (deadline?: { timeRemaining: () => number }) => { - if (job.cancelled) return; - do { - const chunkEnd = Math.min(index + ACCOUNT_INSPECTION_ASSET_STATS_CHUNK_SIZE, files.length); - while (index < chunkEnd) { - accumulateAuthFileAccountStats( - stats, - providerStats, - files[index], - quotaStore, - usedPercentThreshold, - antigravityQuotaMode - ); - index += 1; - } - } while (index < files.length && deadline && deadline.timeRemaining() > 8); - - if (index >= files.length) { - finish(); - return; - } - - scheduleNextChunk(); - }; - - runChunk(); -}; - -const emptyHealthCounts = (): HealthCounts => ({ - total: 0, - healthy: 0, - disabled: 0, - authInvalid: 0, - quotaExhausted: 0, - inspectionError: 0, - recoverable: 0, -}); - -const getManualActionsByHealthStatus = ( - item: AccountInspectionResultItem, - healthStatus: ResultHealthStatus -): ManualAccountInspectionAction[] => { - if (healthStatus === 'healthy') return []; - return [item.disabled ? 'enable' : 'disable', 'delete']; -}; - -const buildInspectionResultsViewState = (items: AccountInspectionResultItem[]): InspectionResultsViewState => { - const healthCounts = emptyHealthCounts(); - const actionableActionCounts = emptyAutoExecutionCounts(); - const filterRows = createEmptyFilterRows(); - const filterRowCounts: Record = { - highAvailable: 0, - accountInvalid: 0, - quotaExhausted: 0, - requestError: 0, - recoverable: 0, - pending: 0, - }; - const rows: InspectionResultViewRow[] = []; - - const pushResultRow = ( - target: InspectionResultViewRow[], - item: AccountInspectionResultItem, - healthStatus: ResultHealthStatus, - existingRow: InspectionResultViewRow | null - ) => { - if (existingRow) { - target.push(existingRow); - return existingRow; - } - const row = { - item, - healthStatus, - manualActions: getManualActionsByHealthStatus(item, healthStatus), - }; - target.push(row); - return row; - }; - - healthCounts.total = items.length; - items.forEach((item) => { - const healthStatus = resolveResultHealthStatus(item); - let row: InspectionResultViewRow | null = null; - row = pushResultRow(rows, item, healthStatus, row); - - switch (healthStatus) { - case 'healthy': - healthCounts.healthy += 1; - filterRowCounts.highAvailable += 1; - row = pushResultRow(filterRows.highAvailable, item, healthStatus, row); - break; - case 'disabled': - healthCounts.disabled += 1; - break; - case 'authInvalid': - healthCounts.authInvalid += 1; - filterRowCounts.accountInvalid += 1; - row = pushResultRow(filterRows.accountInvalid, item, healthStatus, row); - break; - case 'quotaExhausted': - healthCounts.quotaExhausted += 1; - filterRowCounts.quotaExhausted += 1; - row = pushResultRow(filterRows.quotaExhausted, item, healthStatus, row); - break; - case 'inspectionError': - healthCounts.inspectionError += 1; - filterRowCounts.requestError += 1; - row = pushResultRow(filterRows.requestError, item, healthStatus, row); - break; - case 'recoverable': - healthCounts.recoverable += 1; - filterRowCounts.recoverable += 1; - row = pushResultRow(filterRows.recoverable, item, healthStatus, row); - break; - } - - if (isSuggestedAction(item) && !item.executed) { - filterRowCounts.pending += 1; - pushResultRow(filterRows.pending, item, healthStatus, row); - if (item.action === 'delete') actionableActionCounts.delete += 1; - if (item.action === 'disable') actionableActionCounts.disable += 1; - if (item.action === 'enable') actionableActionCounts.enable += 1; - } - }); - - return { - rows, - healthCounts, - actionableActionCounts, - filterRows, - filterRowCounts, - }; -}; - -const collectActionableInspectionResults = (items: AccountInspectionResultItem[]) => { - const targets: AccountInspectionResultItem[] = []; - items.forEach((item) => { - if (isSuggestedAction(item) && !item.executed) { - targets.push(item); - } - }); - return targets; -}; - -const buildManualActionItem = ( - item: AccountInspectionResultItem, - action: ManualAccountInspectionAction -): AccountInspectionResultItem => ({ - ...item, - action, - actionReason: item.actionReason || action, -}); - -const summaryToneClass: Record, string> = { - neutral: '', - good: styles.summaryGood, - warn: styles.summaryWarn, - bad: styles.summaryBad, -}; - -const INSPECTION_TARGET_OPTIONS = [ - { value: ACCOUNT_INSPECTION_ALL_PROVIDER_TYPE, label: 'All' }, - ...ACCOUNT_INSPECTION_SUPPORTED_PROVIDERS.map((provider) => ({ - value: provider, - label: resolveProviderDisplayLabel(provider), - })), -] as const; - -const AUTO_ERROR_ACTION_OPTIONS: Array<{ value: AccountInspectionAutoErrorAction; labelKey: string }> = [ - { value: 'none', labelKey: 'monitoring.account_inspection_settings_account_error_action_none' }, - { value: 'disable', labelKey: 'monitoring.account_inspection_settings_account_error_action_disable' }, - { value: 'delete', labelKey: 'monitoring.account_inspection_settings_account_error_action_delete' }, -]; - -const ANTIGRAVITY_QUOTA_MODE_OPTIONS: Array<{ value: AccountInspectionAntigravityQuotaMode; labelKey: string }> = [ - { value: 'claude-gpt', labelKey: 'monitoring.account_inspection_settings_antigravity_quota_mode_claude_gpt' }, - { value: 'max-used', labelKey: 'monitoring.account_inspection_settings_antigravity_quota_mode_max_used' }, -]; - -const { - workers: WORKER_LIMITS, - deleteWorkers: DELETE_WORKER_LIMITS, - timeout: TIMEOUT_LIMITS, - retries: RETRY_LIMITS, - usedPercentThreshold: THRESHOLD_LIMITS, - sampleSize: SAMPLE_SIZE_LIMITS, - scheduleIntervalMinutes: SCHEDULE_INTERVAL_LIMITS, -} = ACCOUNT_INSPECTION_SETTING_LIMITS; - -const formatTimestamp = (value: number, locale: string) => new Date(value).toLocaleString(locale); - -const formatInspectionInterval = (minutes: number, locale: string) => - new Intl.NumberFormat(locale, { style: 'unit', unit: 'minute', unitDisplay: 'short' }).format(minutes); - -const buildHighAvailabilityBarStyle = (highAvailable: number, total: number): CSSProperties => { - const share = total > 0 ? Math.min(Math.max(highAvailable / total, 0), 1) : 0; - return { - '--bar-width': `${share * 100}%`, - '--bar-color': `hsl(${Math.round(share * 120)}, 72%, 44%)`, - } as CSSProperties; -}; - -const toSettingsDraft = (settings: AccountInspectionConfigurableSettings): InspectionSettingsDraft => ({ - targetType: settings.targetType, - workers: String(settings.workers), - deleteWorkers: String(settings.deleteWorkers), - timeout: String(settings.timeout), - retries: String(settings.retries), - usedPercentThreshold: String(settings.usedPercentThreshold), - sampleSize: String(settings.sampleSize), - antigravityDeepProbeEnabled: settings.antigravityDeepProbeEnabled, - antigravityDeepProbeModel: settings.antigravityDeepProbeModel, - antigravityQuotaMode: settings.antigravityQuotaMode, - xaiDeepProbeEnabled: settings.xaiDeepProbeEnabled, - xaiDeepProbeModel: settings.xaiDeepProbeModel, - autoExecuteQuotaLimitDisable: settings.autoExecuteQuotaLimitDisable, - autoExecuteQuotaRecoveryEnable: settings.autoExecuteQuotaRecoveryEnable, - autoExecuteAccountInvalidAction: settings.autoExecuteAccountInvalidAction, - autoExecuteRequestErrorAction: settings.autoExecuteRequestErrorAction, -}); - -const formatActionLabel = (action: AccountInspectionAction, t: ReturnType['t']) => { - switch (action) { - case 'delete': - return t('monitoring.account_inspection_action_delete'); - case 'disable': - return t('monitoring.account_inspection_action_disable'); - case 'enable': - return t('monitoring.account_inspection_action_enable'); - case 'keep': - default: - return t('monitoring.account_inspection_action_keep'); - } -}; - -const formatQuotaRemainingLabel = (value: number | null) => { - if (value === null) return '--'; - return `${Math.max(0, 100 - value).toFixed(1)}%`; -}; - -const formatTokenRefreshLabel = ( - item: AccountInspectionResultItem, - t: ReturnType['t'] -) => { - if (item.tokenRefreshStatus === 'success') return t('monitoring.account_inspection_token_refresh_success'); - if (item.tokenRefreshStatus === 'failed') return t('monitoring.account_inspection_token_refresh_failed'); - if (item.nextRefreshAt && item.nextRefreshAt > Date.now()) return t('monitoring.account_inspection_token_refresh_pending'); - return t('monitoring.account_inspection_token_refresh_not_triggered'); -}; - -const formatTokenRefreshDetail = ( - item: AccountInspectionResultItem, - locale: string, - t: ReturnType['t'] -) => { - if (item.tokenRefreshStatus === 'failed') return item.tokenRefreshError || ''; - if (item.nextRefreshAt && item.nextRefreshAt > 0) { - return t('monitoring.account_inspection_token_next_refresh_at', { - time: formatTimestamp(item.nextRefreshAt, locale), - }); - } - return ''; -}; - -const tokenRefreshToneClass = (item: AccountInspectionResultItem) => { - if (item.tokenRefreshStatus === 'success') return styles.stateTextGood; - if (item.tokenRefreshStatus === 'failed') return styles.stateTextBad; - if (item.nextRefreshAt && item.nextRefreshAt > Date.now()) return styles.stateTextWarn; - return styles.stateTextMuted; -}; - -const formatInspectionVerdictPrimary = ( - item: AccountInspectionResultItem, - healthStatus: ResultHealthStatus, - t: ReturnType['t'] -) => { - if (item.tokenRefreshStatus === 'failed') return t('monitoring.account_inspection_verdict_token_refresh_failed'); - - switch (healthStatus) { - case 'inspectionError': - return t('monitoring.account_inspection_verdict_probe_error'); - case 'authInvalid': - return t('monitoring.account_inspection_verdict_auth_invalid'); - case 'quotaExhausted': - return item.disabled - ? t('monitoring.account_inspection_verdict_quota_limited_disabled') - : t('monitoring.account_inspection_verdict_quota_limited'); - case 'recoverable': - return t('monitoring.account_inspection_verdict_quota_recovered'); - case 'disabled': - return t('monitoring.account_inspection_verdict_disabled'); - case 'healthy': - default: - return item.disabled - ? t('monitoring.account_inspection_verdict_healthy_disabled') - : t('monitoring.account_inspection_verdict_healthy'); - } -}; - -const inspectionToastTone = (healthStatus: ResultHealthStatus): 'success' | 'warning' | 'error' => { - if (healthStatus === 'healthy' || healthStatus === 'recoverable') return 'success'; - if (healthStatus === 'inspectionError' || healthStatus === 'authInvalid') return 'error'; - return 'warning'; -}; - -const formatInspectionResultToast = ( - item: AccountInspectionResultItem, - t: ReturnType['t'] -) => { - const healthStatus = resolveResultHealthStatus(item); - const primary = formatInspectionVerdictPrimary(item, healthStatus, t); - return { - message: `${item.fileName}: ${primary}`, - tone: inspectionToastTone(healthStatus), - }; -}; - -const formatTokenRefreshToast = ( - item: AccountInspectionResultItem, - fallbackError: string | undefined, - locale: string, - t: ReturnType['t'] -) => { - const detail = item.tokenRefreshError || fallbackError || formatTokenRefreshDetail(item, locale, t); - if (item.tokenRefreshStatus === 'success') { - return { - message: detail - ? `${item.fileName}: ${t('monitoring.account_inspection_token_refresh_success')} · ${detail}` - : `${item.fileName}: ${t('monitoring.account_inspection_token_refresh_success')}`, - tone: 'success' as const, - }; - } - if (item.tokenRefreshStatus === 'failed' || fallbackError) { - return { - message: detail - ? `${item.fileName}: ${t('monitoring.account_inspection_token_refresh_failed')} · ${detail}` - : `${item.fileName}: ${t('monitoring.account_inspection_token_refresh_failed')}`, - tone: 'error' as const, - }; - } - return { - message: `${item.fileName}: ${t('monitoring.account_inspection_token_refresh_not_triggered')}`, - tone: 'warning' as const, - }; -}; - -const formatCurrentStateLabel = (item: AccountInspectionResultItem, t: ReturnType['t']) => { - if (item.disabled) return t('monitoring.account_inspection_state_disabled'); - return t('monitoring.account_inspection_state_enabled'); -}; - -const formatRunInspectionButtonLabel = (status: RunStatus, t: ReturnType['t']) => { - if (status === 'paused') return t('monitoring.account_inspection_resume'); - if (status === 'running') return t('monitoring.account_inspection_running'); - return t('monitoring.account_inspection_run'); -}; - -const countActions = (items: AccountInspectionResultItem[]) => { - const summary = { - delete: 0, - disable: 0, - enable: 0, - }; - - items.forEach((item) => { - if (item.action === 'delete') summary.delete += 1; - if (item.action === 'disable') summary.disable += 1; - if (item.action === 'enable') summary.enable += 1; - }); - - return summary; -}; - -const toAccountInspectionApiItem = (item: AccountInspectionResultItem): AccountInspectionInspectOneItem => ({ - key: item.key, - provider: item.provider, - fileName: item.fileName, - displayName: item.displayAccount, - email: item.email, - name: item.name, - authIndex: item.authIndex, - disabled: item.disabled, -}); - -const buildActionRiskPreview = (items: AccountInspectionResultItem[], t: ReturnType['t']) => - items - .filter((item) => item.action === 'delete' || item.action === 'disable') - .slice(0, 5) - .map((item) => ({ - key: item.key, - account: item.fileName, - provider: item.provider, - action: formatActionLabel(item.action, t), - reason: item.actionReason || item.error || '-', - dangerous: item.action === 'delete', - })); - -const buildExecuteConfirmationMessage = ( - items: AccountInspectionResultItem[], - t: ReturnType['t'], - hasAutoExecutePolicy: boolean -) => { - const counts = countActions(items); - const preview = buildActionRiskPreview(items, t); - const hasDelete = counts.delete > 0; - - return ( -
-

- {t('monitoring.account_inspection_execute_confirm_body', { - total: items.length, - delete: counts.delete, - disable: counts.disable, - enable: counts.enable, - })} -

-
- {`${t('monitoring.account_inspection_action_delete')}: ${counts.delete}`} - {`${t('monitoring.account_inspection_action_disable')}: ${counts.disable}`} - {`${t('monitoring.account_inspection_action_enable')}: ${counts.enable}`} -
- {preview.length > 0 ? ( -
- {t('monitoring.account_inspection_preview_title')} - {preview.map((item) => ( -
- {item.account} - {item.provider} - {item.action} - {item.reason} -
- ))} -
- ) : null} - {hasAutoExecutePolicy ? ( -

- {t('monitoring.account_inspection_settings_auto_section_desc')} -

- ) : null} - {hasDelete ? ( -

- {t('monitoring.account_inspection_delete_irreversible_warning')} -

- ) : null} -
- ); -}; - -const buildConfirmationAccountCard = ( - item: AccountInspectionResultItem, - t: ReturnType['t'] -) => ( -
-
- {item.fileName} - {item.provider} -
- {item.disabled ? t('monitoring.account_inspection_state_disabled') : t('monitoring.account_inspection_state_enabled')} -
-); - -const buildDeleteConfirmationMessage = ( - item: AccountInspectionResultItem, - t: ReturnType['t'] -) => ( -
-
- {t('monitoring.account_inspection_delete_single_title')} - - {t('monitoring.account_inspection_delete_single_confirm_body', { - account: item.fileName, - })} - -
- {buildConfirmationAccountCard(item, t)} -
- {t('monitoring.account_inspection_delete_single_warning')} -
-
-); - -const buildRefreshTokenConfirmationMessage = ( - item: AccountInspectionResultItem, - t: ReturnType['t'] -) => ( -
-
- {t('monitoring.account_inspection_refresh_token_confirm_title')} - - {t('monitoring.account_inspection_refresh_token_confirm_body', { - account: item.fileName, - })} - -
- {buildConfirmationAccountCard(item, t)} -
- {t('monitoring.account_inspection_refresh_token_confirm_hint')} -
-
-); - -const withChanged = ( - state: S, - key: K, - next: S[K], - isEqual: (left: S[K], right: S[K]) => boolean -): S => { - if (isEqual(next, state[key])) return state; - return { ...state, [key]: next }; -}; - -const sameProgressSnapshot = (left: AccountInspectionProgressSnapshot, right: AccountInspectionProgressSnapshot) => - left.total === right.total && - left.completed === right.completed && - left.inFlight === right.inFlight && - left.pending === right.pending && - left.percent === right.percent && - left.status === right.status && - left.startedAt === right.startedAt && - left.summary.totalFiles === right.summary.totalFiles && - left.summary.probeSetCount === right.summary.probeSetCount && - left.summary.sampledCount === right.summary.sampledCount && - left.summary.disabledCount === right.summary.disabledCount && - left.summary.enabledCount === right.summary.enabledCount && - left.summary.deleteCount === right.summary.deleteCount && - left.summary.disableCount === right.summary.disableCount && - left.summary.enableCount === right.summary.enableCount && - left.summary.keepCount === right.summary.keepCount && - left.summary.errorCount === right.summary.errorCount; - -const sameInspectionSettings = (left: AccountInspectionConfigurableSettings, right: AccountInspectionConfigurableSettings) => - left.targetType === right.targetType && - left.workers === right.workers && - left.deleteWorkers === right.deleteWorkers && - left.timeout === right.timeout && - left.retries === right.retries && - left.usedPercentThreshold === right.usedPercentThreshold && - left.sampleSize === right.sampleSize && - left.antigravityDeepProbeEnabled === right.antigravityDeepProbeEnabled && - left.antigravityDeepProbeModel === right.antigravityDeepProbeModel && - left.antigravityQuotaMode === right.antigravityQuotaMode && - left.xaiDeepProbeEnabled === right.xaiDeepProbeEnabled && - left.xaiDeepProbeModel === right.xaiDeepProbeModel && - left.autoExecuteQuotaLimitDisable === right.autoExecuteQuotaLimitDisable && - left.autoExecuteQuotaRecoveryEnable === right.autoExecuteQuotaRecoveryEnable && - left.autoExecuteAccountInvalidAction === right.autoExecuteAccountInvalidAction && - left.autoExecuteRequestErrorAction === right.autoExecuteRequestErrorAction && - left.autoExecuteConfirmations === right.autoExecuteConfirmations; - -const sameSettingsDraft = (left: InspectionSettingsDraft, right: InspectionSettingsDraft) => - left.targetType === right.targetType && - left.workers === right.workers && - left.deleteWorkers === right.deleteWorkers && - left.timeout === right.timeout && - left.retries === right.retries && - left.usedPercentThreshold === right.usedPercentThreshold && - left.sampleSize === right.sampleSize && - left.antigravityDeepProbeEnabled === right.antigravityDeepProbeEnabled && - left.antigravityDeepProbeModel === right.antigravityDeepProbeModel && - left.antigravityQuotaMode === right.antigravityQuotaMode && - left.xaiDeepProbeEnabled === right.xaiDeepProbeEnabled && - left.xaiDeepProbeModel === right.xaiDeepProbeModel && - left.autoExecuteQuotaLimitDisable === right.autoExecuteQuotaLimitDisable && - left.autoExecuteQuotaRecoveryEnable === right.autoExecuteQuotaRecoveryEnable && - left.autoExecuteAccountInvalidAction === right.autoExecuteAccountInvalidAction && - left.autoExecuteRequestErrorAction === right.autoExecuteRequestErrorAction; - -const sameScheduleDraft = (left: ScheduleDraft, right: ScheduleDraft) => - left.enabled === right.enabled && left.intervalMinutes === right.intervalMinutes; - -type InspectionScheduleSnapshot = AccountInspectionScheduleResponse['schedule']; - -const sameScheduleSnapshot = ( - left: InspectionScheduleSnapshot | null, - right: InspectionScheduleSnapshot | null -) => { - if (left === right) return true; - if (!left || !right) return false; - return left.enabled === right.enabled && - left.intervalMinutes === right.intervalMinutes && - left.nextRunAt === right.nextRunAt && - sameInspectionSettings(left.settings, right.settings); -}; - -const sameAutoExecutionCounts = (left: AutoExecutionCounts, right: AutoExecutionCounts) => - left.delete === right.delete && left.disable === right.disable && left.enable === right.enable; - -const sameRunStatus = (left: RunStatus, right: RunStatus) => left === right; - -const handleAccountInspectionControlError = ( - error: unknown, - appendLog: (level: AccountInspectionLogLevel, message: string) => void, - showNotification: (message: string, type?: 'info' | 'success' | 'warning' | 'error') => void, - fallbackMessage: string -) => { - const message = error instanceof Error ? error.message : String(error || fallbackMessage); - appendLog('error', message); - showNotification(message, 'error'); -}; - -type BackendInspectionViewState = ReturnType; - -type InspectionBackendState = { - inspectionSettings: AccountInspectionConfigurableSettings; - settingsDraft: InspectionSettingsDraft; - scheduleDraft: ScheduleDraft; - schedule: InspectionScheduleSnapshot | null; - logs: InspectionLogEntry[]; - logsPage: AccountInspectionPageInfo | null; - runStatus: RunStatus; - progress: AccountInspectionProgressSnapshot; - result: AccountInspectionRunResult | null; - autoExecutionCounts: AutoExecutionCounts; - restoredSnapshot: boolean; -}; - -type InspectionBackendAction = - | { type: 'configChanged'; settings: AccountInspectionConfigurableSettings; syncDraft: boolean } - | { type: 'backendResponseReceived'; response: AccountInspectionScheduleResponse } - | { type: 'clearSchedule' } - | { type: 'appendLog'; level: AccountInspectionLogLevel; message: string; timestamp: number } - | { type: 'clearLogs' } - | { type: 'startRun'; timestamp: number } - | { type: 'runFailed' } - | { type: 'clearAutoExecutionCounts' } - | { type: 'setResult'; result: AccountInspectionRunResult | null } - | { type: 'resetSettings'; settings: AccountInspectionConfigurableSettings } - | { type: 'setSettingsDraft'; draft: InspectionSettingsDraft } - | { type: 'updateSettingsDraft'; values: Partial } - | { type: 'updateScheduleDraft'; values: Partial }; - -const createInspectionBackendState = (settings: AccountInspectionConfigurableSettings): InspectionBackendState => ({ - inspectionSettings: settings, - settingsDraft: toSettingsDraft(settings), - scheduleDraft: { enabled: false, intervalMinutes: '360' }, - schedule: null, - logs: [], - logsPage: null, - runStatus: 'idle', - progress: createIdleAccountInspectionProgressSnapshot(), - result: null, - autoExecutionCounts: emptyAutoExecutionCounts(), - restoredSnapshot: false, -}); - -const applyBackendViewState = ( - state: InspectionBackendState, - response: AccountInspectionScheduleResponse, - viewState: BackendInspectionViewState -) => { - let nextState = state; - nextState = withChanged(nextState, 'inspectionSettings', viewState.settings, sameInspectionSettings); - nextState = withChanged(nextState, 'settingsDraft', toSettingsDraft(viewState.settings), sameSettingsDraft); - nextState = withChanged(nextState, 'scheduleDraft', viewState.scheduleDraft, sameScheduleDraft); - nextState = withChanged(nextState, 'schedule', response.schedule, sameScheduleSnapshot); - nextState = withChanged(nextState, 'autoExecutionCounts', viewState.autoExecutionCounts, sameAutoExecutionCounts); - nextState = withChanged(nextState, 'progress', viewState.progress, sameProgressSnapshot); - nextState = withChanged(nextState, 'runStatus', viewState.runStatus, sameRunStatus); - nextState = withChanged(nextState, 'restoredSnapshot', viewState.restoredSnapshot, Object.is); - if (viewState.logs) { - nextState = withChanged(nextState, 'logs', viewState.logs, Object.is); - } - if (viewState.logsPage !== undefined) { - nextState = withChanged(nextState, 'logsPage', viewState.logsPage ?? null, Object.is); - } - if (viewState.result !== undefined) { - nextState = withChanged(nextState, 'result', viewState.result, Object.is); - } - return nextState; -}; - -const inspectionBackendReducer = ( - state: InspectionBackendState, - action: InspectionBackendAction -): InspectionBackendState => { - switch (action.type) { - case 'configChanged': { - let nextState = withChanged(state, 'inspectionSettings', action.settings, sameInspectionSettings); - if (action.syncDraft) { - nextState = withChanged(nextState, 'settingsDraft', toSettingsDraft(action.settings), sameSettingsDraft); - } - return nextState; - } - case 'backendResponseReceived': - return applyBackendViewState(state, action.response, buildAccountInspectionBackendViewState(action.response)); - case 'clearSchedule': - return state.schedule === null ? state : { ...state, schedule: null }; - case 'appendLog': - return { - ...state, - logs: appendInspectionLogEntry(state.logs, { - id: `${action.timestamp}-${state.logs.length}`, - level: action.level, - message: action.message, - timestamp: action.timestamp, - }), - }; - case 'clearLogs': - return state.logs.length === 0 ? state : { ...state, logs: [] }; - case 'startRun': - return { - ...state, - result: null, - runStatus: 'running', - restoredSnapshot: false, - autoExecutionCounts: emptyAutoExecutionCounts(), - progress: { - ...createIdleAccountInspectionProgressSnapshot(), - status: 'running', - startedAt: action.timestamp, - updatedAt: action.timestamp, - }, - }; - case 'runFailed': - return state.runStatus === 'error' ? state : { ...state, runStatus: 'error' }; - case 'clearAutoExecutionCounts': - return withChanged(state, 'autoExecutionCounts', emptyAutoExecutionCounts(), sameAutoExecutionCounts); - case 'setResult': - return state.result === action.result ? state : { ...state, result: action.result }; - case 'resetSettings': - return { - ...state, - inspectionSettings: action.settings, - settingsDraft: toSettingsDraft(action.settings), - }; - case 'setSettingsDraft': - return withChanged(state, 'settingsDraft', action.draft, sameSettingsDraft); - case 'updateSettingsDraft': - return { ...state, settingsDraft: { ...state.settingsDraft, ...action.values } }; - case 'updateScheduleDraft': - return { ...state, scheduleDraft: { ...state.scheduleDraft, ...action.values } }; - default: - return state; - } -}; +import styles from '@/features/monitoring/accountInspection.module.scss'; export function AccountInspectionPage() { const { t, i18n } = useTranslation(); diff --git a/cliproxyapi-pro-management/overlay/tests/accountInspectionPageModel.test.ts b/cliproxyapi-pro-management/overlay/tests/accountInspectionPageModel.test.ts new file mode 100644 index 0000000..e9f7174 --- /dev/null +++ b/cliproxyapi-pro-management/overlay/tests/accountInspectionPageModel.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, test } from 'bun:test'; +import { + buildInspectionResultsViewState, + getPaginationRange, + toSettingsDraft, +} from '../src/features/monitoring/accountInspectionPageModel'; +import { + DEFAULT_ACCOUNT_INSPECTION_SETTINGS, + type AccountInspectionResultItem, +} from '../src/features/monitoring/accountInspection'; + +const result = (overrides: Partial = {}): AccountInspectionResultItem => ({ + key: 'auth-1', + fileName: 'account.json', + displayAccount: 'owner@example.com', + authIndex: 'auth-1', + accountId: null, + provider: 'codex', + disabled: false, + status: 'active', + state: 'active', + raw: { name: 'account.json', provider: 'codex' }, + action: 'keep', + actionReason: '', + statusCode: 200, + usedPercent: 20, + isQuota: false, + error: '', + ...overrides, +}); + +describe('account inspection page model', () => { + test('classifies result rows and pending actions in one pass', () => { + const view = buildInspectionResultsViewState([ + result(), + result({ key: 'auth-2', fileName: 'invalid.json', statusCode: 401, action: 'delete' }), + result({ key: 'auth-3', fileName: 'quota.json', isQuota: true, action: 'disable' }), + ]); + + expect(view.healthCounts.total).toBe(3); + expect(view.healthCounts.healthy).toBe(1); + expect(view.healthCounts.authInvalid).toBe(1); + expect(view.healthCounts.quotaExhausted).toBe(1); + expect(view.filterRowCounts.pending).toBe(2); + expect(view.actionableActionCounts).toMatchObject({ delete: 1, disable: 1 }); + }); + + test('keeps pagination and settings draft conversion deterministic', () => { + expect(getPaginationRange({ + page: 2, + pageSize: 100, + total: 250, + totalPages: 3, + hasMore: true, + }, 100)).toMatchObject({ from: 101, to: 200, hasPrevious: true, hasNext: true }); + + expect(toSettingsDraft(DEFAULT_ACCOUNT_INSPECTION_SETTINGS)).toMatchObject({ + workers: String(DEFAULT_ACCOUNT_INSPECTION_SETTINGS.workers), + targetType: DEFAULT_ACCOUNT_INSPECTION_SETTINGS.targetType, + }); + }); +}); diff --git a/cliproxyapi-pro-management/tests/test_account_inspection_module_boundaries.py b/cliproxyapi-pro-management/tests/test_account_inspection_module_boundaries.py new file mode 100644 index 0000000..3d4afd9 --- /dev/null +++ b/cliproxyapi-pro-management/tests/test_account_inspection_module_boundaries.py @@ -0,0 +1,33 @@ +import unittest +from pathlib import Path + + +OVERLAY_ROOT = Path(__file__).resolve().parents[1] / 'overlay/src' +PAGE_PATH = OVERLAY_ROOT / 'pages/AccountInspectionPage.tsx' +FEATURE_ROOT = OVERLAY_ROOT / 'features/monitoring' + + +class AccountInspectionModuleBoundariesTest(unittest.TestCase): + def test_page_remains_a_workflow_controller(self) -> None: + source = PAGE_PATH.read_text() + + self.assertLess(len(source.splitlines()), 2300) + self.assertNotIn('const buildInspectionResultsViewState =', source) + self.assertNotIn('const scheduleAuthFileAccountStats =', source) + self.assertNotIn('const inspectionBackendReducer =', source) + self.assertNotIn('function InspectionErrorDetailsPanel(', source) + + def test_feature_owns_page_model_and_styles(self) -> None: + model = (FEATURE_ROOT / 'accountInspectionPageModel.tsx').read_text() + self.assertIn('export const buildInspectionResultsViewState', model) + self.assertIn('export const inspectionBackendReducer', model) + self.assertTrue((FEATURE_ROOT / 'accountInspection.module.scss').is_file()) + self.assertFalse((OVERLAY_ROOT / 'pages/AccountInspectionPage.module.scss').exists()) + self.assertEqual( + len(list((FEATURE_ROOT / 'account-inspection-styles').glob('*.scss'))), + 6, + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/docs/code-review/2026-07.md b/docs/code-review/2026-07.md index 9f3b022..01f7990 100644 --- a/docs/code-review/2026-07.md +++ b/docs/code-review/2026-07.md @@ -23,6 +23,7 @@ The pinned PR baseline lives in `compatibility/upstream.env`. Pull requests and | 9 | Reproducible Core builds and bidirectional overlay provenance gates | Completed | | 10 | Monitoring page decomposition, duplicate removal, and module-boundary guards | Completed | | 11 | Monitoring controller, modal, account-quota, and stylesheet decomposition | Completed | +| 12 | Account-inspection model, reducer, and stylesheet decomposition | Completed | ## Severity @@ -69,11 +70,26 @@ The pinned PR baseline lives in `compatibility/upstream.env`. Pull requests and | FRONTEND-003 | P2 | Monitoring metadata and aggregates | Metadata and aggregate hooks did not include server identity in their request lifecycle | Equal cursors across servers or out-of-order refreshes could leave channels, auth metadata, rankings, or summaries from the previous connection | Switch between servers with the same latest usage ID while a refresh is in flight | Gate loads on authenticated connection identity, invalidate prior requests, clear cross-server data, and suppress stale responses | Fresh-management type, lint, and integration build validation | Resolved | | FRONTEND-004 | P2 | Account inspection transport | The inspection WebSocket was opened once with no close/error recovery or polling fallback | A transient proxy or network disconnect left progress and results stale until page remount or a manual action | Close the active WebSocket after the initial snapshot | Add capped exponential reconnect, five-second status polling while disconnected, request sequencing, and last-good snapshot preservation | WebSocket URL, credential transport, and reconnect-backoff tests | Resolved | | FRONTEND-005 | P2 | Routing runtime polling | Fifteen-second runtime polls could overlap and race with manual refresh, save, or release responses | An older runtime response could overwrite the latest protected-account/event state | Delay a poll response until after a release response | Prevent overlapping polls and apply only the latest request generation across load, poll, save, refresh, and release paths | Type checking, lint, and production build | Resolved | -| FRONTEND-006 | P2 | Monitoring maintainability | Monitoring, inspection, and routing pages had grown to roughly 6.4k, 3.6k, and 1k lines, and the monitoring page still owned analytics, account quota adaptation, settings/price modals, realtime presentation, and a 6.2k-line page stylesheet | Further realtime, pricing, and account-view changes were difficult to test independently and increased regression risk | Inspect page/component sizes, repeated helper ownership, and page-to-feature dependency direction | Extract monitoring analytics/aggregates, account quota/health, realtime preferences/presentation, controlled settings/price components, account panels, and ordered Sass partials; make the page consume those contracts and guard against reabsorption | Focused Bun tests, source/style-boundary tests, and full Management validation | Mitigated | +| FRONTEND-006 | P2 | Monitoring maintainability | Monitoring, inspection, and routing pages had grown to roughly 6.4k, 3.6k, and 1k lines, and the monitoring page still owned analytics, account quota adaptation, settings/price modals, realtime presentation, and a 6.2k-line page stylesheet | Further realtime, pricing, and account-view changes were difficult to test independently and increased regression risk | Inspect page/component sizes, repeated helper ownership, and page-to-feature dependency direction | Extract monitoring analytics/aggregates, account quota/health, realtime preferences/presentation, controlled settings/price components, account panels, inspection view models/reducers, and ordered Sass partials; make pages consume those contracts and guard against reabsorption | Focused Bun tests, source/style-boundary tests, and full Management validation | Resolved | | LIFECYCLE-001 | P2 | Management handler lifecycle | Account-inspection, routing reconciliation, login-attempt cleanup, named usage registration, and embedded-usage callbacks outlived a stopped server | Embedders that repeatedly constructed and stopped servers retained goroutines and handler references | Construct and stop management handlers repeatedly in one process | Give each handler a cancellable lifecycle, wait for scheduled and active work, remove global registrations, and invoke shutdown from `Server.Stop` | Handler shutdown, named-plugin ownership, full Core tests, and race detector | Resolved | | FRONTEND-007 | P2 | API request cancellation | Connection generations rejected stale Axios responses but did not abort the underlying requests | Superseded requests continued consuming client and server resources until response or timeout | Hold an adapter request open and switch server credentials | Abort the previous connection controller and combine its signal with any caller-provided cancellation signal | Physical connection-abort and caller-cancellation tests | Resolved | | FRONTEND-008 | P2 | Inspection reconnect detail recovery | WebSocket downtime used summary polling, so missed individual log entries were not refreshed immediately when the socket reconnected | Operators could briefly see an incomplete log page after transport recovery | Drop the inspection socket after a live log and reconnect it | Refresh the current filtered result/log detail page immediately on socket open while retaining the last good snapshot on failure | Management transport tests, type checking, lint, and production build | Resolved | | FRONTEND-009 | P2 | Realtime log search and ownership | Account-name search derived auth indexes only from recently loaded event rows and then suppressed raw event-text search; request cancellation, stable cursors, auto-follow, and scroll restoration all remained embedded in the page | Older events for a known account could be omitted, raw matches could disappear when account metadata also matched, and further paging changes had a large regression surface | Search for an account absent from the recent event snapshot while another event contains the same raw term, then paginate the combined results | Build auth matches from complete auth-file metadata, OR them with raw text on the server, preserve both conditions in the stable cursor, and extract the realtime controller into a dedicated hook | Combined-search cursor test, auth metadata search tests, controller source guards, full Core/Management validation, and race detector | Resolved | +| FRONTEND-010 | P2 | Analytics compatibility fallback | Monitoring retains client-side analytics when `/usage/aggregates` is unavailable, while retained event details are intentionally bounded | On older servers, analytics can be approximate when more than 6000 candidate rows are retained | Run Management against a server without the aggregate endpoint and exceed `ACCOUNT_STATS_ANALYTICS_ROW_LIMIT` | Prefer server aggregates, cap fallback work at 6000 rows, and retire the fallback only when the compatibility baseline enforces aggregate API support | Aggregate and fallback analytics Bun tests plus full Management validation | Accepted compatibility boundary | + +## Package 12 Residual Risks + +- `AccountInspectionPage.tsx` is now 2211 lines, down from 3493 (about 37%). It intentionally remains the page-level coordinator for transport lifecycle, destructive-action commands, settings persistence, and rendered composition; pure result derivation, account statistics, confirmation builders, and reducer state now live in the feature model. +- Account-inspection styles remain one CSS module to preserve selector names and cascade behavior, but ownership moved out of `pages/` and the 3487-line stylesheet is split into six ordered responsibility-based partials. +- Client fallback analytics are retained as the accepted `FRONTEND-010` compatibility boundary. Server aggregates remain preferred, fallback work is capped, and removal is tied to a future minimum-server-version decision. + +## Package 12 Validation Results + +- Repository validation: 25 Management customization tests and two reproducible archive tests passed. Boundary guards keep the account-inspection page below 2300 lines, prevent extracted model/reducer implementations from returning to it, and enforce feature ownership of its model and styles. +- Management `v1.18.5`: bidirectional overlay replacement preflight, repeated-application invariance, 110 Bun tests, ESLint with no warnings, TypeScript checking, and the `v1.18.5-pro` production single-file build passed on a fresh checkout. +- Core `v7.2.94`: guarded-source preflight, rejected reapplication invariance, embedded usage tests, management handlers, plugin host/store, Redis queue, auth scheduler tests, and the server build passed on a fresh checkout. +- Focused coverage verifies one-pass inspection-result classification, pending-action counts, pagination boundaries, and deterministic settings-draft conversion. +- The former 3487-line page stylesheet is now a six-line feature entrypoint plus ordered base, workbench, analytics, settings, tables/dialogs, and responsive partials. ## Package 11 Residual Risks @@ -220,4 +236,5 @@ The pinned PR baseline lives in `compatibility/upstream.env`. Pull requests and - Release workflows cannot publish before the same Core and Management validation succeeds. - Scheduled latest-upstream checks report patch drift independently of the pinned PR baseline. - All P0 and P1 findings are fixed or explicitly accepted with an owner and rationale. +- All P2 findings are resolved, mitigated with explicit bounds, or recorded as deliberate compatibility boundaries with a retirement trigger. - Each behavioral fix includes a regression test and a fresh-upstream validation result.