Skip to content

feat(kix): install kix.quest as a phone app with push notifications - #165

Merged
kixelated merged 13 commits into
mainfrom
quest/m0/kix/mobile
Oct 21, 2026
Merged

kixelated merged 13 commits into
mainfrom
quest/m0/kix/mobile

Conversation

@kixelated

@kixelated kixelated commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Implements and deletes quest/m0/kix/mobile.md: kix.quest installs as a phone or desktop app and sends a Web Push notification whenever something needs a decision.

What changes

  • Installable app. A web app manifest (start_url: /inbox, standalone), full-bleed app icons (design/theme/icon.svg and its 192/512 PNG exports, maskable-safe), an apple-touch-icon, and a service worker at /sw.js. Signed-in pages load /app.js, which registers it.
  • Turning notifications on. /inbox (compact, until this device is on) and /account show a switch. Permission is requested only from the button click. On iPhone/iPad outside a Home Screen app it explains how to add Quest to the Home Screen. Subscriptions are stored in a new push_subscriptions table (migration 0014_push.sql), keyed by endpoint, with the user and the session that enabled them. Only known push services (FCM, Mozilla, Apple, Windows) are accepted.
  • Removal. Signing out deletes that session's subscriptions, and the sign-out form first clears the app badge and unsubscribes the browser's own, which discards pushes still queued for that device. Known gap: a sign-out with JavaScript off can't drop the browser subscription, so a push already queued for that device can still arrive within its 24h TTL. The switch shows a device as on only while the server stores its subscription for the signed-in user (/push/check, which also moves it to the current session), so whoever signs in next on that browser turns notifications on with their own click. A rotated subscription (pushsubscriptionchange) replaces the old one only for the user who enabled it, and the switch drops a browser subscription made for an earlier VAPID key. A 404/410 from the push service deletes the subscription. pushsubscriptionchange re-registers a rotated subscription and replaces the old one.
  • Only decisions push (cloudflare/src/push/notify.ts): an agent question (runner question), a publication decision, an account limit, a failed job, a new or reopened issue, and a PR opened non-draft or marked ready for review. Progress, presence, completions, draft PRs, other webhook updates, and paused projects (including jobs that stop after a pause) never push. A webhook's push goes after its background read (#170), so a title the webhook lacks can come from GitHub. Each push fires once, on the fresh transition (retries and duplicate webhooks don't re-push). It carries only the project, the item title, the inbox URL, and the unread count.
  • Coalescing. One notification tag per item replaces the previous notification on the device. One push Topic per item lets the push service replace an undelivered message. A tap opens /inbox?project=…&item=…. There are no answer buttons.
  • Web Push without a library. cloudflare/src/push/webpush.ts implements RFC 8291 aes128gcm encryption and RFC 8292 VAPID ES256 signing with WebCrypto, which the Workers runtime supports. It is tested against RFC 8291's worked example byte for byte, and the VAPID JWT is verified with the public key.
  • Unread count. The nav shows "Decisions N", and the installed app's icon badge (Badging API) shows the same count. The service worker also updates the badge from each push. The inbox marks its active item seen before rendering, so the nav and badge leave out the decision being shown.
  • Phone-first. The nav is tighter under 40rem. I checked the inbox and account pages at 390px from HTML rendered by the Worker.
  • Local harness. just worker-seeded leaves notifications off (empty VAPID values), since a push would leave localhost.
  • Config. VAPID_PUBLIC_KEY (var) plus the VAPID_PRIVATE_KEY secret. Both are optional, like Google sign-in: notifications stay off until both are set. bun scripts/vapid.ts prints a pair. README, .dev.vars.example, and deployment steps are updated.

Decisions I made (please confirm)

  1. WebCrypto, no push library. The quest asked to verify that Workers WebCrypto handles the encryption and signing before choosing a library. It does, and the implementation is about 100 lines checked against the RFC vector, so I added no dependency.
  2. Notifications are optional config. They work like Google sign-in, rather than adding VAPID_PRIVATE_KEY to secrets.required and breaking deploys and local dev until keys exist.
  3. Recipients = whoever's inbox shows the project: its members (devices() joins github_project_members, from #174). Each push rechecks their current maintainer authority on GitHub, as the inbox does, and skips them if it fails.
  4. Drafts don't push. Draft PRs are claims. "Ready for review" means non-draft opened/reopened or ready_for_review. The push text uses the signed webhook's title, display only, and nothing about it is stored.
  5. Subscriptions remember their session but have no foreign key to it. An expired session therefore doesn't silently stop notifications; explicit sign-out does.
  6. The nav and badge count reads stored observations without rechecking GitHub authority on every page. It reveals only a number; the inbox still rechecks before showing anything.
  7. TTL 24h, Urgency: high, title cut at 120 characters, VAPID sub = the AUTH_URL origin.

