Skip to content

feat(web): eye-toggle on every password / secret field#1873

Merged
Aureliolo merged 3 commits into
mainfrom
worktree-password-toggle
May 11, 2026
Merged

feat(web): eye-toggle on every password / secret field#1873
Aureliolo merged 3 commits into
mainfrom
worktree-password-toggle

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

Adds a built-in show / hide affordance to every password / secret input across the dashboard.

  • web/src/components/ui/input-field.tsx: when type="password", renders an Eye / EyeOff button as the trailing element. Internally derives effectiveType so the caller's type="password" semantic stays put. Co-locates a PasswordVisibilityGroup React Context provider so paired fields share a single toggle.
  • web/src/pages/setup/AccountStep.tsx and web/src/pages/LoginPage.tsx (setup mode) wrap their Password + Confirm Password fields in <PasswordVisibilityGroup> so toggling one reveals both.
  • All other 17 sensitive call sites (provider modal, credentials rotate, every connection-form credential field via connection-type-fields.ts) inherit the toggle automatically with independent state per field, because they all flow type="password" through the same primitive.
  • Storybook gets Password, PasswordGrouped, PasswordDisabled, PasswordWithError, PasswordNoToggle, PasswordCustomTrailing stories.
  • 13 new test cases cover toggle rendering, accessibility (aria-pressed, aria-label, type="button"), keyboard activation (Enter / Space), value capture while revealed, opt-out, caller-supplied trailingElement, grouped vs independent visibility, and form-submission safety.

Adjacent fixes folded in (caught during pre-PR review)

  • web/index.html: switched the CSP-nonce meta tag to single-quoted attribute syntax so the embedded Caddy template {{placeholder "http.request.uuid"}} parses cleanly under parse5 / Vite (no behavioural change at serve time, the Caddy templates engine substitutes the token regardless of outer-quote style).
  • web/src/pages/LoginPage.tsx: form container now uses space-y-section-gap instead of hardcoded space-y-6 so the layout respects density tokens.
  • web/src/pages/setup/AccountStep.tsx: password and confirm-password fields now set autoComplete="new-password" so password managers categorise them correctly during initial setup.
  • docs/reference/web-design-system.md: documents the new InputField toggle, hidePasswordToggle opt-out, and PasswordVisibilityGroup.
  • docs/security.md: notes the meta tag's single-quoted attribute syntax in the CSP Nonce Infrastructure section.

Test plan

  • npm --prefix web run lint (ESLint, zero warnings).
  • npm --prefix web run type-check (tsc).
  • npm --prefix web run test -- --run (3060 tests pass).
  • Manual: dev server + click-through on Create Admin Account (paired toggle), Login (single toggle), Provider modal API key (independent toggle), Connections form Slack token + signing secret (each independent). All confirmed.

Review coverage

Pre-reviewed by 6 agents (frontend-reviewer, design-token-audit, security-reviewer with frontend supplemental checks, test-quality-reviewer, docs-consistency, comment-quality-rot). 8 valid findings; all 8 implemented in commit 059d868e2.

Tooling note

The /pre-pr-review run surfaced an unrelated harness bug: three sub-agents (pr-test-analyzer, silent-failure-hunter, comment-analyzer) declared in .claude/agents/ are not loaded by the Claude Code harness, so the skill's subagent_type: pr-test-analyzer dispatch fails. Filed as #1871; falls back to code-reviewer with a custom prompt for now.

Aureliolo added 2 commits May 11, 2026 22:43
Adds a built-in show/hide affordance to the InputField primitive whenever type='password'. The trailing eye / eye-off icon flips the rendered DOM type without mutating the caller's prop, so the call-site semantic stays 'this is a secret'. A co-located PasswordVisibilityGroup context lets paired fields (e.g. Password + Confirm Password on the Create Admin Account screen) share a single toggle, while unrelated secrets in the same dialog (provider API keys, OAuth client secrets, connection credentials) keep independent toggles.

Also fixes a parse5 warning surfaced by Vite on web/index.html: the CSP nonce meta tag now uses single-quoted attribute syntax so the embedded Caddy template '{{placeholder "http.request.uuid"}}' parses cleanly without changing what Caddy substitutes at serve time.
Pre-reviewed by 6 agents, 8 findings addressed: docs/reference/web-design-system.md now documents the new toggle, hidePasswordToggle prop, and PasswordVisibilityGroup; docs/security.md notes the single-quoted attribute syntax used by the CSP-nonce meta tag; input-field tests gain keyboard-activation and value-capture cases; Storybook gains PasswordDisabled / PasswordWithError / PasswordNoToggle / PasswordCustomTrailing stories; LoginPage form container uses the space-y-section-gap density token; AccountStep password fields gain autoComplete=new-password for password-manager hinting.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 364669cb-de07-4c48-8c9a-257d97aff5fd

