Skip to content

Commit 00db2dc

Browse files
feat: implement smart polling and webhook instructions UI
1 parent c1b92e2 commit 00db2dc

4 files changed

Lines changed: 91 additions & 23 deletions

File tree

backend/src/modules/github/github.service.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,16 @@ export class GitHubService {
194194
/**
195195
* Fetch a single repository using GraphQL.
196196
*/
197-
async fetchSingleRepo(repoName: string): Promise<GitHubGraphQLRepo | null> {
197+
async fetchSingleRepo(
198+
repoName: string,
199+
owner?: string,
200+
): Promise<GitHubGraphQLRepo | null> {
198201
this.checkCircuitBreaker();
202+
const repoOwner = owner || this.org;
199203

200204
try {
201205
const query = `query {
202-
repository(owner: "${this.org}", name: "${repoName}") {
206+
repository(owner: "${repoOwner}", name: "${repoName}") {
203207
id
204208
databaseId
205209
name
@@ -235,11 +239,15 @@ export class GitHubService {
235239
/**
236240
* Fetch contributors for a repository using REST API with ETag caching.
237241
*/
238-
async fetchContributors(repoName: string): Promise<GitHubContributor[]> {
242+
async fetchContributors(
243+
repoName: string,
244+
owner?: string,
245+
): Promise<GitHubContributor[]> {
239246
this.checkCircuitBreaker();
247+
const repoOwner = owner || this.org;
240248

241-
const etagKey = `etag:contributors:${this.org}/${repoName}`;
242-
const dataKey = `contributors:data:${this.org}/${repoName}`;
249+
const etagKey = `etag:contributors:${repoOwner}/${repoName}`;
250+
const dataKey = `contributors:data:${repoOwner}/${repoName}`;
243251

244252
try {
245253
// Check for stored ETag
@@ -250,7 +258,7 @@ export class GitHubService {
250258
}
251259

252260
const response = await this.restClient.repos.listContributors({
253-
owner: this.org,
261+
owner: repoOwner,
254262
repo: repoName,
255263
per_page: 100,
256264
headers,
@@ -307,11 +315,15 @@ export class GitHubService {
307315
/**
308316
* Fetch languages for a repository using REST API with ETag caching.
309317
*/
310-
async fetchLanguages(repoName: string): Promise<GitHubLanguages> {
318+
async fetchLanguages(
319+
repoName: string,
320+
owner?: string,
321+
): Promise<GitHubLanguages> {
311322
this.checkCircuitBreaker();
323+
const repoOwner = owner || this.org;
312324

313-
const etagKey = `etag:languages:${this.org}/${repoName}`;
314-
const dataKey = `languages:data:${this.org}/${repoName}`;
325+
const etagKey = `etag:languages:${repoOwner}/${repoName}`;
326+
const dataKey = `languages:data:${repoOwner}/${repoName}`;
315327

316328
try {
317329
const storedEtag = await this.cacheService.get<string>(etagKey);
@@ -321,7 +333,7 @@ export class GitHubService {
321333
}
322334

323335
const response = await this.restClient.repos.listLanguages({
324-
owner: this.org,
336+
owner: repoOwner,
325337
repo: repoName,
326338
headers,
327339
});
@@ -362,12 +374,16 @@ export class GitHubService {
362374
/**
363375
* Fetch commit activity (52 weeks) for a repository.
364376
*/
365-
async fetchCommitActivity(repoName: string): Promise<GitHubCommitActivity[]> {
377+
async fetchCommitActivity(
378+
repoName: string,
379+
owner?: string,
380+
): Promise<GitHubCommitActivity[]> {
366381
this.checkCircuitBreaker();
382+
const repoOwner = owner || this.org;
367383

368384
try {
369385
const response = await this.restClient.repos.getCommitActivityStats({
370-
owner: this.org,
386+
owner: repoOwner,
371387
repo: repoName,
372388
});
373389

backend/src/modules/sync/sync.processor.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,34 @@ export class SyncProcessor {
2424

2525
@Process('sync-repo')
2626
async handleSyncJob(job: Job<SyncJobPayload>): Promise<void> {
27-
const { repoName, repoFullName, eventType } = job.data;
27+
const { repoName: jobRepoName, repoFullName, eventType } = job.data;
2828
const org = this.configService.get<string>('GITHUB_ORG', 'c2siorg');
29+
30+
// Parse owner and repoName from fullName
31+
const [owner, name] = repoFullName.includes('/')
32+
? repoFullName.split('/')
33+
: [org, jobRepoName];
34+
2935
this.logger.log(
30-
`Processing sync job: ${repoFullName} (event: ${eventType})`,
36+
`Processing sync job: ${owner}/${name} (event: ${eventType})`,
3137
);
3238

3339
try {
3440
// 1. Fetch repo metadata via GraphQL
35-
const repoData = await this.github.fetchSingleRepo(repoName);
41+
const repoData = await this.github.fetchSingleRepo(name, owner);
3642
if (!repoData) {
37-
this.logger.warn(`Repo "${repoName}" not found on GitHub — skipping`);
43+
this.logger.warn(`Repo "${owner}/${name}" not found on GitHub — skipping`);
3844
return;
3945
}
4046

4147
// 2. Fetch contributors via REST (ETag cached)
42-
const contributors = await this.github.fetchContributors(repoName);
48+
const contributors = await this.github.fetchContributors(name, owner);
4349

4450
// 3. Fetch languages via REST (ETag cached)
45-
const languages = await this.github.fetchLanguages(repoName);
51+
const languages = await this.github.fetchLanguages(name, owner);
4652

4753
// 4. Fetch commit activity via REST
48-
const activity = await this.github.fetchCommitActivity(repoName);
54+
const activity = await this.github.fetchCommitActivity(name, owner);
4955

5056
// 5. Process all data
5157
const repoUpsert = this.processing.processRepository(repoData);

frontend/src/app/features/repository-list/repository-list.component.html

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,38 @@
3737
{{ analyzing ? 'Queuing...' : 'Analyze' }}
3838
</button>
3939
</section>
40-
<div class="analyze-message" *ngIf="analyzeMessage">
41-
<mat-icon>info</mat-icon>
42-
{{ analyzeMessage }}
40+
<div class="analyze-actions">
41+
<div class="analyze-message" *ngIf="analyzeMessage">
42+
<mat-icon>info</mat-icon>
43+
{{ analyzeMessage }}
44+
</div>
45+
<div class="spacer"></div>
46+
<button mat-button class="webhook-toggle" (click)="toggleWebhookInfo()">
47+
<mat-icon>{{ showWebhookInfo ? 'expand_less' : 'add_link' }}</mat-icon>
48+
{{ showWebhookInfo ? 'Hide Instructions' : 'Add via Webhook' }}
49+
</button>
50+
</div>
51+
52+
<!-- Webhook Instructions Panel -->
53+
<div class="webhook-panel mat-elevation-z2" *ngIf="showWebhookInfo">
54+
<div class="panel-header">
55+
<mat-icon>webhook</mat-icon>
56+
<h3>Configure GitHub Webhook</h3>
57+
</div>
58+
<div class="panel-body">
59+
<p>To enable real-time updates for your repository, add a webhook in your GitHub Repository settings:</p>
60+
<ol>
61+
<li>Go to <strong>Settings</strong> > <strong>Webhooks</strong> > <strong>Add webhook</strong>.</li>
62+
<li><strong>Payload URL:</strong> <code>https://repo-arg-backend.onrender.com/api/webhooks/github</code></li>
63+
<li><strong>Content type:</strong> <code>application/json</code></li>
64+
<li><strong>Secret:</strong> <em>(Provided during setup)</em></li>
65+
<li><strong>Events:</strong> Select <code>Just the push event</code> or <code>Send me everything</code>.</li>
66+
</ol>
67+
<div class="info-note">
68+
<mat-icon>lightbulb</mat-icon>
69+
<span>Once configured, any push or activity will automatically update the RepoArg dashboard in real-time.</span>
70+
</div>
71+
</div>
4372
</div>
4473

4574
<!-- Filter & Search Section -->

frontend/src/app/features/repository-list/repository-list.component.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
88
import { MatSelectModule } from '@angular/material/select';
99
import { MatIconModule } from '@angular/material/icon';
1010
import { MatButtonModule } from '@angular/material/button';
11-
import { Subject, takeUntil, debounceTime, distinctUntilChanged } from 'rxjs';
11+
import { Subject, takeUntil, debounceTime, distinctUntilChanged, timer, take } from 'rxjs';
1212
import { RepositoryService, Repository, StatsResponse } from '../../core/services/repository.service';
1313
import { RelativeTimePipe } from '../../shared/pipes/relative-time.pipe';
1414
import { NumberFormatPipe } from '../../shared/pipes/number-format.pipe';
@@ -50,6 +50,7 @@ export class RepositoryListComponent implements OnInit, OnDestroy {
5050

5151
analyzing = false;
5252
analyzeMessage = '';
53+
showWebhookInfo = false;
5354

5455
private destroy$ = new Subject<void>();
5556
readonly SKELETON_ITEMS = Array(12).fill(0);
@@ -170,6 +171,10 @@ export class RepositoryListComponent implements OnInit, OnDestroy {
170171
this.router.navigate(['/repo', id]);
171172
}
172173

174+
toggleWebhookInfo(): void {
175+
this.showWebhookInfo = !this.showWebhookInfo;
176+
}
177+
173178
analyzeRepos(): void {
174179
const urls = this.analyzeControl.value?.split(',').map(url => url.trim()).filter(url => url);
175180
if (!urls || urls.length === 0) return;
@@ -182,6 +187,7 @@ export class RepositoryListComponent implements OnInit, OnDestroy {
182187
this.analyzing = false;
183188
this.analyzeMessage = res.message;
184189
this.analyzeControl.setValue('');
190+
this.startPolling();
185191
setTimeout(() => this.analyzeMessage = '', 5000);
186192
},
187193
error: () => {
@@ -191,4 +197,15 @@ export class RepositoryListComponent implements OnInit, OnDestroy {
191197
}
192198
});
193199
}
200+
201+
startPolling(): void {
202+
// Poll every 5 seconds, up to 12 times (1 minute total)
203+
timer(5000, 5000).pipe(
204+
take(12),
205+
takeUntil(this.destroy$)
206+
).subscribe(() => {
207+
this.loadStats();
208+
this.loadRepositories();
209+
});
210+
}
194211
}

0 commit comments

Comments
 (0)