Skip to content

fix(frontend): streamline web ux#916

Merged
gildesmarais merged 7 commits intomainfrom
slice/conversion-ux-input-handling
Mar 28, 2026
Merged

fix(frontend): streamline web ux#916
gildesmarais merged 7 commits intomainfrom
slice/conversion-ux-input-handling

Conversation

@gildesmarais
Copy link
Copy Markdown
Member

Summary

  • streamline the conversion flow UI and strategy handling
  • normalize user-entered URLs before submit and auto-submit
  • improve access-token storage fallback cleanup for restricted session storage contexts

Validation

  • make ready
  • frontend smoke at http://127.0.0.1:4001/ (initial + token-required states; action uniqueness checked)

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment thread frontend/src/components/AppPanels.tsx Outdated
Comment on lines +201 to +205
type="text"
class="input input--mono input--minimal"
aria-label="Access token"
placeholder="Paste access token"
autocomplete="off"
autoComplete="off"
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/hooks/useAccessToken.ts Outdated
Comment on lines +66 to +70
const legacyToken =
token || typeof window === 'undefined'
? ''
: (window.sessionStorage?.getItem(ACCESS_TOKEN_KEY)?.trim() ?? '');

Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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 = '';
}
}

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/hooks/useFeedConversion.ts Outdated
return `Tried ${requestedStrategy} first, then ${fallbackStrategy}. ${secondMessage}`;
}

return `Tried ${requestedStrategy} first, then ${fallbackStrategy}. ${secondMessage}`;
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
return `Tried ${requestedStrategy} first, then ${fallbackStrategy}. ${secondMessage}`;
return `Tried ${requestedStrategy} first, then ${fallbackStrategy}. First attempt failed with: ${firstMessage}. Second attempt failed with: ${secondMessage}`;

Copilot uses AI. Check for mistakes.
@gildesmarais gildesmarais enabled auto-merge (squash) March 28, 2026 00:38
@gildesmarais gildesmarais merged commit 85e79bf into main Mar 28, 2026
12 checks passed
@gildesmarais gildesmarais deleted the slice/conversion-ux-input-handling branch March 28, 2026 00:48
gildesmarais added a commit that referenced this pull request May 1, 2026
🤖 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>
gildesmarais added a commit that referenced this pull request May 1, 2026
🤖 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>
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.

2 participants