Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions apps/lfx-one/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,37 @@ SUPABASE_STORAGE_BUCKET=your-supabase-bucket-name
# Internal k8s service DNS for NATS cluster
NATS_URL=nats://lfx-platform-nats.lfx.svc.cluster.local:4222

############### SNOWFLAKE CONFIG ###############
# Snowflake Connection Configuration
# Used for analytics endpoints: active-weeks-streak, pull-requests-merged, code-commits
# Account identifier in format: orgname-accountname (e.g., jnmhvwd-xpb85243)
SNOWFLAKE_ACCOUNT=your-org-account
# Service user for read-only analytical queries
SNOWFLAKE_USERNAME=your-username
# Warehouse for query execution
SNOWFLAKE_WAREHOUSE=your-warehouse
# Analytics database name
SNOWFLAKE_DATABASE=your-database
# User role with SELECT-only permissions
SNOWFLAKE_ROLE=your-read-role

# Snowflake Authentication (choose one method)
# Method 1: Direct API Key (recommended for containers/Docker)
# SNOWFLAKE_API_KEY=your-private-key-here

# Method 2: Private Key File (recommended for local development)
# Place rsa_key.p8 file in this directory (same location as .env)
# The service will automatically detect and use it
# SNOWFLAKE_PRIVATE_KEY_PASSPHRASE=your-passphrase-here

# Optional: Connection Pool Configuration (defaults shown)
# SNOWFLAKE_MIN_CONNECTIONS=2
# SNOWFLAKE_MAX_CONNECTIONS=10

# Optional: Lock Strategy for Query Deduplication (defaults to 'memory')
# SNOWFLAKE_LOCK_STRATEGY=memory
############### END SNOWFLAKE CONFIG ###############

# AI Service Configuration
# OpenAI-compatible proxy for meeting agenda generation
AI_PROXY_URL=https://litellm.tools.lfx.dev/chat/completions
Expand Down
4 changes: 4 additions & 0 deletions apps/lfx-one/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ Thumbs.db
/playwright/.auth/
/playwright-report/
/test-results/

