Skip to content

fix(console,runner): render the approvals inbox against one ticking clock, and lint both packages (#2927) - #2930

Merged
os-zhuang merged 1 commit into
mainfrom
fix/lint-2927-react-correctness
Jul 28, 2026
Merged

fix(console,runner): render the approvals inbox against one ticking clock, and lint both packages (#2927)#2930
os-zhuang merged 1 commit into
mainfrom
fix/lint-2927-react-correctness

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #2927. Refs #2911, #2923.

apps/console and packages/runner had no lint script, so turbo run lint skipped them silently and their 17 ESLint errors had never been seen. Both now carry "lint": "eslint .", and the DEBT list in scripts/check-lint-coverage.mjs is empty — 45/45 workspace packages are linted.

The issue framed these as "12 React-correctness bugs". Read one by one they are not one class of problem, and the count is smaller than it looked:

8x react-hooks/purity — real, and the fix is a behaviour fix

The approvals inbox read Date.now() mid-render for every age tint, "5m ago" label and SLA chip. Render must be pure: the output depended on when React happened to render, so it disagreed with itself under StrictMode's double render — and, more visibly, it froze. An inbox left open kept saying "just now"; an SLA countdown never counted down.

The page now renders against a single now held in state and advanced once a minute (the finest granularity anything here displays — "5m ago", "36h", "3d"). Render becomes a pure function of props+state and the figures actually tick. Note a mount-frozen constant would have been worse than the impure read, so the interval is part of the correct fix, not scope creep.

sla_due_at now goes through a parse guard as well: a due date in a shape Date.parse can't read used to render as "SLA NaNh left"; it now renders nothing.

1x react-hooks/static-components — real

StatusBadge was declared inside ApprovalsInboxPage, so it was a brand-new component type on every render and React unmounted/remounted every status chip in the table on each re-render. Hoisted to module scope, translated label passed as a prop.

6x react-hooks/static-components — false positives

3 in the console's settings pages, 3 in the runner's LayoutRenderer. All six render the result of getIcon / getLazyIcon, which memoises per name in a module-level cache — the component reference is stable across renders and nothing is created during render. The rule cannot see through the call.

They carry the same targeted eslint-disable-next-line + justification the repo already uses at a dozen icon-registry sites (MetricCard, NavigationRenderer, action-group, …), and both resolvers now document the cache so the next reader doesn't have to re-derive it.

This is the part of the issue that needed checking rather than believing. The issue called these "the user-visible one … forms, where state loss is most noticeable", and asked whether known Settings flakiness traced to them. It does not: typing 17 characters into the Company → Legal name field keeps focus and every character (screenshot below). Nothing was being reset.

2 minor

  • auth-preflight.ts — dead token initializer. Read rather than blind-deleted, as the issue asked: no intended write is missing; every path out of the try/catch either assigns or returns. Dropped the initializer and documented why.
  • sdui-workbench-preview.tsxprefer-const.

Verification

  • apps/console 0 errors, packages/runner 0 errors (were 14 / 3).
  • node scripts/check-lint-coverage.mjs45/45 packages linted, 0 with outstanding errors.
  • The gate was proved red before being trusted green: injecting a prefer-const into console and a Date.now()-in-render into runner fails turbo run lint in both (Failed: @object-ui/console#lint, @object-ui/runner#lint); reverting returns both to 0 errors.
  • tsc --noEmit clean in both packages; vitest 91/91 in console, runner green.
  • Browser-verified against a live showcase backend (console dev server on :5182):
    • approvals list — status chips, relative time, and all three SLA branches: SLA 已超期 2d in red, SLA 剩余 30h muted, and nothing at all for an unparsable due date (the new guard);
    • drawer — same three branches at the second SLA site;
    • relative time observed ticking 3 → 4 → 5 分钟前 without any other interaction, i.e. the clock advances on its own now;
    • settings hub / view / field render their icons, and the typing-focus check above.
    • Pre-existing console noise (HTTP request failed, a duplicate-React-key warning) reproduces identically with ApprovalsInboxPage.tsx swapped back to its main version, so it is not from this change — filed separately.

🤖 Generated with Claude Code

…lock, and lint both packages (#2927)

`apps/console` and `packages/runner` had no `lint` script, so `turbo run lint`
skipped them silently and their 17 ESLint errors had never been seen. Both now
carry `"lint": "eslint ."`, and the `DEBT` list in check-lint-coverage.mjs is
empty — 45/45 workspace packages are linted.

Read one by one, the 17 were not one class of problem:

8x react-hooks/purity — real. The approvals inbox read `Date.now()` mid-render
for every age tint, "5m ago" label and SLA chip. Render must be pure: the output
depended on when React happened to render, so it disagreed with itself under
StrictMode's double render and then froze — an inbox left open kept saying "just
now" and an SLA countdown never counted down. The page now renders against a
single `now` held in state and advanced once a minute (the finest granularity it
displays), so render is a pure function of props+state and the figures tick.
`sla_due_at` also goes through a parse guard now: a due date the backend sends in
a shape `Date.parse` can't read rendered as "SLA NaNh left"; it renders nothing.

1x react-hooks/static-components — real. `StatusBadge` was declared inside
`ApprovalsInboxPage`, so it was a brand-new component type every render and React
remounted every status chip in the table on each re-render. Hoisted to module
scope with the translated label passed as a prop.

6x react-hooks/static-components — false positives (3 settings pages, 3 in the
runner's LayoutRenderer). All six render the result of `getIcon`/`getLazyIcon`,
which memoises per name in a module-level cache: the reference is stable and
nothing is created during render. They carry the same targeted disable +
justification the repo already uses at a dozen icon-registry sites, and both
resolvers now document the cache. Verified rather than assumed — typing into a
settings field keeps focus and every character, so nothing was being reset.

2 minor — a dead `token` initializer on the auth-preflight path (read, not blind
-deleted: no intended write was missing) and a `prefer-const`.

Closes #2927. Refs #2911, #2923.

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

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Jul 28, 2026 3:41pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 27.9 KB 350 KB
Entry file index-Bfr_-5Cn.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.20KB 2.97KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.25KB 1.01KB
auth (org-roles.js) 6.72KB 2.85KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 450.13KB 97.92KB
core (index.js) 2.12KB 0.77KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 127.78KB 32.15KB
fields (index.js) 220.07KB 53.90KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 25.17KB 5.80KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.45KB 10.67KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 4.42KB 1.27KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 1.77KB 0.77KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 6.84KB 2.42KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.90KB 12.35KB
plugin-charts (index.js) 57.26KB 16.24KB
plugin-chatbot (index.js) 179.93KB 42.67KB
plugin-dashboard (index.js) 109.60KB 28.33KB
plugin-designer (index.js) 210.56KB 42.56KB
plugin-detail (index.js) 216.77KB 53.01KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 103.47KB 25.10KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 179.25KB 46.97KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 98.67KB 23.35KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 37.07KB 9.81KB
plugin-timeline (index.js) 25.03KB 7.11KB
plugin-tree (index.js) 8.36KB 2.81KB
plugin-view (index.js) 85.68KB 20.85KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.55KB 0.67KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 3.19KB 1.38KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 18.70KB 6.09KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.00KB 0.55KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 2.16KB 0.94KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 0.77KB 0.41KB
types (disclosure.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 1.86KB 0.91KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.04KB 1.93KB
types (system-fields.js) 2.39KB 1.17KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 0.75KB 0.46KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-zhuang
os-zhuang merged commit b076050 into main Jul 28, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the fix/lint-2927-react-correctness branch July 28, 2026 15:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console + runner: 17 ESLint errors that were never seen, incl. 12 React-correctness bugs

1 participant