From 1913206d9db716fa65d5ccd1d6ae42a95e2304d1 Mon Sep 17 00:00:00 2001 From: Greg V <6913307+gregv@users.noreply.github.com> Date: Mon, 21 Sep 2026 07:03:08 +0200 Subject: [PATCH] Team dashboard: fire the GitHub activity check from the checklist too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Push code to your repo" checklist row reads the same GitHub activity as the Code activity card, but the fetch only fired when the card (bottom of the page) scrolled near — so the row sat on "Checking your repo's activity…" for anyone who stayed at #project. useGithubActivity now takes a ref or an array of refs and TeamDashboard observes both the DeliverablesChecklist and the CodeActivityCard; whichever comes near first fires the fire-once load. Pairs with the backend change that excludes the repo-bootstrap account's commits (GITHUB_ACTIVITY_EXCLUDED_LOGINS, default gregv) and reads the team doc fresh in get_team so the public team page reflects a story edit immediately. Co-Authored-By: Claude Fable 5.1 --- CLAUDE.md | 4 +- .../TeamDashboard/DeliverablesChecklist.js | 89 ++++++++++--------- src/components/TeamDashboard/TeamDashboard.js | 6 +- src/hooks/use-github-activity.js | 25 ++++-- 4 files changed, 72 insertions(+), 52 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e6210d72..f824c1a9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -493,9 +493,9 @@ The `constraints` object on a hackathon doc carries per-event toggles. Keys cons `manageteam.js` is now a thin composition: the auth wrapper and every fetch/handler stay in the page (verbatim from the earlier refined pass); presentation lives in `src/components/TeamDashboard/*`, and every section component there is **module-scope** (SectionBlock remount lesson). `TeamCreation/TeamStatusPanel.js` + `FormStepper.js` were deleted — don't re-add them (`DemoVideoEditor`/`DevPostEditor` carry their URL validators verbatim). Plan + contracts: `docs/plans/team-dashboard-devpost-replacement.md` (Parts 2.1 / 3 / 4-B) — Part B of `docs/plans/manageteam-eventpage-improvements.md` is superseded by it. - **Composition** (`TeamDashboard.js`, rendered with `key={activeTeam.id}` so the per-team editors re-seed on a `TeamSwitcher` switch): `TeamStatusHero` (IN_REVIEW only — one 220×220 waiting video) → `DeadlineStrip` → `DeliverablesChecklist` → `HackersChoiceCard` (= `PeerVoteCTA variant="dashboard"`) → `ProjectWriteupEditor` → `DemoVideoEditor` → `CodeActivityCard` → `MentorSupportCard` (+ `MentorAvailabilityToggle`) → `SlackCoachCard` → `TeamRoster` → `DevPostEditor` (framed as optional, last). IN_REVIEW renders hero + strip + Slack + roster only. Section ids: `deliverables, project, demo, devpost, code, mentors, slack, roster` — **`#project` is deep-linked** from the team page and the gallery ("Add your project story →"), keep it. `TeamMasthead` owns the page's only `

` ("Your team" / "Create a team"), status via `statusLabel()` (🏆 winning, `INACTIVE` skipped), nonprofit from `event.nonprofits` (no fan-out), `awards[]` as accent tags, "View public project page →". `CreateTeamFlow` + `GatingPanels` are JSX moved out of the page (handlers stay in the page); the stepper uses `refinedStepperSx`/`refinedFormTheme` and an honest indeterminate `LinearProgress` (no simulated progress bar). -- **Page invariants kept (don't regress):** `RequiredAuthProvider` + the `postLoginRedirectUrl` SSR guard (`currentUrl || (typeof window !== "undefined" ? window.location.href : undefined)`); **split error state** — `teamsError` (only from `fetchMyTeams`, renders the hub's calm error card + Retry) vs `formError` (create-team validation/submit) — never merge them; **lazy Slack (`active_days=365`) + nonprofit fetches** gated on `activeStep`/`showNewTeamForm` + `slackFetchedRef`/`nonprofitFetchedRef` — never on initial load; `handleTeamUpdated(teamId, partial)` merges into `myTeams` and every editor/toggle reports through it (no refetch after a save); post-create `fetchMyTeams()` + scroll to `#team-hub`; `noindex` meta (inside the client-rendered tree, so not SSR'd — pre-existing); `SurveyCTA`. Initial load for a team holder = hackathon, `team//me`, hacker application, `users/profile` (whole payload stored as `profile` — own db id for the roster "You" tag), `messages/team/` (roster — `/me` strips `users[]`); GitHub calls only once the code card scrolls near. +- **Page invariants kept (don't regress):** `RequiredAuthProvider` + the `postLoginRedirectUrl` SSR guard (`currentUrl || (typeof window !== "undefined" ? window.location.href : undefined)`); **split error state** — `teamsError` (only from `fetchMyTeams`, renders the hub's calm error card + Retry) vs `formError` (create-team validation/submit) — never merge them; **lazy Slack (`active_days=365`) + nonprofit fetches** gated on `activeStep`/`showNewTeamForm` + `slackFetchedRef`/`nonprofitFetchedRef` — never on initial load; `handleTeamUpdated(teamId, partial)` merges into `myTeams` and every editor/toggle reports through it (no refetch after a save); post-create `fetchMyTeams()` + scroll to `#team-hub`; `noindex` meta (inside the client-rendered tree, so not SSR'd — pre-existing); `SurveyCTA`. Initial load for a team holder = hackathon, `team//me`, hacker application, `users/profile` (whole payload stored as `profile` — own db id for the roster "You" tag), `messages/team/` (roster — `/me` strips `users[]`); GitHub calls only once the deliverables checklist OR the code card scrolls near (`useGithubActivity(team, [checklistRef, codeCardRef])` — fire-once; the checklist sits at the top, so in practice the fetch fires on dashboard view. Observing only the card left the checklist's "Push code" row on "Checking your repo's activity…" until the user scrolled to the bottom). - **Fixed here (Part 9):** #8 `hasApprovedTeam` = `myTeams.some(t => t.status && t.status !== "IN_REVIEW" && t.status !== "INACTIVE")` (it compared against non-existent `APPROVED`/`PROJECT_COMPLETE`, so it was always false); #9 findteam's `sessionStorage['team_members']` handoff is read once on mount → `teamMembers` objects `{id: slack_user_id, name, real_name}` (opens the create form when a team already exists) then the key is removed; #10 `TeamMemberManager`'s render-body `console.log` of the Slack user list removed. Also found: the stepper's Back/Next and the member/nonprofit buttons had no `type`, so inside the `
` they defaulted to submit — clicking Back on the confirmation step could silently create the team; all are `type="button"` now. -- **Libs/hooks:** `src/lib/teamDeliverables.js` — `deadlineState({deadlines, endDate, timezone, submissionStatus, submittedAt})` → `submitted|open|late_open|closed|event_ends|none` (+ `urgent` under 6h; `event_ends` falls back to end-of-day of `end_date` in the event tz), `deriveDeliverables` → rows slack/code/story/video/submit/devpost/completion with state `done|todo|pending|locked|optional` (**`pending`** = GitHub activity not loaded yet; never assert "No commits yet" before the fire-once IO fires), `canSubmit`, `submitBlockedReason`. `use-team-project.js` — draft vs committed, 1.5s debounced `POST /api/team//project` of changed keys only, `saveState.status` `idle|saving|saved|error|closed|unavailable` (409 `submissions_closed` → `closed`, autosave stops and the deadline/late-until render via `formatDeadlineMoment`; 404 → `unavailable` "Project writeups aren't available for this event yet."), `submit()` flushes the pending save then `POST /project/submit`; re-seeds only when `team.id` changes; the unmount flush is keyed on a real-unmount ref, NOT on the token-rotating `doSave`. `use-github-activity.js` — fire-once IO (`rootMargin 200px`), `Promise.all` over repos → ONE setState, `/activity` 404 → `unavailable` (links only). `use-public-team.js` — roster via `GET /api/messages/team/`, refetch on `visibilitychange`. `src/lib/teamDashboardApi.js` — fetch wrappers throwing `ApiError{status, body}` + `isSubmissionsClosed`/`isNotFound`/`isNotTeamMember`/`isInvalidProject`/`formatProjectErrors`. Thumbnail upload = the existing `POST /api/messages/upload-image` (`directory=teams//project`) — no signed-URL mint. Slack "Everyone's in" is a local ack (`localStorage["ohx.team..slackConfirmed"]`), not server state. Slack links via `src/lib/slackLinks.js` (`slackChannelUrl`, `KEY_CHANNELS`); repo parsing via `src/lib/githubLinks.js` (`repoEntriesFromTeam`, copied from ProblemStatement's private helpers — leave those alone). +- **Libs/hooks:** `src/lib/teamDeliverables.js` — `deadlineState({deadlines, endDate, timezone, submissionStatus, submittedAt})` → `submitted|open|late_open|closed|event_ends|none` (+ `urgent` under 6h; `event_ends` falls back to end-of-day of `end_date` in the event tz), `deriveDeliverables` → rows slack/code/story/video/submit/devpost/completion with state `done|todo|pending|locked|optional` (**`pending`** = GitHub activity not loaded yet; never assert "No commits yet" before the fire-once IO fires), `canSubmit`, `submitBlockedReason`. `use-team-project.js` — draft vs committed, 1.5s debounced `POST /api/team//project` of changed keys only, `saveState.status` `idle|saving|saved|error|closed|unavailable` (409 `submissions_closed` → `closed`, autosave stops and the deadline/late-until render via `formatDeadlineMoment`; 404 → `unavailable` "Project writeups aren't available for this event yet."), `submit()` flushes the pending save then `POST /project/submit`; re-seeds only when `team.id` changes; the unmount flush is keyed on a real-unmount ref, NOT on the token-rotating `doSave`. `use-github-activity.js` — fire-once IO (`rootMargin 200px`) over a ref OR an array of refs, `Promise.all` over repos → ONE setState, `/activity` 404 → `unavailable` (links only). **Backend excludes the repo-bootstrap account's commits** (`GITHUB_ACTIVITY_EXCLUDED_LOGINS`, default `gregv` — `create_github_repo` seeds LICENSE + README as the token owner), so a fresh repo honestly reads "No commits yet"; a team member logging in as that account is invisible here too. `use-public-team.js` — roster via `GET /api/messages/team/`, refetch on `visibilitychange`. `src/lib/teamDashboardApi.js` — fetch wrappers throwing `ApiError{status, body}` + `isSubmissionsClosed`/`isNotFound`/`isNotTeamMember`/`isInvalidProject`/`formatProjectErrors`. Thumbnail upload = the existing `POST /api/messages/upload-image` (`directory=teams//project`) — no signed-URL mint. Slack "Everyone's in" is a local ack (`localStorage["ohx.team..slackConfirmed"]`), not server state. Slack links via `src/lib/slackLinks.js` (`slackChannelUrl`, `KEY_CHANNELS`); repo parsing via `src/lib/githubLinks.js` (`repoEntriesFromTeam`, copied from ProblemStatement's private helpers — leave those alone). - **Heads-down toggle:** `MentorAvailabilityToggle` is a `radiogroup` (Open to mentors / Heads-down) → `POST /api/team//mentor-availability {open}`; optimistic, reverts on failure, hidden on 404. **`mentor_help_wanted` absent ⇒ `true`** everywhere. Signal only. - **Portal gotcha:** MUI Dialog/Snackbar content renders outside `RefinedRoot`'s subtree, so `.ohx-*` classes and `var(--x)` custom properties never reach it — the submit-confirm Dialog (and the vote page's) use plain MUI Buttons with literal-fallback sx. - **Older-backend rule:** every new call treats 404 as "feature off" — editors show "not available yet", the toggle hides, the code card shows links only, `PeerVoteCTA` stays hidden. Deploy the backend (`feat/submissions-peer-vote`) first. diff --git a/src/components/TeamDashboard/DeliverablesChecklist.js b/src/components/TeamDashboard/DeliverablesChecklist.js index 9f835219..2866bb58 100644 --- a/src/components/TeamDashboard/DeliverablesChecklist.js +++ b/src/components/TeamDashboard/DeliverablesChecklist.js @@ -112,6 +112,10 @@ function Row({ * "What your team owes" — the dashboard's primary checklist. The Submit * button here is the ONE `.ohx-btn--primary` for the whole section * (`onSubmit` opens the confirm dialog owned by `ProjectWriteupEditor`). + * + * `containerRef` is observed by `useGithubActivity` (via `TeamDashboard`) + * so the "Push code to your repo" row's GitHub check fires as soon as the + * checklist is on screen, not only when the Code activity card is. */ export default function DeliverablesChecklist({ deliverables, @@ -119,51 +123,54 @@ export default function DeliverablesChecklist({ onSlackConfirmChange, onSubmit, submitting, + containerRef, }) { const { items, done, total } = deliverables; return ( - - {DELIVERABLES_LEAD} - - - - {done} of {total} done - - -
    - {items.map((item) => ( - + + {DELIVERABLES_LEAD} + + - ))} -
-
+ + {done} of {total} done + + +
    + {items.map((item) => ( + + ))} +
+ + ); } diff --git a/src/components/TeamDashboard/TeamDashboard.js b/src/components/TeamDashboard/TeamDashboard.js index fc2b1829..749b62c6 100644 --- a/src/components/TeamDashboard/TeamDashboard.js +++ b/src/components/TeamDashboard/TeamDashboard.js @@ -38,7 +38,10 @@ export default function TeamDashboard({ teamFindingEnabled, }) { const codeCardRef = useRef(null); - const activity = useGithubActivity(team, codeCardRef); + const checklistRef = useRef(null); + // Whichever scrolls near first fires the (fire-once) GitHub fetch — the + // checklist's "Push code" row and the Code activity card read the same data. + const activity = useGithubActivity(team, [checklistRef, codeCardRef]); const projectApi = useTeamProject({ team, accessToken, onTeamUpdated }); const { users, loading: rosterLoading } = usePublicTeam(team, onTeamUpdated); @@ -120,6 +123,7 @@ export default function TeamDashboard({ onSlackConfirmChange={handleSlackConfirmChange} onSubmit={() => setConfirmOpen(true)} submitting={submitting} + containerRef={checklistRef} /> r.org && r.repo); const repoKey = repos.map((r) => normalizeRepoLink(r.link)).join(","); + const refs = Array.isArray(refOrRefs) ? refOrRefs : [refOrRefs]; useEffect(() => { - const el = ref?.current; - if (!el || repos.length === 0) { + const els = refs.map((r) => r?.current).filter(Boolean); + if (els.length === 0 || repos.length === 0) { if (repos.length === 0) setState((s) => ({ ...s, status: "ready" })); return undefined; } @@ -108,13 +115,15 @@ export default function useGithubActivity(team, ref) { }, { rootMargin: "200px" }, ); - observer.observe(el); + els.forEach((el) => observer.observe(el)); return () => { cancelled = true; observer.disconnect(); }; - }, [repoKey, ref]); + // `refs` is rebuilt every render; the effect only needs to re-run when + // the set of repos changes (refs are read at effect time). + }, [repoKey]); return state; }