diff --git a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/items/risk-insights-prototype-items.component.ts b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/items/risk-insights-prototype-items.component.ts
index 072e11858a4..b440aaa1c0a 100644
--- a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/items/risk-insights-prototype-items.component.ts
+++ b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/items/risk-insights-prototype-items.component.ts
@@ -42,6 +42,7 @@ export class RiskInsightsPrototypeItemsComponent {
// Processing state
readonly processingPhase = this.orchestrator.processingPhase;
+ readonly error = this.orchestrator.error;
// Results
readonly items = this.orchestrator.items;
diff --git a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/members/risk-insights-prototype-members.component.html b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/members/risk-insights-prototype-members.component.html
index 93cc34613b0..d878ed24c3d 100644
--- a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/members/risk-insights-prototype-members.component.html
+++ b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/members/risk-insights-prototype-members.component.html
@@ -105,42 +105,7 @@
-
-
- @if (cipher.weakPassword === true) {
- W
- }
-
- @if (cipher.reusedPassword === true) {
- R
- }
-
- @if (cipher.exposedPassword === true) {
- E
- }
-
- @if (
- cipher.status === RiskInsightsItemStatus.Healthy &&
- !cipher.weakPassword &&
- !cipher.reusedPassword &&
- !cipher.exposedPassword
- ) {
-
- }
-
- @if (cipher.status === null) {
-
- }
-
+
|
@@ -163,6 +128,15 @@
}
+ } @else if (processingPhase() === ProcessingPhase.Error) {
+
+
+
+
{{ "errorOccurred" | i18n }}
+ @if (error()) {
+
{{ error() }}
+ }
+
diff --git a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/members/risk-insights-prototype-members.component.ts b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/members/risk-insights-prototype-members.component.ts
index d1223890e8a..f1253d035e3 100644
--- a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/members/risk-insights-prototype-members.component.ts
+++ b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/members/risk-insights-prototype-members.component.ts
@@ -20,6 +20,8 @@ import {
import { BadgeModule, TableDataSource, TableModule } from "@bitwarden/components";
/* eslint-enable no-restricted-imports */
+import { CipherHealthBadgesComponent } from "../shared/cipher-health-badges.component";
+
/**
* Members tab component for the Risk Insights Prototype.
*
@@ -33,7 +35,7 @@ import { BadgeModule, TableDataSource, TableModule } from "@bitwarden/components
selector: "app-risk-insights-prototype-members",
templateUrl: "./risk-insights-prototype-members.component.html",
standalone: true,
- imports: [CommonModule, JslibModule, TableModule, BadgeModule],
+ imports: [CommonModule, JslibModule, TableModule, BadgeModule, CipherHealthBadgesComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RiskInsightsPrototypeMembersComponent {
@@ -49,6 +51,7 @@ export class RiskInsightsPrototypeMembersComponent {
// Processing state
readonly processingPhase = this.orchestrator.processingPhase;
readonly memberProgress = this.orchestrator.memberProgress;
+ readonly error = this.orchestrator.error;
// Results
readonly members = this.orchestrator.members;
@@ -77,9 +80,6 @@ export class RiskInsightsPrototypeMembersComponent {
return new Map(items.map((item) => [item.cipherId, item]));
});
- /** Whether we've requested member data to be built (lazy loading trigger) */
- private hasRequestedMemberData = false;
-
// ============================================================================
// Lifecycle
// ============================================================================
@@ -92,12 +92,14 @@ export class RiskInsightsPrototypeMembersComponent {
});
// Effect to trigger lazy loading of member aggregations when phase is ready
+ // Check actual state (members array length) instead of a local flag to handle
+ // component re-creation and state resets correctly
effect(() => {
const phase = this.processingPhase();
const isReady = phase === ProcessingPhase.Complete || phase === ProcessingPhase.RunningHibp;
+ const membersEmpty = this.members().length === 0;
- if (isReady && !this.hasRequestedMemberData) {
- this.hasRequestedMemberData = true;
+ if (isReady && membersEmpty) {
this.orchestrator.ensureMemberAggregationsBuilt();
}
});
diff --git a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/shared/cipher-health-badges.component.ts b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/shared/cipher-health-badges.component.ts
new file mode 100644
index 00000000000..9bf89bebb26
--- /dev/null
+++ b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/shared/cipher-health-badges.component.ts
@@ -0,0 +1,74 @@
+/* eslint-disable no-restricted-imports -- Prototype feature using licensed services */
+import { CommonModule } from "@angular/common";
+import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import {
+ RiskInsightsItem,
+ RiskInsightsItemStatus,
+} from "@bitwarden/common/dirt/reports/risk-insights";
+import { BadgeModule } from "@bitwarden/components";
+/* eslint-enable no-restricted-imports */
+
+/**
+ * Shared component for displaying cipher health badges (Weak/Reused/Exposed indicators).
+ *
+ * Used in the Applications and Members tabs' expanded row views to display
+ * consistent health status badges for individual ciphers.
+ *
+ * Displays:
+ * - W badge (warning) for weak passwords
+ * - R badge (warning) for reused passwords
+ * - E badge (danger) for exposed passwords
+ * - Check icon for healthy items
+ * - Spinner for items still loading
+ */
+@Component({
+ selector: "app-cipher-health-badges",
+ standalone: true,
+ imports: [CommonModule, JslibModule, BadgeModule],
+ template: `
+
+ @if (cipher().weakPassword === true) {
+ W
+ }
+ @if (cipher().reusedPassword === true) {
+ R
+ }
+ @if (cipher().exposedPassword === true) {
+ E
+ }
+ @if (isHealthy()) {
+
+ }
+ @if (cipher().status === null) {
+
+ }
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class CipherHealthBadgesComponent {
+ /** The cipher item to display health badges for */
+ readonly cipher = input.required
();
+
+ /** Computed signal to determine if the cipher is healthy with no issues */
+ protected readonly isHealthy = computed(() => {
+ const c = this.cipher();
+ return (
+ c.status === RiskInsightsItemStatus.Healthy &&
+ !c.weakPassword &&
+ !c.reusedPassword &&
+ !c.exposedPassword
+ );
+ });
+}
diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-prototype-orchestration.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-prototype-orchestration.service.ts
index 9e96c155f76..95e5b83aa20 100644
--- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-prototype-orchestration.service.ts
+++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-prototype-orchestration.service.ts
@@ -4,6 +4,7 @@ import { from, Observable, of } from "rxjs";
import { catchError, switchMap, tap, last, map } from "rxjs/operators";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -48,6 +49,7 @@ export class RiskInsightsPrototypeOrchestrationService {
private readonly cipherAccessMappingService = inject(CipherAccessMappingService);
private readonly passwordHealthService = inject(PasswordHealthService);
private readonly riskInsightsService = inject(RiskInsightsPrototypeService);
+ private readonly logService = inject(LogService);
private readonly destroyRef = inject(DestroyRef);
// ============================================================================
@@ -466,7 +468,8 @@ export class RiskInsightsPrototypeOrchestrationService {
}),
last(),
map((): void => undefined),
- catchError(() => {
+ catchError((err: unknown) => {
+ this.logService.error("[RiskInsightsPrototype] Error loading member counts:", err);
return of(undefined);
}),
);