Skip to content

Batch 2: dashboard cluster (#486, #482, #427)#222

Merged
peterdrier merged 5 commits intomainfrom
sprint/2026-04-13/batch-2
Apr 13, 2026
Merged

Batch 2: dashboard cluster (#486, #482, #427)#222
peterdrier merged 5 commits intomainfrom
sprint/2026-04-13/batch-2

Conversation

@peterdrier
Copy link
Copy Markdown
Owner

Auto-executed by /execute-sprint for sprint 2026-04-13 (batch 2, lightweight tier).

Issues

nobodies-collective#486 — Gate Get Involved dashboard card on volunteer membership

  • Card not rendered when IsVolunteerMember is false
  • Active volunteers with no upcoming shifts still see it
  • Onboarding users still see ThingsToDoViewComponent unchanged
  • No orphan page regressions

One-line condition change in Views/Home/Dashboard.cshtml.

nobodies-collective#482 — UserAvatarViewComponent refactor (id-based, custom-upload precedence)

  • Component takes a user GUID, resolves internally via IProfileService
  • Custom upload shown when present (both nav and dashboard)
  • Google OAuth picture shown when only that exists
  • Initial-letter fallback when neither
  • URL-accepting overload removed
  • IMemoryCache used for avatar URL resolution (keyed on userId + UpdatedAtTicks so picture updates bust the cache)
  • Reporter metadata preserved: fb:f603a8c3

All 11 call sites migrated: Dashboard, _LoginPartial, _HumanPopover, _ProfileCard, ProfileCard/Default, Profile/Edit, Profile/Search, Profile/AdminDetail, Team/Details, Application/ApplicationDetail, Google/_SyncTabContent. Also added UserId to ProfileViewModel in the Edit path so the avatar can resolve from cache.

nobodies-collective#427 — Extract dashboard orchestration from HomeController

  • No term expiry calculation logic in HomeController
  • No shift item aggregation logic in HomeController
  • Dashboard state computed in Application-layer service

Introduces IDashboardService.GetMemberDashboardAsync(userId, isPrivileged, ct) in the Application layer with a new MemberDashboardData DTO. DashboardService in Infrastructure aggregates from IProfileService, IMembershipCalculator, IApplicationDecisionService, IShiftManagementService, IShiftSignupService, ITicketQueryService, IUserService. HomeController becomes a thin auth + mapping layer (constructor shrinks from 13 deps to 7; Index shrinks from ~230 lines to ~80).

Build / tests

  • dotnet build Humans.slnx -v q — clean, 0 warnings, 0 errors.
  • dotnet test Humans.slnx --filter "FullyQualifiedName~Home|FullyQualifiedName~Dashboard|FullyQualifiedName~Avatar|FullyQualifiedName~Profile" — Domain + Application tests pass (12 + 119). Integration tests not runnable in this sandbox (Docker unavailable), unrelated to these changes.
  • dotnet format Humans.slnx --verify-no-changes — clean.

Test plan

  • Sign in as a volunteer: / shows Get Involved card when no upcoming shifts.
  • Sign in as onboarding (not-yet-volunteer) user: Get Involved card is hidden; ThingsToDo wizard still renders.
  • Upload a custom profile picture on /Profile/Edit: custom picture shown in nav avatar and dashboard welcome avatar (both via UserAvatarViewComponent).
  • Delete the custom picture: Google OAuth avatar reappears; deleting OAuth picture too falls back to initial letter.
  • As a Colaborador/Asociado with an application expiring in <90 days: term warning card renders with correct date.
  • Confirm urgent shifts and confirmed signups still render correctly on dashboard when shift browsing is open.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b7248563b8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +43 to 50
var cached = _profileService.GetCachedProfile(userId)
?? await _profileService.GetCachedProfileAsync(userId);

if (cached is not null)
{
displayName = cached.DisplayName;
profilePictureUrl ??= cached.ProfilePictureUrl;
profilePictureUrl = ResolveAvatarUrl(cached);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve avatar fallback when no profile cache entry exists

The new ID-based avatar resolution only populates displayName/profilePictureUrl when GetCachedProfile* returns a profile, but that API explicitly returns null for users without a profile record; in those cases this component now renders ? even when the signed-in user has a valid Google picture and display name (e.g., onboarding/guest users reached via _LoginPartial). Before this change those values were passed directly by callers, so this is a user-visible regression for profileless accounts.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Confirmed — fixed in a44d290.

Good catch. The id-based refactor dropped the caller-supplied display name and picture URL that _LoginPartial and dashboard header used to pass through directly, so any user without a Profile row (onboarding, guest, pre-consent) fell straight into the ? fallback even when their Google claims were fully populated.

Added an IsCurrentUser(userId) check. When GetCachedProfile* returns null AND the requested userId matches the currently signed-in principal, fall back to ClaimTypes.Name and the urn:google:picture claim (same claim AccountController uses on sign-in). Profileless users now see their Google picture and initial on both the nav avatar and the dashboard header. Rendering another user's avatar who has no Profile still shows the initial-letter fallback — that's the intended behavior since we only have claims for the caller.

…recedence (nobodies-collective#482)

The view component now takes only a user GUID and resolves the display name
and avatar URL internally via IProfileService. Resolution precedence:
1. Custom uploaded picture (served via /Profile/{profileId}/Picture)
2. Google OAuth User.ProfilePictureUrl
3. Initial-letter fallback from cached display name

Avatar URL resolution is cached in IMemoryCache keyed on user id and
profile UpdatedAtTicks so picture updates bust the cache automatically.

All call sites (dashboard, nav, profile, team, search, admin, sync tab)
migrated to pass user-id; the URL-accepting overload has been removed.

Reporter: fb:f603a8c3
…ies-collective#427)

Introduces IDashboardService in the Application layer, with a single
GetMemberDashboardAsync method that computes the full member dashboard
snapshot: membership state, application term expiry, urgent-shift and
signup aggregation, ticket state, and participation status. HomeController
becomes a thin adapter that calls the service and maps the DTO to its
existing view model — no term expiry or shift aggregation logic remains
in the controller.
…#482 review)

View components cannot own cache state — caching must live in the
owning service. The cached profile data (from IProfileService) is
already cached; building the route URL for the custom-picture case
is cheap compared to a DB hit, so no additional caching is needed.

- Drop IMemoryCache field and constructor parameter
- Drop the ResolveAvatarUrl cache wrapper; compute the URL inline
  via IUrlHelperFactory each render
@peterdrier peterdrier force-pushed the sprint/2026-04-13/batch-2 branch from 1ea4f56 to 2e9fbab Compare April 13, 2026 15:26
…obodies-collective#482 review)

Codex P2 finding on PR #222: the id-based refactor renders "?" for any
user without a Profile row, which regresses onboarding/guest users in
_LoginPartial who have a valid Google picture and display name on their
signed-in claims but no Profile yet.

When GetCachedProfile returns null AND the requested userId is the
currently signed-in user, fall back to ClaimTypes.Name and the
"urn:google:picture" claim (the same claim AccountController uses on
login). The fallback only applies to the caller's own avatar — rendering
another user's avatar without a Profile still shows the initial-letter
fallback, which is the intended behavior.
@peterdrier peterdrier merged commit 86d84ef into main Apr 13, 2026
3 checks passed
@peterdrier peterdrier deleted the sprint/2026-04-13/batch-2 branch April 13, 2026 16:13
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.

1 participant