Skip to content

Release 0.86.5 - #4272

Merged
odlbot merged 18 commits into
releasefrom
release-candidate
Nov 9, 2026
Merged

odlbot merged 18 commits into
releasefrom
release-candidate

Conversation

@odlbot

@odlbot odlbot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Carey P Gumaer

Ahtesham Quraish

Danielle Frappier

Chris Chudzicki

Tobias Macey

renovate[bot]

Anastasia Beglova

Peter Pinch

blarghmatey and others added 18 commits November 4, 2026 14:22
…roMasters id no letter (#4198)

* Return a null program letter generate URL for certificates without a MicroMasters id

/api/v0/program_certificates/ fails with NoReverseMatch for any learner
holding a certificate whose micromasters_program_id is null, because
program-letter-intercept only accepts an integer program id. Those are the
DEDP Public Policy track and the DEDP umbrella program, which only ever ran
on MITx Online and so have no MicroMasters program. In production that is
292 certificates across 282 learners, and each of them loses every letter
link on the dashboard to the 500 (Sentry MIT-LEARN-4VG).

program_letter_generate_url is now null for those certificates, and nullable
in the v0 schema. program_letter_share_url does not depend on the
MicroMasters id and is still returned.

Fixes MIT-LEARN-4VG

Claude-Session: https://claude.ai/code/session_01FGZebHWZHphHig5GqMDJnd

* Give certificates without a MicroMasters id no program letter

The previous commit stopped the 500, which exposes a second problem. A
certificate with a null micromasters_program_id (DEDP Public Policy and the
DEDP umbrella program) still got a share URL, and the letter page looks up
its content in the MicroMasters CMS by that id. requests drops a None
param, so the CMS returns every ProgramPage and the letter renders the first
one: another program's letter under the learner's name.

Until the content source for those programs is decided, they get no letter.
ProgramCertificate.has_program_letter is the rule. The certificate list
returns a null program_letter_share_url and creates no ProgramLetter for
them, and the v1 program letter endpoint 404s for letters already issued
against them. The share URL becomes nullable in the v0 schema, and both
nullable letter URLs are allowlisted for oasdiff.

Claude-Session: https://claude.ai/code/session_01FGZebHWZHphHig5GqMDJnd

* Test that a certificate with no letter shows no Program Letter item

useProgramLetterMenuItem already returns null for a missing share URL, so a
null one from the API needs no code change. This covers it so the behavior
holds if the hook changes.

Claude-Session: https://claude.ai/code/session_01FGZebHWZHphHig5GqMDJnd

* Say which certificates the share URL comment covers

Claude-Session: https://claude.ai/code/session_01FGZebHWZHphHig5GqMDJnd

* Put the revoked check in has_program_letter

ProgramLetterViewSet checked is_revoked in get_object and the missing
MicroMasters id in its queryset, so the rule for who gets a letter was split
across two places. has_program_letter now covers both and get_object is the
only check.

Claude-Session: https://claude.ai/code/session_01FGZebDFdzPzFjrHohNavU5
… spacing (#4238)

* Apply the card design tweaks to the redesigned resource card

Titles clamp at two lines and reserve no height, so a lone card is only
as tall as its title while the results grid and carousels still stretch
each card to the tallest beside it. Metadata flows in a row, stacking
only in a narrow (<=240px) card.

The card takes a list of actions, and passes the learning-path action
alongside the bookmark for learning-path editors, as the legacy card
does. A vertical card stacks them as 28px circles down the image's
corner; a horizontal card lines them up as bare 18px icons at the end of
the metadata row. Every button is a 32px box whose negative margin lays
it out at its drawn size, so the targets grow without moving anything;
a vertical button's circle is its background clipped to the content box.
A horizontal card with actions but no metadata keeps that row's height
free, so a long title or parent-course line stops above the icons. The
price panel no longer hosts a button, so its first-line reserve and
floor go, and it centres its lines with flex.

Search rows move their spacing from the results container's
`ul > li + li` rule, which outranked PlainList's `itemSpacing` and made
CardGrid undo it, onto a ResultsList: 16px apart (24px below `sm`) with
the redesign flag on, 8px as before with it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Show a resource already in a learning path by shape, not only color

On horizontal cards the learning-path action told its two states apart
only by color: the same menu-add icon in dark gray or red, which fails
WCAG 1.4.1 for color-blind users. Remix Icon's menu-add has no distinct
filled form (RiMenuAddFill renders identically), and has no checked form,
so MenuCheckIcon draws one: menu-add's three lines with the plus
replaced by the check from list-check-3, at the same stroke weight. The
card uses it when the resource is in a learning path, in both layouts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add an Openly Licensed Content section to the Terms of Service

Adds the CC BY-NC-SA 4.0 summary and the permitted-use-for-AI-training
terms as a source-neutral "Openly Licensed Content" section with an
#open-license anchor, and points the content-rights and Trademarks
sections to it.

Refs mitodl/hq#13892

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ainer build (#4259)

- python-tests sets PYTEST_ADDOPTS=--no-cov and jest runs with
  --coverage=false. Neither report had a consumer: the tokenless Codecov
  uploads (Python and JS) were rejected on main and on PR branches ("Token
  required because branch is protected") while the step reported success,
  and Codecov's newest mit-learn data is from 2024-03. Remove both upload
  steps and codecov.yml.
- Move `manage.py check`, the two migration checks and the OpenAPI spec check
  into a python-checks job, one step each, so they run beside pytest instead
  of ahead of it. collectstatic stays in python-tests as its own step.
- Delete the scripts/test/python_tests.sh wrapper and the unused
  scripts/test/test_suite.sh (it called Compose v1's `docker-compose`);
  CI calls pytest and the leaf check scripts directly.
- build-nextjs-container no longer needs javascript-tests. It shares no
  artifacts with it; the Docker build does its own install and `next build`.
- ci-gate requires python-checks.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
REDIS_VIEW_CACHE_DURATION defaulted to 24h, but the clear-views-cache beat
task wiped every cached view (a delete_pattern SCAN over the Redis keyspace
shared with the Celery broker) every hour, so the hourly clear was the real
lifetime and the TTL only mattered if beat stopped.

Set the TTL default to 1h and drop the beat entry and its task. Staleness
stays bounded at an hour, and Redis expires keys on its own instead of a
SCAN.

RedBeat removes static entries absent from beat_schedule when beat starts,
so the old clear-views-cache entry is cleaned up on deploy.
CLEAR_VIEWS_CACHE_SCHEDULE_SECONDS is no longer read; no environment set it.
QA sets REDIS_VIEW_CACHE_DURATION=0 and is unaffected.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Anchor the login and logout URL patterns

The ^login and ^logout patterns were unanchored, so any path starting with
either (e.g. /loginfoo, /login/foo) reached the login or logout view. The
gateway only requires authentication on /login (and /login/* in deployed
environments, but only /login in the local APISIX config), so a request on
another variant could reach CustomLoginView as an anonymous user, which then
redirected them to `next` without logging them in.

This anchors both patterns to an optional trailing slash, and sends an
anonymous user on any path other than LOGIN_URL to LOGIN_URL with their query
string so the gateway authenticates them.

Claude-Session: https://claude.ai/code/session_01FGZebE9MVPhw6MmBqPQzhG

* Check that every login param survives the redirect to LOGIN_URL

Claude-Session: https://claude.ai/code/session_01FGZebE9MVPhw6MmBqPQzhG
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…4247)

* Say analytics are on their way for an org or contract not loaded yet

A manager of a new organization or contract passes MITx Online's manager
check right away, but the warehouse the analytics API reads only picks the
org or contract up on its next load. Until then the API's contract gate
answered 403, which the query client throws to the error boundary as a
generic failure.

The analytics API now answers that case with a 404 whose detail.code is
organization_not_loaded or contract_not_loaded (mitodl/ol-analytics-api,
branch b2b-data-not-loaded). This reads the code and shows a notice saying
the data will be available once it has been loaded, on the analytics page
and on the contract's learners page. Any other 404 still reads as a failed
section.

Claude-Session: https://claude.ai/code/session_01FGZebDGdX8oHGMzSsqf3J9

* Disable learner export while the contract is not loaded

Export reads the same learner-progress endpoint, so for a contract the
warehouse has not loaded it 404s and reported "Could not export learners.
Please try again." under a notice saying retrying won't help.

Claude-Session: https://claude.ai/code/session_01FGZebDGdX8oHGMzSsqf3J9
…d of bootstrapping (#4236)

* Upgrade posthog-js to 1.442.0 and read flags through useFeatureFlag

Since posthog-js 1.350.0 (PostHog/posthog-js#2895), an init with
bootstrap.featureFlags never requests /flags, so the internal sentinel
useFeatureFlagsLoaded watched never cleared and flag-gated pages spun
forever on any deployed env. Stop bootstrapping, and fall back to env
defaults in a hook of our own instead.

Add `useFeatureFlag(flag)`, which reads posthog-js through
`useSyncExternalStore`:
- During SSR and hydration it returns the flag's `NEXT_PUBLIC_FEATURE_*`
  env default ("true" in any case is on; anything else, unset included,
  is off).
- In the browser it returns PostHog's value when PostHog has one (this
  load's /flags, or flags stored from an earlier visit), and the env
  default otherwise: before PostHog answers, when the request fails, and
  for a flag PostHog doesn't return (disabled, deleted, never created, or
  server-only / limited to evaluation contexts, which PostHog leaves out
  of browser responses). So disabling a flag in PostHog hands it back to
  its env default; a 0% rollout, which PostHog returns as off, is how to
  switch one off. This matches `useFeatureFlagEnabled(flag, envDefault)`
  outside hydration.
- Passing the env default as `useFeatureFlagEnabled`'s `defaultValue`
  doesn't avoid the mismatch below: it applies only while posthog-js has
  no value.
- It always returns a boolean.

Not posthog-js/react's `useFeatureFlagEnabled`: it takes its first value
from `useState(() => client.isFeatureEnabled(flag))`, with no server
snapshot. A component that hydrates after posthog-js has flags (a
`<Suspense>` boundary that hydrates after the provider's effect, or
streams in after `/flags` answers) renders PostHog's value where the
server rendered the default, and React throws away and client-renders
that boundary. Reproduced on Next 16.4.4 / React 19.3.1 / posthog-js
1.442.0: returning visitors mismatched in plain and streamed Suspense
boundaries, first visits in streamed ones, and with init in
instrumentation-client.ts the whole shell mismatched. A
`useSyncExternalStore` reader hydrated cleanly in every position.

- Pin posthog-js to 1.442.0 (with @posthog/browser-common 0.10.1,
  core 1.61.2, types 1.418.0), so later versions arrive as their own
  Renovate PRs rather than in lockfile maintenance, and drop the
  Renovate freeze from #4230.
- useFeatureFlagsLoaded reads a context ConfiguredPostHogProvider sets.
  It turns true once flags are final for the identity the page ends up
  with: the first reloadFeatureFlagsAsync() after init, then /me, then
  the reload identify (new distinct id) or reset starts. A `cancelled`
  result (posthog-js resetting for its own identity change) waits on the
  next load. A 10s cap ends the wait with a console.warn. Without an api
  key it is always true. It never flips back.
- The provider keeps the current identity's flag load in a ref and acts
  on a load only while it is still current. So an anonymous answer that
  lands after a /me refetch has identified the visitor doesn't mark flags
  loaded (Copilot), and a /me refetch that keeps the identity (new
  profile data, so syncIdentity returns null) keeps waiting on that
  identity's pending load rather than the page's first one (Codex).
  Strict Mode's effect replay keeps a load the identity effect already
  set (Copilot).
- posthog.init moves into bootstrap/posthog.ts, which
  instrumentation-client.ts calls (in its own try/catch) before
  hydration. posthog-js tells onFeatureFlags subscribers nothing when
  init makes stored flags readable, so with init in the provider's
  effect, components that subscribed first kept their env default until
  /flags answered. Before hydration, every read sees them; identify
  can't run before init either. bootstrapPostHog starts the first flag
  load and the provider adopts it (checked in a browser that the two
  share the module). PosthogIdentifier folds into the provider.
- No PostHog inside an iframe (window.self !== window.top): third-party
  cookies are blocked or partitioned there, and the embed shows none of
  our privacy notices. Embeds on other sites get no PostHog, as before;
  an embed URL opened directly in a tab now does. With an api key but
  posthog not started, the provider still hydrates with flags not
  loaded (the server can't tell), then marks them loaded.
- init: no bootstrap; remote_config_refresh_interval_ms: 0 (no 5-minute
  refresh, as on 1.297); feature_flag_request_max_retries: 0 (give up
  after 3s, as on 1.297, rather than 6.5s per request, which on a first
  load after login would hold gated pages until the 10s cap).
- Calling reloadFeatureFlagsAsync() right after init sends /flags with
  config.js instead of after it; posthog-js's own load then skips, so
  it's still one request.
- Lint: ban posthog-js/react's flag hooks, as a `patterns` entry so it
  survives #4062's usePostHog `paths` entry for the same module.
- Delete EnvFlagsProvider, getBootstrapFeatureFlags, parseFlagValue and
  INTERNAL_BOOTSTRAPPING_FLAG.

Gates keep their notFound()/ForbiddenError shape: they now throw only
after flags are final, so Next's sticky not-found boundary only matters
when a flag changes mid-page or the cap fires.

The call sites move to useFeatureFlag in the next commit, so this one
fails lint, typecheck and the call-site tests on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Route call sites through useFeatureFlag

Mechanical follow-through on the previous commit, in 63 files:
- Swap all 37 `useFeatureFlagEnabled` reads (28 files) for
  `useFeatureFlag`, and point the 34 call-site tests' mocks at
  `@/common/useFeatureFlag` instead of `posthog-js/react`.
- `useFeatureFlag` always returns a boolean, so the `=== true` /
  `!== true` comparisons and `!!` coercions on flag values go, as do
  the test mocks' `undefined` "not loaded" values and
  ContractAdminPage's "flag is absent" case, which is now the same as
  its "flag is disabled" case.
- Comments that described a bootstrapped `false` or an `undefined`
  flag now describe the env default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…#4265)

Cancel now just closes the dialog (or returns to dashboard home from the
contract page) without sending consented: false to MITx Online. Only
"Agree and continue" records a response.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ds (#4240)

* Show run-specific actions only in run rows on multi-run dashboard cards

When a card has a Course runs accordion and the multiple-run-context-menus
flag is on, the card's root menu drops Email Settings, Unenroll and Receipt;
each run row's menu still offers them. Single-run cards are unchanged.

Refs mitodl/hq#14234

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Reset the per-run menus flag mock before each card test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Remove the multiple-run-context-menus flag

It is rolled out to everyone, so the per-run row menus, the run label in the
email settings and unenroll dialogs, and the card menu change are now
unconditional.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Wait for the receipt lookup before asserting the card omits Receipt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Actually hide a dashboard card menu button marked hidden

The button's display: inline-flex overrode the browser's [hidden] rule,
so an empty card menu still showed its trigger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
)

On a wide screen the title column ran the full container width, so a long
department name and the Follow pill after it were drawn over the banner
photograph. Over a photograph, from md up, the title area is now held to the
distance between the column's left edge and the photograph's, measured in
container query units against the hero so it tracks the photo's placement at
every width.

Co-authored-by: Ahtesham Quraish <ahtesham.quraish@arbisoft.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Stop the resource card provider writing to the query cache during render

useLearningResourcesBulkList wrote every resource into its detail query
from inside `select`, which runs during render and, being inline, on every
render. LearningResourceProvider read those detail queries back through
useQueries, so each write re-rendered it and the render wrote again. With
react-query 5.105 the loop often runs until React gives up, so news and
article pages with several cards hit "Maximum update depth
exceeded" and showed the error page.

The provider now builds its map from the bulk list's own data, and
`select` only picks out the results.

Fixes #4268
Fixes MIT-LEARN-4ZT

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep editor cards on screen while an added card loads, or if it fails

Adding a card changes the bulk list's query key. The provider now keeps
the resources earlier lists returned, so the cards already on the canvas
stay put while the new list loads and if that request fails. Loading is
worked out per card, so only the new card shows a skeleton.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Check that only the added card shows a skeleton while its list loads

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Derive card loading from the list query's isLoading, not isFetching

`isFetching` is also true during a background refetch of a list that
already has data, so a card whose resource the list no longer returns
would flash a skeleton on every refetch. The query's `isLoading` (pending
and fetching) is true only while a key has no data yet, which is the
added-card case the context needs.

The failure test waited a fixed 100 ms, which also passes while the
request is still pending. It now waits for the added card to settle with
no skeleton, which only happens once the request has failed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Keep the previous list as placeholder data while an added card's list loads

Adding a card in the editor changes the bulk list's key. The provider asks
the hook for `keepPreviousData`, so the previous list stands in until the
new one loads and the provider no longer needs its own copy of earlier
results. Placeholder data reports `success`, so the provider counts a card
the placeholder lacks as loading.

The "keeps the cards when the list fails" test goes with the state it
covered: once the request has failed after its retries, the cards blank
until the next refetch, like any other query.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Chris Chudzicki <christopher.chudzicki@gmail.com>
@odlbot
odlbot requested a review from a team as a code owner November 6, 2026 16:03
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

OpenAPI Changes

2 changes: 2 error, 0 warning, 0 info

View full changelog

Unexpected changes? Ensure your branch is up-to-date with main (consider rebasing).

@odlbot
odlbot merged commit 700c6ec into release Nov 9, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants