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:
Tomas Dvorak
2026-03-05 23:51:34 +01:00
parent f3a835caa2
commit 954a1a1080
146 changed files with 5800 additions and 25846 deletions
+470 -101
View File
@@ -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>