Fix board members 0-count, bulk email 0-recipients, 502 on cold start, and duplicate statuses
Build and Push Docker Images / build-portal (push) Successful in 1m54s
Details
Build and Push Docker Images / build-infra (docker/db, monacousa-db) (push) Successful in 59s
Details
Build and Push Docker Images / build-infra (docker/kong, monacousa-kong) (push) Successful in 23s
Details
Build and Push Docker Images / build-infra (docker/migrate, monacousa-migrate) (push) Successful in 59s
Details
Build and Push Docker Images / build-portal (push) Successful in 1m54s
Details
Build and Push Docker Images / build-infra (docker/db, monacousa-db) (push) Successful in 59s
Details
Build and Push Docker Images / build-infra (docker/kong, monacousa-kong) (push) Successful in 23s
Details
Build and Push Docker Images / build-infra (docker/migrate, monacousa-migrate) (push) Successful in 59s
Details
- Board members: switch to supabaseAdmin to bypass dues_payments RLS on members_with_dues view - Bulk email: query members_with_dues view with status_name instead of nonexistent status column on members table - 502 error: skip setupCheckHandle DB query for login/signup/public/static routes, increase cache TTL to 5min - Duplicate statuses: add migration 026 to deduplicate Active/active entries, add defensive dedup in admin members loader Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0e04d016da
commit
549ace87c6
|
|
@ -9,7 +9,7 @@ import { supabaseAdmin } from '$lib/server/supabase';
|
||||||
|
|
||||||
// Cache for setup check to avoid hitting DB on every request
|
// Cache for setup check to avoid hitting DB on every request
|
||||||
let setupCheckCache: { needsSetup: boolean; checkedAt: number } | null = null;
|
let setupCheckCache: { needsSetup: boolean; checkedAt: number } | null = null;
|
||||||
const SETUP_CACHE_TTL = 60000; // 1 minute cache
|
const SETUP_CACHE_TTL = 300000; // 5 minute cache
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidate the setup check cache
|
* Invalidate the setup check cache
|
||||||
|
|
@ -110,8 +110,20 @@ const supabaseHandle: Handle = async ({ event, resolve }) => {
|
||||||
const setupCheckHandle: Handle = async ({ event, resolve }) => {
|
const setupCheckHandle: Handle = async ({ event, resolve }) => {
|
||||||
const path = event.url.pathname;
|
const path = event.url.pathname;
|
||||||
|
|
||||||
// Always allow access to setup page and static assets
|
// Always allow access to setup, auth, api, login, signup, public pages, and static assets
|
||||||
if (path === '/setup' || path.startsWith('/api/') || path.startsWith('/auth/')) {
|
if (
|
||||||
|
path === '/setup' ||
|
||||||
|
path.startsWith('/api/') ||
|
||||||
|
path.startsWith('/auth/') ||
|
||||||
|
path === '/login' ||
|
||||||
|
path === '/signup' ||
|
||||||
|
path === '/forgot-password' ||
|
||||||
|
path === '/logout' ||
|
||||||
|
path.startsWith('/public/') ||
|
||||||
|
path.startsWith('/join') ||
|
||||||
|
path.startsWith('/_app/') ||
|
||||||
|
path.includes('.')
|
||||||
|
) {
|
||||||
return resolve(event);
|
return resolve(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,15 @@ export const load: PageServerLoad = async ({ locals }) => {
|
||||||
.limit(20);
|
.limit(20);
|
||||||
|
|
||||||
// Get recipient counts for filter preview
|
// Get recipient counts for filter preview
|
||||||
|
// Use members_with_dues view which has status_name from the joined membership_statuses table
|
||||||
const { data: members } = await supabaseAdmin
|
const { data: members } = await supabaseAdmin
|
||||||
.from('members')
|
.from('members_with_dues')
|
||||||
.select('id, role, status')
|
.select('id, role, status_name, email')
|
||||||
.not('email', 'is', null);
|
.not('email', 'is', null);
|
||||||
|
|
||||||
const recipientCounts = {
|
const recipientCounts = {
|
||||||
all: members?.length || 0,
|
all: members?.length || 0,
|
||||||
active: members?.filter((m: any) => m.status === 'active').length || 0,
|
active: members?.filter((m: any) => m.status_name === 'active').length || 0,
|
||||||
board: members?.filter((m: any) => m.role === 'board' || m.role === 'admin').length || 0,
|
board: members?.filter((m: any) => m.role === 'board' || m.role === 'admin').length || 0,
|
||||||
admin: members?.filter((m: any) => m.role === 'admin').length || 0
|
admin: members?.filter((m: any) => m.role === 'admin').length || 0
|
||||||
};
|
};
|
||||||
|
|
@ -52,14 +53,14 @@ export const actions: Actions = {
|
||||||
return fail(400, { error: 'Email body must be at least 10 characters' });
|
return fail(400, { error: 'Email body must be at least 10 characters' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build recipient query
|
// Build recipient query - use view for status filtering
|
||||||
let query = supabaseAdmin
|
let query = supabaseAdmin
|
||||||
.from('members')
|
.from('members_with_dues')
|
||||||
.select('id, email, first_name, last_name, role, status');
|
.select('id, email, first_name, last_name, role, status_name');
|
||||||
|
|
||||||
switch (target) {
|
switch (target) {
|
||||||
case 'active':
|
case 'active':
|
||||||
query = query.eq('status', 'active');
|
query = query.eq('status_name', 'active');
|
||||||
break;
|
break;
|
||||||
case 'board':
|
case 'board':
|
||||||
query = query.in('role', ['board', 'admin']);
|
query = query.in('role', ['board', 'admin']);
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,20 @@ export const load: PageServerLoad = async ({ locals, url }) => {
|
||||||
filteredMembers = filteredMembers.filter((m: any) => m.status_name === statusFilter);
|
filteredMembers = filteredMembers.filter((m: any) => m.status_name === statusFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load membership statuses for dropdown
|
// Load membership statuses for dropdown (deduplicate by display_name)
|
||||||
const { data: statuses } = await supabaseAdmin
|
const { data: rawStatuses } = await supabaseAdmin
|
||||||
.from('membership_statuses')
|
.from('membership_statuses')
|
||||||
.select('*')
|
.select('*')
|
||||||
.order('sort_order', { ascending: true });
|
.order('sort_order', { ascending: true });
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const statuses = (rawStatuses || []).filter((s: any) => {
|
||||||
|
const key = s.display_name.toLowerCase();
|
||||||
|
if (seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// Calculate stats
|
// Calculate stats
|
||||||
const stats = {
|
const stats = {
|
||||||
total: members?.length || 0,
|
total: members?.length || 0,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
import { supabaseAdmin } from '$lib/server/supabase';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ locals, url }) => {
|
export const load: PageServerLoad = async ({ locals, url }) => {
|
||||||
const searchQuery = url.searchParams.get('search') || '';
|
const searchQuery = url.searchParams.get('search') || '';
|
||||||
|
|
@ -9,8 +10,8 @@ export const load: PageServerLoad = async ({ locals, url }) => {
|
||||||
const { member } = await locals.safeGetSession();
|
const { member } = await locals.safeGetSession();
|
||||||
const isPrivileged = member?.role === 'admin' || member?.role === 'board';
|
const isPrivileged = member?.role === 'admin' || member?.role === 'board';
|
||||||
|
|
||||||
// Build the query - select only needed columns to avoid exposing sensitive fields
|
// Use admin client to bypass RLS on the view (dues_payments RLS blocks the join)
|
||||||
let query = locals.supabase
|
let query = supabaseAdmin
|
||||||
.from('members_with_dues')
|
.from('members_with_dues')
|
||||||
.select('id, member_id, first_name, last_name, email, phone, role, status_name, type_name, avatar_url, created_at, nationality, address, directory_privacy')
|
.select('id, member_id, first_name, last_name, email, phone, role, status_name, type_name, avatar_url, created_at, nationality, address, directory_privacy')
|
||||||
.order('last_name', { ascending: true });
|
.order('last_name', { ascending: true });
|
||||||
|
|
@ -40,7 +41,7 @@ export const load: PageServerLoad = async ({ locals, url }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get membership statuses for filter dropdown
|
// Get membership statuses for filter dropdown
|
||||||
const { data: statuses } = await locals.supabase
|
const { data: statuses } = await supabaseAdmin
|
||||||
.from('membership_statuses')
|
.from('membership_statuses')
|
||||||
.select('*')
|
.select('*')
|
||||||
.order('sort_order', { ascending: true });
|
.order('sort_order', { ascending: true });
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
-- Fix duplicate membership statuses
|
||||||
|
-- The database has both "Active" (capitalized name) and "active" (lowercase) entries.
|
||||||
|
-- The seed data only creates "active" - the "Active" entry was added manually.
|
||||||
|
-- We need to:
|
||||||
|
-- 1. Move any members pointing to the duplicate "Active" status to the original "active" status
|
||||||
|
-- 2. Delete the duplicate "Active" entry
|
||||||
|
|
||||||
|
-- First, update any members that reference the duplicate "Active" (capitalized) status
|
||||||
|
-- to point to the original "active" (lowercase) status
|
||||||
|
UPDATE public.members
|
||||||
|
SET membership_status_id = (
|
||||||
|
SELECT id FROM public.membership_statuses WHERE name = 'active' LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE membership_status_id IN (
|
||||||
|
SELECT id FROM public.membership_statuses WHERE name = 'Active'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Delete the duplicate "Active" (capitalized) status entry
|
||||||
|
DELETE FROM public.membership_statuses WHERE name = 'Active';
|
||||||
|
|
||||||
|
-- Ensure the remaining "active" status has correct sort_order
|
||||||
|
UPDATE public.membership_statuses
|
||||||
|
SET sort_order = 2, display_name = 'Active', color = '#22c55e'
|
||||||
|
WHERE name = 'active';
|
||||||
Loading…
Reference in New Issue