From 00a339f3e794ebcae5d8101b00c641b1575663dd Mon Sep 17 00:00:00 2001 From: Sentinent Agent Date: Wed, 29 Apr 2026 03:42:26 +0000 Subject: [PATCH 1/6] feat(profile): move signals feed from dashboard to profile page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signals (Slack, GitHub, JIRA) are user-scoped not workspace-scoped, so the profile page is their natural home. - profile.ts: inject SignalService, add signals state + filter/markRead/archive methods - profile.html: add signals section below profile cards with source/status filter pills, search bar, signal-board, and styled empty state - profile.css: signals section styles — dark-theme filter pills, divider, empty state card - dashboard.ts: remove SignalService, SignalBoard, SearchBar imports and all signal state/methods - dashboard.html: remove signals section; update hero copy to link users to Profile for signals - dashboard.css: add .hero-link style for the inline Profile link in hero copy Ref Sentinent-AI/Sentinent#36 --- src/app/components/dashboard/dashboard.css | 11 ++ src/app/components/dashboard/dashboard.html | 33 +----- src/app/components/dashboard/dashboard.ts | 36 +------ src/app/components/profile/profile.css | 106 ++++++++++++++++++++ src/app/components/profile/profile.html | 35 +++++++ src/app/components/profile/profile.ts | 36 ++++++- 6 files changed, 189 insertions(+), 68 deletions(-) diff --git a/src/app/components/dashboard/dashboard.css b/src/app/components/dashboard/dashboard.css index 57ac088..c4d38d3 100644 --- a/src/app/components/dashboard/dashboard.css +++ b/src/app/components/dashboard/dashboard.css @@ -96,6 +96,17 @@ color: rgba(247, 247, 247, 0.75); } +.hero-link { + color: #ffffff; + font-weight: 700; + text-decoration: underline; + text-underline-offset: 3px; +} + +.hero-link:hover { + opacity: 0.82; +} + .content { max-width: 1280px; margin: 0 auto; diff --git a/src/app/components/dashboard/dashboard.html b/src/app/components/dashboard/dashboard.html index 16775f5..180a849 100644 --- a/src/app/components/dashboard/dashboard.html +++ b/src/app/components/dashboard/dashboard.html @@ -20,7 +20,7 @@

Decision Workspace Dashboard

-

Track active workspaces, Slack conversations, GitHub updates, and JIRA work in one place with clean structure and minimal noise.

+

Manage your workspaces and decisions. Your Slack, GitHub, and JIRA signals live in your Profile.

@@ -82,35 +82,4 @@

Delete "{{ workspace.name }}"?

-
-
-
-

Signals

- -
-
- - - - - - -
-
-

Slack messages, GitHub updates, and JIRA work appear here as action-ready signals.

- - - - -
-

No matching signals

-

Connect Slack, GitHub, or JIRA in workspace integrations to start pulling messages and assigned work into Sentinent.

-
-
-
diff --git a/src/app/components/dashboard/dashboard.ts b/src/app/components/dashboard/dashboard.ts index 3a9c10b..1bf244d 100644 --- a/src/app/components/dashboard/dashboard.ts +++ b/src/app/components/dashboard/dashboard.ts @@ -4,29 +4,22 @@ 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'; -import { SearchBarComponent } from '../search-bar/search-bar'; @Component({ selector: 'app-dashboard', standalone: true, - imports: [CommonModule, RouterLink, SignalBoardComponent, SearchBarComponent], + imports: [CommonModule, RouterLink], templateUrl: './dashboard.html', styleUrl: './dashboard.css', }) export class Dashboard implements OnInit, OnDestroy { private authService = inject(AuthService); private workspaceService = inject(WorkspaceService); - private signalService = inject(SignalService); private router = inject(Router); private route = inject(ActivatedRoute); private cdr = inject(ChangeDetectorRef); workspaces: Workspace[] = []; - signals: Signal[] = []; - filters: SignalFilters = { source: 'all', status: 'all' }; githubBanner = ''; slackBanner = ''; pendingDeleteWorkspace: Workspace | null = null; @@ -58,8 +51,6 @@ export class Dashboard implements OnInit, OnDestroy { ? 'Slack connection failed. Please try the OAuth flow again.' : ''; }); - - this.loadSignals(); } ngOnDestroy(): void { @@ -121,35 +112,10 @@ export class Dashboard implements OnInit, OnDestroy { 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()); - } - isWorkspacePendingDelete(workspace: Workspace): boolean { return this.pendingDeleteWorkspace?.id === workspace.id; } - private loadSignals(): void { - this.signalService.getSignals(this.filters).subscribe(signals => { - this.signals = signals; - this.cdr.detectChanges(); - }); - } - private showDeleteSuccess(message: string): void { this.deleteWorkspaceSuccess = message; if (this.deleteNoticeTimeoutId !== undefined) { diff --git a/src/app/components/profile/profile.css b/src/app/components/profile/profile.css index e794a6c..07de73d 100644 --- a/src/app/components/profile/profile.css +++ b/src/app/components/profile/profile.css @@ -308,6 +308,102 @@ margin-top: 2px; } +/* ── Signals section ──────────────────────────────────────────── */ + +.signals-section { + margin-top: 48px; + padding-top: 40px; + border-top: 1px solid rgba(255, 255, 255, 0.08); +} + +.signals-header { + display: flex; + flex-direction: column; + gap: 20px; + margin-bottom: 28px; +} + +.signals-title-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; +} + +.signals-heading { + margin: 0; + font-size: 32px; + color: #f5f5f5; + letter-spacing: -0.02em; +} + +.signals-search { + flex: 1; + max-width: 400px; +} + +.filter-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.filter-pill { + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.7); + border-radius: 999px; + padding: 7px 14px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s ease; +} + +.filter-pill:hover { + background: rgba(255, 255, 255, 0.1); + color: #ffffff; +} + +.filter-pill.active { + background: #ffffff; + color: #111111; + border-color: #ffffff; +} + +.filter-divider { + width: 1px; + height: 20px; + background: rgba(255, 255, 255, 0.15); + margin: 0 4px; +} + +.signals-empty { + background: rgba(255, 255, 255, 0.04); + border: 1px dashed rgba(255, 255, 255, 0.12); + border-radius: 24px; + padding: 48px 32px; + text-align: center; +} + +.signals-empty-title { + margin: 0 0 10px; + font-size: 20px; + font-weight: 700; + color: #f5f5f5; +} + +.signals-empty-copy { + margin: 0; + color: rgba(255, 255, 255, 0.5); + max-width: 460px; + margin-inline: auto; + line-height: 1.6; +} + +/* ── Responsive ───────────────────────────────────────────────── */ + @media (max-width: 860px) { .profile-hero, .profile-layout, @@ -320,4 +416,14 @@ .hero-actions { justify-content: flex-start; } + + .signals-title-row { + flex-direction: column; + align-items: flex-start; + } + + .signals-search { + max-width: 100%; + width: 100%; + } } diff --git a/src/app/components/profile/profile.html b/src/app/components/profile/profile.html index 88e9e21..330e799 100644 --- a/src/app/components/profile/profile.html +++ b/src/app/components/profile/profile.html @@ -166,4 +166,39 @@

