diff --git a/apps/lfx-one/src/app/modules/dashboards/components/foundation-health/foundation-health.component.html b/apps/lfx-one/src/app/modules/dashboards/components/foundation-health/foundation-health.component.html index 65cb9c1c..82bf6e84 100644 --- a/apps/lfx-one/src/app/modules/dashboards/components/foundation-health/foundation-health.component.html +++ b/apps/lfx-one/src/app/modules/dashboards/components/foundation-health/foundation-health.component.html @@ -1,184 +1,149 @@ -
- +
-

Foundation Health

- @if (onViewAll()) { +
+

{{ title() }}

+ + + +
+ + +
+ - } +
- -
-
- - - - - - - - - - - - - - - @for (foundation of foundations(); track foundation.id) { - - - - - - - - - - - - + } + } - - + + @case ('bus-factor') { + @if (card.busFactor) { +
+
+ +
+
+ {{ card.busFactor.topCompaniesCount }} Companies ({{ card.busFactor.topCompaniesPercentage }}%) +
+
+
- -
+ } + } - - + } + } + } - - - - } - -
- Foundation - -
- Health Score - -
-
-
- Software Value - -
-
-
- Total Members - -
-
-
- Active Contributors - -
-
-
- Maintainers - -
-
-
- Events - -
-
-
- Org Dependency Risk - -
-
-
-
- + +
+
+ @for (card of metricCards(); track card.title) { +
+
+ +
+ +
{{ card.title }}
+
+ + + @switch (card.customContentType) { + + @case ('sparkline') { + @if (card.chartData) { +
+ +
+ } + } + + + @case ('bar-chart') { + @if (card.chartData) { +
+
+ +
-
-
{{ foundation.name }}
- @if (foundation.projectBreakdown) { -
-
{{ foundation.projectBreakdown.sandbox }} sandbox
-
{{ foundation.projectBreakdown.incubating }} incubating
-
{{ foundation.projectBreakdown.graduated }} graduated
+ } + } + + + @case ('top-projects') { + @if (card.topProjects) { +
+
Top 3 Projects by Value
+ @for (project of card.topProjects; track project.name) { +
+ {{ project.name }} + {{ project.formattedValue }}
- } @else { -
{{ foundation.projectCount }} projects
}
-
-
- - -
- {{ foundation.softwareValueFormatted }} -
-
-
- {{ foundation.totalMembersFormatted }} -
-
-
-
- -
-
- {{ foundation.activeContributorsAvg }} -
-
-
-
-
- -
-
- {{ foundation.maintainersAvg }} + +
+
{{ card.busFactor.otherCompaniesCount }} Other Companies
+
+
+
- -
-
-
-
- @for (height of foundation.barHeights; track $index) { -
+ + @case ('health-scores') { + @if (card.healthScores) { +
+
+ @for (item of healthScoreDistribution(); track item.category) { +
+
+
+
{{ item.category }}
+
{{ item.count }}
+
+
}
-
- {{ foundation.eventsTotal }} -
-
-
-
-
- - - - - - -
-
-
- {{ foundation.orgDependency.topOrgsCount }} orgs: {{ foundation.orgDependency.topOrgsPercentage }}% -
-
- {{ foundation.orgDependency.otherOrgsCount }} orgs: {{ foundation.orgDependency.otherOrgsPercentage }}% -
-
-
-
+ + @if (card.value || card.subtitle) { +
+ @if (card.value) { +
{{ card.value }}
+ } + @if (card.subtitle) { +
{{ card.subtitle }}
+ } +
+ } +
+
+ }
diff --git a/apps/lfx-one/src/app/modules/dashboards/components/foundation-health/foundation-health.component.scss b/apps/lfx-one/src/app/modules/dashboards/components/foundation-health/foundation-health.component.scss index df18259a..b309f957 100644 --- a/apps/lfx-one/src/app/modules/dashboards/components/foundation-health/foundation-health.component.scss +++ b/apps/lfx-one/src/app/modules/dashboards/components/foundation-health/foundation-health.component.scss @@ -4,3 +4,12 @@ :host { display: block; } + +.hide-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + + &::-webkit-scrollbar { + display: none; /* Chrome, Safari and Opera */ + } +} diff --git a/apps/lfx-one/src/app/modules/dashboards/components/foundation-health/foundation-health.component.ts b/apps/lfx-one/src/app/modules/dashboards/components/foundation-health/foundation-health.component.ts index 6c967fee..5e3f4157 100644 --- a/apps/lfx-one/src/app/modules/dashboards/components/foundation-health/foundation-health.component.ts +++ b/apps/lfx-one/src/app/modules/dashboards/components/foundation-health/foundation-health.component.ts @@ -2,109 +2,172 @@ // SPDX-License-Identifier: MIT import { CommonModule } from '@angular/common'; -import { Component, computed, input } from '@angular/core'; +import { Component, computed, ElementRef, signal, ViewChild, input } from '@angular/core'; import { ChartComponent } from '@components/chart/chart.component'; -import { FOUNDATION_HEALTH_DATA } from '@lfx-one/shared/constants'; -import { Foundation, OrgDependencyRiskLevel } from '@lfx-one/shared/interfaces'; +import { FilterOption, FilterPillsComponent } from '@components/filter-pills/filter-pills.component'; +import { AGGREGATE_FOUNDATION_METRICS, FOUNDATION_SPARKLINE_CHART_OPTIONS, FOUNDATION_BAR_CHART_OPTIONS } from '@lfx-one/shared/constants'; +import { FoundationMetricCard, MetricCategory, TopProjectDisplay } from '@lfx-one/shared/interfaces'; import { hexToRgba } from '@lfx-one/shared/utils'; -import { HealthScoreTagComponent } from '../health-score-tag/health-score-tag.component'; - -interface FoundationDisplay extends Foundation { - softwareValueFormatted: string; - totalMembersFormatted: string; - activeContributorsAvg: string; - maintainersAvg: string; - eventsTotal: number; - activeContributorsChartData: { - labels: string[]; - datasets: { - data: number[]; - borderColor: string; - backgroundColor: string; - fill: boolean; - tension: number; - borderWidth: number; - pointRadius: number; - }[]; - }; - maintainersChartData: { - labels: string[]; - datasets: { - data: number[]; - borderColor: string; - backgroundColor: string; - fill: boolean; - tension: number; - borderWidth: number; - pointRadius: number; - }[]; - }; - barHeights: number[]; - pieChartPaths: { - otherPath: string; - topPath: string; - }; - orgDependencyColor: string; - orgDependencyTextColorClass: string; -} - @Component({ selector: 'lfx-foundation-health', - imports: [CommonModule, ChartComponent, HealthScoreTagComponent], + standalone: true, + imports: [CommonModule, FilterPillsComponent, ChartComponent], templateUrl: './foundation-health.component.html', styleUrl: './foundation-health.component.scss', }) export class FoundationHealthComponent { - /** - * Optional callback for "View All" button - */ - public onViewAll = input<() => void>(); - - /** - * Optional filter to show only specific foundation - */ - public foundationFilter = input(); - - /** - * Computed foundations with pre-calculated display values - */ - public readonly foundations = computed(() => { - const filter = this.foundationFilter(); - const filtered = filter ? FOUNDATION_HEALTH_DATA.filter((f) => f.id === filter) : FOUNDATION_HEALTH_DATA; - - return filtered.map((foundation) => ({ - ...foundation, - softwareValueFormatted: this.formatSoftwareValue(foundation.softwareValue), - totalMembersFormatted: foundation.totalMembers.toLocaleString(), - activeContributorsAvg: this.calculateAverage(foundation.activeContributors).toLocaleString(), - maintainersAvg: this.calculateAverage(foundation.maintainers).toLocaleString(), - eventsTotal: this.calculateTotal(foundation.eventsMonthly), - activeContributorsChartData: this.createSparklineData(foundation.activeContributors, '#009AFF'), - maintainersChartData: this.createSparklineData(foundation.maintainers, '#009AFF'), - barHeights: this.calculateBarHeights(foundation.eventsMonthly), - pieChartPaths: this.createPieChartPaths(foundation.orgDependency.topOrgsPercentage), - orgDependencyColor: this.getOrgDependencyColor(foundation.orgDependency.riskLevel), - orgDependencyTextColorClass: this.getOrgDependencyTextColor(foundation.orgDependency.riskLevel), + @ViewChild('carouselScroll') public carouselScrollContainer!: ElementRef; + + public readonly title = input('Foundation Health'); + + public readonly selectedFilter = signal('all'); + + public readonly filterOptions: FilterOption[] = [ + { id: 'all', label: 'All' }, + { id: 'contributors', label: 'Contributors' }, + { id: 'projects', label: 'Projects' }, + { id: 'events', label: 'Events' }, + ]; + + public readonly sparklineOptions = FOUNDATION_SPARKLINE_CHART_OPTIONS; + + public readonly barChartOptions = FOUNDATION_BAR_CHART_OPTIONS; + + private readonly allMetricCards = computed(() => { + const metrics = AGGREGATE_FOUNDATION_METRICS; + + return [ + { + icon: 'fa-light fa-chart-bar', + title: 'Total Projects', + value: metrics.totalProjects.toLocaleString(), + subtitle: 'Across all foundations', + category: 'projects' as MetricCategory, + testId: 'foundation-health-card-total-projects', + customContentType: 'sparkline', + chartData: this.createSparklineData(metrics.totalProjectsData, '#0094FF'), + }, + { + icon: 'fa-light fa-users', + title: 'Total Members', + value: metrics.totalMembers.toLocaleString(), + subtitle: 'Member organizations', + category: 'projects' as MetricCategory, + testId: 'foundation-health-card-total-members', + customContentType: 'sparkline', + chartData: this.createSparklineData(metrics.totalMembersData, '#0094FF'), + }, + { + icon: 'fa-light fa-chart-bar', + title: 'Software Value', + value: this.formatSoftwareValue(metrics.softwareValue), + subtitle: 'Estimated total value of software managed', + category: 'projects' as MetricCategory, + testId: 'foundation-health-card-software-value', + customContentType: 'top-projects', + topProjects: this.formatTopProjects(metrics.topProjectsByValue), + }, + { + icon: 'fa-light fa-shield', + title: 'Company Bus Factor', + value: metrics.companyBusFactor.topCompaniesCount.toString(), + subtitle: 'Companies account for >50% code contributions', + category: 'contributors' as MetricCategory, + testId: 'foundation-health-card-company-bus-factor', + customContentType: 'bus-factor', + busFactor: metrics.companyBusFactor, + }, + { + icon: 'fa-light fa-code', + title: 'Active Contributors', + value: metrics.avgActiveContributors.toLocaleString(), + subtitle: 'Average active contributors over the past year', + category: 'contributors' as MetricCategory, + testId: 'foundation-health-card-active-contributors', + customContentType: 'sparkline', + chartData: this.createSparklineData(metrics.activeContributorsData, '#0094FF'), + }, + { + icon: 'fa-light fa-user-check', + title: 'Maintainers', + value: metrics.avgMaintainers.toString(), + subtitle: 'Average maintainers over the past year', + category: 'contributors' as MetricCategory, + testId: 'foundation-health-card-maintainers', + customContentType: 'sparkline', + chartData: this.createSparklineData(metrics.maintainersData, '#0094FF'), + }, + { + icon: 'fa-light fa-calendar', + title: 'Events', + value: metrics.totalEvents.toString(), + subtitle: 'Total events over 12 months', + category: 'events' as MetricCategory, + testId: 'foundation-health-card-events', + customContentType: 'bar-chart', + chartData: this.createBarChartData(metrics.eventsMonthlyData, '#0094FF'), + }, + { + icon: 'fa-light fa-chart-bar', + title: 'Project Health Scores', + value: '', + subtitle: '', + category: 'projects' as MetricCategory, + testId: 'foundation-health-card-project-health-scores', + customContentType: 'health-scores', + healthScores: metrics.projectHealthDistribution, + }, + ]; + }); + + public readonly metricCards = computed(() => { + const filter = this.selectedFilter(); + const allCards = this.allMetricCards(); + + if (filter === 'all') { + return allCards; + } + + return allCards.filter((card) => card.category === filter); + }); + + public readonly healthScoreDistribution = computed(() => { + const metrics = AGGREGATE_FOUNDATION_METRICS; + const distribution = metrics.projectHealthDistribution; + + const data = [ + { category: 'Critical', count: distribution.critical, color: '#EF4444' }, + { category: 'Unsteady', count: distribution.unsteady, color: '#FB923C' }, + { category: 'Stable', count: distribution.stable, color: '#F59E0B' }, + { category: 'Healthy', count: distribution.healthy, color: '#0094FF' }, + { category: 'Excellent', count: distribution.excellent, color: '#10bc8a' }, + ]; + + const maxCount = Math.max(...data.map((d) => d.count)); + + return data.map((item) => ({ + ...item, + heightPx: Math.round((item.count / maxCount) * 64), })); }); - /** - * Sparkline chart options for contributor/maintainer charts - */ - public readonly sparklineOptions = { - responsive: true, - maintainAspectRatio: false, - plugins: { legend: { display: false }, tooltip: { enabled: false } }, - scales: { - x: { display: false }, - y: { display: false }, - }, - }; - - /** - * Format software value in millions to display format - */ + public handleFilterChange(filter: string): void { + this.selectedFilter.set(filter); + } + + public scrollLeft(): void { + if (!this.carouselScrollContainer?.nativeElement) return; + const container = this.carouselScrollContainer.nativeElement; + container.scrollBy({ left: -320, behavior: 'smooth' }); + } + + public scrollRight(): void { + if (!this.carouselScrollContainer?.nativeElement) return; + const container = this.carouselScrollContainer.nativeElement; + container.scrollBy({ left: 320, behavior: 'smooth' }); + } + private formatSoftwareValue(valueInMillions: number): string { if (valueInMillions >= 1000) { const billions = valueInMillions / 1000; @@ -113,23 +176,13 @@ export class FoundationHealthComponent { return `${valueInMillions.toLocaleString()}M`; } - /** - * Calculate total from array of numbers - */ - private calculateTotal(data: number[]): number { - return data.reduce((sum, val) => sum + val, 0); - } - - /** - * Calculate average from array of numbers - */ - private calculateAverage(data: number[]): number { - return Math.round(data.reduce((sum, val) => sum + val, 0) / data.length); + private formatTopProjects(projects: { name: string; value: number }[]): TopProjectDisplay[] { + return projects.map((project) => ({ + name: project.name, + formattedValue: this.formatSoftwareValue(project.value), + })); } - /** - * Create sparkline chart data for contributors or maintainers - */ private createSparklineData(data: number[], color: string) { return { labels: Array.from({ length: data.length }, (_, i) => `Day ${i + 1}`), @@ -147,76 +200,19 @@ export class FoundationHealthComponent { }; } - /** - * Calculate bar heights for monthly bar chart - */ - private calculateBarHeights(data: number[]): number[] { - const maxValue = Math.max(...data); - const minValue = Math.min(...data); - const range = maxValue - minValue; - - return data.map((value) => { - if (range === 0) { - return 50; - } - const heightPercent = ((value - minValue) / range) * 100; - return Math.max(heightPercent, 10); - }); - } - - /** - * Get color for org dependency pie chart based on risk level - */ - private getOrgDependencyColor(riskLevel: OrgDependencyRiskLevel): string { - const riskColors: Record = { - low: '#0094FF', - moderate: '#F59E0B', - high: '#EF4444', - }; - return riskColors[riskLevel]; - } - - /** - * Get text color class for org dependency based on risk level - */ - private getOrgDependencyTextColor(riskLevel: OrgDependencyRiskLevel): string { - const colorMap: Record = { - low: 'text-[#0094FF]', - moderate: 'text-amber-600', - high: 'text-red-600', - }; - return colorMap[riskLevel]; - } - - /** - * Create SVG path for pie chart slice - */ - private createPieSlice(startAngle: number, endAngle: number): string { - const centerX = 20; - const centerY = 20; - const radius = 16; - - const startRad = ((startAngle - 90) * Math.PI) / 180; - const endRad = ((endAngle - 90) * Math.PI) / 180; + private createBarChartData(data: number[], color: string) { + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - const x1 = centerX + radius * Math.cos(startRad); - const y1 = centerY + radius * Math.sin(startRad); - const x2 = centerX + radius * Math.cos(endRad); - const y2 = centerY + radius * Math.sin(endRad); - - const largeArc = endAngle - startAngle > 180 ? 1 : 0; - - return `M ${centerX} ${centerY} L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} Z`; - } - - /** - * Create pie chart paths for org dependency visualization - */ - private createPieChartPaths(topPercentage: number): { otherPath: string; topPath: string } { - const otherAngle = 360 - topPercentage * 3.6; return { - otherPath: this.createPieSlice(0, otherAngle), - topPath: this.createPieSlice(otherAngle, 360), + labels: months, + datasets: [ + { + data, + backgroundColor: color, + borderColor: color, + borderWidth: 0, + }, + ], }; } } diff --git a/packages/shared/src/constants/foundation-health.constants.ts b/packages/shared/src/constants/foundation-health.constants.ts index 9b78e491..8d2f2191 100644 --- a/packages/shared/src/constants/foundation-health.constants.ts +++ b/packages/shared/src/constants/foundation-health.constants.ts @@ -1,7 +1,7 @@ // Copyright The Linux Foundation and each contributor to LFX. // SPDX-License-Identifier: MIT -import { Foundation } from '../interfaces/dashboard.interface'; +import { Foundation, AggregateFoundationMetrics, ProjectHealthDistribution, CompanyBusFactor, TopProjectByValue } from '../interfaces'; /** * Generate smooth trend data for sparkline charts @@ -26,36 +26,139 @@ const generateSmoothData = (days: number, baseValue: number, variation: number): /** * Foundation health data for board member dashboard - * Matches the React implementation with mock data for 5 major foundations - */ -export const FOUNDATION_HEALTH_DATA: Foundation[] = [ - { - id: 'cncf', - name: 'CNCF', - logo: 'https://www.cncf.io/wp-content/uploads/2023/04/cncf-color.svg', - projectBreakdown: { - sandbox: 45, - incubating: 28, - graduated: 18, - }, - totalMembers: 485, - memberBreakdown: { - platinum: 100, - gold: 150, - silver: 235, - }, - softwareValue: 2800, // $2.8B - activeContributors: generateSmoothData(365, 3000, 200), - maintainers: generateSmoothData(365, 415, 35), - eventsMonthly: [2, 1, 2, 3, 2, 1, 2, 3, 2, 1, 3, 2], - upcomingEvents: 10, - orgDependency: { - topOrgsCount: 28, - topOrgsPercentage: 52, - otherOrgsCount: 497, - otherOrgsPercentage: 48, - riskLevel: 'low', + * Mock aggregate data across all foundations + */ +export const FOUNDATION_HEALTH_DATA: Foundation = { + id: 'aggregate', + name: 'All Foundations', + logo: '', + projectBreakdown: { + sandbox: 45, + incubating: 28, + graduated: 18, + }, + totalMembers: 485, + memberBreakdown: { + platinum: 100, + gold: 150, + silver: 235, + }, + softwareValue: 2800, + activeContributors: generateSmoothData(365, 3000, 200), + maintainers: generateSmoothData(365, 415, 35), + eventsMonthly: [2, 1, 2, 3, 2, 1, 2, 3, 2, 1, 3, 2], + upcomingEvents: 10, + orgDependency: { + topOrgsCount: 28, + topOrgsPercentage: 52, + otherOrgsCount: 497, + otherOrgsPercentage: 48, + riskLevel: 'low', + }, + healthScore: 'excellent', +}; + +/** + * Chart.js configuration for sparkline charts (line charts showing trends) + * Used for displaying small trend visualizations in foundation health metrics + */ +export const FOUNDATION_SPARKLINE_CHART_OPTIONS = { + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: false }, tooltip: { enabled: false } }, + scales: { + x: { display: false }, + y: { display: false }, + }, +}; + +/** + * Chart.js configuration for bar charts + * Used for displaying bar chart visualizations in foundation health metrics + */ +export const FOUNDATION_BAR_CHART_OPTIONS = { + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: false }, tooltip: { enabled: false } }, + scales: { + x: { display: false }, + y: { display: false }, + }, + datasets: { + bar: { + barPercentage: 0.9, + categoryPercentage: 0.95, + borderRadius: 4, + borderSkipped: false, }, - healthScore: 'excellent', }, +}; + +/** + * Calculate average from array of numbers + */ +const calculateAverage = (data: number[]): number => { + return Math.round(data.reduce((sum, val) => sum + val, 0) / data.length); +}; + +/** + * Calculate total from array of numbers + */ +const calculateTotal = (data: number[]): number => { + return data.reduce((sum, val) => sum + val, 0); +}; + +/** + * Project health score distribution for all foundations + * Mock data showing distribution of projects across health categories + */ +export const PROJECT_HEALTH_DISTRIBUTION: ProjectHealthDistribution = { + excellent: 350, + healthy: 438, + stable: 238, + unsteady: 88, + critical: 25, +}; + +/** + * Company bus factor data + * Shows concentration risk from top contributing companies + */ +export const COMPANY_BUS_FACTOR: CompanyBusFactor = { + topCompaniesCount: 28, + topCompaniesPercentage: 52, + otherCompaniesCount: 142, + otherCompaniesPercentage: 48, +}; + +/** + * Top projects by software value + * Highest value projects across all foundations + */ +export const TOP_PROJECTS_BY_VALUE: TopProjectByValue[] = [ + { name: 'Kubernetes', value: 985 }, + { name: 'Linux Kernel', value: 847 }, + { name: 'Envoy', value: 623 }, ]; + +/** + * Aggregate foundation metrics across all foundations + * Mock data for demonstration purposes + */ +export const AGGREGATE_FOUNDATION_METRICS: AggregateFoundationMetrics = { + totalProjects: Object.values(PROJECT_HEALTH_DISTRIBUTION).reduce((sum, val) => sum + val, 0), + totalProjectsData: generateSmoothData(365, 1139, 50), + totalMembers: 485, + totalMembersData: generateSmoothData(365, 485, 25), + softwareValue: 2800, + softwareValueData: generateSmoothData(365, 2800, 150), + topProjectsByValue: TOP_PROJECTS_BY_VALUE, + companyBusFactor: COMPANY_BUS_FACTOR, + avgActiveContributors: calculateAverage(FOUNDATION_HEALTH_DATA.activeContributors), + activeContributorsData: FOUNDATION_HEALTH_DATA.activeContributors, + avgMaintainers: calculateAverage(FOUNDATION_HEALTH_DATA.maintainers), + maintainersData: FOUNDATION_HEALTH_DATA.maintainers, + totalEvents: calculateTotal(FOUNDATION_HEALTH_DATA.eventsMonthly), + eventsMonthlyData: FOUNDATION_HEALTH_DATA.eventsMonthly, + projectHealthDistribution: PROJECT_HEALTH_DISTRIBUTION, +}; diff --git a/packages/shared/src/interfaces/foundation-metrics.interface.ts b/packages/shared/src/interfaces/foundation-metrics.interface.ts new file mode 100644 index 00000000..86e4d38e --- /dev/null +++ b/packages/shared/src/interfaces/foundation-metrics.interface.ts @@ -0,0 +1,139 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +/** + * Metric category type for foundation health filtering + * @description Used to categorize and filter foundation health metrics + */ +export type MetricCategory = 'contributors' | 'projects' | 'events'; + +/** + * Foundation metric card configuration + * @description Configuration for individual metric cards in the foundation health carousel + */ +export interface FoundationMetricCard { + /** Icon name for the metric */ + icon: string; + /** Metric title */ + title: string; + /** Metric display value */ + value: string; + /** Metric subtitle description */ + subtitle: string; + /** Category for filtering */ + category: MetricCategory; + /** Test ID for E2E testing */ + testId: string; + /** Custom content type for the card */ + customContentType?: 'sparkline' | 'bar-chart' | 'top-projects' | 'bus-factor' | 'health-scores'; + /** Chart.js data for sparkline or bar chart */ + chartData?: { + labels: string[]; + datasets: { + data: number[]; + borderColor?: string; + backgroundColor?: string; + fill?: boolean; + tension?: number; + borderWidth?: number; + pointRadius?: number; + }[]; + }; + /** Data for top projects list */ + topProjects?: TopProjectDisplay[]; + /** Data for company bus factor */ + busFactor?: CompanyBusFactor; + /** Data for project health scores distribution */ + healthScores?: ProjectHealthDistribution; +} + +/** + * Project health score distribution + * @description Breakdown of projects by health score category + */ +export interface ProjectHealthDistribution { + /** Number of projects with excellent health */ + excellent: number; + /** Number of projects with healthy status */ + healthy: number; + /** Number of projects with stable status */ + stable: number; + /** Number of projects with unsteady status */ + unsteady: number; + /** Number of projects with critical status */ + critical: number; +} + +/** + * Company bus factor data + * @description Metrics showing concentration risk from top contributing companies + */ +export interface CompanyBusFactor { + /** Number of top companies accounting for >50% contributions */ + topCompaniesCount: number; + /** Percentage of contributions from top companies */ + topCompaniesPercentage: number; + /** Number of other contributing companies */ + otherCompaniesCount: number; + /** Percentage of contributions from other companies */ + otherCompaniesPercentage: number; +} + +/** + * Top project by software value + * @description Individual project with estimated software value + */ +export interface TopProjectByValue { + /** Project name */ + name: string; + /** Estimated software value in millions of dollars */ + value: number; +} + +/** + * Top project display format + * @description Formatted project data for display in foundation health cards + */ +export interface TopProjectDisplay { + /** Project name */ + name: string; + /** Formatted display value (e.g., "985M" or "2.8B") */ + formattedValue: string; +} + +/** + * Aggregate foundation metrics + * @description Summary metrics across all foundations + */ +export interface AggregateFoundationMetrics { + /** Total number of projects across all foundations */ + totalProjects: number; + /** Historical data for total projects (365 days) */ + totalProjectsData: number[]; + /** Total number of member organizations */ + totalMembers: number; + /** Historical data for total members (365 days) */ + totalMembersData: number[]; + /** Total estimated software value in millions */ + softwareValue: number; + /** Historical data for software value (365 days) */ + softwareValueData: number[]; + /** Top 3 projects by software value */ + topProjectsByValue: TopProjectByValue[]; + /** Company bus factor metrics */ + companyBusFactor: CompanyBusFactor; + /** Average active contributors across foundations */ + avgActiveContributors: number; + /** Historical data for active contributors (365 days) */ + activeContributorsData: number[]; + /** Average maintainers across foundations */ + avgMaintainers: number; + /** Historical data for maintainers (365 days) */ + maintainersData: number[]; + /** Total events across all foundations (past year) */ + totalEvents: number; + /** Monthly event counts (12 months) */ + eventsMonthlyData: number[]; + /** Project health score distribution */ + projectHealthDistribution: ProjectHealthDistribution; +} diff --git a/packages/shared/src/interfaces/index.ts b/packages/shared/src/interfaces/index.ts index 931b0fdb..1d2c8b15 100644 --- a/packages/shared/src/interfaces/index.ts +++ b/packages/shared/src/interfaces/index.ts @@ -43,6 +43,9 @@ export * from './meeting-attachment.interface'; // Dashboard interfaces export * from './dashboard.interface'; +// Foundation metrics interfaces +export * from './foundation-metrics.interface'; + // AI interfaces export * from './ai.interface';