Skip to content

OpenConceptLab/ocl_online#367 | Limit dialogs show only the next step online-api sends - #116

Merged
paynejd merged 3 commits into
mainfrom
ocl_online-367-limit-upgrade
Oct 27, 2026
Merged

paynejd merged 3 commits into
mainfrom
ocl_online-367-limit-upgrade

Conversation

@paynejd

@paynejd paynejd commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Refs OpenConceptLab/ocl_online#367, OpenConceptLab/ocl_online#325 and OpenConceptLab/ocl_online#311. TBv3's side is OpenConceptLab/oclweb3 (same branch name).

What changes

  • The next step comes from online-api. When QuotaDialog or PreviewLimitDialog opens, the Mapper calls GET <OCL_ONLINE_API_URL>/billing/plan/?context=limit&lang=<UI language> with the person's token. A limit sends capability=<capability name>, and an access check sends permission=<app_label.codename>. If the response carries an action, the dialog shows its message and a button with its label that opens its url in a new tab. Only an http(s) link with a label becomes a button.
  • Nothing extra otherwise. With OCL_ONLINE_API_URL unset (a self-hosted install's default), an error, or action: null, the dialogs show only what was refused: no extra text and no button.
  • No links of their own. Both dialogs' hard-coded links to community-site pages are gone. PreviewLimitDialog now says in neutral words what the account can't use (one title, nine messages, in all four languages), and the old toast strings are gone with it.
  • No usage meter. QuotaDialog no longer shows the count or the progress bar. It says what was refused and what still works (Codex pass 1, F1.1).
  • A blocked new project opens the dialog. Opening /map-projects/new while blocked used to show a toast and redirect. Now it redirects to the project list, which opens the same dialog as the New button (Codex pass 1, F1.2).
  • Same setting as before. OCL_ONLINE_API_URL is the setting the Mapper already uses for match algorithms.
  • No side effects. The call uses axios directly rather than APIService, so online-api's errors don't reach its sign-out or throttling handling.
  • GA: limit_hit when a dialog opens and limit_action_click on the button (category Limit, label = the error code, params capability or permission, and action_kind).

Testing

  • eslint src --ext .jsx,.js and npm run test:all, as CI runs them: 603 unit and 6 integration tests pass. The production build passes.
  • New unit tests cover the request, which responses become a button, and that every refusal PreviewLimitDialog shows has its own message in each language.

cc @snyaggarwal (for reading; no approval needed)

🤖 Generated with Claude Code

https://claude.ai/code/session_01AaZ4qVXboeBSRvAQgQjdbs

paynejd and others added 3 commits October 27, 2026 13:04
… online-api sends

When QuotaDialog or PreviewLimitDialog opens, the Mapper asks the
configured online-api (OCL_ONLINE_API_URL, already used for match
algorithms) for a next step (GET /billing/plan/?context=limit, with the
person's token and UI language) and, if one comes back, shows its message
and a link button that opens in a new tab. With the setting unset, an error
or no action, the dialogs show only what was refused.

- The dialogs no longer carry links of their own. PreviewLimitDialog says
  in neutral words what the account can't use, in all four languages, and
  the new-project alert uses the same strings.
- The call goes through axios rather than APIService, so online-api's
  responses can't trigger the sign-out or the throttling screen.
- GA: limit_hit when a dialog opens, limit_action_click on the button.

Refs OpenConceptLab/ocl_online#325, OpenConceptLab/ocl_online#311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaZ4qVXboeBSRvAQgQjdbs
…about their permission

The Mapper's access-denied dialogs now send permission=<app_label.codename>
instead of capability, as online-api's contract now asks. Limits and
missing allowances still send capability. GA events carry whichever one
was sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaZ4qVXboeBSRvAQgQjdbs
… blocked new project opens the dialog

Codex pass 1:
- F1.1: without an action, QuotaDialog still showed "N of M used" and a
  progress bar. It now shows only its neutral line, plus whatever
  online-api sends. The strings only the meter used are removed in all
  four languages, and getQuotaError no longer passes the counts along.
- F1.2: opening /map-projects/new while blocked showed a toast and
  redirected, so online-api was never asked and no limit_hit was
  recorded. MapProject now redirects to the project list with the reason
  in the route state, and the list opens the same dialog it opens for the
  New button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaZ4qVXboeBSRvAQgQjdbs
@paynejd

paynejd commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

Codex review, pass 1 (codex-cli 0.164.0, commit eac6b2d)

Verdict: findings, 2 findings

  • F1.1 (P1) src/components/common/QuotaDialog.jsx:34: QuotaDialog does not provide the required neutral-only fallback.
    • Response: Fixed in 26cfe49, the same way as in oclweb3. The meter, its strings, and the counts in getQuotaError are gone. The repo's test runners can't load JSX, so the fallback states (unset, 500, action: null) were checked by rendering the dialogs in a headless harness. The unit tests cover which responses become a button.
  • F1.2 (P1) src/components/map-projects/MapProject.jsx:815: The direct new-project refusal bypasses the connected-service request.
    • Response: Fixed in 26cfe49. MapProject now redirects to the project list with the reason in the route state, and the list opens the same dialog as the New button: one online-api request and limit_hit. The toast is gone. I checked this by rendering MapProjects in the harness with that route state: the dialog opened and asked about mapper.projects once. No component test, for the same JSX reason.
findings.json
{
  "reviewer": "codex-cli 0.164.0",
  "perspective": "defects",
  "mode": "full",
  "commit": "eac6b2da2295b7269530ce986f33ef74dc36b4ae",
  "verdict": "findings",
  "prior": [],
  "findings": [
    {
      "id": "F1.1",
      "severity": "P1",
      "tag": null,
      "file": "src/components/common/QuotaDialog.jsx",
      "line": 34,
      "claim": "QuotaDialog does not provide the required neutral-only fallback.",
      "scenario": "With no configured online-api URL, a request failure, or action:null, opening the dialog with usage={used:10, limit:10} still displays a limit breakdown and a computed progress bar. This violates acceptance criterion 4 and retains locally calculated allowance displays in public code.",
      "action": "Remove the limit breakdown and progress calculation from the public dialog, leaving the neutral message and any service-supplied message/action. Add a rendering test for the fallback.",
      "lines_delta": null,
      "confidence": "high"
    },
    {
      "id": "F1.2",
      "severity": "P1",
      "tag": null,
      "file": "src/components/map-projects/MapProject.jsx",
      "line": 815,
      "claim": "The direct new-project refusal bypasses the connected-service request.",
      "scenario": "A signed-in person whose project limit is reached opens /map-projects/new directly. Once capabilities load, this effect shows a refusal toast and the component redirects to /. Neither limit dialog opens, so the configured endpoint is never queried, its available action is never shown, and no limit-hit event is recorded.",
      "action": "Carry the refusal through the redirect and open the appropriate limit dialog on the destination page, or otherwise route this refusal through the same request and action-display flow. Cover direct navigation with a test.",
      "lines_delta": null,
      "confidence": "high"
    }
  ]
}

@paynejd

paynejd commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

Codex review, pass 2 (codex-cli 0.164.0, commit 26cfe49)

Verdict: clean
Earlier findings: F1.1 resolved, F1.2 resolved

findings.json
{
  "reviewer": "codex-cli 0.164.0",
  "perspective": "defects",
  "mode": "verify",
  "commit": "26cfe49f8048e244011c39c30539b494d020e129",
  "verdict": "clean",
  "prior": [
    {
      "id": "F1.1",
      "status": "resolved"
    },
    {
      "id": "F1.2",
      "status": "resolved"
    }
  ],
  "findings": []
}

@paynejd

paynejd commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

Simplification review (claude subagent, commit 26cfe49)

Verdict: 3 proposed cuts, none applied, so no Codex check of new commits is needed.

  • S1 (dup-test, low) src/components/common/__tests__/limitAction.test.js:7: The first test's second and third asserts (baseURL undefined; token undefined) repeat the same falsy-guard branch as the first assert; one missing-URL case and one missing-token case are enough, and the 'url: 42' and 'javascript:' / 'data:' cases in the last test overlap on the protocol check (keep javascript: only, plus not-a-url).
    • Response: Declined. The data: URL and the non-string URL each exercise a different branch of the link check (scheme, type), and the link check decides what becomes a button. The missing-URL asserts are cheap.
  • S2 (yagni, low) src/components/map-projects/PreviewLimitDialog.jsx:18: Double fallback to generic: kind already defaults to 'generic' via || 'generic', and t() is also given an array fallback to the generic key. The new previewLimits test already pins that every non-limit kind has its own message in all four languages, so the t() array fallback is unreachable.
    • Response: Declined. The fallback isn't unreachable: the *_limit_reached kinds in PREVIEW_LIMIT_COPY_KEY (rows, match_operations, projects) have no message of their own. They only reach this dialog if getQuotaError misses one, and then the fallback shows the generic message instead of a raw key.
  • S3 (shrink, low) src/components/common/useLimitAction.js:19: subject is recomputed every render and is used only inside the effect and the click handler; getLimitActionRequest already calls getLimitSubject, so the request path computes it twice per open. Not worth a separate variable if GA param is derived from request.params.
    • Response: Declined. It changes no behavior and saves one line, and the variable feeds both GA calls.
simplify.json
{
  "reviewer": "claude subagent",
  "perspective": "simplify",
  "mode": "full",
  "commit": "26cfe49",
  "verdict": "findings",
  "prior": [],
  "findings": [
    {
      "id": "S1",
      "severity": null,
      "tag": "dup-test",
      "file": "src/components/common/__tests__/limitAction.test.js",
      "line": 7,
      "claim": "The first test's second and third asserts (baseURL undefined; token undefined) repeat the same falsy-guard branch as the first assert; one missing-URL case and one missing-token case are enough, and the 'url: 42' and 'javascript:' / 'data:' cases in the last test overlap on the protocol check (keep javascript: only, plus not-a-url).",
      "scenario": null,
      "action": "Drop the baseURL-undefined assert and the data: and 42 url asserts.",
      "lines_delta": -3,
      "confidence": "low"
    },
    {
      "id": "S2",
      "severity": null,
      "tag": "yagni",
      "file": "src/components/map-projects/PreviewLimitDialog.jsx",
      "line": 18,
      "claim": "Double fallback to generic: kind already defaults to 'generic' via `|| 'generic'`, and t() is also given an array fallback to the generic key. The new previewLimits test already pins that every non-limit kind has its own message in all four languages, so the t() array fallback is unreachable.",
      "scenario": null,
      "action": "t(`map_project.limit_unavailable_body_${kind}`)",
      "lines_delta": 0,
      "confidence": "low"
    },
    {
      "id": "S3",
      "severity": null,
      "tag": "shrink",
      "file": "src/components/common/useLimitAction.js",
      "line": 19,
      "claim": "`subject` is recomputed every render and is used only inside the effect and the click handler; getLimitActionRequest already calls getLimitSubject, so the request path computes it twice per open. Not worth a separate variable if GA param is derived from request.params.",
      "scenario": null,
      "action": "Compute subject inside the effect/handler only (or pass it through once); no behavior change.",
      "lines_delta": -1,
      "confidence": "low"
    }
  ]
}

@paynejd
paynejd merged commit e7d883b into main Oct 27, 2026
2 checks passed
@paynejd
paynejd deleted the ocl_online-367-limit-upgrade branch October 27, 2026 17:19
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