From 1b9ecc0f194593c24879dc3a977f8cf53c64e4d6 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Thu, 13 Aug 2026 09:12:56 -0400 Subject: [PATCH] fix(calendar): one input for the auth code, and a louder warning Two things reported after testing the flow. There were two boxes and no way to tell which to use. The config template's string branch dispatches widgets from an allow-list of names, and anything missing from it falls through to a plain input type=text -- so the field rendered both the widget's own box and a stray one for the same key. google-oauth is now on that list, which is all the widget ever needed to render in place of the fallback rather than beside it. And the warning that the redirect page fails to load was small grey text under a link, which is where it is least likely to be read. It is now an amber callout that leads with "The next page will fail to load. That is expected." The failure lands at exactly the moment the user has to act on it, and it looks precisely like the flow breaking rather than working. The paste box is labelled too, rather than relying on a placeholder that vanishes on focus. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5 --- .../test_calendar_oauth_endpoints.py | 29 +++++++++++++++++++ .../static/v3/js/widgets/google-oauth.js | 29 +++++++++++++++---- .../templates/v3/partials/plugin_config.html | 2 +- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/test/web_interface/test_calendar_oauth_endpoints.py b/test/web_interface/test_calendar_oauth_endpoints.py index e7208cb8..c170fa8b 100644 --- a/test/web_interface/test_calendar_oauth_endpoints.py +++ b/test/web_interface/test_calendar_oauth_endpoints.py @@ -81,6 +81,35 @@ class TestTheRoutesExistAtAll: assert "'/plugins/calendar/list-calendars'" in source assert "'/plugins/calendar/authenticate'" in source + def test_the_oauth_widget_is_dispatched_not_rendered_as_a_text_box(self): + # The string branch of the config template dispatches on an allow-list + # of widget names; anything missing from it silently falls through to a + # plain . That produced two boxes on the calendar + # page -- the widget's own, and a stray one for the same field -- and + # no way to tell which to paste into. + template = (Path(project_root) + / 'web_interface/templates/v3/partials/plugin_config.html' + ).read_text(encoding='utf-8') + allow_list_line = [ln for ln in template.splitlines() + if "str_widget in [" in ln] + assert allow_list_line, "the string widget allow-list moved" + assert "'google-oauth'" in allow_list_line[0], allow_list_line[0] + + def test_the_widget_script_is_served(self): + base = (Path(project_root) / 'web_interface/templates/v3/base.html' + ).read_text(encoding='utf-8') + assert 'widgets/google-oauth.js' in base + + def test_the_failed_page_is_called_out_loudly(self): + # The loopback redirect lands on a browser error page at exactly the + # moment the user has to act. In small grey text it gets missed and the + # flow reads as broken while it is working. + widget = (Path(project_root) + / 'web_interface/static/v3/js/widgets/google-oauth.js' + ).read_text(encoding='utf-8') + assert 'expected' in widget.lower() + assert 'amber' in widget, "the warning is not visually distinguished" + class TestItSaysWhatIsWrong: def test_listing_without_a_token_asks_for_step_2(self, client): diff --git a/web_interface/static/v3/js/widgets/google-oauth.js b/web_interface/static/v3/js/widgets/google-oauth.js index e288adb7..44088b2f 100644 --- a/web_interface/static/v3/js/widgets/google-oauth.js +++ b/web_interface/static/v3/js/widgets/google-oauth.js @@ -66,12 +66,28 @@ link.className = 'text-blue-400 underline text-sm break-all'; link.textContent = 'Open the Google consent screen'; - const hint = document.createElement('p'); - hint.className = 'text-xs text-gray-400 mt-2'; - hint.textContent = - 'After approving, your browser will try to open a page that fails ' - + 'to load. That is expected. Copy its full address from the bar ' - + 'and paste it below.'; + // Deliberately loud. After consent the browser is redirected to a + // loopback address nothing is listening on, so it lands on a + // browser error page -- which reads as a failure at exactly the + // moment the user has to act on it. Said quietly in grey it gets + // missed, and the flow looks broken when it is working. + const hint = document.createElement('div'); + hint.className = + 'mt-3 p-3 rounded-md border border-amber-500/60 bg-amber-500/10'; + hint.innerHTML = + '

' + + ' ' + + 'The next page will fail to load. That is expected.

' + + '

' + + 'After you approve access, Google sends your browser to ' + + '127.0.0.1, where nothing is running \u2014 so you will see ' + + '"This site can\u2019t be reached" or similar. Nothing has gone wrong. ' + + 'Copy the entire address out of the address bar ' + + '(it contains ?code=...) and paste it in the box below.

'; + + const codeLabel = document.createElement('label'); + codeLabel.className = 'block text-xs text-gray-300 mt-3'; + codeLabel.textContent = 'Paste the address from that failed page here:'; const codeInput = document.createElement('input'); codeInput.type = 'text'; @@ -150,6 +166,7 @@ step2.appendChild(link); step2.appendChild(hint); + step2.appendChild(codeLabel); step2.appendChild(codeInput); step2.appendChild(finishBtn); diff --git a/web_interface/templates/v3/partials/plugin_config.html b/web_interface/templates/v3/partials/plugin_config.html index 41651fb8..f789fd5b 100644 --- a/web_interface/templates/v3/partials/plugin_config.html +++ b/web_interface/templates/v3/partials/plugin_config.html @@ -815,7 +815,7 @@ Changes in the file manager save immediately — no need to click Save Configuration.

- {% elif str_widget in ['text-input', 'textarea', 'select-dropdown', 'toggle-switch', 'radio-group', 'date-picker', 'time-picker', 'slider', 'color-picker', 'email-input', 'url-input', 'password-input', 'font-selector', 'file-upload-single', 'plugin-file-manager'] %} + {% elif str_widget in ['text-input', 'textarea', 'select-dropdown', 'toggle-switch', 'radio-group', 'date-picker', 'time-picker', 'slider', 'color-picker', 'email-input', 'url-input', 'password-input', 'font-selector', 'file-upload-single', 'plugin-file-manager', 'google-oauth'] %} {# Render widget container #}