diff --git a/src/app/components/dashboard/dashboard.css b/src/app/components/dashboard/dashboard.css
index 39a6f2d..ca05606 100644
--- a/src/app/components/dashboard/dashboard.css
+++ b/src/app/components/dashboard/dashboard.css
@@ -226,6 +226,12 @@
font-weight: 600;
}
+.integration-banner.slack-banner {
+ background: rgba(245, 232, 255, 0.9);
+ color: #6b21a8;
+ border-color: rgba(124, 58, 237, 0.22);
+}
+
.filter-row {
display: flex;
flex-wrap: wrap;
diff --git a/src/app/components/dashboard/dashboard.html b/src/app/components/dashboard/dashboard.html
index c12a297..d113e98 100644
--- a/src/app/components/dashboard/dashboard.html
+++ b/src/app/components/dashboard/dashboard.html
@@ -17,10 +17,11 @@
Decision Workspace Dashboard
-
Track active workspaces and GitHub work in one place with clean structure and minimal noise.
+
Track active workspaces, Slack conversations, and GitHub work in one place with clean structure and minimal noise.
+ {{ slackBanner }}
{{ githubBanner }}
@@ -54,10 +55,11 @@ No workspaces yet
-
- Sync status
- Last sync: {{ lastSyncAt | date: 'medium' }}
- No sync has run yet.
-
+
+ GitHub sync status
+ Last sync: {{ githubLastSyncAt | date: 'medium' }}
+ No sync has run yet.
+
Current status:
- {{ syncStatus.status }}
- ({{ syncStatus.itemsSynced }} items)
+ {{ githubSyncStatus.status }}
+ ({{ githubSyncStatus.itemsSynced }} items)
diff --git a/src/app/components/workspace-integrations/workspace-integrations.spec.ts b/src/app/components/workspace-integrations/workspace-integrations.spec.ts
index b88f2b2..da8cf55 100644
--- a/src/app/components/workspace-integrations/workspace-integrations.spec.ts
+++ b/src/app/components/workspace-integrations/workspace-integrations.spec.ts
@@ -11,6 +11,11 @@ describe('WorkspaceIntegrationsComponent', () => {
beforeEach(async () => {
mockIntegrationService = jasmine.createSpyObj('IntegrationService', [
+ 'getSlackAuthUrl',
+ 'getSlackChannels',
+ 'connectSlack',
+ 'updateSlackChannels',
+ 'disconnectSlack',
'getGitHubAuthUrl',
'getGitHubRepos',
'connectGitHub',
@@ -20,6 +25,20 @@ describe('WorkspaceIntegrationsComponent', () => {
'getSyncStatus'
]);
+ mockIntegrationService.getSlackChannels.and.returnValue(of({
+ connected: true,
+ workspaceName: 'Sentinent Ops',
+ workspaceUrl: 'sentinent.slack.com',
+ channels: [
+ { id: 'C123', name: 'general', isConnected: true }
+ ],
+ lastSyncAt: new Date('2026-03-24T09:00:00Z')
+ }));
+ mockIntegrationService.updateSlackChannels.and.returnValue(of(void 0));
+ mockIntegrationService.disconnectSlack.and.returnValue(of(void 0));
+ mockIntegrationService.getSlackAuthUrl.and.returnValue(of({ authUrl: 'https://slack.com/mock' }));
+ mockIntegrationService.connectSlack.and.returnValue(of({ connected: true }));
+
mockIntegrationService.getGitHubRepos.and.returnValue(of({
connected: true,
accountName: 'Sentinent Engineering',
@@ -65,6 +84,8 @@ describe('WorkspaceIntegrationsComponent', () => {
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.textContent).toContain('frontend-angular');
expect(compiled.textContent).toContain('Sentinent Engineering');
+ expect(compiled.textContent).toContain('Sentinent Ops');
+ expect(compiled.textContent).toContain('general');
});
it('should trigger sync and show refreshed item count', () => {
@@ -72,6 +93,13 @@ describe('WorkspaceIntegrationsComponent', () => {
fixture.detectChanges();
expect(mockIntegrationService.syncGitHub).toHaveBeenCalled();
- expect(component.feedbackMessage).toContain('6 items refreshed');
+ expect(component.githubFeedbackMessage).toContain('6 items refreshed');
+ });
+
+ it('should save slack channel selection', () => {
+ component.saveSlackChannelSelection();
+
+ expect(mockIntegrationService.updateSlackChannels).toHaveBeenCalled();
+ expect(component.slackFeedbackMessage).toContain('saved');
});
});
diff --git a/src/app/components/workspace-integrations/workspace-integrations.ts b/src/app/components/workspace-integrations/workspace-integrations.ts
index 76d68de..ac08faa 100644
--- a/src/app/components/workspace-integrations/workspace-integrations.ts
+++ b/src/app/components/workspace-integrations/workspace-integrations.ts
@@ -3,6 +3,7 @@ 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';
+import { SlackChannel } from '../../models/slack-integration.model';
@Component({
selector: 'app-workspace-integrations',
@@ -16,29 +17,58 @@ export class WorkspaceIntegrationsComponent implements OnInit {
private readonly integrationService = inject(IntegrationService);
workspaceId = '';
+ slackChannels: SlackChannel[] = [];
+ selectedSlackChannelIds: string[] = [];
+ isSlackConnected = false;
+ slackWorkspaceName = '';
+ slackWorkspaceUrl = '';
+ slackLastSyncAt?: Date;
+ slackFeedbackMessage = '';
+ slackErrorMessage = '';
+ isSlackSaving = false;
+
repos: GitHubRepo[] = [];
selectedRepoIds: number[] = [];
- isConnected = false;
+ isGitHubConnected = false;
accountName = '';
accountHandle = '';
- lastSyncAt?: Date;
- syncStatus?: SyncStatus;
- feedbackMessage = '';
- errorMessage = '';
- isSaving = false;
+ githubLastSyncAt?: Date;
+ githubSyncStatus?: SyncStatus;
+ githubFeedbackMessage = '';
+ githubErrorMessage = '';
+ isGitHubSaving = false;
isSyncing = false;
ngOnInit(): void {
this.workspaceId = this.route.snapshot.paramMap.get('id') ?? '';
+ this.loadSlackChannels();
this.loadRepos();
}
+ connectSlack(): void {
+ this.slackErrorMessage = '';
+ this.slackFeedbackMessage = 'Starting Slack connection...';
+ this.integrationService.getSlackAuthUrl().subscribe(() => {
+ this.integrationService.connectSlack().subscribe(() => {
+ this.slackFeedbackMessage = 'Slack workspace connected. Choose the channels you want to monitor.';
+ this.loadSlackChannels();
+ });
+ });
+ }
+
+ disconnectSlack(): void {
+ this.integrationService.disconnectSlack().subscribe(() => {
+ this.slackFeedbackMessage = 'Slack integration disconnected.';
+ this.loadSlackChannels();
+ });
+ }
+
connectGitHub(): void {
- this.errorMessage = '';
- this.feedbackMessage = 'Starting GitHub connection...';
+ this.githubErrorMessage = '';
+ this.githubFeedbackMessage = '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.githubFeedbackMessage = 'GitHub account connected. Select the repositories you want Sentinent to monitor.';
this.loadRepos();
});
});
@@ -46,12 +76,23 @@ export class WorkspaceIntegrationsComponent implements OnInit {
disconnectGitHub(): void {
this.integrationService.disconnectGitHub().subscribe(() => {
- this.syncStatus = undefined;
- this.feedbackMessage = 'GitHub integration disconnected.';
+ this.githubSyncStatus = undefined;
+ this.githubFeedbackMessage = 'GitHub integration disconnected.';
this.loadRepos();
});
}
+ toggleSlackChannel(channelId: string, checked: boolean): void {
+ this.selectedSlackChannelIds = checked
+ ? [...this.selectedSlackChannelIds, channelId]
+ : this.selectedSlackChannelIds.filter(id => id !== channelId);
+ }
+
+ onSlackChannelToggle(channelId: string, event: Event): void {
+ const input = event.target as HTMLInputElement | null;
+ this.toggleSlackChannel(channelId, input?.checked ?? false);
+ }
+
toggleRepo(repoId: number, checked: boolean): void {
this.selectedRepoIds = checked
? [...this.selectedRepoIds, repoId]
@@ -63,55 +104,86 @@ export class WorkspaceIntegrationsComponent implements OnInit {
this.toggleRepo(repoId, input?.checked ?? false);
}
+ saveSlackChannelSelection(): void {
+ this.isSlackSaving = true;
+ this.slackErrorMessage = '';
+ this.integrationService.updateSlackChannels(this.selectedSlackChannelIds).subscribe({
+ next: () => {
+ this.isSlackSaving = false;
+ this.slackFeedbackMessage = 'Slack channel selection saved.';
+ this.loadSlackChannels();
+ },
+ error: () => {
+ this.isSlackSaving = false;
+ this.slackErrorMessage = 'Could not save Slack channel selection.';
+ }
+ });
+ }
+
saveRepoSelection(): void {
- this.isSaving = true;
- this.errorMessage = '';
+ this.isGitHubSaving = true;
+ this.githubErrorMessage = '';
this.integrationService.updateGitHubRepos(this.selectedRepoIds).subscribe({
next: () => {
- this.isSaving = false;
- this.feedbackMessage = 'Repository selection saved.';
+ this.isGitHubSaving = false;
+ this.githubFeedbackMessage = 'Repository selection saved.';
this.loadRepos();
},
error: () => {
- this.isSaving = false;
- this.errorMessage = 'Could not save repository selection.';
+ this.isGitHubSaving = false;
+ this.githubErrorMessage = 'Could not save repository selection.';
}
});
}
syncNow(): void {
this.isSyncing = true;
- this.errorMessage = '';
+ this.githubErrorMessage = '';
this.integrationService.syncGitHub().subscribe({
next: ({ syncId }) => {
this.integrationService.getSyncStatus(syncId).subscribe(status => {
- this.syncStatus = status;
+ this.githubSyncStatus = status;
this.isSyncing = false;
- this.lastSyncAt = status.completedAt;
- this.feedbackMessage = status.status === 'completed'
+ this.githubLastSyncAt = status.completedAt;
+ this.githubFeedbackMessage = status.status === 'completed'
? `Sync completed. ${status.itemsSynced ?? 0} items refreshed.`
: 'GitHub sync failed.';
});
},
error: (error: Error) => {
this.isSyncing = false;
- this.errorMessage = error.message;
+ this.githubErrorMessage = error.message;
}
});
}
+ isSlackChannelSelected(channelId: string): boolean {
+ return this.selectedSlackChannelIds.includes(channelId);
+ }
+
isRepoSelected(repoId: number): boolean {
return this.selectedRepoIds.includes(repoId);
}
+ private loadSlackChannels(): void {
+ this.integrationService.getSlackChannels().subscribe(response => {
+ this.isSlackConnected = response.connected;
+ this.slackChannels = response.channels;
+ this.selectedSlackChannelIds = response.channels.filter(channel => channel.isConnected).map(channel => channel.id);
+ this.slackWorkspaceName = response.workspaceName ?? '';
+ this.slackWorkspaceUrl = response.workspaceUrl ?? '';
+ this.slackLastSyncAt = response.lastSyncAt;
+ });
+ }
+
private loadRepos(): void {
this.integrationService.getGitHubRepos().subscribe(response => {
- this.isConnected = response.connected;
+ this.isGitHubConnected = 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;
+ this.githubLastSyncAt = response.lastSyncAt;
});
}
}
diff --git a/src/app/models/slack-integration.model.ts b/src/app/models/slack-integration.model.ts
new file mode 100644
index 0000000..91f3d81
--- /dev/null
+++ b/src/app/models/slack-integration.model.ts
@@ -0,0 +1,13 @@
+export interface SlackChannel {
+ id: string;
+ name: string;
+ isConnected: boolean;
+}
+
+export interface SlackConnectionState {
+ connected: boolean;
+ workspaceName?: string;
+ workspaceUrl?: string;
+ channels: SlackChannel[];
+ lastSyncAt?: Date;
+}
diff --git a/src/app/services/integration.service.ts b/src/app/services/integration.service.ts
index 1255b0e..9fea9e2 100644
--- a/src/app/services/integration.service.ts
+++ b/src/app/services/integration.service.ts
@@ -1,11 +1,21 @@
import { Injectable } from '@angular/core';
import { Observable, of, throwError } from 'rxjs';
import { GitHubConnectionState, GitHubRepo, SyncStatus } from '../models/github-integration.model';
+import { SlackChannel, SlackConnectionState } from '../models/slack-integration.model';
@Injectable({
providedIn: 'root'
})
export class IntegrationService {
+ private slackState: SlackConnectionState = {
+ connected: false,
+ channels: [
+ { id: 'C123456', name: 'general', isConnected: true },
+ { id: 'C456789', name: 'engineering', isConnected: true },
+ { id: 'C987654', name: 'product-launch', isConnected: false }
+ ]
+ };
+
private githubState: GitHubConnectionState = {
connected: false,
repos: [
@@ -17,6 +27,62 @@ export class IntegrationService {
private latestSync: SyncStatus | null = null;
+ getSlackAuthUrl(): Observable<{ authUrl: string }> {
+ return of({
+ authUrl: 'https://slack.com/oauth/v2/authorize?client_id=sentinent-demo&scope=channels:read%20chat:write&state=mock-state'
+ });
+ }
+
+ getSlackChannels(): Observable<{ connected: boolean; channels: SlackChannel[]; workspaceName?: string; workspaceUrl?: string; lastSyncAt?: Date }> {
+ return of({
+ connected: this.slackState.connected,
+ channels: this.slackState.channels,
+ workspaceName: this.slackState.workspaceName,
+ workspaceUrl: this.slackState.workspaceUrl,
+ lastSyncAt: this.slackState.lastSyncAt
+ });
+ }
+
+ connectSlack(): Observable<{ connected: boolean }> {
+ this.slackState = {
+ ...this.slackState,
+ connected: true,
+ workspaceName: 'Sentinent Ops',
+ workspaceUrl: 'sentinent.slack.com'
+ };
+
+ return of({ connected: true });
+ }
+
+ updateSlackChannels(channelIds: string[]): Observable {
+ this.slackState = {
+ ...this.slackState,
+ channels: this.slackState.channels.map(channel => ({
+ ...channel,
+ isConnected: channelIds.includes(channel.id)
+ })),
+ lastSyncAt: new Date()
+ };
+
+ return of(void 0);
+ }
+
+ disconnectSlack(): Observable {
+ this.slackState = {
+ ...this.slackState,
+ connected: false,
+ workspaceName: undefined,
+ workspaceUrl: undefined,
+ lastSyncAt: undefined,
+ channels: this.slackState.channels.map(channel => ({
+ ...channel,
+ isConnected: false
+ }))
+ };
+
+ return of(void 0);
+ }
+
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'
diff --git a/src/app/services/signal.service.ts b/src/app/services/signal.service.ts
index c4b4e04..ca31527 100644
--- a/src/app/services/signal.service.ts
+++ b/src/app/services/signal.service.ts
@@ -7,6 +7,42 @@ import { Signal, SignalFilters } from '../models/signal.model';
})
export class SignalService {
private signals: Signal[] = [
+ {
+ id: 'slack-101',
+ sourceType: 'slack',
+ sourceId: 'C123456',
+ externalId: '1735200123.0001',
+ title: 'Message from #general',
+ content: 'Heads up: the deployment window starts at 5 PM Eastern. Please confirm your checklist items.',
+ author: '@sam.ops',
+ status: 'unread',
+ receivedAt: new Date('2026-03-24T09:10:00Z'),
+ url: 'https://sentinent.slack.com/archives/C123456/p17352001230001',
+ metadata: {
+ channel: 'general',
+ channelId: 'C123456',
+ timestamp: '1735200123.0001',
+ user: 'U123456'
+ }
+ },
+ {
+ id: 'slack-102',
+ sourceType: 'slack',
+ sourceId: 'C456789',
+ externalId: '1735201123.0002',
+ title: 'Message from #engineering',
+ content: 'Can someone review the API rate-limit handling before we push the Slack integration branch?',
+ author: '@jordan.dev',
+ status: 'read',
+ receivedAt: new Date('2026-03-24T10:05:00Z'),
+ url: 'https://sentinent.slack.com/archives/C456789/p17352011230002',
+ metadata: {
+ channel: 'engineering',
+ channelId: 'C456789',
+ timestamp: '1735201123.0002',
+ user: 'U456789'
+ }
+ },
{
id: 'github-101',
sourceType: 'github',