📥 Commits

Reviewing files that changed from the base of the PR and between 059d868 and bee7317.

📒 Files selected for processing (1)
  • web/src/components/ui/input-field.tsx
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Build Web Assets (melange)
  • GitHub Check: CodSpeed Web benchmarks
  • GitHub Check: Dashboard Type Check
  • GitHub Check: Dashboard Build
  • GitHub Check: Dashboard Test
  • GitHub Check: Lighthouse Site
  • GitHub Check: Lighthouse Dashboard
  • GitHub Check: Build Preview
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (go)
🧰 Additional context used
📓 Path-based instructions (7)
web/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx,js,jsx}: Always use createLogger from @/lib/logger; never use bare console.warn, console.error, or console.debug in application code
Logger variable name must always be log (e.g., const log = createLogger('module-name'))
Pass dynamic or untrusted values to logger methods as separate arguments (not interpolated into the message string) so they go through sanitizeArg
Wrap attacker-controlled fields inside structured objects in sanitizeForLog() before embedding them in logs

Files:

  • web/src/components/ui/input-field.tsx
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx}: Callers MUST NOT wrap store mutation calls in try/catch; the store owns the error UX
Any new caller of health/readiness endpoints must handle the 503 path explicitly
Any new WS payload handler that ingests untrusted strings MUST route through sanitizeWsString() or sanitizeWsEnum() helpers
Import ErrorCode and ErrorCategory from @/api/types/errors (re-exported from web/src/api/types/error-codes.gen.ts); discriminate on ErrorCode.<NAME>, never on raw integer literals
Never export a getXIcon(value): LucideIcon factory that returns a component reference called inside JSX render bodies; export a <XIcon value={...} {...lucideProps} /> wrapper component instead using createElement for the lookup
Detect fetch() in effects without AbortController cleanup using the @eslint-react/web-api-no-leaked-fetch rule
Catch the {count && <Foo />} bug using @eslint-react/no-leaked-conditional-rendering; for ReactNode | undefined props use {value != null && value !== false && <jsx>}; for compound truthiness use Boolean(...)
Restrict window, document, localStorage, and other globals inside render using @eslint-react/globals; hoist offenders into useCallback event handler, useEffect, or useSyncExternalStore-backed hook

Files:

  • web/src/components/ui/input-field.tsx
web/src/components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/components/**/*.{ts,tsx}: Never hardcode hex colors, font-family declarations, pixel spacing, Motion transition durations, BCP 47 locale literals, or currency symbols/codes; use design tokens, @/lib/motion presets, helpers in @/utils/format, and DEFAULT_CURRENCY
Base UI primitives are imported directly from @base-ui/react/<subpath> and use the native render prop for polymorphism; the local <Slot> helper is reserved for <Button asChild> only
Use <AnimatedPresence>, <StaggerGroup>, or <LiveRegion> (debounced ARIA live for WebSocket updates) for animation and live feedback instead of custom implementations

Files:

  • web/src/components/ui/input-field.tsx
web/src/components/ui/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/components/ui/**/*.{ts,tsx}: Every new shared component must live in web/src/components/ui/ with a sibling .stories.tsx covering all states
Component Props interface must be named <ComponentName>Props and exported from the same file

Files:

  • web/src/components/ui/input-field.tsx
