feat: add dev startup script, fix push notifications, and scope test sends to device

- pnpm dev:local/dev:reset: orchestrate DB container, migrations, seed, and Next.js dev server in one command; Caddy snippet + docs for HTTPS via dev.ginnoir.com
- Fix dev login on HTTPS: set both authjs.session-token and __Secure-authjs.session-token so Auth.js finds the session regardless of cookie name resolution
- Suppress hydration mismatch on <html> caused by pre-paint script changing data-nav before React hydrates
- VAPID startup warning if keys not configured; remove dead NEXT_PUBLIC_VAPID_PUBLIC_KEY var
- PushOptIn: hydrate subscription state on mount; reuse existing subscription on iOS to avoid redundant prompts
- sendPushToEndpoint: new function to send to a single device endpoint
- sendTestNotification: scoped to the calling device's endpoint (ownership-verified) instead of all user subscriptions
This commit is contained in:
ginnoir
2026-06-01 17:45:20 -05:00
parent 9612a54e52
commit 19308be768
14 changed files with 334 additions and 26 deletions
+26 -6
View File
@@ -1,6 +1,6 @@
"use client";
import { useState, useTransition } from "react";
import { useState, useTransition, useEffect } from "react";
import { Button } from "@/components/ui/button";
import {
subscribeToPush,
@@ -23,6 +23,20 @@ export function PushOptIn({ vapidKey }: { vapidKey: string }) {
const [isPending, startTransition] = useTransition();
const [testSent, setTestSent] = useState(false);
// Hydrate subscription state from the browser on mount so the UI reflects
// reality even when the user navigates away and returns to Settings.
useEffect(() => {
if (!vapidKey || !("serviceWorker" in navigator) || !("PushManager" in window)) return;
navigator.serviceWorker.ready.then((reg) =>
reg.pushManager.getSubscription().then((existing) => {
if (existing) {
setEndpoint(existing.endpoint);
setStatus("subscribed");
}
}),
);
}, [vapidKey]);
if (!vapidKey) return null;
if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
return (
@@ -35,10 +49,15 @@ export function PushOptIn({ vapidKey }: { vapidKey: string }) {
async function subscribe() {
try {
const registration = await navigator.serviceWorker.ready;
const sub = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidKey),
});
// Reuse an existing browser subscription rather than calling subscribe()
// again — avoids redundant prompts and inconsistent behaviour on iOS.
const existing = await registration.pushManager.getSubscription();
const sub =
existing ??
(await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidKey),
}));
const json = sub.toJSON() as { endpoint: string; keys: { p256dh: string; auth: string } };
startTransition(async () => {
await subscribeToPush(json, navigator.userAgent);
@@ -63,8 +82,9 @@ export function PushOptIn({ vapidKey }: { vapidKey: string }) {
}
function sendTest() {
if (!endpoint) return;
startTransition(async () => {
await sendTestNotification();
await sendTestNotification(endpoint);
setTestSent(true);
setTimeout(() => setTestSent(false), 3000);
});