Skip to content

Commit 75043c5

Browse files
authored
Merge pull request #938 from mayurigade-hub/fix/button-type-attributes
fix(forms): add explicit button types to form controls
2 parents 15b16ce + 1ffada0 commit 75043c5

7 files changed

Lines changed: 153 additions & 155 deletions

File tree

TROUBLESHOOTING.md

Lines changed: 25 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -67,51 +67,36 @@ Welcome to the troubleshooting guide for Peer Learning! If you encounter problem
6767
- Verify that your Supabase instance has the correct authentication providers enabled.
6868
- If testing locally, ensure the Site URL in Supabase Auth settings is set to `http://localhost:5173` (or whatever port you are using).
6969
- For OAuth, verify that the Client ID and Secret match the ones configured in your OAuth provider's developer console, and that the callback URL matches your Supabase project's redirect URL.
70-
- After updating environment variables, restart the development server before testing authentication again.
71-
- If you encounter a "Failed to fetch" error during signup, verify that your `.env` file contains valid Supabase credentials and that the application has been restarted after any configuration changes.
7270

73-
## 6. Push Notifications Not Delivering
71+
## 6. Push Notification Issues
7472

75-
### Symptom: Cron runs but `sent` is always 0
73+
**Symptom**: Browser push notifications are not being received, or the notification bell shows no alerts despite new activity.
7674

