Skip to content

Commit d947b5c

Browse files
authored
Merge pull request #201 from Gizmotronn/SSG-270
⛵︎🫓 ↝ [SSG-270 SSM-251 SSM-252]: A lot of notification content, entit…
2 parents 870568f + 9b0d948 commit d947b5c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+4621
-251
lines changed

.env.example

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
1+
# Supabase Configuration
2+
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
3+
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key_here
4+
SUPABASE_DB_URL=postgresql://postgres:postgres@host.docker.internal:54322/postgres?sslmode=disable
5+
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
6+
17
# Database Configuration (Drizzle ORM)
2-
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/navigation"
8+
DATABASE_URL=postgresql://postgres:postgres@localhost:54322/postgres
39

4-
# For production deployment
5-
POSTGRES_DB=navigation
6-
POSTGRES_USER=postgres
7-
POSTGRES_PASSWORD=your_secure_password_here
10+
# PostHog Analytics
11+
NEXT_PUBLIC_POSTHOG_KEY=your_posthog_key_here
12+
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
13+
14+
# Push Notifications (VAPID Keys)
15+
NEXT_PUBLIC_VAPID_PUBLIC_KEY=your_vapid_public_key_here
16+
VAPID_PRIVATE_KEY=your_vapid_private_key_here
817

918
# Next.js Configuration
1019
NODE_ENV=development
1120
NEXTAUTH_SECRET=your_nextauth_secret_here
1221
NEXTAUTH_URL=http://localhost:3000
1322

14-
# Supabase Configuration (if still using alongside Drizzle)
15-
NEXT_PUBLIC_SUPABASE_URL=
16-
NEXT_PUBLIC_SUPABASE_ANON_KEY=
17-
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
18-
19-
# OpenAI Configuration
20-
OPENAI_API_KEY=
21-
22-
# Push Notifications (Web Push)
23-
VAPID_PUBLIC_KEY=your_vapid_public_key
24-
VAPID_PRIVATE_KEY=your_vapid_private_key
23+
# For production deployment
24+
POSTGRES_DB=navigation
25+
POSTGRES_USER=postgres
26+
POSTGRES_PASSWORD=your_secure_password_here
2527

2628
# API Configuration
2729
NEXT_PUBLIC_API_URL=http://localhost:5001

.env.test

Lines changed: 0 additions & 11 deletions
This file was deleted.

.github/workflows/e2e.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,28 +32,28 @@ jobs:
3232
uses: actions/setup-node@v4
3333
with:
3434
node-version: '18'
35-
cache: 'npm'
35+
cache: 'yarn'
3636

3737
- name: Install dependencies
38-
run: npm ci --legacy-peer-deps
38+
run: yarn install --frozen-lockfile
3939

4040
- name: Setup environment
4141
run: |
4242
echo "SKIP_USER_CREATION_TESTS=true" >> $GITHUB_ENV
4343
echo "NODE_ENV=test" >> $GITHUB_ENV
4444
4545
- name: Build application
46-
run: npm run build
46+
run: yarn build
4747

4848
- name: Start application
4949
run: |
50-
npm start &
50+
yarn start &
5151
sleep 10
5252
env:
5353
NODE_ENV: production
5454

5555
- name: Run E2E tests
56-
run: npm run test:e2e:headless
56+
run: yarn test:e2e:headless
5757
env:
5858
SKIP_USER_CREATION_TESTS: true
5959

.github/workflows/send-push.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Send Push Notifications
2+
3+
on:
4+
schedule:
5+
- cron: "*/15 * * * *" # every 15 minutes UTC
6+
workflow_dispatch:
7+
8+
jobs:
9+
notify:
10+
runs-on: ubuntu-latest
11+
defaults:
12+
run:
13+
working-directory: ./jobs
14+
15+
steps:
16+
- name: Checkout code
17+
uses: actions/checkout@v4
18+
19+
- name: Set up Go
20+
uses: actions/setup-go@v5
21+
with:
22+
go-version: '1.21'
23+
24+
- name: Install dependencies
25+
run: go mod tidy
26+
27+
- name: Run push notification job
28+
env:
29+
SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }}
30+
NEXT_PUBLIC_VAPID_PUBLIC_KEY: ${{ secrets.NEXT_PUBLIC_VAPID_PUBLIC_KEY }}
31+
VAPID_PRIVATE_KEY: ${{ secrets.VAPID_PRIVATE_KEY }}
32+
run: go run send_push_notifications.go

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,7 @@ public/workbox-7144475a.js
5656
public/workbox-7144475a.js.map
5757
public/workbox-f1770938.js
5858
certificates
59-
.env.local
59+
.env.local
60+
61+
# Supabase
62+
supabase/seed.sql

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ up-full:
1010

