bug fixes

This commit is contained in:
2026-03-29 12:48:31 -04:00
parent 580ef1307c
commit 54b412c1c1
6 changed files with 135 additions and 90 deletions

48
.env.fcmtest-push Normal file
View File

@@ -0,0 +1,48 @@
#** Required
DB_PASSWORD=C@nuck2024
JWT_SECRET=changemesupersecretjwtkey
#** App identity
PROJECT_NAME=rosterchirp
APP_NAME=RosterChirp
DEFCHAT_NAME=General Chat
ADMIN_NAME=Admin User
ADMIN_EMAIL=admin@rosterchirp.local
ADMIN_PASS=Admin@1234
ADMPW_RESET=false
#** Database
# DB names intentionally kept as 'rosterchirp' — matches the existing live database
DB_NAME=rosterchirp
DB_USER=rosterchirp
# DB_HOST and DB_PORT are set automatically in docker-compose (host=db, port=5432)
#** Tenancy mode
# selfhost = single tenant (RosterChirp-Chat / RosterChirp-Brand / RosterChirp-Team)
# host = multi-tenant (RosterChirp-Host only)
APP_TYPE=host
#** RosterChirp-Host only (ignored in selfhost mode)
HOST_DOMAIN=rosterchirphost.stretchy.ca
HOST_ADMIN_KEY=VBGFHEANTTGRDDWAASJKH
#** Optional
PORT=3144
TZ=America/Toronto
#** Firebase Cloud Messaging (FCM) — Android background push
# Web app config — from Firebase Console → Project Settings → General → Your apps
FIREBASE_API_KEY=AIzaSyAw1v4COZ68Po8CuwVKrQq0ygf7zFd2QCA
FIREBASE_PROJECT_ID=fcmtest-push
FIREBASE_MESSAGING_SENDER_ID=439263996034
FIREBASE_APP_ID=1:439263996034:web:62a6a6b0afdbad99fdec9b
# VAPID key — from Firebase Console → Project Settings → Cloud Messaging → Web Push certificates
FIREBASE_VAPID_KEY=BE6hPKkbf-h0lUQ1tYo249pBOdZFFcWQn9suwg3NDwSE8C_hv8hk1dUY9zxHBQEChO_IAqyFZplF_SUb5c4Ofrw
# Service account — from Firebase Console → Project Settings → Service accounts → Generate new private key
FIREBASE_SERVICE_ACCOUNT={"type": "service_account", "project_id": "fcmtest-push", "private_key_id": "ddbf38d0c5f769b9b8b95000bf05c42b52bb58ad", "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDR4jqMb/0UPVpU\nctpVl9UWHY5lePR4hMEoodbRPofNQgtvx5HuFE61cVrquD8mfUmFB9eZc112KPyy\nMuuZHFkJrVT3iEhK8AoeJTNbxh+YiQvwMhyn9/KO4ntr/HIZxqHs62M46rqehZFS\nFR79zG1ptl/hRFkTTwQoQOOAdqP6gJuX+XpKpVeLPNCBVOAhfM8APA4dvjXUqhrk\ns+L5sH8xdttY/XFdjNhiUtv7uHuvww1hBKliUL6dDZZAlm3uwuYAdsvIHkDWOJ+B\nn8EE7n01h5PUpOihQ2poBAFGHvrT9ifk3bzuGviE74ejErCbBEBwJZvvsaebvaG4\n5dI3SQLXAgMBAAECggEAELX0d24LNmtUH9ktLRdzrdkYl1e0D0xynKuWEP7rjRov\nEu1O3yfaxHOMC5gz3vqmueLP9bXLwTauN/n57Cznoe+dDkBZkS3fgFrx5eK2bUys\nGKnEwlLpixrZPNXSt96q0dRECCoYRbrYwTJRT1/RblNI+wSYGwN1j0brVjUcBTvH\nPjpnt9bkIS+Rb1XJg1+TfQFzt1/WvFscpDpc7zUCGczgD7hAXJU2v2NYZyNtjn2g\niFD4r0AODuFk1Z6C8fbUsgcl8AXXQnJSLPTUXnyzifzBVQmGBu3HewLDHI99pTCZ\nT8aOwgaWYUWrjeg0jfyid08j14OfhE58/PuYGwcNsQKBgQDoh1R/OQ147D75BpXP\nEI1TyKTJNZiwnzRnP64cmzAwbIfc6w06hXTJGKIVqMBwM31R8WUfJ5cBxQOb1g3a\nZDgOTz4zO9M0mFyE/L0V3XrTXzCgPN+pCbZjcAw8oizF3u2rupM5lppxuXnkDiSi\nA2GuVdPR6M8pXUmNubs5XNe1jQKBgQDnEb21yF1K2IF9p1T0jcqLkzDLZDHHudVr\ndZSVVEyhpIFmBFgF8EC3C/ehA5i8Ar1K5JvcP5hpAH345QWOUUAKk000A4WpazZx\nfZM5Ema1xju1AFOSMDzwjt4N32Dg1VPqiLDT+CjiQFH67lSretO1IpS//IKT1Y+W\nOv3/ENfm8wKBgHt7moixUJE9zDdMovPCU3sB21iq6Loq4ZZO//RbCV091WyhOnYw\ndxNvzGt6IS+0eEGy0sOXr56V9FOmedbXT9lxhZOJmqCcpM1Otk9NPbPQIi+GBDRt\nXvkxgJ4WdXZi6449139Glh/8ollUlWmgKBh/pawcWR8bVjs4Pc+5mSflAoGAYfMm\nTRmrWl/evGojXCuC8ZmqdH17kKOY8Z19J7P9bAP1Ck7LFXFbrXx4Mxv4MbKjlUzF\nOR8IN3KK8+f5a/PLRvBcKLFZhpC5GnDV6LqBKYrnonmJ841ZN8wIGy9WvNgRY3kg\nJCqtAgOr/MfswmgluEH5dkzO+WXtIQzOwMHeE7sCgYAhrJV+PvnbnaIpfTaOKXGl\nsvzSXTuey5fQQLKMKsp2haKDVZ2hadDejRHsLJKVGb+KwdJ1s5WmBJ4L80/MnOKZ\n+9Yby9DKviVx0TbvMUGuAuWMl9syo4ICMVpp0cbeSOCM5/ulYjKSeU3sFKo7aWa9\nU8Pskm36I88orq90OBpWOg==\n-----END PRIVATE KEY-----\n","client_email": "firebase-adminsdk-fbsvc@fcmtest-push.iam.gserviceaccount.com", "client_id": "103917424542871804597", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token","auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40fcmtest-push.iam.gserviceaccount.com", "universe_domain": "googleapis.com" }
RC_VERSION=latest
VAPID_SUBJECT=mailto:devpush@rosterchirp.com
VAPID_PUBLIC=BGYS6vMY7zlx31UKRN9QcaOwfomoDJ50_MtfTcfE84q5bhTLq0rM1zSa6uzBTRBxZuFW1kMQP7ardN_jog3T14Y
VAPID_PRIVATE=8SnDSEy_gs2jNwXLtOchZfHW0ppy_RG8wtvjSjYGA48

View File

@@ -199,56 +199,14 @@ router.get('/', authMiddleware, async (req, res) => {
if (to) { sql += ` AND start_at <= $${pi++}`; params.push(to); }
sql += ' ORDER BY start_at ASC';
const rawEvents = await query(req.schema, sql, params);
// Separate master events and exception instances
const masterEvents = [];
const exceptionInstances = [];
for (const e of rawEvents) {
if (e.recurring_master_id) {
exceptionInstances.push(e);
} else {
masterEvents.push(e);
}
}
// Build a map of exception dates by master event
const exceptionDatesByMaster = new Map();
for (const exc of exceptionInstances) {
if (!exceptionDatesByMaster.has(exc.recurring_master_id)) {
exceptionDatesByMaster.set(exc.recurring_master_id, new Set());
}
const dateStr = new Date(exc.original_start_at || exc.start_at).toISOString().split('T')[0];
exceptionDatesByMaster.get(exc.recurring_master_id).add(dateStr);
}
const events = [];
for (const e of masterEvents) {
// Skip master events if all their occurrences in range are replaced by exceptions
if (e.recurrence_rule && exceptionDatesByMaster.has(e.id)) {
const exceptions = exceptionDatesByMaster.get(e.id);
const rule = e.recurrence_rule;
// Check if this is a simple case where all occurrences are replaced
// For now, we'll include the master and let frontend handle filtering
// This is safer to avoid missing edge cases
}
for (const e of rawEvents) {
if (!(await canViewEvent(req.schema, e, req.user.id, itm))) continue;
await enrichEvent(req.schema, e);
const mine = await queryOne(req.schema, 'SELECT response FROM event_availability WHERE event_id=$1 AND user_id=$2', [e.id]);
e.my_response = mine?.response || null;
events.push(e);
}
// Add exception instances (they're standalone events)
for (const e of exceptionInstances) {
if (!(await canViewEvent(req.schema, e, req.user.id, itm))) continue;
await enrichEvent(req.schema, e);
const mine = await queryOne(req.schema, 'SELECT response FROM event_availability WHERE event_id=$1 AND user_id=$2', [e.id]);
e.my_response = mine?.response || null;
events.push(e);
}
res.json({ events });
} catch (e) { res.status(500).json({ error: e.message }); }
});

View File

@@ -0,0 +1,66 @@
services:
rosterchirp:
image: rosterchirp-dev:${RC_VERSION:-latest}
container_name: ${PROJECT_NAME:-rosterchirp}
restart: unless-stopped
ports:
- "${PORT:-3000}:3000"
environment:
- NODE_ENV=production
- TZ=${TZ:-UTC}
- APP_TYPE=${APP_TYPE:-selfhost}
- ADMIN_NAME=${ADMIN_NAME:-Admin User}
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@rosterchirp.local}
- ADMIN_PASS=${ADMIN_PASS:-Admin@1234}
- ADMPW_RESET=${ADMPW_RESET:-false}
- JWT_SECRET=${JWT_SECRET:-changeme_super_secret_jwt_key_2024}
- APP_NAME=${APP_NAME:-rosterchirp}
- DEFCHAT_NAME=${DEFCHAT_NAME:-General Chat}
- DB_HOST=db
- DB_PORT=5432
- DB_NAME=${DB_NAME:-rosterchirp}
- DB_USER=${DB_USER:-rosterchirp}
- DB_PASSWORD=${DB_PASSWORD:?DB_PASSWORD is required}
- HOST_DOMAIN=${HOST_DOMAIN:-}
- HOST_ADMIN_KEY=${HOST_ADMIN_KEY:-}
- FIREBASE_API_KEY=${FIREBASE_API_KEY:-}
- FIREBASE_PROJECT_ID=${FIREBASE_PROJECT_ID:-}
- FIREBASE_MESSAGING_SENDER_ID=${FIREBASE_MESSAGING_SENDER_ID:-}
- FIREBASE_APP_ID=${FIREBASE_APP_ID:-}
- FIREBASE_VAPID_KEY=${FIREBASE_VAPID_KEY:-}
- FIREBASE_SERVICE_ACCOUNT=${FIREBASE_SERVICE_ACCOUNT}
- VAPID_SUBJECT=${VAPID_SUBJECT:-mailto:push@rosterchirp.com}
- VAPID_PUBLIC=${VAPID_PUBLIC:-CHANGEME}
- VAPID_PRIVATE=${VAPID_PRIVATE:-CHANGEME}
volumes:
- rosterchirp_uploads:/app/uploads
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:16-alpine
container_name: ${PROJECT_NAME:-rosterchirp}_db
restart: unless-stopped
environment:
- POSTGRES_DB=${DB_NAME:-rosterchirp}
- POSTGRES_USER=${DB_USER:-rosterchirp}
- POSTGRES_PASSWORD=${DB_PASSWORD:?DB_PASSWORD is required}
volumes:
- rosterchirp_db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-rosterchirp} -d ${DB_NAME:-rosterchirp}"]
interval: 5s
timeout: 5s
retries: 10
volumes:
rosterchirp_db:
driver: local
rosterchirp_uploads:
driver: local

