Add repository link with cached star count to footer#48
Conversation
Co-authored-by: DevSecNinja <14926452+DevSecNinja@users.noreply.github.com>
Co-authored-by: DevSecNinja <14926452+DevSecNinja@users.noreply.github.com>
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
🐳 Docker Images PublishedThe following Docker images have been built and published for this PR: Backenddocker pull ghcr.io/devsecninja/hadiscover/backend:0.0.0-pr.48.86a3324Frontenddocker pull ghcr.io/devsecninja/hadiscover/frontend:0.0.0-pr.48.86a3324Test the PR build# Test backend
docker run -d -p 8000:8000 -e ENVIRONMENT=development ghcr.io/devsecninja/hadiscover/backend:0.0.0-pr.48.86a3324
curl http://localhost:8000/api/v1/health
# Test frontend
docker run -d -p 8080:80 ghcr.io/devsecninja/hadiscover/frontend:0.0.0-pr.48.86a3324
curl http://localhost:8080/hadiscover/Using docker-composeservices:
backend:
image: ghcr.io/devsecninja/hadiscover/backend:0.0.0-pr.48.86a3324
ports:
- "8000:8000"
environment:
- ENVIRONMENT=development
frontend:
image: ghcr.io/devsecninja/hadiscover/frontend:0.0.0-pr.48.86a3324
ports:
- "8080:80"Images are tagged with |
|
@copilot Can you make sure that we don't run this on every browser refresh but make it part of the indexing cron? Otherwise we'll burn through the GitHub API limit quite fast. |
Co-authored-by: DevSecNinja <14926452+DevSecNinja@users.noreply.github.com>
Done! The repository star count is now fetched and cached during the indexing cron job instead of on every browser refresh. Changes in commit
This prevents burning through the GitHub API rate limit while maintaining the same UI functionality. |
Co-authored-by: DevSecNinja <14926452+DevSecNinja@users.noreply.github.com>
🐳 Docker Images PublishedThe following Docker images have been built and published for this PR: Backenddocker pull ghcr.io/devsecninja/hadiscover/backend:0.0.0-pr.48.ed5e517Frontenddocker pull ghcr.io/devsecninja/hadiscover/frontend:0.0.0-pr.48.ed5e517Test the PR build# Test backend
docker run -d -p 8000:8000 -e ENVIRONMENT=development ghcr.io/devsecninja/hadiscover/backend:0.0.0-pr.48.ed5e517
curl http://localhost:8000/api/v1/health
# Test frontend
docker run -d -p 8080:80 ghcr.io/devsecninja/hadiscover/frontend:0.0.0-pr.48.ed5e517
curl http://localhost:8080/hadiscover/Using docker-composeservices:
backend:
image: ghcr.io/devsecninja/hadiscover/backend:0.0.0-pr.48.ed5e517
ports:
- "8000:8000"
environment:
- ENVIRONMENT=development
frontend:
image: ghcr.io/devsecninja/hadiscover/frontend:0.0.0-pr.48.ed5e517
ports:
- "8080:80"Images are tagged with |
🐳 Docker Images PublishedThe following Docker images have been built and published for this PR: Backenddocker pull ghcr.io/devsecninja/hadiscover/backend:0.0.0-pr.48.7dede89Frontenddocker pull ghcr.io/devsecninja/hadiscover/frontend:0.0.0-pr.48.7dede89Test the PR build# Test backend
docker run -d -p 8000:8000 -e ENVIRONMENT=development ghcr.io/devsecninja/hadiscover/backend:0.0.0-pr.48.7dede89
curl http://localhost:8000/api/v1/health
# Test frontend
docker run -d -p 8080:80 ghcr.io/devsecninja/hadiscover/frontend:0.0.0-pr.48.7dede89
curl http://localhost:8080/hadiscover/Using docker-composeservices:
backend:
image: ghcr.io/devsecninja/hadiscover/backend:0.0.0-pr.48.7dede89
ports:
- "8000:8000"
environment:
- ENVIRONMENT=development
frontend:
image: ghcr.io/devsecninja/hadiscover/frontend:0.0.0-pr.48.7dede89
ports:
- "8080:80"Images are tagged with |
🐳 Docker Images PublishedThe following Docker images have been built and published for this PR: Backenddocker pull ghcr.io/devsecninja/hadiscover/backend:0.0.0-pr.48.c747387Frontenddocker pull ghcr.io/devsecninja/hadiscover/frontend:0.0.0-pr.48.c747387Test the PR build# Test backend
docker run -d -p 8000:8000 -e ENVIRONMENT=development ghcr.io/devsecninja/hadiscover/backend:0.0.0-pr.48.c747387
curl http://localhost:8000/api/v1/health
# Test frontend
docker run -d -p 8080:80 ghcr.io/devsecninja/hadiscover/frontend:0.0.0-pr.48.c747387
curl http://localhost:8080/hadiscover/Using docker-composeservices:
backend:
image: ghcr.io/devsecninja/hadiscover/backend:0.0.0-pr.48.c747387
ports:
- "8000:8000"
environment:
- ENVIRONMENT=development
frontend:
image: ghcr.io/devsecninja/hadiscover/frontend:0.0.0-pr.48.c747387
ports:
- "8080:80"Images are tagged with |
There was a problem hiding this comment.
Pull request overview
This PR adds a GitHub repository link with a cached star count to the footer of the hadiscover application. The star count is fetched during the indexing cron job and stored in the database to avoid GitHub API rate limit exhaustion on page loads.
Key Changes:
- Backend caches repository star count during indexing runs via new
_store_repo_star_count()method - API returns star count through existing
/api/v1/statisticsendpoint with updatedStatisticsResponseschema - Frontend displays GitHub repository link with star count and call-to-action in footer
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/app/services/indexer.py | Added _store_repo_star_count() method to fetch and cache hadiscover repository star count from GitHub API during successful indexing runs |
| backend/app/services/search_service.py | Updated get_statistics() to retrieve cached repo_star_count from database metadata table |
| backend/app/api/routes.py | Added repo_star_count field to StatisticsResponse model to expose star count via API |
| backend/tests/test_api.py | Added test case to verify statistics endpoint includes the new repo_star_count field |
| frontend/app/page.tsx | Added repository link component with GitHub logo, star count, and hover effects; also changed time format to 24-hour |
| .first() | ||
| ) | ||
|
|
||
| current_time = datetime.utcnow() |
There was a problem hiding this comment.
Using deprecated datetime.utcnow() which will be removed in future Python versions. Consider using datetime.now(timezone.utc) instead for timezone-aware datetime objects.
| async def _store_repo_star_count(self, db: Session) -> None: | ||
| """ | ||
| Fetch and store the hadiscover repository star count. | ||
|
|
||
| Args: | ||
| db: Database session | ||
| """ | ||
| try: | ||
| async with httpx.AsyncClient() as client: | ||
| url = f"{self.github_service.BASE_URL}/repos/DevSecNinja/hadiscover" | ||
| response = await client.get( | ||
| url, headers=self.github_service.headers, timeout=10.0 | ||
| ) | ||
|
|
||
| if response.status_code == 200: | ||
| data = response.json() | ||
| star_count = data.get("stargazers_count", 0) | ||
|
|
||
| # Check if metadata record exists | ||
| metadata = ( | ||
| db.query(IndexingMetadata) | ||
| .filter_by(key="repo_star_count") | ||
| .first() | ||
| ) | ||
|
|
||
| current_time = datetime.utcnow() | ||
|
|
||
| if metadata: | ||
| # Update existing record | ||
| metadata.value = str(star_count) | ||
| metadata.updated_at = current_time | ||
| else: | ||
| # Create new record | ||
| metadata = IndexingMetadata( | ||
| key="repo_star_count", | ||
| value=str(star_count), | ||
| updated_at=current_time, | ||
| ) | ||
| db.add(metadata) | ||
|
|
||
| db.commit() | ||
| logger.info(f"Stored repository star count: {star_count}") | ||
| else: | ||
| logger.warning( | ||
| f"Failed to fetch repo star count. Status: {response.status_code}" | ||
| ) | ||
| except Exception as e: | ||
| logger.error(f"Error storing repo star count: {e}") | ||
| db.rollback() |
There was a problem hiding this comment.
The _store_repo_star_count method lacks test coverage. While there is a test for the API endpoint that returns the star count, there are no unit tests for the actual fetching and storing logic. Consider adding tests similar to those in test_indexing_completion.py that verify:
- Successful star count fetch and storage
- Behavior when GitHub API returns non-200 status codes
- Behavior when GitHub API times out or returns invalid data
- Updates to existing star count records on subsequent runs
| <a | ||
| href="https://github.com/DevSecNinja/hadiscover" | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="inline-flex items-center gap-2 px-5 py-3 rounded-xl transition-all duration-200" | ||
| style={{ | ||
| color: isDark ? "rgba(255, 255, 255, 0.7)" : "#1f2937", | ||
| background: isDark | ||
| ? "rgba(255, 255, 255, 0.05)" | ||
| : "rgba(0, 0, 0, 0.04)", | ||
| border: isDark | ||
| ? "1px solid rgba(255, 255, 255, 0.08)" | ||
| : "1px solid rgba(0, 0, 0, 0.08)", | ||
| }} | ||
| onMouseEnter={(e) => { | ||
| e.currentTarget.style.background = isDark | ||
| ? "rgba(255, 255, 255, 0.08)" | ||
| : "rgba(0, 0, 0, 0.06)"; | ||
| e.currentTarget.style.transform = "translateY(-1px)"; | ||
| }} | ||
| onMouseLeave={(e) => { | ||
| e.currentTarget.style.background = isDark | ||
| ? "rgba(255, 255, 255, 0.05)" | ||
| : "rgba(0, 0, 0, 0.04)"; | ||
| e.currentTarget.style.transform = "translateY(0)"; | ||
| }} | ||
| > |
There was a problem hiding this comment.
This repository link is hardcoded. If someone forks this project, they would need to manually update this URL. Consider making the repository owner/name configurable via environment variables (e.g., NEXT_PUBLIC_REPO_OWNER and NEXT_PUBLIC_REPO_NAME) or fetching it from the statistics endpoint, which already knows the repository name from the backend configuration.
| <a | |
| href="https://github.com/DevSecNinja/hadiscover" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="inline-flex items-center gap-2 px-5 py-3 rounded-xl transition-all duration-200" | |
| style={{ | |
| color: isDark ? "rgba(255, 255, 255, 0.7)" : "#1f2937", | |
| background: isDark | |
| ? "rgba(255, 255, 255, 0.05)" | |
| : "rgba(0, 0, 0, 0.04)", | |
| border: isDark | |
| ? "1px solid rgba(255, 255, 255, 0.08)" | |
| : "1px solid rgba(0, 0, 0, 0.08)", | |
| }} | |
| onMouseEnter={(e) => { | |
| e.currentTarget.style.background = isDark | |
| ? "rgba(255, 255, 255, 0.08)" | |
| : "rgba(0, 0, 0, 0.06)"; | |
| e.currentTarget.style.transform = "translateY(-1px)"; | |
| }} | |
| onMouseLeave={(e) => { | |
| e.currentTarget.style.background = isDark | |
| ? "rgba(255, 255, 255, 0.05)" | |
| : "rgba(0, 0, 0, 0.04)"; | |
| e.currentTarget.style.transform = "translateY(0)"; | |
| }} | |
| > | |
| {(() => { | |
| const repoOwner = | |
| process.env.NEXT_PUBLIC_REPO_OWNER ?? "DevSecNinja"; | |
| const repoName = | |
| process.env.NEXT_PUBLIC_REPO_NAME ?? "hadiscover"; | |
| const repositoryUrl = `https://github.com/${repoOwner}/${repoName}`; | |
| return ( | |
| <a | |
| href={repositoryUrl} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="inline-flex items-center gap-2 px-5 py-3 rounded-xl transition-all duration-200" | |
| style={{ | |
| color: isDark ? "rgba(255, 255, 255, 0.7)" : "#1f2937", | |
| background: isDark | |
| ? "rgba(255, 255, 255, 0.05)" | |
| : "rgba(0, 0, 0, 0.04)", | |
| border: isDark | |
| ? "1px solid rgba(255, 255, 255, 0.08)" | |
| : "1px solid rgba(0, 0, 0, 0.08)", | |
| }} | |
| onMouseEnter={(e) => { | |
| e.currentTarget.style.background = isDark | |
| ? "rgba(255, 255, 255, 0.08)" | |
| : "rgba(0, 0, 0, 0.06)"; | |
| e.currentTarget.style.transform = "translateY(-1px)"; | |
| }} | |
| onMouseLeave={(e) => { | |
| e.currentTarget.style.background = isDark | |
| ? "rgba(255, 255, 255, 0.05)" | |
| : "rgba(0, 0, 0, 0.04)"; | |
| e.currentTarget.style.transform = "translateY(0)"; | |
| }} | |
| > |
| clipRule="evenodd" | ||
| /> | ||
| </svg> | ||
| <span className="font-medium">DevSecNinja/hadiscover</span> |
There was a problem hiding this comment.
The repository name "DevSecNinja/hadiscover" is hardcoded in the display text. Similar to the URL, this should be made configurable to support forks of the project.
| star_count_metadata = ( | ||
| db.query(IndexingMetadata).filter_by(key="repo_star_count").first() | ||
| ) | ||
| star_count = int(star_count_metadata.value) if star_count_metadata else 0 |
There was a problem hiding this comment.
The int() conversion could raise a ValueError if the stored value is not a valid integer (e.g., corrupted data). While the outer try-except will catch this, it would be better to handle this specific case more gracefully with a try-except around the conversion or use a default value for invalid data, logging a warning about the corrupted metadata.
| star_count = int(star_count_metadata.value) if star_count_metadata else 0 | |
| star_count = 0 | |
| if star_count_metadata: | |
| try: | |
| star_count = int(star_count_metadata.value) | |
| except (TypeError, ValueError): | |
| logger.warning( | |
| "Invalid repo_star_count metadata value %r; defaulting to 0", | |
| star_count_metadata.value, | |
| ) |
| """ | ||
| try: | ||
| async with httpx.AsyncClient() as client: | ||
| url = f"{self.github_service.BASE_URL}/repos/DevSecNinja/hadiscover" |
There was a problem hiding this comment.
The repository owner/name is hardcoded as "DevSecNinja/hadiscover". This makes it difficult for forks to use this feature. Consider making this configurable via an environment variable (e.g., HADISCOVER_REPO_OWNER and HADISCOVER_REPO_NAME) so that forks can easily update the configuration without modifying code.
| onMouseEnter={(e) => { | ||
| e.currentTarget.style.background = isDark | ||
| ? "rgba(255, 255, 255, 0.08)" | ||
| : "rgba(0, 0, 0, 0.06)"; | ||
| e.currentTarget.style.transform = "translateY(-1px)"; | ||
| }} | ||
| onMouseLeave={(e) => { | ||
| e.currentTarget.style.background = isDark | ||
| ? "rgba(255, 255, 255, 0.05)" | ||
| : "rgba(0, 0, 0, 0.04)"; | ||
| e.currentTarget.style.transform = "translateY(0)"; | ||
| }} |
There was a problem hiding this comment.
Inline event handlers with style mutations can cause performance issues in React and make the component harder to test. Consider using CSS hover states via className instead, or extract this into a reusable styled component. This would also improve consistency with the rest of the application's styling approach.
| day: "numeric", | ||
| hour: "2-digit", | ||
| minute: "2-digit", | ||
| hour12: false, |
There was a problem hiding this comment.
This change to use 24-hour time format appears unrelated to the main PR purpose of adding repository link and star count. While not problematic, it should ideally be in a separate commit or PR for better traceability. Additionally, this changes the user experience for users in locales that typically use 12-hour format.
| total_repositories: int | ||
| total_automations: int | ||
| last_indexed_at: Optional[str] = None | ||
| repo_star_count: int |
There was a problem hiding this comment.
The repo_star_count field lacks a docstring or description comment. Consider adding a brief comment explaining what this field represents (e.g., "Star count of the hadiscover GitHub repository") to improve API documentation clarity, especially since this is exposed through the OpenAPI/Swagger docs.
| async with httpx.AsyncClient() as client: | ||
| url = f"{self.github_service.BASE_URL}/repos/DevSecNinja/hadiscover" | ||
| response = await client.get( | ||
| url, headers=self.github_service.headers, timeout=10.0 | ||
| ) | ||
|
|
||
| if response.status_code == 200: | ||
| data = response.json() | ||
| star_count = data.get("stargazers_count", 0) | ||
|
|
||
| # Check if metadata record exists | ||
| metadata = ( | ||
| db.query(IndexingMetadata) | ||
| .filter_by(key="repo_star_count") | ||
| .first() | ||
| ) | ||
|
|
||
| current_time = datetime.utcnow() | ||
|
|
||
| if metadata: | ||
| # Update existing record | ||
| metadata.value = str(star_count) | ||
| metadata.updated_at = current_time | ||
| else: | ||
| # Create new record | ||
| metadata = IndexingMetadata( | ||
| key="repo_star_count", | ||
| value=str(star_count), | ||
| updated_at=current_time, | ||
| ) | ||
| db.add(metadata) | ||
|
|
||
| db.commit() | ||
| logger.info(f"Stored repository star count: {star_count}") | ||
| else: | ||
| logger.warning( | ||
| f"Failed to fetch repo star count. Status: {response.status_code}" | ||
| ) |
There was a problem hiding this comment.
This code doesn't use the existing GitHubService._check_rate_limit() method to check for rate limit responses. While the outer error handling will catch rate limit errors, it would be more consistent to use the existing rate limit checking infrastructure. Consider either:
- Creating a method in
GitHubServicefor fetching repository metadata (recommended for consistency) - Extracting and calling
_check_rate_limit()after the API call
This would provide better error handling and consistency with other GitHub API calls in the codebase.
Adds a link to the hadiscover GitHub repository in the footer, displaying the current star count and encouraging users to star the project.
Changes
Backend
backend/app/services/indexer.py: Added_store_repo_star_count()method to fetch and cache repository star count during indexing runsbackend/app/services/search_service.py: Updatedget_statistics()to return cachedrepo_star_countfrom databasebackend/app/api/routes.py: UpdatedStatisticsResponseto includerepo_star_countfieldFrontend (
frontend/app/page.tsx)Tests (
backend/tests/test_api.py)test_statistics_includes_star_count()to validate star count in statistics responseImplementation Details
The repository star count is fetched and cached during the indexing cron job (not on every page load) to prevent GitHub API rate limit exhaustion. The star count is stored in the
indexing_metadatatable and returned via the existing/api/v1/statisticsendpoint.Benefits:
Screenshot
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.