1111
down:
1212
docker-compose down
13+
docker-compose --profile studio down
14+
docker-compose --profile test down
15+
docker-compose --profile notify down
16+
# supabase stop || true
1317

1418
# Run locally without Docker (recommended for Supabase development)
1519
dev:

add_unlocked_column.sql

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-- Add unlocked column to linked_anomalies table
2+
-- This column tracks whether a satellite anomaly has been unlocked by the user
3+
4+
ALTER TABLE public.linked_anomalies
5+
ADD COLUMN unlocked boolean DEFAULT NULL;
6+
7+
-- Add unlock_time column for tracking when the unlock occurred
8+
-- This ensures compatibility with any cached frontend code
9+
ALTER TABLE public.linked_anomalies
10+
ADD COLUMN unlock_time timestamp with time zone DEFAULT NULL;
11+
12+
-- Add comment for documentation
13+
COMMENT ON COLUMN public.linked_anomalies.unlocked IS 'Tracks whether a satellite anomaly has been unlocked by the user. NULL means not applicable, false means locked, true means unlocked.';
14+
COMMENT ON COLUMN public.linked_anomalies.unlock_time IS 'Timestamp when the satellite anomaly was unlocked by the user.';
15+
16+
-- Create index for better query performance when filtering by unlocked status
17+
CREATE INDEX IF NOT EXISTS idx_linked_anomalies_unlocked
18+
ON public.linked_anomalies (unlocked)
19+
WHERE unlocked IS NOT NULL;
20+
21+
-- Create composite index for common query patterns (author + automaton + unlocked)
22+
CREATE INDEX IF NOT EXISTS idx_linked_anomalies_author_automaton_unlocked
23+
ON public.linked_anomalies (author, automaton, unlocked);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
3+
import { cookies } from 'next/headers';
4+
5+
export async function GET(request: NextRequest) {
6+
try {
7+
// Create Supabase client
8+
const supabase = createRouteHandlerClient({ cookies });
9+
10+
// Get all push subscriptions with creation date
11+
const { data: subscriptions, error } = await supabase
12+
.from('push_subscriptions')
13+
.select('id, profile_id, endpoint, created_at')
14+
.order('created_at', { ascending: false });
15+
16+
if (error) {
17+
console.error('Error fetching subscriptions:', error);
18+
return NextResponse.json({ error: 'Failed to fetch subscriptions' }, { status: 500 });
19+
}
20+
21+
return NextResponse.json({
22+
message: `Found ${subscriptions?.length || 0} subscriptions`,
23+
subscriptions: subscriptions || []
24+
});
25+
26+
} catch (error) {
27+
console.error('Error fetching subscription data:', error);
28+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
29+
}
30+
}

