mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-13 14:48:06 +00:00
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
This commit is contained in:
co-authored by
Claude Opus 5
parent
7685e94ca5
commit
1b9ecc0f19
@@ -81,6 +81,35 @@ class TestTheRoutesExistAtAll:
|
|||||||
assert "'/plugins/calendar/list-calendars'" in source
|
assert "'/plugins/calendar/list-calendars'" in source
|
||||||
assert "'/plugins/calendar/authenticate'" 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 <input type="text">. 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:
|
class TestItSaysWhatIsWrong:
|
||||||
def test_listing_without_a_token_asks_for_step_2(self, client):
|
def test_listing_without_a_token_asks_for_step_2(self, client):
|
||||||
|
|||||||
@@ -66,12 +66,28 @@
|
|||||||
link.className = 'text-blue-400 underline text-sm break-all';
|
link.className = 'text-blue-400 underline text-sm break-all';
|
||||||
link.textContent = 'Open the Google consent screen';
|
link.textContent = 'Open the Google consent screen';
|
||||||
|
|
||||||
const hint = document.createElement('p');
|
// Deliberately loud. After consent the browser is redirected to a
|
||||||
hint.className = 'text-xs text-gray-400 mt-2';
|
// loopback address nothing is listening on, so it lands on a
|
||||||
hint.textContent =
|
// browser error page -- which reads as a failure at exactly the
|
||||||
'After approving, your browser will try to open a page that fails '
|
// moment the user has to act on it. Said quietly in grey it gets
|
||||||
+ 'to load. That is expected. Copy its full address from the bar '
|
// missed, and the flow looks broken when it is working.
|
||||||
+ 'and paste it below.';
|
const hint = document.createElement('div');
|
||||||
|
hint.className =
|
||||||
|
'mt-3 p-3 rounded-md border border-amber-500/60 bg-amber-500/10';
|
||||||
|
hint.innerHTML =
|
||||||
|
'<p class="text-sm text-amber-300 font-semibold">'
|
||||||
|
+ '<i class="fas fa-triangle-exclamation"></i> '
|
||||||
|
+ 'The next page will fail to load. That is expected.</p>'
|
||||||
|
+ '<p class="text-xs text-amber-200/90 mt-1">'
|
||||||
|
+ 'After you approve access, Google sends your browser to '
|
||||||
|
+ '<code>127.0.0.1</code>, where nothing is running \u2014 so you will see '
|
||||||
|
+ '"This site can\u2019t be reached" or similar. Nothing has gone wrong. '
|
||||||
|
+ 'Copy the <strong>entire address</strong> out of the address bar '
|
||||||
|
+ '(it contains <code>?code=...</code>) and paste it in the box below.</p>';
|
||||||
|
|
||||||
|
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');
|
const codeInput = document.createElement('input');
|
||||||
codeInput.type = 'text';
|
codeInput.type = 'text';
|
||||||
@@ -150,6 +166,7 @@
|
|||||||
|
|
||||||
step2.appendChild(link);
|
step2.appendChild(link);
|
||||||
step2.appendChild(hint);
|
step2.appendChild(hint);
|
||||||
|
step2.appendChild(codeLabel);
|
||||||
step2.appendChild(codeInput);
|
step2.appendChild(codeInput);
|
||||||
step2.appendChild(finishBtn);
|
step2.appendChild(finishBtn);
|
||||||
|
|
||||||
|
|||||||
@@ -815,7 +815,7 @@
|
|||||||
<i class="fas fa-info-circle mr-1"></i>
|
<i class="fas fa-info-circle mr-1"></i>
|
||||||
Changes in the file manager save immediately — no need to click Save Configuration.
|
Changes in the file manager save immediately — no need to click Save Configuration.
|
||||||
</p>
|
</p>
|
||||||
{% 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 #}
|
{# Render widget container #}
|
||||||
<div id="{{ field_id }}_container" class="{{ str_widget }}-container"></div>
|
<div id="{{ field_id }}_container" class="{{ str_widget }}-container"></div>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
Reference in New Issue
Block a user