mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-07-29 14:03:51 +00:00
feat: migrate to DragonflyDB and clean up environment configuration
- Replace Redis with DragonflyDB for better performance and memory efficiency - Remove redundant environment variables (POSTGRES_*, ENCRYPTION_KEY, OAUTH_SERVICE_URL) - Consolidate database configuration to use single DB_* variables - Use JWT_SECRET for both JWT tokens and encryption - Remove PORT variable redundancy, use BACKEND_PORT consistently - Clean up docker-compose configurations for dev/prod consistency - Add DragonflyDB configuration with optimized memory usage - Remove redis.conf as it's no longer needed - Update health checks to use Redis-compatible CLI for DragonflyDB - Add missing VITE_API_URL to production frontend - Fix GitHub Actions to use correct go.sum path - Clean up development directories and unused files
This commit is contained in:
+470
-101
@@ -3,6 +3,7 @@ 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 { startGitHubOAuth } from '@/lib/oauth';
|
||||
import {
|
||||
IconBrandGithub,
|
||||
IconTrendingUp,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
IconRefresh,
|
||||
IconActivity
|
||||
} from '@tabler/icons-solidjs';
|
||||
import { useHaptics } from '@/lib/haptics';
|
||||
|
||||
interface GitHubRepo {
|
||||
id: number;
|
||||
@@ -32,6 +34,40 @@ interface GitHubRepo {
|
||||
default_branch: string;
|
||||
}
|
||||
|
||||
interface GitHubAppInstallation {
|
||||
installation_id: number;
|
||||
account_login: string;
|
||||
account_type: string;
|
||||
}
|
||||
|
||||
interface GitHubAppStatus {
|
||||
app_slug: string;
|
||||
install_enabled: 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 GitHubStats {
|
||||
totalRepos: number;
|
||||
totalStars: number;
|
||||
@@ -54,6 +90,7 @@ interface GitHubStats {
|
||||
const API_BASE_URL = getApiV1BaseUrl();
|
||||
|
||||
export const GitHub = () => {
|
||||
const haptics = useHaptics();
|
||||
const [githubStats, setGithubStats] = createSignal<GitHubStats>({
|
||||
totalRepos: 0,
|
||||
totalStars: 0,
|
||||
@@ -65,13 +102,40 @@ export const GitHub = () => {
|
||||
});
|
||||
|
||||
const [weeklyActivity, setWeeklyActivity] = createSignal([0, 0, 0, 0, 0, 0, 0]);
|
||||
|
||||
const [username, setUsername] = createSignal('');
|
||||
const [isConnected, setIsConnected] = createSignal(false);
|
||||
const [appStatus, setAppStatus] = createSignal<GitHubAppStatus>({
|
||||
app_slug: '',
|
||||
install_enabled: false,
|
||||
credentials_configured: false,
|
||||
installed: false
|
||||
});
|
||||
const [backups, setBackups] = createSignal<GitHubBackupRecord[]>([]);
|
||||
const [backupRoot, setBackupRoot] = createSignal('');
|
||||
const [selectedRepos, setSelectedRepos] = createSignal<string[]>([]);
|
||||
const [isBackingUp, setIsBackingUp] = createSignal(false);
|
||||
const [isInstallingApp, setIsInstallingApp] = createSignal(false);
|
||||
const [backupMessage, setBackupMessage] = createSignal('');
|
||||
const [backupError, setBackupError] = createSignal('');
|
||||
|
||||
const weeklyTotal = () => weeklyActivity().reduce((a, b) => a + b, 0);
|
||||
const selectedCount = () => selectedRepos().length;
|
||||
|
||||
const getAuthToken = () => {
|
||||
return localStorage.getItem('trackeep_token') || localStorage.getItem('token') || '';
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
// Check if user is authenticated and has GitHub connected
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -86,86 +150,12 @@ export const GitHub = () => {
|
||||
recentActivity: [],
|
||||
repos: []
|
||||
});
|
||||
};
|
||||
|
||||
const checkGitHubConnection = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('trackeep_token') || localStorage.getItem('token');
|
||||
if (!token) {
|
||||
resetGitHubData();
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/auth/me`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const userData = await response.json();
|
||||
if (userData.user.github_id) {
|
||||
setIsConnected(true);
|
||||
setUsername(userData.user.username);
|
||||
await fetchGitHubStats();
|
||||
} else {
|
||||
resetGitHubData();
|
||||
}
|
||||
} else {
|
||||
resetGitHubData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check GitHub connection:', error);
|
||||
resetGitHubData();
|
||||
}
|
||||
};
|
||||
const fetchGitHubStats = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('trackeep_token') || localStorage.getItem('token');
|
||||
if (!token) {
|
||||
throw new Error('No authentication token');
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/github/repos`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch GitHub stats');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const repos = Array.isArray(data) ? data : (Array.isArray(data?.repos) ? data.repos : []);
|
||||
|
||||
// Process real GitHub data
|
||||
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
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch GitHub stats:', error);
|
||||
resetGitHubData();
|
||||
}
|
||||
setSelectedRepos([]);
|
||||
};
|
||||
|
||||
const processLanguages = (repos: GitHubRepo[]) => {
|
||||
const languageMap = new Map<string, number>();
|
||||
|
||||
|
||||
repos.forEach(repo => {
|
||||
if (repo.language) {
|
||||
languageMap.set(repo.language, (languageMap.get(repo.language) || 0) + 1);
|
||||
@@ -180,8 +170,7 @@ export const GitHub = () => {
|
||||
};
|
||||
|
||||
const generateRecentActivity = (repos: GitHubRepo[]) => {
|
||||
// Sort repos by updated_at and take recent ones
|
||||
const sortedRepos = repos
|
||||
const sortedRepos = [...repos]
|
||||
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
|
||||
.slice(0, 5);
|
||||
|
||||
@@ -193,15 +182,291 @@ export const GitHub = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
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<GitHubAppStatus | null> => {
|
||||
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),
|
||||
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<boolean> => {
|
||||
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 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,
|
||||
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 hasOAuthConnection = Boolean(userData?.user?.github_id);
|
||||
setUsername(typeof userData?.user?.username === 'string' ? userData.user.username : '');
|
||||
setIsConnected(hasOAuthConnection);
|
||||
|
||||
if (hasOAuthConnection) {
|
||||
await fetchGitHubStats();
|
||||
return;
|
||||
}
|
||||
|
||||
if (appInfo?.installed && appInfo.credentials_configured) {
|
||||
await fetchGitHubAppRepos();
|
||||
return;
|
||||
}
|
||||
|
||||
resetGitHubData();
|
||||
} catch (error) {
|
||||
console.error('Failed to check GitHub connection:', error);
|
||||
resetGitHubData();
|
||||
}
|
||||
};
|
||||
|
||||
const connectGitHub = () => {
|
||||
// Redirect to centralized OAuth service
|
||||
window.location.href = 'https://oauth.trackeep.org/auth/github?redirect_uri=' + encodeURIComponent(window.location.origin + '/api/v1/auth/oauth/callback');
|
||||
startGitHubOAuth();
|
||||
};
|
||||
|
||||
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' : 'oauth';
|
||||
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 {
|
||||
// In a real implementation, you might want to disconnect the GitHub account
|
||||
// For now, we'll just clear the local state
|
||||
setIsConnected(false);
|
||||
setUsername('');
|
||||
resetGitHubData();
|
||||
@@ -228,18 +493,25 @@ export const GitHub = () => {
|
||||
<p class="text-muted-foreground mt-2">Track your GitHub repositories and activity</p>
|
||||
</div>
|
||||
<div class="flex gap-2 flex-shrink-0">
|
||||
<Button variant="outline" size="sm" onClick={() => {
|
||||
checkGitHubConnection();
|
||||
haptics.selection();
|
||||
}}>
|
||||
<IconRefresh class="size-4 mr-2" />
|
||||
Refresh
|
||||
</Button>
|
||||
{isConnected() ? (
|
||||
<>
|
||||
<Button variant="outline" size="sm" onClick={() => fetchGitHubStats()}>
|
||||
<IconRefresh class="size-4 mr-2" />
|
||||
Refresh
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={disconnectGitHub}>
|
||||
Disconnect
|
||||
</Button>
|
||||
</>
|
||||
<Button variant="outline" size="sm" onClick={() => {
|
||||
disconnectGitHub();
|
||||
haptics.warning();
|
||||
}}>
|
||||
Disconnect
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={connectGitHub}>
|
||||
<Button onClick={() => {
|
||||
connectGitHub();
|
||||
haptics.impact();
|
||||
}}>
|
||||
<IconBrandGithub class="size-4 mr-2" />
|
||||
Connect GitHub
|
||||
</Button>
|
||||
@@ -248,20 +520,61 @@ export const GitHub = () => {
|
||||
</div>
|
||||
|
||||
{/* Connection Status */}
|
||||
{isConnected() && (
|
||||
{(isConnected() || (appStatus().installed && appStatus().credentials_configured)) && (
|
||||
<Card class="p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="bg-primary/10 flex items-center justify-center p-2 rounded-lg">
|
||||
<IconBrandGithub class="size-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Connected as @{username()}</p>
|
||||
<p class="text-xs text-muted-foreground">Syncing data from GitHub API</p>
|
||||
<p class="text-sm font-medium text-foreground">
|
||||
{isConnected() ? `Connected via OAuth as @${username()}` : `Connected via GitHub App as @${username()}`}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{isConnected() ? 'Syncing data from GitHub OAuth API' : 'Syncing data from GitHub App installation'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* GitHub App Status */}
|
||||
<Card class="p-4">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">GitHub App Backup Access</p>
|
||||
{appStatus().install_enabled ? (
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
{appStatus().installed
|
||||
? `Installed${appStatus().installation?.account_login ? ` for ${appStatus().installation?.account_login}` : ''}`
|
||||
: 'Not installed yet'}
|
||||
</p>
|
||||
) : (
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
GitHub App install is not configured on this server.
|
||||
</p>
|
||||
)}
|
||||
{appStatus().install_enabled && !appStatus().credentials_configured && (
|
||||
<p class="text-xs text-amber-500 mt-1">
|
||||
App credentials are missing (`GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY`).
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<Button variant="outline" size="sm" onClick={() => fetchGitHubAppStatus()}>
|
||||
Refresh App Status
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!appStatus().install_enabled || isInstallingApp()}
|
||||
onClick={() => installGitHubApp()}
|
||||
>
|
||||
{isInstallingApp() ? 'Opening...' : (appStatus().installed ? 'Reinstall GitHub App' : 'Install GitHub App')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Stats Overview - 2-column layout with larger left column */}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Left Column - Main Stats */}
|
||||
@@ -456,15 +769,66 @@ export const GitHub = () => {
|
||||
|
||||
{/* Repositories */}
|
||||
<Card class="p-6">
|
||||
<h3 class="text-lg font-semibold text-foreground mb-4">Repositories</h3>
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3 mb-4">
|
||||
<h3 class="text-lg font-semibold text-foreground">Repositories</h3>
|
||||
{githubStats().repos.length > 0 && (
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<Button variant="outline" size="sm" onClick={() => selectAllRepos()}>
|
||||
Select All
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => clearRepoSelection()} disabled={selectedCount() === 0}>
|
||||
Clear
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => backupSelectedRepositories()}
|
||||
disabled={selectedCount() === 0 || isBackingUp()}
|
||||
>
|
||||
{isBackingUp() ? 'Backing Up...' : `Backup Selected (${selectedCount()})`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{backupMessage() && (
|
||||
<p class="text-sm text-emerald-500 mb-3">{backupMessage()}</p>
|
||||
)}
|
||||
{backupError() && (
|
||||
<p class="text-sm text-red-500 mb-3">{backupError()}</p>
|
||||
)}
|
||||
{backupRoot() && (
|
||||
<p class="text-xs text-muted-foreground mb-3">
|
||||
Backup root: <span class="font-mono break-all">{backupRoot()}</span>
|
||||
</p>
|
||||
)}
|
||||
{backups().length > 0 && (
|
||||
<div class="mb-4 p-3 border border-border rounded-lg bg-muted/30 space-y-2">
|
||||
<p class="text-xs font-medium text-foreground">Recent Local Backups</p>
|
||||
{backups().slice(0, 5).map((backup) => (
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span class="truncate pr-2">{backup.repository_full_name}</span>
|
||||
<span class={backup.last_backup_status === 'success' ? 'text-emerald-500' : 'text-red-500'}>
|
||||
{backup.last_backup_status}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{githubStats().repos.length === 0 ? (
|
||||
<p class="text-sm text-muted-foreground">No repositories available yet.</p>
|
||||
) : (
|
||||
<div class="space-y-4">
|
||||
{githubStats().repos.map((repo) => (
|
||||
<div class="border border-border rounded-lg p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="mt-1 h-4 w-4 rounded border-border accent-primary"
|
||||
checked={selectedRepos().includes(repo.full_name)}
|
||||
onChange={() => toggleRepoSelection(repo.full_name)}
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h4 class="text-lg font-medium text-foreground">{repo.name}</h4>
|
||||
{repo.language && (
|
||||
@@ -493,7 +857,12 @@ export const GitHub = () => {
|
||||
<span>Updated {formatDate(repo.updated_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => window.open(repo.html_url, '_blank', 'noopener,noreferrer')}
|
||||
aria-label={`Open ${repo.full_name}`}
|
||||
>
|
||||
<IconExternalLink class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user