('IntegrationService', [
+ 'getGitHubAuthUrl',
+ 'getGitHubRepos',
+ 'connectGitHub',
+ 'updateGitHubRepos',
+ 'disconnectGitHub',
+ 'syncGitHub',
+ 'getSyncStatus'
+ ]);
+
+ mockIntegrationService.getGitHubRepos.and.returnValue(of({
+ connected: true,
+ accountName: 'Sentinent Engineering',
+ accountHandle: '@sentinent-dev',
+ repos: [
+ { id: 1, name: 'frontend-angular', fullName: 'Sentinent-AI/frontend-angular', isConnected: true }
+ ],
+ lastSyncAt: new Date('2026-03-23T10:00:00Z')
+ }));
+ mockIntegrationService.updateGitHubRepos.and.returnValue(of(void 0));
+ mockIntegrationService.syncGitHub.and.returnValue(of({ syncId: 'sync-1' }));
+ mockIntegrationService.getSyncStatus.and.returnValue(of({
+ syncId: 'sync-1',
+ status: 'completed',
+ itemsSynced: 6,
+ completedAt: new Date('2026-03-23T10:05:00Z')
+ }));
+ mockIntegrationService.disconnectGitHub.and.returnValue(of(void 0));
+ mockIntegrationService.getGitHubAuthUrl.and.returnValue(of({ authUrl: 'https://github.com/mock' }));
+ mockIntegrationService.connectGitHub.and.returnValue(of({ connected: true }));
+
+ await TestBed.configureTestingModule({
+ imports: [WorkspaceIntegrationsComponent],
+ providers: [
+ { provide: IntegrationService, useValue: mockIntegrationService },
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ snapshot: {
+ paramMap: convertToParamMap({ id: 'workspace-1' })
+ }
+ }
+ }
+ ]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(WorkspaceIntegrationsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should render connected repository information', () => {
+ const compiled = fixture.nativeElement as HTMLElement;
+ expect(compiled.textContent).toContain('frontend-angular');
+ expect(compiled.textContent).toContain('Sentinent Engineering');
+ });
+
+ it('should trigger sync and show refreshed item count', () => {
+ component.syncNow();
+ fixture.detectChanges();
+
+ expect(mockIntegrationService.syncGitHub).toHaveBeenCalled();
+ expect(component.feedbackMessage).toContain('6 items refreshed');
+ });
+});
diff --git a/src/app/components/workspace-integrations/workspace-integrations.ts b/src/app/components/workspace-integrations/workspace-integrations.ts
new file mode 100644
index 0000000..76d68de
--- /dev/null
+++ b/src/app/components/workspace-integrations/workspace-integrations.ts
@@ -0,0 +1,117 @@
+import { CommonModule } from '@angular/common';
+import { Component, OnInit, inject } from '@angular/core';
+import { ActivatedRoute, RouterLink } from '@angular/router';
+import { IntegrationService } from '../../services/integration.service';
+import { GitHubRepo, SyncStatus } from '../../models/github-integration.model';
+
+@Component({
+ selector: 'app-workspace-integrations',
+ standalone: true,
+ imports: [CommonModule, RouterLink],
+ templateUrl: './workspace-integrations.html',
+ styleUrl: './workspace-integrations.css'
+})
+export class WorkspaceIntegrationsComponent implements OnInit {
+ private readonly route = inject(ActivatedRoute);
+ private readonly integrationService = inject(IntegrationService);
+
+ workspaceId = '';
+ repos: GitHubRepo[] = [];
+ selectedRepoIds: number[] = [];
+ isConnected = false;
+ accountName = '';
+ accountHandle = '';
+ lastSyncAt?: Date;
+ syncStatus?: SyncStatus;
+ feedbackMessage = '';
+ errorMessage = '';
+ isSaving = false;
+ isSyncing = false;
+
+ ngOnInit(): void {
+ this.workspaceId = this.route.snapshot.paramMap.get('id') ?? '';
+ this.loadRepos();
+ }
+
+ connectGitHub(): void {
+ this.errorMessage = '';
+ this.feedbackMessage = 'Starting GitHub connection...';
+ this.integrationService.getGitHubAuthUrl().subscribe(() => {
+ this.integrationService.connectGitHub().subscribe(() => {
+ this.feedbackMessage = 'GitHub account connected. Select the repositories you want Sentinent to monitor.';
+ this.loadRepos();
+ });
+ });
+ }
+
+ disconnectGitHub(): void {
+ this.integrationService.disconnectGitHub().subscribe(() => {
+ this.syncStatus = undefined;
+ this.feedbackMessage = 'GitHub integration disconnected.';
+ this.loadRepos();
+ });
+ }
+
+ toggleRepo(repoId: number, checked: boolean): void {
+ this.selectedRepoIds = checked
+ ? [...this.selectedRepoIds, repoId]
+ : this.selectedRepoIds.filter(id => id !== repoId);
+ }
+
+ onRepoToggle(repoId: number, event: Event): void {
+ const input = event.target as HTMLInputElement | null;
+ this.toggleRepo(repoId, input?.checked ?? false);
+ }
+
+ saveRepoSelection(): void {
+ this.isSaving = true;
+ this.errorMessage = '';
+ this.integrationService.updateGitHubRepos(this.selectedRepoIds).subscribe({
+ next: () => {
+ this.isSaving = false;
+ this.feedbackMessage = 'Repository selection saved.';
+ this.loadRepos();
+ },
+ error: () => {
+ this.isSaving = false;
+ this.errorMessage = 'Could not save repository selection.';
+ }
+ });
+ }
+
+ syncNow(): void {
+ this.isSyncing = true;
+ this.errorMessage = '';
+ this.integrationService.syncGitHub().subscribe({
+ next: ({ syncId }) => {
+ this.integrationService.getSyncStatus(syncId).subscribe(status => {
+ this.syncStatus = status;
+ this.isSyncing = false;
+ this.lastSyncAt = status.completedAt;
+ this.feedbackMessage = status.status === 'completed'
+ ? `Sync completed. ${status.itemsSynced ?? 0} items refreshed.`
+ : 'GitHub sync failed.';
+ });
+ },
+ error: (error: Error) => {
+ this.isSyncing = false;
+ this.errorMessage = error.message;
+ }
+ });
+ }
+
+ isRepoSelected(repoId: number): boolean {
+ return this.selectedRepoIds.includes(repoId);
+ }
+
+ private loadRepos(): void {
+ this.integrationService.getGitHubRepos().subscribe(response => {
+ this.isConnected = response.connected;
+ this.repos = response.repos;
+ this.selectedRepoIds = response.repos.filter(repo => repo.isConnected).map(repo => repo.id);
+ this.accountName = response.accountName ?? '';
+ this.accountHandle = response.accountHandle ?? '';
+ this.lastSyncAt = response.lastSyncAt;
+ });
+ }
+}
diff --git a/src/app/components/workspace/workspace-details/workspace-details.html b/src/app/components/workspace/workspace-details/workspace-details.html
index a7f6e6f..f864e8f 100644
--- a/src/app/components/workspace/workspace-details/workspace-details.html
+++ b/src/app/components/workspace/workspace-details/workspace-details.html
@@ -9,6 +9,7 @@ {{ workspace.name }}
diff --git a/src/app/models/github-integration.model.ts b/src/app/models/github-integration.model.ts
new file mode 100644
index 0000000..49c29e0
--- /dev/null
+++ b/src/app/models/github-integration.model.ts
@@ -0,0 +1,21 @@
+export interface GitHubRepo {
+ id: number;
+ name: string;
+ fullName: string;
+ isConnected: boolean;
+}
+
+export interface SyncStatus {
+ syncId: string;
+ status: 'in_progress' | 'completed' | 'failed';
+ itemsSynced?: number;
+ completedAt?: Date;
+}
+
+export interface GitHubConnectionState {
+ connected: boolean;
+ accountName?: string;
+ accountHandle?: string;
+ repos: GitHubRepo[];
+ lastSyncAt?: Date;
+}
diff --git a/src/app/models/signal.model.ts b/src/app/models/signal.model.ts
new file mode 100644
index 0000000..529ccfb
--- /dev/null
+++ b/src/app/models/signal.model.ts
@@ -0,0 +1,33 @@
+export type SignalSourceType = 'slack' | 'github' | 'email' | 'decision';
+export type SignalStatus = 'unread' | 'read' | 'archived';
+
+export interface SignalMetadata {
+ type?: 'issue' | 'pull_request';
+ number?: number;
+ repository?: string;
+ state?: 'open' | 'closed';
+ labels?: string[];
+ assignees?: string[];
+ createdAt?: Date;
+ updatedAt?: Date;
+ [key: string]: unknown;
+}
+
+export interface Signal {
+ id: string;
+ sourceType: SignalSourceType;
+ sourceId: string;
+ externalId: string;
+ title: string;
+ content: string;
+ author: string;
+ status: SignalStatus;
+ receivedAt: Date;
+ url?: string;
+ metadata: SignalMetadata;
+}
+
+export interface SignalFilters {
+ source: 'all' | 'github' | 'slack' | 'decision';
+ status: 'all' | SignalStatus;
+}
diff --git a/src/app/services/integration.service.ts b/src/app/services/integration.service.ts
new file mode 100644
index 0000000..1255b0e
--- /dev/null
+++ b/src/app/services/integration.service.ts
@@ -0,0 +1,106 @@
+import { Injectable } from '@angular/core';
+import { Observable, of, throwError } from 'rxjs';
+import { GitHubConnectionState, GitHubRepo, SyncStatus } from '../models/github-integration.model';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class IntegrationService {
+ private githubState: GitHubConnectionState = {
+ connected: false,
+ repos: [
+ { id: 101, name: 'frontend-angular', fullName: 'Sentinent-AI/frontend-angular', isConnected: true },
+ { id: 102, name: 'backend-go', fullName: 'Sentinent-AI/backend-go', isConnected: false },
+ { id: 103, name: 'Sentinent', fullName: 'Sentinent-AI/Sentinent', isConnected: true }
+ ]
+ };
+
+ private latestSync: SyncStatus | null = null;
+
+ getGitHubAuthUrl(): Observable<{ authUrl: string }> {
+ return of({
+ authUrl: 'https://github.com/login/oauth/authorize?client_id=sentinent-demo&scope=read:user%20read:org%20repo&state=mock-state'
+ });
+ }
+
+ getGitHubRepos(): Observable<{ connected: boolean; repos: GitHubRepo[]; accountName?: string; accountHandle?: string; lastSyncAt?: Date }> {
+ return of({
+ connected: this.githubState.connected,
+ repos: this.githubState.repos,
+ accountName: this.githubState.accountName,
+ accountHandle: this.githubState.accountHandle,
+ lastSyncAt: this.githubState.lastSyncAt
+ });
+ }
+
+ connectGitHub(): Observable<{ connected: boolean }> {
+ this.githubState = {
+ ...this.githubState,
+ connected: true,
+ accountName: 'Sentinent Engineering',
+ accountHandle: '@sentinent-dev'
+ };
+
+ return of({ connected: true });
+ }
+
+ updateGitHubRepos(repoIds: number[]): Observable {
+ this.githubState = {
+ ...this.githubState,
+ repos: this.githubState.repos.map(repo => ({
+ ...repo,
+ isConnected: repoIds.includes(repo.id)
+ }))
+ };
+
+ return of(void 0);
+ }
+
+ disconnectGitHub(): Observable {
+ this.githubState = {
+ ...this.githubState,
+ connected: false,
+ accountName: undefined,
+ accountHandle: undefined,
+ lastSyncAt: undefined,
+ repos: this.githubState.repos.map(repo => ({
+ ...repo,
+ isConnected: false
+ }))
+ };
+ this.latestSync = null;
+
+ return of(void 0);
+ }
+
+ syncGitHub(): Observable<{ syncId: string }> {
+ if (!this.githubState.connected) {
+ return throwError(() => new Error('Connect GitHub before starting a sync.'));
+ }
+
+ const syncId = `sync-${Date.now()}`;
+ this.latestSync = {
+ syncId,
+ status: 'completed',
+ itemsSynced: this.githubState.repos.filter(repo => repo.isConnected).length * 6,
+ completedAt: new Date()
+ };
+ this.githubState = {
+ ...this.githubState,
+ lastSyncAt: this.latestSync.completedAt
+ };
+
+ return of({ syncId });
+ }
+
+ getSyncStatus(syncId: string): Observable {
+ if (this.latestSync && this.latestSync.syncId === syncId) {
+ return of(this.latestSync);
+ }
+
+ return of({
+ syncId,
+ status: 'failed'
+ });
+ }
+}
diff --git a/src/app/services/signal.service.ts b/src/app/services/signal.service.ts
new file mode 100644
index 0000000..c4b4e04
--- /dev/null
+++ b/src/app/services/signal.service.ts
@@ -0,0 +1,103 @@
+import { Injectable } from '@angular/core';
+import { Observable, of } from 'rxjs';
+import { Signal, SignalFilters } from '../models/signal.model';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class SignalService {
+ private signals: Signal[] = [
+ {
+ id: 'github-101',
+ sourceType: 'github',
+ sourceId: 'Sentinent-AI/frontend-angular',
+ externalId: '42',
+ title: 'Refine invitation acceptance flow',
+ content: 'The accept invitation screen should redirect users back to the intended workspace after login.',
+ author: '@neethi',
+ status: 'unread',
+ receivedAt: new Date('2026-03-23T09:00:00Z'),
+ url: 'https://github.com/Sentinent-AI/frontend-angular/issues/42',
+ metadata: {
+ type: 'issue',
+ number: 42,
+ repository: 'Sentinent-AI/frontend-angular',
+ state: 'open',
+ labels: ['frontend', 'ux'],
+ assignees: ['@neethi'],
+ createdAt: new Date('2026-03-22T20:00:00Z'),
+ updatedAt: new Date('2026-03-23T09:00:00Z')
+ }
+ },
+ {
+ id: 'github-102',
+ sourceType: 'github',
+ sourceId: 'Sentinent-AI/Sentinent',
+ externalId: '14',
+ title: 'Story: GitHub Integration (US-6)',
+ content: 'Build OAuth connection, repository selection, and GitHub signal filtering in the dashboard.',
+ author: '@yashrastogi',
+ status: 'read',
+ receivedAt: new Date('2026-03-23T11:15:00Z'),
+ url: 'https://github.com/Sentinent-AI/Sentinent/issues/14',
+ metadata: {
+ type: 'issue',
+ number: 14,
+ repository: 'Sentinent-AI/Sentinent',
+ state: 'open',
+ labels: ['user-story'],
+ assignees: ['@me'],
+ createdAt: new Date('2026-03-23T01:20:00Z'),
+ updatedAt: new Date('2026-03-23T11:15:00Z')
+ }
+ },
+ {
+ id: 'github-103',
+ sourceType: 'github',
+ sourceId: 'Sentinent-AI/backend-go',
+ externalId: '31',
+ title: 'Add GitHub sync status endpoint',
+ content: 'Expose last run progress so the frontend can show whether a manual sync completed successfully.',
+ author: '@backend-bot',
+ status: 'unread',
+ receivedAt: new Date('2026-03-23T12:05:00Z'),
+ url: 'https://github.com/Sentinent-AI/backend-go/pull/31',
+ metadata: {
+ type: 'pull_request',
+ number: 31,
+ repository: 'Sentinent-AI/backend-go',
+ state: 'open',
+ labels: ['backend', 'integrations'],
+ assignees: ['@neethi', '@backend-bot'],
+ createdAt: new Date('2026-03-23T10:00:00Z'),
+ updatedAt: new Date('2026-03-23T12:05:00Z')
+ }
+ }
+ ];
+
+ getSignals(filters: SignalFilters): Observable {
+ return of(
+ this.signals.filter(signal => {
+ const sourceMatch = filters.source === 'all' || signal.sourceType === filters.source;
+ const statusMatch = filters.status === 'all' || signal.status === filters.status;
+ return sourceMatch && statusMatch;
+ })
+ );
+ }
+
+ markAsRead(signalId: string): Observable {
+ this.signals = this.signals.map(signal =>
+ signal.id === signalId ? { ...signal, status: 'read' } : signal
+ );
+
+ return of(void 0);
+ }
+
+ archive(signalId: string): Observable {
+ this.signals = this.signals.map(signal =>
+ signal.id === signalId ? { ...signal, status: 'archived' } : signal
+ );
+
+ return of(void 0);
+ }
+}