Skip to content

feat: redesign Auto Approve panel, move PR controls to top, show review time in local zone - #147

Merged
LinceMathew merged 2 commits into
masterfrom
feat/auto-approve-ui-and-pr-layout
Oct 29, 2026
Merged

LinceMathew merged 2 commits into
masterfrom
feat/auto-approve-ui-and-pr-layout

Conversation

@LinceMathew

@LinceMathew LinceMathew commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

What

Three related changes around auto-approve.

1. Auto Approve tab (review page)

  • Stage rows sit on one aligned grid: status icon, stage name, reason, actions.
  • Status icons are solid circles: green check (pass), red cross (fail), yellow dash (skipped). The green uses #22C55E directly because tailwind.config.js defines green as one flat colour, so bg-green-500 generates no CSS.
  • Final decision is bold with a stronger divider above it.
  • Owners/admins get Configure on every stage except Final decision. It opens that stage's sub-tab in Settings → Auto Approve Policies (/settings?subtab=<tab>#auto-approve). The Eligibility row also has Your level, which opens the viewer's own user settings.
  • Clicking a stage row opens its details. The Details button stays for keyboard users.

2. PR/MR description layout

  • The controls (Start a review, Review history, policy links) are now a separate block at the top of the description, above a --- line.
  • The "LiveReview auto-approval decisions" table stays at the bottom in its own block.
  • Old single-block PRs are migrated on their next review: the old block is removed, then the two new blocks are written. Text the author wrote is kept.

3. Review time in the decisions table

  • New Time column, shown in the time zone of whoever started the review (UTC if none saved).
  • New nullable users.time_zone column, plus PUT /users/time-zone (session only, validates the IANA name, writes only when the value changed).
  • The UI sends the browser's zone once per user and only again if it changes (remembered in localStorage).

Migration

db/migrations/20261029120000_add_user_time_zone.sql adds users.time_zone. It must run before the new PR table query works.

Docs

Updated routes_guide/reviews/review-detail.md, routes_guide/reviews/pr-review-link.md and docs/pr-review-links-plan.md.

Checked

  • npx tsc --noEmit passes.
  • go vet passes on internal/jobqueue and internal/api/users. internal/api was not vetted.
  • The migration was applied to a local dev database.
  • No automated tests added or run.

Not checked

  • The new UI in a browser after the last edits.
  • The PR description rewrite on a real PR for each provider.

🔍 LiveReview

Auto-approval decisions

Review Decision Reason
#13489 ❌ Not approved Author level 1 < required 2
#13487 ❌ Not approved Author level 1 < required 2

⚙️ Customize risk-based approval policy for this repository or your organization.

@livereview-agent

Copy link
Copy Markdown

Add User Time Zone Support

Overview

This change introduces user time zone preferences. It stores time zone data, provides an API for updates, and automatically syncs client-side. This enables displaying time-sensitive information, like review timestamps, in the user's local time.

Technical Highlights

  • db/migrations/444-28-734320000_add_user_time_zone.sql: Adds a nullable time_zone column to the users table.
  • internal/api/server.go: Registers a new PUT /users/time-zone endpoint for authenticated users.
  • internal/api/users/profile_handlers.go: Implements UpdateTimeZone handler, validating IANA time zones.
  • ui/src/hooks/useSyncTimeZone.ts: Introduces a new hook to detect and persist browser time zones.
  • internal/jobqueue/repo_sync_worker.go: Refactors PR description updates to support time zone-adjusted timestamps.
  • docs/pr-review-links-plan.md: Documents the new PR content structure with time zone-adjusted times.

Impact

  • Functionality: Users now see review timestamps localized to their preferred time zone.
  • Risk: Incorrect time zone display may occur if client-side detection or server-side validation fails.

@@ -0,0 +1,5 @@
-- migrate:up
ALTER TABLE users ADD COLUMN IF NOT EXISTS time_zone TEXT;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: warning

You should add a NOT NULL constraint if the time_zone field is always required, or alternatively, provide a default value for it.

Suggestions:

  1. Consider adding NOT NULL constraint and a default value (e.g., 'UTC') if a time zone is always expected for users.
  2. If NULL is acceptable, ensure application logic handles NULL time zones gracefully (e.g., falling back to UTC).

