import { createSignal, onMount } from 'solid-js'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { GitHubActivity } from '@/components/ui/GitHubActivity'; import { getApiV1BaseUrl } from '@/lib/api-url'; import { startGitHubSignIn } from '@/lib/oauth'; import { IconBrandGithub, IconTrendingUp, IconFolder, IconStar, IconGitFork, IconEye, IconExternalLink, IconRefresh, IconActivity } from '@tabler/icons-solidjs'; import { useHaptics } from '@/lib/haptics'; interface GitHubRepo { id: number; name: string; full_name: string; description: string; html_url: string; stargazers_count: number; forks_count: number; watchers_count: number; language: string; updated_at: string; created_at: string; size: number; open_issues_count: number; default_branch: string; } interface GitHubAppInstallation { installation_id: number; account_login: string; account_type: string; } interface GitHubAppStatus { app_slug: string; install_enabled: boolean; sign_in_configured: boolean; credentials_configured: boolean; installed: boolean; installation?: GitHubAppInstallation; } interface GitHubBackupRecord { id: number; repository_full_name: string; local_path: string; source: string; last_backup_status: string; last_backup_error?: string; last_backup_at?: string; last_backup_size: number; } interface GitHubBackupResult { repository: string; status: string; local_path?: string; source: string; size_bytes?: number; error?: string; } interface GitHubActivityResponse { contributions: Array<{ date: string; count: number; level: number; }>; weekly_data: number[]; total_count: number; } interface GitHubStats { totalRepos: number; totalStars: number; totalForks: number; totalWatchers: number; languages: Array<{ name: string; count: number; color: string; }>; recentActivity: Array<{ type: string; repo: string; date: string; message: string; }>; repos: GitHubRepo[]; } const API_BASE_URL = getApiV1BaseUrl(); export const GitHub = () => { const haptics = useHaptics(); const [githubStats, setGithubStats] = createSignal({ totalRepos: 0, totalStars: 0, totalForks: 0, totalWatchers: 0, languages: [], recentActivity: [], repos: [] }); const [weeklyActivity, setWeeklyActivity] = createSignal([0, 0, 0, 0, 0, 0, 0]); const [username, setUsername] = createSignal(''); const [isConnected, setIsConnected] = createSignal(false); const [appStatus, setAppStatus] = createSignal({ app_slug: '', install_enabled: false, sign_in_configured: false, credentials_configured: false, installed: false }); const [backups, setBackups] = createSignal([]); const [backupRoot, setBackupRoot] = createSignal(''); const [selectedRepos, setSelectedRepos] = createSignal([]); const [isBackingUp, setIsBackingUp] = createSignal(false); const [isInstallingApp, setIsInstallingApp] = createSignal(false); const [backupMessage, setBackupMessage] = createSignal(''); const [backupError, setBackupError] = createSignal(''); const [selectedLanguage, setSelectedLanguage] = createSignal(''); const [searchTerm, setSearchTerm] = createSignal(''); const weeklyTotal = () => weeklyActivity().reduce((a, b) => a + b, 0); const selectedCount = () => selectedRepos().length; const filteredRepos = () => { let repos = githubStats().repos; // Filter by language if (selectedLanguage()) { repos = repos.filter(repo => repo.language === selectedLanguage()); } // Filter by search term if (searchTerm()) { const term = searchTerm().toLowerCase(); repos = repos.filter(repo => repo.name.toLowerCase().includes(term) || repo.description?.toLowerCase().includes(term) || repo.language?.toLowerCase().includes(term) ); } return repos; }; const uniqueLanguages = () => { const languages = new Set(githubStats().repos.map(repo => repo.language).filter(Boolean)); return Array.from(languages).sort(); }; const getAuthToken = () => { return localStorage.getItem('trackeep_token') || localStorage.getItem('token') || ''; }; onMount(() => { const params = new URLSearchParams(window.location.search); if (params.get('github_app_installed') === '1') { setBackupMessage('GitHub App installation completed successfully.'); window.history.replaceState({}, '', window.location.pathname); } const callbackError = params.get('github_app_error'); if (callbackError) { setBackupError(`GitHub App setup failed: ${callbackError}`); window.history.replaceState({}, '', window.location.pathname); } checkGitHubConnection(); }); const resetGitHubData = () => { setWeeklyActivity([0, 0, 0, 0, 0, 0, 0]); setGithubStats({ totalRepos: 0, totalStars: 0, totalForks: 0, totalWatchers: 0, languages: [], recentActivity: [], repos: [] }); setSelectedRepos([]); }; const processLanguages = (repos: GitHubRepo[]) => { const languageMap = new Map(); repos.forEach(repo => { if (repo.language) { languageMap.set(repo.language, (languageMap.get(repo.language) || 0) + 1); } }); return Array.from(languageMap.entries()).map(([name, count]) => ({ name, count, color: getLanguageColor() })); }; const generateRecentActivity = (repos: GitHubRepo[]) => { const sortedRepos = [...repos] .sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()) .slice(0, 5); return sortedRepos.map(repo => ({ type: 'push', repo: repo.name, date: formatDate(repo.updated_at), message: `Updated ${repo.name}` })); }; const updateStatsFromRepos = (repos: GitHubRepo[]) => { const languages = processLanguages(repos); const recentActivity = generateRecentActivity(repos); const totalStars = repos.reduce((sum: number, repo: GitHubRepo) => sum + repo.stargazers_count, 0); const totalForks = repos.reduce((sum: number, repo: GitHubRepo) => sum + repo.forks_count, 0); const totalWatchers = repos.reduce((sum: number, repo: GitHubRepo) => sum + repo.watchers_count, 0); setGithubStats({ totalRepos: repos.length, totalStars, totalForks, totalWatchers, languages, recentActivity, repos }); setSelectedRepos(prev => prev.filter(repoName => repos.some(repo => repo.full_name === repoName))); }; const extractRepos = (payload: unknown): GitHubRepo[] => { if (Array.isArray(payload)) { return payload as GitHubRepo[]; } if (payload && typeof payload === 'object' && Array.isArray((payload as { repos?: unknown[] }).repos)) { return (payload as { repos: GitHubRepo[] }).repos; } return []; }; const fetchGitHubAppStatus = async (): Promise => { try { const token = getAuthToken(); if (!token) { return null; } const response = await fetch(`${API_BASE_URL}/github/app/status`, { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) { return null; } const data = (await response.json()) as GitHubAppStatus; setAppStatus({ app_slug: data.app_slug || '', install_enabled: Boolean(data.install_enabled), sign_in_configured: Boolean(data.sign_in_configured), credentials_configured: Boolean(data.credentials_configured), installed: Boolean(data.installed), installation: data.installation }); return data; } catch (error) { console.error('Failed to fetch GitHub App status:', error); return null; } }; const fetchGitHubBackups = async () => { try { const token = getAuthToken(); if (!token) { return; } const response = await fetch(`${API_BASE_URL}/github/backups`, { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) { return; } const data = await response.json(); const backupList = Array.isArray(data?.backups) ? (data.backups as GitHubBackupRecord[]) : []; setBackups(backupList); setBackupRoot(typeof data?.backup_root === 'string' ? data.backup_root : ''); } catch (error) { console.error('Failed to fetch GitHub backups:', error); } }; const fetchRepos = async (endpoint: string): Promise => { try { const token = getAuthToken(); if (!token) { throw new Error('No authentication token'); } const response = await fetch(`${API_BASE_URL}${endpoint}`, { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) { throw new Error('Failed to fetch GitHub repositories'); } const data = await response.json(); const repos = extractRepos(data); updateStatsFromRepos(repos); return true; } catch (error) { console.error('Failed to fetch GitHub repositories:', error); return false; } }; const fetchGitHubActivity = async () => { try { const token = getAuthToken(); if (!token) { return; } const response = await fetch(`${API_BASE_URL}/github/activity`, { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) { return; } const data = await response.json() as GitHubActivityResponse; setWeeklyActivity(data.weekly_data); // Update GitHubActivity component with real data updateGitHubActivityComponent(data); } catch (error) { console.error('Failed to fetch GitHub activity:', error); } }; const updateGitHubActivityComponent = (data: GitHubActivityResponse) => { // This will be used to update the GitHubActivity component // For now, we'll store the data and let the component pick it up const event = new CustomEvent('githubActivityData', { detail: data }); window.dispatchEvent(event); }; const fetchGitHubStats = async () => { const loaded = await fetchRepos('/github/repos'); if (!loaded) { resetGitHubData(); } }; const fetchGitHubAppRepos = async () => { const loaded = await fetchRepos('/github/app/repos'); if (!loaded) { resetGitHubData(); } }; const checkGitHubConnection = async () => { try { const token = getAuthToken(); if (!token) { setIsConnected(false); setUsername(''); setAppStatus({ app_slug: '', install_enabled: false, sign_in_configured: false, credentials_configured: false, installed: false }); setBackups([]); setBackupRoot(''); setBackupMessage(''); setBackupError(''); resetGitHubData(); return; } const response = await fetch(`${API_BASE_URL}/auth/me`, { headers: { 'Authorization': `Bearer ${token}` } }); const appInfo = await fetchGitHubAppStatus(); await fetchGitHubBackups(); if (!response.ok) { resetGitHubData(); return; } const userData = await response.json(); const hasGitHubSignIn = Boolean(userData?.user?.github_id); setUsername(typeof userData?.user?.username === 'string' ? userData.user.username : ''); setIsConnected(hasGitHubSignIn); if (hasGitHubSignIn) { await fetchGitHubStats(); await fetchGitHubActivity(); return; } if (appInfo?.installed && appInfo.credentials_configured) { await fetchGitHubAppRepos(); await fetchGitHubActivity(); return; } resetGitHubData(); } catch (error) { console.error('Failed to check GitHub connection:', error); resetGitHubData(); } }; const connectGitHub = () => { startGitHubSignIn(); }; const installGitHubApp = async () => { try { setIsInstallingApp(true); setBackupError(''); const token = getAuthToken(); if (!token) { throw new Error('No authentication token'); } const response = await fetch(`${API_BASE_URL}/github/app/install-url`, { headers: { 'Authorization': `Bearer ${token}` } }); const data = await response.json(); if (!response.ok || !data?.install_url) { const message = typeof data?.error === 'string' ? data.error : 'Failed to create install URL'; throw new Error(message); } window.location.href = data.install_url as string; } catch (error) { console.error('Failed to start GitHub App installation:', error); setBackupError(error instanceof Error ? error.message : 'Failed to start GitHub App installation'); haptics.warning(); } finally { setIsInstallingApp(false); } }; const toggleRepoSelection = (repoFullName: string) => { setSelectedRepos(prev => ( prev.includes(repoFullName) ? prev.filter(name => name !== repoFullName) : [...prev, repoFullName] )); }; const selectAllRepos = () => { setSelectedRepos(githubStats().repos.map(repo => repo.full_name)); }; const clearRepoSelection = () => { setSelectedRepos([]); }; const backupSelectedRepositories = async () => { try { const token = getAuthToken(); if (!token) { throw new Error('No authentication token'); } if (selectedRepos().length === 0) { return; } setIsBackingUp(true); setBackupMessage(''); setBackupError(''); const source = appStatus().installed ? 'github_app' : 'github_user'; const response = await fetch(`${API_BASE_URL}/github/backups`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ repositories: selectedRepos(), source }) }); const data = await response.json(); if (!response.ok) { const errorMessage = typeof data?.error === 'string' ? data.error : 'Backup failed'; throw new Error(errorMessage); } const results = Array.isArray(data?.results) ? (data.results as GitHubBackupResult[]) : []; const failed = Number(data?.failed ?? 0); const backedUp = Number(data?.backed_up ?? 0); const firstFailure = results.find(result => result.status !== 'success'); if (failed > 0 && firstFailure?.error) { setBackupError(firstFailure.error); } setBackupMessage(`Backups completed: ${backedUp} succeeded, ${failed} failed.`); await fetchGitHubBackups(); haptics.impact(); } catch (error) { console.error('Failed to backup repositories:', error); setBackupError(error instanceof Error ? error.message : 'Failed to backup repositories'); haptics.warning(); } finally { setIsBackingUp(false); } }; const disconnectGitHub = async () => { try { setIsConnected(false); setUsername(''); resetGitHubData(); } catch (error) { console.error('Failed to disconnect GitHub:', error); } }; const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString(); }; const getLanguageColor = () => { // Use primary color for all languages instead of language-specific colors return 'hsl(var(--primary))'; }; const getLanguageIcon = (language: string) => { const iconMap: Record = { 'Go': '🐹', 'TypeScript': '🔷', 'JavaScript': '🟨', 'CSS': '🎨', 'HTML': '🌐', 'Python': '🐍', 'Dart': '🎯', 'C++': '⚙️', 'MDX': '📝', 'Shell': '🐚', 'Rust': '🦀', 'Java': '☕', 'Ruby': '💎', 'PHP': '🐘', 'Swift': '🍎', 'Kotlin': '🎯', 'Vue': '💚', 'React': '⚛️', 'Angular': '🔺', 'Svelte': '🔥', 'Docker': '🐳', 'YAML': '📄', 'JSON': '📋', 'Markdown': '📝', 'SQL': '🗃️', 'GraphQL': '◈' }; return iconMap[language] || '📄'; }; return (
{/* Header */}

GitHub Integration

Track your GitHub repositories and activity

{isConnected() ? ( ) : ( )}
{/* Connection Status */} {(isConnected() || (appStatus().installed && appStatus().credentials_configured)) && (

{isConnected() ? `Connected via GitHub App sign-in as @${username()}` : `Connected via GitHub App installation as @${username()}`}

{isConnected() ? 'Syncing user data with your GitHub App sign-in token' : 'Syncing repositories from the GitHub App installation'}

)} {/* GitHub App Status - Only show if not installed */} {!appStatus().installed && appStatus().install_enabled && (

GitHub App Backup Access

Not installed yet

{!appStatus().sign_in_configured && (

GitHub sign-in is not available from the unified Trackeep control service.

)} {appStatus().install_enabled && !appStatus().credentials_configured && (

Unified GitHub App credentials are not configured on `hq.trackeep.org`.

)}
)} {/* Stats Overview - 2-column layout with larger left column */}
{/* Left Column - Main Stats */}

{githubStats().totalRepos}

Repositories

{githubStats().totalStars}

Total Stars

{githubStats().totalForks}

Total Forks

{githubStats().totalWatchers}

Watchers

{/* Right Column - Additional Stats */}
{/* Additional GitHub stats can go here */}

{weeklyActivity().reduce((a, b) => a + b, 0)}

Weekly Activity

{/* Two-way Grid: Contribution Graph and Languages - Responsive */}
{/* Contribution Graph - Left Column (larger) */}
{/* Languages - Right Column (smaller) */}

Languages

{githubStats().languages.length === 0 ? (

No language data yet.

) : (
{githubStats().languages.map((language) => (
{getLanguageIcon(language.name)}
{language.name}
{language.count} repos
))}
)}
{/* Weekly Activity Chart */}

Weekly Activity

{weeklyTotal() === 0 ? (

No weekly GitHub activity yet.

) : (
{['M', 'T', 'W', 'T', 'F', 'S', 'S'].map((day, index) => { const weeklyActivityData = weeklyActivity(); const activity = weeklyActivityData[index]; const maxActivity = Math.max(...weeklyActivityData, 1); const heightPercent = (activity / maxActivity) * 85; const finalHeightPercent = activity > 0 ? Math.max(heightPercent, 6) : 0; return (
{activity}
{day}
); })}
)}
Total: {weeklyTotal()} contributions Avg: {Math.round(weeklyTotal() / 7)} per day
{/* Recent Activity */}

Recent Activity

{githubStats().recentActivity.length === 0 ? (

No recent GitHub events yet.

) : (
{githubStats().recentActivity.map((activity) => (

{activity.message}

{activity.repo} • {activity.date}

{activity.type.replace('_', ' ')}
))}
)}
{/* Repositories */}

Repositories

{githubStats().repos.length > 0 && (
)}
{/* Filters */} {githubStats().repos.length > 0 && (
setSearchTerm(e.currentTarget.value)} />
{(selectedLanguage() || searchTerm()) && ( )}
)} {backupMessage() && (

{backupMessage()}

)} {backupError() && (

{backupError()}

)} {backupRoot() && (

Backup root: {backupRoot()}

)} {backups().length > 0 && (

Recent Local Backups

{backups().slice(0, 5).map((backup) => (
{backup.repository_full_name} {backup.last_backup_status}
))}
)} {githubStats().repos.length === 0 ? (

No repositories available yet.

) : (
{filteredRepos().map((repo) => (
toggleRepoSelection(repo.full_name)} onClick={(e) => e.stopPropagation()} />

{repo.name}

{repo.language && ( {getLanguageIcon(repo.language)} {repo.language} )}

{repo.description}

{repo.stargazers_count}
{repo.forks_count}
{repo.watchers_count}
Updated {formatDate(repo.updated_at)}
))}
)}
); };