Validation

  • just check passes.
  • just test passes: 285 CLI tests and 295 Worker tests, including the new test/push.test.ts and a hub test in test/jobs.test.ts.
  • The tests decrypt what a subscribed device receives. They cover:
    • pushes only on decisions, once each, including duplicate webhook deliveries and a retried runner question
    • paused projects, draft PRs, and shared topics
    • origin, sign-in, and push-service checks on subscribe
    • sign-out removal and 410 pruning
    • nav and badge counts
  • Not verified: a real install and push on Android, iOS 16.4+, or desktop, against FCM/APNs. That needs the deployed Worker (deploy and operator setup) and fits the kix epic's live rehearsal. The client scripts were syntax-checked, not run in a browser.

Suggested follow-ups

  • Verify install and push on real Android, iOS, and desktop devices during the epic's live rehearsal. I recommend folding this into operator setup rather than a new quest.
  • Push draft PRs that gain a ## Decisions needed hand-back, once the inbox models them as decision items.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 21, 2026 10:21
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kix.quest installs as a PWA with a web app manifest, app icons, and a
service worker. Maintainers turn on Web Push per device from /inbox or
/account; only decisions push (agent questions, publication decisions,
account limits, failed jobs, new issues, PRs ready for review), once
each, coalesced per item by notification tag and push Topic, and a tap
opens the item in the inbox. Subscriptions live in D1 per user and
session, are deleted on sign-out and when the push service reports them
gone. RFC 8291 encryption and VAPID signing use WebCrypto directly. The
nav and the installed app's icon show the unread decision count, and the
phone nav is tighter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Owner Author

