diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 3f5eb99..f0e64c2 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -2,6 +2,7 @@ import { Routes } from '@angular/router'; import { Login } from './components/login/login'; import { authGuard } from './guards/auth-guard'; import { CreateWorkspace } from './components/workspace/create-workspace'; +import { WorkspaceIntegrationsComponent } from './components/workspace-integrations/workspace-integrations'; export const routes: Routes = [ { path: 'login', component: Login }, @@ -16,6 +17,11 @@ export const routes: Routes = [ component: CreateWorkspace, canActivate: [authGuard] }, + { + path: 'workspaces/:id/settings/integrations', + component: WorkspaceIntegrationsComponent, + canActivate: [authGuard] + }, { path: 'workspaces/:id', loadComponent: () => import('./components/workspace/workspace-details/workspace-details').then(m => m.WorkspaceDetailsComponent), diff --git a/src/app/components/dashboard/dashboard.css b/src/app/components/dashboard/dashboard.css index ec09dd4..39a6f2d 100644 --- a/src/app/components/dashboard/dashboard.css +++ b/src/app/components/dashboard/dashboard.css @@ -92,6 +92,10 @@ padding: 30px 32px 40px; } +.signals-section { + padding-top: 0; +} + .section-header { display: flex; justify-content: space-between; @@ -105,6 +109,11 @@ font-size: 28px; } +.section-copy { + margin: 6px 0 0; + color: #4a5568; +} + .create-btn { text-decoration: none; background: #050505; @@ -206,6 +215,39 @@ color: #555; } +.integration-banner { + max-width: 1200px; + margin: 22px auto 0; + padding: 14px 18px; + border-radius: 16px; + background: rgba(229, 255, 245, 0.9); + color: #166534; + border: 1px solid rgba(34, 197, 94, 0.2); + font-weight: 600; +} + +.filter-row { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.filter-pill { + border: 1px solid rgba(15, 23, 42, 0.1); + background: #fff; + color: #1f2937; + border-radius: 999px; + padding: 9px 14px; + font-weight: 600; + cursor: pointer; +} + +.filter-pill.active { + background: #111827; + color: #fff; + border-color: #111827; +} + @media (max-width: 768px) { .hero, .content { @@ -216,4 +258,9 @@ .hero-copy h1 { font-size: 32px; } + + .integration-banner { + margin-left: 18px; + margin-right: 18px; + } } diff --git a/src/app/components/dashboard/dashboard.html b/src/app/components/dashboard/dashboard.html index 7da68ac..c12a297 100644 --- a/src/app/components/dashboard/dashboard.html +++ b/src/app/components/dashboard/dashboard.html @@ -17,10 +17,12 @@

Decision Workspace Dashboard

-

Track all active workspaces in one place with clean structure and minimal noise.

+

Track active workspaces and GitHub work in one place with clean structure and minimal noise.

+

{{ githubBanner }}

+

Your Workspaces

@@ -47,4 +49,33 @@

No workspaces yet

+ +
+
+
+

Signals

+

Assigned GitHub issues and pull requests appear here as action-ready signals.

+
+
+ + + + +
+
+ + + + +
+

No matching signals

+

Connect GitHub and run a sync to start pulling assigned issues and pull requests into Sentinent.

+
+
+
diff --git a/src/app/components/dashboard/dashboard.spec.ts b/src/app/components/dashboard/dashboard.spec.ts index e0ec9dc..3b048cf 100644 --- a/src/app/components/dashboard/dashboard.spec.ts +++ b/src/app/components/dashboard/dashboard.spec.ts @@ -5,12 +5,14 @@ import { AuthService } from '../../services/auth'; import { of } from 'rxjs'; import { RouterTestingModule } from '@angular/router/testing'; import { Router } from '@angular/router'; +import { SignalService } from '../../services/signal.service'; describe('Dashboard', () => { let component: Dashboard; let fixture: ComponentFixture; let mockWorkspaceService: any; let mockAuthService: any; + let mockSignalService: any; let router: Router; beforeEach(async () => { @@ -24,11 +26,38 @@ describe('Dashboard', () => { logout: jasmine.createSpy('logout') }; + mockSignalService = { + getSignals: jasmine.createSpy('getSignals').and.returnValue(of([ + { + id: 'github-1', + sourceType: 'github', + sourceId: 'Sentinent-AI/frontend-angular', + externalId: '10', + title: 'Test GitHub Signal', + content: 'Signal content', + author: '@tester', + status: 'unread', + receivedAt: new Date(), + metadata: { + type: 'issue', + number: 10, + repository: 'Sentinent-AI/frontend-angular', + state: 'open', + labels: ['frontend'], + assignees: ['@tester'] + } + } + ])), + markAsRead: jasmine.createSpy('markAsRead').and.returnValue(of(void 0)), + archive: jasmine.createSpy('archive').and.returnValue(of(void 0)) + }; + await TestBed.configureTestingModule({ imports: [Dashboard, RouterTestingModule], providers: [ { provide: WorkspaceService, useValue: mockWorkspaceService }, - { provide: AuthService, useValue: mockAuthService } + { provide: AuthService, useValue: mockAuthService }, + { provide: SignalService, useValue: mockSignalService } ] }).compileComponents(); @@ -52,4 +81,9 @@ describe('Dashboard', () => { button.click(); expect(mockAuthService.logout).toHaveBeenCalled(); }); + + it('should render github signals', () => { + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.textContent).toContain('Test GitHub Signal'); + }); }); diff --git a/src/app/components/dashboard/dashboard.ts b/src/app/components/dashboard/dashboard.ts index ccd9b38..0d4d3be 100644 --- a/src/app/components/dashboard/dashboard.ts +++ b/src/app/components/dashboard/dashboard.ts @@ -1,28 +1,47 @@ import { Component, inject, OnInit } from '@angular/core'; -import { Router, RouterLink } from '@angular/router'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { AuthService } from '../../services/auth'; import { WorkspaceService } from '../../services/workspace'; import { Workspace } from '../../models/workspace'; import { CommonModule } from '@angular/common'; +import { Signal, SignalFilters } from '../../models/signal.model'; +import { SignalService } from '../../services/signal.service'; +import { SignalBoardComponent } from '../signal-board/signal-board'; @Component({ selector: 'app-dashboard', standalone: true, - imports: [CommonModule, RouterLink], + imports: [CommonModule, RouterLink, SignalBoardComponent], templateUrl: './dashboard.html', styleUrl: './dashboard.css', }) export class Dashboard implements OnInit { private authService = inject(AuthService); private workspaceService = inject(WorkspaceService); + private signalService = inject(SignalService); private router = inject(Router); + private route = inject(ActivatedRoute); workspaces: Workspace[] = []; + signals: Signal[] = []; + filters: SignalFilters = { source: 'all', status: 'all' }; + githubBanner = ''; ngOnInit() { this.workspaceService.getWorkspaces().subscribe(ws => { this.workspaces = ws; }); + + this.route.queryParamMap.subscribe(params => { + const githubStatus = params.get('github'); + this.githubBanner = githubStatus === 'connected' + ? 'GitHub connected successfully. Review repository access in your workspace integrations.' + : githubStatus === 'error' + ? 'GitHub connection failed. Please try the OAuth flow again.' + : ''; + }); + + this.loadSignals(); } editWorkspace(workspace: Workspace) { @@ -64,4 +83,28 @@ export class Dashboard implements OnInit { this.authService.logout(); this.router.navigate(['/login']); } + + setSourceFilter(source: SignalFilters['source']): void { + this.filters = { ...this.filters, source }; + this.loadSignals(); + } + + setStatusFilter(status: SignalFilters['status']): void { + this.filters = { ...this.filters, status }; + this.loadSignals(); + } + + markSignalAsRead(signalId: string): void { + this.signalService.markAsRead(signalId).subscribe(() => this.loadSignals()); + } + + archiveSignal(signalId: string): void { + this.signalService.archive(signalId).subscribe(() => this.loadSignals()); + } + + private loadSignals(): void { + this.signalService.getSignals(this.filters).subscribe(signals => { + this.signals = signals; + }); + } } diff --git a/src/app/components/signal-board/signal-board.css b/src/app/components/signal-board/signal-board.css new file mode 100644 index 0000000..5d48abd --- /dev/null +++ b/src/app/components/signal-board/signal-board.css @@ -0,0 +1,105 @@ +.signals-board { + display: grid; + gap: 1rem; +} + +.signal-card { + background: #ffffff; + border: 1px solid #dbe4f0; + border-radius: 20px; + padding: 1.25rem; + box-shadow: 0 18px 35px rgba(15, 23, 42, 0.06); +} + +.signal-top, +.signal-footer, +.signal-actions { + display: flex; + justify-content: space-between; + gap: 0.75rem; + align-items: center; +} + +.signal-source { + display: inline-flex; + margin-bottom: 0.45rem; + padding: 0.2rem 0.55rem; + border-radius: 999px; + background: #111827; + color: #ffffff; + font-size: 0.76rem; + font-weight: 700; +} + +.signal-card h3 { + margin: 0; + font-size: 1.05rem; +} + +.signal-status, +.badge { + display: inline-flex; + align-items: center; + border-radius: 999px; + padding: 0.28rem 0.6rem; + font-size: 0.78rem; + font-weight: 700; + background: #e2e8f0; + color: #0f172a; +} + +.signal-status.unread { + background: #dbeafe; + color: #1d4ed8; +} + +.signal-meta, +.signal-body { + color: #475569; +} + +.signal-badges, +.assignees { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; + margin: 0.9rem 0; +} + +.badge.neutral { + background: #f1f5f9; +} + +.signal-actions { + margin-top: 1rem; + justify-content: flex-start; + flex-wrap: wrap; +} + +.link-btn, +.ghost-btn { + border-radius: 999px; + padding: 0.65rem 0.9rem; + font-weight: 600; + text-decoration: none; +} + +.link-btn { + background: #111827; + color: #ffffff; +} + +.ghost-btn { + border: 1px solid #cbd5e1; + background: #ffffff; + color: #0f172a; + cursor: pointer; +} + +@media (max-width: 720px) { + .signal-top, + .signal-footer { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/src/app/components/signal-board/signal-board.html b/src/app/components/signal-board/signal-board.html new file mode 100644 index 0000000..39b4ef9 --- /dev/null +++ b/src/app/components/signal-board/signal-board.html @@ -0,0 +1,36 @@ +
+
+
+
+ GitHub +

{{ signal.title }}

+
+ + {{ signal.status }} + +
+ +

+ {{ getTypeLabel(signal) }} #{{ signal.metadata.number }} in {{ signal.metadata.repository }} +

+

{{ signal.content }}

+ +
+ {{ signal.metadata.state }} + {{ label }} +
+ + + +
+ Open in GitHub + + +
+
+
diff --git a/src/app/components/signal-board/signal-board.ts b/src/app/components/signal-board/signal-board.ts new file mode 100644 index 0000000..93ba377 --- /dev/null +++ b/src/app/components/signal-board/signal-board.ts @@ -0,0 +1,24 @@ +import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Signal } from '../../models/signal.model'; + +@Component({ + selector: 'app-signal-board', + standalone: true, + imports: [CommonModule], + templateUrl: './signal-board.html', + styleUrl: './signal-board.css' +}) +export class SignalBoardComponent { + @Input() signals: Signal[] = []; + @Output() markAsRead = new EventEmitter(); + @Output() archive = new EventEmitter(); + + trackBySignal(_: number, signal: Signal): string { + return signal.id; + } + + getTypeLabel(signal: Signal): string { + return signal.metadata.type === 'pull_request' ? 'Pull Request' : 'Issue'; + } +} diff --git a/src/app/components/workspace-integrations/workspace-integrations.css b/src/app/components/workspace-integrations/workspace-integrations.css new file mode 100644 index 0000000..1cff064 --- /dev/null +++ b/src/app/components/workspace-integrations/workspace-integrations.css @@ -0,0 +1,163 @@ +.integrations-shell { + display: grid; + gap: 1.5rem; + padding: 1rem 0 2rem; +} + +.integrations-header, +.integration-card, +.selection-card, +.sync-card { + background: #fff; + border: 1px solid #d8dee9; + border-radius: 20px; + padding: 1.5rem; + box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06); +} + +.integrations-header { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: flex-start; +} + +.eyebrow { + margin: 0 0 0.35rem; + text-transform: uppercase; + letter-spacing: 0.14em; + font-size: 0.75rem; + color: #2563eb; + font-weight: 700; +} + +.integrations-header h2, +.integration-card h3, +.selection-card h3, +.sync-card h3 { + margin: 0 0 0.5rem; +} + +.intro, +.integration-card p, +.selection-card p, +.sync-card p { + margin: 0; + color: #475569; + line-height: 1.5; +} + +.back-link { + color: #0f172a; + text-decoration: none; + border: 1px solid #cbd5e1; + border-radius: 999px; + padding: 0.75rem 1rem; + font-weight: 600; +} + +.integration-card { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; +} + +.provider-badge { + display: inline-flex; + margin-bottom: 0.75rem; + border-radius: 999px; + background: #e2e8f0; + color: #0f172a; + padding: 0.3rem 0.7rem; + font-size: 0.8rem; + font-weight: 700; +} + +.card-actions, +.section-head { + display: flex; + gap: 0.75rem; + align-items: center; + justify-content: space-between; +} + +.section-head { + margin-bottom: 1rem; +} + +.primary-btn, +.secondary-btn { + border-radius: 999px; + padding: 0.8rem 1.1rem; + font-weight: 700; + cursor: pointer; +} + +.primary-btn { + border: 1px solid #111827; + background: #111827; + color: #fff; +} + +.secondary-btn { + border: 1px solid #cbd5e1; + background: #fff; + color: #0f172a; +} + +.primary-btn:disabled { + opacity: 0.6; + cursor: wait; +} + +.feedback { + margin: 0; + padding: 0.9rem 1rem; + border-radius: 14px; + font-weight: 600; +} + +.feedback.success { + background: #ecfdf5; + color: #166534; +} + +.feedback.error { + background: #fef2f2; + color: #991b1b; +} + +.repo-list { + display: grid; + gap: 0.9rem; +} + +.repo-item { + display: flex; + gap: 0.85rem; + align-items: flex-start; + padding: 1rem; + border-radius: 16px; + border: 1px solid #e2e8f0; + background: #f8fafc; +} + +.repo-item strong, +.repo-item small { + display: block; +} + +.repo-item small { + margin-top: 0.2rem; + color: #64748b; +} + +@media (max-width: 720px) { + .integrations-header, + .integration-card, + .section-head { + flex-direction: column; + align-items: stretch; + } +} diff --git a/src/app/components/workspace-integrations/workspace-integrations.html b/src/app/components/workspace-integrations/workspace-integrations.html new file mode 100644 index 0000000..0f303f3 --- /dev/null +++ b/src/app/components/workspace-integrations/workspace-integrations.html @@ -0,0 +1,74 @@ +
+
+
+

Workspace Integration

+

GitHub Activity Feed

+

+ Connect GitHub to pull assigned issues and pull requests into Sentinent without leaving your workspace flow. +

+
+ Back to workspace +
+ +
+
+ GitHub +

Connection status

+

+ Connected as {{ accountName }} + ({{ accountHandle }}). +

+ +

Not connected yet. Start the OAuth flow to import assigned work from GitHub.

+
+
+ +
+ + + +
+
+ + + + +
+
+
+

Repository selection

+

Choose which repositories should contribute GitHub signals to the dashboard.

+
+ +
+ +
+ +
+
+ +
+

Sync status

+

Last sync: {{ lastSyncAt | date: 'medium' }}

+

No sync has run yet.

+

+ Current status: + {{ syncStatus.status }} + ({{ syncStatus.itemsSynced }} items) +

+
+
diff --git a/src/app/components/workspace-integrations/workspace-integrations.spec.ts b/src/app/components/workspace-integrations/workspace-integrations.spec.ts new file mode 100644 index 0000000..b88f2b2 --- /dev/null +++ b/src/app/components/workspace-integrations/workspace-integrations.spec.ts @@ -0,0 +1,77 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; +import { WorkspaceIntegrationsComponent } from './workspace-integrations'; +import { IntegrationService } from '../../services/integration.service'; + +describe('WorkspaceIntegrationsComponent', () => { + let component: WorkspaceIntegrationsComponent; + let fixture: ComponentFixture; + let mockIntegrationService: jasmine.SpyObj; + + beforeEach(async () => { + mockIntegrationService = jasmine.createSpyObj('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); + } +}