diff --git a/app-clients/android-kotlin/.gradle/8.9/checksums/checksums.lock b/app-clients/android-kotlin/.gradle/8.9/checksums/checksums.lock new file mode 100644 index 00000000..14d5fc7c Binary files /dev/null and b/app-clients/android-kotlin/.gradle/8.9/checksums/checksums.lock differ diff --git a/app-clients/android-kotlin/.gradle/8.9/dependencies-accessors/gc.properties b/app-clients/android-kotlin/.gradle/8.9/dependencies-accessors/gc.properties new file mode 100644 index 00000000..e69de29b diff --git a/app-clients/android-kotlin/.gradle/8.9/executionHistory/executionHistory.lock b/app-clients/android-kotlin/.gradle/8.9/executionHistory/executionHistory.lock new file mode 100644 index 00000000..13fd3c51 Binary files /dev/null and b/app-clients/android-kotlin/.gradle/8.9/executionHistory/executionHistory.lock differ diff --git a/app-clients/android-kotlin/.gradle/8.9/fileChanges/last-build.bin b/app-clients/android-kotlin/.gradle/8.9/fileChanges/last-build.bin new file mode 100644 index 00000000..f76dd238 Binary files /dev/null and b/app-clients/android-kotlin/.gradle/8.9/fileChanges/last-build.bin differ diff --git a/app-clients/android-kotlin/.gradle/8.9/fileHashes/fileHashes.lock b/app-clients/android-kotlin/.gradle/8.9/fileHashes/fileHashes.lock new file mode 100644 index 00000000..ff47bc1d Binary files /dev/null and b/app-clients/android-kotlin/.gradle/8.9/fileHashes/fileHashes.lock differ diff --git a/app-clients/android-kotlin/.gradle/8.9/gc 2.properties b/app-clients/android-kotlin/.gradle/8.9/gc 2.properties new file mode 100644 index 00000000..e69de29b diff --git a/app-clients/android-kotlin/.gradle/8.9/gc.properties b/app-clients/android-kotlin/.gradle/8.9/gc.properties new file mode 100644 index 00000000..e69de29b diff --git a/app-clients/android-kotlin/.gradle/9.2.0/checksums/checksums.lock b/app-clients/android-kotlin/.gradle/9.2.0/checksums/checksums.lock new file mode 100644 index 00000000..900ea5d3 Binary files /dev/null and b/app-clients/android-kotlin/.gradle/9.2.0/checksums/checksums.lock differ diff --git a/app-clients/android-kotlin/.gradle/9.2.0/fileChanges/last-build.bin b/app-clients/android-kotlin/.gradle/9.2.0/fileChanges/last-build.bin new file mode 100644 index 00000000..f76dd238 Binary files /dev/null and b/app-clients/android-kotlin/.gradle/9.2.0/fileChanges/last-build.bin differ diff --git a/app-clients/android-kotlin/.gradle/9.2.0/fileHashes/fileHashes.bin b/app-clients/android-kotlin/.gradle/9.2.0/fileHashes/fileHashes.bin new file mode 100644 index 00000000..5ca7449f Binary files /dev/null and b/app-clients/android-kotlin/.gradle/9.2.0/fileHashes/fileHashes.bin differ diff --git a/app-clients/android-kotlin/.gradle/9.2.0/fileHashes/fileHashes.lock b/app-clients/android-kotlin/.gradle/9.2.0/fileHashes/fileHashes.lock new file mode 100644 index 00000000..b2701bba Binary files /dev/null and b/app-clients/android-kotlin/.gradle/9.2.0/fileHashes/fileHashes.lock differ diff --git a/app-clients/android-kotlin/.gradle/9.2.0/gc.properties b/app-clients/android-kotlin/.gradle/9.2.0/gc.properties new file mode 100644 index 00000000..e69de29b diff --git a/app-clients/android-kotlin/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/app-clients/android-kotlin/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 00000000..c5156449 Binary files /dev/null and b/app-clients/android-kotlin/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/app-clients/android-kotlin/.gradle/buildOutputCleanup/cache.properties b/app-clients/android-kotlin/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 00000000..c6960bbe --- /dev/null +++ b/app-clients/android-kotlin/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Mon Aug 03 21:46:29 IST 2026 +gradle.version=8.9 diff --git a/app-clients/android-kotlin/.gradle/vcs-1/gc.properties b/app-clients/android-kotlin/.gradle/vcs-1/gc.properties new file mode 100644 index 00000000..e69de29b diff --git a/backend/routes/calendarRoutes.js b/backend/routes/calendarRoutes.js index 336ccb7b..0bb4bf98 100644 --- a/backend/routes/calendarRoutes.js +++ b/backend/routes/calendarRoutes.js @@ -152,54 +152,29 @@ router.get('/holidays', verifyToken, async (req, res) => { // This is an if statement that checks if the 'ok' property of the 'response' object is 'false'. The 'ok' property is 'true' if the HTTP status code is in the 200-299 range, otherwise 'false'. // This checks if the external API request was unsuccessful (e.g., 5xx server error, 4xx client error other than 404), indicating a general failure to retrieve data. if (!response.ok) { - // If the 'response.ok' is 'false', this line sends an HTTP 502 (Bad Gateway) status code to the client with a generic JSON error message. 'return' stops further execution. - // This indicates that the server, acting as a gateway, received an invalid response from the upstream (Nager.Date) server, informing the client of an issue with the external service. - return res.status(502).json({ message: 'Failed to fetch holidays from Nager.Date API.' }); + return res.json([]); } - // Declares a constant variable 'data' and assigns it the result of asynchronously parsing the 'response' body as JSON. 'await' pauses execution until the JSON parsing is complete. - // This extracts the actual holiday data from the successful HTTP response received from the Nager.Date API, converting it from a raw JSON string into a JavaScript object. const data = await response.json(); + if (!Array.isArray(data)) { + return res.json([]); + } - // Declares a constant variable 'holidays' and assigns it a new array. 'data.map()' iterates over each item ('h') in the 'data' array and transforms it into a new object. - // This processes the raw holiday data received from the external API, mapping it to a standardized and potentially simplified format that is more suitable for the application's needs and client consumption. const holidays = data.map((h) => ({ - // Creates a 'date' property in the new object, assigning it the value of the 'date' property from the original holiday object 'h'. - // This extracts the date of the holiday, ensuring it's included in the standardized output. date: h.date, - // Creates a 'localName' property in the new object, assigning it the value of the 'localName' property from the original holiday object 'h'. - // This extracts the local name of the holiday, ensuring it's included in the standardized output. localName: h.localName, - // Creates a 'name' property in the new object, assigning it the value of the 'name' property from the original holiday object 'h'. - // This extracts the common name of the holiday, ensuring it's included in the standardized output. name: h.name, - // Creates a 'countryCode' property in the new object, assigning it the value of the 'countryCode' property from the original holiday object 'h'. - // This extracts the country code associated with the holiday, ensuring it's included in the standardized output. + countryCode: h.countryCode || countryCode, fixed: h.fixed, - // Creates a 'fixed' property in the new object, assigning it the value of the 'fixed' property from the original holiday object 'h'. - // This extracts the boolean indicating if the holiday has a fixed date, ensuring it's included in the standardized output. global: h.global, - // Creates a 'global' property in the new object, assigning it the value of the 'global' property from the original holiday object 'h'. - // This extracts the boolean indicating if the holiday is global for the country, ensuring it's included in the standardized output. types: h.types || [], })); - // Calls the 'set()' method on the 'holidayCache' Map, storing a new key-value pair. The 'cacheKey' is the key, and the value is an object containing the current timestamp (Date.now()) and the processed 'holidays' data. - // This stores the newly fetched and processed holiday data in the cache, along with a timestamp, so that subsequent requests for the same year and country can be served from the cache, improving performance. holidayCache.set(cacheKey, { timestamp: Date.now(), data: holidays }); - - // Sends an HTTP 200 (OK) status code to the client with the processed 'holidays' array as a JSON response. - // This sends the final, formatted holiday data back to the client, successfully fulfilling the API request. res.json(holidays); - // This keyword starts a 'catch' block, which executes if an error occurs in the preceding 'try' block. The 'error' object contains details about the exception. - // This provides a mechanism to gracefully handle any unexpected errors that might occur during the API call or data processing, preventing the server from crashing. } catch (error) { - // Calls the 'error()' method of the 'console' object to log an error message to the console, including a descriptive string and the 'error' object itself. - // This logs detailed error information to the server's console, which is crucial for debugging and monitoring issues in a production environment. - console.error('Error fetching holidays:', error); - // Sends an HTTP 500 (Internal Server Error) status code to the client with a generic JSON error message. - // This informs the client that an unexpected server-side error occurred, providing a general error message without exposing sensitive internal details. - res.status(500).json({ message: 'Server error fetching holidays.' }); + console.warn('Error fetching holidays from external service:', error); + res.json([]); } }); diff --git a/backend/scripts/ops/purge-cloudinary 2.js b/backend/scripts/ops/purge-cloudinary 2.js new file mode 100644 index 00000000..71fefcd4 --- /dev/null +++ b/backend/scripts/ops/purge-cloudinary 2.js @@ -0,0 +1,66 @@ +require('dotenv').config({ path: require('path').resolve(__dirname, '../../.env') }); +const mongoose = require('mongoose'); +const User = require('../../models/User'); +const Team = require('../../models/Team'); +const { getApps, initializeApp, cert } = require('firebase-admin/app'); +const { getAuth } = require('firebase-admin/auth'); + +// Ensure firebase admin is initialized for fetching user profiles if needed +if (!getApps().length) { + try { + const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT); + initializeApp({ + credential: cert(serviceAccount) + }); + } catch (err) { + console.error('Failed to initialize firebase admin:', err); + } +} + +async function run() { + try { + await mongoose.connect(process.env.MONGO_URI); + console.log('Connected to DB'); + + // 1. Purge Cloudinary from Users + const users = await User.find({ photoURL: { $regex: 'cloudinary' } }); + console.log(`Found ${users.length} users with Cloudinary photos.`); + + for (const user of users) { + let providerPhoto = null; + + // Try to fetch from Firebase + if (getApps().length) { + try { + const fbUser = await getAuth().getUser(user.uid); + providerPhoto = fbUser.photoURL; + } catch (e) { + console.error(`Failed to fetch fb user ${user.uid}:`, e.message); + } + } + + await User.updateOne( + { _id: user._id }, + { $set: { photoURL: providerPhoto || null } } + ); + console.log(`Updated user ${user.uid}, photoURL reset to: ${providerPhoto}`); + } + + // 2. Purge Cloudinary from Teams (if they have team logos) + // Team logo might be stored as logoId or photoURL. Let's check Team model. + // If it exists, it's usually `logoId` or something similar, but let's check `photoURL` just in case. + const teams = await Team.find({ logoId: { $regex: 'cloudinary' } }); + console.log(`Found ${teams.length} teams with Cloudinary logos.`); + for (const team of teams) { + await Team.updateOne({ _id: team._id }, { $set: { logoId: null } }); + } + + console.log('Purge complete.'); + process.exit(0); + } catch (error) { + console.error(error); + process.exit(1); + } +} + +run(); diff --git a/backend/utils/githubInstallation 2.js b/backend/utils/githubInstallation 2.js new file mode 100644 index 00000000..ee6449ff --- /dev/null +++ b/backend/utils/githubInstallation 2.js @@ -0,0 +1,361 @@ +/** + * @fileoverview githubInstallation.js + * @module githubInstallation + * + * ============================================================================ + * SELF-HEALING GITHUB APP INSTALLATION RESOLVER + * ============================================================================ + * + * PROBLEM THIS SOLVES + * ---------------------------------------------------------------------------- + * Zync used to treat `user.githubIntegration.installationId` as the single + * source of truth, and it would NULL that field whenever a GitHub API call + * returned 401/404. That is unsafe, because GitHub returns 401/404 for many + * *transient* reasons that have nothing to do with the app being uninstalled: + * + * - Octokit caches installation access tokens for ~1 hour. Deleting a repo + * (or changing the installation's repository selection, which happens when + * a repo is created) invalidates those cached tokens => "401 Bad credentials". + * - Rate limiting / secondary rate limits. + * - Transient GitHub outages and 5xx responses surfaced as 401/404. + * + * Once the ID was nulled, every later request short-circuited to + * `notInstalled: true` and the UI showed "Install Zync GitHub App" FOREVER, + * even though the app was still installed. The only escape was reinstalling. + * + * THE FIX + * ---------------------------------------------------------------------------- + * The stored installationId is now treated as a CACHE, never as the truth. + * GitHub is the truth. This module: + * + * 1. VERIFIES the stored id against `GET /app/installations/{id}` (App JWT). + * 2. REDISCOVERS the id via `GET /users/{login}/installation` and + * `GET /orgs/{login}/installation` when the stored one is missing/stale, + * then re-persists it. This means a wiped or drifted id heals itself. + * 3. Reports `notInstalled` ONLY when GitHub authoritatively 404s the + * rediscovery — i.e. the user really did uninstall it or never installed. + * 4. Treats every other failure as TRANSIENT and keeps the stored id intact. + * + * @author Chitkul Lakshya + * @copyright Copyright (c) 2026 Zync Meet. All rights reserved. + * @license Proprietary and Confidential + * ============================================================================ + */ +const axios = require('axios'); +const User = require('../models/User'); +const cache = require('./cache'); +const { getAppJwt } = require('./githubAppAuth'); + +const GITHUB_API = 'https://api.github.com'; +const ACCEPT = 'application/vnd.github.v3+json'; + +// Short TTL: long enough to avoid hammering GitHub with App-JWT verification on +// every request, short enough that a real uninstall is noticed quickly. +const INSTALLATION_CACHE_TTL_SECONDS = 300; + +const installationCacheKey = (uid) => `gh:installation:${uid}`; +const reposCacheKey = (uid) => `gh:user-repos:${uid}`; + +/** + * Reasons returned by resolveInstallation, so callers can react correctly + * instead of collapsing everything into "not installed". + */ +const RESOLUTION = { + OK: 'ok', + NOT_INSTALLED: 'not_installed', + NOT_CONNECTED: 'not_connected', + SUSPENDED: 'suspended', + UNKNOWN: 'unknown', +}; + +const statusOf = (error) => + error?.status || error?.response?.status || null; + +/** + * WHAT: Distinguishes an authoritative "this does not exist" from a transient + * failure. WHY: Only an authoritative 404 may clear a stored installation id. + */ +const isAuthoritativeMissing = (error) => { + const status = statusOf(error); + return status === 404 || status === 410; +}; + +const appRequest = async (path) => { + const jwtToken = getAppJwt(); + return axios.get(`${GITHUB_API}${path}`, { + headers: { + Authorization: `Bearer ${jwtToken}`, + Accept: ACCEPT, + }, + // Never let a hung GitHub connection wedge a request. + timeout: 10000, + }); +}; + +/** + * WHAT: Persists a freshly resolved installation id onto the user document. + * WHY: Keeps the cached copy aligned with GitHub so subsequent calls are fast. + */ +const persistInstallationId = async (uid, installationId) => { + const user = await User.findOne({ uid }).select('githubIntegration').lean(); + const existing = user?.githubIntegration || {}; + + const normalized = installationId ? String(installationId) : null; + if (String(existing.installationId || '') === String(normalized || '')) { + return; + } + + await User.updateOne( + { uid }, + { + $set: { + githubIntegration: { + ...existing, + installationId: normalized, + // An installation that resolves is by definition connected. + connected: normalized ? true : Boolean(existing.accessToken), + ...(normalized ? { installationVerifiedAt: new Date().toISOString() } : {}), + }, + }, + } + ); + + // The accessible repository set changes with the installation. + await cache.invalidate(reposCacheKey(uid), installationCacheKey(uid)).catch(() => {}); +}; + +/** + * WHAT: Confirms a stored installation id still exists on GitHub. + * WHY: Cheap authoritative check before we trust or discard the cached id. + * + * @returns {'valid'|'suspended'|'missing'|'unknown'} + */ +const verifyInstallationId = async (installationId) => { + const numeric = Number.parseInt(installationId, 10); + if (!Number.isFinite(numeric) || numeric <= 0) { + return 'missing'; + } + + try { + const { data } = await appRequest(`/app/installations/${numeric}`); + if (data?.suspended_at) { + return 'suspended'; + } + return 'valid'; + } catch (error) { + if (isAuthoritativeMissing(error)) { + return 'missing'; + } + // 401 here means OUR App JWT/private key is wrong - a server misconfiguration, + // not a user problem. Never punish the user's stored id for it. + console.warn( + `[GitHubInstallation] Could not verify installation ${installationId} (status ${statusOf(error)}): ${error.message}` + ); + return 'unknown'; + } +}; + +/** + * WHAT: Finds the installation id for a GitHub account by login. + * WHY: Lets Zync recover an id that was never stored, or was wrongly wiped. + * + * @returns {{ id: number|null, authoritativeMissing: boolean }} + */ +const discoverInstallationId = async (login) => { + if (!login) { + return { id: null, authoritativeMissing: false }; + } + + const encoded = encodeURIComponent(login); + let sawAuthoritativeMissing = false; + + // A GitHub account is either a user or an organization; try both. + for (const path of [`/users/${encoded}/installation`, `/orgs/${encoded}/installation`]) { + try { + const { data } = await appRequest(path); + if (data?.id) { + return { id: data.id, authoritativeMissing: false }; + } + } catch (error) { + if (isAuthoritativeMissing(error)) { + sawAuthoritativeMissing = true; + continue; + } + console.warn( + `[GitHubInstallation] Discovery via ${path} failed (status ${statusOf(error)}): ${error.message}` + ); + // Transient failure - we cannot conclude anything. + return { id: null, authoritativeMissing: false }; + } + } + + return { id: null, authoritativeMissing: sawAuthoritativeMissing }; +}; + +/** + * WHAT: Resolves the effective GitHub App installation id for a Zync user, + * healing the stored value when it has drifted from GitHub. + * WHY: Single, trustworthy entry point so no caller ever has to guess whether + * a failure means "uninstalled" or "GitHub hiccuped". + * + * @param {string} uid Zync user id + * @param {{ forceRefresh?: boolean }} [options] + * @returns {Promise<{installationId: string|null, reason: string, login: string|null}>} + */ +const resolveInstallation = async (uid, options = {}) => { + const { forceRefresh = false } = options; + + if (!uid) { + return { installationId: null, reason: RESOLUTION.NOT_CONNECTED, login: null }; + } + + if (!forceRefresh) { + const cached = await cache.getJson(installationCacheKey(uid)).catch(() => null); + if (cached && cached.installationId) { + return cached; + } + } + + const user = await User.findOne({ uid }).select('githubIntegration').lean(); + const github = user?.githubIntegration || {}; + const login = github.username || null; + const storedId = github.installationId || null; + + // 1. Trust-but-verify the stored id. + if (storedId) { + const state = await verifyInstallationId(storedId); + + if (state === 'valid') { + const result = { installationId: String(storedId), reason: RESOLUTION.OK, login }; + await cache + .setJson(installationCacheKey(uid), result, INSTALLATION_CACHE_TTL_SECONDS) + .catch(() => {}); + return result; + } + + if (state === 'suspended') { + return { installationId: String(storedId), reason: RESOLUTION.SUSPENDED, login }; + } + + if (state === 'unknown') { + // Transient. Keep using what we have rather than degrading the user. + return { installationId: String(storedId), reason: RESOLUTION.OK, login }; + } + // state === 'missing' -> fall through to rediscovery. + } + + // 2. Rediscover from GitHub. This is what heals a wiped/stale id. + const { id: discoveredId, authoritativeMissing } = await discoverInstallationId(login); + + if (discoveredId) { + await persistInstallationId(uid, discoveredId); + const result = { installationId: String(discoveredId), reason: RESOLUTION.OK, login }; + await cache + .setJson(installationCacheKey(uid), result, INSTALLATION_CACHE_TTL_SECONDS) + .catch(() => {}); + return result; + } + + // 3. Only now, with GitHub explicitly saying "no installation", do we clear. + if (authoritativeMissing) { + if (storedId) { + await persistInstallationId(uid, null); + } + return { installationId: null, reason: RESOLUTION.NOT_INSTALLED, login }; + } + + if (!login) { + // Never connected a GitHub account at all. + return { + installationId: storedId ? String(storedId) : null, + reason: storedId ? RESOLUTION.OK : RESOLUTION.NOT_CONNECTED, + login: null, + }; + } + + // Could not reach GitHub. Preserve whatever we had; do NOT claim uninstalled. + return { + installationId: storedId ? String(storedId) : null, + reason: storedId ? RESOLUTION.OK : RESOLUTION.UNKNOWN, + login, + }; +}; + +/** + * WHAT: Builds an Octokit client scoped to the user's installation, retrying + * once with a forced re-resolution if the first attempt is rejected. + * WHY: Absorbs the stale-cached-token 401s that used to nuke the installation id. + */ +const getInstallationOctokit = async (uid, options = {}) => { + const appId = process.env.GITHUB_APP_ID; + let privateKey = process.env.GITHUB_PRIVATE_KEY || process.env.GITHUB_APP_PRIVATE_KEY; + + if (!appId || !privateKey) { + const err = new Error('Server configuration error: missing GitHub App credentials'); + err.code = 'GITHUB_APP_MISCONFIGURED'; + throw err; + } + privateKey = privateKey.replace(/\\n/g, '\n'); + + const { App } = await import('octokit'); + + const attempt = async (forceRefresh) => { + const resolution = await resolveInstallation(uid, { forceRefresh }); + + if (!resolution.installationId) { + const err = new Error('GitHub App installation not found for user'); + err.code = + resolution.reason === RESOLUTION.NOT_INSTALLED + ? 'GITHUB_APP_NOT_INSTALLED' + : 'GITHUB_INSTALLATION_UNRESOLVED'; + err.resolution = resolution; + throw err; + } + + if (resolution.reason === RESOLUTION.SUSPENDED) { + const err = new Error('GitHub App installation is suspended'); + err.code = 'GITHUB_APP_SUSPENDED'; + err.resolution = resolution; + throw err; + } + + // A fresh App instance avoids reusing Octokit's cached installation token, + // which is exactly what goes stale after a repo is created or deleted. + const app = new App({ appId, privateKey }); + return app.getInstallationOctokit(Number.parseInt(resolution.installationId, 10)); + }; + + try { + return await attempt(Boolean(options.forceRefresh)); + } catch (error) { + if (error.code && error.code.startsWith('GITHUB_APP')) { + throw error; + } + // Stale token or drifted id: re-resolve against GitHub and try once more. + return attempt(true); + } +}; + +/** + * WHAT: Clears every cached artifact derived from the installation. + * WHY: Called after repo create/delete and installation webhooks so the next + * read reflects reality instead of a 5-minute-old snapshot. + */ +const invalidateInstallationCaches = async (uid) => { + if (!uid) { + return; + } + await cache.invalidate(installationCacheKey(uid), reposCacheKey(uid)).catch(() => {}); +}; + +module.exports = { + RESOLUTION, + resolveInstallation, + getInstallationOctokit, + discoverInstallationId, + verifyInstallationId, + persistInstallationId, + invalidateInstallationCaches, + installationCacheKey, + reposCacheKey, +}; diff --git a/bugs.md b/bugs.md new file mode 100644 index 00000000..c27db78c --- /dev/null +++ b/bugs.md @@ -0,0 +1,86 @@ +analyze the notes section the share feature is not working. +do not edit any chnages just analyze the code and tell why the feature is not working + +1. The Email Invitation is Stubbed Out (Incomplete) +In + +ShareDialog.tsx +, the handleInvite function (which triggers when clicking the "Invite" button) only displays an informational toast stating that the email invitation feature is coming soon: + +const handleInvite = async () => { + if (!email) {return;} + setLoading(true); + try { + toast.info("Invite by email coming soon! Please share the link."); + } catch (e: any) { + toast.error("Failed to invite user"); + } finally { + setLoading(false); + } +}; +No actual backend call or DB write occurs when inviting someone by email. + +2. Missing User Identity Lookup / Mapping +The permissions store maps user IDs (uid) to roles (viewer, editor, owner). The dialog asks for the colleague's email but does not resolve that email to a corresponding uid in order to update the permissions dictionary correctly. + +Based on the analysis of + +PeopleView.tsx +, the activity status bug is caused by the following logic in lines 930–950: + +let statusText = status; +if (lastSeenDate && !isNaN(lastSeenDate.getTime())) { + try { + const duration = formatDistanceToNow(lastSeenDate, { addSuffix: false }) + .replace('less than a minute', '1m') + .replace(' minutes', 'm') + .replace(' minute', 'm') + .replace(' hours', 'h') + .replace(' hour', 'h') + .replace(' days', 'd') + .replace(' day', 'd'); + + if (status === 'online') { + statusText = `Online (${duration})`; + } else { + statusText = `Offline ${duration}`; + } + } catch (e) { + } +} +Why the Activity Status is Showing Wrong: +Away status is shown as Offline: The status property can be 'online' | 'offline' | 'away'. If a user is away, the code falls into the else block because status !== 'online'. Consequently, an active but idle user who should show as Away is incorrectly labeled as Offline. +Confusing Online durations: When a user is actively online, showing a duration since their last heartbeat (e.g., Online (2h)) is confusing because it makes the active status seem stale or contradictory. +Dirty duration strings: The string replacement on the output of formatDistanceToNow only replaces specific strings (like ' hours') but leaves prefixes like 'about ', 'over ', or 'almost ' intact (e.g., showing as Offline about 2h or Online (almost 15m)). +How to Solve It: +You can fix the conditional blocks to handle the three states properly (online, away, and offline), and optionally clean up the duration prefix. + +Update the logic in + +PeopleView.tsx + to: + +typescript +let statusText = status; +if (lastSeenDate && !isNaN(lastSeenDate.getTime())) { + try { + const duration = formatDistanceToNow(lastSeenDate, { addSuffix: false }) + .replace(/^(about|over|almost)\s+/, '') // Strip prefixes like "about", "over", etc. + .replace('less than a minute', '1m') + .replace(' minutes', 'm') + .replace(' minute', 'm') + .replace(' hours', 'h') + .replace(' hour', 'h') + .replace(' days', 'd') + .replace(' day', 'd'); + if (status === 'online') { + statusText = 'Online'; // Keep it clean for currently active users + } else if (status === 'away') { + statusText = `Away (${duration})`; // Correctly handle the away state + } else { + statusText = `Offline ${duration}`; // Only display Offline when they are actually offline + } + } catch (e) { + // Fallback if parsing fails + } +} \ No newline at end of file diff --git a/docs/architecture/feature-fcm-frontend-setup 2.md b/docs/architecture/feature-fcm-frontend-setup 2.md new file mode 100644 index 00000000..24d0ef64 --- /dev/null +++ b/docs/architecture/feature-fcm-frontend-setup 2.md @@ -0,0 +1,158 @@ +# Feature Architecture: Frontend Firebase Cloud Messaging (FCM) Setup + +## Overview +This document details the frontend Firebase Cloud Messaging implementation for Zync, including messaging initialization, the push notification hook, service worker for background notifications, and Vite build configuration for service worker environment variable injection. + +## 1. Firebase Messaging Initialization + +**File**: `src/lib/firebase.ts` + +### Changes +- Imported `getMessaging`, `getToken`, and `onMessage` from `firebase/messaging`. +- Initialized the `messaging` instance with a safe null fallback: +```typescript +import { getMessaging, type Messaging } from 'firebase/messaging'; + +let messaging: Messaging | null = null; +try { + messaging = getMessaging(app); +} catch (error) { + console.warn('Firebase Messaging not available:', error); +} +export { messaging, getToken, onMessage }; +``` + +### Why Null Fallback? +- Firebase Messaging requires browser APIs (`navigator`, `serviceWorker`) that may not exist in all environments. +- The null check in the hook prevents runtime crashes when messaging is unavailable. +- This allows the app to function normally on browsers without FCM support. + +## 2. Push Notifications Hook + +**File**: `src/hooks/use-push-notifications.ts` + +### Purpose +Registers the FCM device token with the backend and listens for incoming push messages while the app is in the foreground. + +### Hook Flow +``` +usePushNotifications() + │ + ├── onAuthStateChanged(user) + │ ├── No user → clear token, return + │ ├── Notification permission !== 'granted' → return + │ └── getToken(messaging, { vapidKey, swRegistration }) + │ ├── New token? → POST /api/users/fcm-token + │ └── Store in ref to prevent duplicate registrations + │ + └── onMessage(messaging, callback) + └── Show toast notification with payload data +``` + +### Key Implementation Details + +- **VAPID Key**: Read from `import.meta.env.VITE_FIREBASE_VAPID_KEY`. +- **Token Deduplication**: Uses `useRef` to track the currently registered token and only sends to backend if the token changed. +- **Permission Check**: Only attempts token registration if `Notification.permission === 'granted'`. +- **Service Worker Registration**: Uses `navigator.serviceWorker.ready` to ensure the SW is active before requesting a token. +- **Foreground Messages**: `onMessage` callback displays a `sonner` toast with the notification title and body. +- **Cleanup**: The `onAuthStateChanged` and `onMessage` listeners are properly unsubscribed on unmount. + +### Token Registration Request +```typescript +await fetch(`${API_BASE_URL}/api/users/fcm-token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${firebaseToken}`, + }, + body: JSON.stringify({ token, platform: navigator.userAgent }), +}); +``` + +## 3. Firebase Messaging Service Worker + +**File**: `public/firebase-messaging-sw.js` + +### Purpose +Handles push notifications when the app is in the background or closed. The service worker receives the push event, displays a notification, and handles click actions. + +### Background Message Handler +```javascript +messaging.onBackgroundMessage((payload) => { + const { title, body } = payload.notification || payload.data; + self.registration.showNotification(title || 'Zync', { + body: body || '', + icon: '/zync-white.webp', + badge: '/zync-white.webp', + data: payload.data || {}, + }); +}); +``` + +### Notification Click Handler +```javascript +self.addEventListener('notificationclick', (event) => { + event.notification.close(); + const data = event.notification.data; + const url = data?.url || '/'; + event.waitUntil(clients.openWindow(url)); +}); +``` + +- Clicking a notification opens/focuses the app and navigates to the relevant section. +- The `data.url` field can be set by the backend to deep-link to a specific task or meeting. + +## 4. Vite Configuration for Service Worker + +**File**: `vite.config.ts` + +### Problem +The service worker (`firebase-messaging-sw.js`) is a standalone file in `public/` and is not processed by Vite's bundler. Environment variables (`import.meta.env.VITE_*`) are not available in `public/` files. + +### Solution +Use Vite's `define` option to replace placeholder strings in the service worker at build time: +```typescript +define: { + '__VITE_FIREBASE_API_KEY__': JSON.stringify(process.env.VITE_FIREBASE_API_KEY), + '__VITE_FIREBASE_AUTH_DOMAIN__': JSON.stringify(process.env.VITE_FIREBASE_AUTH_DOMAIN), + '__VITE_FIREBASE_PROJECT_ID__': JSON.stringify(process.env.VITE_FIREBASE_PROJECT_ID), + '__VITE_FIREBASE_MESSAGING_SENDER_ID__': JSON.stringify(process.env.VITE_FIREBASE_MESSAGING_SENDER_ID), + '__VITE_FIREBASE_APP_ID__': JSON.stringify(process.env.VITE_FIREBASE_APP_ID), +} +``` + +In the service worker, these placeholders are used in the Firebase config object: +```javascript +const firebaseConfig = { + apiKey: '__VITE_FIREBASE_API_KEY__', + authDomain: '__VITE_FIREBASE_AUTH_DOMAIN__', + projectId: '__VITE_FIREBASE_PROJECT_ID__', + messagingSenderId: '__VITE_FIREBASE_MESSAGING_SENDER_ID__', + appId: '__VITE_FIREBASE_APP_ID__', +}; +``` + +At build time, Vite replaces the `__VITE_*__` strings with the actual environment variable values. + +## 5. App Integration + +**File**: `src/App.tsx` + +- `usePushNotifications()` is called inside the `AppContent` component. +- The hook activates on app load and reacts to authentication state changes. +- No props or configuration needed — the hook is self-contained. + +## Environment Variables Required + +| Variable | Scope | Description | +|----------|-------|-------------| +| `VITE_FIREBASE_VAPID_KEY` | Frontend | Web push VAPID key for FCM | +| `VITE_FIREBASE_API_KEY` | Frontend + SW | Firebase API key | +| `VITE_FIREBASE_AUTH_DOMAIN` | Frontend + SW | Firebase auth domain | +| `VITE_FIREBASE_PROJECT_ID` | Frontend + SW | Firebase project ID | +| `VITE_FIREBASE_MESSAGING_SENDER_ID` | Frontend + SW | FCM sender ID | +| `VITE_FIREBASE_APP_ID` | Frontend + SW | Firebase app ID | + +## Conclusion +The frontend FCM setup provides real-time push notification delivery to installed PWA users. The architecture cleanly separates token management (hook), background delivery (service worker), and build-time configuration (Vite define), ensuring maintainability and environment isolation. diff --git a/docs/architecture/feature-github-integration-enhancements 2.md b/docs/architecture/feature-github-integration-enhancements 2.md new file mode 100644 index 00000000..55a77786 --- /dev/null +++ b/docs/architecture/feature-github-integration-enhancements 2.md @@ -0,0 +1,28 @@ +# Feature Architecture: GitHub Integration Enhancements + +## Overview +This document outlines the recent enhancements made to the **My Projects** view and the **Workspace** dashboard, heavily focusing on deepening the GitHub integration within the Zync platform. + +## 1. My Projects: Repository Settings Management +To allow developers to manage their linked GitHub repositories without ever leaving the Zync platform, we introduced native repository settings management directly inside the `MyProjectsView`. + +### Features +- **Contextual Editing**: Repositories now feature an **"Edit Settings"** button, which dynamically appears only if the currently authenticated user is the repository owner. +- **Settings Dialog UI**: A responsive, blur-backed Modal (`Dialog`) was implemented to manage key repository metadata. +- **Modifiable Parameters**: + - **Description**: A multi-line text area allowing updates to the repository description (enforced maximum of 350 characters). + - **Website URL**: A dedicated input for updating the repository's `homepage` URL. + - **Topics**: A text input to manage repository topic tags (comma-separated). +- **API Integration**: Upon saving, a `PATCH` request is securely dispatched to the Zync backend (`/api/github/repos/:owner/:repo/settings`) using the user's authentication token. The backend orchestrates the update with the GitHub API. +- **Revalidation**: Upon successful modification, React Query (`queryClient.invalidateQueries`) is utilized to instantly refresh the `['github']` and `['projects']` data caches, ensuring the UI reflects the changes instantly without a hard reload. + +## 2. Workspace: Task Assignment & GitHub App Fallbacks +The `Workspace` component was enhanced to provide better error handling and feedback during collaborator task assignments, specifically regarding GitHub App installations. + +### Enhancements +- **App Installation State**: Introduced a new state variable (`githubAppNotInstalled`) that tracks whether the Zync GitHub App is actively installed on the target repository. +- **Graceful Degradation**: When a user attempts to load assignable users or invite collaborators, the backend now returns this installation status. If the app is missing, the UI gracefully falls back to using Personal Access Tokens (PAT) or displays localized error messages indicating the missing installation. +- **State Management Polish**: Cleaned up the `useEffect` synchronization blocks to satisfy strict ESLint rules (ensuring curly braces around return statements inside hooks) and preventing unintended re-renders during project selection. + +## Conclusion +These updates vastly improve the quality of life for project managers and developers utilizing Zync. By centralizing repository management and implementing strict, graceful error handling for missing GitHub integrations, the platform feels significantly more robust and integrated. diff --git a/docs/bug-fixes/backend-github-pagination 2.md b/docs/bug-fixes/backend-github-pagination 2.md new file mode 100644 index 00000000..d41feb9f --- /dev/null +++ b/docs/bug-fixes/backend-github-pagination 2.md @@ -0,0 +1,22 @@ +# Backend GitHub API Pagination Enhancement + +## Overview +This document outlines the enhancement made to the backend GitHub integration to resolve a silent omission of user repositories caused by API pagination defaults. + +## The Problem +Users who had linked their GitHub accounts were noticing that not all of their repositories were appearing in the "Add Project" / "Import Existing" modal checklist. +Upon investigating the `/user-repos` route in the backend, it was discovered that the GitHub Octokit `request('GET /installation/repositories')` API implements default pagination, automatically capping responses at 30 repositories per page. Because the frontend `Workspace.tsx` was not designed to send subsequent paginated requests, any repositories beyond the initial 30 were silently truncated from the user interface. + +## The Solution +Instead of placing the burden of pagination on the frontend (which would require complex looping and UI loading states), the backend route was updated to autonomously fetch and combine all paginated repositories into a single payload. + +### Implementation Details +We modified the `/user-repos` route in `backend/routes/github.js`: +1. Increased the `per_page` query parameter to the GitHub API maximum of `100`. +2. Wrapped the `octokit.request` call in a `while` loop that checks the response headers for a `rel="next"` pagination link. +3. To prevent potential infinite loops or server timeouts for extreme edge cases, the loop is hard-capped at 5 iterations, effectively allowing the server to fetch up to 500 repositories in one go. +4. The server iteratively concatenates the fetched repositories into a single `allRepos` array. +5. This massive, combined array is then formatted and cached in Redis under the `gh:user-repos:${uid}` key for 5 minutes, allowing subsequent frontend requests to instantly pull the complete list without pinging GitHub. + +**File Changed:** `backend/routes/github.js` +- Route: `router.get('/user-repos', ...)` diff --git a/docs/bug-fixes/backend-jest-node-protocol-fix 2.md b/docs/bug-fixes/backend-jest-node-protocol-fix 2.md new file mode 100644 index 00000000..29a70581 --- /dev/null +++ b/docs/bug-fixes/backend-jest-node-protocol-fix 2.md @@ -0,0 +1,42 @@ +# Bug Fix: Backend Jest Test Suite Crashes (`node:zlib` / `node:fs`) + +## 🐛 The Bug +During the backend CI validation step in GitHub Actions (`Run ESLint & Type Check & Tests`), the entire Jest test suite started fatally crashing before any tests could execute. + +The console output yielded errors such as: +```text +ENOENT: no such file or directory, open 'node:zlib' +ENOENT: no such file or directory, open 'node:fs' +``` + +Additionally, once tests did begin running, the `teamRoutes.delete.test.js` failed unexpectedly: +```text +Expected: 200 +Received: 500 +``` + +## 🔍 Root Cause Analysis + +### 1. `node:` Protocol Resolution Failure +The `ENOENT` errors were caused by an extremely outdated version of `jest` (`^25.0.0`, circa 2020) explicitly defined in the `backend/package.json`. +Modern backend dependencies (like Express 5.x) leverage standard Node.js module imports using the `node:` protocol prefix (e.g., `require('node:zlib')`). The legacy `jest-runtime` module resolver from v25 did not understand the `node:` protocol and incorrectly attempted to resolve it as a physical file on disk, leading to fatal crashes. + +### 2. Security PIN Unmocked in Tests +The `500` error in `teamRoutes.delete.test.js` surfaced because we recently introduced a strict `securityPin` requirement (utilizing `bcryptjs` for hash comparison) to the team deletion route. The test suite was still simulating a legacy payload without the PIN, causing the API to reject the request and the mock database chain (`createLeanChain`) to fail since it lacked the `.select()` modifier required by the new logic. + +## 🛠️ The Fix + +1. **Jest Version Upgrade**: We bumped the `jest` dependency in `backend/package.json` to the modern `^29.7.0` version. Jest v29+ includes a native, fully-featured module resolver that easily handles `node:` protocol imports. +2. **Delete Route Mocking**: + - Mocked `bcryptjs` to return `true` for `bcrypt.compare`. + - Updated `createLeanChain` to elegantly handle the `.select('+securityPin')` chain method. + - Updated the simulated user payload to include `securityPin: "hashed_pin"`. + - Modified the supertest request to include `.send({ pin: "123456" })`. + +## ✅ Verification +Running `npm test` inside the backend directory now resolves flawlessly: +```text +Test Suites: 12 passed, 12 total +Tests: 44 passed, 44 total +``` +All GitHub Actions CI steps pass cleanly. diff --git a/docs/bug-fixes/frontend-session-error-handling 2.md b/docs/bug-fixes/frontend-session-error-handling 2.md new file mode 100644 index 00000000..65bc9f4d --- /dev/null +++ b/docs/bug-fixes/frontend-session-error-handling 2.md @@ -0,0 +1,39 @@ +# Frontend Session Error Handling + +## Overview +This document outlines the investigation and resolution of an edge-case bug where the `/dashboard/projects` ("My Projects") screen would permanently freeze on a "Loading GitHub projects..." text after local server restarts. + +## The Problem +During development, if the terminal running the backend server is restarted, any in-memory database or locally volatile database instance gets wiped. However, the user's browser retains an active Firebase authentication session (persisted in IndexedDB). +When the user refreshed the frontend app: +1. Firebase Auth declared the user "logged in". +2. The `useMe` React Query hook automatically called the backend `/api/users/me` endpoint using the valid Firebase token. +3. The backend, having just been wiped, could not find the corresponding MongoDB `User` document, and correctly responded with a `404 User Not Found` error. +4. The `useMe` hook caught the `404`, returning `null` as the `userData`. +5. In `MyProjectsView.tsx`, the logic `if (!userData)` simply returned a `
` reading "Loading GitHub projects...". Because the data would never resolve, the user was permanently trapped on this fake loading screen without any explanation. + +## The Solution +We updated the frontend UI component to gracefully intercept this specific state mismatch (Authentication exists, but Database Record does not). + +### Implementation Details +We modified `src/components/views/MyProjectsView.tsx` to explicitly check the `userLoading` state alongside the missing `userData`: +```typescript +if (!userData) { + if (!userLoading) { + // We finished loading, but userData is still falsy (404 Not Found) + return ( +
+

Session Error

+

Your user profile could not be found in the database. Please log out and log back in to restore your session.

+
+ ); + } + return ( +
Loading GitHub projects…
+ ); +} +``` + +By adding this conditional check, if a database wipe occurs in the future, the user will now immediately see a "Session Error" prompting them to log out and log back in. The act of logging back in safely hits the `/api/users/sync` endpoint, completely restoring their missing MongoDB document and returning the app to a functional state. + +**File Changed:** `src/components/views/MyProjectsView.tsx` diff --git a/docs/bug-fixes/frontend-ui-fixes 2.md b/docs/bug-fixes/frontend-ui-fixes 2.md new file mode 100644 index 00000000..59109baf --- /dev/null +++ b/docs/bug-fixes/frontend-ui-fixes 2.md @@ -0,0 +1,34 @@ +# Frontend UI Layout & Animation Fixes + +## Overview +This document outlines two major frontend bug fixes related to the user interface: a rendering bug in the dialog animation specific to Chromium browsers, and a Flexbox layout overflow issue in the Workspace modal. + +## 1. Chromium Dialog Animation Glitch + +### The Problem +When users attempted to close the "Add Project" modal, the closing animation (shrink/fade out) appeared extremely jagged, corrupted, and visually torn. +This is a known Chromium rendering engine glitch: when a modal combines a CSS transform animation (e.g., `animate-in zoom-in` or `animate-out zoom-out`) simultaneously with the `backdrop-blur` CSS property, the browser struggles to calculate the blur effect in real-time on moving elements. + +### The Solution +To resolve this, we removed the `backdrop-blur-xl` and semi-transparent `bg-background/80` classes directly from the modal content itself, replacing it with a solid `bg-background` background color. The underlying page still retains a blurred overlay via the generic `DialogOverlay`, but the moving `` box no longer requires expensive real-time blur calculations during its transform animation. + +**File Changed:** `src/components/ui/dialog.tsx` +- Removed: `bg-background/80 backdrop-blur-xl` +- Added: `bg-background` on `` + +## 2. Tabs Flexbox Overflow (Create Project Modal) + +### The Problem +In the "Add Project" modal (`Workspace.tsx`), the list of GitHub repositories was spilling outside of the dark grey modal boundaries entirely, completely ignoring the `overflow-y-auto` rules. +This issue stems from deeply nested CSS Flexbox layouts when combined with Radix UI (Shadcn UI) primitives like ``. The `TabsContent` component conditionally strips or overrides `display: flex` settings when switching tabs. Because this flex chain was broken, the flex children lost their max-height constraints and defaulted to their natural content height, growing endlessly and spilling out of the screen. + +### The Solution +We implemented a robust CSS absolute-positioning architecture to completely bypass the flawed flex calculation chain for the tabs. + +1. We wrapped the `` components inside a strict flex container: `
`. This forces the parent container to exactly fit the remaining height in the modal. +2. We applied `absolute inset-0` to the `` elements themselves. This absolutely positions them to snap to the exact pixel dimensions of the new relative wrapper, completely detaching the repository list from the Flexbox calculation flow. +3. The internal list of repositories can now successfully trigger `overflow-y-auto` because its height is strictly clamped by the absolute bounds. + +**File Changed:** `src/components/workspace/Workspace.tsx` +- Added relative wrapper around `TabsContent`. +- Switched `TabsContent` state modifiers to `absolute inset-0`. diff --git a/docs/bug-fixes/github-app-installation-false-negative 2.md b/docs/bug-fixes/github-app-installation-false-negative 2.md new file mode 100644 index 00000000..94813e22 --- /dev/null +++ b/docs/bug-fixes/github-app-installation-false-negative 2.md @@ -0,0 +1,300 @@ +# GitHub App Installation False-Negative Fix + +## Overview +This document details the investigation and resolution of a critical bug where the **"Install Zync GitHub App"** prompt was incorrectly displayed on the "My Workspace" page when clicking "Add Project" or "Link GitHub Repository" — even though the app was already installed. The bug was most reliably triggered by **creating a repository** from within Zync or **deleting a repository** on GitHub that was linked to a workspace project. + +## The Problem + +### Symptoms +- User has the Zync GitHub App installed on their GitHub account. +- User creates a new repository via Zync's "Add Project" modal, or deletes a linked repo on GitHub. +- On the next open of "Add Project" or "Link GitHub Repository", the UI shows: + > "No repositories found. [Install Zync App on GitHub]" +- The prompt persists indefinitely, even after page refresh. +- The only escape was to uninstall and reinstall the GitHub App. + +### Root Cause Analysis + +The bug had **four independent root causes** that compounded: + +#### 1. Backend: `installationId` Wiped on Transient Errors +The `/user-repos` endpoint in `backend/routes/github.js` treated `user.githubIntegration.installationId` as the single source of truth. Whenever a GitHub API call returned `401` or `404`, the code nulled the `installationId` from the `User` document. + +However, GitHub returns `401`/`404` for many **transient** reasons that have nothing to do with the app being uninstalled: +- **Stale Octokit tokens**: Octokit caches installation access tokens for ~1 hour. Creating or deleting a repo invalidates these cached tokens, causing `401 Bad credentials` on the next call. +- **Rate limiting** / secondary rate limits. +- **Transient GitHub outages** (5xx responses sometimes surfaced as 401/404). + +Once the `installationId` was nulled, every subsequent request short-circuited to `notInstalled: true`, and the UI showed the install prompt **forever**. + +#### 2. Frontend: `repos.length === 0` Treated as "Not Installed" +`Workspace.tsx` used a naive check: if `repos.length === 0`, it unconditionally showed the "Install Zync App on GitHub" link. This meant any failure — transient error, rate limit, stale token, or genuinely having zero accessible repos — all collapsed into the same "install the app" message. + +#### 3. `generateProjectRoutes.js` / `projectRoutes.js`: New Repo Not Granted to App +Repos created via Zync used the user's **personal access token** (via `POST /user/repos`). On a "selected repositories" installation, the GitHub App would not automatically gain access to the new repo. The code never called `PUT /user/installations/:id/repositories/:repoId` to grant the App access, so the App couldn't see the newly created repo — which looked identical to "app not installed" from the frontend's perspective. + +#### 4. Webhooks: Installation Events Not Synced +The webhook worker (`githubWebhookWorker.js`) did not handle `installation.deleted`, `installation.created`, `installation.unsuspend`, or `new_permissions_accepted` actions. This meant: +- If a user uninstalled the app on GitHub, the `installationId` stayed in the DB (false "installed"). +- If a user reinstalled (which mints a new `installationId`), the old stale ID remained, causing all API calls to fail. + +--- + +## The Solution + +A multi-layered fix was implemented across 7 files, centered on a new **self-healing installation resolver**. + +### Architecture: Self-Healing Installation Resolver + +The core insight: **the stored `installationId` is a cache, not the truth. GitHub is the truth.** + +The new `backend/utils/githubInstallation.js` module implements a 3-step resolution strategy: + +1. **VERIFY** — Check the stored ID against `GET /app/installations/{id}` (authenticated as the App via JWT). Returns `valid`, `suspended`, `missing`, or `unknown`. +2. **REDISCOVER** — If the stored ID is missing/stale, query `GET /users/{login}/installation` and `GET /orgs/{login}/installation` to find the current installation. Re-persist the discovered ID. +3. **REPORT** — Only report `NOT_INSTALLED` when GitHub authoritatively returns `404` on rediscovery. Every other failure is treated as `TRANSIENT` and the stored ID is preserved. + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ resolveInstallation(uid) │ +│ │ +│ ┌─────────────┐ ┌──────────────┐ ┌───────────────────────┐ │ +│ │ 1. VERIFY │───▶│ 2. REDISCOVER │───▶│ 3. REPORT │ │ +│ │ stored ID │ │ by login │ │ OK / NOT_INSTALLED / │ │ +│ │ vs GitHub │ │ (user + org) │ │ SUSPENDED / UNKNOWN │ │ +│ └─────────────┘ └──────────────┘ └───────────────────────┘ │ +│ │ │ │ │ +│ valid? → OK found? → persist 404? → clear ID │ +│ suspended? → STOP not found? → check else → keep ID │ +│ unknown? → OK authoritative? (transient) │ +│ missing? → rediscover │ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Files Changed + +--- + +#### 1. `backend/utils/githubAppAuth.js` +**Change**: Exported `getAppJwt` for reuse by the installation resolver. + +```js +module.exports = { getInstallationAccessToken, getAppJwt }; +``` + +**Why**: The resolver needs to authenticate as the GitHub App itself (via JWT) to verify and rediscover installations. Previously `getAppJwt` was internal-only. + +--- + +#### 2. `backend/utils/githubInstallation.js` (NEW FILE) +**Change**: Created the self-healing installation resolver module. + +**Key exports**: +- `RESOLUTION` — Enum: `OK`, `NOT_INSTALLED`, `NOT_CONNECTED`, `SUSPENDED`, `UNKNOWN` +- `resolveInstallation(uid, { forceRefresh })` — The 3-step resolver described above. Returns `{ installationId, reason, login }`. +- `getInstallationOctokit(uid, { forceRefresh })` — Builds an installation-scoped Octokit client. Creates a fresh `App` instance each time to avoid Octokit's internal token cache. Retries once with `forceRefresh: true` if the first attempt fails with a non-app-specific error. +- `persistInstallationId(uid, installationId)` — Saves a resolved installation ID to the `User` document and invalidates derived caches. +- `invalidateInstallationCaches(uid)` — Clears both `gh:installation:{uid}` and `gh:user-repos:{uid}` from Redis. +- `verifyInstallationId(id)` — Checks if a stored ID is `valid`, `suspended`, `missing`, or `unknown`. +- `discoverInstallationId(login)` — Queries GitHub by user login and org login to find an installation ID. +- `installationCacheKey(uid)` / `reposCacheKey(uid)` — Redis key helpers. + +**Caching**: Installation resolution is cached in Redis for 5 minutes (`INSTALLATION_CACHE_TTL_SECONDS = 300`). Long enough to avoid hammering GitHub, short enough that a real uninstall is noticed quickly. + +**Critical design decision**: `isAuthoritativeMissing(error)` only returns `true` for HTTP `404` or `410`. A `401` (which could be a stale token or server misconfiguration) is treated as transient — the stored ID is never cleared for it. + +--- + +#### 3. `backend/routes/github.js` +**Changes**: + +**`/user-repos` endpoint (rewritten)**: +- Now uses `getInstallationOctokit(uid, { forceRefresh })` instead of manually constructing Octokit. +- Distinguishes between 5 response states: + - `notConnected` (400) — No GitHub account linked at all. + - `notInstalled` (400) — GitHub authoritatively confirms no installation. Only state that clears `installationId`. + - `suspended` (400) — Installation exists but is suspended. + - `transient` (503) — Could not reach GitHub. Does NOT clear `installationId`. + - `noRepoAccess` (200) — App is installed but has zero accessible repos. Returns `repos: []` with `noRepoAccess: true`. Does NOT show install prompt. +- Extracted `listInstallationRepos(octokit)` helper for retry capability. +- On first `listInstallationRepos` failure, retries once with `getInstallationOctokit(uid, { forceRefresh: true })` to handle stale tokens. +- Supports `?refresh=1` query param to bypass Redis cache. + +**`/installation-status` endpoint (NEW)**: +- Returns granular installation status: `{ connected, installed, notInstalled, suspended, indeterminate, login }`. +- Uses `resolveInstallation(uid, { forceRefresh: true })`. +- Returns `503` with `indeterminate: true` on transient failures — the UI must NOT show the install prompt in this case. + +**`/install` endpoint (updated)**: +- Now calls `invalidateInstallationCaches(uid)` after saving the installation ID, in addition to the existing `cache.delByPattern`. + +--- + +#### 4. `backend/routes/projectRoutes.js` +**Changes**: + +**`buildInstallationOctokitFromOwner` (simplified)**: +- Now delegates to `getInstallationOctokit(ownerUid)` from the resolver, instead of manually constructing Octokit with the raw stored `installationId`. +- This means branch creation, PR merging, and collaborator lookups all benefit from the self-healing logic. + +**`/new-repo` endpoint (updated)**: +- After creating a repo with the user's personal token, now calls `PUT /user/installations/:installationId/repositories/:repoId` to grant the Zync GitHub App access to the new repo. +- Best-effort: a failure (e.g., 304 = already accessible on "all repositories" installs) does not fail project creation. +- Replaced `cache.invalidate('gh:user-repos:uid')` with `invalidateInstallationCaches(ownerUid)` to bust both repo and installation token caches. + +--- + +#### 5. `backend/routes/generateProjectRoutes.js` +**Changes**: + +Same fix as `projectRoutes.js` `/new-repo`: +- After creating a repo via `POST /user/repos`, grants the App access via `PUT /user/installations/:id/repositories/:repoId`. +- Calls `invalidateInstallationCaches(uid)` instead of just `cache.invalidate('gh:user-repos:uid')`. + +**Why this file separately**: `generateProjectRoutes.js` has its own `/new-repo` endpoint that was not using the installation resolver at all. It created repos with the personal token and only invalidated the repo cache, leaving the installation token cache stale. + +--- + +#### 6. `backend/services/githubWebhookWorker.js` +**Changes**: + +**Installation event handling (rewritten)**: +Now handles `installation` and `installation_repositories` events with full action coverage: +- `action === 'deleted'` — Clears `installationId` from the `User` document and invalidates caches. This is the **only** place allowed to conclude "the app was uninstalled". +- `action === 'created' | 'unsuspend' | 'new_permissions_accepted'` — Persists the current `installationId` via `persistInstallationId()` and invalidates caches. This is what lets a re-install (which mints a new ID) reattach to the right Zync user immediately. +- All other actions (e.g., `added`, `removed` repositories) — Invalidates caches so the next read reflects the updated repo set. + +**User matching**: Matches by stored `installationId` first, then falls back to matching by `githubIntegration.username` (GitHub login). The login fallback is what lets a re-install with a new ID find the right user. + +**`repository.deleted` event (updated)**: +- Now calls `invalidateInstallationCaches(linkedProject.ownerUid)` in addition to invalidating project caches. Deleting a repo invalidates cached installation tokens, so both must be cleared. + +--- + +#### 7. `src/components/workspace/Workspace.tsx` +**Changes**: + +**`repoLoadState` state variable (NEW)**: +```typescript +const [repoLoadState, setRepoLoadState] = useState< + 'idle' | 'ok' | 'not-connected' | 'not-installed' | 'suspended' | 'no-repo-access' | 'error' +>('idle'); +``` + +**`loadRepos` function (NEW)**: +- Single source of truth for repo loading. Replaces duplicated inline fetch logic in both `handleOpenLinkModal` and `handleOpenCreateModal`. +- Accepts `{ force: boolean }` option. When `true`, appends `?refresh=1` to the API call to bypass Redis cache. +- Sets `repoLoadState` based on backend response: + - `response.ok` + repos → `ok` + - `response.ok` + empty repos → `no-repo-access` + - `data.notInstalled` → `not-installed` + - `data.notConnected` → `not-connected` + - `data.suspended` → `suspended` + - Everything else → `error` + +**`handleOpenLinkModal` (updated)**: +- Now calls `loadRepos({ force: true })` — always force-refreshes so a stale cache from a previous open doesn't show the wrong state. + +**`handleOpenCreateModal` (updated)**: +- Replaced old inline fetch with `loadRepos({ force: true })`. + +**Both "No repositories found" UI blocks (rewritten)**: +Both the "Link GitHub Repository" dialog and the "Add Project" Import tab now show state-aware messaging: + +| `repoLoadState` | Message shown | Install link? | +|---|---|---| +| `not-installed` | "The Zync GitHub App is not installed on your account." | Yes | +| `not-connected` | "Please connect your GitHub account first." | No | +| `suspended` | "The Zync GitHub App installation is suspended. Re-enable it on GitHub." | No | +| `no-repo-access` | "No repositories accessible. Grant the Zync App access to repositories on GitHub." | Yes (manage permissions) | +| `error` | "Could not load repositories. Please try again." | No | +| default (idle/ok with 0 repos) | "No repositories found." | No | + +--- + +## Data Flow After Fix + +``` +User clicks "Add Project" + │ + ▼ +Workspace.tsx: loadRepos({ force: true }) + │ + ▼ +GET /api/github/user-repos?refresh=1 + │ + ▼ +github.js: getInstallationOctokit(uid, { forceRefresh: true }) + │ + ▼ +githubInstallation.js: resolveInstallation(uid) + │ + ├── 1. VERIFY stored ID ── GET /app/installations/{id} (App JWT) + │ ├── valid → use it + │ ├── suspended → return SUSPENDED + │ ├── unknown → use stored ID anyway (transient) + │ └── missing → fall through to rediscover + │ + ├── 2. REDISCOVER ── GET /users/{login}/installation + /orgs/{login}/installation + │ ├── found → persistInstallationId() → use it + │ └── 404 authoritative → clear ID → return NOT_INSTALLED + │ + └── 3. Build fresh Octokit (no cached token) + │ + ▼ + GET /installation/repositories (paginated) + │ + ├── success → return repos (or noRepoAccess if empty) + └── failure → retry once with forceRefresh + ├── success → return repos + └── GITHUB_APP_NOT_INSTALLED → return notInstalled + └── other → return transient (503) + │ + ▼ +Workspace.tsx: setRepoLoadState based on response + │ + ▼ +UI shows appropriate message (install link ONLY if not-installed) +``` + +## Webhook Sync Flow + +``` +GitHub sends webhook + │ + ├── installation: deleted + │ └── Clear installationId from User → invalidate caches + │ + ├── installation: created / unsuspend / new_permissions_accepted + │ └── persistInstallationId() → invalidate caches + │ + ├── installation_repositories: added / removed + │ └── invalidate caches (repo set changed) + │ + └── repository: deleted + └── Delete linked project + steps + tasks → invalidate all caches +``` + +## Testing Checklist + +- [ ] Open "Add Project" modal when app is installed → should show repo list, not install prompt +- [ ] Create a new repo via Zync → immediately reopen "Add Project" → should show the new repo, not install prompt +- [ ] Delete a linked repo on GitHub → reopen "Add Project" → should show remaining repos, not install prompt +- [ ] Uninstall the Zync App on GitHub → reopen "Add Project" → should show "Install Zync App on GitHub" link +- [ ] Reinstall the Zync App on GitHub → reopen "Add Project" → should show repo list again (webhook persists new ID) +- [ ] Open "Link GitHub Repository" on a project → should show repo list +- [ ] App installed but zero repos granted → should show "Grant the Zync App access" message, not install prompt +- [ ] GitHub API down → should show "Could not load repositories. Please try again." — never the install prompt +- [ ] App suspended on GitHub → should show "installation is suspended" message + +## Files Modified + +| File | Type | Summary | +|---|---|---| +| `backend/utils/githubAppAuth.js` | Modified | Exported `getAppJwt` | +| `backend/utils/githubInstallation.js` | **New** | Self-healing installation resolver | +| `backend/routes/github.js` | Modified | Rewrote `/user-repos`, added `/installation-status`, updated `/install` | +| `backend/routes/projectRoutes.js` | Modified | `buildInstallationOctokitFromOwner` uses resolver; `/new-repo` grants App access | +| `backend/routes/generateProjectRoutes.js` | Modified | `/new-repo` grants App access + invalidates installation caches | +| `backend/services/githubWebhookWorker.js` | Modified | Handles installation created/deleted/unsuspend; repo deletion busts installation caches | +| `src/components/workspace/Workspace.tsx` | Modified | `repoLoadState` tracking, `loadRepos` helper, state-aware messaging in both modals | diff --git a/docs/bug-fixes/team-settings-state-fix 2.md b/docs/bug-fixes/team-settings-state-fix 2.md new file mode 100644 index 00000000..9604fcc8 --- /dev/null +++ b/docs/bug-fixes/team-settings-state-fix 2.md @@ -0,0 +1,25 @@ +# Bug Fix: Team Settings UI State Crash (`prev.map is not a function`) + +## 🐛 The Bug +During the `feature/team-quick-chat-ui` sprint, modifying team settings (such as creating a new team or editing existing team parameters) triggered a fatal UI crash on the frontend: + +```text +Uncaught TypeError: prev.map is not a function + at TeamSettingsSidebar.tsx:127 +``` + +## 🔍 Root Cause Analysis +The crash was isolated to `TeamSettingsSidebar.tsx`, specifically within the React state hook logic responsible for mutating the local UI state after a backend mutation succeeded. + +The underlying problem was a structural mismatch in the state object: +- The component was initialized anticipating `prev` to be an `Array` (since `TeamSettingsSidebar` often handles iterations of teams). +- However, when a discrete update payload was dispatched to the reducer/state hook, it was mistakenly passed an `Object` instead of an `Array`. +- Calling `.map()` on an `Object` instantly crashes the React rendering cycle. + +## 🛠️ The Fix +1. **State Isolation**: We refactored `TeamSettingsSidebar` to operate exclusively on the discrete `Object` representation of a single team rather than trying to map over a global array of teams internally. +2. **Prop Drilling Safeties**: We ensured that any modifications made to the team inside the sidebar securely passed the updated discrete `Object` up to the parent `PeopleView` context. +3. **Array Safeties**: For arrays that *were* required (like team members lists), we added defensive fallback checks (`prev?.map` and `Array.isArray(prev)`) to guarantee execution safety even if an API promise resolved unexpectedly. + +## ✅ Verification +- Testing team creation and data updates inside `TeamSettingsSidebar` now successfully commit to the database and update the UI cleanly without triggering a React hydration crash. diff --git a/src/api/calendar.ts b/src/api/calendar.ts index e4328d15..ca4c1655 100644 --- a/src/api/calendar.ts +++ b/src/api/calendar.ts @@ -94,16 +94,22 @@ export interface Country { } export const fetchHolidays = async (year: number, countryCode: string): Promise => { - const headers = await getAuthHeaders(); - const response = await fetch( - `${API_URL}/holidays?year=${year}&countryCode=${encodeURIComponent(countryCode)}`, - { headers }, - ); - if (!response.ok) { - console.error('Failed to fetch holidays:', response.status); + try { + const headers = await getAuthHeaders(); + const response = await fetch( + `${API_URL}/holidays?year=${year}&countryCode=${encodeURIComponent(countryCode)}`, + { headers }, + ); + if (!response.ok) { + console.warn('Failed to fetch holidays:', response.status); + return []; + } + const data = await response.json(); + return Array.isArray(data) ? data : []; + } catch (err) { + console.warn('Error fetching holidays:', err); return []; } - return response.json(); }; export const fetchCountries = async (): Promise => { diff --git a/src/components/kibo-ui/contribution-graph.tsx b/src/components/kibo-ui/contribution-graph.tsx index 8ed1dc3d..9094116d 100644 --- a/src/components/kibo-ui/contribution-graph.tsx +++ b/src/components/kibo-ui/contribution-graph.tsx @@ -143,9 +143,10 @@ const ContributionGraph = ({ interface ContributionGraphCalendarProps { children: (props: { activity: Activity; dayIndex: number; weekIndex: number }) => ReactNode; + maxWeeks?: number; } -const ContributionGraphCalendar = ({ children }: ContributionGraphCalendarProps) => { +const ContributionGraphCalendar = ({ children, maxWeeks }: ContributionGraphCalendarProps) => { const { data, blockSize, blockMargin } = useContributionGraph(); if (data.length === 0) { @@ -184,22 +185,30 @@ const ContributionGraphCalendar = ({ children }: ContributionGraphCalendarProps) weeks.push(week); } + // 150 days is ~21 weeks. Default mobile view to 21 weeks if maxWeeks is unspecified. + const isMobileViewport = typeof window !== 'undefined' && window.innerWidth < 768; + const effectiveMaxWeeks = maxWeeks ?? (isMobileViewport ? 21 : undefined); + + const displayWeeks = + effectiveMaxWeeks && weeks.length > effectiveMaxWeeks + ? weeks.slice(-effectiveMaxWeeks) + : weeks; + const height = 7 * (blockSize + blockMargin); - const width = weeks.length * (blockSize + blockMargin); - const marginLeft = 30; + const width = displayWeeks.length * (blockSize + blockMargin); + const marginLeft = 26; const months: { name: string; weekIndex: number }[] = []; let lastMonth = -1; - weeks.forEach((week, weekIndex) => { + displayWeeks.forEach((week, weekIndex) => { const firstDayOfWeek = parseLocalDate(week[0].date); const month = firstDayOfWeek.getMonth(); if (month !== lastMonth) { - - - - if (firstDayOfWeek < startDate) { - lastMonth = month; - return; + if (months.length > 0) { + const lastAdded = months[months.length - 1]; + if (weekIndex - lastAdded.weekIndex < 3) { + months.pop(); + } } months.push({ name: firstDayOfWeek.toLocaleString('en-US', { month: 'short' }), @@ -210,45 +219,47 @@ const ContributionGraphCalendar = ({ children }: ContributionGraphCalendarProps) }); return ( -
- {} -
- {months.map((month, idx) => ( - - {month.name} - - ))} -
- -
- {} +
+
+ {/* Month Labels */}
- Sum - Mon - Tue - Wed - Thu - Fri - Sat + {months.map((month, idx) => ( + + {month.name} + + ))}
- - {weeks.map((week, weekIndex) => - week.map((activity, dayIndex) => children({ activity, dayIndex, weekIndex })) - )} - +
+ {/* Day Labels */} +
+ Sum + Mon + Tue + Wed + Thu + Fri + Sat +
+ + + {displayWeeks.map((week, weekIndex) => + week.map((activity, dayIndex) => children({ activity, dayIndex, weekIndex })) + )} + +
); @@ -335,13 +346,15 @@ const ContributionGraphTotalCount = ({ className, ...props }: ComponentProps<'sp const ContributionGraphLegend = ({ className, ...props }: ComponentProps<'div'>) => { return ( -
+
Less -
-
-
-
-
+
+
+
+
+
+
+
More
); diff --git a/src/components/landing/CTASection.tsx b/src/components/landing/CTASection.tsx index 1df1ea11..d17d7d42 100644 --- a/src/components/landing/CTASection.tsx +++ b/src/components/landing/CTASection.tsx @@ -105,12 +105,12 @@ const CTASection = () => { return (
{/* Isometric Architectural Matrix */} -
+
{/* Massive Typography */}

diff --git a/src/components/landing/FeaturesSection.tsx b/src/components/landing/FeaturesSection.tsx index 129fb8ce..842cc286 100644 --- a/src/components/landing/FeaturesSection.tsx +++ b/src/components/landing/FeaturesSection.tsx @@ -131,64 +131,66 @@ const FeaturesSection = () => { ]; return ( -
+
{} -
-

- Everything to ship faster -

-

- From AI-powered planning to GitHub integration—the tools your team needs, - without the bloat. -

-
- - {/* Bento Box Grid */} -
- - {/* AI Project Setup - Interactive Walkthrough */} -
-
- -
-
- -
-

- AI Project Setup -

-

- Describe your idea and get a complete project structure, workflows, and task breakdown in seconds. Watch it happen live. -

-
- -
- -
+ {/* Everything to ship faster Header & Bento Box Grid (Hidden on mobile) */} +
+
+

+ Everything to ship faster +

+

+ From AI-powered planning to GitHub integration—the tools your team needs, + without the bloat. +

- {/* GitHub Sync - Interactive Walkthrough */} -
-
-
- + {/* Bento Box Grid */} +
+ {/* AI Project Setup - Interactive Walkthrough */} +
+
+ +
+
+ +
+

+ AI Project Setup +

+

+ Describe your idea and get a complete project structure, workflows, and task breakdown in seconds. Watch it happen live. +

-

- GitHub Sync -

-

- Connect repositories and auto-complete tasks when commits are pushed. Your code drives your workflow. -

-
- +
+
+
+ + {/* GitHub Sync - Interactive Walkthrough */} +
+
+
+ +
+

+ GitHub Sync +

+

+ Connect repositories and auto-complete tasks when commits are pushed. Your code drives your workflow. +

+ +
+ +
+
{/* Real-Time Notes */} -
+
@@ -215,7 +217,7 @@ const FeaturesSection = () => {
{/* Smart Calendar */} -
+

Smart Calendar @@ -240,7 +242,7 @@ const FeaturesSection = () => {

{/* Built-in Chat */} -
+
@@ -267,7 +269,7 @@ const FeaturesSection = () => {
{/* Focused Notifications */} -
+

Focused Notifications diff --git a/src/components/landing/Footer.tsx b/src/components/landing/Footer.tsx index 26010320..5cb7c99f 100644 --- a/src/components/landing/Footer.tsx +++ b/src/components/landing/Footer.tsx @@ -105,16 +105,16 @@ const Footer = () => { }; return ( -

); diff --git a/src/components/landing/MobileAppSection.tsx b/src/components/landing/MobileAppSection.tsx index 647b93d0..cd84addf 100644 --- a/src/components/landing/MobileAppSection.tsx +++ b/src/components/landing/MobileAppSection.tsx @@ -108,7 +108,7 @@ const MobileAppSection = () => { }; return ( -
+
diff --git a/src/components/landing/MobilePreview.tsx b/src/components/landing/MobilePreview.tsx index ad260391..cb818cd0 100644 --- a/src/components/landing/MobilePreview.tsx +++ b/src/components/landing/MobilePreview.tsx @@ -190,7 +190,7 @@ const MobilePreview = () => { {mockProjects.map(project => (
@@ -238,7 +238,7 @@ const MobilePreview = () => {

People

{mockPeople.map(person => ( - +
@@ -263,7 +263,7 @@ const MobilePreview = () => { {activeTab === "calendar" && (

January 2026

- +
{["S", "M", "T", "W", "T", "F", "S"].map((d, i) => (
{d}
@@ -293,7 +293,7 @@ const MobilePreview = () => { })}
- +
Sprint Planning
@@ -312,7 +312,7 @@ const MobilePreview = () => {
{mockNotes.map(note => ( - +
@@ -331,7 +331,7 @@ const MobilePreview = () => {

My Tasks

{mockTasks.map(task => ( - +
diff --git a/src/components/layout/MobileLayout.tsx b/src/components/layout/MobileLayout.tsx index 24c63387..37b9bbb1 100644 --- a/src/components/layout/MobileLayout.tsx +++ b/src/components/layout/MobileLayout.tsx @@ -2,80 +2,12 @@ * @fileoverview MobileLayout.tsx * @module MobileLayout * - * ============================================================================ - * ZYNC ENTERPRISE ARCHITECTURE DOCUMENTATION - * ============================================================================ - * - * 1. ARCHITECTURAL CONTEXT - * ---------------------------------------------------------------------------- - * This module is a critical component of the Zync platform's Client-Side Presentation & Logic Layer. - * It is designed to operate within a highly scalable, distributed micro-services - * or monolithic-hybrid architecture. The logic contained within this file has - * been strictly organized to adhere to SOLID principles, ensuring maintainability, - * scalability, and ease of testing. - * - * 2. SECURITY CONSIDERATIONS - * ---------------------------------------------------------------------------- - * - Data Sanitization: All inputs processed by this module must be sanitized - * to prevent Cross-Site Scripting (XSS) and SQL/NoSQL Injection attacks. - * - Authentication: If this module handles sensitive user data, it assumes - * that the calling context has already verified the user's JWT or session token. - * - Rate Limiting: High-frequency operations triggered by this file should be - * subject to API rate limiting to prevent Denial of Service (DoS) attacks. - * - PII Handling: Personally Identifiable Information (PII) must never be - * logged in plaintext by this module. - * - * 3. PERFORMANCE & OPTIMIZATION - * ---------------------------------------------------------------------------- - * - Time Complexity: Operations within this file are optimized for O(1) or O(n) - * where possible. Nested iterations should be strictly reviewed. - * - Memory Management: Variables and closures should be properly scoped to - * prevent memory leaks, especially in long-running Node.js processes or - * React component lifecycles. - * - Caching: Redundant data fetching or heavy computations should leverage - * Redis (backend) or React Query / local state (frontend) caching mechanisms. - * - * 4. TESTING GUIDELINES - * ---------------------------------------------------------------------------- - * - Unit Tests: Every exported function or component in this file must have - * accompanying unit tests covering at least 90% of the code paths. - * - Mocking: External dependencies (APIs, databases, third-party libraries) - * must be mocked using Jest to ensure deterministic test results. - * - Integration: This module should be tested in conjunction with its immediate - * dependencies to verify data flow integrity. - * - * 5. ERROR HANDLING STRATEGY - * ---------------------------------------------------------------------------- - * - Graceful Degradation: If a non-critical subsystem fails, this module should - * catch the error and fallback to a safe default state rather than crashing. - * - Logging: All unhandled exceptions must be logged to the central monitoring - * system (e.g., Sentry, Datadog) with full stack traces and context. - * - User Feedback: Frontend components must provide clear, localized error - * messages to the user without exposing sensitive technical details. - * - * 6. STATE MANAGEMENT (FRONTEND SPECIFIC) - * ---------------------------------------------------------------------------- - * - If this is a React component, avoid prop drilling by leveraging Context API - * or global state stores (Zustand/Redux) for deeply nested state. - * - Side effects (useEffect) must carefully manage their dependency arrays to - * prevent infinite render loops. - * - * 7. DATABASE INTERACTIONS (BACKEND SPECIFIC) - * ---------------------------------------------------------------------------- - * - Queries must be indexed and optimized. Avoid N+1 query problems by using - * Prisma's include/select capabilities effectively. - * - Database transactions should be used for all multi-step write operations - * to ensure ACID compliance and data consistency. - * - * ============================================================================ - * @author Chitkul Lakshya - * @copyright Copyright (c) 2026 Zync Meet. All rights reserved. - * @license Proprietary and Confidential - * ============================================================================ + * Premium Mobile Layout component for Zync. + * Features a 5-item bottom navigation bar: Home, People, + (center), Tasks, Meet. + * Includes glassmorphism, fluid micro-interactions, and a sleek user side drawer. */ import React from 'react'; -import { Plus, Home, CheckSquare, FileText, Folder, Users, Calendar, Video } from 'lucide-react'; -import { Button } from '@/components/ui/button'; +import { Plus, Home, Users, CheckSquare, Video } from 'lucide-react'; import { Sheet, SheetContent, SheetTrigger, SheetHeader, SheetTitle, SheetDescription } from '@/components/ui/sheet'; import { cn } from '@/lib/utils'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; @@ -106,47 +38,64 @@ export const MobileLayout = ({ user, onFabClick, rightHeaderAction, - hideActivityLog }: MobileLayoutProps) => { const [isDrawerOpen, setIsDrawerOpen] = React.useState(false); const { hasCheckedStatus, requiresInstallWall, isIOS, isAndroid } = useAppInstallStatus(); - if (hasCheckedStatus && requiresInstallWall) { - return ; - } + // if (hasCheckedStatus && requiresInstallWall) { + // return ; + // } + // Bottom Navigation Items strictly matching user specification: Home, People, +, Tasks, Meet const leftNavItems = [ { id: 'Home', icon: Home, label: 'Home' }, { id: 'People', icon: Users, label: 'People' }, - { id: 'Calendar', icon: Calendar, label: 'Cal' }, ]; const rightNavItems = [ - { id: 'Notes', icon: FileText, label: 'Notes' }, { id: 'Tasks', icon: CheckSquare, label: 'Tasks' }, { id: 'Meet', icon: Video, label: 'Meet' }, ]; - - const isMainTab = [...leftNavItems, ...rightNavItems].some(item => item.id === activeTab); - return ( -
-
-
+
+ {/* Top Header Bar */} +
+
+ Zync + Zync + + Zync + +
+ +
{rightHeaderAction} + + {/* User Profile Avatar / Drawer Trigger */} - - + Navigation Menu @@ -155,14 +104,20 @@ export const MobileLayout = ({
{user && ( -
- +
+ - {user.displayName?.substring(0, 1) || 'U'} + + {user.displayName?.substring(0, 1) || 'U'} + -
- {user.displayName} - {user.email} +
+ + {user.displayName || 'User'} + + + {user.email} +
)} @@ -183,14 +138,15 @@ export const MobileLayout = ({
- {} -
+ {/* Main View Area */} +
{children}
- {/* Bottom Navigation */} -