Outcome: the quest is implemented and its file deleted in this PR. just check and just test pass locally (227 CLI and 259 Worker tests). The PR body lists the routine decisions I made unattended (no push library, optional VAPID config, recipients = the enrolling maintainer, drafts don't push) for confirmation. Real-device install and push on Android, iOS, and desktop are not verified yet; they need the deployed Worker.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T20:32:51.915252Z 4ad2bf6 New commits
🔒 Security Review ✅ Completed 2026-10-21T20:34:11.474306Z 4ad2bf6 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8a1a9907c5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cloudflare/src/app.js
Comment on lines +31 to +35
const subscription = supported && (await (await registration).pushManager.getSubscription());
const on = !!subscription && Notification.permission === "granted";
const blocked = supported && Notification.permission === "denied";
// The inbox's compact control steps aside once there is nothing to switch on.
control.hidden = (on || blocked) && "compact" in control.dataset && !message;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Re-register subscriptions after users sign back in

When a user signs out, /sign-out removes the D1 subscription but does not unsubscribe the browser's PushSubscription. After the user signs back in, this check treats that stale browser subscription as enabled, hides the compact control, and never posts it back to /push/subscribe; the UI therefore claims the device is notified while the server has no destination and sends it nothing. Reconcile the existing subscription with the server before reporting notifications as on.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 6f17641a. Signing out now also unsubscribes the browser's own subscription (the sign-out form waits for it), so the next person to sign in on that browser opts in for themselves. When the switch loads with notification permission granted, it stores any existing browser subscription again for the current session, so it never shows a device as notified that the server forgot (sign-out without JS, or a session that expired and signed back in).

(Written by Claude Opus 5.5)

Comment thread cloudflare/src/push/notify.ts Outdated
Comment on lines +106 to +109
const project = await env.DB.prepare("SELECT id,owner,name,state FROM github_projects WHERE id=?")
.bind(decision.project)
.first<Enrollment>();
if (project) await deliver(env, { ...decision, project }, targets);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Suppress job notifications while a project is paused

When a project is paused while an attempt is still running, that attempt may subsequently ask a question, hit an account limit, or fail. Those paths call push.job, and this lookup accepts the paused project and delivers the notification anyway, unlike webhook notifications and the documented rule that paused projects do not push. Restrict this lookup to enabled projects before delivery.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 6f17641a. notify now looks up only enabled projects, so a job that asks, hits a limit, or fails after its project pauses doesn't push. The question test covers it.

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 21, 2026 11:44
Rename the push migration to 0012_push.sql, after main's 0011_project_scan.sql.

Signing out now drops the browser's subscription too, and the switch stores
an existing subscription again when it loads, so it never shows a device as
notified that the server forgot. Jobs on a paused project no longer push.

Main keeps quests available across webhooks that cannot change them, so the
badge in the new-issue test now counts the ready quest.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f176415dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cloudflare/src/app.js Outdated
const synced = (async () => {
if (!supported || !controls.length || Notification.permission !== "granted") return;
const subscription = await (await registration).pushManager.getSubscription();
if (subscription) await post("/push/subscribe", subscription.toJSON());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require opt-in before transferring a retained subscription

When browser-side unsubscribe does not happen—for example, JavaScript was disabled during sign-out or unsubscribe() rejects—the server deletes the old session's row but the browser retains its PushSubscription. On the next account's inbox or account page, this unconditional POST transfers the endpoint to that user through the server's ON CONFLICT update even though they never clicked the enable button. The newly added unconditional resync is fresh evidence distinct from the prior stale-state report; reclaim an existing subscription only after verifying that it belonged to the same user, or require a new click.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed, fixed in dc9e51c. The switch no longer stores the browser's subscription on load. It asks a new /push/check whether the server notifies that endpoint for the signed-in user. A match moves to the current session, so an expired session's device stays on and signing out here forgets it. Otherwise the switch shows notifications off, and turning them on again takes a click. That also made the sign-out handler from 6f17641 unnecessary, so I removed it.

(Written by Claude Opus 5.5)

Comment thread cloudflare/src/push/notify.ts Outdated
export async function devices(env: Env, project: number): Promise<Device[]> {
if (!keys(env)) return [];
const { results } = await env.DB.prepare(
"SELECT s.endpoint,s.p256dh,s.auth,s.user FROM push_subscriptions s JOIN github_projects p ON p.enrolledBy=s.user WHERE p.id=?",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop pushing after maintainer access is revoked

When the enrolled user loses their maintain/admin role without the project being disconnected, this join still selects all of their devices for subsequent webhook and job notifications. inbox.load excludes that project's items when its live authority check fails, while disconnecting also requires current authority, so the former maintainer can keep receiving issue, PR, and job-title notifications for a project no longer present in their decision inbox and cannot disable only that project. Revalidate or prune the recipient's authority before delivery.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed, fixed in dc9e51c. deliver now rechecks each recipient's current maintainer authority (GitHubService.maintains, the same check the inbox makes before listing a project's items) and skips anyone who fails it. A test covers a maintainer demoted to write.

(Written by Claude Opus 5.5)

…ent maintainers

The switch no longer stores a browser's leftover subscription for whoever
signs in. It asks /push/check whether the server notifies that endpoint for
the signed-in user, which moves a match to the current session, and shows
notifications off otherwise, so turning them on again takes a click. The
sign-out handler that dropped the browser's subscription is no longer needed.

Each push rechecks that its recipient still maintains the project, as the
inbox does before listing its items. Tests sign with the test App key by
default so background work in the runner hub can make that check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	cloudflare/src/inbox/page.tsx
#	cloudflare/test/publish.test.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7c64c41f65

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cloudflare/src/sw.js
Comment on lines +59 to +63
await fetch("/push/subscribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...subscription.toJSON(), replaces: old.endpoint }),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve opt-in during subscription rotation

When a retained browser subscription rotates while a different user is signed in, this unconditional POST stores the replacement for the current user through /push/subscribe, even if that user never enabled notifications. This can happen after the prior user signs out while the browser retains its subscription, so the next account is silently opted in. The remaining pushsubscriptionchange POST is fresh evidence distinct from the fixed page-load resync; check that the old endpoint belongs to the signed-in user before replacing it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed, fixed in 8132d53. A subscribe that names replaces now stores the new endpoint only when the replaced one belongs to the signed-in user, and otherwise returns 403 and stores nothing. A test covers a rotation while another user is signed in.

(Written by Claude Opus 5.5)

Comment on lines +195 to +198
const result = await c.env.DB.prepare("UPDATE push_subscriptions SET session=? WHERE endpoint=? AND user=?")
.bind(session.session.id, body.endpoint, session.user.id)
.run();
return c.json({ stored: result.meta.changes > 0 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Invalidate subscriptions when VAPID keys rotate

When an operator replaces the VAPID key pair, existing browser subscriptions remain bound to the old public key, but this endpoint-only check still reports them as stored. The UI consequently shows notifications as enabled and hides the enable action, while sends signed with the new key are rejected and retained as transient failures rather than removed. Store or compare the application-server key used by each subscription, or clear mismatched subscriptions so the device can enroll again.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed, fixed in 8132d53 on the client. Before checking or enabling, the switch compares the browser subscription's applicationServerKey with the page's current VAPID key. On a mismatch it removes the subscription from the server and the browser, so the device shows off and can enroll again with the new key. Devices that never reopen the page keep their stale rows, which only log failed sends. I didn't add a key column, because the client check covers the UI case without a schema change.

(Written by Claude Opus 5.5)

A rotated subscription replaces the old one only when the signed-in user
enabled it, so a rotation never opts in whoever is signed in now. The switch
drops a browser subscription made for an earlier VAPID key pair, which could
not receive pushes and would block enrolling with the new key.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8132d5353b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cloudflare/src/index.tsx
Comment on lines +503 to +504
// This device stops notifying once it signs out.
if (session) await push.forget(c.env.DB, session.session.id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Unsubscribe the browser before completing sign-out

If a push has already been accepted while the device is offline, deleting only the D1 row does not cancel that message at the push service; with the 24-hour TTL in push/notify.ts, the retained browser subscription can therefore display the previous user's project and item title after sign-out, potentially while another user is using the device. Fresh evidence since the earlier thread is that the current app.js no longer attaches the browser-side sign-out unsubscribe handler added in 6f17641, so sign-out needs to deactivate the PushSubscription as well as forget the server row.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed, fixed in 7a68873. The sign-out form now unsubscribes the browser's push subscription before it submits, then the server forgets the session's rows. Unsubscribing discards any push the push service still holds for that device. A test runs app.js against a stand-in browser and checks that the unsubscribe happens before the submit.

A sign-out with JavaScript off still deletes the server rows but can't drop the browser subscription. A push already queued for that device can then still arrive within its 24-hour TTL. I recorded that gap in the PR body and the Worker README.

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 21, 2026 13:14
The sign-out form unsubscribes the browser's push subscription before it
submits, so a push the push service still holds for that device is discarded
rather than shown to whoever uses it next. A sign-out without JavaScript
leaves such pushes to arrive within their 24-hour TTL.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a68873f56

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cloudflare/src/index.tsx Outdated
Comment on lines +286 to +287
// Seen once shown, so the nav's count agrees with the page that shows it.
if (active?.need) await inbox.markSeen(c.env.DB, user, active);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Render the unread count after marking the active item seen

When /inbox opens an unread active item, page() computes and embeds the unread count before this update runs, so the returned nav and data-unread still include the decision now being shown. In particular, tapping a notification leaves the installed-app badge and nav one too high until another page load; mark the item before computing the rendered count or explicitly adjust that count.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed, fixed in dadfe66. /inbox marks its active decision seen before rendering, and drops it from the page's own "new" count. The nav, data-unread, and the installed-app badge now leave out the decision being shown. The push test checks the counts on that page.

(Written by Claude Opus 5.5)

Comment thread cloudflare/src/app.js Outdated
Comment on lines +51 to +55
registration
.then((found) => found.pushManager.getSubscription())
.then((subscription) => subscription?.unsubscribe())
.catch(() => {})
.finally(() => form.submit());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear the installed-app badge during sign-out

When a user signs out with a nonzero unread badge, this handler unsubscribes and submits without clearing that badge. The redirect lands on a signed-out page where Layout no longer loads /app.js, so the old account's badge remains on the installed app indefinitely; clear it before submitting, independently of whether a push subscription exists.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed, fixed in dadfe66. The sign-out handler now clears the app badge along with unsubscribing, and runs whether or not push is supported or a subscription exists. It submits once both finish. The app.js test covers both steps.

(Written by Claude Opus 5.5)

kixelated and others added 3 commits October 21, 2026 13:22
…ign-out

The inbox marks its active decision seen before the nav and the app badge
count what is new, so opening a notification lowers the badge at once. The
sign-out form also clears the app badge, which signed-out pages never update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	cloudflare/src/inbox/model.ts
Rename the push migration to 0014_push.sql after 0012_project_members.sql and
0013_reconcile.sql. Push recipients are now the project's members. A webhook's
notification goes after its background read, so a title the webhook lacks can
come from GitHub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Owner Author

Merge summary for head 4ad2bf66ae33429e20c983f6ba62df77faca26ed. Codex's latest review is clean (👍, no findings), and CI passes.

Since the first review

Decisions

  • No Web Push library. The RFC 8291 aes128gcm and RFC 8292 VAPID implementation uses WebCrypto and is tested against the RFC 8291 vector. No review finding touched it.
  • VAPID keys are optional config. Notifications stay off until both keys are set.
  • The client checks for VAPID key rotation. There is no per-subscription key column.

Known gaps and follow-ups

  • A sign-out with JavaScript off can't drop the browser subscription. A push already queued for that device can still arrive within its 24h TTL.
  • No test covers a title that comes from the background read instead of the webhook.
  • Real-device install and push on Android, iOS and desktop wait for the deployed Worker, during the operator-setup rehearsal.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit 9a35963 into main Oct 21, 2026
5 checks passed
@kixelated
kixelated deleted the quest/m0/kix/mobile branch October 21, 2026 21:05
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