web/src/{components,pages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/{components,pages}/**/*.{ts,tsx}: Use useViewportSize() from @/hooks/useViewportSize (useSyncExternalStore over window resize) instead of reading window.innerWidth / window.innerHeight directly
Never read window.innerWidth / window.innerHeight directly inside a component render body or useMemo

Files:

  • web/src/components/ui/input-field.tsx
web/src/{pages,components}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/{pages,components}/**/*.{ts,tsx}: Status badge display should use <StatusBadge> component with role="img" + aria-label by default; use decorative for adjacent-labeled, announce for live WebSocket updates
KPI displays should use <MetricCard>, <Sparkline>, <ProgressGauge>, or <TokenUsageBar> instead of creating custom implementations
Card containers should use <SectionCard> (titled wrapper with icon and action slot), <AgentCard>, or <DeptHealthBar> for domain-specific cards instead of recreating inline
Form fields should use <InputField>, <SelectField>, <SliderField>, <ToggleField>, <SegmentedControl>, <TagInput>, or <SearchInput> instead of creating custom field implementations
Slide-in panels must use <Drawer width="compact|narrow|default|wide"> (Base UI) without inline w-[40vw] overrides
Loading, empty, and error states should use <Skeleton> family, <EmptyState>, <ErrorBoundary>, <ErrorBanner>, or <ProgressIndicator> instead of custom implementations
Use useEmptyStateProps({ filteredCount, totalCount, filterActive, empty, filtered }) from @/hooks/use-empty-state-props to branch on a single value instead of duplicating the 'no data ever' / 'no data after filter' discriminator
Status/role/risk/urgency badge classes must use STATUS_COLORS family from @/styles/status-colors (typed lookups); never define inline Record<EnumValue, string> constants per page
Use <ConfirmDialog> or <Toast> (Zustand-backed queue) for confirmation and toast feedback instead of Base UI's Toast
Use <CommandPalette>, <KeyboardShortcutHint>, or <CommandCheatsheet> for command palette and keyboard shortcut UI instead of custom implementations

Files:

  • web/src/components/ui/input-field.tsx
web/src/**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Reuse web/src/components/ui/ design tokens only in web dashboard design system.

Files:

  • web/src/components/ui/input-field.tsx
🔇 Additional comments (5)
web/src/components/ui/input-field.tsx (5)

1-2: LGTM!

Imports are appropriate for React 19. The use() hook is the correct modern approach for consuming context in React 19+.


62-73: LGTM!

The PasswordVisibilityGroup correctly uses useMemo to stabilize the context value. The dependency array [visible] is correct since setVisible from useState is guaranteed stable. The React 19 <Context value={...}> syntax is properly used.


173-201: LGTM!

The PasswordToggleButton is well-implemented with proper accessibility attributes (aria-label, aria-pressed, type="button" to prevent form submission). Design tokens are correctly used for colors and the icon is properly hidden from assistive technology with aria-hidden="true".


225-251: LGTM!

The password visibility logic correctly handles both grouped (via context) and standalone (local state) scenarios. The use() hook is the appropriate React 19 pattern for context consumption. The useCallback stabilization from the commit message is properly applied.


253-310: LGTM!

The effectiveType correctly toggles between 'password' and 'text' based on visibility state. The renderedTrailing computation properly prioritizes caller-provided trailingElement over the built-in toggle. The hasTrailingElement flag for padding is correctly synchronized with the actual rendered content.


Walkthrough

This PR implements password visibility toggle functionality for InputField with a new PasswordVisibilityGroup context provider for shared visibility. InputField gains hidePasswordToggle and respects trailingElement overrides; a PasswordToggleButton renders the eye/eye-off control. The feature is covered by new tests and Storybook stories and is integrated into LoginPage and AccountStep. Separately, the CSP nonce template and docs use a single-quoted placeholder and clarify Caddy substitution behavior.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 40.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(web): eye-toggle on every password / secret field' accurately reflects the main feature being introduced—a built-in password visibility toggle component. It is concise, specific, and clearly indicates the primary change.
Description check ✅ Passed The description is comprehensive and directly related to the changeset. It explains the summary, implementation details, adjacent fixes, test plan, and review coverage—all of which align with the changes made across multiple files in the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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


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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 11, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview May 11, 2026 21:44 — with GitHub Actions Inactive
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a built-in password visibility toggle for the InputField component and a PasswordVisibilityGroup context provider to synchronize visibility across multiple password fields. The changes include comprehensive unit tests, updated Storybook stories, and integration into the login and account setup pages. Additionally, the CSP nonce meta tag in web/index.html was updated to use single quotes for better compatibility with HTML parsers. A performance improvement was suggested to wrap the visibility toggle function in useCallback to prevent unnecessary re-renders of the toggle button.

Comment thread web/src/components/ui/input-field.tsx Outdated
Comment on lines +232 to +238
const toggleVisible = () => {
if (groupContext !== null) {
groupContext.setVisible(!groupContext.visible)
} else {
setLocalVisible((prev) => !prev)
}
}
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.

medium

For performance optimization and to follow React best practices, it's recommended to wrap the toggleVisible function in useCallback. This will prevent the function from being recreated on every render of InputVariant, which in turn prevents unnecessary re-renders of the PasswordToggleButton component. You'll also need to add useCallback to your React imports.

Suggested change
const toggleVisible = () => {
if (groupContext !== null) {
groupContext.setVisible(!groupContext.visible)
} else {
setLocalVisible((prev) => !prev)
}
}
const toggleVisible = useCallback(() => {
if (groupContext !== null) {
groupContext.setVisible(!groupContext.visible)
} else {
setLocalVisible((prev) => !prev)
}
}, [groupContext])

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 11, 2026

Merging this PR will not alter performance

✅ 21 untouched benchmarks
⏩ 33 skipped benchmarks1


Comparing worktree-password-toggle (bee7317) with main (af33ddb)2

Open in CodSpeed

Footnotes

  1. 33 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (6263795) during the generation of this report, so af33ddb was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 11, 2026
Wrap toggleVisible in useCallback (gemini-code-assist suggestion on input-field.tsx:238) to stabilize the handler reference and avoid recreating the closure on every render.
@Aureliolo Aureliolo merged commit 9070387 into main May 11, 2026
75 checks passed
@Aureliolo Aureliolo deleted the worktree-password-toggle branch May 11, 2026 22:22
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview May 11, 2026 22:22 — with GitHub Actions Inactive
Aureliolo pushed a commit that referenced this pull request May 12, 2026
<!-- HIGHLIGHTS_START -->
## Highlights

> _AI-generated summary (model: `openai/gpt-4.1-mini` via GitHub
Models). Commit-based changelog below._

### What you'll notice
- Password and secret fields now include an eye-toggle for easier
visibility control.
- Containers running without probes are shown as healthy in the doctor
command.
- Unloaded and missing PR-review agents are restored and available
again.

### What's new
- Gate baseline protection is enhanced to block em-dashes during
writing.

### Under the hood
- Replaced Atlas with yoyo-migrations for persistence management.
- Refactored codebase extensively, including context-bound user
authentication and registry pattern for enums.
- Improved linting by draining magic number usages and tightening mock
and constant checks.
- Updated CI to retry Docker pushes on network timeout errors.
- Updated apko lockfiles for dependency management.

<!-- HIGHLIGHTS_END -->

:robot: I have created a release *beep* *boop*
---


##
[0.8.3](v0.8.2...v0.8.3)
(2026-05-12)


### Features

* harden gate baseline protection + block em-dashes at write time
([#1860](#1860))
([b41f151](b41f151))
* **web:** eye-toggle on every password / secret field
([#1873](#1873))
([9070387](9070387))


### Bug Fixes

* **ci:** retry Docker push on Go net/http deadline + cancellation
errors ([#1877](#1877))
([23a0bfa](23a0bfa))
* **cli:** render running-no-probe containers as healthy in doctor
([#1870](#1870))
([6263795](6263795))
* restore unloaded and missing PR-review agents
([#1875](#1875))
([db004fd](db004fd)),
closes [#1871](#1871)


### Refactoring

* bind authenticated user via ContextVar
([#1858](#1858))
([57ed0b4](57ed0b4))
* code-structure cleanup (sub-tasks D + F + G + H + I)
([#1859](#1859))
([362e5c8](362e5c8))
* convert enum dispatch to registry pattern
([#1854](#1854))
([e90550e](e90550e))
* drain no_magic_numbers baseline to zero via Final hoists
([#1856](#1856) phase 2)
([#1872](#1872))
([ec8109e](ec8109e))
* drain pagination + loop-init + kill-switch baselines
([#1857](#1857))
([#1868](#1868))
([115c3c2](115c3c2))
* **persistence:** replace Atlas with yoyo-migrations
([#1876](#1876))
([1b7e975](1b7e975)),
closes [#1874](#1874)
* protocols audit follow-up (REVIEW + fold pass)
([#1869](#1869))
([af33ddb](af33ddb))
* protocols audit follow-up REMOVE pass
([#1867](#1867))
([dd1eebc](dd1eebc))
* tighten check_mock_spec gate, add mock_of[T], drain baseline
([#1862](#1862))
([240a253](240a253))
* tighten check_no_magic_numbers for named module constants
([#1856](#1856))
([#1866](#1866))
([90c933b](90c933b))


### CI/CD

* update apko lockfiles
([#1863](#1863))
([2bd32e6](2bd32e6))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: synthorg-repo-bot[bot] <279117679+synthorg-repo-bot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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