-
Notifications
You must be signed in to change notification settings - Fork 169
feat(worker,web): Improved connection management #579
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughThis PR introduces a declarative configuration management system with a job-based connection synchronization model, unifies repository source return shapes from Changes
Sequence Diagram(s)sequenceDiagram
participant ConfigManager
participant Chokidar as File Watcher
participant DB as Prisma DB
participant ConnectionMgr as ConnectionManager
participant Queue as Job Queue
ConfigManager->>Chokidar: watch(configPath)
ConfigManager->>ConfigManager: syncConfig()
ConfigManager->>DB: query existing connections
loop For each declared connection
ConfigManager->>DB: find connection by name + orgId
alt Connection exists
ConfigManager->>DB: update config
else New connection
ConfigManager->>DB: create connection
end
ConfigManager->>ConnectionMgr: createJobs([connection])
ConnectionMgr->>DB: create ConnectionSyncJob(PENDING)
ConnectionMgr->>Queue: enqueue sync job
end
Chokidar->>ConfigManager: file changed event
ConfigManager->>ConfigManager: syncConfig()
Note over Queue: Job Lifecycle
Queue->>ConnectionMgr: runJob(job)
ConnectionMgr->>ConnectionMgr: compile*Config()
ConnectionMgr->>DB: update ConnectionSyncJob(IN_PROGRESS)
alt Success
ConnectionMgr->>DB: update ConnectionSyncJob(COMPLETED)<br/>mark connection synced
ConnectionMgr->>Queue: emit telemetry
else Failure
ConnectionMgr->>DB: update ConnectionSyncJob(FAILED)<br/>record errorMessage
alt Retry available
ConnectionMgr->>Queue: requeue job
else Max attempts exceeded
ConnectionMgr->>DB: mark job FAILED
end
end
sequenceDiagram
participant RepoSource as Repo Source<br/>(GitHub/GitLab/etc)
participant Compile as Compile*Config
participant Source as getX*FromConfig
Note over RepoSource,Source: Before: { validRepos, notFound }
Compile->>Source: fetch repos
Source->>Source: collect validRepos[]
Source->>Source: collect notFound{ users, orgs, repos }
Source-->>Compile: { validRepos, notFound }
Compile-->>Compile: return { repoData, notFound }
Note over RepoSource,Source: After: { repos, warnings }
Compile->>Source: fetch repos
Source->>Source: collect repos[]
Source->>Source: collect warnings[] (404/errors)
Source-->>Compile: { repos, warnings }
Compile-->>Compile: return { repoData, warnings }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60–75 minutes Areas requiring extra attention:
Possibly related PRs
Suggested labels
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 26
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (11)
docs/snippets/schemas/v3/index.schema.mdx (1)
4283-4407: Schema duplication in apps configuration.The
GitHubAppConfigschema is defined twice: once underdefinitions.GitHubAppConfig(lines 4284-4343) and again inline within theoneOfarray (lines 4346-4405). This duplication violates the DRY principle and creates a maintenance burden.In standard JSON Schema practice, definitions should be referenced using
$refrather than duplicated. TheoneOfshould reference the definition like this:"oneOf": [ { "$ref": "#/properties/apps/items/definitions/GitHubAppConfig" } ]Since this is an auto-generated file, the schema generator should be updated to emit
$refreferences instead of duplicating the schema definition.packages/schemas/src/v3/app.schema.ts (1)
5-128: Schema duplication between definitions and oneOf.The
GitHubAppConfigschema is fully defined indefinitions.GitHubAppConfig(lines 5-65) and then duplicated verbatim in theoneOfarray (lines 67-128). This creates a maintenance issue where changes must be made in two places.The standard JSON Schema pattern uses
$refto reference definitions:"oneOf": [ { - "type": "object", - "properties": { ... }, - "required": [...], - "additionalProperties": false + "$ref": "#/definitions/GitHubAppConfig" } ]Since this is an auto-generated file (line 1), the schema generator should be updated to emit proper
$refreferences instead of duplicating schema definitions.docs/snippets/schemas/v3/app.schema.mdx (1)
6-129: Schema duplication between definitions and oneOf.The
GitHubAppConfigschema is defined indefinitions.GitHubAppConfig(lines 6-66) and then duplicated in theoneOfarray (lines 68-129). This duplication creates unnecessary maintenance overhead.Following JSON Schema best practices, the
oneOfshould reference the definition using$ref:"oneOf": [ { "$ref": "#/definitions/GitHubAppConfig" } ]Since this is auto-generated (line 1), the underlying schema generator needs to be updated to produce proper
$refreferences instead of duplicating schema content.packages/web/src/app/[domain]/repos/[id]/page.tsx (2)
23-29: Fix Next.js page params typing and dynamic back link (prevents route validation failures).
- params is not a Promise in App Router; remove await and correct type.
- Use the dynamic domain from params to build the BackButton href.
-export default async function RepoDetailPage({ params }: { params: Promise<{ id: string }> }) { - const { id } = await params +export default async function RepoDetailPage({ + params, +}: { params: { domain: string; id: string } }) { + const { id, domain } = params; @@ - <BackButton - href={`/${SINGLE_TENANT_ORG_DOMAIN}/repos`} + <BackButton + href={`/${domain}/repos`} name="Back to repositories" className="mb-2" />As per Next.js 15 route typing.
Also applies to: 55-62
183-206: Prisma: findUnique with additional filters is invalid; use findFirst with where on both fields.Unless you have a composite unique on (id, orgId), this will fail type-check or at runtime.
-const getRepoWithJobs = async (repoId: number) => sew(() => - withOptionalAuthV2(async ({ prisma, org }) => { - - const repo = await prisma.repo.findUnique({ - where: { - id: repoId, - orgId: org.id, - }, +const getRepoWithJobs = async (repoId: number) => sew(() => + withOptionalAuthV2(async ({ prisma, org }) => { + const repo = await prisma.repo.findFirst({ + where: { + id: repoId, + orgId: org.id, + }, include: { jobs: { orderBy: { createdAt: 'desc' }, } }, - }); + });If you do have a composite unique on (id, orgId), switch to that unique alias in findUnique’s where accordingly.
packages/web/src/initialize.ts (1)
15-27: Prisma: findUnique cannot include non-unique filters; switch to findFirst with combined where.Current query won’t validate if role condition is present.
- const guestUser = await prisma.userToOrg.findUnique({ - where: { - orgId_userId: { - orgId: SINGLE_TENANT_ORG_ID, - userId: SOURCEBOT_GUEST_USER_ID, - }, - role: { - not: OrgRole.GUEST, - } - }, - }); + const guestUser = await prisma.userToOrg.findFirst({ + where: { + orgId: SINGLE_TENANT_ORG_ID, + userId: SOURCEBOT_GUEST_USER_ID, + role: { not: OrgRole.GUEST }, + }, + });If you do have a composite unique alias orgId_userId, reserve findUnique for exact matches without extra predicates.
packages/backend/src/index.ts (1)
78-91: Add a reentrancy guard for cleanup to prevent concurrent shutdownscleanup() can be called from SIGINT, SIGTERM, uncaughtException, and unhandledRejection. Guard once to avoid double-dispose:
+let shuttingDown = false const cleanup = async (signal: string) => { + if (shuttingDown) return + shuttingDown = true logger.info(`Received ${signal}, cleaning up...`); const shutdownTimeout = 30000; try { await Promise.race([ Promise.all([ repoIndexManager.dispose(), connectionManager.dispose(), repoPermissionSyncer.dispose(), userPermissionSyncer.dispose(), promClient.dispose(), configManager.dispose(), ]), new Promise((_, reject) => setTimeout(() => reject(new Error('Shutdown timeout')), shutdownTimeout) ) ]);packages/web/src/app/[domain]/repos/components/reposTable.tsx (1)
321-338: Fix filtering for “No status” (null) and header typo.
- The Select sets "null" but default filtering won’t match row values that are actually null.
- Also, header says “Lastest status”.
Apply:
{ accessorKey: "latestJobStatus", size: 150, - header: "Lastest status", + header: "Latest status", cell: ({ row }) => getStatusBadge(row.getValue("latestJobStatus")), + // Ensure filtering handles null (no jobs) and exact matches + filterFn: (row, id, filterValue) => { + const v = row.getValue(id) as Repo["latestJobStatus"]; + if (!filterValue) return true; + if (filterValue === "null") return v == null; + return v === filterValue; + }, },And update the Select handler:
<Select - value={(table.getColumn("latestJobStatus")?.getFilterValue() as string) ?? "all"} + value={(table.getColumn("latestJobStatus")?.getFilterValue() as string) ?? "all"} onValueChange={(value) => { - table.getColumn("latestJobStatus")?.setFilterValue(value === "all" ? "" : value) + table.getColumn("latestJobStatus")?.setFilterValue(value === "all" ? undefined : value) }} >Also applies to: 147-151
packages/web/src/actions.ts (1)
1766-1813: Fix SSRF + token leakage when proxying repo images. Correct implementation bugs in the proposed diff.The security concern is valid—unvalidated
repo.imageUrl+ authentication headers = SSRF + potential token leakage to untrusted hosts. The proposed fix's approach (host allowlist, protocol checks, timeout, size limits) is sound, but the implementation has bugs:
(await new Blob(chunks))– Blob constructor is not async; should beawait new Blob(chunks).arrayBuffer()new URL(repo.imageUrl)can throw TypeError but isn't wrapped in try-catch – will crash instead of returningnotFound()Corrected diff:
try { + // Basic URL validation + let urlObj: URL; + try { + urlObj = new URL(repo.imageUrl); + } catch { + logger.warn(`Invalid image URL for repo ${repoId}`); + return notFound(); + } + if (!['https:', 'http:'].includes(urlObj.protocol)) { + logger.warn(`Blocked non-http(s) URL for repo ${repoId}`); + return notFound(); + } + // Allow only known code-host domains for this repo's connections + const allowedHosts = new Set<string>(); + for (const { connection } of repo.connections) { + if (connection.connectionType === 'github') allowedHosts.add('github.com'); + if (connection.connectionType === 'gitlab') { + const c = connection.config as unknown as GitlabConnectionConfig; + allowedHosts.add(c.url ? new URL(c.url).hostname : 'gitlab.com'); + } + if (connection.connectionType === 'gitea') { + const c = connection.config as unknown as GiteaConnectionConfig; + if (c.url) allowedHosts.add(new URL(c.url).hostname); + } + } + if (!allowedHosts.has(urlObj.hostname)) { + logger.warn(`Blocked image host ${urlObj.hostname} for repo ${repoId}`); + return notFound(); + } + // Timeout + size guard + const ac = new AbortController(); + const t = setTimeout(() => ac.abort(), 8000); const response = await fetch(repo.imageUrl, { headers: authHeaders, + signal: ac.signal, - }); + }).finally(() => clearTimeout(t)); if (!response.ok) { logger.warn(`Failed to fetch image from ${repo.imageUrl}: ${response.status}`); return notFound(); } - const imageBuffer = await response.arrayBuffer(); + const reader = response.body?.getReader(); + if (!reader) return notFound(); + const chunks: Uint8Array[] = []; + let received = 0; + const MAX = 2 * 1024 * 1024; // 2MB + while (true) { + const { done, value } = await reader.read(); + if (done) break; + received += value.byteLength; + if (received > MAX) { + logger.warn(`Image too large (${received} bytes) from ${repo.imageUrl}`); + return notFound(); + } + chunks.push(value); + } + const imageBuffer = await new Blob(chunks).arrayBuffer(); return imageBuffer; } catch (error) {packages/backend/src/gitea.ts (2)
53-60: Duplicate filtering prevents warning from ever loggingYou filter out repos with undefined full_name twice; the first filter removes them, so the subsequent warning never triggers. Merge into a single filter with logging.
- allRepos = allRepos.filter(repo => repo.full_name !== undefined); - allRepos = allRepos.filter(repo => { - if (repo.full_name === undefined) { - logger.warn(`Repository with undefined full_name found: orgId=${orgId}, repoId=${repo.id}`); - return false; - } - return true; - }); + allRepos = allRepos.filter(repo => { + if (repo.full_name == null) { + logger.warn(`Repository with undefined full_name found: orgId=${orgId}, repoId=${repo.id}`); + return false; + } + return true; + });
207-211: Validate repo identifier format before calling the APIIf repo isn’t “owner/name”, owner or name becomes undefined and yields a hard error rather than a friendly warning (Bitbucket path handles this).
- const [owner, repoName] = repo.split('/'); + const [owner, repoName] = repo.split('/'); + if (!owner || !repoName) { + const warning = `Invalid repo ${repo}`; + logger.warn(warning); + return { + type: 'warning' as const, + warning, + }; + }
🧹 Nitpick comments (50)
packages/backend/src/ee/syncSearchContexts.ts (1)
38-70: Consider deduplicating repos when using bothincludeandincludeConnections.When both filtering criteria are specified, the same repo can appear multiple times in
newReposInContextafter the concatenation on line 68. While Prisma'sconnecttypically handles duplicate IDs gracefully, it's more efficient to deduplicate before the upsert operations.Apply this diff to deduplicate repos:
for (const connection of connections) { newReposInContext = newReposInContext.concat(connection.repos.map(repo => repo.repo)); } + + // Deduplicate repos by ID + const uniqueRepoMap = new Map(newReposInContext.map(repo => [repo.id, repo])); + newReposInContext = Array.from(uniqueRepoMap.values()); }packages/web/src/app/api/(server)/stripe/route.ts (1)
63-96: Consider handling additional subscription event types.The webhook currently handles only
customer.subscription.createdandcustomer.subscription.deleted. Consider handlingcustomer.subscription.updatedto capture status changes like trial ending, plan changes, or transitions topast_due,unpaid, orcanceledstates. This would ensure the organization's subscription status remains synchronized with Stripe throughout the entire subscription lifecycle.Example implementation for handling subscription updates:
else if (event.type === 'customer.subscription.updated') { const subscription = event.data.object as Stripe.Subscription; const customerId = subscription.customer as string; const org = await prisma.org.findFirst({ where: { stripeCustomerId: customerId } }); if (!org) { return new Response('Org not found', { status: 404 }); } const status = subscription.status === 'active' ? StripeSubscriptionStatus.ACTIVE : StripeSubscriptionStatus.INACTIVE; await prisma.org.update({ where: { id: org.id }, data: { stripeSubscriptionStatus: status, stripeLastUpdatedAt: new Date() } }); logger.info(`Org ${org.id} subscription status updated to ${status}`); return new Response(JSON.stringify({ received: true }), { status: 200 }); }packages/schemas/src/v3/index.type.ts (1)
1073-1102: Tighten “id” semantics and fix “token” wording for privateKey.
- Clarify that id is the GitHub App ID (numeric). Consider enforcing a numeric string (or number) in the schema/types.
- The generated doc under privateKey mentions “token”; switch wording to “private key”.
Apply via generator (don’t edit this file directly):
export interface GitHubAppConfig { @@ - /** - * The ID of the GitHub App. - */ - id: string; + /** + * The GitHub App ID (numeric). + */ + id: string; // consider constraining to numeric string in schema @@ - /** - * The name of the secret that contains the token. - */ + /** + * The name of the secret that contains the private key. + */ @@ - /** - * The name of the environment variable that contains the token. Only supported in declarative connection configs. - */ + /** + * The name of the environment variable that contains the private key. Only supported in declarative connection configs. + */Optionally allow id to come from env/secret (string | Token) if desired for config parity.
packages/schemas/src/v3/index.schema.ts (1)
4282-4342: Avoid duplication: reference the definition from oneOf to prevent drift.You define GitHubAppConfig under definitions then re-inline the same shape in oneOf. This will diverge over time.
Apply via generator (don’t edit this file directly). Replace the inlined oneOf object with a $ref to the local definition:
"oneOf": [ - { - "type": "object", - "properties": { - "type": { "const": "githubApp", "description": "GitHub App Configuration" }, - "deploymentHostname": { "type": "string", "format": "hostname", "default": "github.com", ... }, - "id": { "type": "string", "description": "The ID of the GitHub App." }, - "privateKey": { "anyOf": [ { ...secret... }, { ...env... } ], "description": "The private key of the GitHub App." } - }, - "required": ["type", "id", "privateKey"], - "additionalProperties": false - } + { + "$ref": "#/properties/apps/items/definitions/GitHubAppConfig" + } ]Also consider:
- Constrain id to a numeric string:
"id": { "type": "string", "pattern": "^[0-9]+$", "description": "GitHub App ID (numeric)." }- Optional: allow id via env/secret for config parity (
anyOf: [ {type:string,pattern:^[0-9]+$}, { $ref: "./shared.json#/definitions/Token"} ]).Also applies to: 4345-4402
schemas/v3/app.json (1)
4-38: Schema looks solid; add numeric pattern for id and fix privateKey wording.
- Enforce numeric App ID and correct “token” wording for the private key.
"id": { - "type": "string", - "description": "The ID of the GitHub App." + "type": "string", + "pattern": "^[0-9]+$", + "description": "GitHub App ID (numeric)." }, "privateKey": { - "$ref": "./shared.json#/definitions/Token", - "description": "The private key of the GitHub App." + "$ref": "./shared.json#/definitions/Token", + "description": "The private key of the GitHub App." }Optional: permit id via env/secret for deployment flexibility:
- "id": { "type": "string", "pattern": "^[0-9]+$", "description": "GitHub App ID (numeric)." }, + "id": { + "anyOf": [ + { "type": "string", "pattern": "^[0-9]+$" }, + { "$ref": "./shared.json#/definitions/Token" } + ], + "description": "GitHub App ID (numeric)." + },packages/web/src/app/[domain]/chat/components/demoCards.tsx (1)
43-61: Preserve provider icon classes in selected state; avoid brittle cast.
- Selected path drops provider-specific classes (e.g., rounding/filters) by replacing them with invert-only styles.
- Casting to CodeHostType trusts upstream data; add a minimal runtime guard or preserve default class to avoid regressions.
Apply:
- const codeHostIcon = getCodeHostIcon(searchScope.codeHostType as CodeHostType); - const selectedIconClass = isSelected - ? "invert dark:invert-0" - : codeHostIcon.className; + const codeHostIcon = getCodeHostIcon(searchScope.codeHostType as CodeHostType); + const iconClass = cn( + codeHostIcon.className, + isSelected && "invert dark:invert-0" + ); return ( <Image src={codeHostIcon.src} alt={`${searchScope.codeHostType} icon`} width={size} height={size} - className={cn(sizeClass, selectedIconClass)} + className={cn(sizeClass, iconClass)} /> );Please confirm getCodeHostIcon is exhaustive for all possible searchScope.codeHostType values and returns a non-null fallback.
packages/web/src/app/[domain]/repos/page.tsx (1)
15-29: Compute from the single “latest job” instead of filtering.You already fetch at most one job (take: 1). Simplify and avoid extra allocations.
- const repos = _repos - .map((repo) => ({ - ...repo, - latestJobStatus: repo.jobs.length > 0 ? repo.jobs[0].status : null, - isFirstTimeIndex: repo.indexedAt === null && repo.jobs.filter((job) => job.status === RepoIndexingJobStatus.PENDING || job.status === RepoIndexingJobStatus.IN_PROGRESS).length > 0, - })) + const repos = _repos + .map((repo) => { + const latest = repo.jobs[0]; + const latestStatus = latest?.status ?? null; + const isFirstTimeIndex = + repo.indexedAt === null && + (latestStatus === RepoIndexingJobStatus.PENDING || + latestStatus === RepoIndexingJobStatus.IN_PROGRESS); + return { ...repo, latestJobStatus: latestStatus, isFirstTimeIndex }; + }) .sort((a, b) => { if (a.isFirstTimeIndex && !b.isFirstTimeIndex) return -1; if (!a.isFirstTimeIndex && b.isFirstTimeIndex) return 1; return a.name.localeCompare(b.name); });packages/web/src/app/[domain]/repos/[id]/page.tsx (1)
52-53: Defensive parse for repo.metadata.repo.metadata may be null. Safe default avoids runtime parse errors.
- const repoMetadata = repoMetadataSchema.parse(repo.metadata); + const repoMetadata = repoMetadataSchema.parse(repo.metadata ?? {});Confirm repo.metadata is always non-null in your schema; if so, ignore.
packages/web/src/initialize.ts (1)
67-94: Sync disable state from config too (not only enable).Currently, enablePublicAccess=false does nothing if entitlement exists; metadata may remain true unintentionally.
- if (env.CONFIG_PATH) { + if (env.CONFIG_PATH) { const config = await loadConfig(env.CONFIG_PATH); - const forceEnableAnonymousAccess = config.settings?.enablePublicAccess ?? env.FORCE_ENABLE_ANONYMOUS_ACCESS === 'true'; + const forceEnableAnonymousAccess = + (config.settings?.enablePublicAccess ?? + (env.FORCE_ENABLE_ANONYMOUS_ACCESS === 'true')); if (forceEnableAnonymousAccess) { if (!hasAnonymousAccessEntitlement) { logger.warn(`FORCE_ENABLE_ANONYMOUS_ACCESS env var is set to true but anonymous access entitlement is not available. Setting will be ignored.`); } else { const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); if (org) { const currentMetadata = getOrgMetadata(org); const mergedMetadata = { ...(currentMetadata ?? {}), anonymousAccessEnabled: true, }; await prisma.org.update({ where: { id: org.id }, data: { metadata: mergedMetadata }, }); logger.info(`Anonymous access enabled via FORCE_ENABLE_ANONYMOUS_ACCESS environment variable`); } } + } else { + // Explicitly disable when config/env says so + const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); + if (org) { + const currentMetadata = getOrgMetadata(org); + const mergedMetadata = { ...(currentMetadata ?? {}), anonymousAccessEnabled: false }; + await prisma.org.update({ + where: { id: org.id }, + data: { metadata: mergedMetadata }, + }); + logger.info(`Anonymous access disabled per configuration`); + } } }Confirm desired precedence between config.settings.enablePublicAccess and FORCE_ENABLE_ANONYMOUS_ACCESS; adjust the coalesce order accordingly.
packages/backend/src/connectionUtils.ts (1)
8-14: Apply exhaustive union handling for future-proof type safety.The downstream API change verification passed—no references to
NotFoundResultornotFoundItemsexist in the codebase, so callers have already been updated or never relied on those types.However, the current implementation handles the union via implicit
elselogic. Applying the exhaustive switch pattern withassertNeveris a recommended best practice to prevent runtime errors if new union members are added later:type CustomResult<T> = ValidResult<T> | WarningResult; export function processPromiseResults<T>( results: PromiseSettledResult<CustomResult<T>>[], ): { validItems: T[]; warnings: string[]; } { const validItems: T[] = []; const warnings: string[] = []; - results.forEach(result => { - if (result.status === 'fulfilled') { - const value = result.value; - if (value.type === 'valid') { - validItems.push(...value.data); - } else { - warnings.push(value.warning); - } - } - }); + results.forEach((result) => { + if (result.status !== "fulfilled") return; + const value = result.value; + switch (value.type) { + case "valid": + validItems.push(...value.data); + break; + case "warning": + warnings.push(value.warning); + break; + default: + assertNever(value); + } + }); return { validItems, warnings }; } + +function assertNever(_x: never): never { + throw new Error("Unhandled result type"); +}packages/db/prisma/schema.prisma (2)
158-170: Add indexes for common queries (by connection + createdAt, and by status).UI lists jobs per connection sorted by time and filters by status. Add indexes to avoid seq scans.
model ConnectionSyncJob { id String @id @default(cuid()) status ConnectionSyncJobStatus @default(PENDING) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt completedAt DateTime? - warningMessages String[] + warningMessages String[] errorMessage String? connection Connection @relation(fields: [connectionId], references: [id], onDelete: Cascade) connectionId Int + + @@index([connectionId, createdAt], map: "idx_conn_sync_job_conn_created") + @@index([status], map: "idx_conn_sync_job_status") }
13-20: Remove unused legacy enum ConnectionSyncStatus (and drop DB type in migration).This appears orphaned after switching to job-based sync. Keeping it invites confusion.
If confirmed unused, remove the enum and add a migration that drops the old enum type from Postgres (after ensuring no remaining columns reference it).
packages/web/src/app/[domain]/settings/connections/components/connectionJobsTable.tsx (3)
81-97: Use a single TooltipProvider and add accessible labels for icon triggers.Reduce provider duplication and improve a11y. Add
aria-labels for the icon-only triggers.- {job.errorMessage ? ( - <TooltipProvider> - <Tooltip> + {job.errorMessage ? ( + <Tooltip> <TooltipTrigger> - <AlertCircle className="h-4 w-4 text-destructive" /> + <AlertCircle aria-label="View error details" className="h-4 w-4 text-destructive" /> </TooltipTrigger> <TooltipContent className="max-w-[750px] max-h-96 overflow-scroll p-4"> {/* … */} </TooltipContent> - </Tooltip> - </TooltipProvider> + </Tooltip> ) : job.warningMessages.length > 0 ? ( - <TooltipProvider> - <Tooltip> + <Tooltip> <TooltipTrigger> - <AlertTriangle className="h-4 w-4 text-warning" /> + <AlertTriangle aria-label="View sync warnings" className="h-4 w-4 text-warning" /> </TooltipTrigger> <TooltipContent className="max-w-[750px] max-h-96 overflow-scroll p-4"> {/* … */} </TooltipContent> - </Tooltip> - </TooltipProvider> + </Tooltip> ) : null}Wrap once near the table:
- <div className="w-full"> + <TooltipProvider> + <div className="w-full"> {/* …table… */} - </div> + </div> + </TooltipProvider>Also applies to: 98-119, 256-289
101-113: Verify Tailwind token “text-warning” exists.ShadCN defaults don’t include
text-warning. If not defined in your theme, switch to a semantic token you have (e.g.,text-amber-500 dark:text-amber-400) or add the token.
172-178: Clipboard UX: add aria-live toast or label for screen readers.The copy icon relies on visual affordance only. Consider adding
aria-label="Copy job ID"on the button or emitting a success toast withrole="status".packages/backend/package.json (1)
43-43: Confirm Chokidar v4 compatibility and typing; ensure watcher lifecycle hygiene.
- If using TypeScript, verify v4 ships types or add
@types/chokidar.- Ensure Node runtime meets chokidar v4 requirements.
- Close watchers on shutdown and ignore large dirs (e.g.,
.git, cache) to avoid leaks and CPU churn.packages/backend/src/constants.ts (1)
4-4: Avoid magic ID; document or make configurable.Tying single-tenant flows to
1works with the migration but is brittle. Prefer reading from env (e.g.,SINGLE_TENANT_ORG_ID) with a sane default, or clearly document this contract here.packages/backend/src/index.ts (1)
57-57: Order-dependent shutdown: avoid racing disposals if there are dependenciesConfigManager likely watches and mutates connections; disposing it in parallel with ConnectionManager can race. Prefer sequencing (configManager → repoIndexManager/permission syncers → connectionManager → promClient), or ensure disposals are idempotent and independent. Example:
- await Promise.all([ - repoIndexManager.dispose(), - connectionManager.dispose(), - repoPermissionSyncer.dispose(), - userPermissionSyncer.dispose(), - promClient.dispose(), - configManager.dispose(), - ]) + // dispose config first if it depends on connectionManager being alive + await configManager.dispose().catch(logger.warn) + await Promise.all([ + repoIndexManager.dispose(), + repoPermissionSyncer.dispose(), + userPermissionSyncer.dispose(), + promClient.dispose(), + connectionManager.dispose(), + ])packages/backend/src/utils.ts (2)
139-154: Token resolution calls: OK; consider explicit usernames for GitHub PATThe new getTokenFromConfig(db) calls are correct. For GitHub PATs, pass a username ("x-access-token") to ensure Basic auth works consistently across providers (we already do this for GitHub App and GitLab):
- cloneUrlWithToken: createGitCloneUrlWithToken( - repo.cloneUrl, - { - password: token, - } - ), + cloneUrlWithToken: createGitCloneUrlWithToken( + repo.cloneUrl, + { + username: 'x-access-token', + password: token, + } + ),GitLab already uses 'oauth2'; Bitbucket sets username from config/user or 'x-token-auth'. Gitea can remain password-only unless we see auth issues.
Also applies to: 155-170, 171-186, 187-206
94-101: Minor: HTTP status 443 is invalid; use 503 for transient server errorsIn fetchWithRetry(),
e.status === 443looks like a port number typo. Replace with 503 (or include 500/502/503 as retryable):- if ((e.status === 403 || e.status === 429 || e.status === 443) && attempts < maxAttempts) { + if (([403, 429, 500, 502, 503].includes(e.status)) && attempts < maxAttempts) {packages/backend/src/ee/githubAppManager.ts (1)
56-66: Avoid hard-coded org id for private key resolutionSINGLE_TENANT_ORG_ID = 1 will break in multi-tenant scenarios. Make it configurable or derive from config/env:
- const SINGLE_TENANT_ORG_ID = 1; - const privateKey = await getTokenFromConfig(app.privateKey, SINGLE_TENANT_ORG_ID, this.db); + const orgIdForKms = Number(process.env.SINGLE_TENANT_ORG_ID ?? 1); + const privateKey = await getTokenFromConfig(app.privateKey, orgIdForKms, this.db);packages/backend/src/repoIndexManager.ts (1)
152-154: Clarify variable naming: this is a Date, not millisecondsgcGracePeriodMs holds a Date. Rename for clarity:
- const gcGracePeriodMs = new Date(Date.now() - this.settings.repoGarbageCollectionGracePeriodMs); + const gcCutoffDate = new Date(Date.now() - this.settings.repoGarbageCollectionGracePeriodMs); ... - { indexedAt: { lt: gcGracePeriodMs } }, + { indexedAt: { lt: gcCutoffDate } },Also applies to: 162-162
packages/web/src/app/[domain]/components/notificationDot.tsx (1)
7-9: Add aria-hidden for decorative indicator (a11y)If this dot is purely decorative, hide it from screen readers:
-export const NotificationDot = ({ className }: NotificationDotProps) => { - return <div className={cn("w-2 h-2 rounded-full bg-green-600", className)} /> -} +export const NotificationDot = ({ className }: NotificationDotProps) => { + return <div aria-hidden="true" className={cn("w-2 h-2 rounded-full bg-green-600", className)} /> +}packages/backend/src/azuredevops.ts (2)
166-235: Treat 401/403 like 404 to produce warnings instead of hard-failingCurrently only 404 becomes a warning. Adding 401/403 covers “not found or no access” uniformly:
- if (error && typeof error === 'object' && 'statusCode' in error && error.statusCode === 404) { + if (error && typeof error === 'object' && 'statusCode' in error && [401,403,404].includes((error as any).statusCode)) {Apply in getReposForOrganizations, getReposForProjects, and getRepos.
Also applies to: 237-289, 291-344
30-35: Explicit return type to lock the contractAnnotate the return type for getAzureDevOpsReposFromConfig to prevent accidental regressions:
-export const getAzureDevOpsReposFromConfig = async (...) => { +export const getAzureDevOpsReposFromConfig = async (...): Promise<{ repos: GitRepository[]; warnings: string[] }> => {Also applies to: 86-101
packages/web/src/app/[domain]/repos/components/reposTable.tsx (3)
104-110: Alt text can be “null logo” when displayName is null.Use a safe fallback:
- alt={`${repo.displayName} logo`} + alt={`${repo.displayName || repo.name} logo`}
199-205: Use for external commit URLs and open in a new tab.- const HashComponent = commitUrl ? ( - <Link - href={commitUrl} - className="font-mono text-sm text-link hover:underline" - > - {smallHash} - </Link> - ) : ( + const HashComponent = commitUrl ? ( + <a + href={commitUrl} + target="_blank" + rel="noopener noreferrer" + className="font-mono text-sm text-link hover:underline" + > + {smallHash} + </a> + ) : (
396-399: Pluralization should depend on filtered count.- {table.getFilteredRowModel().rows.length} {data.length > 1 ? 'repositories' : 'repository'} total + {table.getFilteredRowModel().rows.length} {table.getFilteredRowModel().rows.length === 1 ? 'repository' : 'repositories'} totalpackages/web/src/app/[domain]/settings/components/sidebar-nav.tsx (2)
33-39: Harden regex handling and improve a11y.Guard against invalid regex strings and set aria-current:
- const isActive = item.hrefRegex ? new RegExp(item.hrefRegex).test(pathname) : pathname === item.href; + let isActive = pathname === item.href; + if (item.hrefRegex) { + try { + isActive = new RegExp(item.hrefRegex).test(pathname); + } catch { + // ignore invalid regex; fall back to exact match + } + } ... - <Link + <Link key={item.href} href={item.href} + aria-current={isActive ? "page" : undefined}Also applies to: 46-49
1-1: Filename styleConsider renaming to sidebarNav.tsx to follow camelCase filename guideline, unless this directory already uses kebab-case. As per coding guidelines.
packages/web/src/app/[domain]/settings/connections/page.tsx (2)
21-23: Minor: simplify job checks since only the latest job is loadedYou
take: 1; use optional chaining instead of.filter(...).length > 0and array index for status.- isFirstTimeSync: connection.syncedAt === null && connection.syncJobs.filter((job) => job.status === ConnectionSyncJobStatus.PENDING || job.status === ConnectionSyncJobStatus.IN_PROGRESS).length > 0, - latestJobStatus: connection.syncJobs.length > 0 ? connection.syncJobs[0].status : null, + isFirstTimeSync: connection.syncedAt === null && ['PENDING', 'IN_PROGRESS'].includes(connection.syncJobs[0]?.status ?? ''), + latestJobStatus: connection.syncJobs[0]?.status ?? null,
9-12: Prefer dynamic rendering to avoid stale job statusThese statuses change frequently. Explicitly mark the page dynamic in Next.js 15 to prevent accidental static optimization.
const DOCS_URL = "https://docs.sourcebot.dev/docs/connections/overview"; +export const dynamic = "force-dynamic"; + export default async function ConnectionsPage({ params }: { params: { domain: string }}) {packages/web/src/app/[domain]/settings/layout.tsx (2)
66-70: Avoid unnecessary fetch for non‑ownersYou only show the “Connections” item to OWNERS. Fetch
connectionStatsonly for owners to save a DB round‑trip.-const connectionStats = await getConnectionStats(); -if (isServiceError(connectionStats)) { - throw new ServiceErrorException(connectionStats); -} +const connectionStats = userRoleInOrg === OrgRole.OWNER ? await getConnectionStats() : null; +if (connectionStats && isServiceError(connectionStats)) { + throw new ServiceErrorException(connectionStats); +}
102-108: ConfirmhrefRegextype expectationIf
SidebarNavexpects aRegExp, passing a string could cause mismatches. Consider supplying aRegExp:- hrefRegex: `/${domain}/settings/connections(\/[^/]+)?$`, + hrefRegex: new RegExp(`^/${domain}/settings/connections(/[^/]+)?$`),Please confirm
SidebarNavItem['hrefRegex']acceptsRegExp. If it already parses strings, ignore.packages/web/src/app/[domain]/components/navigationMenu/navigationItems.tsx (1)
61-66: Add accessible labels to notification dotsProvide an aria-label or title so screen readers convey why the dot is shown.
-{isReposButtonNotificationDotVisible && <NotificationDot className="absolute -right-0.5 -top-0.5" />} +{isReposButtonNotificationDotVisible && ( + <NotificationDot className="absolute -right-0.5 -top-0.5" aria-label="Repository indexing in progress" /> +)} ... -{isSettingsButtonNotificationDotVisible && <NotificationDot className="absolute -right-0.5 -top-0.5" />} +{isSettingsButtonNotificationDotVisible && ( + <NotificationDot className="absolute -right-0.5 -top-0.5" aria-label="Connection sync in progress" /> +)}Also applies to: 76-79
packages/web/src/app/[domain]/settings/connections/components/connectionsTable.tsx (1)
117-117: Typo: “Lastest status” → “Latest status”Minor copy fix in the column header.
- header: "Lastest status", + header: "Latest status",packages/backend/src/github.ts (1)
392-412: Case-insensitive topic matching consistency.You lower-case config topics but not repo topics; GitHub topics can be mixed case. Consider normalizing repo topics too before micromatch.
-const repoTopics = repo.topics ?? []; +const repoTopics = (repo.topics ?? []).map(t => t.toLowerCase());packages/backend/src/gitlab.ts (1)
235-256: Normalize topics for matching.Mirror the GitHub change: lower-case project topics before micromatch to avoid case mismatches.
-const projectTopics = project.topics ?? []; +const projectTopics = (project.topics ?? []).map(t => t.toLowerCase());packages/web/src/actions.ts (1)
579-609: Minor: consider counting pending/in-progress INDEX jobs regardless of indexedAt=null.Edge: a repo could start an INDEX job after being indexed (reindex). If you only want first-time indexing, current filter is correct; else drop indexedAt:null.
packages/backend/src/connectionManager.ts (2)
315-343: Sanitize logged errors to avoid leaking secrets.job.failedReason may include provider error messages with tokens/URLs. Consider redaction before logging/emitting.
150-156: PassabortControllerto allcompile*function calls for consistency, or remove it if unused.The
abortControlleris created at line 151 but only passed tocompileGithubConfig(line 175). The other five compile functions—compileGitlabConfig,compileGiteaConfig,compileGerritConfig,compileBitbucketConfig, andcompileAzureDevOpsConfig—do not accept anabortControllerparameter and cannot receive it currently. The suggested diff requires updating function signatures inpackages/backend/src/repoCompileUtils.tsto accept the new parameter and updating all call sites in the switch block to pass it consistently.packages/backend/src/configManager.ts (3)
29-39: Add a simple concurrency guard to coalesce rapid file changes.Back-to-back “change” events can race. Guard to ensure one in-flight sync and coalesce pending changes.
- this.watcher.on('change', async () => { + let syncing = false, pending = false; + this.watcher.on('change', async () => { logger.info(`Config file ${configPath} changed. Syncing config.`); - try { - await this.syncConfig(configPath); - } catch (error) { - logger.error(`Failed to sync config: ${error}`); - } + if (syncing) { pending = true; return; } + syncing = true; + do { + pending = false; + try { + await this.syncConfig(configPath); + } catch (error) { + logger.error(`Failed to sync config: ${error}`); + } + } while (pending); + syncing = false; });
66-69: Avoid JSON.stringify for deep equality.Key order can cause false positives. Prefer a deep-equal or stable-stringify.
- (JSON.stringify(existingConnectionConfig) !== JSON.stringify(newConnectionConfig)); + (await import('fast-deep-equal')).then(m => !m.default(existingConnectionConfig, newConnectionConfig))
102-121: Use deleteMany for stale declarative connections; log count.One query is simpler and faster; log the number deleted.
- const deletedConnections = await this.db.connection.findMany({ + const deletedConnections = await this.db.connection.findMany({ where: { isDeclarative: true, name: { notIn: Object.keys(connections ?? {}), }, orgId: SINGLE_TENANT_ORG_ID, } }); - - for (const connection of deletedConnections) { - logger.info(`Deleting connection with name '${connection.name}'. Connection ID: ${connection.id}`); - await this.db.connection.delete({ - where: { - id: connection.id, - } - }) - } + const ids = deletedConnections.map(c => c.id); + if (ids.length) { + await this.db.connection.deleteMany({ where: { id: { in: ids } }}); + logger.info(`Deleted ${ids.length} declarative connection(s): ${ids.join(', ')}`); + }packages/backend/src/gitea.ts (2)
126-130: Set an explicit page size for user repo pagination (consistency)Org flow uses limit: 100 but user flow doesn’t. Aligning avoids server defaults variance.
- const { durationMs, data } = await measure(() => - paginate((page) => api.users.userListRepos(user, { - page, - })) - ); + const { durationMs, data } = await measure(() => + paginate((page) => api.users.userListRepos(user, { + page, + limit: 100, + })) + );
138-146: Avoid sending 404s to Sentry; treat them as warnings only404s are expected during discovery; capturing them pollutes error monitoring. Only capture non-404.
- } catch (e: any) { - Sentry.captureException(e); - - if (e?.status === 404) { + } catch (e: any) { + if (e?.status === 404) { const warning = `User ${user} not found or no access`; logger.warn(warning); return { type: 'warning' as const, warning }; } + Sentry.captureException(e); throw e; }Repeat the same pattern in getReposForOrgs and getRepos catch blocks.
Also applies to: 179-187, 218-226
packages/backend/src/bitbucket.ts (3)
212-223: Stop capturing expected 404s in Sentry across Bitbucket fetchers404s during discovery (workspace/project/repo not found) should be warnings, not captured exceptions.
- } catch (e: any) { - Sentry.captureException(e); - logger.error(`Failed ...: ${e}`); - const status = e?.cause?.response?.status; - if (status == 404) { + } catch (e: any) { + logger.error(`Failed ...: ${e}`); + const status = e?.cause?.response?.status; + if (status == 404) { const warning = `... not found or invalid access`; logger.warn(warning); return { type: 'warning' as const, warning }; } + Sentry.captureException(e); throw e; }Apply in cloudGetReposForWorkspace, cloudGetReposForProjects, cloudGetRepos, serverGetReposForProjects, serverGetRepos.
Also applies to: 275-286, 324-335, 474-485, 523-534
386-392: Don’t send an empty Authorization header for Bitbucket ServerAn empty Authorization: header can trigger auth middleware quirks. Omit the header when no creds.
- const clientOptions: ClientOptions = { - baseUrl: url, - headers: { - Accept: "application/json", - Authorization: authorizationString, - }, - }; + const clientOptions: ClientOptions = { + baseUrl: url, + headers: { + Accept: "application/json", + ...(authorizationString ? { Authorization: authorizationString } : {}), + }, + };
352-360: Align exclude.repos semantics with other providers (glob patterns)Gitea uses micromatch for patterns; here we do exact includes. Consider glob support for parity.
+import micromatch from 'micromatch'; ... - if (config.exclude?.repos && config.exclude.repos.includes(cloudRepo.full_name!)) { + if (config.exclude?.repos && micromatch.isMatch(cloudRepo.full_name!, config.exclude.repos)) { return true; }And for server: compare
${projectName}/${repoSlug}with micromatch.Also applies to: 554-565
packages/backend/src/repoCompileUtils.ts (1)
27-31: Export CompileResult for consumersFunctions export Promise, but the type alias isn’t exported. Export for downstream typing clarity.
-type CompileResult = { +export type CompileResult = { repoData: RepoData[], warnings: string[], }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (61)
LICENSE.md(1 hunks)docs/snippets/schemas/v3/app.schema.mdx(2 hunks)docs/snippets/schemas/v3/index.schema.mdx(2 hunks)packages/backend/package.json(1 hunks)packages/backend/src/azuredevops.ts(7 hunks)packages/backend/src/bitbucket.ts(11 hunks)packages/backend/src/configManager.ts(1 hunks)packages/backend/src/connectionManager.ts(4 hunks)packages/backend/src/connectionUtils.ts(1 hunks)packages/backend/src/constants.ts(1 hunks)packages/backend/src/ee/githubAppManager.ts(3 hunks)packages/backend/src/ee/syncSearchContexts.ts(1 hunks)packages/backend/src/env.ts(2 hunks)packages/backend/src/gerrit.ts(0 hunks)packages/backend/src/git.ts(1 hunks)packages/backend/src/gitea.ts(7 hunks)packages/backend/src/github.ts(9 hunks)packages/backend/src/gitlab.ts(8 hunks)packages/backend/src/index.ts(4 hunks)packages/backend/src/repoCompileUtils.ts(12 hunks)packages/backend/src/repoIndexManager.ts(3 hunks)packages/backend/src/utils.ts(6 hunks)packages/db/prisma/migrations/20251026194617_add_connection_job_table/migration.sql(1 hunks)packages/db/prisma/migrations/20251026194628_ensure_single_tenant_org/migration.sql(1 hunks)packages/db/prisma/schema.prisma(1 hunks)packages/schemas/src/v3/app.schema.ts(2 hunks)packages/schemas/src/v3/app.type.ts(1 hunks)packages/schemas/src/v3/githubApp.schema.ts(0 hunks)packages/schemas/src/v3/githubApp.type.ts(0 hunks)packages/schemas/src/v3/index.schema.ts(2 hunks)packages/schemas/src/v3/index.type.ts(2 hunks)packages/shared/src/index.server.ts(0 hunks)packages/web/package.json(0 hunks)packages/web/src/actions.ts(4 hunks)packages/web/src/app/[domain]/chat/components/demoCards.tsx(2 hunks)packages/web/src/app/[domain]/components/backButton.tsx(1 hunks)packages/web/src/app/[domain]/components/navigationMenu/index.tsx(3 hunks)packages/web/src/app/[domain]/components/navigationMenu/navigationItems.tsx(4 hunks)packages/web/src/app/[domain]/components/navigationMenu/progressIndicator.tsx(2 hunks)packages/web/src/app/[domain]/components/notificationDot.tsx(1 hunks)packages/web/src/app/[domain]/components/repositoryCarousel.tsx(1 hunks)packages/web/src/app/[domain]/repos/[id]/page.tsx(7 hunks)packages/web/src/app/[domain]/repos/components/reposTable.tsx(7 hunks)packages/web/src/app/[domain]/repos/layout.tsx(2 hunks)packages/web/src/app/[domain]/repos/page.tsx(3 hunks)packages/web/src/app/[domain]/settings/components/header.tsx(0 hunks)packages/web/src/app/[domain]/settings/components/sidebar-nav.tsx(1 hunks)packages/web/src/app/[domain]/settings/connections/[id]/page.tsx(1 hunks)packages/web/src/app/[domain]/settings/connections/components/connectionJobsTable.tsx(1 hunks)packages/web/src/app/[domain]/settings/connections/components/connectionsTable.tsx(1 hunks)packages/web/src/app/[domain]/settings/connections/layout.tsx(1 hunks)packages/web/src/app/[domain]/settings/connections/page.tsx(1 hunks)packages/web/src/app/[domain]/settings/layout.tsx(4 hunks)packages/web/src/app/[domain]/settings/secrets/components/importSecretCard.tsx(1 hunks)packages/web/src/app/api/(server)/stripe/route.ts(1 hunks)packages/web/src/features/chat/components/searchScopeIcon.tsx(2 hunks)packages/web/src/initialize.ts(2 hunks)packages/web/src/lib/syncStatusMetadataSchema.ts(0 hunks)packages/web/src/lib/utils.ts(12 hunks)schemas/v3/app.json(1 hunks)schemas/v3/githubApp.json(0 hunks)
💤 Files with no reviewable changes (8)
- packages/web/src/app/[domain]/settings/components/header.tsx
- packages/web/package.json
- packages/schemas/src/v3/githubApp.schema.ts
- packages/backend/src/gerrit.ts
- schemas/v3/githubApp.json
- packages/web/src/lib/syncStatusMetadataSchema.ts
- packages/shared/src/index.server.ts
- packages/schemas/src/v3/githubApp.type.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*
📄 CodeRabbit inference engine (.cursor/rules/style.mdc)
Filenames should always be camelCase. Exception: if there are filenames in the same directory with a format other than camelCase, use that format to keep things consistent.
Files:
packages/backend/src/env.tspackages/backend/src/ee/syncSearchContexts.tspackages/web/src/app/[domain]/components/repositoryCarousel.tsxpackages/backend/src/constants.tspackages/schemas/src/v3/index.type.tspackages/web/src/app/[domain]/settings/connections/components/connectionJobsTable.tsxpackages/web/src/app/api/(server)/stripe/route.tspackages/web/src/app/[domain]/settings/components/sidebar-nav.tsxpackages/web/src/features/chat/components/searchScopeIcon.tsxpackages/backend/src/repoIndexManager.tspackages/web/src/app/[domain]/repos/components/reposTable.tsxpackages/web/src/app/[domain]/settings/connections/page.tsxpackages/backend/src/utils.tsschemas/v3/app.jsonLICENSE.mdpackages/web/src/lib/utils.tspackages/web/src/app/[domain]/components/notificationDot.tsxpackages/web/src/app/[domain]/settings/connections/components/connectionsTable.tsxpackages/web/src/app/[domain]/components/navigationMenu/navigationItems.tsxpackages/web/src/app/[domain]/repos/page.tsxpackages/backend/package.jsonpackages/web/src/app/[domain]/repos/[id]/page.tsxpackages/backend/src/azuredevops.tspackages/web/src/app/[domain]/settings/connections/[id]/page.tsxpackages/backend/src/connectionManager.tspackages/backend/src/configManager.tspackages/db/prisma/migrations/20251026194617_add_connection_job_table/migration.sqlpackages/backend/src/ee/githubAppManager.tspackages/web/src/app/[domain]/components/navigationMenu/progressIndicator.tsxpackages/backend/src/gitlab.tsdocs/snippets/schemas/v3/index.schema.mdxpackages/schemas/src/v3/app.schema.tspackages/db/prisma/schema.prismapackages/schemas/src/v3/app.type.tspackages/backend/src/connectionUtils.tspackages/backend/src/git.tspackages/web/src/app/[domain]/settings/secrets/components/importSecretCard.tsxpackages/web/src/app/[domain]/components/navigationMenu/index.tsxpackages/web/src/app/[domain]/components/backButton.tsxpackages/web/src/app/[domain]/settings/connections/layout.tsxpackages/backend/src/bitbucket.tspackages/db/prisma/migrations/20251026194628_ensure_single_tenant_org/migration.sqlpackages/backend/src/repoCompileUtils.tspackages/backend/src/index.tspackages/web/src/initialize.tspackages/web/src/app/[domain]/chat/components/demoCards.tsxpackages/web/src/app/[domain]/repos/layout.tsxpackages/backend/src/github.tspackages/web/src/actions.tspackages/schemas/src/v3/index.schema.tsdocs/snippets/schemas/v3/app.schema.mdxpackages/backend/src/gitea.tspackages/web/src/app/[domain]/settings/layout.tsx
af104ed to
d6783e4
Compare
Similar to #563 and #572, this PR does two things:
ConnectionSyncJobtable for job tracking. Also moves to using groupmq.Screenshots
Connection list view

Connection details view

Hovering over a warning icon

Hovering over a error icon

Info banner in repository page when no repos are configured

Notification dot when there is a connection that is syncing for the same time

Notification dot in details view

Notification dot in repos table to indicate first time indexing

TODO:
Summary by CodeRabbit
Release Notes
New Features
Improvements