View File

@@ -1,6 +1,6 @@
services:
rosterchirp:
image: rosterchirp:${ROSTERCHIRP_VERSION:-latest}
image: rosterchirp:${RC_VERSION:-latest}
container_name: ${PROJECT_NAME:-rosterchirp}
restart: unless-stopped
ports:
@@ -29,6 +29,9 @@ services:
- FIREBASE_APP_ID=${FIREBASE_APP_ID:-}
- FIREBASE_VAPID_KEY=${FIREBASE_VAPID_KEY:-}
- FIREBASE_SERVICE_ACCOUNT=${FIREBASE_SERVICE_ACCOUNT}
- VAPID_SUBJECT=${VAPID_SUBJECT:-mailto:push@rosterchirp.com}
- VAPID_PUBLIC=${VAPID_PUBLIC:-CHANGEME}
- VAPID_PRIVATE=${VAPID_PRIVATE:-CHANGEME}
volumes:
- rosterchirp_uploads:/app/uploads
depends_on:

View File

@@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "fcmtest-push",
"private_key_id": "ddbf38d0c5f769b9b8b95000bf05c42b52bb58ad",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDR4jqMb/0UPVpU\nctpVl9UWHY5lePR4hMEoodbRPofNQgtvx5HuFE61cVrquD8mfUmFB9eZc112KPyy\nMuuZHFkJrVT3iEhK8AoeJTNbxh+YiQvwMhyn9/KO4ntr/HIZxqHs62M46rqehZFS\nFR79zG1ptl/hRFkTTwQoQOOAdqP6gJuX+XpKpVeLPNCBVOAhfM8APA4dvjXUqhrk\ns+L5sH8xdttY/XFdjNhiUtv7uHuvww1hBKliUL6dDZZAlm3uwuYAdsvIHkDWOJ+B\nn8EE7n01h5PUpOihQ2poBAFGHvrT9ifk3bzuGviE74ejErCbBEBwJZvvsaebvaG4\n5dI3SQLXAgMBAAECggEAELX0d24LNmtUH9ktLRdzrdkYl1e0D0xynKuWEP7rjRov\nEu1O3yfaxHOMC5gz3vqmueLP9bXLwTauN/n57Cznoe+dDkBZkS3fgFrx5eK2bUys\nGKnEwlLpixrZPNXSt96q0dRECCoYRbrYwTJRT1/RblNI+wSYGwN1j0brVjUcBTvH\nPjpnt9bkIS+Rb1XJg1+TfQFzt1/WvFscpDpc7zUCGczgD7hAXJU2v2NYZyNtjn2g\niFD4r0AODuFk1Z6C8fbUsgcl8AXXQnJSLPTUXnyzifzBVQmGBu3HewLDHI99pTCZ\nT8aOwgaWYUWrjeg0jfyid08j14OfhE58/PuYGwcNsQKBgQDoh1R/OQ147D75BpXP\nEI1TyKTJNZiwnzRnP64cmzAwbIfc6w06hXTJGKIVqMBwM31R8WUfJ5cBxQOb1g3a\nZDgOTz4zO9M0mFyE/L0V3XrTXzCgPN+pCbZjcAw8oizF3u2rupM5lppxuXnkDiSi\nA2GuVdPR6M8pXUmNubs5XNe1jQKBgQDnEb21yF1K2IF9p1T0jcqLkzDLZDHHudVr\ndZSVVEyhpIFmBFgF8EC3C/ehA5i8Ar1K5JvcP5hpAH345QWOUUAKk000A4WpazZx\nfZM5Ema1xju1AFOSMDzwjt4N32Dg1VPqiLDT+CjiQFH67lSretO1IpS//IKT1Y+W\nOv3/ENfm8wKBgHt7moixUJE9zDdMovPCU3sB21iq6Loq4ZZO//RbCV091WyhOnYw\ndxNvzGt6IS+0eEGy0sOXr56V9FOmedbXT9lxhZOJmqCcpM1Otk9NPbPQIi+GBDRt\nXvkxgJ4WdXZi6449139Glh/8ollUlWmgKBh/pawcWR8bVjs4Pc+5mSflAoGAYfMm\nTRmrWl/evGojXCuC8ZmqdH17kKOY8Z19J7P9bAP1Ck7LFXFbrXx4Mxv4MbKjlUzF\nOR8IN3KK8+f5a/PLRvBcKLFZhpC5GnDV6LqBKYrnonmJ841ZN8wIGy9WvNgRY3kg\nJCqtAgOr/MfswmgluEH5dkzO+WXtIQzOwMHeE7sCgYAhrJV+PvnbnaIpfTaOKXGl\nsvzSXTuey5fQQLKMKsp2haKDVZ2hadDejRHsLJKVGb+KwdJ1s5WmBJ4L80/MnOKZ\n+9Yby9DKviVx0TbvMUGuAuWMl9syo4ICMVpp0cbeSOCM5/ulYjKSeU3sFKo7aWa9\nU8Pskm36I88orq90OBpWOg==\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-fbsvc@fcmtest-push.iam.gserviceaccount.com",
"client_id": "103917424542871804597",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40fcmtest-push.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View File

