From 54b412c1c1b83adfd78e1fc14f2d5123152b3b76 Mon Sep 17 00:00:00 2001 From: Ricky Stretch Date: Sun, 29 Mar 2026 12:48:31 -0400 Subject: [PATCH] bug fixes --- .env.fcmtest-push | 48 ++++++++++++++ backend/src/routes/schedule.js | 44 +------------ docker-compose-fcmtest-push.yaml | 66 +++++++++++++++++++ docker-compose.yaml | 5 +- ...sh-firebase-adminsdk-fbsvc-ddbf38d0c5.json | 13 ++++ frontend/src/components/SchedulePage.jsx | 49 +------------- 6 files changed, 135 insertions(+), 90 deletions(-) create mode 100644 .env.fcmtest-push create mode 100644 docker-compose-fcmtest-push.yaml create mode 100644 fcmtest-push-firebase-adminsdk-fbsvc-ddbf38d0c5.json diff --git a/.env.fcmtest-push b/.env.fcmtest-push new file mode 100644 index 0000000..aaeb7d9 --- /dev/null +++ b/.env.fcmtest-push @@ -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 diff --git a/backend/src/routes/schedule.js b/backend/src/routes/schedule.js index 9286843..1863749 100644 --- a/backend/src/routes/schedule.js +++ b/backend/src/routes/schedule.js @@ -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 }); } }); diff --git a/docker-compose-fcmtest-push.yaml b/docker-compose-fcmtest-push.yaml new file mode 100644 index 0000000..1e2ff5f --- /dev/null +++ b/docker-compose-fcmtest-push.yaml @@ -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 diff --git a/docker-compose.yaml b/docker-compose.yaml index a11ce05..215f43e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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: diff --git a/fcmtest-push-firebase-adminsdk-fbsvc-ddbf38d0c5.json b/fcmtest-push-firebase-adminsdk-fbsvc-ddbf38d0c5.json new file mode 100644 index 0000000..31cfe64 --- /dev/null +++ b/fcmtest-push-firebase-adminsdk-fbsvc-ddbf38d0c5.json @@ -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" +} diff --git a/frontend/src/components/SchedulePage.jsx b/frontend/src/components/SchedulePage.jsx index bbd3e32..d000507 100644 --- a/frontend/src/components/SchedulePage.jsx +++ b/frontend/src/components/SchedulePage.jsx @@ -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;