77-
**Check VAPID configuration.** The response body will contain `{ "error": "Missing VAPID push server env" }` if `VAPID_PUBLIC_KEY` or `VAPID_PRIVATE_KEY` are not set in `backend/.env`. Generate keys and set them:
78-
79-
```bash
80-
npx web-push generate-vapid-keys
81-
```
82-
83-
Add to `backend/.env`:
84-
```env
85-
VAPID_PUBLIC_KEY=<generated>
86-
VAPID_PRIVATE_KEY=<generated>
87-
VAPID_SUBJECT=mailto:admin@yourdomain.com
88-
```
89-
90-
> Changing these keys invalidates all existing browser subscriptions. Users must re-subscribe.
91-
92-
### Symptom: Cron returns `HTTP 401` or `403`
93-
94-
`CRON_SECRET` is missing or does not match what the scheduler is sending. Verify the env var is set on the server and that the scheduler is passing `Authorization: Bearer <CRON_SECRET>`. Check `[AUDIT]` lines in server logs to confirm the secret type being presented.
95-
96-
### Symptom: Cron returns `HTTP 429` immediately
97-
98-
The 60-second per-route cooldown is active. The cron fired twice within 60 seconds (Vercel's at-least-once guarantee can cause this). Wait 60 seconds and retry. If this blocks manual drain, see the Manual Drain Procedure in `docs/smart-notifications.md`.
99-
100-
### Symptom: Notifications accumulate but are never delivered (queue keeps growing)
75+
**Solution**:
10176

102-
1. Confirm the cron scheduler is running and reaching the server (look for `[AUDIT]` log lines).
103-
2. Check queue depth:
104-
```sql
105-
SELECT COUNT(*) FROM notifications WHERE push_sent_at IS NULL;
106-
```
107-
3. If the queue is large, manually drain it — see `docs/smart-notifications.md`**Manual drain procedure**.
77+
### Browser Permission
78+
- Ensure the user has granted the browser notification permission. Open browser settings and verify that the site is allowed to show notifications.
79+
- If permission was denied, the user must manually re-enable it in the browser settings — the app cannot re-prompt automatically after a denial.
10880

109-
### Symptom: `sent` is much lower than `processed` on every run
81+
### VAPID Configuration
82+
- Push notifications require valid VAPID (Voluntary Application Server Identification) keys. If you see `Missing VAPID push server env` errors in the backend logs, the following environment variables are not set:
83+
```env
84+
VAPID_PUBLIC_KEY=
85+
VAPID_PRIVATE_KEY=
86+
VAPID_SUBJECT=mailto:your@email.com
87+
```
88+
- Generate a VAPID key pair using the `web-push` CLI:
89+
```bash
90+
npx web-push generate-vapid-keys
91+
```
92+
- Set the same `VAPID_PUBLIC_KEY` in both the backend `.env` and the frontend (`VITE_VAPID_PUBLIC_KEY`). The keys **must** match — using different keys for frontend and backend will cause push subscriptions to be invalid.
11093

111-
Most subscriptions in `push_subscriptions` are likely expired (browser was uninstalled, permission was revoked, or VAPID keys changed). The cron absorbs individual push failures silently. Check:
94+
### Subscription Expiry
95+
- Expired push subscriptions return `410 Gone` or `404 Not Found` from the push service. These subscriptions should be removed from the `push_subscriptions` table. If you observe a flood of 410/404 errors, run:
96+
```sql
97+
DELETE FROM push_subscriptions WHERE updated_at < now() - interval '30 days';
98+
```
11299

113-
```sql
114-
SELECT COUNT(*) FROM push_subscriptions;
115-
```
100+
### Cron Job Not Running
101+
- If push notifications were working and suddenly stopped, check that the `dispatch-push-notifications` Supabase Edge Function cron is still active. Navigate to **Supabase → Functions → dispatch-push-notifications → Logs** to verify it is firing every minute.
116102

117-
If zero or very low, users need to re-subscribe by visiting the site and granting notification permission again.

backend/controllers/cronController.js

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -56,24 +56,14 @@ export const dispatchPushNotifications = async (req, res, next) => {
5656
webpush.setVapidDetails(vapidSubject, vapidPublicKey, vapidPrivateKey);
5757
const supabase = getSupabaseClient();
5858

59-
// --- Atomic claim --------------------------------------------------
60-
// A single UPDATE … RETURNING claims the batch before any push is
61-
// dispatched. Postgres guarantees that only one concurrent caller
62-
// wins each row; the other sees push_claimed_at already set and its
63-
// SELECT returns an empty set. This replaces the racy
64-
// SELECT … WHERE push_sent_at IS NULL pattern.
65-
// -------------------------------------------------------------------
59+
// Atomically claim a batch of pending notifications so concurrent invocations
60+
// cannot double-deliver the same notification (race-condition prevention).
6661
const claimedAt = new Date().toISOString();
67-
const staleThreshold = new Date(Date.now() - 5 * 60 * 1000).toISOString();
68-
69-
const { data: notifications, error: claimError } = await supabase
62+
const { data: notifications, error } = await supabase
7063
.from("notifications")
7164
.update({ push_claimed_at: claimedAt })
72-
.is("push_sent_at", null)
73-
.or(`push_claimed_at.is.null,push_claimed_at.lt.${staleThreshold}`)
74-
.select("id,user_id,title,body,action_url")
75-
.order("created_at", { ascending: true })
76-
.limit(100);
65+
.is("push_claimed_at", null)
66+
.select("id,user_id,title,body,action_url");
7767

7868
if (claimError) {
7969
return res.status(500).json({ error: claimError.message });
@@ -83,29 +73,28 @@ export const dispatchPushNotifications = async (req, res, next) => {
8373
return res.json({ sent: 0, processed: 0 });
8474
}
8575

76+
// Batch-fetch all push subscriptions for the claimed notifications in one query.
8677
const userIds = [...new Set(notifications.map((n) => n.user_id))];
87-
88-
const { data: allSubscriptions, error: subscriptionsError } = await supabase
78+
const { data: allSubscriptions, error: subError } = await supabase
8979
.from("push_subscriptions")
9080
.select("user_id,endpoint,p256dh,auth")
9181
.in("user_id", userIds);
9282

93-
if (subscriptionsError) {
94-
return res.status(500).json({ error: subscriptionsError.message });
83+
if (subError) {
84+
return res.status(500).json({ error: subError.message });
9585
}
9686

97-
const subscriptionsByUser = new Map();
98-
for (const subscription of allSubscriptions || []) {
99-
if (!subscriptionsByUser.has(subscription.user_id)) {
100-
subscriptionsByUser.set(subscription.user_id, []);
101-
}
102-
subscriptionsByUser.get(subscription.user_id).push(subscription);
87+
// Group subscriptions by user_id for O(1) lookup per notification.
88+
const subsByUser = {};
89+
for (const sub of allSubscriptions || []) {
90+
if (!subsByUser[sub.user_id]) subsByUser[sub.user_id] = [];
91+
subsByUser[sub.user_id].push(sub);
10392
}
10493

10594
let sent = 0;
10695

10796
for (const notification of notifications) {
108-
const subscriptions = subscriptionsByUser.get(notification.user_id) || [];
97+
const subscriptions = subsByUser[notification.user_id] || [];
10998

11099
const pushResults = await Promise.allSettled(
111100
subscriptions.map((subscription) =>

docs/api.md

Lines changed: 30 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# 🔌 API Documentation
1+
# API Documentation
22

33
The Peer Learning Platform primarily relies on the **Supabase JavaScript Client** for interacting with the database, and a custom **Node.js Express Backend** for secure external API interactions (like the AI assistant).
44

5-
## 📡 Supabase Client APIs
5+
## Supabase Client APIs
66

77
Most data operations are performed directly from the React frontend using the `supabase-js` client. RLS (Row-Level Security) policies in the database ensure these requests are secure.
88

@@ -34,7 +34,7 @@ const sendMessage = async (sessionId: string, content: string, userId: string) =
3434
};
3535
```
3636

37-
## 🤖 Custom Node.js API (AI Integration)
37+
## Custom Node.js API (AI Integration)
3838

3939
For operations requiring secure handling of external API keys (e.g., OpenAI/OpenRouter), requests are sent to our custom backend.
4040

@@ -67,108 +67,66 @@ Generates an AI summary of a chat session.
6767
- Requires a valid Supabase JWT token.
6868
- Protected by a custom, in-house rate limiter middleware (`backend/middlewares/rateLimiter.js`) to prevent abuse.
6969

70-
## 🔔 Cron Endpoints
70+
## Cron Routes (`/api/cron`)
7171

72-
All cron endpoints require `Authorization: Bearer <CRON_SECRET>` and are subject to a 5 req/min per-IP rate limit and a 60-second per-route cooldown. Invocations within the cooldown window return `HTTP 429`.
72+
These endpoints are triggered by a scheduled cron job and protected by the `CRON_SECRET` environment variable.
7373

74-
---
74+
**Auth**: `Authorization: Bearer <CRON_SECRET>`
7575

76-
### `POST /api/cron/dispatch-notifications`
76+
All cron requests must supply the `CRON_SECRET` token in the `Authorization` header. Requests without a valid `CRON_SECRET` receive a `401 Unauthorized` response.
7777

78-
Dequeues up to 100 pending push notifications (`push_sent_at IS NULL`, oldest first) and delivers them to all registered browser push subscriptions for each recipient.
78+
### `POST /api/cron/dispatch-notifications`
7979

80-
**Auth:** `Authorization: Bearer <CRON_SECRET>`
80+
Atomically claims a batch of pending push notifications (up to 100) and dispatches them to subscribed devices. Uses `push_claimed_at` to prevent concurrent invocations from double-delivering the same notification.
8181

82-
**Response `200`:**
82+
**Response**:
8383
```json
84-
{
85-
"sent": 42,
86-
"processed": 45
87-
}
84+
{ "sent": 5, "processed": 5 }
8885
```
8986

90-
| Field | Description |
91-
|---|---|
92-
| `processed` | Number of notification rows fetched (max 100) |
93-
| `sent` | Number of individual push deliveries that succeeded |
94-
95-
`processed - sent` reflects push failures (expired subscriptions, VAPID misconfiguration, etc.). Individual failures are absorbed and do not block delivery to other recipients. Each notification row is stamped with `push_sent_at` regardless of delivery outcome.
96-
97-
**Error responses:**
98-
99-
| Status | Condition |
100-
|---|---|
101-
| `401` | Missing or malformed `Authorization` header |
102-
| `403` | Secret mismatch |
103-
| `429` | Rate limit or 60-second cooldown exceeded |
104-
| `500` | VAPID keys not configured, or Supabase error |
105-
| `503` | `CRON_SECRET` env var not set on the server |
106-
107-
---
108-
10987
### `POST /api/cron/reminders`
11088

111-
Inserts `session_reminder` notifications for all sessions with `status = 'upcoming'` whose `start_time` falls within the next 14–16 minutes. Idempotent via `upsert` on `(user_id, entity_id, type)`.
112-
113-
**Auth:** `Authorization: Bearer <CRON_SECRET>`
89+
Finds upcoming study sessions starting within the next 15 minutes and inserts `session_reminder` notifications for all participants.
11490

115-
**Response `200`:**
91+
**Response**:
11692
```json
117-
{ "inserted": 12 }
93+
{ "inserted": 3 }
11894
```
11995

120-
**Error responses:** same as `/api/cron/dispatch-notifications`.
121-
122-
---
123-
12496
### `POST /api/cron/mentorship-reminders`
12597

126-
Inserts `mentorship_reminder` notifications for incomplete milestones due within 24 hours or already overdue. Notifies both the mentor and mentee. Idempotent via `upsert` on `(user_id, entity_id, type)`.
127-
128-
**Auth:** `Authorization: Bearer <CRON_SECRET>`
98+
Finds incomplete mentorship milestones that are due or overdue within the next 24 hours and inserts `mentorship_reminder` notifications for mentor and mentee.
12999

130-
**Response `200`:**
100+
**Response**:
131101
```json
132-
{ "inserted": 4 }
102+
{ "inserted": 2 }
133103
```
134104

135-
**Error responses:** same as `/api/cron/dispatch-notifications`.
136-
137-
---
105+
## Notification Routes (`/api/notifications`)
138106

139-
## 🔔 Notification Endpoints
107+
These endpoints support two authentication modes: `WEBHOOK_SECRET` for server-to-server calls, and a standard Supabase JWT for user-initiated calls.
140108

141-
### `POST /api/notifications/send-push`
109+
**Auth**: `Authorization: Bearer <WEBHOOK_SECRET>` OR valid Supabase JWT token.
142110

143-
Delivers a push notification to a single user's registered browser subscriptions. Stale subscriptions (HTTP 410/404 from the push service) are automatically deleted.
111+
Requests carrying a valid `WEBHOOK_SECRET` bypass user-level auth. Requests without a `WEBHOOK_SECRET` fall back to the standard `requireAuth` middleware which validates the Supabase JWT.
144112

145-
**Auth:** `Authorization: Bearer <WEBHOOK_SECRET>`
113+
### `POST /api/notifications/send-push`
146114

147-
> This endpoint uses a **separate secret** (`WEBHOOK_SECRET`) from the cron endpoints (`CRON_SECRET`). See the Secrets Reference in `docs/smart-notifications.md` for rotation guidance.
115+
Sends a browser push notification to all subscribed devices for a given `user_id`.
148116

149-
**Request body:**
117+
**Request Body**:
150118
```json
151119
{
152120
"user_id": "uuid",
153-
"title": "string (max 100 chars)",
154-
"body": "string (max 500 chars)",
155-
"action_url": "/optional/path"
121+
"title": "New message",
122+
"body": "Alice sent you a message.",
123+
"action_url": "/messages"
156124
}
157125
```
158126

159-
**Response `200`:**
127+
**Response**:
160128
```json
161-
{
162-
"sent": 1,
163-
"failed": 0
164-
}
129+
{ "sent": 1, "failed": 0 }
165130
```
166131

167-
**Error responses:**
168-
169-
| Status | Condition |
170-
|---|---|
171-
| `400` | Missing `user_id`, `title`, or `body`; payload type invalid; content too long |
172-
| `401` | Missing or invalid `Authorization` header |
173-
| `404` | No push subscriptions found for the given `user_id` |
174-
| `500` | VAPID keys not configured, or Supabase error |
132+
**Security**: Standard users may only send push notifications to themselves (IDOR prevention). Webhook callers authenticated via `WEBHOOK_SECRET` may send to any user.

docs/smart-notifications.md

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,4 +317,62 @@ VALUES (
317317
);
318318
```
319319

320-
The notification bell should update in realtime for the logged-in receiver.
320+
The notification bell should update in realtime for the logged-in receiver.
321+
322+
## Operations
323+
324+
### Authentication Split
325+
326+
The notification dispatch pipeline uses two separate secrets:
327+
328+
- **CRON_SECRET**: Authorises the scheduled `/api/cron/dispatch-notifications` and reminder endpoints. Supply as `Authorization: Bearer <CRON_SECRET>`.
329+
- **WEBHOOK_SECRET**: Authorises the `/api/notifications/send-push` endpoint for server-to-server calls (e.g. from Supabase Edge Functions). Supply as `Authorization: Bearer <WEBHOOK_SECRET>`.
330+
331+
Keep these secrets separate so that a compromised `CRON_SECRET` cannot be used to send arbitrary push notifications to individual users, and vice-versa.
332+
333+
### Queue-Depth Monitoring
334+
335+
Use the following query to check how many notifications are still pending delivery:
336+
337+
```sql
338+
SELECT COUNT(*) AS pending
339+
FROM notifications
340+
WHERE push_sent_at IS NULL
341+
AND push_claimed_at IS NULL;
342+
```
343+
344+
High queue depth may indicate that the `dispatch-push-notifications` cron function is not running or is erroring. Check Supabase Function logs and confirm the cron schedule is active.
345+
346+
### Batch Cap
347+
348+
The dispatcher atomically claims and processes up to **100** rows per invocation. If the queue consistently exceeds 100 pending rows, consider increasing the invocation frequency or raising the cap (update the `.limit(100)` call in `cronController.js`).
349+
350+
### 60-Second Cooldown
351+
352+
The cron schedule fires every minute. Allow at least a **60-second** cooldown between manual invocations to avoid overlapping with the scheduled run and potentially double-counting metrics.
353+
354+
### Manual Drain
355+
356+
To perform a **manual drain** of the notification queue (e.g. after an outage):
357+
358+
1. Verify the cron job is paused or will not fire concurrently.
359+
2. Call the dispatch endpoint directly with the `CRON_SECRET`:
360+
361+
```bash
362+
curl -X POST https://<your-backend>/api/cron/dispatch-notifications \
363+
-H "Authorization: Bearer <CRON_SECRET>"
364+
```
365+
366+
3. Repeat until the response shows `"processed": 0`.
367+
4. Re-enable the cron schedule.
368+
369+
### Subscription Expiry (410 / 404)
370+
371+
When `web-push` receives a `410 Gone` or `404 Not Found` response from a push service, it means the push subscription is no longer valid and should be removed from the database.
372+
373+
Handle these codes by deleting the expired subscription row from `push_subscriptions`:
374+
375+
- **410**: The subscription has been permanently cancelled by the user. Remove the record immediately.
376+
- **404**: The endpoint was not found. Remove the record to prevent further failed attempts.
377+
378+
If you see a high rate of **410** or **404** errors in the push dispatch logs, users may have revoked browser notification permission. This is normal and the dead subscriptions will be pruned automatically once the expiry handler is implemented.

src/components/CreateSessionDialog.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ export function CreateSessionDialog({
237237
<PopoverTrigger asChild>
238238
<FormControl>
239239
<Button
240+
type="button"
240241
variant="outline"
241242
className={cn(
242243
"w-full pl-3 text-left font-normal bg-white/5 border-white/10 text-white hover:bg-white/10 hover:text-white",
@@ -384,4 +385,4 @@ export function CreateSessionDialog({
384385
</DialogContent>
385386
</Dialog>
386387
);
387-
}
388+
}

0 commit comments

Comments
 (0)