Skip to content

feat(newsletters): add recipient-facing my newsletters archive - #1154

Open
niravpatel27 wants to merge 10 commits into
mainfrom
feat/LFXV2-2803
Open

feat(newsletters): add recipient-facing my newsletters archive#1154
niravpatel27 wants to merge 10 commits into
mainfrom
feat/LFXV2-2803

Conversation

@niravpatel27

@niravpatel27 niravpatel27 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a Me-lens /my-newsletters archive for recipients: logged-in users see every sent newsletter that targeted a committee (group) they belong to. Newsletters are filterable by foundation, showing subject and sent date. Clicking a row lazily loads the full newsletter content and opens the existing NewsletterPreviewDrawerComponent.

Access model: User sees a sent newsletter iff they are a member of at least one committee in that newsletter's committee_uids. Membership is verified server-side by the archive endpoints (no per-recipient send records, no schema changes).

JIRA

LFXV2-2803

Companion Work

Upstream archive endpoints implemented in parallel:

  • Backend PR: lfx-v2-newsletter-service#57
  • Provides GET /newsletters/archive?committee_uids=<csv>&page_token=... (list) and GET /newsletters/archive/{uid} (detail)
  • Membership verification via committee-api (email + optional username match)
  • Returns sent-only newsletters, keyset-paginated by sent_at DESC, id DESC

Implementation Details

Shared Package

  • MyNewsletterListItem and MyNewsletterArchiveResponse interfaces (newsletters, next_page_token)

BFF (Express)

  • POST /api/newsletters/my-newsletters controller + service
  • Calls upstream archive endpoints with user bearer token
  • Enriches project UIDs → project/foundation names and slugs via batch NATS lookup (25-item batches)
  • Handles project lookup failures gracefully (warning log, empty foundation fields)
  • Empty committee set → returns { newsletters: [] } without upstream call

Frontend (Angular 20)

  • /my-newsletters route (Me-lens, authGuard only — no newsletterAccessGuard)
  • Signals-based list: loading skeleton, empty state, foundation filter (pills when >1 foundation)
  • "Load more" pagination via next_page_token
  • Row click → lazy detail fetch → NewsletterPreviewDrawerComponent (shared with sender preview)
  • Silent error handling (Me-lens pattern: no toasts, graceful fallback to empty state)

Sidebar

  • "My Newsletters" added to meLensItems "My Engagement" section

Security

  • authGuard-only route: Recipients must be logged in
  • Upstream is authoritative: BFF verifies membership fast-path via set intersection; upstream archive detail endpoint performs the definitive membership check (returns 403 if not a member, 404 if not found/not sent)
  • No per-recipient send records: Design trades storage for server-side verification per request

Deferred: Component Spec Coverage

Status: Component spec for my-newsletters-list.component.ts is deferred.

