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:
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Dev environment reset script.
|
||||
*
|
||||
* Tears down the local Postgres container and deletes its volume (all local
|
||||
* data is lost), then performs a fresh startup: container up, migrate, seed,
|
||||
* Next.js dev server.
|
||||
*
|
||||
* Use this when you want a completely clean local database — e.g. after a
|
||||
* destructive schema change or to reproduce a fresh-install scenario.
|
||||
*
|
||||
* Usage: pnpm dev:reset
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import { main } from "./dev.mjs";
|
||||
|
||||
console.log("\n⚠ Resetting dev environment — all local data will be deleted.");
|
||||
execSync("docker compose -f docker-compose.dev.yaml down -v", {
|
||||
stdio: "inherit",
|
||||
shell: true,
|
||||
});
|
||||
|
||||
console.log("\n▸ Restarting from scratch...");
|
||||
await main();
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Dev environment startup script.
|
||||
*
|
||||
* Starts the local Postgres container, waits for it to be ready, runs
|
||||
* migrations + seed, then spawns `next dev` bound to 0.0.0.0 so the app
|
||||
* is reachable from other devices on the LAN.
|
||||
*
|
||||
* Usage: pnpm dev:local
|
||||
*/
|
||||
|
||||
import { execSync, spawn } from "child_process";
|
||||
import net from "net";
|
||||
import os from "os";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
/** Return the first LAN IPv4 address (prefers 192.168.x / 10.x ranges). */
|
||||
function getLanIp() {
|
||||
const all = Object.values(os.networkInterfaces())
|
||||
.flat()
|
||||
.filter((n) => n.family === "IPv4" && !n.internal)
|
||||
.map((n) => n.address);
|
||||
return (
|
||||
all.find((a) => a.startsWith("192.168.") || a.startsWith("10.")) ??
|
||||
all[0] ??
|
||||
"unknown"
|
||||
);
|
||||
}
|
||||
|
||||
/** Poll TCP host:port until it accepts a connection or the timeout expires. */
|
||||
function waitForTcp(host, port, timeoutMs = 30_000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
||||
function attempt() {
|
||||
const sock = net.createConnection({ host, port });
|
||||
sock.on("connect", () => {
|
||||
sock.destroy();
|
||||
resolve();
|
||||
});
|
||||
sock.on("error", () => {
|
||||
sock.destroy();
|
||||
if (Date.now() >= deadline) {
|
||||
reject(
|
||||
new Error(
|
||||
`Postgres not ready on ${host}:${port} after ${timeoutMs / 1000}s. ` +
|
||||
"Is Docker running? Check: docker compose -f docker-compose.dev.yaml logs"
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setTimeout(attempt, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
attempt();
|
||||
});
|
||||
}
|
||||
|
||||
/** Run a shell command synchronously, streaming output to the terminal. */
|
||||
function run(cmd) {
|
||||
execSync(cmd, { stdio: "inherit", shell: true });
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
// 1. Start DB container (idempotent — safe to call when already running)
|
||||
console.log("\n▸ Starting database container...");
|
||||
run("docker compose -f docker-compose.dev.yaml up -d");
|
||||
|
||||
// 2. Wait for Postgres to accept connections
|
||||
process.stdout.write("▸ Waiting for Postgres");
|
||||
const tick = setInterval(() => process.stdout.write("."), 500);
|
||||
try {
|
||||
await waitForTcp("127.0.0.1", 5432);
|
||||
} finally {
|
||||
clearInterval(tick);
|
||||
process.stdout.write(" ready\n");
|
||||
}
|
||||
|
||||
// 3. Apply any pending migrations (no-op if already current)
|
||||
console.log("▸ Running migrations...");
|
||||
run("pnpm db:migrate");
|
||||
|
||||
// 4. Seed default data (idempotent)
|
||||
console.log("▸ Seeding...");
|
||||
run("pnpm db:seed");
|
||||
|
||||
// 5. Print access URLs before the Next.js banner appears
|
||||
const lanIp = getLanIp();
|
||||
console.log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
console.log(` Local → http://localhost:3000`);
|
||||
console.log(` LAN → http://${lanIp}:3000 (phone on same WiFi)`);
|
||||
console.log(` HTTPS → https://dev.ginnoir.com (push/PWA — needs Caddy snippet)`);
|
||||
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
|
||||
|
||||
// 6. Spawn Next.js dev server bound to all interfaces.
|
||||
// HMR handles most code changes without a restart.
|
||||
// Restart needed for: .env changes, next.config.ts changes.
|
||||
// Pass command as a single string with no args array — shell handles .cmd
|
||||
// resolution on Windows and the empty args avoids DEP0190.
|
||||
const next = spawn("pnpm run dev:network", [], { stdio: "inherit", shell: true });
|
||||
|
||||
// Forward Ctrl+C / SIGTERM to the child so it shuts down cleanly.
|
||||
const forward = (sig) => () => next.kill(sig);
|
||||
process.on("SIGINT", forward("SIGINT"));
|
||||
process.on("SIGTERM", forward("SIGTERM"));
|
||||
|
||||
await new Promise((resolve) => next.on("exit", (code) => resolve(code)));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Run when invoked directly (not imported by dev-reset.mjs)
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
if (process.argv[1] === __filename) {
|
||||
main().catch((err) => {
|
||||
console.error("\ndev startup failed:", err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Dev script: send a test push notification to all subscriptions for the dev user.
|
||||
* Usage: tsx --env-file=.env scripts/send-test-push.mts
|
||||
*/
|
||||
import webPush from "web-push";
|
||||
import postgres from "postgres";
|
||||
|
||||
const subject = process.env["VAPID_SUBJECT"];
|
||||
const publicKey = process.env["VAPID_PUBLIC_KEY"];
|
||||
const privateKey = process.env["VAPID_PRIVATE_KEY"];
|
||||
const databaseUrl = process.env["DATABASE_URL"];
|
||||
|
||||
if (!subject || !publicKey || !privateKey) {
|
||||
console.error("VAPID env vars not set");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!databaseUrl) {
|
||||
console.error("DATABASE_URL not set");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
webPush.setVapidDetails(subject, publicKey, privateKey);
|
||||
|
||||
const sql = postgres(databaseUrl);
|
||||
|
||||
const subs = await sql`SELECT id, user_id, endpoint, p256dh, auth FROM push_subscriptions`;
|
||||
console.log(`Found ${subs.length} push subscription(s)`);
|
||||
|
||||
if (subs.length === 0) {
|
||||
console.log("No subscriptions — enable push notifications in Settings first.");
|
||||
await sql.end();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
for (const sub of subs) {
|
||||
try {
|
||||
await webPush.sendNotification(
|
||||
{ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
|
||||
JSON.stringify({ title: "famapp test", body: "Push notifications are working!", url: "/settings" }),
|
||||
);
|
||||
console.log(`Sent to ${sub.endpoint.slice(0, 60)}...`);
|
||||
} catch (err: unknown) {
|
||||
const e = err as { statusCode?: number; message?: string };
|
||||
console.error(`Failed (${e.statusCode ?? "?"}): ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await sql.end();
|
||||
@@ -3,4 +3,3 @@ import webPush from "web-push";
|
||||
const { publicKey, privateKey } = webPush.generateVAPIDKeys();
|
||||
console.log(`VAPID_PUBLIC_KEY=${publicKey}`);
|
||||
console.log(`VAPID_PRIVATE_KEY=${privateKey}`);
|
||||
console.log(`NEXT_PUBLIC_VAPID_PUBLIC_KEY=${publicKey}`);
|
||||
|
||||
Reference in New Issue
Block a user