# Snowflake
rsa_key.p8
rsa_key.pub
4 changes: 3 additions & 1 deletion apps/lfx-one/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@
"outputMode": "server",
"ssr": {
"entry": "src/server/server.ts"
}
},
"allowedCommonJsDependencies": ["@linuxfoundation/lfx-ui-core"],
"externalDependencies": ["snowflake-sdk", "open"]
},
"configurations": {
"production": {
Expand Down
1 change: 1 addition & 0 deletions apps/lfx-one/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"pino-http": "^10.5.0",
"primeng": "^19.1.4",
"rxjs": "~7.8.2",
"snowflake-sdk": "^2.3.1",
"tslib": "^2.8.1"
},
"devDependencies": {
Expand Down
12 changes: 6 additions & 6 deletions apps/lfx-one/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { AuthContext } from '@lfx-one/shared/interfaces';
import { ToastModule } from 'primeng/toast';

import { HeaderComponent } from './shared/components/header/header.component';
import { AnalyticsService } from './shared/services/analytics.service';
import { SegmentService } from './shared/services/segment.service';
import { UserService } from './shared/services/user.service';

@Component({
Expand All @@ -19,15 +19,15 @@ import { UserService } from './shared/services/user.service';
})
export class AppComponent {
private readonly userService = inject(UserService);
private readonly analyticsService = inject(AnalyticsService);
private readonly segmentService = inject(SegmentService);

public auth: AuthContext | undefined;
public transferState = inject(TransferState);
public serverKey = makeStateKey<AuthContext>('auth');

public constructor() {
// Initialize Segment analytics
this.analyticsService.initialize();
// Initialize Segment tracking
this.segmentService.initialize();

const reqContext = inject(REQUEST_CONTEXT, { optional: true }) as {
auth: AuthContext;
Expand All @@ -51,8 +51,8 @@ export class AppComponent {
this.userService.authenticated.set(true);
this.userService.user.set(this.auth.user);

// Identify user with Segment analytics (pass entire Auth0 user object)
this.analyticsService.identifyUser(this.auth.user);
// Identify user with Segment tracking (pass entire Auth0 user object)
this.segmentService.identifyUser(this.auth.user);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ <h2 class="font-display font-semibold text-gray-900">My Meetings</h2>
</div>

<!-- Scrollable Content -->
<div class="flex flex-col flex-1 max-h-[28.125rem] overflow-y-auto">
<div class="flex flex-col gap-6" data-testid="dashboard-my-meetings-list">
<div class="flex flex-col flex-1">
<div class="flex flex-col gap-6 overflow-scroll max-h-[30rem]" data-testid="dashboard-my-meetings-list">
@if (todayMeetings().length > 0 || upcomingMeetings().length > 0) {
<!-- TODAY Section - only show if there are meetings today -->
@if (todayMeetings().length > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

import { CommonModule } from '@angular/common';
import { Component, computed, ElementRef, inject, ViewChild } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { AnalyticsService } from '@app/shared/services/analytics.service';
import { PersonaService } from '@app/shared/services/persona.service';
import { ChartComponent } from '@components/chart/chart.component';
import { CORE_DEVELOPER_PROGRESS_METRICS, MAINTAINER_PROGRESS_METRICS } from '@lfx-one/shared/constants';

import type { ProgressItemWithChart } from '@lfx-one/shared/interfaces';
import type { ActiveWeeksStreakResponse, ProgressItemWithChart, UserCodeCommitsResponse, UserPullRequestsResponse } from '@lfx-one/shared/interfaces';

@Component({
selector: 'lfx-recent-progress',
Expand All @@ -20,20 +22,66 @@ export class RecentProgressComponent {
@ViewChild('progressScroll') protected progressScrollContainer!: ElementRef;

private readonly personaService = inject(PersonaService);
private readonly analyticsService = inject(AnalyticsService);

/**
* Active weeks streak data from Snowflake
*/
private readonly activeWeeksStreakData = toSignal(this.analyticsService.getActiveWeeksStreak(), {
initialValue: {
data: [],
currentStreak: 0,
totalWeeks: 0,
},
});

/**
* Pull requests merged data from Snowflake
*/
private readonly pullRequestsMergedData = toSignal(this.analyticsService.getPullRequestsMerged(), {
initialValue: {
data: [],
totalPullRequests: 0,
totalDays: 0,
},
});

/**
* Code commits data from Snowflake
*/
private readonly codeCommitsData = toSignal(this.analyticsService.getCodeCommits(), {
initialValue: {
data: [],
totalCommits: 0,
totalDays: 0,
},
});

/**
* Computed signal that returns progress metrics based on the current persona
* Merges hardcoded metrics with real data from Snowflake
*/
protected readonly progressItems = computed<ProgressItemWithChart[]>(() => {
const persona = this.personaService.currentPersona();
const activeWeeksData = this.activeWeeksStreakData();
const pullRequestsData = this.pullRequestsMergedData();
const codeCommitsDataValue = this.codeCommitsData();

const baseMetrics = persona === 'maintainer' ? MAINTAINER_PROGRESS_METRICS : CORE_DEVELOPER_PROGRESS_METRICS;

switch (persona) {
case 'maintainer':
return MAINTAINER_PROGRESS_METRICS;
case 'core-developer':
default:
return CORE_DEVELOPER_PROGRESS_METRICS;
}
// Replace metrics with real data if available
return baseMetrics.map((metric) => {
if (metric.label === 'Active Weeks Streak') {
return this.transformActiveWeeksStreak(activeWeeksData);
}
if (metric.label === 'Pull Requests Merged') {
return this.transformPullRequestsMerged(pullRequestsData);
}
if (metric.label === 'Code Commits') {
return this.transformCodeCommits(codeCommitsDataValue);
}
return metric;
});
});

protected scrollLeft(): void {
Expand All @@ -45,4 +93,167 @@ export class RecentProgressComponent {
const container = this.progressScrollContainer.nativeElement;
container.scrollBy({ left: 300, behavior: 'smooth' });
}

/**
* Transform Active Weeks Streak API response to chart format
* API returns data in ascending order by WEEKS_AGO (0, 1, 2, 3...)
* Display as-is with newest week (week 0) on the left
*/
private transformActiveWeeksStreak(data: ActiveWeeksStreakResponse): ProgressItemWithChart {
// Use data as-is: week 0 (newest) on the left, older weeks on the right
const chartData = data.data;

return {
label: 'Active Weeks Streak',
value: data.currentStreak.toString(),
trend: data.currentStreak > 0 ? 'up' : 'down',
subtitle: 'Current streak',
chartType: 'bar',
chartData: {
labels: chartData.map((row) => `Week ${row.WEEKS_AGO}`),
datasets: [
{
data: chartData.map((row) => row.IS_ACTIVE),
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
fill: true,
tension: 0.4,
borderWidth: 2,
pointRadius: 0,
},
],
},
chartOptions: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
enabled: true,
callbacks: {
title: (context) => context[0].label,
label: (context) => {
const isActive = context.parsed.y === 1;
return isActive ? 'Active' : 'Inactive';
},
},
},
},
scales: {
x: { display: false },
y: { display: false, min: 0, max: 1 },
},
},
};
}

/**
* Transform Pull Requests Merged API response to chart format
* API returns data in ascending order by ACTIVITY_DATE
* Display as-is with oldest date on the left, newest on the right
*/
private transformPullRequestsMerged(data: UserPullRequestsResponse): ProgressItemWithChart {
const chartData = data.data;

return {
label: 'Pull Requests Merged',
value: data.totalPullRequests.toString(),
trend: data.totalPullRequests > 0 ? 'up' : 'down',
subtitle: 'Last 30 days',
chartType: 'line',
chartData: {
labels: chartData.map((row) => row.ACTIVITY_DATE),
datasets: [
{
data: chartData.map((row) => row.DAILY_COUNT),
borderColor: '#0094FF',
backgroundColor: 'rgba(0, 148, 255, 0.1)',
fill: true,
tension: 0,
borderWidth: 2,
pointRadius: 0,
},
],
},
chartOptions: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
enabled: true,
callbacks: {
title: (context) => {
const date = new Date(context[0].label);
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
},
label: (context) => {
const count = context.parsed.y;
return `PRs Merged: ${count}`;
},
},
},
},
scales: {
x: { display: false },
y: { display: false },
},
},
};
}

/**
* Transform Code Commits API response to chart format
* API returns data in ascending order by ACTIVITY_DATE
* Display as-is with oldest date on the left, newest on the right
*/
private transformCodeCommits(data: UserCodeCommitsResponse): ProgressItemWithChart {
const chartData = data.data;

return {
label: 'Code Commits',
value: data.totalCommits.toString(),
trend: data.totalCommits > 0 ? 'up' : 'down',
subtitle: 'Last 30 days',
chartType: 'line',
chartData: {
labels: chartData.map((row) => row.ACTIVITY_DATE),
datasets: [
{
data: chartData.map((row) => row.DAILY_COUNT),
borderColor: '#0094FF',
backgroundColor: 'rgba(0, 148, 255, 0.1)',
fill: true,
tension: 0.4,
borderWidth: 2,
pointRadius: 0,
},
],
},
chartOptions: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
enabled: true,
callbacks: {
title: (context) => {
const date = new Date(context[0].label);
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
},
label: (context) => {
const count = context.parsed.y;
return `Commits: ${count}`;
},
},
},
},
scales: {
x: { display: false },
y: { display: false },
},
},
};
}
}
Loading