Reason: Test infrastructure mismatch. Component tests in this repo assume Jasmine (vi.fn mocks, expect().toHaveBeenCalledWith, SpyObj types); the server test runner is vitest with hoisted mocks. Adding component specs requires:

  1. Installing @types/jasmine (or switching component harness to vitest pattern)
  2. Resolving protected member access in tests (casts don't suppress Angular build errors)

Covered: Server service has full vitest coverage (12 tests: empty committees, enrichment, project failures, error propagation, batching, parent_uid foundation lookup).

Follow-up: Align component test harness (either jasmine types or vitest pattern) and add component specs.

Protected Files

apps/lfx-one/src/server/server.ts — 4 lines changed (route mount for /api/newsletters/my-newsletters before project-scoped routes). Flags for code-owner review per CODEOWNERS.

Test Plan

  • yarn format:check — All files use Prettier code style
  • yarn lint:check — 0 new linting errors (pre-existing campaigns.component warning unrelated)
  • yarn check-types — TypeScript strict mode passes
  • yarn build — Full monorepo build successful
  • yarn test — 33 tests pass (12 new service spec + 21 existing)

Manual flow:

  1. Log in as a user belonging to one or more committees
  2. Navigate to /my-newsletters (Me lens)
  3. Verify: List renders sent newsletters from user's committees with foundation names
  4. Filter by foundation → list updates
  5. Click a row → skeleton loads, drawer opens with full newsletter body matching sender preview
  6. Log out and re-visit → 401 authGuard redirect
  7. Verify a non-member cannot fetch another user's newsletter detail (upstream returns 403)
  8. Verify user with zero committees sees empty state "Newsletters sent to committees you belong to will appear here."

Implement the My Newsletters feature under the Me lens, allowing users to view
sent newsletters targeted at committees they belong to. Includes BFF service
layer with project enrichment, Angular component with foundation filtering,
and sidebar navigation integration.

- Shared: MyNewsletterListItem and MyNewsletterArchiveResponse interfaces
- Backend: my-newsletters service, controller, and route with project enrichment
- Frontend: my-newsletters-list component with foundation filtering and detail drawer
- Newsletter service: new listMyNewsletters() and getMyNewsletterDetail() methods
- Sidebar: "My Newsletters" entry in My Engagement section

Signed-off-by: Nirav Patel <npatel@linuxfoundation.org>
- Removed dead-code unused `newsletters` signal (was redundant with allNewsletters)
- Converted clickable div rows to keyboard-accessible button elements with Enter/Space handlers and aria-label
- Fixed logger operation name dedup violation in getArchiveDetail service method (moved from startOperation to debug/warning logs per pattern)
- Typed projects array properly as (Project | null)[] instead of (any | null)[]
- Applied prettier formatting
- Verified: silent error handling matches Me-lens component pattern
- Verified: signal<boolean> with [(visible)] binding matches newsletter-list.component pattern

Signed-off-by: Nirav Patel <npatel@linuxfoundation.org>
Add comprehensive vitest suite covering:
- Empty committee set short-circuit without upstream call
- Happy path newsletter fetching and enrichment
- Project/foundation resolution (parent_uid and self-foundation cases)
- Project lookup failures with graceful degradation
- Batch fetching in groups of 25
- Error propagation (403/404 from archive endpoints)

Also enhance enrichNewslettersWithProjectData to fetch parent_uid
foundations in addition to project UIDs, ensuring complete enrichment.

Signed-off-by: Nirav Patel <npatel@linuxfoundation.org>
Replace raw query parameter cast `(req.query['page_token'] as string)`
with getStringQueryParam(req, 'page_token') helper to safely handle
query string casting and prevent type confusion from repeated keys.

Fixes KB pattern violation: raw-query-string-cast

Signed-off-by: Nirav Patel <npatel@linuxfoundation.org>
Run prettier to fix formatting issues in spec file.

Signed-off-by: Nirav Patel <npatel@linuxfoundation.org>
Copilot AI review requested due to automatic review settings July 22, 2026 21:23
@niravpatel27
niravpatel27 requested a review from a team as a code owner July 22, 2026 21:23
@cursor

cursor Bot commented Jul 22, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
New user-scoped read path for newsletter HTML with committee-based access enforced primarily upstream; moderate surface area (BFF enrichment, pagination, route ordering in server.ts) but no sender/send flows changed.

Overview
Introduces a recipient-facing newsletter archive at /my-newsletters (Me lens, authGuard only—no sender newsletterAccessGuard), with a My Newsletters entry under My Engagement in the sidebar.

The BFF exposes GET /api/newsletters/my-newsletters (keyset pagination via page_token) and GET /api/newsletters/my-newsletters/:newsletterUid (full body_html). It resolves the caller’s committee UIDs via CommitteeService, skips upstream when there are none, caps committees at 500, proxies to upstream /newsletters/archive, and enriches list rows with project/foundation names and slugs (batched project lookups). Detail access relies on upstream membership checks (403/404).

The Angular list uses signals for loading, empty state, foundation filter pills, load-more, and lazy detail fetch into the existing NewsletterPreviewDrawerComponent, with Me-lens-style silent error handling. Shared types MyNewsletterListItem and MyNewsletterArchiveResponse were added; server coverage is in my-newsletters.service vitest specs.

Reviewed by Cursor Bugbot for commit 410acaa. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9f297366-9c36-4d1c-88fd-482b8c4b691e

📥 Commits

Reviewing files that changed from the base of the PR and between 99918e4 and 410acaa.

📒 Files selected for processing (2)
  • apps/lfx-one/src/app/modules/newsletters/my-newsletters/my-newsletters-list.component.ts
  • apps/lfx-one/src/server/services/my-newsletters.service.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/lfx-one/src/server/services/my-newsletters.service.ts
  • apps/lfx-one/src/app/modules/newsletters/my-newsletters/my-newsletters-list.component.ts

Walkthrough

Adds an authenticated “My Newsletters” archive with paginated retrieval, project and foundation enrichment, filtering, preview details, navigation, server endpoints, and service tests.

Changes

My Newsletters Archive

Layer / File(s) Summary
Archive contracts and API clients
packages/shared/src/interfaces/newsletter.interface.ts, apps/lfx-one/src/server/services/newsletter-service.client.ts, apps/lfx-one/src/app/shared/services/newsletter.service.ts
Adds archive response types and client methods for paginated listing and newsletter detail retrieval.
Archive service and enrichment
apps/lfx-one/src/server/services/my-newsletters.service.ts, apps/lfx-one/src/server/services/my-newsletters.service.spec.ts
Resolves committee access, fetches archive entries, enriches project and foundation metadata in batches, retrieves details, and tests success and error paths.
HTTP controller and route wiring
apps/lfx-one/src/server/controllers/my-newsletters.controller.ts, apps/lfx-one/src/server/routes/my-newsletters.route.ts, apps/lfx-one/src/server/server.ts
Adds validated collection and detail handlers and mounts the recipient archive routes.
Frontend archive experience
apps/lfx-one/src/app/modules/newsletters/my-newsletters/*, apps/lfx-one/src/app/app.routes.ts, apps/lfx-one/src/app/shared/services/sidebar-nav.service.ts
Adds the authenticated route, navigation entry, signal-based loading and pagination, foundation filtering, newsletter cards, and preview drawer.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant MyNewslettersListComponent
  participant NewsletterService
  participant MyNewslettersController
  participant MyNewslettersService
  User->>MyNewslettersListComponent: Open My Newsletters
  MyNewslettersListComponent->>NewsletterService: listMyNewsletters(pageToken)
  NewsletterService->>MyNewslettersController: GET archive endpoint
  MyNewslettersController->>MyNewslettersService: listArchive(request, pageToken)
  MyNewslettersService-->>MyNewslettersController: Enriched archive response
  MyNewslettersController-->>NewsletterService: JSON response
  NewsletterService-->>MyNewslettersListComponent: Newsletter list
  MyNewslettersListComponent-->>User: Render filtered newsletter cards
  User->>MyNewslettersListComponent: Select newsletter
  MyNewslettersListComponent->>NewsletterService: getMyNewsletterDetail(newsletterUid)
  NewsletterService-->>MyNewslettersListComponent: Newsletter body
  MyNewslettersListComponent-->>User: Show preview drawer
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a recipient-facing My Newsletters archive.
Description check ✅ Passed The description is directly related to the implemented archive, UI, backend endpoints, and supporting changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/LFXV2-2803

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Comment @coderabbitai help to get the list of available commands.

Comment thread apps/lfx-one/src/app/modules/newsletters/newsletters.routes.ts
Comment thread apps/lfx-one/src/server/controllers/my-newsletters.controller.ts Outdated
1. HIGH: Move /my-newsletters to top-level Me-lens route with authGuard only,
   removing it from foundation/newsletters children. This bypasses the parent
   newsletterAccessGuard + projectQueryParamGuard that was blocking access.
   Update sidebar link from /newsletters/my-newsletters to /my-newsletters.

2. loadMore: Check guards before bumping generation counter, and ensure
   loadingMore flag is cleared even when stale responses are discarded.
   Prevents loading state from getting stuck on concurrent requests.

3. Controller param validation: Move UUID validation inside try/catch block
   so Express routes validation errors to next() rather than hanging the request.
   Validation throw happens before the handler's request/response boundary.

4. Foundation filter: Make filter pills reversible by toggling on reclick.
   Clicking an already-selected pill now clears selection (show "all"),
   matching FilterPillsComponent idiom.

5. Native button: Remove redundant (keydown.enter)/(keydown.space) handlers.
   Native <button> type="button" handles Enter and Space automatically.
   Handlers cause double-firing of preview requests.

6. Date pipe: Pin locale to 'en-US' to prevent SSR hydration mismatch.
   Change from `| date: 'short'` to `| date: 'MMM d, y': 'en-US'`
   to match repo's SSR-safe date formatting pattern.

Signed-off-by: Nirav Patel <npatel@linuxfoundation.org>
Signed-off-by: Nirav Patel <npatel@linuxfoundation.org>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
apps/lfx-one/src/server/services/my-newsletters.service.ts (2)

78-98: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Unexpected upstream failures logged only at warning, not error.

getArchiveDetail's catch block always logs at warning level, even for genuinely unexpected failures (5xx/network errors), per the docstring "logs warnings for expected and unexpected failures" and confirmed by test logs unexpected errors at warning level. This is intentional, but it means real outages in this path won't be distinguishable from expected 403/404s by log level, which could reduce alerting visibility if alerts are keyed off error-level logs.

Consider branching on error status/type to log unexpected failures (non-403/404) at error level while keeping 403/404 at warning.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/lfx-one/src/server/services/my-newsletters.service.ts` around lines 78 -
98, Update the catch block in getArchiveDetail to distinguish expected 403/404
upstream responses from unexpected failures such as 5xx or network errors;
retain warning logging for 403/404 and use error-level logging for all other
failures, preserving the existing context and rethrow behavior.

137-160: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Duplicate foundation lookups when a foundation project is also directly referenced.

The second-pass parentUids collection isn't filtered against UIDs already fetched in the first pass (projectUids), so any foundation that is both a parent_uid and a directly-referenced project_uid gets fetched twice via projectService.getProjectById on every archive request.

♻️ Proposed fix to skip already-fetched UIDs
     const parentUids = Array.from(
       new Set(
         projects
           .filter((p): p is Project => p !== null)
           .map((p) => p.parent_uid)
-          .filter((uid): uid is string => uid !== undefined && uid !== null)
+          .filter((uid): uid is string => uid !== undefined && uid !== null && !projectUids.includes(uid))
       )
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/lfx-one/src/server/services/my-newsletters.service.ts` around lines 137
- 160, Update the parent UID collection in the second-pass flow to exclude any
UIDs already present in the first-pass projectUids set before batching and
calling projectService.getProjectById. Preserve collection of unique non-null
parent_uid values and leave fetching for genuinely new parent projects
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/lfx-one/src/app/modules/newsletters/my-newsletters/my-newsletters-list.component.html`:
- Around line 36-43: Remove the explicit keydown.enter and keydown.space
bindings from the newsletter preview button while retaining its click binding to
openPreview(newsletter). Keep the button’s existing accessibility attributes and
styling unchanged.

In
`@apps/lfx-one/src/app/modules/newsletters/my-newsletters/my-newsletters-list.component.ts`:
- Around line 101-116: Update the catchError fallback in the listMyNewsletters
load-more flow to preserve the current token by returning it as next_page_token
instead of undefined. Keep the existing newsletter fallback and response
handling unchanged so transient failures leave the Load more control available
for retry.

In `@apps/lfx-one/src/server/controllers/my-newsletters.controller.ts`:
- Around line 47-56: Move the newsletterUid presence and UUID validation in
getArchiveDetail inside its existing try block so ServiceValidationError is
handled by the method’s local catch and forwarded through Express error
handling. Preserve the current validation message and operation metadata.

---

Nitpick comments:
In `@apps/lfx-one/src/server/services/my-newsletters.service.ts`:
- Around line 78-98: Update the catch block in getArchiveDetail to distinguish
expected 403/404 upstream responses from unexpected failures such as 5xx or
network errors; retain warning logging for 403/404 and use error-level logging
for all other failures, preserving the existing context and rethrow behavior.
- Around line 137-160: Update the parent UID collection in the second-pass flow
to exclude any UIDs already present in the first-pass projectUids set before
batching and calling projectService.getProjectById. Preserve collection of
unique non-null parent_uid values and leave fetching for genuinely new parent
projects unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c7b022fa-d2c5-4a4a-bad9-fac961531d2a

📥 Commits

Reviewing files that changed from the base of the PR and between 8092163 and ca52495.

📒 Files selected for processing (13)
  • apps/lfx-one/src/app/modules/newsletters/my-newsletters/my-newsletters-list.component.html
  • apps/lfx-one/src/app/modules/newsletters/my-newsletters/my-newsletters-list.component.scss
  • apps/lfx-one/src/app/modules/newsletters/my-newsletters/my-newsletters-list.component.ts
  • apps/lfx-one/src/app/modules/newsletters/newsletters.routes.ts
  • apps/lfx-one/src/app/shared/services/newsletter.service.ts
  • apps/lfx-one/src/app/shared/services/sidebar-nav.service.ts
  • apps/lfx-one/src/server/controllers/my-newsletters.controller.ts
  • apps/lfx-one/src/server/routes/my-newsletters.route.ts
  • apps/lfx-one/src/server/server.ts
  • apps/lfx-one/src/server/services/my-newsletters.service.spec.ts
  • apps/lfx-one/src/server/services/my-newsletters.service.ts
  • apps/lfx-one/src/server/services/newsletter-service.client.ts
  • packages/shared/src/interfaces/newsletter.interface.ts

Comment thread apps/lfx-one/src/server/controllers/my-newsletters.controller.ts Outdated
MRashad26
MRashad26 previously approved these changes Jul 22, 2026

@MRashad26 MRashad26 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — full-stack newsletter archive is clean. takeUntilDestroyed(this.destroyRef) pattern is correct for subscriptions outside injection context; computed signals for filteredNewsletters/foundationOptions/canLoadMore/isEmpty are well-structured; UUID validation in the controller gates the upstream path interpolation; route ordering is safe. No template method calls, no effect(), no local interfaces.

this.previewVisible.set(true);
}
this.previewLoading.set(false);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preview fetches lack ordering guard

Medium Severity

openPreview fires detail requests without correlating responses to the clicked row. A slower older response can overwrite a newer one, and a failed fetch leaves the drawer open with the previous newsletter’s content.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 99918e4. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a recipient-facing newsletter archive to the Me lens, backed by authenticated archive endpoints and project/foundation enrichment.

Changes:

  • Adds shared archive contracts and Express list/detail endpoints.
  • Adds the Angular archive list, filtering, pagination, and preview drawer.
  • Adds navigation and server route registration.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/shared/src/interfaces/newsletter.interface.ts Defines archive response types.
apps/lfx-one/src/server/services/newsletter-service.client.ts Calls upstream archive endpoints.
apps/lfx-one/src/server/services/my-newsletters.service.ts Resolves memberships and enriches projects.
apps/lfx-one/src/server/services/my-newsletters.service.spec.ts Tests archive service behavior.
apps/lfx-one/src/server/server.ts Mounts the archive API router.
apps/lfx-one/src/server/routes/my-newsletters.route.ts Defines list and detail routes.
apps/lfx-one/src/server/controllers/my-newsletters.controller.ts Validates and handles archive requests.
apps/lfx-one/src/app/shared/services/sidebar-nav.service.ts Adds the Me-lens navigation item.
apps/lfx-one/src/app/shared/services/newsletter.service.ts Adds frontend archive requests.
apps/lfx-one/src/app/modules/newsletters/newsletters.routes.ts Registers the archive page.
apps/lfx-one/src/app/modules/newsletters/my-newsletters/my-newsletters-list.component.ts Implements archive state and interactions.
apps/lfx-one/src/app/modules/newsletters/my-newsletters/my-newsletters-list.component.scss Adds the component stylesheet placeholder.
apps/lfx-one/src/app/modules/newsletters/my-newsletters/my-newsletters-list.component.html Renders filtering, archive rows, and preview.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +170 to +172
public async archiveList(req: Request, committeeUids: string[], pageToken?: string): Promise<MyNewsletterArchiveResponse> {
const query: Record<string, string> = {
committee_uids: committeeUids.join(','),
Comment on lines +169 to +172
logger.debug(req, 'enrich_newsletters_project_data', 'Project enrichment complete', {
resolved: projectMap.size,
unresolved: projectUids.length - projectMap.size,
});
Comment on lines +220 to +223
await service.listArchive(mockRequest);

// Verify that getProjectById was called 51 times total (batching is internal)
expect(projectService.getProjectById).toHaveBeenCalledTimes(51);
Comment on lines +123 to +128
for (let i = 0; i < projectUids.length; i += batchSize) {
const batch = projectUids.slice(i, i + batchSize);
const results = await Promise.all(
batch.map(async (uid) => {
try {
return await this.projectService.getProjectById(req, uid, false);
Comment on lines +70 to +72
<!-- Newsletter preview drawer -->
@if (previewNewsletter()) {
<lfx-newsletter-preview-drawer
Comment on lines +191 to +192
public async archiveDetail(req: Request, newsletterUid: string): Promise<Newsletter> {
return this.microserviceProxy.proxyRequest<Newsletter>(req, 'LFX_V2_SERVICE', `/newsletters/archive/${encodeURIComponent(newsletterUid)}`, 'GET');
Copilot AI review requested due to automatic review settings July 22, 2026 21:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.

Comments suppressed due to low confidence (2)

apps/lfx-one/src/server/services/my-newsletters.service.ts:171

  • projectMap includes both requested projects and their parent foundations, so subtracting its total size can report a negative unresolved count whenever parents were fetched. Count unresolved requested UIDs directly to keep operational telemetry accurate.
      unresolved: projectUids.length - projectMap.size,

apps/lfx-one/src/server/services/my-newsletters.service.spec.ts:223

  • This assertion only proves that every UID was looked up; it still passes if batching is removed or if all 51 calls run concurrently. It also contradicts the comment above: a batch size of 25 produces 25 + 25 + 1, not 25 + 26. Add a concurrency-aware assertion so the test actually covers the 25-request cap.
      // Verify that getProjectById was called 51 times total (batching is internal)
      expect(projectService.getProjectById).toHaveBeenCalledTimes(51);

page_token: pageToken ? 'present' : 'absent',
});

const archiveResponse = await this.newsletterClient.archiveList(req, committeeUidList, pageToken);
this.previewSubject.set(newsletter.subject);
this.previewBodyHtml.set(newsletter.body_html);
// Use created_by as the display name (creator email/identifier)
this.previewDisplayName.set(newsletter.created_by);
Comment on lines +130 to +134
this.newsletterService
.getMyNewsletterDetail(item.id)
.pipe(
catchError(() => of(null)),
takeUntilDestroyed(this.destroyRef)
event.stopPropagation();
}

this.previewLoading.set(true);
return all.filter((nl) => nl.foundation_slug === selectedFoundation);
});

protected readonly canLoadMore: Signal<boolean> = computed(() => !!this.nextPageToken() && !this.loading() && !this.loadingMore());
Comment on lines +70 to +74
<lfx-newsletter-preview-drawer
[(visible)]="previewVisible"
[subject]="previewSubject()"
[bodyHtml]="previewBodyHtml()"
[displayName]="previewDisplayName()"
<span class="text-gray-500"> • {{ newsletter.project_name }}</span>
}
</div>
<div class="text-xs text-gray-500">Sent {{ newsletter.sent_at | date: 'MMM d, y' : 'en-US' }}</div>
Copilot AI review requested due to automatic review settings July 22, 2026 21:47
1. Use foundation_name (fallback to project_name) as the newsletter preview
   displayName instead of created_by, which doesn't exist in the upstream DTO.
   Matches sender-side preview pattern (project/foundation name).

2. Add defensive cap for committee UIDs at 500 to prevent exceeding the
   upstream archive endpoint limit. Truncate with warning log if exceeded.

Signed-off-by: Nirav Patel <npatel@linuxfoundation.org>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 410acaa. Configure here.

this.previewVisible.set(true);
}
this.previewLoading.set(false);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preview fetch shows no loading UI

Medium Severity

Row click starts a lazy detail fetch and toggles previewLoading, but the template never reads that signal. The preview drawer is only rendered after previewNewsletter is set, so users see no loading feedback until the request finishes or fails silently.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 410acaa. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (7)

apps/lfx-one/src/server/services/my-newsletters.service.ts:49

  • The linked archive endpoint rejects more than 50 committee_uids, while getMyCommitteeUids() intentionally fetches every membership and this call sends the entire set. A user in 51+ committees will receive a 400, which the UI converts into an empty archive. The BFF/upstream contract needs to support the full set (simply truncating would violate the “every sent newsletter” requirement; chunking also requires globally correct cursor merging).
          original_count: committeeUidList.length,

apps/lfx-one/src/server/services/newsletter-service.client.ts:192

  • This return type does not match the linked upstream contract. /newsletters/archive/{uid} returns only id, project_uid, subject, body_html, sent_at, committee_uids, and status; Newsletter additionally promises fields such as created_by, ed_reply_email, and timestamps. The false type already masks the undefined created_by consumed by the new component. Add a dedicated archive-detail interface and use it through the BFF and Angular client.
  public async archiveDetail(req: Request, newsletterUid: string): Promise<Newsletter> {
    return this.microserviceProxy.proxyRequest<Newsletter>(req, 'LFX_V2_SERVICE', `/newsletters/archive/${encodeURIComponent(newsletterUid)}`, 'GET');

apps/lfx-one/src/app/modules/newsletters/my-newsletters/my-newsletters-list.component.ts:142

  • The upstream archive detail DTO does not include created_by, so this sets an undefined display name and the drawer falls back to the generic “Foundation” branding. The clicked list item already has the enriched foundation name; use that instead.
          this.previewDisplayName.set(item.foundation_name || item.project_name);

apps/lfx-one/src/app/modules/newsletters/my-newsletters/my-newsletters-list.component.ts:134

  • Multiple row clicks can leave concurrent detail requests in flight because rows remain enabled and each request subscribes independently. If A is clicked and then B, a slower A response can arrive last and replace B in the drawer. Cancel the prior request (for example with switchMap) or add a preview request generation check before applying the response.
    this.newsletterService
      .getMyNewsletterDetail(item.id)
      .pipe(
        catchError(() => of(null)),
        takeUntilDestroyed(this.destroyRef)

apps/lfx-one/src/app/modules/newsletters/my-newsletters/my-newsletters-list.component.html:69

  • The drawer is only instantiated after previewNewsletter is populated, and previewLoading is not rendered anywhere. Consequently a row click shows no drawer or skeleton while detail loads, contrary to the described/manual flow, and failures provide no visible response at all. Open a loading-state drawer immediately and render a skeleton (or remove the stated behavior and provide another accessible progress indication).
@if (previewNewsletter()) {

apps/lfx-one/src/server/services/my-newsletters.service.ts:128

  • This is not the batch NATS enrichment described by the PR: getProjectById() performs one HTTP proxy request to /projects/{uid}. Grouping promises by 25 only limits concurrency, so one archive page can fan out to one request per project plus one per parent foundation. Use the intended batch lookup, or update the implementation description and explicitly accept the downstream request fan-out.
    apps/lfx-one/src/server/services/my-newsletters.service.ts:171
  • projectMap.size includes fetched parent foundations, so this telemetry can report a negative unresolved count whenever the map contains more parents than missing child projects. Count unresolved requested project UIDs directly.
    }


try {
// Resolve user's committee UIDs via query service
const committeeUids = await this.committeeService.getMyCommitteeUids(req);
Copilot AI review requested due to automatic review settings July 22, 2026 21:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (6)

apps/lfx-one/src/server/services/my-newsletters.service.ts:46

  • The companion service currently rejects requests with more than 50 committee_uids, not 500. This sends 51–500 IDs and receives a 400, so users in that range cannot load the archive. Align both contracts; merely truncating to 50 would still omit newsletters despite the “every sent newsletter” requirement, so the upstream API or pagination design must support the complete membership set.
      // Cap committee UIDs at 500 (upstream service limit)
      const COMMITTEE_UID_CAP = 500;

apps/lfx-one/src/server/services/newsletter-service.client.ts:192

  • The archive detail contract is not a full Newsletter: the companion API returns only id, project_uid, subject, body_html, sent_at, committee_uids, and status. Typing this response as Newsletter falsely promises fields such as ed_reply_email, total_recipients, and timestamps, which will be undefined at runtime. Add a dedicated archive-detail DTO and use it through the server and frontend clients.
  public async archiveDetail(req: Request, newsletterUid: string): Promise<Newsletter> {
    return this.microserviceProxy.proxyRequest<Newsletter>(req, 'LFX_V2_SERVICE', `/newsletters/archive/${encodeURIComponent(newsletterUid)}`, 'GET');

apps/lfx-one/src/app/modules/newsletters/my-newsletters/my-newsletters-list.component.ts:134

  • Each row click starts an independent detail subscription without canceling or ordering earlier requests. If the user clicks A and then B but A resolves last, A overwrites the preview for the newer B selection. Use switchMap or a preview request generation/selected-ID check before applying the response.
    this.newsletterService
      .getMyNewsletterDetail(item.id)
      .pipe(
        catchError(() => of(null)),
        takeUntilDestroyed(this.destroyRef)

apps/lfx-one/src/app/modules/newsletters/my-newsletters/my-newsletters-list.component.ts:128

  • previewLoading is never consumed by this component's template or the drawer, so clicking a newsletter shows no skeleton or other loading feedback despite the documented flow. On a slow request the UI appears inert. Render a loading state (or open a loading-capable drawer immediately) while the detail request is pending.
    this.previewLoading.set(true);

apps/lfx-one/src/app/modules/newsletters/my-newsletters/my-newsletters-list.component.html:51

  • Angular's DatePipe arguments are (format, timezone, locale), so this passes en-US as a timezone and leaves the locale unchanged. Put it in the locale position (or omit it if the application locale is intended).
                <div class="text-xs text-gray-500">Sent {{ newsletter.sent_at | date: 'MMM d, y' : 'en-US' }}</div>

apps/lfx-one/src/server/services/my-newsletters.service.ts:139

  • This still makes one downstream request per distinct project and another per parent; limiting concurrency to 25 is not a batch lookup. A page can therefore fan out to dozens of project requests. ProjectService.getProjectsByIds (project.service.ts:365-403) already performs batched query-service lookups; use it for the initial and parent UID sets.
            return await this.projectService.getProjectById(req, uid, false);

this.newsletterService
.listMyNewsletters(token)
.pipe(
catchError(() => of({ newsletters: [], next_page_token: undefined as string | undefined })),
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants