Skip to content

fix(calendar): improve setup flow and fix OAuth for headless Pi - #85

Merged
ChuckBuilds merged 2 commits into
mainfrom
fix/calendar-setup-flow
Apr 2, 2026
Merged

fix(calendar): improve setup flow and fix OAuth for headless Pi#85
ChuckBuilds merged 2 commits into
mainfrom
fix/calendar-setup-flow

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented Apr 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • Setup flow: Reorder config fields into a clear 3-step process (Step 1: Upload Credentials → Step 2: Authenticate → Step 3: Select Calendars)
  • Headless OAuth: Rewrite calendar_registration.py to use OOB (out-of-band) flow when running from the web UI on a headless Pi, instead of run_local_server() which tries to open a browser that doesn't exist
  • Cleanup: Remove token_file from the config schema (internal detail users shouldn't see), improve field descriptions

How the OAuth flow works now

  1. User clicks "Authenticate Google Calendar" button
  2. Script detects headless mode, generates an auth URL, returns it as JSON
  3. Web UI displays the URL as a clickable link with instructions
  4. User visits the URL in their own browser, authorizes, gets a code
  5. User clicks "Complete Authentication" and pastes the code
  6. Script exchanges the code for a token and saves it

The web UI JS already supported this two-step flow pattern (used by other plugins like Spotify).

Test plan

  • Web UI shows fields in order: credentials upload → authenticate button → calendar picker → settings
  • "Step 1/2/3" titles appear in the UI
  • Clicking Authenticate without credentials shows clear error with instructions
  • With credentials uploaded, clicking Authenticate shows auth URL
  • After completing OAuth, "Load My Calendars" returns calendar list
  • Terminal mode (python calendar_registration.py) still works with browser popup

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Two-step Google Calendar authentication with explicit second-step UI and support for headless/non-interactive setups.
  • Bug Fixes

    • Improved structured error output for missing credentials and authentication failures; streamlined re-auth flow.
  • Documentation

    • Updated configuration UI text, property ordering, and descriptions; revised authentication prompts and button labels.

The calendar plugin setup was confusing and the OAuth flow didn't work
on headless devices (tried to open a browser on the Pi).

Setup flow improvements:
- Reorder config fields with x-propertyOrder: credentials upload first,
  then calendar picker, then display settings
- Add "Step 1/2/3" titles to guide users through the setup process
- Remove token_file from the schema (internal implementation detail)
- Improve field descriptions with actionable instructions

OAuth fix for headless devices:
- Rewrite calendar_registration.py to detect headless mode and use
  the OOB (out-of-band) OAuth flow instead of run_local_server()
- Step 1: generates an auth URL and returns JSON with requires_step2
- Step 2: accepts the authorization code via stdin and completes the
  token exchange
- The web UI already supports this two-step flow (shows clickable URL,
  provides input for the code)
- Terminal mode still uses run_local_server() for interactive use

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 2, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a dual-mode (interactive + headless) OAuth flow for Google Calendar registration, JSON-first headless I/O, credential testing, and updates to the plugin configuration schema and web UI authentication text to support the multi-step flow.

Changes

Cohort / File(s) Summary
OAuth Registration Logic
plugins/calendar/calendar_registration.py
Introduces headless detection and a two-step JSON-driven OAuth flow; adds _is_headless() and _test_credentials() helpers; emits JSON for missing credentials, step1 (auth_url + requires_step2) and step2 (consumes redirect/code via stdin, exchanges tokens, saves token.pickle, returns discovered calendars); refactors interactive prompts and token handling.
Configuration Schema
plugins/calendar/config_schema.json
Reorders and updates schema UI metadata (x-propertyOrder); removes token_file; updates credentials_file title/description; reintroduces/updates calendars, max_events, update_interval, show_all_day_events, display_duration; relaxes top-level required to ["enabled"] and sets additionalProperties: true.
Authentication UI Text
plugins/calendar/manifest.json
Updates web_ui_actions item id: "authenticate-google" with new step-2 wording: revised title and description, adds step2_prompt and step2_button_text; updates last_updated metadata.

Sequence Diagram

sequenceDiagram
    actor User
    participant Plugin as Calendar Plugin
    participant OAuth as Google OAuth
    participant Storage as File System

    User->>Plugin: Trigger authentication (headless)
    Plugin->>Plugin: Detect headless mode
    Plugin->>User: Respond JSON with auth_url + requires_step2

    User->>OAuth: Visit auth_url and authorize
    OAuth->>User: Provide authorization code (redirect or copy)

    User->>Plugin: Send redirect/code via stdin (step2)
    Plugin->>OAuth: Exchange code for tokens
    OAuth->>Plugin: Return tokens
    Plugin->>Storage: Persist token.pickle
    Storage-->>Plugin: Persist confirmation
    Plugin->>User: Return JSON with calendars and success
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: improving the calendar setup flow and fixing OAuth for headless Raspberry Pi systems.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/calendar-setup-flow

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugins/calendar/manifest.json (1)

37-49: ⚠️ Potential issue | 🟡 Minor

Date inconsistency: last_updated is older than versions[0].released.

last_updated is 2026-02-25 while the top version entry shows released: 2026-03-02. Consider updating last_updated to match or exceed the most recent release date.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/calendar/manifest.json` around lines 37 - 49, The manifest currently
has a date mismatch: the top "versions" entry has "released": "2026-03-02" while
"last_updated" is "2026-02-25"; update the "last_updated" field in manifest.json
to match or be later than the most recent release date (e.g., set "last_updated"
to "2026-03-02" or a later ISO date) so "versions" -> "released" and
"last_updated" remain consistent.
🧹 Nitpick comments (2)
plugins/calendar/calendar_registration.py (2)

122-129: Redundant step2 UI text duplicated in manifest and script.

The step2_prompt and step2_button_text values are defined both here and in manifest.json (lines 31-32). This creates a maintenance burden if the text needs to change. Consider having the script omit these fields and let the web UI use the manifest values as the source of truth.

♻️ Remove duplicate UI text from script output
             print(json.dumps({
                 "status": "success",
                 "requires_step2": True,
                 "auth_url": auth_url,
-                "message": "Open the link below to authorize Google Calendar access",
-                "step2_prompt": "Paste the authorization code from Google:",
-                "step2_button_text": "Complete Authentication"
+                "message": "Open the link below to authorize Google Calendar access"
             }))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/calendar/calendar_registration.py` around lines 122 - 129, The
printed JSON in calendar_registration.py currently includes duplicate UI strings
("step2_prompt" and "step2_button_text") that are already defined in
manifest.json; remove the "step2_prompt" and "step2_button_text" keys from the
dict passed to print(json.dumps(...)) (the block that constructs the success
response with "status", "requires_step2", and "auth_url") so the web UI uses
manifest.json as the single source of truth for those labels.

69-72: Silent exception swallowing on stdin read.

The try-except-pass pattern silently ignores all errors when reading stdin. While this is likely intentional to handle edge cases gracefully, consider at minimum logging to stderr for debugging purposes, or catching a more specific exception type.

🔧 Optional: Log stdin read failures to stderr
         try:
             stdin_data = sys.stdin.read().strip()
-        except Exception:
-            pass
+        except Exception as e:
+            import sys
+            print(f"Debug: stdin read failed: {e}", file=sys.stderr)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/calendar/calendar_registration.py` around lines 69 - 72, The code
currently swallows all exceptions when calling sys.stdin.read() into stdin_data;
replace the bare except with a specific catch (e.g., except (IOError, OSError)
as e) and ensure you log the failure (for example using sys.stderr.write or
logging.getLogger(__name__).exception) while preserving behavior by setting
stdin_data = "" on error; update the block around sys.stdin.read() / stdin_data
accordingly so failures are visible but do not crash the process.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugins/calendar/calendar_registration.py`:
- Around line 84-97: The code currently sets flow.redirect_uri to the deprecated
OOB URI and tries to extract an auth code from redirect_url; replace this with a
loopback redirect flow: use InstalledAppFlow.run_local_server (or set
redirect_uri to a localhost loopback like http://127.0.0.1:PORT and start a
transient local HTTP listener) to receive the authorization code for the client
created from CREDENTIALS_FILE with SCOPES, remove the manual parsing of
redirect_url and the urn:ietf:wg:oauth:2.0:oob usage, and then call
flow.fetch_token or obtain credentials from run_local_server to complete the
token exchange.

In `@plugins/calendar/config_schema.json`:
- Around line 26-36: The calendars schema entry ("calendars" in
config_schema.json) has minItems: 1 which causes validation failures if the UI
initializes an empty array or omits the key during setup; fix by either removing
the minItems constraint from the "calendars" property or making "calendars"
required in the root schema so it's guaranteed to exist with at least one item,
alternatively change the UI to omit the "calendars" key until the user selects
one—locate the "calendars" property (x-widget: "google-calendar-picker",
default: ["primary"]) and apply one of these fixes to ensure consistent
validation.

---

Outside diff comments:
In `@plugins/calendar/manifest.json`:
- Around line 37-49: The manifest currently has a date mismatch: the top
"versions" entry has "released": "2026-03-02" while "last_updated" is
"2026-02-25"; update the "last_updated" field in manifest.json to match or be
later than the most recent release date (e.g., set "last_updated" to
"2026-03-02" or a later ISO date) so "versions" -> "released" and "last_updated"
remain consistent.

---

Nitpick comments:
In `@plugins/calendar/calendar_registration.py`:
- Around line 122-129: The printed JSON in calendar_registration.py currently
includes duplicate UI strings ("step2_prompt" and "step2_button_text") that are
already defined in manifest.json; remove the "step2_prompt" and
"step2_button_text" keys from the dict passed to print(json.dumps(...)) (the
block that constructs the success response with "status", "requires_step2", and
"auth_url") so the web UI uses manifest.json as the single source of truth for
those labels.
- Around line 69-72: The code currently swallows all exceptions when calling
sys.stdin.read() into stdin_data; replace the bare except with a specific catch
(e.g., except (IOError, OSError) as e) and ensure you log the failure (for
example using sys.stderr.write or logging.getLogger(__name__).exception) while
preserving behavior by setting stdin_data = "" on error; update the block around
sys.stdin.read() / stdin_data accordingly so failures are visible but do not
crash the process.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5fb40bff-1750-484c-8836-971db3bddd53

📥 Commits

Reviewing files that changed from the base of the PR and between 02f6262 and 2602b8a.

📒 Files selected for processing (3)
  • plugins/calendar/calendar_registration.py
  • plugins/calendar/config_schema.json
  • plugins/calendar/manifest.json

Comment on lines +84 to +97
try:
flow = InstalledAppFlow.from_client_secrets_file(str(CREDENTIALS_FILE), SCOPES)
flow.redirect_uri = 'urn:ietf:wg:oauth:2.0:oob'
# Extract the authorization code from the redirect URL
if 'code=' in redirect_url:
from urllib.parse import urlparse, parse_qs
parsed = urlparse(redirect_url)
code = parse_qs(parsed.query).get('code', [None])[0]
if not code:
code = redirect_url # Maybe it's just the code
else:
code = redirect_url.strip()

flow.fetch_token(code=code)

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Is Google OAuth OOB flow (urn:ietf:wg:oauth:2.0:oob) still supported for Desktop OAuth clients in 2026?

💡 Result:

No, Google OAuth OOB flow (urn:ietf:wg:oauth:2.0:oob) is not supported for Desktop OAuth clients in 2026. It was deprecated and fully blocked as of January 31, 2023, for all client types including desktop apps. Desktop apps must migrate to loopback IP redirect URIs like http://127.0.0.1 or http://localhost (with a local server to capture the code).

Citations:


Google fully deprecated and blocked the OOB OAuth flow—this code is non-functional as of January 31, 2023.

The redirect URI urn:ietf:wg:oauth:2.0:oob was fully blocked by Google on January 31, 2023, for all OAuth client types. Any attempt to use this flow will fail. Desktop apps must migrate to loopback IP redirects (e.g., http://127.0.0.1 or http://localhost) with a local server to intercept the authorization code.

Refer to the OOB migration guide and native app guide for implementation details.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/calendar/calendar_registration.py` around lines 84 - 97, The code
currently sets flow.redirect_uri to the deprecated OOB URI and tries to extract
an auth code from redirect_url; replace this with a loopback redirect flow: use
InstalledAppFlow.run_local_server (or set redirect_uri to a localhost loopback
like http://127.0.0.1:PORT and start a transient local HTTP listener) to receive
the authorization code for the client created from CREDENTIALS_FILE with SCOPES,
remove the manual parsing of redirect_url and the urn:ietf:wg:oauth:2.0:oob
usage, and then call flow.fetch_token or obtain credentials from
run_local_server to complete the token exchange.

Comment thread plugins/calendar/config_schema.json
…mismatch

- Replace deprecated urn:ietf:wg:oauth:2.0:oob with http://127.0.0.1
  loopback redirect for OAuth flow
- Remove duplicate step2_prompt/step2_button_text from JSON output
  (manifest.json is the single source of truth for those labels)
- Narrow bare except on stdin.read() to (IOError, OSError) with
  stderr logging
- Remove minItems: 1 from calendars schema to prevent validation
  failures when UI initializes with an empty array
- Fix last_updated date to match latest release (2026-03-02)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (2)
plugins/calendar/config_schema.json (1)

150-151: Consider keeping root validation strict.

additionalProperties: true will make misspelled settings silently valid. If this was added only to tolerate legacy token_file configs, I'd rather whitelist that field explicitly and keep unknown top-level keys rejected.

🔧 Suggested direction
   "required": ["enabled"],
-  "additionalProperties": true
+  "additionalProperties": false

If backward compatibility is still needed, add token_file back under properties as a deprecated/hidden field instead of accepting arbitrary keys.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/calendar/config_schema.json` around lines 150 - 151, Replace the
permissive root schema setting "additionalProperties": true with a strict policy
and explicitly whitelist any legacy field (e.g., add a "token_file" entry under
"properties" and mark it deprecated/hidden), so unknown top-level keys are
rejected; keep "required": ["enabled"] as-is and ensure the "token_file"
property is documented as deprecated rather than allowing arbitrary extra keys
via additionalProperties.
plugins/calendar/calendar_registration.py (1)

76-83: Accept raw authorization codes in step 2.

The Line 77 gate only treats URLs/query strings as step-2 input. A plain pasted code, or a JSON payload that only carries the code, falls back to Step 1 again, so the documented headless flow breaks unless the user pastes the full redirect URL.

🔧 Proposed fix
-    # If stdin contains an auth code (step 2), complete the flow
-    if stdin_data and ('http' in stdin_data or 'code=' in stdin_data):
+    # If stdin contains step-2 data, complete the flow
+    if stdin_data:
         # Try to parse as JSON first (web UI sends JSON params)
         redirect_url = stdin_data
         try:
             params = json.loads(stdin_data)
-            redirect_url = params if isinstance(params, str) else params.get('redirect_url', stdin_data)
+            if isinstance(params, str):
+                redirect_url = params
+            elif isinstance(params, dict):
+                redirect_url = params.get('redirect_url') or params.get('code') or stdin_data
         except (json.JSONDecodeError, AttributeError):
             pass
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/calendar/calendar_registration.py` around lines 76 - 83, The step-2
stdin handling currently only treats inputs containing 'http' or 'code=' as auth
responses; update the conditional and parsing around stdin_data, redirect_url,
and the json.loads block so raw authorization codes and JSON payloads with a
'code' field are accepted: detect JSON that contains 'code' (use
params.get('code')) and, when stdin_data is a plain code string (e.g.,
alphanumeric token), convert it into a redirect-style value (e.g., '?code=...')
or directly use the code to complete the flow instead of falling back to step 1;
ensure redirect_url is set from params.get('redirect_url') or params.get('code')
as appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@plugins/calendar/calendar_registration.py`:
- Around line 76-83: The step-2 stdin handling currently only treats inputs
containing 'http' or 'code=' as auth responses; update the conditional and
parsing around stdin_data, redirect_url, and the json.loads block so raw
authorization codes and JSON payloads with a 'code' field are accepted: detect
JSON that contains 'code' (use params.get('code')) and, when stdin_data is a
plain code string (e.g., alphanumeric token), convert it into a redirect-style
value (e.g., '?code=...') or directly use the code to complete the flow instead
of falling back to step 1; ensure redirect_url is set from
params.get('redirect_url') or params.get('code') as appropriate.

In `@plugins/calendar/config_schema.json`:
- Around line 150-151: Replace the permissive root schema setting
"additionalProperties": true with a strict policy and explicitly whitelist any
legacy field (e.g., add a "token_file" entry under "properties" and mark it
deprecated/hidden), so unknown top-level keys are rejected; keep "required":
["enabled"] as-is and ensure the "token_file" property is documented as
deprecated rather than allowing arbitrary extra keys via additionalProperties.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 461a46fa-61dd-4824-ba88-64d57a12a9c3

📥 Commits

Reviewing files that changed from the base of the PR and between 2602b8a and f150786.

📒 Files selected for processing (3)
  • plugins/calendar/calendar_registration.py
  • plugins/calendar/config_schema.json
  • plugins/calendar/manifest.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugins/calendar/manifest.json

@ChuckBuilds
ChuckBuilds merged commit f78fd7f into main Apr 2, 2026
1 check passed
@ChuckBuilds
ChuckBuilds deleted the fix/calendar-setup-flow branch August 5, 2026 17:31
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