Skip to content

Add Google Calendar app - #675

Draft
nsluke wants to merge 4 commits into
tronbyt:mainfrom
nsluke:google-calendar
Draft

Add Google Calendar app#675
nsluke wants to merge 4 commits into
tronbyt:mainfrom
nsluke:google-calendar

Conversation

@nsluke

@nsluke nsluke commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Your next event, at a glance — a recreation of Tidbyt's first-party Google Calendar app (13th most-installed on the platform). It was OAuth-only through their cloud, so it died with the shutdown, and nothing has replaced it: the catalog covers iCal feeds (anycalendar, universalical, hellocal) and Home Assistant, but not Google directly.

Screenshots

Next event In progress
soon now
All-day event All clear
allday clear

Three ways to authenticate

  1. A server-injected token. If config["auth"] is present it's used directly as a bearer token — the tronbyt-server Connections contract. A Google provider on the server side would light this app up with no changes here.
  2. Your own OAuth client — client ID, secret and refresh token as text fields, the pattern outlookcalendar and spotify already use. The app exchanges the refresh token and caches the access token for expires_in minus a minute.
  3. Nothing configured → a setup card with zero network, which is also the default-config path CI renders.

Two API details worth knowing

Both came from the live discovery doc (revision 20260826), fetched keyless rather than recalled from memory:

  • timeMin is a lower bound on the event's end time, not its start. That's what makes the "NOW" state work from a single query — in-progress events come back naturally, no second request.
  • maxAttendees=1 returns only the requesting participant, which gives exactly the self-attendee needed to filter declined events while shrinking the payload.

Also confirmed live: the token endpoint answers 401 {"error":"invalid_client"} (invalid_grant is 400), and the unauthenticated events endpoint returns 403 PERMISSION_DENIED, not 401 — so the app treats both as auth failures and clears the cached token so the next cycle re-exchanges.

Verification

20 states from a stub: soon, imminent, later today, now, back-to-back, all-day, tomorrow, next-all-day, clear-7-days, no items, long title, declined, nulls, 401, 500, non-JSON, token failure, token endpoint down, and the Connections path.

pixlet check passes under the repo's pinned v0.50.1. Default render 31 ms, no network.

Adversarial review found nine issues, all reproduced and fixed. The two most interesting:

  • A shape-valid but out-of-range date in the payload aborted every render indefinitely — the bad value was cached outside the gate, so it poisoned the app permanently.
  • Countdowns were lost across local midnight: "in 25 min" and the imminent-red state broke when the event and "now" fell on different calendar dates. The countdown no longer consults the calendar date at all.

Also fixed: Google's non-meeting event types (workingLocation, outOfOffice) were hijacking the card and hiding your real next event.

Caveats

  • The events API has never been called for real — there are no Google credentials on this machine. The contract comes from the live discovery doc plus a stub built to it. The token endpoint and its error shapes were probed live. Someone with a Google client should try it before this merges.
  • Primary calendar only; the display timezone is the device's $tz.
  • hash.star isn't available, so cache keys use a hand-rolled djb2 hash of the credentials. Collision risk is negligible, and outlookcalendar keys by the raw token for comparison.
  • Tentative and transparent events are shown; only self-declined ones are filtered.
  • Titles in scripts with no pixlet font coverage (CJK) collapse to a placeholder glyph — no font ships that coverage, so there's nothing to fix at this layer.

🤖 Generated with Claude Code


64x64 square panel support

The wide card shows exactly one event because 2:1 is all it fits — while fetch_events had already pulled, filtered and cached a whole week of them with nowhere to put them. On square an event card becomes an agenda: the wide card's hero across the top (badge grown to the panel), a rule, then up to three "when | what" rows. Cards that aren't about an event (setup, offline, reconnect, all-clear) become a centred poster using the full 64px width rather than the 43px the icon leaves beside it on wide. The plumbing is one optional rest argument: None means poster, a list means agenda.

square 64x64

The wide layout is untouched. An additive is_square() branch on canvas.size() (h * 2 > w + 16, matching the convention in #655/#666 — shape, never size, so a 2x wide panel is never mistaken for a square one). Wide output was rendered before and after the port and compared byte for byte: 262 comparisons across configs at both 64x32 and 128x64, zero differences, with a deliberately broken copy used as a negative control to prove the comparison actually discriminates. pixlet check green on the pinned v0.50.1. No manifest change — upstream has no supports64x64 field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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

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