Skip to content

Add embeddable admin UI plan: Leptos vs React analysis and implementation roadmap#918

Open
knutties wants to merge 4 commits intomainfrom
claude/embeddable-admin-ui-yqliC
Open

Add embeddable admin UI plan: Leptos vs React analysis and implementation roadmap#918
knutties wants to merge 4 commits intomainfrom
claude/embeddable-admin-ui-yqliC

Conversation

@knutties
Copy link
Copy Markdown
Collaborator

@knutties knutties commented Mar 16, 2026

https://claude.ai/code/session_01MQDdbiwrn4RSwde6qAoNGE

Summary by CodeRabbit

  • New Features
    • Introduced embeddable React-based admin interface for managing configurations, dimensions, context overrides, and experiments
    • Added audit log viewer with filtering capabilities
    • Implemented JSON configuration and context visualization tools
    • Created pagination and modal dialogs for data management
    • Added multi-tab dashboard interface for administrative tasks

claude added 2 commits March 16, 2026 17:25
- Phase 1: TypeScript API client for all Superposition REST endpoints
  (dimensions, configs, overrides, experiments, resolve, audit)
- Phase 2: Core React components (Table, Modal, Pagination, FormField,
  AlertBar, ConditionBadges, StatusBadge, JsonViewer)
- Phase 3: Feature pages (ConfigManager, DimensionManager, OverrideManager,
  ExperimentManager, ResolveView, AuditLog, SuperpositionAdmin)
- Phase 4: Context scoping - filter overrides/experiments by scoped context,
  auto-merge scoped dimensions on create, locked dimension indicators
- Full test suite: 57 tests across 10 files (API client, context filtering,
  providers, components, pages)
- TypeScript strict mode, Vitest, React Testing Library

https://claude.ai/code/session_01MQDdbiwrn4RSwde6qAoNGE
@knutties knutties requested a review from a team as a code owner March 16, 2026 19:20
@semanticdiff-com
Copy link
Copy Markdown

semanticdiff-com bot commented Mar 16, 2026

Review changes with  SemanticDiff

Changed Files
File Status
  PLAN-embeddable-admin-ui.md Unsupported file format
  clients/admin-ui/.prettierrc Unsupported file format
  clients/admin-ui/__tests__/api/client.test.ts  0% smaller
  clients/admin-ui/__tests__/api/context-filter.test.ts  0% smaller
  clients/admin-ui/__tests__/api/dimensions.test.ts  0% smaller
  clients/admin-ui/__tests__/components/Modal.test.tsx  0% smaller
  clients/admin-ui/__tests__/components/Pagination.test.tsx  0% smaller
  clients/admin-ui/__tests__/components/Table.test.tsx  0% smaller
  clients/admin-ui/__tests__/pages/ConfigManager.test.tsx  0% smaller
  clients/admin-ui/__tests__/pages/OverrideManager.test.tsx  0% smaller
  clients/admin-ui/__tests__/providers/AlertProvider.test.tsx  0% smaller
  clients/admin-ui/__tests__/providers/SuperpositionProvider.test.tsx  0% smaller
  clients/admin-ui/__tests__/setup.ts  0% smaller
  clients/admin-ui/demo/index.html  0% smaller
  clients/admin-ui/demo/vite.config.ts  0% smaller
  clients/admin-ui/package-lock.json  0% smaller
  clients/admin-ui/package.json  0% smaller
  clients/admin-ui/src/api/audit.ts  0% smaller
  clients/admin-ui/src/api/client.ts  0% smaller
  clients/admin-ui/src/api/default-configs.ts  0% smaller
  clients/admin-ui/src/api/dimensions.ts  0% smaller
  clients/admin-ui/src/api/experiments.ts  0% smaller
  clients/admin-ui/src/api/index.ts  0% smaller
  clients/admin-ui/src/api/overrides.ts  0% smaller
  clients/admin-ui/src/api/resolve.ts  0% smaller
  clients/admin-ui/src/components/AlertBar.tsx  0% smaller
  clients/admin-ui/src/components/ConditionBadges.tsx  0% smaller
  clients/admin-ui/src/components/FormField.tsx  0% smaller
  clients/admin-ui/src/components/JsonViewer.tsx  0% smaller
  clients/admin-ui/src/components/Modal.tsx  0% smaller
  clients/admin-ui/src/components/Pagination.tsx  0% smaller
  clients/admin-ui/src/components/StatusBadge.tsx  0% smaller
  clients/admin-ui/src/components/Table.tsx  0% smaller
  clients/admin-ui/src/components/index.ts  0% smaller
  clients/admin-ui/src/hooks/index.ts  0% smaller
  clients/admin-ui/src/hooks/useApi.ts  0% smaller
  clients/admin-ui/src/index.ts  0% smaller
  clients/admin-ui/src/pages/AuditLog.tsx  0% smaller
  clients/admin-ui/src/pages/ConfigManager.tsx  0% smaller
  clients/admin-ui/src/pages/DimensionManager.tsx  0% smaller
  clients/admin-ui/src/pages/ExperimentManager.tsx  0% smaller
  clients/admin-ui/src/pages/OverrideManager.tsx  0% smaller
  clients/admin-ui/src/pages/ResolveView.tsx  0% smaller
  clients/admin-ui/src/pages/SuperpositionAdmin.tsx  0% smaller
  clients/admin-ui/src/pages/index.ts  0% smaller
  clients/admin-ui/src/providers/AlertProvider.tsx  0% smaller
  clients/admin-ui/src/providers/SuperpositionProvider.tsx  0% smaller
  clients/admin-ui/src/providers/index.ts  0% smaller
  clients/admin-ui/src/types/api.ts  0% smaller
  clients/admin-ui/src/types/index.ts  0% smaller
  clients/admin-ui/src/utils/context-filter.ts  0% smaller
  clients/admin-ui/src/utils/format.ts  0% smaller
  clients/admin-ui/src/utils/index.ts  0% smaller
  clients/admin-ui/tsconfig.json  0% smaller
  clients/admin-ui/vite.config.ts  0% smaller
  clients/admin-ui/vitest.config.ts  0% smaller

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 16, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 52966996-175c-456f-a379-31076a060f76

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This pull request introduces a complete React-based embeddable admin UI package for Superposition, including an HTTP API client, context providers for state management, reusable UI components, feature pages for managing configurations/dimensions/experiments/overrides/audits, comprehensive TypeScript typing, utility functions, and full test coverage with configuration for build and testing infrastructure.

Changes

