feat(org-lens): add foundations and projects section to org overview#706
Conversation
…FXV2-1680) Add the Foundations and Projects section to /org/overview. Renders a 4-tile stat strip (Foundations / Projects / Governance Roles / Meetings This Week) and a per-foundation table with inline-detail project rows, sourced from two pre-aggregated dbt rollups via a single Snowflake query per render. Server side - new GET /api/orgs/:accountId/lens/foundations-and-projects mounted under a fresh /api/orgs router (kept distinct from the existing /api/organizations router to avoid widening that surface) - OrgLensFoundationsController validates accountId against the Salesforce id pattern and emits start / success lifecycle logs - OrgLensFoundationsService runs one LEFT JOIN against the ORG_LENS_FOUNDATIONS_AND_PROJECTS rollup and the ORG_LENS_FOUNDATION_PROJECTS_DETAIL per-project detail and shapes the wire response (rows + stat strip), normalising the '__outside_lf__' sentinel to the 'outside-lf' kebab slug at the wire boundary - empty-org case returns a 200 with an empty rows envelope (never a 404), matching the rest of Org Lens Client side - OrgOverviewFoundationsAndProjectsComponent owns fetch + loading / error / ready / empty state, retry, per-row expansion (reset on org switch), and first-render telemetry - FoundationRowComponent renders the 4-cell main row (logo + tier-ribbon subtitle + chevron, Org Role, Voting Status, Governance Participation) using :host { display: contents } so the inner <tr> becomes a direct tbody child - FoundationsStatStripComponent renders the 4 stat tiles with per-tile breakdown subtext - foundation-logo / tier-ribbon helpers centralise the deterministic-by-foundation-id colour and class derivations - OrgLensFoundationsService HTTP proxy fronts the new endpoint - mounted into org-overview.component via @defer (on viewport) with a 4-tile skeleton placeholder; non-LF project-row clicks remain no-ops, LF rows route to /org/projects (slug-aware drilldown is a follow-on) Shared - OrgLensRowKind, OrgRoleBadge, VotingStatusBadge, GovernanceParticipationBucket, ProjectInfluenceBucket unions plus OrgLensFoundationProject / OrgLensFoundationRow / OrgLensFoundationsStatStrip / OrgLensFoundationsAndProjectsResponse interfaces - MembershipTierClass expanded from the prior 10-class union to the canonical 13-class ladder (Founding / Strategic / End User / Contributor added; Sponsor removed; Steering demoted to rank 6) with the rank order captured in JSDoc Temporary dev-schema bridge - organization.service.ts ORG_LENS_ACCOUNT_CONTEXT query and the new OrgLensFoundationsService both read from ANALYTICS_DEV.LF_AOPEYEMI_PLATINUM_LFX_ONE rather than ANALYTICS.PLATINUM_LFX_ONE, because the dbt PR that promotes the ORG_LENS_FOUNDATIONS_AND_PROJECTS / ORG_LENS_FOUNDATION_PROJECTS_DETAIL rollups (and the 13-class tier ladder) into prod is still in flight. Both qualifiers MUST flip back to ANALYTICS.PLATINUM_LFX_ONE before this PR's parent (CD-3507 org lens shell) merges to main. This PR stacks on feat/CD-3507-org-lens-shell (PR #646). Generated with [Cursor Composer](https://cursor.com/composer) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: ahmedomosanya <aopeyemi@contractor.linuxfoundation.org>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Comment |
There was a problem hiding this comment.
Pull request overview
Adds the Foundations and Projects section to the Org Lens overview page (/org/overview), backed by a new BFF endpoint that joins two pre-aggregated dbt rollups in Snowflake. The shared MembershipTierClass union is widened from 10 to 13 canonical tier classes (Sponsor removed; Founding/Strategic/End User/Contributor added; Steering demoted). Both Snowflake reads are temporarily wired to a developer-scoped dev schema (ANALYTICS_DEV.LF_AOPEYEMI_PLATINUM_LFX_ONE) and must be flipped back to ANALYTICS.PLATINUM_LFX_ONE before the parent feature branch merges to main.
Changes:
- New API
GET /api/orgs/:accountId/lens/foundations-and-projectswith controller + service + dedicated/api/orgsrouter. - New Angular section component (stat strip, foundations table with chevron expansion to per-project inline detail) deferred on viewport, plus tier/logo helpers and a client
OrgLensFoundationsService. - Shared interface additions for the new response shape and a breaking widening of
MembershipTierClassto the 13-class ladder.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/shared/src/interfaces/org-lens.interface.ts | Widens MembershipTierClass; adds row/project/stat-strip/response interfaces |
| apps/lfx-one/src/server/services/organization.service.ts | Switches ORG_LENS_ACCOUNT_CONTEXT to dev schema |
| apps/lfx-one/src/server/services/org-lens-foundations.service.ts | New service: joined Snowflake query, sentinel slug normalization, stat-strip aggregation |
| apps/lfx-one/src/server/controllers/org-lens-foundations.controller.ts | New controller with accountId regex validation |
| apps/lfx-one/src/server/routes/orgs.route.ts | New router for /api/orgs/:accountId/lens/... |
| apps/lfx-one/src/server/server.ts | Mounts the new /api/orgs router |
| apps/lfx-one/src/app/shared/services/org-lens-foundations.service.ts | Client HTTP proxy for the new endpoint |
| apps/lfx-one/src/app/modules/dashboards/org/org-overview/org-overview.component.{ts,html} | Adds deferred-on-viewport section with skeleton placeholder |
| .../org-overview-foundations-and-projects.component.{ts,html,scss} | Parent section: state stream, retry, expansion, telemetry |
| .../components/foundations-stat-strip.component.{ts,html} | 4-tile stat strip with detail-line aggregation |
| .../components/foundation-row.component.{ts,html,scss} | Per-foundation row with badges, chevron, navigation |
| .../helpers/{tier-ribbon,foundation-logo}.helper.ts | Tier-pill palette and deterministic logo-square fallback |
Comments suppressed due to low confidence (4)
apps/lfx-one/src/server/services/org-lens-foundations.service.ts:210
- The Outside-LF sentinel is normalized to
outside-lfonly onfoundationSlug;foundationId(__outside_lf__) is still used as the React-style key in@for ... track row.foundationIdin the parent template, as the expansion-state map key, and as theMapkey inrowsByFoundation. The PR description states the__outside_lf__sentinel "MUST NOT leak into testids, telemetry payloads, or future routing keys" — but it currently leaks into themfp_caret_toggle/mfp_row_clickPlausible payloads (which receivefoundationName, but the parent'stoggleExpansion(row.foundationId)and the testid fallbackrow.foundationSlug || row.foundationIdwould still emit it for any non-outside-lf row whose dbt slug is null). Recommend normalizing bothfoundationIdandfoundationSlugtooutside-lffor the umbrella row, or computing the testid/expansion key from the already-normalizedfoundationSlug.
// Normalize the Outside-LF umbrella row's slug at the wire boundary.
// dbt emits FOUNDATION_ID='__outside_lf__' and FOUNDATION_SLUG also
// equal to '__outside_lf__' (literal sentinel string, NOT null) for
// the umbrella row. The wire value MUST be the kebab-case slug
// 'outside-lf'. We force this for any row_kind='outside_lf'
// regardless of what dbt sends — the sentinel is an internal
// implementation detail and MUST NOT leak into testids, telemetry
// payloads, or future routing keys.
const foundationSlug =
raw.ROW_KIND === 'outside_lf' ? 'outside-lf' : (raw.FOUNDATION_SLUG ?? raw.FOUNDATION_ID);
row = {
foundationId: raw.FOUNDATION_ID,
foundationSlug,
foundationName: raw.FOUNDATION_NAME,
foundationLogoUrl: raw.FOUNDATION_LOGO_URL,
rowKind: raw.ROW_KIND,
membershipTierClass: raw.MEMBERSHIP_TIER_CLASS,
membershipTierDisplayName: raw.MEMBERSHIP_TIER_DISPLAY_NAME,
projectCount: raw.PROJECT_COUNT_LF ?? 0,
badges: {
orgRole: raw.ORG_ROLE,
votingStatus: raw.VOTING_STATUS,
// Outside LF emits NULL bucket → render em-dash.
governanceParticipation: raw.GOVERNANCE_PARTICIPATION_BUCKET ?? '—',
governanceAttendancePct: raw.GOVERNANCE_ATTENDANCE_PCT,
},
projects: [],
};
rowsByFoundation.set(raw.FOUNDATION_ID, row);
apps/lfx-one/src/server/services/org-lens-foundations.service.ts:231
projectCountis set toraw.PROJECT_COUNT_LF ?? 0from the upstream rollup, while theprojectsarray is populated from the LEFT JOIN and may include projects that fall outside thePROJECT_COUNT_LFdefinition (e.g. non-LF projects under the Outside-LF umbrella, or projects gated out byPROJECT_INFLUENCE_MAX_BUCKETbeing null on line 231). As a result,projectCountmay not matchprojects.length, andprojectsInvolvedText("{count} project(s) involved") on the row can display a different number than the inline-detail caption ("Projects Involved ({{ row.projects.length }})"). Consider either derivingprojectCountfromprojects.lengthserver-side or documenting the intentional discrepancy.
projectCount: raw.PROJECT_COUNT_LF ?? 0,
badges: {
orgRole: raw.ORG_ROLE,
votingStatus: raw.VOTING_STATUS,
// Outside LF emits NULL bucket → render em-dash.
governanceParticipation: raw.GOVERNANCE_PARTICIPATION_BUCKET ?? '—',
governanceAttendancePct: raw.GOVERNANCE_ATTENDANCE_PCT,
},
projects: [],
};
rowsByFoundation.set(raw.FOUNDATION_ID, row);
// Foundations tile: count member foundations only.
if (row.rowKind === 'member' && row.membershipTierClass) {
foundationsTotal += 1;
tierBreakdown[row.membershipTierClass] = (tierBreakdown[row.membershipTierClass] ?? 0) + 1;
}
// Governance + meetings tiles: LF foundations only.
// Outside-LF emits zero from dbt, but skip to be explicit.
if (row.rowKind !== 'outside_lf') {
boardMembers += raw.BOARD_MEMBER_SEAT_COUNT ?? 0;
committeeMembers += raw.COMMITTEE_MEMBER_SEAT_COUNT ?? 0;
mtwBoard += raw.MEETINGS_THIS_WEEK_BOARD ?? 0;
mtwTechnical += raw.MEETINGS_THIS_WEEK_TECHNICAL ?? 0;
mtwMarketing += raw.MEETINGS_THIS_WEEK_MARKETING ?? 0;
mtwWorkingGroup += raw.MEETINGS_THIS_WEEK_WORKING_GROUP ?? 0;
mtwOther += raw.MEETINGS_THIS_WEEK_OTHER ?? 0;
}
}
if (raw.PROJECT_ID && raw.PROJECT_INFLUENCE_MAX_BUCKET) {
apps/lfx-one/src/server/services/org-lens-foundations.service.ts:261
- Project rows are skipped silently if
PROJECT_INFLUENCE_MAX_BUCKETis null even whenPROJECT_IDis non-null — this drops them from both the inline-detail table and the projects-tile counts with no log. If dbt ever emits a project row before the influence bucket is computed (or with an unmapped value), the user sees a foundation row claiming N projects involved with fewer rendered. Either default the bucket to'Silent'in the SQL/CASE or log a warning when this branch is hit.
if (raw.PROJECT_ID && raw.PROJECT_INFLUENCE_MAX_BUCKET) {
const project: OrgLensFoundationProject = {
projectId: raw.PROJECT_ID,
projectSlug: raw.PROJECT_SLUG ?? raw.PROJECT_ID,
projectName: raw.PROJECT_NAME ?? raw.PROJECT_ID,
isLfProject: raw.PROJECT_IS_LF_PROJECT === true,
influence: raw.PROJECT_INFLUENCE_MAX_BUCKET,
maintainers: raw.PROJECT_MAINTAINERS_COUNT ?? 0,
contributors: raw.PROJECT_CONTRIBUTORS_COUNT ?? 0,
collaborators: raw.PROJECT_COLLABORATORS_COUNT ?? 0,
commits: raw.PROJECT_COMMITS_COUNT ?? 0,
};
row.projects.push(project);
// Projects tile: count every project on every row (including
// Outside-LF) per the influence bucket.
switch (project.influence) {
case 'Leading':
projectsLeading += 1;
break;
case 'Contributing':
projectsContributing += 1;
break;
case 'Participating':
projectsParticipating += 1;
break;
case 'Silent':
projectsSilent += 1;
break;
}
}
apps/lfx-one/src/server/services/org-lens-foundations.service.ts:301
- When
result.rows.length === 0the service returns an empty response withaccountName: ''. The FE component then usescompanyName()which falls back to the AccountContext, so the UI is fine — but consumers of the API directly would see a blank account name where the org actually exists (just has no foundation involvement). Consider either fetching/echoing the account name from the existingORG_LENS_ACCOUNT_CONTEXTtable for the empty path, or documenting on the response interface thataccountNameis empty string for the zero-engagement case.
private emptyResponse(accountId: string): OrgLensFoundationsAndProjectsResponse {
return {
accountId,
accountName: '',
statStrip: {
foundations: { total: 0, breakdown: {} },
projects: { total: 0, leading: 0, contributing: 0, participating: 0, silent: 0 },
governanceRoles: { total: 0, boardMembers: 0, committeeMembers: 0 },
meetingsThisWeek: { total: 0, board: 0, technical: 0, marketing: 0, workingGroup: 0, other: 0 },
},
rows: [],
};
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| FROM ANALYTICS_DEV.LF_AOPEYEMI_PLATINUM_LFX_ONE.ORG_LENS_FOUNDATIONS_AND_PROJECTS f | ||
| LEFT JOIN ANALYTICS_DEV.LF_AOPEYEMI_PLATINUM_LFX_ONE.ORG_LENS_FOUNDATION_PROJECTS_DETAIL p |
| MEMBERSHIP_TIER_DISPLAY_NAME, | ||
| MEMBERSHIP_TIER_CLASS | ||
| FROM ANALYTICS.PLATINUM_LFX_ONE.ORG_LENS_ACCOUNT_CONTEXT | ||
| FROM ANALYTICS_DEV.LF_AOPEYEMI_PLATINUM_LFX_ONE.ORG_LENS_ACCOUNT_CONTEXT |
| <tr | ||
| [class]="trClasses()" | ||
| [attr.role]="trRole()" | ||
| [attr.tabindex]="trTabIndex()" | ||
| [attr.data-testid]="'org-overview-foundations-and-projects-row-' + testIdSlug()" | ||
| (click)="onRowClick($event)" | ||
| (keydown)="onRowKeydown($event)"> | ||
| <td class="px-4 py-3 align-middle"> | ||
| <div class="flex items-center gap-3"> | ||
| <button | ||
| type="button" | ||
| class="h-7 w-7 p-0 flex items-center justify-center rounded text-gray-500 hover:bg-gray-200 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:outline-none" | ||
| [attr.aria-expanded]="expanded()" | ||
| [attr.aria-label]="chevronAriaLabel()" | ||
| [attr.data-testid]="'org-overview-foundations-and-projects-row-' + testIdSlug() + '-caret'" | ||
| (click)="onChevronClick($event)"> | ||
| <i class="fa-light" [class.fa-chevron-down]="!expanded()" [class.fa-chevron-up]="expanded()" aria-hidden="true"></i> | ||
| </button> |
| private readonly state = toSignal( | ||
| combineLatest([this.accountId$, this.retryTrigger$.pipe(startWith(0))]).pipe( | ||
| switchMap(([accountId]) => { | ||
| if (!accountId) { | ||
| return of<SectionState>({ status: 'empty', data: EMPTY_RESPONSE }); | ||
| } | ||
| return this.foundationsService.getFoundationsAndProjects(accountId).pipe( | ||
| map<OrgLensFoundationsAndProjectsResponse, SectionState>((data) => ({ | ||
| status: data.rows.length === 0 ? 'empty' : 'ready', | ||
| data, | ||
| })), | ||
| startWith<SectionState>({ status: 'loading', data: null }), | ||
| catchError(() => of<SectionState>({ status: 'error', data: null })), | ||
| tap((s) => { | ||
| if (s.status === 'ready' || s.status === 'empty') { | ||
| this.emitOverviewViewOnce(accountId); | ||
| } | ||
| }) | ||
| ); | ||
| }) | ||
| ), | ||
| { initialValue: INITIAL_STATE } | ||
| ); |
| <a | ||
| routerLink="/org/memberships" | ||
| class="text-sm text-blue-600 hover:text-blue-700 hover:underline focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:outline-none rounded" | ||
| data-testid="org-overview-foundations-and-projects-view-all-link" | ||
| >View all on Memberships page ›</a | ||
| > |
| <tr | ||
| class="border-b border-gray-100" | ||
| [class.cursor-pointer]="project.isLfProject" | ||
| [class.hover:bg-white]="project.isLfProject" | ||
| [class.cursor-default]="!project.isLfProject" | ||
| [attr.role]="project.isLfProject ? 'button' : null" | ||
| [attr.tabindex]="project.isLfProject ? 0 : null" | ||
| (click)="onProjectRowClick(project)" | ||
| (keydown)="onProjectRowKeydown($event, project)" | ||
| [attr.data-testid]=" | ||
| 'org-overview-foundations-and-projects-row-' + | ||
| (row.foundationSlug || row.foundationId) + | ||
| '-project-' + | ||
| projectSlugTestId(project.projectSlug, project.projectId) | ||
| "> | ||
| <td class="py-2"> | ||
| <span class="inline-flex items-center gap-2"> | ||
| <span | ||
| class="inline-block w-2 h-2 rounded-full" | ||
| [class]="influenceDotClasses(project.influence)" | ||
| [attr.aria-label]="project.influence + ' influence'"></span> | ||
| <span class="text-gray-900">{{ project.projectName }}</span> | ||
| </span> | ||
| </td> | ||
| <td class="py-2 text-right text-gray-700">{{ project.maintainers.toLocaleString() }}</td> | ||
| <td class="py-2 text-right text-gray-700">{{ project.contributors.toLocaleString() }}</td> | ||
| <td class="py-2 text-right text-gray-700">{{ project.collaborators.toLocaleString() }}</td> | ||
| <td class="py-2 text-right text-gray-700">{{ project.commits.toLocaleString() }}</td> | ||
| </tr> |
| * Rank-banded mapping into 4 LFX semantic families: | ||
| * - Top-4 (premium): Platinum / Premier / Founding / Strategic → violet | ||
| * - Mid (sponsor): Gold / Steering / Silver → amber | ||
| * - Standard: General / Associate → blue | ||
| * - Free / observer: End User / Academic / Contributor / Other → gray | ||
| * - Non-member LF row: → amber outline | ||
| * - Outside LF row: → gray |
| * Subtitle pill text. Binds to the canonical `membershipTierClass` | ||
| * (NOT `membershipTierDisplayName`) so the badge renders the canonical | ||
| * tier label. | ||
| */ | ||
| protected readonly subtitleText = computed<string>(() => { | ||
| const r = this.row(); | ||
| if (r.rowKind === 'outside_lf') return 'Outside LF'; | ||
| if (r.rowKind === 'non_member') return 'Non-member'; | ||
| const tier = r.membershipTierClass ?? 'Member'; | ||
| return `${tier} Member`; |
| @@ -178,6 +179,7 @@ app.use('/api/committees', committeesRouter); | |||
| app.use('/api/mailing-lists', mailingListsRouter); | |||
| app.use('/api/meetings', meetingsRouter); | |||
| public async getFoundationsAndProjects(req: Request, res: Response, next: NextFunction): Promise<void> { | ||
| const accountId = req.params['accountId']; | ||
| const startTime = logger.startOperation(req, 'get_org_lens_foundations_and_projects', { | ||
| account_id: accountId, | ||
| }); | ||
|
|
||
| try { | ||
| if (!accountId || typeof accountId !== 'string') { | ||
| throw ServiceValidationError.forField('accountId', 'accountId path parameter is required', { | ||
| operation: 'get_org_lens_foundations_and_projects', | ||
| }); | ||
| } | ||
|
|
||
| if (!ACCOUNT_ID_PATTERN.test(accountId)) { | ||
| throw ServiceValidationError.forField('accountId', 'Invalid Salesforce accountId format', { | ||
| operation: 'get_org_lens_foundations_and_projects', | ||
| }); | ||
| } | ||
|
|
||
| const response = await this.service.getFoundationsAndProjects(accountId); | ||
|
|
||
| const projectCountTotal = response.rows.reduce((sum, row) => sum + row.projects.length, 0); | ||
|
|
||
| logger.success(req, 'get_org_lens_foundations_and_projects', startTime, { | ||
| account_id: accountId, | ||
| row_count: response.rows.length, | ||
| project_count_total: projectCountTotal, | ||
| }); | ||
|
|
||
| // No PII or tokens in logs. account_id is already a Salesforce | ||
| // opaque ID, safe. | ||
| res.setHeader('Cache-Control', 'no-store'); | ||
| res.json(response); | ||
| } catch (error) { | ||
| next(error); | ||
| } |
…files Six files introduced by PR #706 (LFXV2-1680 foundations and projects section) had minor whitespace / line-length drift flagged by the CI prettier check. No logic changes — pure prettier --write output to keep the line-length and self-closing-tag rules consistent with the rest of the org lens code. - foundation-row.component.html - foundations-stat-strip.component.html - org-overview-foundations-and-projects.component.html - shared/services/org-lens-foundations.service.ts (client proxy) - server/routes/orgs.route.ts - server/services/org-lens-foundations.service.ts Mirrors the precedent set by 2577089 ("style(org-lens): fix prettier formatting on org lens files") which fixed the same class of CI drift on the prior org lens batch. Generated with [Cursor Composer](https://cursor.com/composer) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: ahmedomosanya <aopeyemi@contractor.linuxfoundation.org>
#731) Flip three table references in the server-side org lens queries from the personal dev schema back to the prod platinum schema, now that lf-dbt#2406 has merged and materialised the three org lens models in ANALYTICS.PLATINUM_LFX_ONE (97k / 237k / 3.2M rows as of 2026-05-18). No logic change — pure schema-qualifier flip. - org-lens-foundations.service.ts: FROM and LEFT JOIN switched from ANALYTICS_DEV.LF_AOPEYEMI_PLATINUM_LFX_ONE.ORG_LENS_FOUNDATIONS_AND_PROJECTS and ORG_LENS_FOUNDATION_PROJECTS_DETAIL to ANALYTICS.PLATINUM_LFX_ONE.* - organization.service.ts: ORG_LENS_ACCOUNT_CONTEXT restored to ANALYTICS.PLATINUM_LFX_ONE — same qualifier the original org lens live-data PR (#667) shipped; it had been temporarily demoted to dev in #706 for demo consistency with the two new tables Generated with [Cursor Composer](https://cursor.com/composer) Signed-off-by: ahmedomosanya <aopeyemi@contractor.linuxfoundation.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Ticket: LFXV2-1680
Stacked on: #646
Summary
/org/overview(mounted via@defer (on viewport)with a 4-tile skeleton placeholder):GET /api/orgs/:accountId/lens/foundations-and-projects(single LEFT JOIN against theORG_LENS_FOUNDATIONS_AND_PROJECTSrollup ×ORG_LENS_FOUNDATION_PROJECTS_DETAILper-project detail). Empty-org case returns 200 with an empty rows envelope, never 404.__outside_lf__sentinel becomes the kebab-caseoutside-lfslug before it reaches testids / telemetry.MembershipTierClassexpanded from the prior 10-class union to the canonical 13-class ladder (Founding,Strategic,End User,Contributoradded;Sponsorremoved;Steeringdemoted to rank 6) with the rank order documented in JSDoc.Important — temporary dev-schema bridge
organization.service.ts(the existingORG_LENS_ACCOUNT_CONTEXTquery) and the newOrgLensFoundationsServiceboth read fromANALYTICS_DEV.LF_AOPEYEMI_PLATINUM_LFX_ONErather thanANALYTICS.PLATINUM_LFX_ONE, because the upstream dbt PR that promotes theORG_LENS_FOUNDATIONS_AND_PROJECTS/ORG_LENS_FOUNDATION_PROJECTS_DETAILrollups (and the 13-class tier ladder) into prod is still in flight. Both qualifiers MUST flip back toANALYTICS.PLATINUM_LFX_ONEbefore the parent PR (#646) merges tomain. Tracked in this PR so it can't get lost.Out of scope (intentionally)
/org/memberships(placeholder) without a slug; restore the slug arg + registerorg/memberships/:foundationSlugonce the detail page lands./org/projects(no slug, noProjectContextmutation, no lens switch) until the slug-aware destination ships.Test plan
/org/overviewmounts the new section after the involvement carousel and renders the 4 stat tiles with non-zero counts for a Platinum / Premier org (Toyota, IBM family).overview_viewPlausible event for the new org id.project_count_lfdesc then name), then the Outside-LF umbrella row./org/projectsand emitsmfp_project_row_click. Non-LF rows under Outside-LF are no-ops (no nav, no event).outside-lftestid slug (never the__outside_lf__sentinel).yarn build,yarn check-types,yarn lint:check,yarn format:checkall pass (pre-push build already green).