app/api/save-subscription/route.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { NextResponse } from "next/server"
2+
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"
3+
import { cookies } from "next/headers"
4+
5+
export async function POST(req: Request) {
6+
try {
7+
const body = await req.json();
8+
9+
const {
10+
profileId,
11+
subscription
12+
} = body;
13+
14+
const {
15+
endpoint,
16+
keys
17+
} = subscription;
18+
19+
console.log('Received subscription request:', { profileId, endpoint: endpoint?.substring(0, 50) + '...', hasAuth: !!keys?.auth, hasP256dh: !!keys?.p256dh });
20+
21+
if (!profileId || !endpoint || !keys?.auth || !keys?.p256dh) {
22+
console.log('Missing fields:', { profileId: !!profileId, endpoint: !!endpoint, auth: !!keys?.auth, p256dh: !!keys?.p256dh });
23+
return NextResponse.json({
24+
error: "Missing fields",
25+
}, {
26+
status: 400
27+
});
28+
}
29+
30+
const supabase = createRouteHandlerClient({ cookies });
31+
32+
console.log('Inserting push subscription...');
33+
34+
const {
35+
error
36+
} = await supabase
37+
.from("push_subscriptions")
38+
.insert({
39+
profile_id: profileId,
40+
endpoint,
41+
auth: keys.auth,
42+
p256dh: keys.p256dh,
43+
});
44+
45+
if (error) {
46+
console.error('Push subscription upsert error:', error);
47+
return NextResponse.json({
48+
error: error.message,
49+
}, {
50+
status: 500
51+
});
52+
};
53+
54+
console.log('Push subscription saved successfully');
55+
return NextResponse.json({
56+
success: true
57+
});
58+
} catch (error) {
59+
console.error('API Error:', error);
60+
return NextResponse.json({
61+
error: 'Internal server error'
62+
}, {
63+
status: 500
64+
});
65+
}
66+
};
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { createClient } from '@supabase/supabase-js';
3+
import webpush from 'web-push';
4+
5+
// Configure web-push with your VAPID keys
6+
webpush.setVapidDetails(
7+
'mailto:your-email@example.com',
8+
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
9+
process.env.VAPID_PRIVATE_KEY!
10+
);
11+
12+
export async function POST(request: NextRequest) {
13+
try {
14+
console.log('Environment check:');
15+
console.log('SUPABASE_URL:', process.env.NEXT_PUBLIC_SUPABASE_URL);
16+
console.log('SERVICE_ROLE_KEY exists:', !!process.env.SUPABASE_SERVICE_ROLE_KEY);
17+
console.log('VAPID_PUBLIC_KEY exists:', !!process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY);
18+
console.log('VAPID_PRIVATE_KEY exists:', !!process.env.VAPID_PRIVATE_KEY);
19+
20+
// Check if we're in Docker environment
21+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL?.includes('127.0.0.1')
22+
? process.env.NEXT_PUBLIC_SUPABASE_URL?.replace('127.0.0.1', 'host.docker.internal')
23+
: process.env.NEXT_PUBLIC_SUPABASE_URL;
24+
25+
console.log('Using Supabase URL:', supabaseUrl);
26+
27+
if (!supabaseUrl || !process.env.SUPABASE_SERVICE_ROLE_KEY) {
28+
return NextResponse.json({
29+
error: 'Missing required environment variables',
30+
details: {
31+
hasUrl: !!supabaseUrl,
32+
hasServiceKey: !!process.env.SUPABASE_SERVICE_ROLE_KEY
33+
}
34+
}, { status: 500 });
35+
}
36+
37+
// Create Supabase client with service role for admin access
38+
const supabase = createClient(
39+
supabaseUrl,
40+
process.env.SUPABASE_SERVICE_ROLE_KEY!
41+
);
42+
43+
console.log('Attempting to fetch subscriptions...');
44+
45+
// Get all push subscriptions, but deduplicate by endpoint to avoid sending multiple notifications to the same device
46+
const { data: allSubscriptions, error } = await supabase
47+
.from('push_subscriptions')
48+
.select('*')
49+
.order('created_at', { ascending: false });
50+
51+
console.log('Supabase response:', { data: allSubscriptions, error });
52+
53+
if (error) {
54+
console.error('Error fetching subscriptions:', error);
55+
return NextResponse.json({
56+
error: 'Failed to fetch subscriptions',
57+
details: error.message,
58+
code: error.code,
59+
hint: error.hint
60+
}, { status: 500 });
61+
}
62+
63+
if (!allSubscriptions || allSubscriptions.length === 0) {
64+
return NextResponse.json({ message: 'No subscriptions found' }, { status: 200 });
65+
}
66+
67+
// Deduplicate by endpoint - keep only the most recent subscription for each unique endpoint
68+
const uniqueEndpoints = new Map();
69+
allSubscriptions.forEach(sub => {
70+
if (!uniqueEndpoints.has(sub.endpoint)) {
71+
uniqueEndpoints.set(sub.endpoint, sub);
72+
}
73+
});
74+
75+
const subscriptions = Array.from(uniqueEndpoints.values());
76+
console.log(`Deduplicated from ${allSubscriptions.length} to ${subscriptions.length} unique endpoints`);
77+
78+
// Parse request body for custom message
79+
const body = await request.json().catch(() => ({}));
80+
const title = body.title || 'Test Notification';
81+
const message = body.message || 'This is a test push notification!';
82+
const url = body.url || '/';
83+
84+
const payload = JSON.stringify({
85+
title,
86+
body: message,
87+
url,
88+
icon: '/assets/Captn.jpg'
89+
});
90+
91+
console.log(`Sending test notification to ${subscriptions.length} subscribers`);
92+
93+
// Send notifications to all subscribers
94+
const promises = subscriptions.map(async (subscription) => {
95+
try {
96+
const pushSubscription = {
97+
endpoint: subscription.endpoint,
98+
keys: {
99+
auth: subscription.auth,
100+
p256dh: subscription.p256dh
101+
}
102+
};
103+
104+
await webpush.sendNotification(pushSubscription, payload);
105+
console.log(`Sent notification to user ${subscription.profile_id}`);
106+
return { success: true, userId: subscription.profile_id };
107+
} catch (error) {
108+
console.error(`Failed to send notification to user ${subscription.profile_id}:`, error);
109+
return { success: false, userId: subscription.profile_id, error: String(error) };
110+
}
111+
});
112+
113+
const results = await Promise.all(promises);
114+
const successful = results.filter(r => r.success).length;
115+
const failed = results.filter(r => !r.success).length;
116+
117+
return NextResponse.json({
118+
message: `Sent ${successful} notifications successfully, ${failed} failed`,
119+
results
120+
});
121+
122+
} catch (error) {
123+
console.error('Error sending test notifications:', error);
124+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
125+
}
126+
}

0 commit comments

Comments
 (0)