@@ -1125,57 +1125,14 @@ function expandRecurringEvent(ev, rangeStart, rangeEnd) {
// Expand all recurring events in a list within a date range
function expandEvents(events, rangeStart, rangeEnd) {
const result = [];
// First, collect all exception instances and their dates by master event
const exceptionDatesByMaster = new Map();
const masterEvents = [];
const standaloneEvents = [];
for (const ev of events) {
if (ev.recurring_master_id) {
// This is an exception instance
if (!exceptionDatesByMaster.has(ev.recurring_master_id)) {
exceptionDatesByMaster.set(ev.recurring_master_id, new Set());
}
const dateStr = new Date(ev.original_start_at || ev.start_at).toISOString().split('T')[0];
exceptionDatesByMaster.get(ev.recurring_master_id).add(dateStr);
standaloneEvents.push(ev); // Exception instances are standalone events
} else if (ev.recurrence_rule?.freq) {
masterEvents.push(ev);
} else {
standaloneEvents.push(ev);
}
}
// Expand master events, skipping dates that have exception instances
for (const ev of masterEvents) {
const exceptions = exceptionDatesByMaster.get(ev.id);
if (exceptions) {
// Merge the rule's exceptions with the instance exceptions
const rule = ev.recurrence_rule || {};
const ruleExceptions = new Set(rule.exceptions || []);
for (const date of exceptions) {
ruleExceptions.add(date);
}
// Create a copy of the event with merged exceptions
const evWithMergedExceptions = {
...ev,
recurrence_rule: {
...rule,
exceptions: Array.from(ruleExceptions)
}
};
const expanded = expandRecurringEvent(evWithMergedExceptions, rangeStart, rangeEnd);
result.push(...expanded);
} else {
if (ev.recurrence_rule?.freq) {
const expanded = expandRecurringEvent(ev, rangeStart, rangeEnd);
result.push(...expanded);
} else {
result.push(ev);
}
}
// Add all standalone events (non-recurring + exception instances)
result.push(...standaloneEvents);
// Sort by start_at
result.sort((a,b) => new Date(a.start_at) - new Date(b.start_at));
return result;