Cohort / File(s) Summary
Planning Documentation
PLAN-embeddable-admin-ui.md
Outlines architecture rationale for React SDK embedding, context scoping behavior, phased implementation plan, and long-term embedding strategy.
Core API Client
src/api/client.ts
Implements SuperpositionClient with request orchestration, header/query building, error handling (SuperpositionApiError), and HTTP method convenience functions (get, post, patch, put, delete).
API Modules
src/api/dimensions.ts, src/api/default-configs.ts, src/api/experiments.ts, src/api/overrides.ts, src/api/resolve.ts, src/api/audit.ts, src/api/index.ts
Factory functions for CRUD/state operations on dimensions, configs, experiments, overrides, config resolution, and audit logs; all delegate to SuperpositionClient with appropriate HTTP verbs.
React Providers
src/providers/SuperpositionProvider.tsx, src/providers/AlertProvider.tsx, src/providers/index.ts
SuperpositionProvider initializes SuperpositionClient and exposes APIs; AlertProvider manages auto-dismissing alerts with useAlerts hook.
UI Components
src/components/Modal.tsx, src/components/Pagination.tsx, src/components/Table.tsx, src/components/FormField.tsx, src/components/AlertBar.tsx, src/components/ConditionBadges.tsx, src/components/StatusBadge.tsx, src/components/JsonViewer.tsx, src/components/index.ts
Reusable presentational components (modal dialogs, pagination controls, data tables, form fields, alert display, styled badges) with consistent inline styling.
Page Managers
src/pages/ConfigManager.tsx, src/pages/DimensionManager.tsx, src/pages/OverrideManager.tsx, src/pages/ExperimentManager.tsx, src/pages/ResolveView.tsx, src/pages/AuditLog.tsx, src/pages/SuperpositionAdmin.tsx, src/pages/index.ts
Feature pages for CRUD operations on configurations, dimensions, overrides, experiments, audit log viewing, config resolution, and a tab-based admin dashboard orchestrating all managers.
Utilities
src/utils/context-filter.ts, src/utils/format.ts, src/utils/index.ts
Context scoping and filtering logic for experiments/overrides; formatting utilities for dates, JSON preview, and condition descriptions.
Type Definitions
src/types/api.ts, src/types/index.ts
Comprehensive TypeScript interfaces and unions (Dimension, DefaultConfig, ContextOverride, Experiment, Config, AuditLogEntry, etc.) with request/response shapes and filters.
Custom Hooks
src/hooks/useApi.ts, src/hooks/index.ts
useApi for data fetching with loading/error state; useMutation for mutations with loading/error tracking.
Tests
__tests__/api/client.test.ts, __tests__/api/context-filter.test.ts, __tests__/api/dimensions.test.ts, __tests__/components/*.test.tsx, __tests__/pages/*.test.tsx, __tests__/providers/*.test.tsx, __tests__/setup.ts
Comprehensive unit test coverage for API client, utilities, components (Modal, Pagination, Table), and page managers using Vitest and React Testing Library.
Package Configuration
package.json, .prettierrc, tsconfig.json, vite.config.ts, vitest.config.ts
Build/dev/test scripts, linting/formatting config, TypeScript strict mode, Vite library bundling (ES/CJS exports with external React), and Vitest test environment setup.
Main Exports
src/index.ts
Centralized public API barrel exporting all client, providers, pages, components, hooks, utilities, and types.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Page as Manager Page
    participant Hook as useApi/useMutation
    participant Provider as SuperpositionProvider
    participant Client as SuperpositionClient
    participant API as API Module
    participant Server

    User->>Page: Interact (create/fetch/delete)
    Page->>Hook: Call useApi/useMutation
    Hook->>Provider: useSuperposition()
    Provider-->>Hook: Return client + API modules
    Hook->>API: Call module method
    API->>Client: request(method, path, body/query)
    Client->>Client: Build headers, query string
    Client->>Server: HTTP Request
    Server-->>Client: Response
    Client->>API: Resolve/throw error
    API-->>Hook: Return typed data
    Hook->>Page: Update state (data/loading/error)
    Page->>User: Render UI + alerts
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Suggested labels

enhancement, UI

Suggested reviewers

  • ayushjain17
  • Datron
  • sauraww

Poem

🐰 A React realm hops to life today,
With APIs and providers leading the way,
Components reusable, managers galore,
Types keep us safe—what more could we adore?
From client to page, the patterns align,
An admin UI that sparkles and shines!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add embeddable admin UI plan: Leptos vs React analysis and implementation roadmap' accurately describes the main change in the pull request, which adds a planning document outlining a strategy for creating an embeddable admin UI SDK with analysis of Leptos vs React approaches and a phased implementation roadmap, supported by foundational code for an embeddable React admin UI SDK.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/embeddable-admin-ui-yqliC
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can use Trivy to scan for security misconfigurations and secrets in Infrastructure as Code files.

Add a .trivyignore file to your project to customize which findings Trivy reports.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (13)
clients/admin-ui/src/components/Pagination.tsx-39-43 (1)

39-43: ⚠️ Potential issue | 🟡 Minor

Set explicit type="button" on pagination controls.

Without explicit type, these buttons default to submit inside forms and can trigger accidental form submission.

🔧 Suggested fix
       <button
+        type="button"
         style={btnStyle}
         disabled={currentPage <= 1}
         onClick={() => onPageChange(currentPage - 1)}
       >
@@
         <button
+          type="button"
           key={p}
           style={p === currentPage ? activeBtnStyle : btnStyle}
           onClick={() => onPageChange(p)}
         >
@@
       <button
+        type="button"
         style={btnStyle}
         disabled={currentPage >= totalPages}
         onClick={() => onPageChange(currentPage + 1)}
       >

Also applies to: 47-51, 55-59

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/components/Pagination.tsx` around lines 39 - 43, The
pagination button elements in the Pagination component default to type="submit"
when rendered inside a form; update each button (the previous/next/page number
buttons that use props/currentPage and the onPageChange handler and btnStyle) to
explicitly include type="button" to prevent accidental form submission—add
type="button" to the button that calls onPageChange(currentPage - 1), the one
that calls onPageChange(currentPage + 1), and any page-number buttons that
invoke onPageChange.
clients/admin-ui/src/components/AlertBar.tsx-35-39 (1)

35-39: ⚠️ Potential issue | 🟡 Minor

Set type="button" on the dismiss control.

This avoids accidental form submission when the component is rendered inside a form subtree.

🔧 Suggested fix
           <button
+            type="button"
             onClick={() => removeAlert(alert.id)}
             style={{ background: "none", border: "none", cursor: "pointer", padding: "0 0 0 12px", fontSize: 16 }}
             aria-label="Dismiss"
           >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/components/AlertBar.tsx` around lines 35 - 39, The
dismiss button in AlertBar is missing an explicit type which can cause
accidental form submission when used inside a form; update the button element
(the control that calls removeAlert(alert.id) in the AlertBar component) to
include type="button" so it behaves as a non-submit button and preserves the
existing onClick, styles, aria-label, and removeAlert usage.
PLAN-embeddable-admin-ui.md-46-71 (1)

46-71: ⚠️ Potential issue | 🟡 Minor

Specify a language for the fenced code block.

The architecture tree block should declare a fence language to satisfy markdown linting (MD040).

📝 Suggested fix
-```
+```text
 `@anthropic/superposition-admin-ui` (npm package)
 ...
-```
+```
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@PLAN-embeddable-admin-ui.md` around lines 46 - 71, Add a language identifier
to the fenced code block that starts with "@anthropic/superposition-admin-ui
(npm package)" in PLAN-embeddable-admin-ui.md (e.g., change ``` to ```text) so
the markdown linter MD040 passes; update the opening fence only and leave the
block contents intact.
clients/admin-ui/src/components/JsonViewer.tsx-11-13 (1)

11-13: ⚠️ Potential issue | 🟡 Minor

Handle potential JSON.stringify errors for circular references or BigInt.

JSON.stringify throws a TypeError for circular references or BigInt values. If data contains such values, this component will crash.

🛡️ Proposed defensive fix
+function safeStringify(data: unknown, space?: number): string {
+  try {
+    return JSON.stringify(data, null, space);
+  } catch {
+    return "[Unable to display]";
+  }
+}
+
 export function JsonViewer({ data, collapsed = true }: JsonViewerProps) {
   const [expanded, setExpanded] = useState(!collapsed);

-  const formatted = JSON.stringify(data, null, 2);
-  const preview = JSON.stringify(data);
+  const formatted = safeStringify(data, 2);
+  const preview = safeStringify(data);
   const isLong = preview.length > 60;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/components/JsonViewer.tsx` around lines 11 - 13, The
JSON.stringify calls for formatted and preview can throw on circular refs or
BigInt; update the JsonViewer component to use a safe stringify strategy: wrap
the current const formatted, preview, and isLong logic in a try/catch and on
error fall back to a safe serializer (e.g., a replacer that converts BigInt to
string and a WeakSet-based circular-reference handler) or a simple toString
fallback, then compute isLong from the safe preview; ensure the new logic
replaces the existing `formatted`, `preview`, and `isLong` definitions so the
component no longer crashes when `data` contains BigInt or circular references.
clients/admin-ui/__tests__/pages/OverrideManager.test.tsx-57-71 (1)

57-71: ⚠️ Potential issue | 🟡 Minor

Test name and assertion mismatch: list rendering is not actually validated.

Line 57 through Line 71 claims to verify override list rendering, but it only checks the static heading "Context Overrides". Please assert at least one row-level value from mockOverrides.data so the test fails when list rendering breaks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/__tests__/pages/OverrideManager.test.tsx` around lines 57 -
71, The test "renders override list" currently only checks the static heading;
update the test for OverrideManager to assert a row-level value from
mockOverrides.data (e.g., the first override's name or id) so list rendering is
actually validated—use the same SuperpositionProvider/AlertProvider render and
then await screen.findByText or waitFor + expect(screen.getByText(<first
mockOverrides.data item>)).toBeInTheDocument() to ensure a specific override row
is present (and update the test name if desired to reflect that it verifies list
rows).
clients/admin-ui/src/pages/ConfigManager.tsx-67-85 (1)

67-85: ⚠️ Potential issue | 🟡 Minor

Use the caught exception for error alerts.

Current catches ignore err and rely on mutation state, which can be stale and also masks JSON parse errors from JSON.parse.

Suggested fix
-    } catch {
-      addAlert("error", createMutation.error || "Failed to create config");
+    } catch (err) {
+      addAlert("error", err instanceof Error ? err.message : String(err));
@@
-    } catch {
-      addAlert("error", deleteMutation.error || "Failed to delete config");
+    } catch (err) {
+      addAlert("error", err instanceof Error ? err.message : String(err));
     }

Also applies to: 88-95

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/pages/ConfigManager.tsx` around lines 67 - 85, The
try/catch currently ignores the caught exception and uses mutation state which
can be stale and misses JSON.parse errors; update handleCreate (and the
analogous handleUpdate) to (1) accept the caught error parameter (e.g., catch
(err)), (2) call createMutation.mutateAsync(...) instead of
createMutation.mutate so thrown errors propagate to the try/catch, and (3) pass
the actual error message/object to addAlert (e.g., addAlert("error",
createMutation.error || err?.message || String(err))). Ensure the JSON.parse
calls remain inside the try so parse errors are caught and surfaced via the
alert.
clients/admin-ui/src/pages/ResolveView.tsx-24-37 (1)

24-37: ⚠️ Potential issue | 🟡 Minor

Clear stale result state on new/failed resolves.

If resolve fails, previous successful data remains visible, which is misleading.

Suggested fix
   const handleResolve = async () => {
+    setResult(null);
     setLoading(true);
@@
     } catch (err) {
+      setResult(null);
       addAlert("error", err instanceof Error ? err.message : String(err));

Also applies to: 73-78

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/pages/ResolveView.tsx` around lines 24 - 37, Before
starting a new resolve attempt, clear any previous result and ensure failures
also clear it so stale data isn't shown; in handleResolve setResult(undefined)
immediately before setLoading(true) (or right after) and also
setResult(undefined) inside the catch block when an error occurs, and apply the
same changes to the other resolve handler in this file that calls
resolve.resolve so both new attempts and failed resolves clear prior results.
clients/admin-ui/src/pages/ResolveView.tsx-27-31 (1)

27-31: ⚠️ Potential issue | 🟡 Minor

Validate Additional Context is a JSON object before merge.

JSON.parse accepts primitives/arrays, but this flow expects a key-value object context.

Suggested fix
-      const ctx = mergeScopedContext(
-        JSON.parse(contextInput),
-        scopedContext,
-      );
+      const parsed = JSON.parse(contextInput);
+      if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
+        throw new Error("Additional Context must be a JSON object");
+      }
+      const ctx = mergeScopedContext(parsed, scopedContext);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/pages/ResolveView.tsx` around lines 27 - 31, Before
calling mergeScopedContext and resolve.resolve, ensure the parsed contextInput
yields a plain object: parse contextInput, verify the result is non-null, typeof
=== "object" and not Array.isArray(result); if the check fails, surface a clear
validation error (or fallback to an empty object) rather than passing
primitives/arrays into mergeScopedContext. Update the code around contextInput,
mergeScopedContext, scopedContext and the use before calling resolve.resolve to
perform this validation and handle the invalid-case path.
clients/admin-ui/src/utils/format.ts-4-9 (1)

4-9: ⚠️ Potential issue | 🟡 Minor

Invalid datetime inputs bypass the intended fallback.

new Date(invalid).toLocaleString() does not throw; it returns "Invalid Date", so catch never runs.

Suggested fix
 export function formatDateTime(iso: string): string {
-  try {
-    return new Date(iso).toLocaleString();
-  } catch {
-    return iso;
-  }
+  const d = new Date(iso);
+  return Number.isNaN(d.getTime()) ? iso : d.toLocaleString();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/utils/format.ts` around lines 4 - 9, formatDateTime
currently assumes new Date(iso).toLocaleString() will throw on bad input, but
Date returns an "Invalid Date" object instead; update formatDateTime to
instantiate the Date (use new Date(iso)), check validity (e.g.,
isNaN(date.getTime()) or date.toString() === "Invalid Date") and if invalid
return the original iso string, otherwise return date.toLocaleString(); keep the
try/catch only if you still want to guard against unexpected errors but ensure
the explicit validity check is used (reference function name: formatDateTime).
clients/admin-ui/src/pages/OverrideManager.tsx-108-144 (1)

108-144: ⚠️ Potential issue | 🟡 Minor

Stale closure: handleDelete is not in the dependency array.

The columns memoization uses handleDelete in the render function (line 134) but the dependency array only includes [lockedDims]. This can lead to stale closures where handleDelete captures outdated values of deleteMutation or refetch.

🔧 Proposed fix
+  const handleDelete = useCallback(async (id: string) => {
+    if (!confirm("Delete this override?")) return;
+    try {
+      await deleteMutation.mutate(id);
+      refetch();
+    } catch {
+      addAlert("error", deleteMutation.error || "Failed to delete");
+    }
+  }, [deleteMutation, refetch, addAlert]);

   const columns: Column<ContextOverride>[] = useMemo(
     () => [
       // ... columns
     ],
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    [lockedDims],
+    [lockedDims, handleDelete],
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/pages/OverrideManager.tsx` around lines 108 - 144, The
useMemo for columns creates a stale closure because the render uses handleDelete
but the dependency array only lists lockedDims; update the memoization so
columns sees the latest handleDelete by either adding handleDelete to the
useMemo dependency array or make handleDelete stable (wrap delete logic that
uses deleteMutation/refetch in a useCallback) and then include that stable
function in the dependency list for columns; reference the columns constant, the
handleDelete function, and the deleteMutation/refetch callers when changing
dependencies.
clients/admin-ui/src/pages/ExperimentManager.tsx-141-196 (1)

141-196: ⚠️ Potential issue | 🟡 Minor

Stale closure: handleRamp is not in the dependency array.

The columns memoization uses handleRamp in the render function (line 186) but the dependency array only includes [lockedDims]. Since handleRamp references rampMutation and refetch, it could become stale. Consider wrapping handleRamp in useCallback and adding it to the dependencies, or moving the handler outside the memoized columns.

🔧 Proposed fix
+  const handleRamp = useCallback(async (id: string) => {
+    const input = prompt("Enter traffic percentage (0-100):");
+    if (input === null) return;
+    const pct = parseInt(input, 10);
+    if (isNaN(pct) || pct < 0 || pct > 100) {
+      addAlert("error", "Invalid percentage");
+      return;
+    }
+    try {
+      await rampMutation.mutate(id, pct);
+      refetch();
+    } catch {
+      addAlert("error", rampMutation.error || "Failed to ramp");
+    }
+  }, [rampMutation, refetch, addAlert]);

   const columns: Column<Experiment>[] = useMemo(
     () => [
       // ... columns
     ],
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    [lockedDims],
+    [lockedDims, handleRamp],
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/pages/ExperimentManager.tsx` around lines 141 - 196, The
columns useMemo captures handleRamp in its render but doesn't list it in the
dependency array, which can cause a stale closure because handleRamp depends on
rampMutation and refetch; fix by either wrapping handleRamp in useCallback
(including rampMutation and refetch in its dependencies) and then adding that
memoized handleRamp to the columns dependency array, or move the onClick handler
logic out of the columns definition so columns only depend on lockedDims; update
the columns useMemo dependency array accordingly to include handleRamp if you
choose the useCallback approach.
clients/admin-ui/src/pages/DimensionManager.tsx-67-86 (1)

67-86: ⚠️ Potential issue | 🟡 Minor

JSON.parse error will produce a confusing error message.

When JSON.parse(newSchema) fails on Line 72, the error is caught but Line 84 uses createMutation.error which may be null (since the JSON parsing failure happens before the mutation runs). This results in the generic "Failed to create dimension" message rather than indicating the actual JSON syntax error.

🛠️ Proposed fix: catch and display JSON parse errors explicitly
   const handleCreate = async () => {
+    let schema;
+    try {
+      schema = JSON.parse(newSchema);
+    } catch (e) {
+      addAlert("error", "Invalid JSON schema");
+      return;
+    }
     try {
       await createMutation.mutate({
         dimension: newName,
         position: parseInt(newPosition, 10),
-        schema: JSON.parse(newSchema),
+        schema,
         description: newDesc || "No description",
         change_reason: newReason || "Created via admin UI",
       });
       setShowCreate(false);
       // ... rest of reset logic
       refetch();
     } catch {
       addAlert("error", createMutation.error || "Failed to create dimension");
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/pages/DimensionManager.tsx` around lines 67 - 86, The
handleCreate function currently calls JSON.parse(newSchema) inline so a syntax
error is caught by the outer try but the alert uses createMutation.error (which
may be null), producing a misleading message; fix by parsing the schema into a
variable before invoking createMutation and explicitly catching JSON.parse
errors: attempt JSON.parse(newSchema) in its own try/catch (or validate), and if
it throws call addAlert("error", parseError.message) and return early; only
after successful parse call createMutation.mutate(...) and keep the existing
catch to report createMutation.error or a fallback message for mutation
failures.
clients/admin-ui/src/pages/SuperpositionAdmin.tsx-38-46 (1)

38-46: ⚠️ Potential issue | 🟡 Minor

Edge case: empty features array results in rendering hidden content.

If config.features is set to an empty array [], visibleTabs will be empty and no tabs will render. However, activeTab falls back to "config" and ActiveComponent will still render ConfigManager, creating a disconnected UI state.

Consider handling the empty tabs case explicitly:

🛠️ Proposed fix: handle empty visible tabs
   const visibleTabs = features
     ? allTabs.filter((t) => features.includes(t.id))
     : allTabs;

+  if (visibleTabs.length === 0) {
+    return (
+      <div style={{ padding: 20, color: "#6b7280" }}>
+        No features enabled. Check your configuration.
+      </div>
+    );
+  }
+
   const [activeTab, setActiveTab] = useState<Tab>(
-    visibleTabs.find((t) => t.id === defaultTab)?.id ?? visibleTabs[0]?.id ?? "config",
+    visibleTabs.find((t) => t.id === defaultTab)?.id ?? visibleTabs[0].id,
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/pages/SuperpositionAdmin.tsx` around lines 38 - 46,
visibleTabs can be empty when features is an empty array, but activeTab
currently defaults to "config" and ActiveComponent renders regardless, producing
a disconnected UI; update the logic so that visibleTabs being empty is handled
explicitly: compute visibleTabs as now, but set activeTab to visibleTabs[0]?.id
?? null (and update the generic type if needed), add an effect that resets
setActiveTab whenever visibleTabs changes (e.g., to visibleTabs[0]?.id ?? null),
and guard rendering of ActiveComponent (from tabComponents[activeTab]) so it
only renders when activeTab is non-null and exists in tabComponents; reference
visibleTabs, activeTab, setActiveTab, ActiveComponent, and tabComponents when
making these changes.
🧹 Nitpick comments (24)
clients/admin-ui/tsconfig.json (1)

12-12: Avoid emitting test declarations from the library tsconfig.

With declaration: true and include: ["src", "__tests__"], tsc can emit test .d.ts files into dist. Prefer keeping this config build-focused (src only) and moving test typing to a separate test tsconfig.

♻️ Minimal change
-  "include": ["src", "__tests__"],
+  "include": ["src"],

Also applies to: 26-27

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/tsconfig.json` at line 12, The library tsconfig enables
"declaration": true while also including tests, causing .d.ts for tests to be
emitted; update the tsconfig used for building the admin-ui library so that the
"include" (or "files") only contains "src" (remove "__tests__") or move test
sources into a separate test tsconfig (e.g., tsconfig.test.json) and leave
"declaration": true only on the build tsconfig; target the "declaration" setting
in clients/admin-ui/tsconfig.json and ensure test typing is produced by the
separate test config instead of the main library config.
clients/admin-ui/src/providers/AlertProvider.tsx (1)

32-38: Clean up auto-dismiss timers on provider unmount.

setTimeout callbacks are not tracked/cleared, so timers can outlive the provider. Add cleanup for better reliability.

♻️ Suggested approach
-import React, { createContext, useCallback, useContext, useState } from "react";
+import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
@@
 export function AlertProvider({ children }: { children: React.ReactNode }) {
   const [alerts, setAlerts] = useState<Alert[]>([]);
+  const timeoutsRef = useRef<number[]>([]);
@@
-    setTimeout(() => {
+    const timeoutId = window.setTimeout(() => {
       setAlerts((prev) => prev.filter((a) => a.id !== id));
     }, 5000);
+    timeoutsRef.current.push(timeoutId);
   }, []);
+
+  useEffect(() => {
+    return () => {
+      timeoutsRef.current.forEach((id) => clearTimeout(id));
+      timeoutsRef.current = [];
+    };
+  }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/providers/AlertProvider.tsx` around lines 32 - 38, The
addAlert implementation schedules timeouts that aren’t cleared on unmount;
update the AlertProvider to track timer IDs (e.g., a useRef holding a Set or Map
of timeout IDs keyed by alert id) when addAlert calls setTimeout, store the
returned timer ID, and clear that specific timeout if the alert is removed early
(in the same removal callback) and also clear all remaining timers in a cleanup
returned from useEffect when AlertProvider unmounts; update references to
addAlert, alertCounter, and setAlerts accordingly so every scheduled timeout is
cancelled on unmount or when its alert is dismissed.
clients/admin-ui/__tests__/providers/AlertProvider.test.tsx (1)

70-72: Consider referencing the timeout constant to avoid test brittleness.

The test uses a hardcoded 5100 ms value that's slightly more than the actual 5000 ms timeout in AlertProvider.tsx. If the timeout constant changes, this test could silently pass for any value ≤ 5100ms. Consider either:

  1. Exporting the timeout constant from AlertProvider.tsx and using it here
  2. Adding a comment explaining the relationship to the source timeout
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/__tests__/providers/AlertProvider.test.tsx` around lines 70
- 72, The test advances timers by a hardcoded 5100ms which mirrors the hide
timeout in AlertProvider.tsx and is brittle; export the timeout constant (e.g.,
ALERT_HIDE_TIMEOUT or similar) from AlertProvider (where the 5000ms is defined)
and import it into AlertProvider.test.tsx, then replace the literal 5100 with a
value derived from that exported constant (e.g.,
advanceTimersByTime(ALERT_HIDE_TIMEOUT + someGraceMs)) or add an inline comment
referencing the original constant if you prefer not to export it; update
references to AlertProvider and the timeout constant names accordingly so the
test tracks future changes to the source timeout.
clients/admin-ui/src/components/JsonViewer.tsx (1)

45-58: Consider adding an accessible label for screen readers.

The expand/collapse button only has visible text. Adding an aria-expanded attribute would improve accessibility.

♿ Accessibility improvement
       <button
         onClick={() => setExpanded(!expanded)}
+        aria-expanded={expanded}
+        aria-label={expanded ? "Collapse JSON" : "Expand JSON"}
         style={{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/components/JsonViewer.tsx` around lines 45 - 58, The
expand/collapse button in JsonViewer.tsx lacks accessible state; update the
button rendered where setExpanded and expanded are used to include
aria-expanded={expanded} and an appropriate aria-label (e.g., "Expand JSON" /
"Collapse JSON" or a single label that reflects the current state) so screen
readers receive state info; ensure the aria-expanded value is wired to the
expanded boolean and the onClick still toggles via setExpanded(!expanded).
clients/admin-ui/__tests__/api/client.test.ts (1)

100-104: Avoid making two fetch calls for the same assertion.

Lines 100 and 101 each call client.get("/missing"), resulting in two separate fetch calls. Capture the promise once:

♻️ Consolidated assertion
-    await expect(client.get("/missing")).rejects.toThrow(SuperpositionApiError);
-    await expect(client.get("/missing")).rejects.toMatchObject({
-      status: 404,
-      body: "Not found",
-    });
+    const promise = client.get("/missing");
+    await expect(promise).rejects.toThrow(SuperpositionApiError);
+    await expect(promise).rejects.toMatchObject({
+      status: 404,
+      body: "Not found",
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/__tests__/api/client.test.ts` around lines 100 - 104, The
test currently calls client.get("/missing") twice causing two fetches; change it
to call client.get("/missing") once, store the returned promise in a variable
(e.g., const promise = client.get("/missing")), then use that single promise in
both assertions (expect(promise).rejects.toThrow(SuperpositionApiError) and
expect(promise).rejects.toMatchObject({...})), ensuring you reference the same
promise for both checks and keep the assertions on the single promise returned
by client.get.
clients/admin-ui/src/hooks/useApi.ts (1)

64-80: Potential stale closure if mutationFn is not memoized by caller.

The mutate callback depends on mutationFn (line 79), but if the caller passes an inline function, mutate will be recreated on every render. This could cause issues if components depend on stable mutate references.

Consider using a ref pattern similar to useApi's fetcherRef:

♻️ Proposed fix using ref pattern
 export function useMutation<TArgs extends unknown[], TResult>(
   mutationFn: (...args: TArgs) => Promise<TResult>,
 ): {
   mutate: (...args: TArgs) => Promise<TResult>;
   loading: boolean;
   error: string | null;
 } {
   const [loading, setLoading] = useState(false);
   const [error, setError] = useState<string | null>(null);
+  const mutationFnRef = useRef(mutationFn);
+  mutationFnRef.current = mutationFn;

   const mutate = useCallback(
     async (...args: TArgs): Promise<TResult> => {
       setLoading(true);
       setError(null);
       try {
-        const result = await mutationFn(...args);
+        const result = await mutationFnRef.current(...args);
         return result;
       } catch (err) {
         const msg = err instanceof Error ? err.message : String(err);
         setError(msg);
         throw err;
       } finally {
         setLoading(false);
       }
     },
-    [mutationFn],
+    [],
   );

   return { mutate, loading, error };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/hooks/useApi.ts` around lines 64 - 80, The mutate
callback captures mutationFn and can cause stale/unstable references if callers
pass inline functions; change to the ref pattern used by fetcherRef: create a
mutationFnRef that is updated whenever mutationFn changes (e.g., in an effect or
assignment), then have mutate (created with useCallback and an empty/depsafe
array) call mutationFnRef.current(...args); keep the existing
setLoading/setError logic and ensure mutationFnRef is updated whenever
mutationFn changes so mutate remains stable even if callers don't memoize their
function.
clients/admin-ui/__tests__/components/Pagination.test.tsx (2)

6-11: Improve the "renders nothing" assertion.

container.innerHTML being falsy is fragile — an empty container might still have whitespace or an empty wrapper. Consider checking container.firstChild:

♻️ More robust empty check
   it("renders nothing for single page", () => {
     const { container } = render(
       <Pagination currentPage={1} totalPages={1} onPageChange={() => {}} />,
     );
-    expect(container.innerHTML).toBeFalsy();
+    expect(container.firstChild).toBeNull();
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/__tests__/components/Pagination.test.tsx` around lines 6 -
11, Update the "renders nothing for single page" test for the Pagination
component so it asserts the container is truly empty by checking
container.firstChild instead of relying on container.innerHTML being falsy; in
the test that renders <Pagination currentPage={1} totalPages={1} ... />, replace
the fragile expect(container.innerHTML).toBeFalsy() with a robust assertion such
as expect(container.firstChild).toBeNull() to ensure no DOM nodes are present.

18-22: Consider using toBeInTheDocument() for clearer assertions.

Using toBeDefined() with getByText is redundant since getByText throws if the element isn't found. The toBeInTheDocument() matcher from @testing-library/jest-dom is more idiomatic and self-documenting.

♻️ More idiomatic assertions
-    expect(screen.getByText("1")).toBeDefined();
-    expect(screen.getByText("2")).toBeDefined();
-    expect(screen.getByText("3")).toBeDefined();
-    expect(screen.getByText("Previous")).toBeDefined();
-    expect(screen.getByText("Next")).toBeDefined();
+    expect(screen.getByText("1")).toBeInTheDocument();
+    expect(screen.getByText("2")).toBeInTheDocument();
+    expect(screen.getByText("3")).toBeInTheDocument();
+    expect(screen.getByText("Previous")).toBeInTheDocument();
+    expect(screen.getByText("Next")).toBeInTheDocument();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/__tests__/components/Pagination.test.tsx` around lines 18 -
22, Replace the redundant toBeDefined() assertions in Pagination.test.tsx with
the more idiomatic toBeInTheDocument() matcher (e.g., change
expect(screen.getByText("1")).toBeDefined() to
expect(screen.getByText("1")).toBeInTheDocument()) for each occurrence
("1","2","3","Previous","Next"); also ensure `@testing-library/jest-dom` is
imported/setup so toBeInTheDocument() is available (or add import
'@testing-library/jest-dom' to the test setup if missing).
clients/admin-ui/src/api/client.ts (2)

82-86: Type safety concern: undefined as T may mislead callers.

When a 204 or zero-content response is returned, undefined as T tells TypeScript the return is of type T, but it's actually undefined. Callers expecting T may encounter runtime errors.

Consider:

  1. Changing the return type to Promise<T | undefined> for methods that can return 204
  2. Or creating a separate deleteVoid method that explicitly returns Promise<void>
♻️ Option: Explicit void method for deletions
-  delete<T>(path: string): Promise<T> {
-    return this.request<T>("DELETE", path);
+  delete(path: string): Promise<void> {
+    return this.request<void>("DELETE", path);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/api/client.ts` around lines 82 - 86, The current
response-handling branch returns "undefined as T" when response.status === 204
or content-length is "0", which misleads TypeScript callers; update the API
surface by either (A) changing the function signature that contains this branch
to return Promise<T | undefined> and propagate that union through callers, or
(B) implement a separate explicit deletion/void variant (e.g., deleteVoid or
fetchVoid) that returns Promise<void> and use that for endpoints that can yield
204; modify the branch so it returns undefined (for the Promise<T | undefined>
approach) or returns void for the new deleteVoid method, and update call sites
to use the new signature/method accordingly (look for the function that contains
the response.status check and the return response.json() line to make the
changes).

85-87: Consider handling JSON parse errors gracefully.

If the server returns a 200 with malformed JSON, response.json() will throw an uncaught error. Consider wrapping this in a try/catch to provide a more descriptive error message.

🛡️ Defensive JSON parsing
-    return response.json();
+    try {
+      return await response.json();
+    } catch {
+      throw new SuperpositionApiError(
+        response.status,
+        "Invalid JSON response",
+        url,
+      );
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/api/client.ts` around lines 85 - 87, Wrap the direct
call to response.json() in a try/catch in the function that currently does
"return response.json()": attempt to parse with response.json(), and on failure
call await response.text() to capture the raw body and then throw a new,
descriptive Error that includes the HTTP status and the raw body (and optionally
attach the original parse error as the cause) so callers get a clear message for
malformed JSON responses; use response.text() and include
response.status/response.statusText in the error details.
clients/admin-ui/__tests__/api/dimensions.test.ts (1)

76-83: Add a test for the update operation.

The test suite covers list, get, create, and delete, but the update method is not tested. Consider adding a test case for api.update() to ensure consistent coverage across all API operations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/__tests__/api/dimensions.test.ts` around lines 76 - 83, Add
a new test case covering api.update similar to the existing create/get/delete
tests: mockFetch.mockResolvedValue with an appropriate success response, call
api.update("dim-id", payload) (use the same payload shape used by api.create
tests), then assert mockFetch was called with a URL containing
"/dimension/dim-id", that init.method is the expected HTTP verb used by the
implementation (e.g., "PUT" or "PATCH"), and that init.body equals
JSON.stringify(payload) and headers include the JSON content-type; reference the
api.update method and mockFetch.mock.calls to locate where to add the test.
clients/admin-ui/__tests__/pages/ConfigManager.test.tsx (1)

57-99: Extract a shared render helper to remove repeated provider boilerplate.

Line 57 through Line 99 repeats the same provider composition in every test, which makes future test updates noisy. Consider centralizing it into one helper.

♻️ Suggested refactor
+function renderConfigManager(configOverrides: Partial<{
+  host: string;
+  orgId: string;
+  workspace: string;
+}> = {}) {
+  return render(
+    <SuperpositionProvider
+      config={{ host: "https://test.com", orgId: "org", workspace: "ws", ...configOverrides }}
+    >
+      <AlertProvider>
+        <ConfigManager />
+      </AlertProvider>
+    </SuperpositionProvider>,
+  );
+}
+
 it("renders config list", async () => {
-  render(
-    <SuperpositionProvider
-      config={{ host: "https://test.com", orgId: "org", workspace: "ws" }}
-    >
-      <AlertProvider>
-        <ConfigManager />
-      </AlertProvider>
-    </SuperpositionProvider>,
-  );
+  renderConfigManager();
   ...
 });

 it("shows create button", async () => {
-  render(
-    <SuperpositionProvider
-      config={{ host: "https://test.com", orgId: "org", workspace: "ws" }}
-    >
-      <AlertProvider>
-        <ConfigManager />
-      </AlertProvider>
-    </SuperpositionProvider>,
-  );
+  renderConfigManager();
   ...
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/__tests__/pages/ConfigManager.test.tsx` around lines 57 -
99, Extract a shared test render helper that wraps components with the repeated
SuperpositionProvider and AlertProvider usage to remove boilerplate; create a
function (e.g., renderWithProviders or renderConfigManager) that takes a React
node or props and calls the test renderer inside the shared provider
composition, then replace the inline render(...) calls in the tests that render
ConfigManager with this helper (references: ConfigManager,
SuperpositionProvider, AlertProvider, and the existing test cases "renders
config list", "shows create button", "shows heading").
clients/admin-ui/__tests__/providers/SuperpositionProvider.test.tsx (1)

42-47: Strengthen provider wiring assertions beyond truthiness.

Line 42 through Line 47 only verifies objects exist. A broken wiring that returns empty objects would still pass. Assert key method bindings (e.g., dimensions.list, defaultConfigs.list, overrides.list, experiments.list) to catch regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/__tests__/providers/SuperpositionProvider.test.tsx` around
lines 42 - 47, The tests currently only assert truthiness for provider props;
update the assertions in SuperpositionProvider.test.tsx to verify key methods
are bound and callable by checking that dimensions.list, defaultConfigs.list,
overrides.list, and experiments.list are functions (or have expected behavior)
and optionally call them (mocking return values) to assert they return expected
results; locate the test expectations referencing "has-client", "has-dims",
"has-configs", "has-overrides", and "has-experiments" and replace or augment
them with assertions that screen.getByTestId("has-dims").textContent (or the
underlying prop) exposes a callable dimensions.list, similarly assert
defaultConfigs.list, overrides.list, and experiments.list so empty objects no
longer pass.
clients/admin-ui/src/api/overrides.ts (1)

44-46: Consider stronger typing for bulkOperations.

The unknown[] type for operations provides flexibility but loses type safety. If the bulk operations schema is well-defined, consider creating a dedicated type (e.g., BulkOperation) to improve type safety and self-documentation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/api/overrides.ts` around lines 44 - 46, The
bulkOperations method currently accepts unknown[] which loses type safety;
define a dedicated interface/type (e.g., BulkOperation) that models the expected
shape of each operation, update the bulkOperations signature to accept
BulkOperation[] (or Array<BulkOperation> | ReadonlyArray<BulkOperation> as
appropriate), and update any callsites and tests to use the new type; keep the
client.put call in bulkOperations as-is but ensure its payload is typed
accordingly so the compiler enforces correct operation structure.
clients/admin-ui/src/pages/AuditLog.tsx (1)

95-99: Consider resetting page when filter changes.

When actionFilter changes, the user might be on a page that no longer exists in the filtered result set. Consider resetting page to 1 when the filter is toggled to avoid showing an empty or out-of-bounds page.

♻️ Proposed fix
             onClick={() =>
-              setActionFilter((prev) =>
-                prev.includes(a) ? prev.filter((x) => x !== a) : [...prev, a],
-              )
+              setActionFilter((prev) => {
+                setPage(1);
+                return prev.includes(a) ? prev.filter((x) => x !== a) : [...prev, a];
+              })
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/pages/AuditLog.tsx` around lines 95 - 99, Toggling the
action filter via the onClick handler that calls setActionFilter can leave the
current pagination page out-of-range; update the handler so that whenever
actionFilter is changed you also reset the page state (e.g., call setPage(1)) —
modify the onClick logic that updates setActionFilter to also call setPage(1)
(or include page reset in a combined state updater) so filtering always returns
the user to the first page.
clients/admin-ui/src/components/Modal.tsx (1)

33-43: Consider adding focus management for better accessibility.

The modal has good ARIA attributes but lacks focus trapping (Tab can escape to background elements) and doesn't restore focus to the triggering element on close. The ref on line 34 is unused and could be leveraged for focus management.

For an admin UI this may be acceptable, but consider adding focus trapping for keyboard-only users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/components/Modal.tsx` around lines 33 - 43, The Modal
currently adds Escape handling but doesn't manage focus; use the existing ref
(ref) and the Modal component to implement focus trapping and restore focus: on
open, save document.activeElement, focus the modal or its first focusable child,
and add a keydown handler (in the same useEffect or a new one tied to open) that
intercepts Tab/Shift+Tab to keep focus inside the modal container (using
ref.current.querySelectorAll to find focusable elements), and on close restore
focus to the previously focused element and remove the keydown listener; update
the cleanup to remove listeners and clear any saved state.
clients/admin-ui/src/pages/OverrideManager.tsx (1)

78-96: JSON parse errors will show generic mutation error message.

Similar to ExperimentManager, when JSON.parse(newContext) or JSON.parse(newOverride) throws, the catch block shows createMutation.error which won't contain the actual parse error. Consider providing specific feedback for JSON syntax errors.

♻️ Proposed fix
   const handleCreate = async () => {
     try {
-      const ctx = mergeScopedContext(JSON.parse(newContext), scopedContext);
+      let ctx: Record<string, unknown>;
+      let override: Record<string, unknown>;
+      try {
+        ctx = mergeScopedContext(JSON.parse(newContext), scopedContext);
+      } catch {
+        addAlert("error", "Invalid JSON in context field");
+        return;
+      }
+      try {
+        override = JSON.parse(newOverride);
+      } catch {
+        addAlert("error", "Invalid JSON in override field");
+        return;
+      }
       await createMutation.mutate({
         context: ctx,
-        override: JSON.parse(newOverride),
+        override,
         description: newDesc || undefined,
         change_reason: newReason || "Created via admin UI",
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/pages/OverrideManager.tsx` around lines 78 - 96,
handleCreate currently parses newContext and newOverride inline so a JSON.parse
SyntaxError ends up swallowed and addAlert shows createMutation.error; change it
to validate/parse newContext and newOverride before calling createMutation
(e.g., try parsing each into ctx and overrideVars with JSON.parse and if a
SyntaxError occurs call addAlert("error", `Invalid JSON: ${error.message}`) and
return), then call mergeScopedContext and createMutation.mutate with the parsed
objects as before; reference functions/variables: handleCreate,
mergeScopedContext, createMutation, newContext, newOverride, addAlert.
clients/admin-ui/src/pages/ExperimentManager.tsx (1)

88-109: JSON parse errors will show generic mutation error message.

When JSON.parse(newContext) or JSON.parse(newVariants) throws a SyntaxError, the catch block shows createMutation.error which won't contain the parse error details. Consider providing more specific feedback for JSON syntax errors.

♻️ Proposed fix
   const handleCreate = async () => {
     try {
-      const ctx = scopedContext
-        ? { ...JSON.parse(newContext), ...scopedContext }
-        : JSON.parse(newContext);
+      let ctx: Record<string, unknown>;
+      let variants: unknown[];
+      try {
+        ctx = scopedContext
+          ? { ...JSON.parse(newContext), ...scopedContext }
+          : JSON.parse(newContext);
+      } catch {
+        addAlert("error", "Invalid JSON in context field");
+        return;
+      }
+      try {
+        variants = JSON.parse(newVariants);
+      } catch {
+        addAlert("error", "Invalid JSON in variants field");
+        return;
+      }
       await createMutation.mutate({
         name: newName,
         context: ctx,
-        variants: JSON.parse(newVariants),
+        variants,
         description: newDesc || "No description",
         change_reason: newReason || "Created via admin UI",
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/pages/ExperimentManager.tsx` around lines 88 - 109, The
handleCreate function currently parses newContext and newVariants inline so a
JSON.parse SyntaxError is swallowed by the outer catch that only shows
createMutation.error; change the flow to first attempt parsing newContext and
newVariants inside their own try/catch (or validate upfront) and if parsing
fails call addAlert("error", `Invalid JSON in context: ${err.message}`) or
similar, then proceed to call createMutation.mutate in a separate try/catch that
uses addAlert("error", createMutation.error || err.message || "Failed to create
experiment") on mutation failure; update references: handleCreate, newContext,
newVariants, createMutation, addAlert to implement this split-error handling so
JSON syntax errors are surfaced separately from server/mutation errors.
clients/admin-ui/src/components/Table.tsx (1)

3-8: Consider typing key as keyof T for better type safety.

Currently, key is string which allows any value, but the default cell renderer accesses row[col.key]. Typing it as keyof T & string would catch mismatches at compile time when not using a custom render function.

💡 Alternative with stricter typing
-export interface Column<T> {
-  key: string;
+export interface Column<T, K extends keyof T = keyof T> {
+  key: K | string; // string allows action columns that don't map to data
   header: string;
   render?: (row: T) => React.ReactNode;
   width?: string;
 }

Note: Keeping | string allows "virtual" columns like "actions" that don't correspond to actual data fields.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/components/Table.tsx` around lines 3 - 8, The Column<T>
interface types the column "key" as string which permits invalid keys when the
default renderer accesses row[col.key]; change the "key" type to constrain it to
actual properties of T by using keyof T & string (or keyof T | string if you
need to allow virtual keys like "actions") so TypeScript catches mismatches;
update usages of Column<T>, and ensure the default cell renderer that reads
row[col.key] compiles against the new keyof typing and adjust any places passing
arbitrary strings to either use a custom render or the allowed union form.
clients/admin-ui/src/pages/DimensionManager.tsx (2)

98-134: Stale closure risk in columns definition.

The columns useMemo has an empty dependency array but captures handleDelete which references deleteMutation. If deleteMutation is not referentially stable across renders, the delete action could use stale state.

Either add handleDelete to the dependency array or verify that useMutation returns stable function references.

♻️ Proposed fix: include handleDelete in dependencies
     ],
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    [],
+    [handleDelete],
   );

This requires wrapping handleDelete in useCallback:

-  const handleDelete = async (name: string) => {
+  const handleDelete = useCallback(async (name: string) => {
     if (!confirm(`Delete dimension "${name}"?`)) return;
     try {
       await deleteMutation.mutate(name);
       refetch();
     } catch {
       addAlert("error", deleteMutation.error || "Failed to delete");
     }
-  };
+  }, [deleteMutation, refetch, addAlert]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/pages/DimensionManager.tsx` around lines 98 - 134, The
columns useMemo captures handleDelete causing a stale-closure risk; to fix, make
handleDelete referentially stable (wrap the existing handleDelete in
useCallback) and include that handleDelete in the dependency array of the
columns useMemo (or alternatively add deleteMutation if you prefer to depend on
the mutation directly). Ensure you reference the same handleDelete name used in
the render function and update the useMemo dependency list rather than disabling
eslint for exhaustive-deps.

88-96: Consider replacing confirm() with a confirmation modal.

Using window.confirm() works but has limitations:

  • Not testable without mocking globals
  • Cannot be styled to match the UI
  • Blocks the main thread

The codebase already has a Modal component that could be adapted for confirmation dialogs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/pages/DimensionManager.tsx` around lines 88 - 96,
Replace the blocking window.confirm call in handleDelete with a non-blocking
Modal flow: add local state (e.g., confirmOpen:boolean and
pendingDeleteName:string) and render the existing Modal (or a small ConfirmModal
variant) that shows the message and exposes onConfirm/onCancel callbacks; when
user clicks delete in the UI, set pendingDeleteName and open the modal, and in
the modal's onConfirm call perform deleteMutation.mutate(pendingDeleteName) then
refetch() and addAlert on error (using deleteMutation.error || "Failed to
delete"), and onCancel just close the modal—update handleDelete to only open the
modal and move the actual deletion logic into the modal confirm handler.
clients/admin-ui/src/providers/SuperpositionProvider.tsx (1)

41-59: Consider stabilizing useMemo dependencies to prevent unnecessary re-instantiation.

The useMemo depends on the entire config object reference. If the parent component re-renders and passes a new object reference (even with identical values), the client and all API modules will be recreated. This could cause unnecessary work and potentially disrupt in-flight requests.

Options to consider:

  1. Document that consumers should memoize the config object
  2. Use a deep comparison or serialize config fields as dependencies
💡 Alternative: Destructure stable dependencies
 export function SuperpositionProvider({
   config,
   children,
 }: SuperpositionProviderProps) {
+  const { host, orgId, workspace, auth } = config;
   const value = useMemo<SuperpositionContextValue>(() => {
     const client = new SuperpositionClient({
-      host: config.host,
-      orgId: config.orgId,
-      workspace: config.workspace,
-      auth: config.auth,
+      host,
+      orgId,
+      workspace,
+      auth,
     });

     return {
       config,
       client,
       dimensions: dimensionsApi(client),
       defaultConfigs: defaultConfigsApi(client),
       overrides: overridesApi(client),
       experiments: experimentsApi(client),
       resolve: resolveApi(client),
       audit: auditApi(client),
     };
-  }, [config]);
+  }, [config, host, orgId, workspace, auth]);

Note: The auth object reference would still need stable identity from the consumer, or you could serialize it (e.g., JSON.stringify(auth)).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/providers/SuperpositionProvider.tsx` around lines 41 -
59, The useMemo in SuperpositionProvider currently depends on the whole config
object which causes SuperpositionClient and all APIs to be re-created when a new
config reference is passed; update the dependency list for the useMemo that
builds the SuperpositionContextValue to use stable primitives (e.g.,
config.host, config.orgId, config.workspace) and a stable representation of auth
(either require consumers to memoize auth or use a serialized form like
JSON.stringify(config.auth)) so SuperpositionClient and the API factories
(dimensionsApi, defaultConfigsApi, overridesApi, experimentsApi, resolveApi,
auditApi) only re-instantiate when actual config values change.
clients/admin-ui/src/utils/context-filter.ts (1)

63-68: Always return a fresh context object from mergeScopedContext.

Line 67 currently returns the original userContext reference when scopedContext is absent, even though this helper is documented as producing a new merged context. That makes the no-scope path behave differently and can leak mutations back into caller state.

Suggested fix
 export function mergeScopedContext(
   userContext: Condition,
   scopedContext?: ScopedContext,
 ): Condition {
-  if (!scopedContext) return userContext;
-  return { ...userContext, ...scopedContext };
+  return { ...userContext, ...(scopedContext ?? {}) };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/utils/context-filter.ts` around lines 63 - 68,
mergeScopedContext currently returns the original userContext reference when
scopedContext is undefined, which can leak mutations; change the no-scope path
in mergeScopedContext to return a fresh shallow copy of userContext (e.g.,
construct and return a new object with the same entries) so the function always
returns a new Condition object and preserves immutability between callers and
the helper.
clients/admin-ui/src/index.ts (1)

27-34: Re-export ScopedContext with the scoped helpers.

These helpers are part of the root package API, but the shared ScopedContext type from clients/admin-ui/src/utils/context-filter.ts is not. Consumers will otherwise need a deep import just to type the same object they pass into contextMatchesScope and the filter*ByScope helpers.

Suggested fix
 export {
   contextMatchesScope,
   filterOverridesByScope,
   filterExperimentsByScope,
   mergeScopedContext,
   getLockedDimensions,
 } from "./utils";
+export type { ScopedContext } from "./utils/context-filter";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/index.ts` around lines 27 - 34, Re-export the Shared
ScopedContext type from the utils module alongside the scoped helper functions
so consumers can import the type without a deep import; specifically add an
export for ScopedContext (the type defined in utils/context-filter.ts) to the
same export block that currently re-exports contextMatchesScope,
filterOverridesByScope, filterExperimentsByScope, mergeScopedContext, and
getLockedDimensions (i.e., ensure ScopedContext is exported from the package
entry so callers can type values passed to contextMatchesScope and the
filter*ByScope helpers).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bbaac5e9-72f5-4eb7-a660-6c4875a4425d

📥 Commits

Reviewing files that changed from the base of the PR and between 95468ee and 9ae47e5.

⛔ Files ignored due to path filters (1)
  • clients/admin-ui/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (53)
  • PLAN-embeddable-admin-ui.md
  • clients/admin-ui/.prettierrc
  • clients/admin-ui/__tests__/api/client.test.ts
  • clients/admin-ui/__tests__/api/context-filter.test.ts
  • clients/admin-ui/__tests__/api/dimensions.test.ts
  • clients/admin-ui/__tests__/components/Modal.test.tsx
  • clients/admin-ui/__tests__/components/Pagination.test.tsx
  • clients/admin-ui/__tests__/components/Table.test.tsx
  • clients/admin-ui/__tests__/pages/ConfigManager.test.tsx
  • clients/admin-ui/__tests__/pages/OverrideManager.test.tsx
  • clients/admin-ui/__tests__/providers/AlertProvider.test.tsx
  • clients/admin-ui/__tests__/providers/SuperpositionProvider.test.tsx
  • clients/admin-ui/__tests__/setup.ts
  • clients/admin-ui/package.json
  • clients/admin-ui/src/api/audit.ts
  • clients/admin-ui/src/api/client.ts
  • clients/admin-ui/src/api/default-configs.ts
  • clients/admin-ui/src/api/dimensions.ts
  • clients/admin-ui/src/api/experiments.ts
  • clients/admin-ui/src/api/index.ts
  • clients/admin-ui/src/api/overrides.ts
  • clients/admin-ui/src/api/resolve.ts
  • clients/admin-ui/src/components/AlertBar.tsx
  • clients/admin-ui/src/components/ConditionBadges.tsx
  • clients/admin-ui/src/components/FormField.tsx
  • clients/admin-ui/src/components/JsonViewer.tsx
  • clients/admin-ui/src/components/Modal.tsx
  • clients/admin-ui/src/components/Pagination.tsx
  • clients/admin-ui/src/components/StatusBadge.tsx
  • clients/admin-ui/src/components/Table.tsx
  • clients/admin-ui/src/components/index.ts
  • clients/admin-ui/src/hooks/index.ts
  • clients/admin-ui/src/hooks/useApi.ts
  • clients/admin-ui/src/index.ts
  • clients/admin-ui/src/pages/AuditLog.tsx
  • clients/admin-ui/src/pages/ConfigManager.tsx
  • clients/admin-ui/src/pages/DimensionManager.tsx
  • clients/admin-ui/src/pages/ExperimentManager.tsx
  • clients/admin-ui/src/pages/OverrideManager.tsx
  • clients/admin-ui/src/pages/ResolveView.tsx
  • clients/admin-ui/src/pages/SuperpositionAdmin.tsx
  • clients/admin-ui/src/pages/index.ts
  • clients/admin-ui/src/providers/AlertProvider.tsx
  • clients/admin-ui/src/providers/SuperpositionProvider.tsx
  • clients/admin-ui/src/providers/index.ts
  • clients/admin-ui/src/types/api.ts
  • clients/admin-ui/src/types/index.ts
  • clients/admin-ui/src/utils/context-filter.ts
  • clients/admin-ui/src/utils/format.ts
  • clients/admin-ui/src/utils/index.ts
  • clients/admin-ui/tsconfig.json
  • clients/admin-ui/vite.config.ts
  • clients/admin-ui/vitest.config.ts

Comment on lines +73 to +94
it("filters overrides by scoped context", async () => {
render(
<SuperpositionProvider
config={{
host: "https://test.com",
orgId: "org",
workspace: "ws",
context: { region: "us-east-1" },
}}
>
<AlertProvider>
<OverrideManager />
</AlertProvider>
</SuperpositionProvider>,
);

await waitFor(() => {
// Should show US override (region matches) and EU override (region doesn't conflict because us-east-1 ≠ eu-west-1)
// Actually: ctx-2 has region: eu-west-1 but scoped is region: us-east-1
// contextMatchesScope: condition[region] = eu-west-1 !== us-east-1 → no match
expect(screen.getByText("Context Overrides")).toBeDefined();
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scoped filtering behavior is untested (false-positive test).

Line 73 through Line 94 should verify filtering by config.context, but it currently only checks the heading, so filtering regressions won’t be caught.

✅ Suggested assertion upgrade
 it("filters overrides by scoped context", async () => {
   render(
     <SuperpositionProvider
       config={{
         host: "https://test.com",
         orgId: "org",
         workspace: "ws",
         context: { region: "us-east-1" },
       }}
     >
       <AlertProvider>
         <OverrideManager />
       </AlertProvider>
     </SuperpositionProvider>,
   );

   await waitFor(() => {
-    // Should show US override (region matches) and EU override (region doesn't conflict because us-east-1 ≠ eu-west-1)
-    // Actually: ctx-2 has region: eu-west-1 but scoped is region: us-east-1
-    // contextMatchesScope: condition[region] = eu-west-1 !== us-east-1 → no match
-    expect(screen.getByText("Context Overrides")).toBeDefined();
+    expect(screen.getByText("us-east-1")).toBeDefined();
+    expect(screen.queryByText("eu-west-1")).toBeNull();
   });
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("filters overrides by scoped context", async () => {
render(
<SuperpositionProvider
config={{
host: "https://test.com",
orgId: "org",
workspace: "ws",
context: { region: "us-east-1" },
}}
>
<AlertProvider>
<OverrideManager />
</AlertProvider>
</SuperpositionProvider>,
);
await waitFor(() => {
// Should show US override (region matches) and EU override (region doesn't conflict because us-east-1 ≠ eu-west-1)
// Actually: ctx-2 has region: eu-west-1 but scoped is region: us-east-1
// contextMatchesScope: condition[region] = eu-west-1 !== us-east-1 → no match
expect(screen.getByText("Context Overrides")).toBeDefined();
});
it("filters overrides by scoped context", async () => {
render(
<SuperpositionProvider
config={{
host: "https://test.com",
orgId: "org",
workspace: "ws",
context: { region: "us-east-1" },
}}
>
<AlertProvider>
<OverrideManager />
</AlertProvider>
</SuperpositionProvider>,
);
await waitFor(() => {
expect(screen.getByText("us-east-1")).toBeDefined();
expect(screen.queryByText("eu-west-1")).toBeNull();
});
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/__tests__/pages/OverrideManager.test.tsx` around lines 73 -
94, The test "filters overrides by scoped context" currently only asserts the
heading and misses validating actual filtering; update the test that renders
OverrideManager inside SuperpositionProvider (with config.context) to assert
that overrides whose scope matches config.context (e.g., the US override) are
present and that non-matching scoped overrides (e.g., the EU override / ctx-2)
are not rendered; use the existing render/waitFor and screen matchers
(screen.getByText / queryByText or getByRole) to explicitly expect presence of
the matching override label and absence (null) of the non-matching override so
the test will fail if contextMatchesScope logic in OverrideManager breaks.

@@ -0,0 +1,56 @@
{
"name": "@anthropic/superposition-admin-ui",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
import json, pathlib
for p in pathlib.Path(".").rglob("package.json"):
    try:
        data = json.loads(p.read_text())
    except Exception:
        continue
    if "name" in data:
        print(f"{p}: {data['name']}")
PY

Repository: juspay/superposition

Length of output: 834


Fix package scope mismatch before publishing.

The @anthropic scope at line 2 is inconsistent with the repository ownership (juspay/superposition) and conflicts with all other packages in the project. A scan of all package.json files shows this is the only package using the @anthropic scope; all others use unscoped names (e.g., superposition-client, superposition-sdk, superposition_browser_client). Publishing under a scope you do not own will fail or create namespace conflicts. Update the package name to match the repository convention, such as @juspay/superposition-admin-ui or superposition-admin-ui.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/package.json` at line 2, The package name currently uses the
incorrect scope "@anthropic/superposition-admin-ui" in the package.json "name"
field; change that value to match repository conventions (either an unscoped
"superposition-admin-ui" or the org-scoped "@juspay/superposition-admin-ui") so
it aligns with other packages and avoids publishing namespace conflicts—update
the "name" entry accordingly and verify npm publish/tagging workflows if any
rely on the package name.

Comment on lines +17 to +23
"files": [
"dist"
],
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"test": "vitest run",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

build currently risks publishing compiled test artifacts.

Line 22 runs tsc, and clients/admin-ui/tsconfig.json includes __tests__. With "files": ["dist"] (Lines 17-19), test outputs can be shipped in the package.

📦 Suggested packaging fix
   "scripts": {
-    "build": "tsc && vite build",
+    "build": "tsc -p tsconfig.build.json --emitDeclarationOnly && vite build",
// tsconfig.build.json (new file)
{
  "extends": "./tsconfig.json",
  "include": ["src"],
  "exclude": ["__tests__", "dist", "node_modules"]
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/package.json` around lines 17 - 23, The build script
currently runs plain tsc which compiles __tests__ per
clients/admin-ui/tsconfig.json; create a new tsconfig.build.json that extends
tsconfig.json and restricts compilation to source (include "src" and exclude
"__tests__", "dist", "node_modules"), then update the package.json "build"
script to invoke tsc with that project file (e.g., change the "build" command to
run tsc -p tsconfig.build.json && vite build) so only intended source files are
emitted to dist.

Comment on lines +31 to +40
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For a library package, what validation is recommended before declaring peerDependencies as "^18 || ^19" for react/react-dom, and are there known React 19 type/runtime compatibility checks maintainers should run?

💡 Result:

Before you declare peerDependencies: { react: "^18 || ^19", react-dom: "^18 || ^19" }, the most defensible validation is to prove your published artifacts work in both “React 18 consumer” and “React 19 consumer” environments, at both runtime and type-level, because React 19 includes removals/deprecations and TypeScript type changes that can break libraries even when runtime is fine.

Recommended validation for ^18 || ^19 peer deps

  1. CI runtime matrix (build + tests)

    • Run your test suite against React 18 + ReactDOM 18 and React 19 + ReactDOM 19.
    • Make sure you exercise any renderer-specific paths you support (client render, hydration/SSR if applicable).
    • Specifically ensure you are not calling APIs removed in React 19, such as:
      • ReactDOM.unmountComponentAtNode (removed) [1]
      • findDOMNode usage (removed from supported patterns; migrate to refs) [1]
      • other removed react-dom unstable APIs listed in the upgrade guide [1]
  2. CI type matrix (your library’s public types, not just its build)

    • Typecheck your library (and ideally a tiny “consumer” project) with:
      • typescript@<your supported range> + @types/react@18 + @types/react-dom@18
      • typescript@<your supported range> + @types/react@19 + @types/react-dom@19 [1]
    • If you publish .d.ts, prefer an actual consumer-style check (e.g., tsd) so you catch breaking exported types, not just internal compilation.
  3. No React internals

    • Audit for any reliance on React internals (e.g. SECRET_INTERNALS...), which React explicitly warns can block upgrades [1].
  4. Testing strategy sanity

    • If your tests (or docs) rely on react-test-renderer, expect React 19 deprecation warnings and behavior differences; React recommends migrating to modern testing libraries (e.g. Testing Library) [1].

Known React 19 type/runtime compatibility checks maintainers should run

Type-level gotchas (common library-breakers)

  • JSX namespace / augmentation changes: if your library augments JSX (custom elements, intrinsic elements), React 19’s guidance is to scope it via declare module "react" { namespace JSX { ... } } (exact module depends on JSX runtime) [1].
  • Ref callback typing is stricter: callback refs must not implicitly return a value (because ref cleanup functions exist); this often breaks library typings until fixed [1].
  • useRef now requires an initial value in the types (can break code and examples) [1].
  • ReactNode / renderable types changed in React 19 types: React 19 types were updated to reflect runtime changes like rendering Promise (async rendering patterns), which can create cross-version friction if your public API exposes React 19-only renderable types to React 18 consumers (e.g., ReactNode | Promise<ReactNode> not assignable to ReactNode) [2].

Runtime/API removals to smoke-test

  • Anything that still uses legacy root APIs or removed DOM APIs should be validated (or eliminated) following the React 19 upgrade guide (e.g., unmountComponentAtNode, findDOMNode, other removed react-dom APIs) [1].

If you support / mention React Compiler

  • React’s guidance for library maintainers is to compile and test your library with the compiler and ship compiled output; if you need to support apps not yet on React 19, you may need to set a minimum compiler target and depend on react-compiler-runtime [3].

Practical rule: don’t ship ^18 || ^19 until you have (1) a React 18/19 runtime CI matrix, and (2) an @types/react@18 + @types/react@19 consumer type-test matrix that covers your exported surface area (especially anything involving JSX, ref, ReactNode, or component return types).

Sources: React 19 upgrade guidance (API removals, ref/test-renderer deprecations, TS changes) [1]; React 19 type-change tracking (including Promise renderability) [2]; React Compiler guidance for library maintainers [3].

[1] https://react.dev/blog/2024/04/25/react-19-upgrade-guide
[2] DefinitelyTyped/DefinitelyTyped#64451
[3] https://react.dev/blog/2024/10/21/react-compiler-beta-release


🏁 Script executed:

cd clients/admin-ui && cat -n package.json | head -50

Repository: juspay/superposition

Length of output: 1956


🏁 Script executed:

find . -type f \( -name "*.yml" -o -name "*.yaml" -o -name ".github" \) | head -20

Repository: juspay/superposition

Length of output: 678


🏁 Script executed:

rg -l "react.*19|React.*19" --type json --type md --type yml

Repository: juspay/superposition

Length of output: 91


🏁 Script executed:

cat -n ./.github/workflows/ci_check_pr.yaml

Repository: juspay/superposition

Length of output: 19030


🏁 Script executed:

rg "19" clients/admin-ui/ --type json

Repository: juspay/superposition

Length of output: 2659


🏁 Script executed:

rg -i "react.*19|react 19|version.*19" clients/admin-ui/ -A 2 -B 2

Repository: juspay/superposition

Length of output: 2159


🏁 Script executed:

cat -n clients/admin-ui/README.md 2>/dev/null | head -50

Repository: juspay/superposition

Length of output: 46


🏁 Script executed:

find clients/admin-ui/src -name "*.tsx" -o -name "*.ts" | head -5 | xargs grep -l "unmountComponentAtNode\|findDOMNode" 2>/dev/null

Repository: juspay/superposition

Length of output: 48


🏁 Script executed:

rg "react" clients/admin-ui/README.md -i -A 3 -B 1 2>/dev/null || echo "No README or grep found React references"

Repository: juspay/superposition

Length of output: 104


Add React 19 validation to CI or narrow peerDependencies to React 18 only.

This package declares react/react-dom as ^18.0.0 || ^19.0.0 but all dev/type dependencies are pinned to React 18.x (@types/react@^18.3.12, react@^18.3.1, etc.) and CI does not test against React 19. Before shipping dual-version support to consumers, validate both runtime (test suite against React 19) and types (typecheck with @types/react@19) in CI. Otherwise, narrow peerDependencies to ^18.0.0 until React 19 compatibility is confirmed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/package.json` around lines 31 - 40, The package.json
declares peerDependencies "react" and "react-dom" as "^18.0.0 || ^19.0.0" while
devDependencies and types (e.g., "@types/react": "^18.3.12") are pinned to React
18; either update CI to validate React 19 or narrow the peerDependencies to
React 18 only. To fix, either (A) add React 19 job(s) to the CI matrix that
install react@^19 and react-dom@^19, run the test suite and run typechecking
with `@types/react`@^19 (or install `@types/react`@^19 in devDependencies for that
job), or (B) change the package.json peerDependencies entries "react" and
"react-dom" to "^18.0.0" to match current dev/type deps until full React 19
validation is added.

Comment on lines +6 to +10
getConfig(
context: Record<string, JsonValue> = {},
): Promise<Config> {
return client.get("/config", context);
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect query serialization behavior in client
rg -n -C4 'get<T>\(|request<T>\(|URLSearchParams|query' clients/admin-ui/src/api/client.ts

Repository: juspay/superposition

Length of output: 1328


🏁 Script executed:

# View the full buildQueryString implementation
sed -n '40,80p' clients/admin-ui/src/api/client.ts

Repository: juspay/superposition

Length of output: 1345


🏁 Script executed:

# Find the JsonValue type definition
rg -n 'type JsonValue|interface JsonValue' clients/admin-ui/src

Repository: juspay/superposition

Length of output: 123


🏁 Script executed:

# View the full JsonValue type definition
sed -n '3,20p' clients/admin-ui/src/types/api.ts

Repository: juspay/superposition

Length of output: 503


🏁 Script executed:

# Find usages of getConfig
rg -n 'getConfig\(' clients/admin-ui/src --type ts --type tsx

Repository: juspay/superposition

Length of output: 91


🏁 Script executed:

# Find usages of getConfig without type filter
rg -n 'getConfig\(' clients/admin-ui/src

Repository: juspay/superposition

Length of output: 116


🏁 Script executed:

# Search for where the resolve API is imported and used
rg -n 'from.*resolve|import.*resolve' clients/admin-ui/src -A 3

Repository: juspay/superposition

Length of output: 591


🏁 Script executed:

# Find where resolveApi.getConfig is called
rg -n 'resolveApi\.getConfig\|\.getConfig\(' clients/admin-ui/src -B 2 -A 5

Repository: juspay/superposition

Length of output: 46


🏁 Script executed:

# Search more broadly for getConfig usage
rg -n 'getConfig' clients/admin-ui/src -B 2 -A 5

Repository: juspay/superposition

Length of output: 578


🏁 Script executed:

# Search in SuperpositionProvider for how getConfig is called
sed -n '1,100p' clients/admin-ui/src/providers/SuperpositionProvider.tsx | cat -n

Repository: juspay/superposition

Length of output: 2561


🏁 Script executed:

# View the complete resolve.ts file to understand the full context
cat -n clients/admin-ui/src/api/resolve.ts

Repository: juspay/superposition

Length of output: 798


🏁 Script executed:

# Check if there are server-side handlers or tests that show how context should be passed
rg -n 'context' clients/admin-ui/src/api/resolve.ts -B 2 -A 2

Repository: juspay/superposition

Length of output: 507


Fix unsafe query parameter serialization in getConfig().

The context is passed as query parameters using buildQueryString(), which converts nested objects to "[object Object]" instead of proper JSON serialization. This causes incorrect server-side context matching.

Consider passing context as JSON in the request body (similar to the resolve() method in the same file) or explicitly serializing it:

Possible fix direction
-    getConfig(
-      context: Record<string, JsonValue> = {},
-    ): Promise<Config> {
-      return client.get("/config", context);
+    getConfig(
+      context: Record<string, JsonValue> = {},
+    ): Promise<Config> {
+      return Object.keys(context).length === 0
+        ? client.get("/config")
+        : client.get("/config", { context: JSON.stringify(context) });
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getConfig(
context: Record<string, JsonValue> = {},
): Promise<Config> {
return client.get("/config", context);
},
getConfig(
context: Record<string, JsonValue> = {},
): Promise<Config> {
return Object.keys(context).length === 0
? client.get("/config")
: client.get("/config", { context: JSON.stringify(context) });
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/api/resolve.ts` around lines 6 - 10, getConfig currently
passes the context object directly to client.get which relies on
buildQueryString and turns nested objects into "[object Object]"; change
getConfig to send the context as JSON like the resolve() method does (e.g., post
the context in the request body) or explicitly serialize context with
JSON.stringify before sending so nested objects are preserved. Update the
getConfig function signature/implementation to mirror resolve()'s approach (use
the same HTTP verb and body shape or ensure the client call sends a serialized
context payload) so server-side matching receives correct JSON context.

Comment on lines +20 to +32
<label
style={{
display: "block",
marginBottom: 4,
fontSize: 13,
fontWeight: 500,
color: "#374151",
}}
>
{label}
{required && <span style={{ color: "#ef4444", marginLeft: 2 }}>*</span>}
</label>
{children}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Associate labels with inputs.

<label> is currently unbound to a control, so click-to-focus and screen-reader association are lost.

Suggested fix
 export interface FormFieldProps {
   label: string;
+  htmlFor?: string;
@@
 export function FormField({
   label,
+  htmlFor,
@@
-      <label
+      <label
+        htmlFor={htmlFor}
         style={{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/components/FormField.tsx` around lines 20 - 32,
FormField's <label> is not associated with the input, breaking click-to-focus
and screen-reader binding; update the FormField component to accept or generate
an input id and associate it with the label via htmlFor (or wrap the input) and
ensure the child input receives that id (use props.id or generate one and pass
it to children via React.cloneElement when children is a React element).
Specifically modify the FormField component (props: label, required, children)
to produce/propagate a unique id and set <label htmlFor={id}> and give the input
element the same id so the label correctly targets the control.

Comment on lines +98 to +133
const columns: Column<DefaultConfig>[] = useMemo(
() => [
{ key: "key", header: "Key", width: "25%" },
{
key: "value",
header: "Value",
width: "25%",
render: (row) => <JsonViewer data={row.value} />,
},
{
key: "schema",
header: "Schema",
width: "20%",
render: (row) => <JsonViewer data={row.schema} />,
},
{ key: "description", header: "Description", width: "20%" },
{
key: "actions",
header: "",
width: "10%",
render: (row) => (
<button
style={buttonDanger}
onClick={(e) => {
e.stopPropagation();
handleDelete(row.key);
}}
>
Delete
</button>
),
},
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify stale-closure risk pattern in this file
rg -n -C3 'useMemo\(|eslint-disable-next-line react-hooks/exhaustive-deps|handleDelete' clients/admin-ui/src/pages/ConfigManager.tsx

Repository: juspay/superposition

Length of output: 752


🏁 Script executed:

#!/bin/bash
# Get broader context around handleDelete definition and useMemo
head -n 150 clients/admin-ui/src/pages/ConfigManager.tsx | tail -n +60

Repository: juspay/superposition

Length of output: 2558


🏁 Script executed:

#!/bin/bash
# Check for the definition of deleteMutation, refetch, and addAlert
rg -n 'deleteMutation|const refetch|const addAlert|useAlert|useMutation' clients/admin-ui/src/pages/ConfigManager.tsx | head -30

Repository: juspay/superposition

Length of output: 421


🏁 Script executed:

#!/bin/bash
# Find refetch definition and broader hook context
rg -n 'refetch|useQuery|const.*=.*use' clients/admin-ui/src/pages/ConfigManager.tsx | head -20

Repository: juspay/superposition

Length of output: 801


🏁 Script executed:

#!/bin/bash
# Check if useCallback is used elsewhere in the file to understand team patterns
rg -n 'useCallback' clients/admin-ui/src/pages/ConfigManager.tsx

Repository: juspay/superposition

Length of output: 161


🏁 Script executed:

#!/bin/bash
# Get full context of handleDelete and surrounding code
sed -n '46,96p' clients/admin-ui/src/pages/ConfigManager.tsx

Repository: juspay/superposition

Length of output: 1460


Wrap handleDelete in useCallback to stabilize columns memoization.

columns is frozen with [] while its onClick handler calls handleDelete, which references deleteMutation, refetch, and addAlert—all hook-derived values that may change on each render. This creates a stale-closure risk where the memoized callback holds stale references. Wrap handleDelete in useCallback with proper dependencies and include it in the useMemo deps instead of suppressing the lint warning.

Suggested refactor
-  const handleDelete = async (key: string) => {
+  const handleDelete = useCallback(async (key: string) => {
     if (!confirm(`Delete config "${key}"?`)) return;
     try {
       await deleteMutation.mutate(key);
       refetch();
     } catch {
       addAlert("error", deleteMutation.error || "Failed to delete config");
     }
-  };
+  }, [deleteMutation, refetch, addAlert]);

   const columns: Column<DefaultConfig>[] = useMemo(
@@
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    [],
+    [handleDelete],
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/pages/ConfigManager.tsx` around lines 98 - 133, The
columns memo captures a mutable handleDelete closure (which uses deleteMutation,
refetch, addAlert) while being memoized with an empty deps array; wrap
handleDelete in useCallback with the correct dependencies (deleteMutation,
refetch, addAlert, and any other hooks it references) so it always has fresh
references, then include that handleDelete in the useMemo dependency array for
columns (remove the eslint-disable for exhaustive-deps) so the column render's
onClick uses a stable, non-stale function.

Comment on lines +116 to +128
export interface ContextOverride {
id: string;
value: Condition;
override_id: string;
created_at: string;
created_by: string;
override_: Overrides;
last_modified_at: string;
last_modified_by: string;
weight: string;
description: string;
change_reason: string;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Use the wire name override, not override_.

Line 122 is typed as override_, but the backend model in crates/superposition_types/src/database/models/cac.rs:32-53 serializes this field as override, and this file already uses override in PutContextRequest on Line 132. Unless the client explicitly remaps the field, downstream code will read a property that is never present on the wire.

Suggested fix
 export interface ContextOverride {
   id: string;
   value: Condition;
   override_id: string;
   created_at: string;
   created_by: string;
-  override_: Overrides;
+  override: Overrides;
   last_modified_at: string;
   last_modified_by: string;
   weight: string;
   description: string;
   change_reason: string;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/types/api.ts` around lines 116 - 128, The
ContextOverride interface defines the property as override_ but the backend wire
name is override; update the ContextOverride type to use override: Overrides
(matching the existing PutContextRequest usage) so the client deserializes the
same field the server sends; locate the ContextOverride interface and replace
the override_ property with override (type Overrides) and ensure any
usages/serializations reference ContextOverride.override.

Comment on lines +185 to +205
export interface Experiment {
id: string;
created_at: string;
created_by: string;
last_modified: string;
name: string;
experiment_type: ExperimentType;
override_keys: string[];
status: ExperimentStatus;
traffic_percentage: number;
started_at: string | null;
started_by: string | null;
context: Condition;
variants: Variant[];
last_modified_by: string;
chosen_variant: string | null;
description: string;
change_reason: string;
metrics: Record<string, JsonValue>;
metrics_url?: string;
experiment_group_id: string | null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Align experiment identifiers with the backend's numeric contract.

The backend Experiment model in crates/superposition_types/src/database/models/experimentation.rs:275-303 exposes id: i64 and experiment_group_id: Option<i64>, so the JSON payload is numeric. Typing Line 186 and Line 205 as strings drifts from that contract and will leak string/number casts into every consumer unless the client normalizes these values before exposure.

Suggested fix
 export interface Experiment {
-  id: string;
+  id: number;
   created_at: string;
   created_by: string;
   last_modified: string;
   name: string;
   experiment_type: ExperimentType;
   override_keys: string[];
   status: ExperimentStatus;
   traffic_percentage: number;
   started_at: string | null;
   started_by: string | null;
   context: Condition;
   variants: Variant[];
   last_modified_by: string;
   chosen_variant: string | null;
   description: string;
   change_reason: string;
   metrics: Record<string, JsonValue>;
   metrics_url?: string;
-  experiment_group_id: string | null;
+  experiment_group_id: number | null;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/types/api.ts` around lines 185 - 205, The Experiment
interface incorrectly types the identifiers as strings; update the Experiment.id
from string to number and Experiment.experiment_group_id from string | null to
number | null to match the backend i64/Option<i64> contract (locate these in the
Experiment interface declaration), and ensure any client code that assumes
string IDs is adjusted to accept numeric IDs (or normalize at the API boundary)
so consumers no longer need to cast between string/number.

Comment on lines +15 to +18
export function formatJsonPreview(value: unknown, maxLength = 80): string {
const str = JSON.stringify(value);
if (str.length <= maxLength) return str;
return str.slice(0, maxLength - 3) + "...";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

formatJsonPreview can throw for valid unknown inputs.

JSON.stringify(undefined) returns undefined, so str.length crashes at runtime.

Suggested fix
 export function formatJsonPreview(value: unknown, maxLength = 80): string {
-  const str = JSON.stringify(value);
+  const str = JSON.stringify(value) ?? String(value);
   if (str.length <= maxLength) return str;
   return str.slice(0, maxLength - 3) + "...";
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function formatJsonPreview(value: unknown, maxLength = 80): string {
const str = JSON.stringify(value);
if (str.length <= maxLength) return str;
return str.slice(0, maxLength - 3) + "...";
export function formatJsonPreview(value: unknown, maxLength = 80): string {
const str = JSON.stringify(value) ?? String(value);
if (str.length <= maxLength) return str;
return str.slice(0, maxLength - 3) + "...";
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/utils/format.ts` around lines 15 - 18, formatJsonPreview
can throw because JSON.stringify(value) may return undefined (e.g., for
undefined) so accessing .length crashes; update formatJsonPreview to coerce the
stringify result into a safe string (for example use const str =
JSON.stringify(value) ?? String(value) or String(value)) before checking length
and slicing, then continue to apply the maxLength logic; modify the
formatJsonPreview function to use that safe string variable so .length and
.slice are never called on undefined.

claude added 2 commits March 17, 2026 16:59
Adds demo/index.html with a toolbar to configure host, org, workspace,
auth token, and feature subset, then renders the embeddable admin UI
against a local Superposition instance. Run with `npm run demo`.

https://claude.ai/code/session_01MQDdbiwrn4RSwde6qAoNGE
Requests from the demo page to /api/* are proxied to localhost:8080,
so the browser never makes cross-origin requests. The host field now
defaults to /api.

https://claude.ai/code/session_01MQDdbiwrn4RSwde6qAoNGE
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.

2 participants