fix(frontend): streamline web ux#916
Conversation
There was a problem hiding this comment.
Pull request overview
This PR streamlines the feed conversion UX by normalizing user-entered URLs, improving conversion retry behavior + preview loading UX, and making access-token persistence more robust across restricted browser storage contexts.
Changes:
- Add URL normalization (frontend + backend) so hostname-only inputs are coerced to
https://…before feed creation and bookmarklet auto-submit. - Update conversion flow to publish results immediately, hydrate preview async, and support automatic fallback retry (with UI messaging).
- Prefer persistent storage (localStorage) for tokens and add legacy session-token migration/cleanup.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/html2rss/web/api/v1_spec.rb | Adds API spec coverage for hostname-only URL normalization on feed creation. |
| public/shared-ui.css | Ensures new brand lockup link styling doesn’t inherit underlines. |
| frontend/src/utils/url.ts | Introduces shared frontend URL normalization + validation helpers. |
| frontend/src/styles/main.css | Adds spacing for new notice action area. |
| frontend/src/hooks/useFeedConversion.ts | Implements URL normalization, async preview hydration, and automatic fallback retry metadata. |
| frontend/src/hooks/useAuth.ts | Prefers localStorage over sessionStorage for auth persistence with fallback handling. |
| frontend/src/hooks/useAccessToken.ts | Prefers localStorage, migrates legacy session token, and attempts to clear old session copies. |
| frontend/src/components/ResultDisplay.tsx | Adds “Subscribe in reader” link, retry notice, and preview-loading UI state. |
| frontend/src/components/DominantField.tsx | Extends input props (inputMode, autoCapitalize, spellcheck) and fixes autoComplete casing. |
| frontend/src/components/Bookmarklet.tsx | Simplifies bookmarklet URL base calculation and uses location.assign. |
| frontend/src/components/AppPanels.tsx | Makes URL entry more permissive + adds retry CTA; updates token entry field attributes. |
| frontend/src/components/App.tsx | Normalizes URLs on submit/auto-submit, prefers faraday by default, and wires manual retry UX. |
| frontend/src/api/contracts.ts | Extends result contract with preview loading state + retry metadata. |
| frontend/src/tests/useFeedConversion.test.ts | Updates unit tests for async preview hydration, URL normalization, and auto-retry behavior. |
| frontend/src/tests/useFeedConversion.contract.test.ts | Extends contract tests for new preview loading field. |
| frontend/src/tests/useAuth.test.ts | Updates tests for localStorage-backed auth persistence. |
| frontend/src/tests/useAccessToken.test.ts | Adds coverage for localStorage persistence and legacy session migration/cleanup. |
| frontend/src/tests/ResultDisplay.test.tsx | Adds assertions for subscribe link, loading preview state, and retry notice. |
| frontend/src/tests/App.test.tsx | Updates tests for new default strategy, permissive URL field, auto-submit behavior, and manual retry CTA. |
| frontend/src/tests/App.contract.test.tsx | Updates contract expectations for faraday default + localStorage token usage. |
| app/web/security/url_validator.rb | Adds canonical_url wrapper and routes validation/allowlist checks through it. |
| app/web/api/v1/create_feed.rb | Normalizes hostname-only/// inputs before canonicalization + validation. |
| type="text" | ||
| class="input input--mono input--minimal" | ||
| aria-label="Access token" | ||
| placeholder="Paste access token" | ||
| autocomplete="off" | ||
| autoComplete="off" |
There was a problem hiding this comment.
The access token input was changed from type="password" to type="text", which exposes the token in cleartext (shoulder-surfing/screen recording) and may weaken user expectations around secret entry. Consider keeping this as a password field (or making visibility an explicit opt-in toggle) while retaining the autocomplete/manager-ignore attributes.
| const legacyToken = | ||
| token || typeof window === 'undefined' | ||
| ? '' | ||
| : (window.sessionStorage?.getItem(ACCESS_TOKEN_KEY)?.trim() ?? ''); | ||
|
|
There was a problem hiding this comment.
legacyToken is read directly from window.sessionStorage inside the effect. In restricted contexts where sessionStorage access throws (the case this PR mentions), this read will throw and put the hook into an error state, even though localStorage (or memoryStorage) would work fine. Consider wrapping the legacy-session read in its own try/catch (or gating it behind a successful sessionStorage probe) so restricted sessionStorage access is treated as “no legacy token” instead of a hard failure.
| const legacyToken = | |
| token || typeof window === 'undefined' | |
| ? '' | |
| : (window.sessionStorage?.getItem(ACCESS_TOKEN_KEY)?.trim() ?? ''); | |
| let legacyToken = ''; | |
| if (!token && typeof window !== 'undefined') { | |
| try { | |
| legacyToken = | |
| window.sessionStorage?.getItem(ACCESS_TOKEN_KEY)?.trim() ?? ''; | |
| } catch { | |
| // Treat restricted sessionStorage access as "no legacy token". | |
| legacyToken = ''; | |
| } | |
| } |
| return `Tried ${requestedStrategy} first, then ${fallbackStrategy}. ${secondMessage}`; | ||
| } | ||
|
|
||
| return `Tried ${requestedStrategy} first, then ${fallbackStrategy}. ${secondMessage}`; |
There was a problem hiding this comment.
buildRetryFailureMessage has an if (firstMessage === secondMessage) branch, but both branches currently return the exact same string (and firstMessage is otherwise unused). This is effectively dead logic and makes the intent unclear. Either remove the conditional/unused variable, or include the first failure’s message when it differs so the combined error is more informative.
| return `Tried ${requestedStrategy} first, then ${fallbackStrategy}. ${secondMessage}`; | |
| return `Tried ${requestedStrategy} first, then ${fallbackStrategy}. First attempt failed with: ${firstMessage}. Second attempt failed with: ${secondMessage}`; |
🤖 I have created a release *beep* *boop* --- ## [1.1.0](html2rss-web-v1.0.0...html2rss-web/v1.1.0) (2026-05-01) ### Features * add help text on error page ([eeee345](eeee345)), closes [#338](#338) * add routed frontend feed creation workflow ([#963](#963)) ([2d1b71a](2d1b71a)) * **auto_source:** add support for `auto_source` feature ([#676](#676)) ([531dced](531dced)) * default browserless onboarding and request strategies ([#895](#895)) ([377cff0](377cff0)) * **deps:** use html2rss in latest development status ([#728](#728)) ([5885d1d](5885d1d)) * **docker:** switch to alpine 21 ([7adcc89](7adcc89)) * **docker:** upgrade to use ruby 3.3 image ([ceafe24](ceafe24)) * **docker:** use multilayer build to cut image size in half ([2f6e322](2f6e322)) * **docker:** use Ruby 3.4 ([4f7d795](4f7d795)) * **frontend:** polish result experience and validation tooling ([#964](#964)) ([b11665e](b11665e)) * **frontend:** relaunch the app with a focused v1 flow ([e0692d7](e0692d7)) * **frontend:** unify feed/result state flow ([#943](#943)) ([6dfa1a9](6dfa1a9)) * **health_check:** add HTTP Basic authentication to `GET /health_check.txt` ([#559](#559)) ([d0ccd83](d0ccd83)) * improve example feed config in feed.yml and link to it ([#552](#552)) ([de08695](de08695)) * install Gemfile.lock specified bundler version ([4190160](4190160)) * integrate request_service and use ssrf_filter strategy by default ([#707](#707)) ([b7516fd](b7516fd)) * link included feeds to the instance feed directory ([#901](#901)) ([51ce79a](51ce79a)) * optionally allow APM using Sentry via env variable ([#696](#696)) ([94477d5](94477d5)) * redact sensitive feed data in structured logs ([#903](#903)) ([ee7df73](ee7df73)) * remove dependency on activesupport ([048cb73](048cb73)) * **runtime:** rebuild feed and api behavior around typed v1 services ([b61602d](b61602d)) * simplify feed creation contract & backend error handling ([#962](#962)) ([dfca027](dfca027)) * stabilize public http interface & slimmer docker ([#882](#882)) ([fe3f4be](fe3f4be)) * unify web and feed result surfaces ([#896](#896)) ([e747b23](e747b23)) * use parallel processing for feed retrieval in health_check.rb ([#665](#665)) ([4a24997](4a24997)) ### Bug Fixes * ArgumentError when RACK_TIMEOUT_SERVICE_TIMEOUT env var is set ([96acbab](96acbab)), closes [#527](#527) * **auto_source:** respect headers from global config ([#691](#691)) ([3e9ba91](3e9ba91)) * **build:** only cleanup when there is a test container ([f7bafa6](f7bafa6)) * caching with dynamic parameters yields incorrect rss ([#589](#589)) ([bb945c2](bb945c2)), closes [#587](#587) * **ci:** repair Ruby, OpenAPI, and frontend checks ([#880](#880)) ([ec6673b](ec6673b)) * defects for token/retry/loading UX ([#924](#924)) ([2d38633](2d38633)) * **docker:** missing curl installation for health check ([0bd9157](0bd9157)) * example feed in config/feeds.yml broken ([#664](#664)) ([b961897](b961897)) * **frontend:** preserve created feeds when preview loading fails ([#915](#915)) ([383ecc3](383ecc3)) * **frontend:** streamline web ux ([#916](#916)) ([85e79bf](85e79bf)) * harden container config defaults ([392997c](392997c)) * healthcheck broken due to missing curl ([c97e746](c97e746)) * keep unknown api v1 paths inside the api contract ([a820478](a820478)) * responds with http status 422 ([#738](#738)) ([ad9394c](ad9394c)) * **runtime:** polish relaunch smoke behavior and health checks ([65e1644](65e1644)) * stylesheets not included in feed ([#779](#779)) ([9116d9d](9116d9d)) * tzdata package not installed but required for tz conversion ([#663](#663)) ([55814d2](55814d2)) * **web:** harden feed reader fallback and rss rendering ([#944](#944)) ([438d9f6](438d9f6)) * **web:** harden observability env handling and Sentry log redaction ([#917](#917)) ([ed2b3e9](ed2b3e9)) ### Performance Improvements * enable YJIT ([729f31f](729f31f)) --- 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: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
🤖 I have created a release *beep* *boop* --- ## [1.2.0](v1.1.0...v1.2.0) (2026-05-01) ### Features * add help text on error page ([eeee345](eeee345)), closes [#338](#338) * add routed frontend feed creation workflow ([#963](#963)) ([2d1b71a](2d1b71a)) * **auto_source:** add support for `auto_source` feature ([#676](#676)) ([531dced](531dced)) * default browserless onboarding and request strategies ([#895](#895)) ([377cff0](377cff0)) * **deps:** use html2rss in latest development status ([#728](#728)) ([5885d1d](5885d1d)) * **docker:** switch to alpine 21 ([7adcc89](7adcc89)) * **docker:** upgrade to use ruby 3.3 image ([ceafe24](ceafe24)) * **docker:** use multilayer build to cut image size in half ([2f6e322](2f6e322)) * **docker:** use Ruby 3.4 ([4f7d795](4f7d795)) * **frontend:** polish result experience and validation tooling ([#964](#964)) ([b11665e](b11665e)) * **frontend:** relaunch the app with a focused v1 flow ([e0692d7](e0692d7)) * **frontend:** unify feed/result state flow ([#943](#943)) ([6dfa1a9](6dfa1a9)) * **health_check:** add HTTP Basic authentication to `GET /health_check.txt` ([#559](#559)) ([d0ccd83](d0ccd83)) * improve example feed config in feed.yml and link to it ([#552](#552)) ([de08695](de08695)) * install Gemfile.lock specified bundler version ([4190160](4190160)) * integrate request_service and use ssrf_filter strategy by default ([#707](#707)) ([b7516fd](b7516fd)) * link included feeds to the instance feed directory ([#901](#901)) ([51ce79a](51ce79a)) * optionally allow APM using Sentry via env variable ([#696](#696)) ([94477d5](94477d5)) * redact sensitive feed data in structured logs ([#903](#903)) ([ee7df73](ee7df73)) * remove dependency on activesupport ([048cb73](048cb73)) * **runtime:** rebuild feed and api behavior around typed v1 services ([b61602d](b61602d)) * simplify feed creation contract & backend error handling ([#962](#962)) ([dfca027](dfca027)) * stabilize public http interface & slimmer docker ([#882](#882)) ([fe3f4be](fe3f4be)) * unify web and feed result surfaces ([#896](#896)) ([e747b23](e747b23)) * use parallel processing for feed retrieval in health_check.rb ([#665](#665)) ([4a24997](4a24997)) ### Bug Fixes * ArgumentError when RACK_TIMEOUT_SERVICE_TIMEOUT env var is set ([96acbab](96acbab)), closes [#527](#527) * **auto_source:** respect headers from global config ([#691](#691)) ([3e9ba91](3e9ba91)) * **build:** only cleanup when there is a test container ([f7bafa6](f7bafa6)) * caching with dynamic parameters yields incorrect rss ([#589](#589)) ([bb945c2](bb945c2)), closes [#587](#587) * **ci:** repair Ruby, OpenAPI, and frontend checks ([#880](#880)) ([ec6673b](ec6673b)) * **ci:** robustly parse release tags and align config ([#972](#972)) ([2efd6ef](2efd6ef)) * defects for token/retry/loading UX ([#924](#924)) ([2d38633](2d38633)) * **docker:** missing curl installation for health check ([0bd9157](0bd9157)) * example feed in config/feeds.yml broken ([#664](#664)) ([b961897](b961897)) * **frontend:** preserve created feeds when preview loading fails ([#915](#915)) ([383ecc3](383ecc3)) * **frontend:** streamline web ux ([#916](#916)) ([85e79bf](85e79bf)) * harden container config defaults ([392997c](392997c)) * healthcheck broken due to missing curl ([c97e746](c97e746)) * keep unknown api v1 paths inside the api contract ([a820478](a820478)) * responds with http status 422 ([#738](#738)) ([ad9394c](ad9394c)) * **runtime:** polish relaunch smoke behavior and health checks ([65e1644](65e1644)) * stylesheets not included in feed ([#779](#779)) ([9116d9d](9116d9d)) * tzdata package not installed but required for tz conversion ([#663](#663)) ([55814d2](55814d2)) * **web:** harden feed reader fallback and rss rendering ([#944](#944)) ([438d9f6](438d9f6)) * **web:** harden observability env handling and Sentry log redaction ([#917](#917)) ([ed2b3e9](ed2b3e9)) ### Performance Improvements * enable YJIT ([729f31f](729f31f)) --- 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: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Summary
Validation