Comment thread internal/api/users/profile_handlers.go Outdated
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request body")
}
if _, err := time.LoadLocation(req.TimeZone); err != nil || req.TimeZone == "" || req.TimeZone == "Local" {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: warning

It would be beneficial to validate the length of req.TimeZone to prevent excessively long strings from being submitted.

Suggestions:

  1. Add a length check for req.TimeZone to prevent potential denial-of-service or storage issues with very long strings.

// ponytail: only the last 10 rows, so the table stays under description size limits (Azure DevOps: 4000 chars).
`SELECT id, policy, forced_reason FROM (
SELECT r.id, r.metadata->'policy_result' AS policy,
`SELECT id, created_at, policy, forced_reason, time_zone FROM (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: warning

Please add r.user_email to the select list for the time_zone subquery.

Suggestions:

  1. Add r.user_email to the SELECT clause of the outer query. The time_zone subquery needs r.user_email to link to the users table, but r.user_email is not available in the outer query's SELECT list. This will cause a SQL error.

func formatReviewTime(t time.Time, timeZone sql.NullString) string {
if timeZone.Valid {
if loc, err := time.LoadLocation(timeZone.String); err == nil {
return t.In(loc).Format("2 Jan 2006, 15:04 MST")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: info

Consider using time.RFC822 or time.RFC1123Z for a more standard date and time format.

Suggestions:

  1. Use a standard time format constant like time.RFC822 or time.RFC1123Z for consistency and easier parsing if this output is consumed by other systems.
  2. Ensure the chosen format is clear and unambiguous for users across different locales.

Comment thread internal/jobqueue/repo_sync_worker.go Outdated
section = strings.TrimRight(section, "\n")
j := strings.Index(body[i:], end)
if j < 0 {
return body[:i] + strings.TrimRight(section, "\n"), true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: warning

You need to handle the case where the end marker is missing after the start marker has been found.

Suggestions:

  1. If j < 0, the end marker is missing. The current logic return body[:i] + strings.TrimRight(section, "\n"), true effectively truncates the body after the start marker and appends the new section. This might silently delete content if the end marker is expected but absent. Consider logging a warning or returning false for ok in this specific case to indicate a partial replacement.

return strings.TrimRight(next, "\n") + "\n"
}
if i := strings.Index(body, legacySectionStart); i >= 0 {
return strings.TrimRight(body[:i], "\n") + "\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: warning

Ensure that body[:i] does not inadvertently remove content that appears before legacySectionStart.

Suggestions:

  1. The removeOldSection function for legacySectionStart (if i := strings.Index(body, legacySectionStart); i >= 0 { return strings.TrimRight(body[:i], "\n") + "\n" }) removes everything after the start of the legacy section. This assumes the legacy section was always at the very end of the PR description. If any content could appear after the legacy section but before the new sections, it would be deleted. Confirm this assumption is safe.

if strings.TrimSpace(body) == "" {
return section
}
return section + "\n" + strings.TrimLeft(body, "\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: warning

Make sure that strings.TrimLeft(body, "\n") does not remove more newlines than intended.

Suggestions:

  1. If body starts with multiple newlines, strings.TrimLeft(body, "\n") will remove all of them. This might lead to less vertical spacing than desired between the new controls section and the original PR body. Consider strings.TrimLeft(body, "\n") or strings.TrimSpace(body) if the intent is to normalize leading newlines, but be aware of potential formatting changes.

className={`grid grid-cols-[1.25rem_1fr] items-center gap-x-3 gap-y-2 text-sm md:grid-cols-[1.25rem_11rem_1fr_auto] ${canExpand ? 'cursor-pointer' : ''}`}
onClick={(e) => {
// Links and the Details button handle their own clicks.
if (canExpand && !(e.target as HTMLElement).closest('a,button')) toggle();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: warning

Regarding the click handler logic, please ensure that e.target.closest('a,button') correctly identifies clicks on nested interactive elements.

Suggestions:

  1. Thoroughly test click behavior for links/buttons inside expandable rows.

<span className="hidden w-[5.5rem] md:block" aria-hidden="true" />
)}
{canManageCurrentOrg && st.stage === 'eligibility' && viewerId && (
<Link to={`/settings/users/edit/${viewerId}`} className={actionClass} title="Open your user settings (review level)">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: warning

The path /settings/users/edit/${viewerId} is hardcoded. It would be better for maintainability to use a named route or a constant.

Suggestions:

  1. Use a named route or centralize path definitions.

</Link>
)}
{canManageCurrentOrg && settingsTab && (
<Link to={`/settings?subtab=${settingsTab}#auto-approve`} className={actionClass} title="Open this stage in the org policy settings">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: warning

The path /settings?subtab=${settingsTab}#auto-approve is hardcoded. It would be better for maintainability to use a named route or a constant.

Suggestions:

  1. Use a named route or centralize path definitions.

@livereview-agent livereview-agent 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.

❌ LiveReview policy: Not approved (blocked at Eligibility) · Review #13487

Stage Result Detail
Eligibility ❌ author level 1 < required 2
Static rules ⏭️ skipped (earlier stage blocked)
Risk score ⏭️ skipped (earlier stage blocked)
LLM classification ⏭️ skipped (earlier stage blocked)
Final decision ❌ eligibility: author level 1 < required 2

@livereview-agent

Copy link
Copy Markdown

Implement User Time Zone Localization and PR Description Structure

Overview

This change introduces user-specific time zone preferences. It localizes timestamp displays across the application. It also refactors Pull Request description content into distinct sections.

Technical Highlights

  • db/migrations/212-48-404280000_add_user_time_zone.sql: Adds a nullable time_zone TEXT column to the users table.
  • internal/api/server.go & internal/api/users/profile_handlers.go: Registers and implements a new PUT /users/time-zone API endpoint for time zone updates.
  • ui/src/hooks/useSyncTimeZone.ts: Introduces a new hook to detect and persist the user's browser time zone to the backend.
  • internal/jobqueue/repo_sync_worker.go: Refactors PR description update logic to manage new 'controls' and 'results' sections; formats review times using user's time zone.
  • docs/pr-review-links-plan.md & internal/docindex/docs/routes_guide/reviews/pr-review-link.md: Documents the new two-section layout for PR description content.
  • ui/src/components/reviews/AutoApprovePanel.tsx: Enhances UI with SVG icons, direct links to policy settings, and permission-based configuration visibility.

Impact

  • Functionality: Users see review timestamps in their local time; PR descriptions have a clearer, structured layout; review UI provides direct access to policy settings.
  • Risk: Existing PR descriptions require careful migration to the new two-section format; time zone detection and persistence can fail, leading to UTC fallback.

return err
}
if next := upsertPRSection(body, section); next != body {
if next := update(body); next != body {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: warning

This is a read-modify-write operation on the PR body. Concurrent updates could overwrite changes. Consider implementing optimistic locking or job serialization to handle this.

Suggestions:

  1. Implement optimistic locking using SCM API's ETag/versioning if available.
  2. Ensure job queue processing for a given PR is strictly serialized to prevent concurrent updates.

Comment thread ui/src/App.tsx
}
}, [dispatch, accessToken]);

useSyncTimeZone();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: warning

A new hook is being called. Please ensure that there are no blocking operations or critical errors introduced by this hook.

Suggestions:

  1. Verify useSyncTimeZone is non-blocking and handles its own errors gracefully, as it's called at the top level.


const verdictIcon = (v: string) =>
v === 'pass' || v === 'approved' ? '✅' : v === 'fail' || v === 'blocked' ? '❌' : '⏭️';
const VerdictIcon: React.FC<{ verdict: string }> = ({ verdict }) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: info

This is a new component. Please ensure that the accessibility attributes, specifically aria-label, are correctly implemented.

Suggestions:

  1. Confirm role='img' and aria-label are sufficient for screen readers. Consider adding title attributes for hover text.

'inline-flex items-center gap-1.5 rounded-md border border-slate-700 px-2 py-1 text-xs text-slate-300 ' +
'hover:border-slate-500 hover:text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-400 transition-colors';

const GearIcon = () => (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: info

This is a new component. Please ensure that the accessibility attributes, specifically aria-hidden, are correctly implemented.

Suggestions:

  1. Ensure aria-hidden='true' is appropriate for purely decorative icons. If the icon conveys meaning without text, a screen reader accessible alternative might be needed.

</svg>
);

const UserIcon = () => (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: info

This is a new component. Please ensure that the accessibility attributes, specifically aria-hidden, are correctly implemented.

Suggestions:

  1. Ensure aria-hidden='true' is appropriate for purely decorative icons. If the icon conveys meaning without text, a screen reader accessible alternative might be needed.

</svg>
);

const Chevron: React.FC<{ open: boolean }> = ({ open }) => (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: info

This is a new component. Please ensure that the accessibility attributes, specifically aria-hidden, are correctly implemented.

Suggestions:

  1. Ensure aria-hidden='true' is appropriate for purely decorative icons. If the icon conveys meaning without text, a screen reader accessible alternative might be needed.

className={`grid grid-cols-[1.25rem_1fr] items-center gap-x-3 gap-y-2 text-sm md:grid-cols-[1.25rem_11rem_1fr_auto] ${canExpand ? 'cursor-pointer' : ''}`}
onClick={(e) => {
// Links, the Details button and the info icon handle their own clicks.
if (canExpand && !(e.target as HTMLElement).closest('a,button,[aria-label="Help"]')) toggle();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: warning

This click handler appears to be complex. Please ensure that all interactive elements are correctly excluded from its scope.

Suggestions:

  1. Test thoroughly to ensure e.target.closest('a,button,[aria-label="Help"]') correctly identifies all interactive elements that should prevent the parent div's click handler from firing.
  2. Consider using e.stopPropagation() on the interactive elements themselves if the list of selectors becomes too complex.

<span className="hidden w-[5.5rem] md:block" aria-hidden="true" />
)}
{canManageCurrentOrg && st.stage === 'eligibility' && viewerId && (
<Link to={`/settings/users/edit/${viewerId}`} className={actionClass} title="Open your user settings (review level)">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: warning

The path /settings/users/edit/${viewerId} is hardcoded. This could lead to broken links if the path structure changes.

Suggestions:

  1. Use a named route or a centralized route helper to construct this path for better maintainability and to prevent broken links if the route structure changes.

<UserIcon /> Your level
</Link>
)}
{canManageCurrentOrg && settingsTab && (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: warning

The path /settings?subtab=${settingsTab}#auto-approve is hardcoded. This could lead to broken links if the path structure changes.

Suggestions:

  1. Use a named route or a centralized route helper to construct this path for better maintainability and to prevent broken links if the route structure changes.

const userId = useAppSelector((state) => state.Auth.user?.id);
useEffect(() => {
if (!userId) return;
const key = `lr_tz_synced:${userId}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: info

The format for the localStorage key is being used. Please ensure that the keys are unique and clearly indicate their purpose.

Suggestions:

  1. The key lr_tz_synced:${userId} is good for uniqueness. Ensure lr prefix is consistent with other app-specific localStorage keys.

let zone = '';
try {
zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (!zone || localStorage.getItem(key) === zone) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: info

Please check if localStorage.getItem(key) === zone is being used. This check can prevent redundant API calls.

Suggestions:

  1. Good use of localStorage to avoid unnecessary API calls when the time zone hasn't changed.

}
saveTimeZone(zone)
.then(() => {
try { localStorage.setItem(key, zone); } catch { /* storage blocked: retry next visit */ }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: info

Error handling for localStorage writes is implemented. This is good practice, especially for privacy or security settings.

Suggestions:

  1. Catching localStorage errors is good practice, as it can be blocked by browser settings or security policies.

.then(() => {
try { localStorage.setItem(key, zone); } catch { /* storage blocked: retry next visit */ }
})
.catch(() => { /* not critical: the PR table falls back to UTC */ });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: info

Error handling for API calls is implemented. This is beneficial for non-critical background tasks.

Suggestions:

  1. Explicitly catching and ignoring errors for a non-critical background task like time zone sync is appropriate, with a fallback to UTC.

const [overrideDraft, setOverrideDraft] = useState<Record<string, number>>({});
const saveTimers = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
const [outcomes, setOutcomes] = useState<OutcomeRow[]>([]);
const [subtab, setSubtab] = useState('general');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: warning

The initial state is being derived from the URL. Please ensure that input validation is performed for the subtab parameter.

Suggestions:

  1. Add validation for the subtab value retrieved from window.location.search to ensure it's one of the expected tab names. This prevents potential issues if an invalid or malicious value is injected into the URL.

@livereview-agent livereview-agent 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.

❌ LiveReview policy: Not approved (blocked at Eligibility) · Review #13489

Stage Result Detail
Eligibility ❌ author level 1 < required 2
Static rules ⏭️ skipped (earlier stage blocked)
Risk score ⏭️ skipped (earlier stage blocked)
LLM classification ⏭️ skipped (earlier stage blocked)
Final decision ❌ eligibility: author level 1 < required 2

…ew time in local zone

Auto Approve panel: aligned rows, status icons, Configure and Your level links, click a row to open its details. PR description: controls block at the top, decisions table at the bottom with a Time column in the review starter's saved time zone (new users.time_zone, saved once from the browser).

LiveReview Pre-Commit Check: vouched (iter:1, coverage:0%)
…ore info icon clicks

A missing end marker no longer deletes the text after the start marker. The time zone is capped at 64 characters. Clicking the info icon in a stage row no longer opens the row.

LiveReview Pre-Commit Check: vouched (iter:1, coverage:0%)
@LinceMathew
LinceMathew force-pushed the feat/auto-approve-ui-and-pr-layout branch from 1ad1271 to adae38a Compare October 29, 2026 19:40
@LinceMathew
LinceMathew merged commit 20ee5b4 into master Oct 29, 2026
15 checks passed
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