Skip to content

Fix React #418 hydration abort from clock-derived relative times#169

Merged
isuttell merged 3 commits into
mainfrom
fix/ap-122-react-418-hydration-relative-time
Jun 2, 2026
Merged

Fix React #418 hydration abort from clock-derived relative times#169
isuttell merged 3 commits into
mainfrom
fix/ap-122-react-418-hydration-relative-time

Conversation

@isuttell

@isuttell isuttell commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Every authenticated page threw React hydration error #418 and the entire app was non-interactive — clicking "Create key", revoking, navigating, anything, did nothing. Reproduced live on deployed preview and production (/keys, /dashboard).

Root cause: formatRelativeTime() called Date.now() at render time. The SSR server (Cloudflare Worker, UTC) renders a relative string like "2 minutes ago"; by the time the browser hydrates, the clock has advanced and it computes a different string. React treats the text mismatch as error #418, throws away the server-rendered tree, and never attaches event handlers — so the page paints but no button works.

This was rendered at 8 SSR call sites (dashboard, keys, artifacts list + detail, audit, admin lockdowns/events), so a mismatch on the landing dashboard alone killed interactivity everywhere.

Note on AP-122: the ticket was filed hypothesizing the /claim ?token= controlled-input as the cause. That specific claim-page mismatch was already fixed by the merged #165. The actual still-live #418 — the one that blocked API-key creation — is this separate clock-derived relative-time bug. Confirmed via live console on /keys and /dashboard.

