diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..2f5a55fd --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,7 @@ +This is a typescript application with a frontend module and a backend module that provides an API for the front end. + +All logic that aligns with the existing API (documented below) should be added to the proper existing backend components. +All other logic should be in the front end page components. + +API docs +{"openapi":"3.0.0","info":{"title":"GitHub Value API","version":"1.0.0","description":"API for GitHub Value - Copilot ROI and adoption tracking"},"servers":[{"url":"http://www2.gvm-chart.com/api","description":"Main API server"}],"paths":{"/survey":{"get":{"summary":"Get all surveys","parameters":[{"name":"org","in":"query","schema":{"type":"string"},"description":"Filter by organization"},{"name":"team","in":"query","schema":{"type":"string"},"description":"Filter by team"},{"name":"reasonLength","in":"query","schema":{"type":"string"},"description":"Filter by reason length"},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"},"description":"Filter by start date (ISO format)"},{"name":"until","in":"query","schema":{"type":"string","format":"date-time"},"description":"Filter by end date (ISO format)"},{"name":"status","in":"query","schema":{"type":"string"},"description":"Filter by status"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Survey"}}}}}}},"post":{"summary":"Create a new survey","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewSurvey"}}}},"responses":{"201":{"description":"Survey created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Survey"}}}}}}},"/survey/{id}":{"get":{"summary":"Get survey by ID","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"},"description":"Survey ID"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Survey"}}}},"404":{"description":"Survey not found"}}},"put":{"summary":"Update survey by ID","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"},"description":"Survey ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSurvey"}}}},"responses":{"200":{"description":"Survey updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Survey"}}}},"404":{"description":"Survey not found"}}},"delete":{"summary":"Delete survey by ID","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"},"description":"Survey ID"}],"responses":{"204":{"description":"Survey deleted"},"404":{"description":"Survey not found"}}}},"/survey/{id}/github":{"post":{"summary":"Update survey GitHub comment","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"},"description":"Survey ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSurvey"}}}},"responses":{"201":{"description":"Survey GitHub comment updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Survey"}}}}}}},"/metrics":{"get":{"summary":"Get metrics","parameters":[{"name":"org","in":"query","schema":{"type":"string"},"description":"Filter by organization"},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"},"description":"Filter by start date (ISO format)"},{"name":"until","in":"query","schema":{"type":"string","format":"date-time"},"description":"Filter by end date (ISO format)"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Metric"}}}}}}}},"/metrics/totals":{"get":{"summary":"Get metrics totals","parameters":[{"name":"org","in":"query","schema":{"type":"string"},"description":"Filter by organization"},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"},"description":"Filter by start date (ISO format)"},{"name":"until","in":"query","schema":{"type":"string","format":"date-time"},"description":"Filter by end date (ISO format)"}],"responses":{"200":{"description":"Successful response"}}}},"/seats":{"get":{"summary":"Get all seats","parameters":[{"name":"org","in":"query","schema":{"type":"string"},"description":"Filter by organization"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Seat"}}}}}}}},"/seats/activity":{"get":{"summary":"Get seats activity","parameters":[{"name":"enterprise","in":"query","schema":{"type":"string"},"description":"Filter by enterprise"},{"name":"org","in":"query","schema":{"type":"string"},"description":"Filter by organization"},{"name":"team","in":"query","schema":{"type":"string"},"description":"Filter by team"},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"},"description":"Filter by start date (ISO format)"},{"name":"until","in":"query","schema":{"type":"string","format":"date-time"},"description":"Filter by end date (ISO format)"},{"name":"seats","in":"query","schema":{"type":"string","enum":["0","1"]},"description":"Include seat data (1 to include)"}],"responses":{"200":{"description":"Successful response"}}}},"/seats/activity/totals":{"get":{"summary":"Get seats activity totals","parameters":[{"name":"org","in":"query","schema":{"type":"string"},"description":"Filter by organization"},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"},"description":"Filter by start date (ISO format)"},{"name":"until","in":"query","schema":{"type":"string","format":"date-time"},"description":"Filter by end date (ISO format)"},{"name":"limit","in":"query","schema":{"type":"integer"},"description":"Limit results"}],"responses":{"200":{"description":"Successful response"}}}},"/seats/{id}":{"get":{"summary":"Get seat by ID or login","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Seat ID or login"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Seat"}}}}}}},"/teams":{"get":{"summary":"Get all teams","parameters":[{"name":"org","in":"query","schema":{"type":"string"},"description":"Filter by organization"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Team"}}}}}}}},"/members":{"get":{"summary":"Get all members","parameters":[{"name":"org","in":"query","schema":{"type":"string"},"description":"Filter by organization"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Member"}}}}}}}},"/members/search":{"get":{"summary":"Search members by login","parameters":[{"name":"query","in":"query","required":true,"schema":{"type":"string"},"description":"Search query"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Member"}}}}}}}},"/members/{login}":{"get":{"summary":"Get member by login","parameters":[{"name":"login","in":"path","required":true,"schema":{"type":"string"},"description":"Member login"},{"name":"exact","in":"query","schema":{"type":"string","enum":["true","false"]},"description":"Exact match ('true' for exact)"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Member"}}}},"404":{"description":"Member not found"}}}},"/settings":{"get":{"summary":"Get all settings","responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"settings":{"type":"array","items":{"$ref":"#/components/schemas/Setting"}}}}}}}}},"post":{"summary":"Create settings","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Setting"}}}},"responses":{"201":{"description":"Settings created"}}},"put":{"summary":"Update settings","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Setting"}}}},"responses":{"200":{"description":"Settings updated"}}}},"/settings/{name}":{"get":{"summary":"Get settings by name","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"},"description":"Setting name"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Setting"}}}},"404":{"description":"Setting not found"}}},"delete":{"summary":"Delete settings by name","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"},"description":"Setting name"}],"responses":{"200":{"description":"Setting deleted"}}}},"/setup/registration/complete":{"get":{"summary":"Complete GitHub App registration","parameters":[{"name":"code","in":"query","required":true,"schema":{"type":"string"},"description":"GitHub code"}],"responses":{"302":{"description":"Redirect to GitHub App installation page"}}}},"/setup/install/complete":{"get":{"summary":"Complete GitHub App installation","responses":{"302":{"description":"Redirect to home page"}}}},"/setup/install":{"get":{"summary":"Get GitHub App installation","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"owner":{"type":"string"}}}}}},"responses":{"200":{"description":"Successful response"}}}},"/setup/manifest":{"get":{"summary":"Get GitHub App manifest","responses":{"200":{"description":"Successful response"}}}},"/setup/existing-app":{"post":{"summary":"Add existing GitHub App","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["appId","privateKey","webhookSecret"],"properties":{"appId":{"type":"string"},"privateKey":{"type":"string"},"webhookSecret":{"type":"string"}}}}}},"responses":{"200":{"description":"Successful response"},"400":{"description":"Missing required fields"}}}},"/setup/db":{"post":{"summary":"Set up database connection","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["uri"],"properties":{"uri":{"type":"string"}}}}}},"responses":{"200":{"description":"Database setup started"}}}},"/setup/status":{"get":{"summary":"Get setup status","responses":{"200":{"description":"Successful response"}}}},"/status":{"get":{"summary":"Get application status","responses":{"200":{"description":"Successful response"}}}},"/targets":{"get":{"summary":"Get target values","responses":{"200":{"description":"Successful response"}}},"post":{"summary":"Update target values","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TargetValues"}}}},"responses":{"200":{"description":"Target values updated"}}}},"/targets/calculate":{"get":{"summary":"Calculate target values","parameters":[{"name":"org","in":"query","schema":{"type":"string","default":"enterprise"},"description":"Organization (defaults to 'enterprise')"},{"name":"enableLogging","in":"query","schema":{"type":"string","enum":["true","false"]},"description":"Enable logging ('true' to enable)"},{"name":"includeLogs","in":"query","schema":{"type":"string","enum":["true","false"]},"description":"Include logs in response ('true' to include)"}],"responses":{"200":{"description":"Successful response"}}}},"/docs":{"get":{"summary":"Get API documentation","parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","html"]},"description":"Documentation format (html for interactive UI)"}],"responses":{"200":{"description":"Successful response"}}}}},"components":{"schemas":{"Survey":{"type":"object","properties":{"id":{"type":"integer"},"status":{"type":"string","enum":["pending","completed"]},"hits":{"type":"integer"},"userId":{"type":"string"},"org":{"type":"string"},"repo":{"type":"string"},"prNumber":{"type":"integer"},"usedCopilot":{"type":"boolean"},"percentTimeSaved":{"type":"number"},"reason":{"type":"string"},"timeUsedFor":{"type":"string"}}},"NewSurvey":{"type":"object","required":["status","userId","org","repo","prNumber","usedCopilot"],"properties":{"status":{"type":"string","enum":["pending","completed"]},"userId":{"type":"string"},"org":{"type":"string"},"repo":{"type":"string"},"prNumber":{"type":"integer"},"usedCopilot":{"type":"boolean"},"percentTimeSaved":{"type":"number"},"reason":{"type":"string"},"timeUsedFor":{"type":"string"}}},"UpdateSurvey":{"type":"object","properties":{"status":{"type":"string","enum":["pending","completed"]},"usedCopilot":{"type":"boolean"},"percentTimeSaved":{"type":"number"},"reason":{"type":"string"},"timeUsedFor":{"type":"string"}}},"Metric":{"type":"object","properties":{"org":{"type":"string"},"date":{"type":"string","format":"date-time"},"completions":{"type":"integer"},"suggestions":{"type":"integer"},"acceptances":{"type":"integer"}}},"Seat":{"type":"object","properties":{"assignee_id":{"type":"integer"},"assignee_login":{"type":"string"},"last_activity_at":{"type":"string","format":"date-time"},"last_activity_editor":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"assignee":{"type":"object","properties":{"login":{"type":"string"},"id":{"type":"integer"},"avatar_url":{"type":"string"}}}}},"Team":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"slug":{"type":"string"},"description":{"type":"string"},"privacy":{"type":"string"},"members_count":{"type":"integer"}}},"Member":{"type":"object","properties":{"login":{"type":"string"},"id":{"type":"integer"},"name":{"type":"string"},"avatar_url":{"type":"string"},"team":{"type":"string"},"org":{"type":"string"},"seat":{"$ref":"#/components/schemas/Seat"}}},"Setting":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"},"secure":{"type":"boolean"}}},"TargetValues":{"type":"object","properties":{"devCostPerYear":{"type":"string"},"developerCount":{"type":"string"},"hoursPerYear":{"type":"string"},"percentTimeSaved":{"type":"string"},"percentCoding":{"type":"string"}}}}}} diff --git a/backend/src/controllers/setup.controller.ts b/backend/src/controllers/setup.controller.ts index 1b2bec97..1899fa3d 100644 --- a/backend/src/controllers/setup.controller.ts +++ b/backend/src/controllers/setup.controller.ts @@ -32,6 +32,7 @@ interface InstallationDiagnostic { octokitTest: OctokitTestResult | null; isValid: boolean; validationErrors: string[]; + repositoryCount: number; } interface AppInfo { @@ -55,6 +56,8 @@ interface DiagnosticsResponse { invalidInstallations: number; organizationNames: string[]; accountTypes: Record; + repositoryCounts: Record; + totalRepositories: number; }; } @@ -180,7 +183,9 @@ class SetupController { validInstallations: 0, invalidInstallations: 0, organizationNames: [], - accountTypes: {} + accountTypes: {}, + repositoryCounts: {}, + totalRepositories: 0 } }; @@ -213,7 +218,8 @@ class SetupController { hasOctokit: !!octokit, octokitTest: null, isValid: true, - validationErrors: [] + validationErrors: [], + repositoryCount: 0 }; // Validate required fields @@ -232,7 +238,7 @@ class SetupController { installationDiag.validationErrors.push('Missing account.type'); } - // Test Octokit functionality + // Test Octokit functionality and fetch repository count if (octokit) { try { // Test basic API call with the installation's octokit @@ -243,6 +249,16 @@ class SetupController { appOwner: (authTest.data?.owner && 'login' in authTest.data.owner) ? authTest.data.owner.login : 'Unknown', permissions: authTest.data?.permissions || {} }; + + // Fetch repositories for this installation + try { + const repos = await octokit.request(installation.repositories_url); + installationDiag.repositoryCount = repos.data.repositories?.length || 0; + } catch (repoError) { + // If repository fetching fails, log it but don't mark installation as invalid + installationDiag.validationErrors.push(`Failed to fetch repositories: ${repoError instanceof Error ? repoError.message : 'Unknown error'}`); + installationDiag.repositoryCount = 0; + } } catch (error) { installationDiag.octokitTest = { success: false, @@ -250,10 +266,12 @@ class SetupController { }; installationDiag.isValid = false; installationDiag.validationErrors.push(`Octokit API test failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + installationDiag.repositoryCount = 0; } } else { installationDiag.isValid = false; installationDiag.validationErrors.push('Octokit instance is missing'); + installationDiag.repositoryCount = 0; } // Update summary @@ -270,6 +288,11 @@ class SetupController { const accountType = installation.account?.type || 'Unknown'; diagnostics.summary.accountTypes[accountType] = (diagnostics.summary.accountTypes[accountType] || 0) + 1; + // Track repository counts per organization + const orgName = installation.account?.login || 'Unknown'; + diagnostics.summary.repositoryCounts[orgName] = installationDiag.repositoryCount; + diagnostics.summary.totalRepositories += installationDiag.repositoryCount; + diagnostics.installations.push(installationDiag); } diff --git a/frontend/src/app/main/diagnostics/main-diagnostics.component.ts b/frontend/src/app/main/diagnostics/main-diagnostics.component.ts index 2fa30939..c91658a4 100644 --- a/frontend/src/app/main/diagnostics/main-diagnostics.component.ts +++ b/frontend/src/app/main/diagnostics/main-diagnostics.component.ts @@ -41,6 +41,7 @@ import { DiagnosticsResponse } from '../../types/diagnostics.types';
  • Validating that all installation accounts have proper data
  • Testing Octokit authentication for each installation
  • Listing all organization names (account.login) available
  • +
  • Counting repositories for each GitHub App installation
  • Verifying account types and permissions
  • Providing detailed error information for troubleshooting
  • @@ -84,13 +85,19 @@ import { DiagnosticsResponse } from '../../types/diagnostics.types';
    {{ getSuccessRate() }}%
    Success Rate
    +
    +
    + {{ lastResult.summary.totalRepositories }} +
    +
    Total Repositories
    +

    Organizations Found:

    - {{ org }} + {{ org }} ({{ getRepositoryCount(org) }})
    @@ -232,6 +239,11 @@ export class MainDiagnosticsComponent { return Math.round((this.lastResult.summary.validInstallations / this.lastResult.totalInstallations) * 100); } + getRepositoryCount(orgName: string): number { + if (!this.lastResult || !this.lastResult.summary.repositoryCounts) return 0; + return this.lastResult.summary.repositoryCounts[orgName] || 0; + } + showFullDetails(): void { this.dialog.open(InstallationDiagnosticsDialogComponent, { width: '90vw', @@ -282,6 +294,7 @@ export class MainDiagnosticsComponent {

    Valid: {{ data.summary.validInstallations }}

    Invalid: {{ data.summary.invalidInstallations }}

    Success Rate: {{ getSuccessRate() }}%

    +

    Total Repositories: {{ data.summary.totalRepositories }}

    @@ -304,7 +317,7 @@ export class MainDiagnosticsComponent { - {{ org }} + {{ org }} ({{ getRepositoryCount(org) }} repos) @@ -361,6 +374,9 @@ export class MainDiagnosticsComponent {
    Has Octokit: {{ installation.hasOctokit ? 'Yes' : 'No' }}
    +
    + Repository Count: {{ installation.repositoryCount }} +
    Created: {{ installation.createdAt | date:'medium' }}
    @@ -469,6 +485,11 @@ export class InstallationDiagnosticsDialogComponent { return Math.round((this.data.summary.validInstallations / this.data.totalInstallations) * 100); } + getRepositoryCount(orgName: string): number { + if (!this.data.summary.repositoryCounts) return 0; + return this.data.summary.repositoryCounts[orgName] || 0; + } + downloadDiagnostics(): void { const dataStr = JSON.stringify(this.data, null, 2); const dataBlob = new Blob([dataStr], { type: 'application/json' }); diff --git a/frontend/src/app/types/diagnostics.types.ts b/frontend/src/app/types/diagnostics.types.ts index 63b95b2e..1319da35 100644 --- a/frontend/src/app/types/diagnostics.types.ts +++ b/frontend/src/app/types/diagnostics.types.ts @@ -27,6 +27,7 @@ export interface InstallationDiagnostic { octokitTest: OctokitTestResult | null; isValid: boolean; validationErrors: string[]; + repositoryCount: number; } export interface AppInfo { @@ -50,5 +51,7 @@ export interface DiagnosticsResponse { invalidInstallations: number; organizationNames: string[]; accountTypes: Record; + repositoryCounts: Record; + totalRepositories: number; }; }