Personal summary

+ +
+
+
+
+

Live feed

+

Your Signals

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

No signals yet

+

Connect Slack, GitHub, or JIRA in your workspace integrations to start pulling messages and assigned work into Sentinent.

+
+
+
diff --git a/src/app/components/profile/profile.ts b/src/app/components/profile/profile.ts index 8fd5049..ab7032a 100644 --- a/src/app/components/profile/profile.ts +++ b/src/app/components/profile/profile.ts @@ -5,11 +5,15 @@ import { RouterLink } from '@angular/router'; import { Subscription } from 'rxjs'; import { UserProfile, UserProfileUpdate } from '../../models/user-profile.model'; import { UserProfileService } from '../../services/user-profile.service'; +import { Signal, SignalFilters } from '../../models/signal.model'; +import { SignalService } from '../../services/signal.service'; +import { SignalBoardComponent } from '../signal-board/signal-board'; +import { SearchBarComponent } from '../search-bar/search-bar'; @Component({ selector: 'app-profile', standalone: true, - imports: [CommonModule, FormsModule, RouterLink], + imports: [CommonModule, FormsModule, RouterLink, SignalBoardComponent, SearchBarComponent], templateUrl: './profile.html', styleUrl: './profile.css', }) @@ -17,6 +21,7 @@ export class ProfileComponent implements OnInit, OnDestroy { private static readonly PROFILE_LOAD_TIMEOUT_MS = 8000; private readonly userProfileService = inject(UserProfileService); + private readonly signalService = inject(SignalService); private readonly cdr = inject(ChangeDetectorRef); private loadSubscription?: Subscription; private loadTimeoutId?: ReturnType; @@ -36,8 +41,12 @@ export class ProfileComponent implements OnInit, OnDestroy { errorMessage = ''; successMessage = ''; + signals: Signal[] = []; + signalFilters: SignalFilters = { source: 'all', status: 'all' }; + ngOnInit(): void { this.loadProfile(); + this.loadSignals(); } ngOnDestroy(): void { @@ -126,6 +135,31 @@ export class ProfileComponent implements OnInit, OnDestroy { return this.displayValue(value) || fallback; } + setSourceFilter(source: SignalFilters['source']): void { + this.signalFilters = { ...this.signalFilters, source }; + this.loadSignals(); + } + + setStatusFilter(status: SignalFilters['status']): void { + this.signalFilters = { ...this.signalFilters, 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.signalFilters).subscribe(signals => { + this.signals = signals; + this.syncView(); + }); + } + private loadProfile(): void { this.clearActiveLoad(); this.isLoading = true; From b51e5fdd3d6aa2ea0430559f9e33b98fe25e53cf Mon Sep 17 00:00:00 2001 From: Sentinent Agent Date: Wed, 29 Apr 2026 03:49:26 +0000 Subject: [PATCH 2/6] feat(profile): move integrations (Slack/GitHub/Jira) to profile page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrations are account-level connections, not workspace content, so the profile page is their natural home. - WorkspaceIntegrationsComponent: add @Input() inputWorkspaceId + ngOnChanges so the component reloads when the workspace selection changes when embedded outside the router (e.g. profile page) - profile.ts: inject WorkspaceService, load workspaces, expose selectedWorkspaceId + selectWorkspace() for workspace switcher - profile.html: add integrations section below profile cards — embeds with workspace selector dropdown (shown only when user has more than one workspace); empty state if no workspaces - profile.css: integrations section styles, workspace selector dropdown - dashboard.ts/html: revert signals-move (signals stay on dashboard); restore original hero copy - workspace-details.html: replace Integrations nav link with a 'Integrations →' link that routes to /profile Ref Sentinent-AI/Sentinent#36 --- src/app/components/dashboard/dashboard.html | 33 +++++- src/app/components/dashboard/dashboard.ts | 53 ++++++--- src/app/components/profile/profile.css | 111 +++++++----------- src/app/components/profile/profile.html | 57 +++++---- src/app/components/profile/profile.ts | 75 ++++-------- .../workspace-integrations.ts | 16 ++- .../workspace-details/workspace-details.html | 2 +- 7 files changed, 179 insertions(+), 168 deletions(-) diff --git a/src/app/components/dashboard/dashboard.html b/src/app/components/dashboard/dashboard.html index 180a849..7045a5e 100644 --- a/src/app/components/dashboard/dashboard.html +++ b/src/app/components/dashboard/dashboard.html @@ -20,7 +20,7 @@

Decision Workspace Dashboard

-

Manage your workspaces and decisions. Your Slack, GitHub, and JIRA signals live in your Profile.

+

Track active workspaces, Slack conversations, GitHub updates, and JIRA work in one place with clean structure and minimal noise.

@@ -82,4 +82,35 @@

Delete "{{ workspace.name }}"?

+
+
+
+

Signals

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

Slack messages, GitHub updates, and JIRA work appear here as action-ready signals.

+ + + + +
+

No matching signals

+

Connect Slack, GitHub, or JIRA in your profile integrations to start pulling messages and assigned work into Sentinent.

+
+
+
diff --git a/src/app/components/dashboard/dashboard.ts b/src/app/components/dashboard/dashboard.ts index 1bf244d..6b75f83 100644 --- a/src/app/components/dashboard/dashboard.ts +++ b/src/app/components/dashboard/dashboard.ts @@ -4,22 +4,29 @@ 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'; +import { SearchBarComponent } from '../search-bar/search-bar'; @Component({ selector: 'app-dashboard', standalone: true, - imports: [CommonModule, RouterLink], + imports: [CommonModule, RouterLink, SignalBoardComponent, SearchBarComponent], templateUrl: './dashboard.html', styleUrl: './dashboard.css', }) export class Dashboard implements OnInit, OnDestroy { private authService = inject(AuthService); private workspaceService = inject(WorkspaceService); + private signalService = inject(SignalService); private router = inject(Router); private route = inject(ActivatedRoute); private cdr = inject(ChangeDetectorRef); workspaces: Workspace[] = []; + signals: Signal[] = []; + filters: SignalFilters = { source: 'all', status: 'all' }; githubBanner = ''; slackBanner = ''; pendingDeleteWorkspace: Workspace | null = null; @@ -51,6 +58,8 @@ export class Dashboard implements OnInit, OnDestroy { ? 'Slack connection failed. Please try the OAuth flow again.' : ''; }); + + this.loadSignals(); } ngOnDestroy(): void { @@ -61,26 +70,20 @@ export class Dashboard implements OnInit, OnDestroy { } requestDeleteWorkspace(workspace: Workspace): void { - if (this.isDeletingWorkspace) { - return; - } + if (this.isDeletingWorkspace) return; this.pendingDeleteWorkspace = workspace; this.deleteWorkspaceError = ''; } cancelDeleteWorkspace(): void { - if (this.isDeletingWorkspace) { - return; - } + if (this.isDeletingWorkspace) return; this.pendingDeleteWorkspace = null; this.deleteWorkspaceError = ''; } confirmDeleteWorkspace(): void { const workspace = this.pendingDeleteWorkspace; - if (!workspace || this.isDeletingWorkspace) { - return; - } + if (!workspace || this.isDeletingWorkspace) return; this.isDeletingWorkspace = true; this.deleteWorkspaceError = ''; @@ -92,7 +95,6 @@ export class Dashboard implements OnInit, OnDestroy { this.deleteWorkspaceError = 'Unable to delete workspace. Please try again.'; return; } - this.workspaces = this.workspaces.filter(ws => ws.id !== workspace.id); this.pendingDeleteWorkspace = null; this.isDeletingWorkspace = false; @@ -112,15 +114,38 @@ export class Dashboard implements OnInit, OnDestroy { 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()); + } + isWorkspacePendingDelete(workspace: Workspace): boolean { return this.pendingDeleteWorkspace?.id === workspace.id; } + private loadSignals(): void { + this.signalService.getSignals(this.filters).subscribe(signals => { + this.signals = signals; + this.cdr.detectChanges(); + }); + } + private showDeleteSuccess(message: string): void { this.deleteWorkspaceSuccess = message; - if (this.deleteNoticeTimeoutId !== undefined) { - clearTimeout(this.deleteNoticeTimeoutId); - } + if (this.deleteNoticeTimeoutId !== undefined) clearTimeout(this.deleteNoticeTimeoutId); this.deleteNoticeTimeoutId = setTimeout(() => { this.deleteWorkspaceSuccess = ''; }, 2600); diff --git a/src/app/components/profile/profile.css b/src/app/components/profile/profile.css index 07de73d..934d330 100644 --- a/src/app/components/profile/profile.css +++ b/src/app/components/profile/profile.css @@ -308,98 +308,71 @@ margin-top: 2px; } -/* ── Signals section ──────────────────────────────────────────── */ +/* ── Integrations section ─────────────────────────────────────── */ -.signals-section { +.integrations-section, +.integrations-empty { margin-top: 48px; padding-top: 40px; border-top: 1px solid rgba(255, 255, 255, 0.08); } -.signals-header { +.integrations-section-header { display: flex; - flex-direction: column; - gap: 20px; - margin-bottom: 28px; -} - -.signals-title-row { - display: flex; - align-items: center; justify-content: space-between; + align-items: flex-start; gap: 24px; + margin-bottom: 32px; } -.signals-heading { - margin: 0; +.integrations-heading { + margin: 4px 0 8px; font-size: 32px; color: #f5f5f5; letter-spacing: -0.02em; } -.signals-search { - flex: 1; - max-width: 400px; +.integrations-intro { + margin: 0; + color: rgba(255, 255, 255, 0.55); + max-width: 540px; + line-height: 1.6; } -.filter-row { +.workspace-selector { display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; -} - -.filter-pill { - border: 1px solid rgba(255, 255, 255, 0.14); - background: rgba(255, 255, 255, 0.05); - color: rgba(255, 255, 255, 0.7); - border-radius: 999px; - padding: 7px 14px; - font-size: 13px; - font-weight: 600; - cursor: pointer; - transition: all 0.15s ease; -} - -.filter-pill:hover { - background: rgba(255, 255, 255, 0.1); - color: #ffffff; -} - -.filter-pill.active { - background: #ffffff; - color: #111111; - border-color: #ffffff; -} - -.filter-divider { - width: 1px; - height: 20px; - background: rgba(255, 255, 255, 0.15); - margin: 0 4px; + flex-direction: column; + gap: 6px; + flex-shrink: 0; } -.signals-empty { - background: rgba(255, 255, 255, 0.04); - border: 1px dashed rgba(255, 255, 255, 0.12); - border-radius: 24px; - padding: 48px 32px; - text-align: center; +.workspace-selector-label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + color: rgba(255, 255, 255, 0.45); } -.signals-empty-title { - margin: 0 0 10px; - font-size: 20px; - font-weight: 700; +.workspace-select { + appearance: none; + border: 1px solid rgba(255, 255, 255, 0.16); + border-radius: 12px; + background: rgba(255, 255, 255, 0.06); color: #f5f5f5; + padding: 10px 36px 10px 14px; + font: inherit; + font-size: 14px; + font-weight: 600; + cursor: pointer; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23ffffff' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; } -.signals-empty-copy { - margin: 0; - color: rgba(255, 255, 255, 0.5); - max-width: 460px; - margin-inline: auto; - line-height: 1.6; +.workspace-select:focus { + outline: none; + border-color: rgba(255, 255, 255, 0.4); } /* ── Responsive ───────────────────────────────────────────────── */ @@ -417,13 +390,11 @@ justify-content: flex-start; } - .signals-title-row { + .integrations-section-header { flex-direction: column; - align-items: flex-start; } - .signals-search { - max-width: 100%; + .workspace-select { width: 100%; } } diff --git a/src/app/components/profile/profile.html b/src/app/components/profile/profile.html index 330e799..f709da8 100644 --- a/src/app/components/profile/profile.html +++ b/src/app/components/profile/profile.html @@ -2,14 +2,14 @@

Loading

Loading your profile...

-

We’re pulling your account details from Sentinent now.

+

We're pulling your account details from Sentinent now.

Profile unavailable

-

We couldn’t load this profile yet.

+

We couldn't load this profile yet.

{{ errorMessage || 'Try refreshing after the backend is running with the new profile API.' }}

@@ -167,38 +167,35 @@

Personal summary

-
-
-
-
-

Live feed

-

Your Signals

-
- + +
+
+
+

Connected accounts

+

Integrations

+

Connect Slack, GitHub, and Jira to pull your assigned work and team messages into the signals feed.

-
- - - - - - - + +
+ +
- - - -
-

No signals yet

-

Connect Slack, GitHub, or JIRA in your workspace integrations to start pulling messages and assigned work into Sentinent.

-
-
+ +
+

Connected accounts

+

Integrations

+

Create a workspace first to set up your Slack, GitHub, and Jira connections.

+ Create a workspace +
diff --git a/src/app/components/profile/profile.ts b/src/app/components/profile/profile.ts index ab7032a..a1e30d9 100644 --- a/src/app/components/profile/profile.ts +++ b/src/app/components/profile/profile.ts @@ -5,15 +5,14 @@ import { RouterLink } from '@angular/router'; import { Subscription } from 'rxjs'; import { UserProfile, UserProfileUpdate } from '../../models/user-profile.model'; import { UserProfileService } from '../../services/user-profile.service'; -import { Signal, SignalFilters } from '../../models/signal.model'; -import { SignalService } from '../../services/signal.service'; -import { SignalBoardComponent } from '../signal-board/signal-board'; -import { SearchBarComponent } from '../search-bar/search-bar'; +import { WorkspaceService } from '../../services/workspace'; +import { Workspace } from '../../models/workspace'; +import { WorkspaceIntegrationsComponent } from '../workspace-integrations/workspace-integrations'; @Component({ selector: 'app-profile', standalone: true, - imports: [CommonModule, FormsModule, RouterLink, SignalBoardComponent, SearchBarComponent], + imports: [CommonModule, FormsModule, RouterLink, WorkspaceIntegrationsComponent], templateUrl: './profile.html', styleUrl: './profile.css', }) @@ -21,7 +20,7 @@ export class ProfileComponent implements OnInit, OnDestroy { private static readonly PROFILE_LOAD_TIMEOUT_MS = 8000; private readonly userProfileService = inject(UserProfileService); - private readonly signalService = inject(SignalService); + private readonly workspaceService = inject(WorkspaceService); private readonly cdr = inject(ChangeDetectorRef); private loadSubscription?: Subscription; private loadTimeoutId?: ReturnType; @@ -41,22 +40,33 @@ export class ProfileComponent implements OnInit, OnDestroy { errorMessage = ''; successMessage = ''; - signals: Signal[] = []; - signalFilters: SignalFilters = { source: 'all', status: 'all' }; + workspaces: Workspace[] = []; + selectedWorkspaceId = ''; ngOnInit(): void { this.loadProfile(); - this.loadSignals(); + this.workspaceService.getWorkspaces().subscribe({ + next: (ws) => { + this.workspaces = ws; + if (ws.length > 0) { + this.selectedWorkspaceId = String(ws[0].id); + } + this.cdr.detectChanges(); + }, + error: () => {} + }); } ngOnDestroy(): void { this.clearActiveLoad(); } + selectWorkspace(id: string): void { + this.selectedWorkspaceId = id; + } + startEditing(): void { - if (!this.profile) { - return; - } + if (!this.profile) return; this.isEditing = true; this.errorMessage = ''; this.successMessage = ''; @@ -67,9 +77,7 @@ export class ProfileComponent implements OnInit, OnDestroy { this.isEditing = false; this.errorMessage = ''; this.successMessage = ''; - if (this.profile) { - this.draft = this.toDraft(this.profile); - } + if (this.profile) this.draft = this.toDraft(this.profile); } saveProfile(): void { @@ -111,14 +119,10 @@ export class ProfileComponent implements OnInit, OnDestroy { displayName(): string { const resolvedName = this.displayValue(this.profile?.fullName); - if (resolvedName) { - return resolvedName; - } + if (resolvedName) return resolvedName; const emailName = this.displayValue(this.profile?.email)?.split('@')[0] ?? ''; - if (!emailName) { - return 'Sentinent User'; - } + if (!emailName) return 'Sentinent User'; return emailName .split(/[^a-zA-Z0-9]+/) @@ -135,31 +139,6 @@ export class ProfileComponent implements OnInit, OnDestroy { return this.displayValue(value) || fallback; } - setSourceFilter(source: SignalFilters['source']): void { - this.signalFilters = { ...this.signalFilters, source }; - this.loadSignals(); - } - - setStatusFilter(status: SignalFilters['status']): void { - this.signalFilters = { ...this.signalFilters, 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.signalFilters).subscribe(signals => { - this.signals = signals; - this.syncView(); - }); - } - private loadProfile(): void { this.clearActiveLoad(); this.isLoading = true; @@ -199,9 +178,7 @@ export class ProfileComponent implements OnInit, OnDestroy { } private clearLoadTimeout(): void { - if (this.loadTimeoutId === undefined) { - return; - } + if (this.loadTimeoutId === undefined) return; clearTimeout(this.loadTimeoutId); this.loadTimeoutId = undefined; } diff --git a/src/app/components/workspace-integrations/workspace-integrations.ts b/src/app/components/workspace-integrations/workspace-integrations.ts index 37ffdad..92fcff4 100644 --- a/src/app/components/workspace-integrations/workspace-integrations.ts +++ b/src/app/components/workspace-integrations/workspace-integrations.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, OnInit, OnDestroy, inject } from '@angular/core'; +import { Component, Input, OnInit, OnDestroy, OnChanges, SimpleChanges, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { IntegrationService } from '../../services/integration.service'; import { GitHubRepo, SyncStatus } from '../../models/github-integration.model'; @@ -13,7 +13,10 @@ import { AtlassianResource, JiraSyncStatus } from '../../models/jira-integration templateUrl: './workspace-integrations.html', styleUrl: './workspace-integrations.css' }) -export class WorkspaceIntegrationsComponent implements OnInit, OnDestroy { +export class WorkspaceIntegrationsComponent implements OnInit, OnChanges, OnDestroy { + /** When provided (e.g. embedded in profile), this takes precedence over the route param. */ + @Input() inputWorkspaceId?: string; + private readonly route = inject(ActivatedRoute); private readonly integrationService = inject(IntegrationService); @@ -51,11 +54,18 @@ export class WorkspaceIntegrationsComponent implements OnInit, OnDestroy { isJiraSyncing = false; ngOnInit(): void { - this.workspaceId = this.getWorkspaceIdFromRoute() ?? ''; + this.workspaceId = this.inputWorkspaceId ?? this.getWorkspaceIdFromRoute() ?? ''; this.loadAllIntegrations(); window.addEventListener('focus', this.onWindowFocus); } + ngOnChanges(changes: SimpleChanges): void { + if (changes['inputWorkspaceId'] && !changes['inputWorkspaceId'].firstChange) { + this.workspaceId = this.inputWorkspaceId ?? ''; + this.loadAllIntegrations(); + } + } + private getWorkspaceIdFromRoute(): string | null { const path = this.route.pathFromRoot || []; for (const route of path) { diff --git a/src/app/components/workspace/workspace-details/workspace-details.html b/src/app/components/workspace/workspace-details/workspace-details.html index 1c7336d..5268189 100644 --- a/src/app/components/workspace/workspace-details/workspace-details.html +++ b/src/app/components/workspace/workspace-details/workspace-details.html @@ -38,7 +38,7 @@

{{ workspace.name }}

From 9ca6630e7709011f2915ff61e49e0c4056736e4e Mon Sep 17 00:00:00 2001 From: Sentinent Agent Date: Wed, 29 Apr 2026 03:57:34 +0000 Subject: [PATCH 3/6] feat(nav): add persistent top nav bar with unread signal badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create AppNavComponent — a fixed dark nav bar shared across all auth'd pages (dashboard, profile, workspace detail). - app-nav: Sentinent logo (links /dashboard), Dashboard + Profile links with routerLinkActive highlight, unread signal badge on Profile (polls every 60s), Logout button - dashboard: remove inline brand/profile-btn/logout-btn; add padding-top: 56px to offset fixed nav - profile: remove 'Back to dashboard' button (nav replaces it); add padding-top offset - workspace-details: add ; update padding-top; shorten 'Back to Dashboard' label to '← Dashboard' Ref Sentinent-AI/Sentinent#36 --- src/app/components/app-nav/app-nav.css | 114 ++++++++++++++++++ src/app/components/app-nav/app-nav.html | 24 ++++ src/app/components/app-nav/app-nav.ts | 49 ++++++++ src/app/components/dashboard/dashboard.css | 1 + src/app/components/dashboard/dashboard.html | 18 +-- src/app/components/dashboard/dashboard.ts | 13 +- src/app/components/profile/profile.css | 4 + src/app/components/profile/profile.html | 3 +- src/app/components/profile/profile.ts | 3 +- .../workspace-details/workspace-details.css | 2 +- .../workspace-details/workspace-details.html | 4 +- .../workspace-details/workspace-details.ts | 3 +- 12 files changed, 207 insertions(+), 31 deletions(-) create mode 100644 src/app/components/app-nav/app-nav.css create mode 100644 src/app/components/app-nav/app-nav.html create mode 100644 src/app/components/app-nav/app-nav.ts diff --git a/src/app/components/app-nav/app-nav.css b/src/app/components/app-nav/app-nav.css new file mode 100644 index 0000000..515e5b3 --- /dev/null +++ b/src/app/components/app-nav/app-nav.css @@ -0,0 +1,114 @@ +:host { + display: block; +} + +.app-nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + height: 56px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 28px; + background: rgba(5, 5, 5, 0.88); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + border-bottom: 1px solid rgba(255, 255, 255, 0.07); +} + +/* ── Brand ───────────────────────────────────────────────── */ + +.nav-brand { + display: inline-flex; + align-items: center; + gap: 10px; + text-decoration: none; + color: #f7f7f7; + font-size: 17px; + font-weight: 800; + letter-spacing: -0.02em; +} + +.nav-brand-icon { + width: 32px; + height: 32px; + border-radius: 8px; + background: #f7f7f7; + color: #050505; + display: grid; + place-items: center; + flex-shrink: 0; +} + +/* ── Links ───────────────────────────────────────────────── */ + +.nav-links { + display: flex; + align-items: center; + gap: 6px; +} + +.nav-link { + position: relative; + display: inline-flex; + align-items: center; + gap: 7px; + text-decoration: none; + color: rgba(247, 247, 247, 0.65); + font-size: 14px; + font-weight: 600; + padding: 6px 12px; + border-radius: 8px; + transition: color 0.15s ease, background 0.15s ease; +} + +.nav-link:hover { + color: #f7f7f7; + background: rgba(255, 255, 255, 0.07); +} + +.nav-link.active { + color: #f7f7f7; + background: rgba(255, 255, 255, 0.1); +} + +/* ── Unread badge ────────────────────────────────────────── */ + +.nav-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 5px; + border-radius: 999px; + background: #f7f7f7; + color: #050505; + font-size: 11px; + font-weight: 800; + line-height: 1; +} + +/* ── Logout ──────────────────────────────────────────────── */ + +.nav-logout { + border: 1px solid rgba(255, 255, 255, 0.18); + background: transparent; + color: rgba(247, 247, 247, 0.65); + border-radius: 8px; + padding: 6px 12px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease; + margin-left: 6px; +} + +.nav-logout:hover { + color: #f7f7f7; + background: rgba(255, 255, 255, 0.07); + border-color: rgba(255, 255, 255, 0.3); +} diff --git a/src/app/components/app-nav/app-nav.html b/src/app/components/app-nav/app-nav.html new file mode 100644 index 0000000..a4d6fee --- /dev/null +++ b/src/app/components/app-nav/app-nav.html @@ -0,0 +1,24 @@ + diff --git a/src/app/components/app-nav/app-nav.ts b/src/app/components/app-nav/app-nav.ts new file mode 100644 index 0000000..28cc63d --- /dev/null +++ b/src/app/components/app-nav/app-nav.ts @@ -0,0 +1,49 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit, OnDestroy, inject } from '@angular/core'; +import { Router, RouterLink, RouterLinkActive } from '@angular/router'; +import { AuthService } from '../../services/auth'; +import { SignalService } from '../../services/signal.service'; + +@Component({ + selector: 'app-nav', + standalone: true, + imports: [CommonModule, RouterLink, RouterLinkActive], + templateUrl: './app-nav.html', + styleUrl: './app-nav.css', +}) +export class AppNavComponent implements OnInit, OnDestroy { + private readonly authService = inject(AuthService); + private readonly signalService = inject(SignalService); + private readonly router = inject(Router); + + unreadCount = 0; + private pollInterval?: ReturnType; + + ngOnInit(): void { + this.refreshUnread(); + // Poll every 60s so the badge stays fresh without hammering the API + this.pollInterval = setInterval(() => this.refreshUnread(), 60_000); + } + + ngOnDestroy(): void { + if (this.pollInterval !== undefined) { + clearInterval(this.pollInterval); + } + } + + logout(): void { + this.authService.logout(); + this.router.navigate(['/login']); + } + + get badgeLabel(): string { + return this.unreadCount > 99 ? '99+' : String(this.unreadCount); + } + + private refreshUnread(): void { + this.signalService.getSignals({ source: 'all', status: 'unread' }).subscribe({ + next: (signals) => { this.unreadCount = signals.length; }, + error: () => {} + }); + } +} diff --git a/src/app/components/dashboard/dashboard.css b/src/app/components/dashboard/dashboard.css index c4d38d3..8f91724 100644 --- a/src/app/components/dashboard/dashboard.css +++ b/src/app/components/dashboard/dashboard.css @@ -21,6 +21,7 @@ position: relative; z-index: 1; min-height: 100vh; + padding-top: 56px; } .hero { diff --git a/src/app/components/dashboard/dashboard.html b/src/app/components/dashboard/dashboard.html index 7045a5e..e35f767 100644 --- a/src/app/components/dashboard/dashboard.html +++ b/src/app/components/dashboard/dashboard.html @@ -1,23 +1,9 @@ + +
-
-
-
- -
- Sentinent -
-
- Profile - -
-
-

Decision Workspace Dashboard

Track active workspaces, Slack conversations, GitHub updates, and JIRA work in one place with clean structure and minimal noise.

diff --git a/src/app/components/dashboard/dashboard.ts b/src/app/components/dashboard/dashboard.ts index 6b75f83..21a7fb9 100644 --- a/src/app/components/dashboard/dashboard.ts +++ b/src/app/components/dashboard/dashboard.ts @@ -1,6 +1,5 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angular/core'; -import { ActivatedRoute, Router, RouterLink } from '@angular/router'; -import { AuthService } from '../../services/auth'; +import { ActivatedRoute, RouterLink } from '@angular/router'; import { WorkspaceService } from '../../services/workspace'; import { Workspace } from '../../models/workspace'; import { CommonModule } from '@angular/common'; @@ -8,19 +7,18 @@ import { Signal, SignalFilters } from '../../models/signal.model'; import { SignalService } from '../../services/signal.service'; import { SignalBoardComponent } from '../signal-board/signal-board'; import { SearchBarComponent } from '../search-bar/search-bar'; +import { AppNavComponent } from '../app-nav/app-nav'; @Component({ selector: 'app-dashboard', standalone: true, - imports: [CommonModule, RouterLink, SignalBoardComponent, SearchBarComponent], + imports: [CommonModule, RouterLink, SignalBoardComponent, SearchBarComponent, AppNavComponent], templateUrl: './dashboard.html', styleUrl: './dashboard.css', }) export class Dashboard implements OnInit, OnDestroy { - private authService = inject(AuthService); private workspaceService = inject(WorkspaceService); private signalService = inject(SignalService); - private router = inject(Router); private route = inject(ActivatedRoute); private cdr = inject(ChangeDetectorRef); @@ -109,11 +107,6 @@ export class Dashboard implements OnInit, OnDestroy { }); } - logout() { - this.authService.logout(); - this.router.navigate(['/login']); - } - setSourceFilter(source: SignalFilters['source']): void { this.filters = { ...this.filters, source }; this.loadSignals(); diff --git a/src/app/components/profile/profile.css b/src/app/components/profile/profile.css index 934d330..2770ecf 100644 --- a/src/app/components/profile/profile.css +++ b/src/app/components/profile/profile.css @@ -17,6 +17,10 @@ padding: 42px 24px 64px; } +:host > .profile-shell:first-of-type { + padding-top: calc(56px + 42px); +} + .profile-hero { display: flex; justify-content: space-between; diff --git a/src/app/components/profile/profile.html b/src/app/components/profile/profile.html index f709da8..1eadcd9 100644 --- a/src/app/components/profile/profile.html +++ b/src/app/components/profile/profile.html @@ -1,3 +1,5 @@ + +

Loading

@@ -32,7 +34,6 @@

{{ displayName() }}

- Back to dashboard
diff --git a/src/app/components/profile/profile.ts b/src/app/components/profile/profile.ts index a1e30d9..1683cf1 100644 --- a/src/app/components/profile/profile.ts +++ b/src/app/components/profile/profile.ts @@ -8,11 +8,12 @@ import { UserProfileService } from '../../services/user-profile.service'; import { WorkspaceService } from '../../services/workspace'; import { Workspace } from '../../models/workspace'; import { WorkspaceIntegrationsComponent } from '../workspace-integrations/workspace-integrations'; +import { AppNavComponent } from '../app-nav/app-nav'; @Component({ selector: 'app-profile', standalone: true, - imports: [CommonModule, FormsModule, RouterLink, WorkspaceIntegrationsComponent], + imports: [CommonModule, FormsModule, RouterLink, WorkspaceIntegrationsComponent, AppNavComponent], templateUrl: './profile.html', styleUrl: './profile.css', }) diff --git a/src/app/components/workspace/workspace-details/workspace-details.css b/src/app/components/workspace/workspace-details/workspace-details.css index d103101..4121517 100644 --- a/src/app/components/workspace/workspace-details/workspace-details.css +++ b/src/app/components/workspace/workspace-details/workspace-details.css @@ -1,6 +1,6 @@ .workspace-page { min-height: 100vh; - padding: 28px 22px 36px; + padding: 84px 22px 36px; background: radial-gradient(circle at 12% 8%, rgba(0, 0, 0, 0.05), transparent 34%), linear-gradient(160deg, #f7f7f7 0%, #ffffff 52%, #f2f2f2 100%); diff --git a/src/app/components/workspace/workspace-details/workspace-details.html b/src/app/components/workspace/workspace-details/workspace-details.html index 5268189..5a8f329 100644 --- a/src/app/components/workspace/workspace-details/workspace-details.html +++ b/src/app/components/workspace/workspace-details/workspace-details.html @@ -1,7 +1,9 @@ + +
- Back to Dashboard + ← Dashboard
Edit Workspace New Decision diff --git a/src/app/components/workspace/workspace-details/workspace-details.ts b/src/app/components/workspace/workspace-details/workspace-details.ts index 0160bc5..c9c5a18 100644 --- a/src/app/components/workspace/workspace-details/workspace-details.ts +++ b/src/app/components/workspace/workspace-details/workspace-details.ts @@ -4,11 +4,12 @@ import { ActivatedRoute, RouterModule, RouterLink, RouterOutlet } from '@angular import { WorkspaceService } from '../../../services/workspace'; import { Workspace } from '../../../models/workspace'; import { Observable } from 'rxjs'; +import { AppNavComponent } from '../../app-nav/app-nav'; @Component({ selector: 'app-workspace-details', standalone: true, - imports: [CommonModule, RouterModule, RouterLink, RouterOutlet], + imports: [CommonModule, RouterModule, RouterLink, RouterOutlet, AppNavComponent], templateUrl: './workspace-details.html', styleUrls: ['./workspace-details.css'] }) From a3d2bce5afdfacbbf47e26b5b924e2d66a6eb1f2 Mon Sep 17 00:00:00 2001 From: Sentinent Agent Date: Wed, 29 Apr 2026 03:59:01 +0000 Subject: [PATCH 4/6] feat(dashboard): show unread signal count badge on workspace cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After signals load, compute a workspaceId→unreadCount map from the already-fetched signal list (no extra API call). Each workspace card now shows a dark pill badge (e.g. '3 unread') when there are unread signals for that workspace, giving users at-a-glance triage from the dashboard. Ref Sentinent-AI/Sentinent#36 --- src/app/components/dashboard/dashboard.css | 25 +++++++++++++++++++++ src/app/components/dashboard/dashboard.html | 7 +++++- src/app/components/dashboard/dashboard.ts | 11 +++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/app/components/dashboard/dashboard.css b/src/app/components/dashboard/dashboard.css index 8f91724..fe07a91 100644 --- a/src/app/components/dashboard/dashboard.css +++ b/src/app/components/dashboard/dashboard.css @@ -208,6 +208,31 @@ border-color: rgba(0, 0, 0, 0.15); } +.card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 8px; +} + +.card-header h3 { + margin: 0; + font-size: 20px; + font-weight: 700; + letter-spacing: -0.01em; +} + +.unread-badge { + flex-shrink: 0; + background: #050505; + color: #f7f7f7; + border-radius: 999px; + padding: 3px 10px; + font-size: 12px; + font-weight: 700; +} + .workspace-card h3 { margin: 0 0 8px; font-size: 20px; diff --git a/src/app/components/dashboard/dashboard.html b/src/app/components/dashboard/dashboard.html index e35f767..5640147 100644 --- a/src/app/components/dashboard/dashboard.html +++ b/src/app/components/dashboard/dashboard.html @@ -23,7 +23,12 @@

Your Workspaces

-

{{ ws.name }}

+
+

{{ ws.name }}

+ + {{ unreadCountFor(ws.id) }} unread + +

{{ ws.description || 'No description yet.' }}

Created: {{ ws.createdDate | date: 'mediumDate' }}
diff --git a/src/app/components/dashboard/dashboard.ts b/src/app/components/dashboard/dashboard.ts index 21a7fb9..9781005 100644 --- a/src/app/components/dashboard/dashboard.ts +++ b/src/app/components/dashboard/dashboard.ts @@ -24,6 +24,7 @@ export class Dashboard implements OnInit, OnDestroy { workspaces: Workspace[] = []; signals: Signal[] = []; + unreadByWorkspace: Record = {}; filters: SignalFilters = { source: 'all', status: 'all' }; githubBanner = ''; slackBanner = ''; @@ -129,9 +130,19 @@ export class Dashboard implements OnInit, OnDestroy { return this.pendingDeleteWorkspace?.id === workspace.id; } + unreadCountFor(workspaceId: number): number { + return this.unreadByWorkspace[workspaceId] ?? 0; + } + private loadSignals(): void { this.signalService.getSignals(this.filters).subscribe(signals => { this.signals = signals; + this.unreadByWorkspace = signals.reduce>((acc, s) => { + if (s.status === 'unread' && s.workspaceId != null) { + acc[s.workspaceId] = (acc[s.workspaceId] ?? 0) + 1; + } + return acc; + }, {}); this.cdr.detectChanges(); }); } From 2dee1c61d78b1760b0db2d40714d2f3a34af42b9 Mon Sep 17 00:00:00 2001 From: Sentinent Agent Date: Wed, 29 Apr 2026 04:07:05 +0000 Subject: [PATCH 5/6] feat(signals): replace filter pills with source tabs + unread toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signal filtering is now client-side on an already-fetched full list, so switching tabs is instant with no extra network calls. - Source tabs (All / Slack / GitHub / JIRA) each show a live count badge; active tab gets an underline indicator - 'Unread only' toggle replaces the old status filter pill — one click to focus, one click to clear - allSignals holds the full status-filtered list; signals is the tab-filtered view; applyFilters() recomputes on tab switch without re-fetching Ref Sentinent-AI/Sentinent#36 --- src/app/components/dashboard/dashboard.css | 103 ++++++++++++++++++-- src/app/components/dashboard/dashboard.html | 40 +++++--- src/app/components/dashboard/dashboard.ts | 34 +++++-- 3 files changed, 149 insertions(+), 28 deletions(-) diff --git a/src/app/components/dashboard/dashboard.css b/src/app/components/dashboard/dashboard.css index fe07a91..96ae964 100644 --- a/src/app/components/dashboard/dashboard.css +++ b/src/app/components/dashboard/dashboard.css @@ -132,26 +132,111 @@ } .signals-header { + display: flex; flex-direction: column; - align-items: flex-start; - gap: 20px; + gap: 0; + margin-bottom: 0; } -.title-group { +.signals-title-row { display: flex; align-items: center; - gap: 24px; - width: 100%; + justify-content: space-between; + gap: 16px; + margin-bottom: 20px; +} + +.signals-title-row h2 { + margin: 0; + font-size: 28px; +} + +.signals-header-right { + display: flex; + align-items: center; + gap: 12px; } .signals-search { flex: 1; - max-width: 400px; + max-width: 380px; +} + +.unread-toggle { + border: 1px solid rgba(0, 0, 0, 0.18); + background: transparent; + color: #555; + border-radius: 8px; + padding: 7px 13px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + transition: all 0.15s ease; +} + +.unread-toggle:hover { + background: #f0f0f0; + color: #111; +} + +.unread-toggle.active { + background: #111; + color: #fff; + border-color: #111; +} + +/* ── Source tabs ─────────────────────────────────────────── */ + +.signal-tabs { + display: flex; + gap: 2px; + border-bottom: 2px solid rgba(0, 0, 0, 0.08); + margin-bottom: 28px; } -.section-copy { - margin: -10px 0 18px; - color: #4a5568; +.signal-tab { + display: inline-flex; + align-items: center; + gap: 7px; + background: transparent; + border: none; + border-bottom: 2px solid transparent; + margin-bottom: -2px; + padding: 10px 16px; + font-size: 14px; + font-weight: 600; + color: #666; + cursor: pointer; + transition: color 0.15s ease, border-color 0.15s ease; +} + +.signal-tab:hover { + color: #111; +} + +.signal-tab.active { + color: #111; + border-bottom-color: #111; +} + +.tab-count { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; + padding: 0 6px; + border-radius: 999px; + background: rgba(0, 0, 0, 0.07); + color: #444; + font-size: 11px; + font-weight: 700; +} + +.signal-tab.active .tab-count { + background: #111; + color: #fff; } .create-btn { diff --git a/src/app/components/dashboard/dashboard.html b/src/app/components/dashboard/dashboard.html index 5640147..47d5a60 100644 --- a/src/app/components/dashboard/dashboard.html +++ b/src/app/components/dashboard/dashboard.html @@ -74,21 +74,39 @@

Delete "{{ workspace.name }}"?

-
-
+
+

Signals

- +
+ + +
-
- - - - - - + +
+ + + +
-

Slack messages, GitHub updates, and JIRA work appear here as action-ready signals.

= {}; + sourceTab: 'all' | 'slack' | 'github' | 'jira' = 'all'; + showUnreadOnly = false; filters: SignalFilters = { source: 'all', status: 'all' }; githubBanner = ''; slackBanner = ''; @@ -108,16 +111,21 @@ export class Dashboard implements OnInit, OnDestroy { }); } - setSourceFilter(source: SignalFilters['source']): void { - this.filters = { ...this.filters, source }; - this.loadSignals(); + setSourceTab(tab: typeof this.sourceTab): void { + this.sourceTab = tab; + this.applyFilters(); } - setStatusFilter(status: SignalFilters['status']): void { - this.filters = { ...this.filters, status }; + toggleUnreadOnly(): void { + this.showUnreadOnly = !this.showUnreadOnly; + this.filters = { ...this.filters, status: this.showUnreadOnly ? 'unread' : 'all' }; this.loadSignals(); } + countFor(source: 'slack' | 'github' | 'jira'): number { + return this.allSignals.filter(s => s.sourceType === source).length; + } + markSignalAsRead(signalId: string): void { this.signalService.markAsRead(signalId).subscribe(() => this.loadSignals()); } @@ -135,18 +143,28 @@ export class Dashboard implements OnInit, OnDestroy { } private loadSignals(): void { - this.signalService.getSignals(this.filters).subscribe(signals => { - this.signals = signals; + // Always fetch with status filter only; source filtering is done client-side + const fetchFilter: SignalFilters = { source: 'all', status: this.filters.status }; + this.signalService.getSignals(fetchFilter).subscribe(signals => { + this.allSignals = signals; this.unreadByWorkspace = signals.reduce>((acc, s) => { if (s.status === 'unread' && s.workspaceId != null) { acc[s.workspaceId] = (acc[s.workspaceId] ?? 0) + 1; } return acc; }, {}); + this.applyFilters(); this.cdr.detectChanges(); }); } + private applyFilters(): void { + this.signals = this.sourceTab === 'all' + ? this.allSignals + : this.allSignals.filter(s => s.sourceType === this.sourceTab); + this.cdr.detectChanges(); + } + private showDeleteSuccess(message: string): void { this.deleteWorkspaceSuccess = message; if (this.deleteNoticeTimeoutId !== undefined) clearTimeout(this.deleteNoticeTimeoutId); From dfb1d79013017231a5282156e5bc44befd022b99 Mon Sep 17 00:00:00 2001 From: Sentinent Agent Date: Wed, 29 Apr 2026 04:11:14 +0000 Subject: [PATCH 6/6] feat(ux): contextual empty states for signals and decisions - Signals: two distinct empty states depending on context - 'No signals yet' with icon + 'Connect integrations' CTA when allSignals is empty (no tools connected) - 'All caught up' with check icon when integrations are connected but the active tab/filter returns nothing; message adapts to sourceTab and showUnreadOnly flag - Decision list: replace bare text with structured empty state (icon, heading, sub-copy, 'Create your first decision' CTA button) - CSS: empty-icon, empty-cta, caught-up/no-integrations variants --- src/app/components/dashboard/dashboard.css | 41 ++++++++++++++- src/app/components/dashboard/dashboard.html | 25 +++++++-- .../decision-list/decision-list.component.css | 51 +++++++++++++++++-- .../decision-list.component.html | 12 ++++- 4 files changed, 120 insertions(+), 9 deletions(-) diff --git a/src/app/components/dashboard/dashboard.css b/src/app/components/dashboard/dashboard.css index 96ae964..d89cd23 100644 --- a/src/app/components/dashboard/dashboard.css +++ b/src/app/components/dashboard/dashboard.css @@ -472,7 +472,7 @@ border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 16px; background: #fff; - padding: 48px 32px; + padding: 56px 32px; text-align: center; } @@ -486,6 +486,45 @@ color: #555; } +.empty-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 56px; + height: 56px; + border-radius: 16px; + margin: 0 auto 20px; + color: #888; +} + +.empty-no-integrations .empty-icon { + background: #f3f4f6; + border: 1px solid rgba(0, 0, 0, 0.07); +} + +.empty-caught-up .empty-icon { + background: #edfaf3; + border: 1px solid rgba(34, 197, 94, 0.18); + color: #22863a; +} + +.empty-cta { + display: inline-block; + margin-top: 18px; + padding: 10px 18px; + background: #111; + color: #fff; + border-radius: 10px; + text-decoration: none; + font-size: 13px; + font-weight: 600; + transition: opacity 0.15s ease; +} + +.empty-cta:hover { + opacity: 0.85; +} + .integration-banner { max-width: 1200px; margin: 22px auto 0; diff --git a/src/app/components/dashboard/dashboard.html b/src/app/components/dashboard/dashboard.html index 47d5a60..457571e 100644 --- a/src/app/components/dashboard/dashboard.html +++ b/src/app/components/dashboard/dashboard.html @@ -116,9 +116,28 @@

Signals

/> -
-

No matching signals

-

Connect Slack, GitHub, or JIRA in your profile integrations to start pulling messages and assigned work into Sentinent.

+ +
+ +

No signals yet

+

Connect Slack, GitHub, or JIRA to start pulling messages and assigned work into Sentinent.

+ Connect integrations → +
+ + +
+ +

All caught up{{ sourceTab !== 'all' ? ' on ' + sourceTab : '' }}

+

No unread {{ sourceTab === 'all' ? '' : sourceTab + ' ' }}signals — you're up to date.

+

No {{ sourceTab === 'all' ? '' : sourceTab + ' ' }}signals match the current filter.

diff --git a/src/app/components/decision-list/decision-list.component.css b/src/app/components/decision-list/decision-list.component.css index b0fd5e5..a0ad961 100644 --- a/src/app/components/decision-list/decision-list.component.css +++ b/src/app/components/decision-list/decision-list.component.css @@ -151,12 +151,55 @@ } .empty-state { - padding: 48px; + padding: 56px 32px; text-align: center; - background: #f9f9f9; - border-radius: 12px; - border: 2px dashed #e5e5e5; + background: #fff; + border-radius: 14px; + border: 1px solid rgba(0, 0, 0, 0.08); + color: #444; +} + +.empty-state h3 { + margin: 0 0 8px; + font-size: 20px; + color: #111; +} + +.empty-state p { + margin: 0; + font-size: 14px; color: #666; + line-height: 1.6; +} + +.empty-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 52px; + height: 52px; + border-radius: 14px; + background: #f3f4f6; + border: 1px solid rgba(0, 0, 0, 0.07); + color: #555; + margin: 0 auto 18px; +} + +.empty-cta { + display: inline-block; + margin-top: 20px; + padding: 10px 18px; + background: #0a0a0a; + color: #fff; + border-radius: 10px; + text-decoration: none; + font-size: 13px; + font-weight: 600; + transition: opacity 0.15s ease; +} + +.empty-cta:hover { + opacity: 0.82; } .loading-state { diff --git a/src/app/components/decision-list/decision-list.component.html b/src/app/components/decision-list/decision-list.component.html index 628d28e..7cc72b9 100644 --- a/src/app/components/decision-list/decision-list.component.html +++ b/src/app/components/decision-list/decision-list.component.html @@ -17,7 +17,17 @@

Decisions

- No decisions found. Create one to get started! + +

No decisions yet

+

Capture key decisions here — track context, options, and outcomes in one place.

+ Create your first decision