Changes

  • New <RelativeTime> component: renders a deterministic, UTC-pinned absolute timestamp on the server and first client paint (byte-identical both sides), then upgrades to a live relative string after mount; suppressHydrationWarning + <time dateTime> for a11y.
  • formatAbsoluteTime() — clock- and timezone-independent formatter for the SSR/first-paint value.
  • formatRelativeTime(input, now?)now is now injectable so a single timestamp can be pinned.
  • useHydrated() hook — gates the keys table's Date.now() expiry check so the Active/Expired badge doesn't read the wall clock until after mount.
  • Routed all 8 SSR call sites through <RelativeTime>.
  • Tests: RelativeTime.test.tsx (asserts identical server/client renderToString output — direct feat(api): runtime-tunable ephemeral provision cap via KV (AP-174) #418 regression guard) and new format cases for determinism.

Risk: LOW

  • Areas touched: web frontend time rendering only (no API, schema, auth, or secrets).
  • Security: none.
  • Performance: negligible (one post-mount effect per timestamp, same as before).
  • Breaking: none. Timestamps now show an absolute UTC string for ~one frame before upgrading to relative; the title is now human-readable instead of raw ISO.

Test plan

Issue: AP-122

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Unified, hydration-safe Relative Time display across admin, operator events, audit logs, artifacts, dashboard, and keys to ensure stable, consistent timestamps and tooltips.
    • Keys view delays expiry checks until after hydration, preventing premature "Expired" labels and showing stable "never"/expiry states.
  • Testing

    • Added/expanded tests for SSR/client stability and deterministic absolute/relative time formatting.

@linear-code

linear-code Bot commented Jun 2, 2026

Copy link
Copy Markdown
AP-122 Fix React #418 hydration error on the ephemeral claim page

Outcome

The authenticated ephemeral claim page (/claim) loads without a React hydration error. Today every load throws Minified React error #418 (a text-content hydration mismatch) in the browser console.

Evidence (observed live on preview)

Deployed app.preview.agent-paste.sh/claim (web Worker version eee98f47, branch chore/ap-105-turnstile-real-sitekeys), authenticated session. Every page load logs:

Error: Minified React error #418
  https://react.dev/errors/418  (text content does not match server-rendered HTML)
  at xa / zm / w1 / Zc ... (apps/web/src/assets/index-*.js)

It fires independently of Turnstile state (reproduced before and after the Turnstile widget started passing), so it is not related to the Turnstile fix — it is a pre-existing defect on the surface AP-105 shipped.

Likely cause

apps/web/src/routes/_authed.claim.tsx:

  • Line 48: const [claimToken, setClaimToken] = useState(token) seeds state from the ?token= search param.
  • Lines 45 + 55-56: Route.useSearch() + a useEffect(() => setClaimToken(token), [token]).

#418 is a server/client text divergence. Prime suspects to confirm:

  1. The token input's server-rendered value/markup differs from the first client render (SSR sees one ?token= resolution, client hydrates with another), or
  2. A conditionally rendered text node (error/success/label) whose server and client branches differ on first paint.

In scope

  • Diagnose the exact mismatching node (run a non-minified/dev build to get the full feat(api): runtime-tunable ephemeral provision cap via KV (AP-174) #418 message and the offending text).
  • Make SSR and first client render produce identical markup (e.g. derive the input value deterministically from the same search source on both, avoid first-render-only branches, or gate the divergent node behind a mounted flag / suppressHydrationWarning only as a last resort).
  • Keep the existing claim behavior, Turnstile wiring, and the ?token= deep-link prefill intact.

Out of scope

  • Turnstile / wrangler site-key changes (already done).
  • Any claim endpoint / API change.

Acceptance criteria

Required checks

pnpm lint, pnpm typecheck, pnpm test, pnpm smoke:web.

Dependencies

Surfaced during post-merge preview verification of AP-105 (claim endpoint). No blocker; AP-105 is merged.

Review in Linear

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Replace render-time relative formatting with a hydration-safe RelativeTime component, add formatAbsoluteTime and injectable formatRelativeTime, introduce useHydrated, and update UI components, routes, and tests to avoid SSR/client content mismatches.

Changes

Relative Time Hydration Fix

Layer / File(s) Summary
Format utilities and hydration hook
apps/web/src/lib/format.ts, apps/web/src/lib/use-hydrated.ts, apps/web/test/format.test.ts
formatRelativeTime accepts optional now; added formatAbsoluteTime; new useHydrated hook; tests for deterministic UTC formatting, injectable now, and invalid-date handling.
RelativeTime component and tests
apps/web/src/components/ui/RelativeTime.tsx, apps/web/test/RelativeTime.test.tsx
New RelativeTime renders absolute UTC-stable timestamp for SSR/first paint and upgrades to relative text after mount; includes toIso and tests for SSR/client stability and invalid input.
Dashboard timestamp columns
apps/web/src/components/dashboard/RecentArtifacts.tsx, apps/web/src/components/dashboard/RecentAudit.tsx
Replace formatRelativeTime calls with <RelativeTime value=... /> for "Last published" and "Time" columns; update imports and remove title-based tooltips.
Admin panel timestamp rendering
apps/web/src/components/admin/LockdownList.tsx, apps/web/src/components/admin/OperatorEventsPanel.tsx
Render "Set at" and "Time" via RelativeTime; consolidate muted styling and apply max width to change summary cell in OperatorEventsPanel.
Keys table with hydration-gated expiry
apps/web/src/components/keys/KeysTable.tsx, apps/web/test/KeysTable.test.tsx
Use useHydrated() to gate Date.now()-based expiry checks; keyState accepts hydrated; render last-used/expires with RelativeTime; tests include future-expiry fixture and relaxed Active assertion.
Route pages timestamp updates
apps/web/src/routes/_authed.artifacts.$artifactId.tsx, apps/web/src/routes/_authed.artifacts.index.tsx, apps/web/src/routes/_authed.audit.tsx
Replace formatRelativeTime with RelativeTime across artifact and audit pages; update imports and remove title tooltips.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • zaks-io/agent-paste#70: Introduces the LockdownList component whose timestamp rendering this PR updates to use RelativeTime.
  • zaks-io/agent-paste#46: Adds RecentArtifacts/RecentAudit implementations that this PR migrates from formatRelativeTime to RelativeTime.

Poem

🐰 I render time with a gentle hop,

Absolute first, then relative on top.
No more hydration mismatches in sight,
Buttons stay lively, the UI feels right.
A tiny rabbit fixed rendering tonight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix React #418 hydration abort from clock-derived relative times' accurately summarizes the main problem and solution in the changeset.
Linked Issues check ✅ Passed All acceptance criteria from AP-122 are met: the PR eliminates #418 hydration errors via RelativeTime component, adds deterministic formatAbsoluteTime, covers all 8 SSR call sites, and includes regression tests.
Out of Scope Changes check ✅ Passed All changes directly address the hydration issue: RelativeTime component, format helpers, useHydrated hook, and updates to 8 SSR call sites are all within scope of fixing the root cause.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ap-122-react-418-hydration-relative-time

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/web/test/RelativeTime.test.tsx (1)

12-19: ⚡ Quick win

This test isn't exercising the hydrated client path.

Lines 13-15 call renderToString() twice, so both sides are using the server renderer and neither side runs the client hydration flow. That makes this weaker than the test name suggests for guarding the #418 regression. I'd switch this to a real hydrateRoot assertion (or equivalent DOM hydration check) so the test actually covers server markup vs first hydrated client render.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/test/RelativeTime.test.tsx` around lines 12 - 19, The test currently
calls renderToString twice so it never exercises client hydration; change it to
render the server markup with renderToString(<RelativeTime value={ISO} />),
inject that HTML into a DOM container, then hydrate the client into that
container using hydrateRoot (from react-dom/client) or an equivalent hydrate
method with <RelativeTime value={ISO} /> and assert that the container.innerHTML
(or rendered DOM) after hydration equals the original server markup and still
does not contain relative phrases (no /ago|in /) and does contain the absolute
year (/2026/); update imports/setup in RelativeTime.test.tsx accordingly and
replace the second renderToString call with the hydration flow so the test
validates server vs first-client paint behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/components/ui/RelativeTime.tsx`:
- Around line 16-20: The component currently snapshots the relative label once
in the useEffect (setting state with setRelative(formatRelativeTime(value))) and
never updates it, causing timestamps to freeze; change the effect in
RelativeTime (the useEffect that references value, setRelative, and
formatRelativeTime) to set up a timer (setInterval) that recomputes
setRelative(formatRelativeTime(value)) at an appropriate cadence (e.g., every
second or every minute depending on precision) and return a cleanup that clears
the interval to avoid leaks so the label updates live after hydration.

---

Nitpick comments:
In `@apps/web/test/RelativeTime.test.tsx`:
- Around line 12-19: The test currently calls renderToString twice so it never
exercises client hydration; change it to render the server markup with
renderToString(<RelativeTime value={ISO} />), inject that HTML into a DOM
container, then hydrate the client into that container using hydrateRoot (from
react-dom/client) or an equivalent hydrate method with <RelativeTime value={ISO}
/> and assert that the container.innerHTML (or rendered DOM) after hydration
equals the original server markup and still does not contain relative phrases
(no /ago|in /) and does contain the absolute year (/2026/); update imports/setup
in RelativeTime.test.tsx accordingly and replace the second renderToString call
with the hydration flow so the test validates server vs first-client paint
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1e5981eb-cb69-4727-b3c0-fac6af3d2fa7

📥 Commits

Reviewing files that changed from the base of the PR and between 1c3b14b and 8c19708.

📒 Files selected for processing (13)
  • apps/web/src/components/admin/LockdownList.tsx
  • apps/web/src/components/admin/OperatorEventsPanel.tsx
  • apps/web/src/components/dashboard/RecentArtifacts.tsx
  • apps/web/src/components/dashboard/RecentAudit.tsx
  • apps/web/src/components/keys/KeysTable.tsx
  • apps/web/src/components/ui/RelativeTime.tsx
  • apps/web/src/lib/format.ts
  • apps/web/src/lib/use-hydrated.ts
  • apps/web/src/routes/_authed.artifacts.$artifactId.tsx
  • apps/web/src/routes/_authed.artifacts.index.tsx
  • apps/web/src/routes/_authed.audit.tsx
  • apps/web/test/RelativeTime.test.tsx
  • apps/web/test/format.test.ts

Comment thread apps/web/src/components/ui/RelativeTime.tsx
isuttell and others added 2 commits June 2, 2026 13:46
…hydration abort

formatRelativeTime() read Date.now() during render, so SSR produced a
different relative string than the client's first hydration paint
("2 minutes ago" vs "3 minutes ago"). React treated the text mismatch as
hydration error #418, discarded the server-rendered tree, and never
attached event handlers — leaving every button in the app dead. This is
why creating an API key (and any other action) did nothing on the
deployed dashboard/keys pages.

Add a <RelativeTime> component that renders a deterministic, UTC-pinned
absolute timestamp on the server and first client paint, then upgrades to
a live relative string after mount. Route all 8 SSR call sites (dashboard,
keys, artifacts list/detail, audit, admin lockdowns/events) through it,
and gate the keys table's Date.now() expiry check behind useHydrated().

Issue: AP-122

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI Validate failed on the branch-coverage gate (79.99% vs 80%). The
uncovered branch was RelativeTime's invalid-date path in toIso(). Adding
that test surfaced a real latent crash: formatRelativeTime() fed an
unparseable value throws RangeError from Intl.RelativeTimeFormat. Guard
it to return "" (matching formatAbsoluteTime), and cover the keyState
not-yet-expired branch in KeysTable. Branch coverage back to 80.04%.

Issue: AP-122

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

isuttell commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@isuttell isuttell force-pushed the fix/ap-122-react-418-hydration-relative-time branch from e681519 to 137f217 Compare June 2, 2026 20:50
@isuttell

isuttell commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/web/src/components/ui/RelativeTime.tsx (1)

16-20: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the relative label live after hydration.

Line 19 snapshots the relative text once, so mounted labels can stay stuck at "just now" / "2m ago" indefinitely, and a prop change can briefly keep showing the old label until the effect reruns. This needs a timer-driven refresh (or shared ticker) with cleanup, not a one-time post-mount write.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/ui/RelativeTime.tsx` around lines 16 - 20, The
RelativeTime component currently snapshots the label once in the useEffect
(setRelative(formatRelativeTime(value))) causing stale text; change the effect
to set the initial relative value immediately and then start a timer (or
subscribe to a shared ticker) that updates
setRelative(formatRelativeTime(value)) periodically (e.g., every 30s or
appropriate granularity) and ensure you clear the interval/subscription in the
cleanup function; make the effect depend on value so the timer resets when value
changes and avoid memory leaks by clearing on unmount.
🧹 Nitpick comments (1)
apps/web/test/KeysTable.test.tsx (1)

90-90: 💤 Low value

Consider a more precise assertion for Active badge count.

With the test fixture now containing exactly two Active keys (null expiry + future expiry), using toHaveLength(2) would catch regressions more reliably than > 0.

♻️ Suggested change
-    expect(screen.getAllByText("Active").length).toBeGreaterThan(0);
+    expect(screen.getAllByText("Active")).toHaveLength(2);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/test/KeysTable.test.tsx` at line 90, Replace the imprecise assertion
that checks Active badge count with an exact length check: change the assertion
that uses screen.getAllByText("Active").length with toBeGreaterThan(0) to assert
the exact expected count (toHaveLength(2)) so the test verifies there are
exactly two "Active" badges given the fixture (null expiry + future expiry);
update the assertion in KeysTable.test.tsx where screen.getAllByText("Active")
is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/lib/format.ts`:
- Around line 39-47: formatAbsoluteTime currently fixes locale and timeZone but
leaves hour formatting and the timeZoneName label to runtime Intl defaults,
causing non-deterministic server vs browser output; update formatAbsoluteTime to
explicitly set hour12 (or hourCycle) to a concrete value (e.g., hour12: false
for 24-hour) to ensure byte-stable output, and if you need fully deterministic
labels across environments, produce the string by using
Intl.DateTimeFormat.prototype.formatToParts and assemble the parts yourself
rather than relying on the final formatted string.

---

Duplicate comments:
In `@apps/web/src/components/ui/RelativeTime.tsx`:
- Around line 16-20: The RelativeTime component currently snapshots the label
once in the useEffect (setRelative(formatRelativeTime(value))) causing stale
text; change the effect to set the initial relative value immediately and then
start a timer (or subscribe to a shared ticker) that updates
setRelative(formatRelativeTime(value)) periodically (e.g., every 30s or
appropriate granularity) and ensure you clear the interval/subscription in the
cleanup function; make the effect depend on value so the timer resets when value
changes and avoid memory leaks by clearing on unmount.

---

Nitpick comments:
In `@apps/web/test/KeysTable.test.tsx`:
- Line 90: Replace the imprecise assertion that checks Active badge count with
an exact length check: change the assertion that uses
screen.getAllByText("Active").length with toBeGreaterThan(0) to assert the exact
expected count (toHaveLength(2)) so the test verifies there are exactly two
"Active" badges given the fixture (null expiry + future expiry); update the
assertion in KeysTable.test.tsx where screen.getAllByText("Active") is used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4bc93fd5-8d32-49b4-9c0c-952f415c1c6a

📥 Commits

Reviewing files that changed from the base of the PR and between cdc13f3 and 137f217.

📒 Files selected for processing (14)
  • apps/web/src/components/admin/LockdownList.tsx
  • apps/web/src/components/admin/OperatorEventsPanel.tsx
  • apps/web/src/components/dashboard/RecentArtifacts.tsx
  • apps/web/src/components/dashboard/RecentAudit.tsx
  • apps/web/src/components/keys/KeysTable.tsx
  • apps/web/src/components/ui/RelativeTime.tsx
  • apps/web/src/lib/format.ts
  • apps/web/src/lib/use-hydrated.ts
  • apps/web/src/routes/_authed.artifacts.$artifactId.tsx
  • apps/web/src/routes/_authed.artifacts.index.tsx
  • apps/web/src/routes/_authed.audit.tsx
  • apps/web/test/KeysTable.test.tsx
  • apps/web/test/RelativeTime.test.tsx
  • apps/web/test/format.test.ts

Comment thread apps/web/src/lib/format.ts
…t output (AP-122)

formatAbsoluteTime left hour12/hourCycle to the runtime Intl default, which
Cloudflare Workers SSR and the browser can resolve differently for the same
instant (09:30 vs 09:30 AM). Since AP-122's entire purpose is hydration-stable
time rendering, that non-determinism could reintroduce the React #418 mismatch
the PR fixes. Pin hour12:false so the hour format is identical across runtimes.

Add byte-lock tests asserting the exact UTC string for a morning and an
afternoon instant so a future runtime-default drift is caught here instead of
as a hydration abort in production.

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

isuttell commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/web/src/components/ui/RelativeTime.tsx (1)

18-20: ⚡ Quick win

Consider adding a timer to keep relative labels fresh.

The component currently snapshots the relative text once after mount (line 19) and updates only when value changes. If a user stays on the page, "2 minutes ago" will never update to "3 minutes ago" as time passes. While the hydration fix is complete, consider setting up a setInterval in the useEffect to periodically recompute the relative string for better UX.

⏱️ Suggested enhancement for live-updating labels
 useEffect(() => {
   setRelative(formatRelativeTime(value));
+  const interval = setInterval(() => {
+    setRelative(formatRelativeTime(value));
+  }, 60_000); // Refresh every minute
+  return () => clearInterval(interval);
 }, [value]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/ui/RelativeTime.tsx` around lines 18 - 20, The effect
that sets relative time (useEffect updating
setRelative(formatRelativeTime(value))) only runs on value changes; add a
setInterval inside the same useEffect to recompute and call
setRelative(formatRelativeTime(value)) periodically (e.g., every 30–60s) so
labels stay fresh, and return a cleanup that clears the interval to avoid leaks;
keep value in the dependency array so the label updates immediately when value
changes and the interval uses the latest value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/web/src/components/ui/RelativeTime.tsx`:
- Around line 18-20: The effect that sets relative time (useEffect updating
setRelative(formatRelativeTime(value))) only runs on value changes; add a
setInterval inside the same useEffect to recompute and call
setRelative(formatRelativeTime(value)) periodically (e.g., every 30–60s) so
labels stay fresh, and return a cleanup that clears the interval to avoid leaks;
keep value in the dependency array so the label updates immediately when value
changes and the interval uses the latest value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 015b743f-94b3-42d1-a464-c23e2d824e8e

📥 Commits

Reviewing files that changed from the base of the PR and between cdc13f3 and 678a19d.

📒 Files selected for processing (14)
  • apps/web/src/components/admin/LockdownList.tsx
  • apps/web/src/components/admin/OperatorEventsPanel.tsx
  • apps/web/src/components/dashboard/RecentArtifacts.tsx
  • apps/web/src/components/dashboard/RecentAudit.tsx
  • apps/web/src/components/keys/KeysTable.tsx
  • apps/web/src/components/ui/RelativeTime.tsx
  • apps/web/src/lib/format.ts
  • apps/web/src/lib/use-hydrated.ts
  • apps/web/src/routes/_authed.artifacts.$artifactId.tsx
  • apps/web/src/routes/_authed.artifacts.index.tsx
  • apps/web/src/routes/_authed.audit.tsx
  • apps/web/test/KeysTable.test.tsx
  • apps/web/test/RelativeTime.test.tsx
  • apps/web/test/format.test.ts

@isuttell isuttell merged commit 8dcb942 into main Jun 2, 2026
5 checks passed
@isuttell isuttell deleted the fix/ap-122-react-418-hydration-relative-time branch June 2, 2026 21:15
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown

agent-paste PR preview resources were cleaned up. The shared Preview GitHub Environment is retained for future preview deploys.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant