refactor: unify docker deployment and restructure frontend architecture

This commit implements a unified Docker deployment strategy, moving from separate frontend and backend images to a single, multi-stage build image containing both services. It also introduces a major reorganization of the frontend directory structure and simplifies the environment configuration.

Key changes:
- **Deployment**: Added a multi-stage `Dockerfile` and `docker-entrypoint.sh` to package the Go backend and Nginx-served frontend into a single container.
- **CI/CD**: Updated GitHub Actions workflows (`ci-cd.yml`, `release.yml`) to build and push the new unified image instead of separate ones.
- **Frontend Refactor**: Reorganized `frontend/src/pages` into a domain-driven directory structure (e.g., `auth/`, `admin/`, `content/`, `communication/`, `productivity/`, `settings/`, `misc/`).
- **Configuration**: Simplified `.env.example` and updated `docker-compose.yml` to reflect the unified service model and single host port.
- **Cleanup**: Removed deprecated `docker-compose.demo.yml`, `docker-compose.prod.yml`, and various unused frontend components and services.
- **Backend**: Refactored configuration loading to use exported `GetDurationEnv` for better consistency.
This commit is contained in:
Tomas Dvorak
2026-05-10 10:48:41 +02:00
parent c6a99c7e21
commit 6c448b336a
71 changed files with 135367 additions and 4481 deletions
+580
View File
@@ -0,0 +1,580 @@
import { createSignal, onMount, For, Show } from 'solid-js';
import {
IconChartLine,
IconBookmarks,
IconChecklist,
IconClock,
IconTarget,
IconBrain,
IconGitBranch,
IconBulb,
IconAward
} from '@tabler/icons-solidjs';
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { useHaptics } from '@/lib/haptics';
interface AnalyticsData {
period: {
start_date: string;
end_date: string;
days: number;
};
summary: {
hours_tracked: number;
tasks_completed: number;
bookmarks_added: number;
notes_created: number;
courses_completed: number;
github_commits: number;
};
analytics: Array<{
date: string;
hours_tracked: number;
tasks_completed: number;
bookmarks_added: number;
notes_created: number;
courses_completed: number;
github_commits: number;
study_streak: number;
productivity_score: number;
}>;
productivity_metrics: Array<{
period: string;
start_date: string;
end_date: string;
total_hours: number;
billable_hours: number;
non_billable_hours: number;
tasks_completed: number;
average_task_time: number;
peak_productivity_hour: number;
focus_score: number;
efficiency_score: number;
}>;
learning_analytics: Array<{
id: number;
course: {
title: string;
description: string;
};
start_date: string;
last_accessed: string;
time_spent: number;
progress: number;
modules_completed: number;
total_modules: number;
average_score: number;
streak_days: number;
skills_acquired: string[];
}>;
github_analytics: Array<{
date: string;
commits: number;
pull_requests: number;
issues_opened: number;
issues_closed: number;
reviews: number;
contributions: number;
languages: Record<string, number>;
repositories: string[];
}>;
goals: Array<{
id: number;
title: string;
description: string;
category: string;
target_value: number;
current_value: number;
unit: string;
deadline: string;
status: string;
priority: string;
progress: number;
is_completed: boolean;
milestones: Array<{
id: number;
title: string;
target_value: number;
current_value: number;
deadline: string;
status: string;
is_completed: boolean;
}>;
}>;
habit_analytics: Array<{
habit_name: string;
start_date: string;
last_completed: string;
streak: number;
best_streak: number;
total_days: number;
completion_rate: number;
frequency: string;
category: string;
goal_target: number;
goal_achieved: boolean;
}>;
}
export const Analytics = () => {
const haptics = useHaptics();
const [analytics, setAnalytics] = createSignal<AnalyticsData | null>(null);
const [loading, setLoading] = createSignal(true);
const [error, setError] = createSignal<string | null>(null);
const [selectedPeriod, setSelectedPeriod] = createSignal('30');
const createFallbackAnalyticsData = (): AnalyticsData => ({
period: {
start_date: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
end_date: new Date().toISOString(),
days: 30,
},
summary: {
hours_tracked: 0,
tasks_completed: 0,
bookmarks_added: 0,
notes_created: 0,
courses_completed: 0,
github_commits: 0,
},
analytics: [],
productivity_metrics: [],
learning_analytics: [],
github_analytics: [],
goals: [],
habit_analytics: [],
});
const normalizeAnalyticsData = (raw: any): AnalyticsData => {
const fallback = createFallbackAnalyticsData();
if (!raw || typeof raw !== 'object') {
return fallback;
}
const periodRaw = raw.period && typeof raw.period === 'object' ? raw.period : {};
const summaryRaw = raw.summary && typeof raw.summary === 'object' ? raw.summary : {};
return {
period: {
start_date: typeof periodRaw.start_date === 'string' ? periodRaw.start_date : fallback.period.start_date,
end_date: typeof periodRaw.end_date === 'string' ? periodRaw.end_date : fallback.period.end_date,
days: Number(periodRaw.days) || fallback.period.days,
},
summary: {
hours_tracked: Number(summaryRaw.hours_tracked) || 0,
tasks_completed: Number(summaryRaw.tasks_completed) || 0,
bookmarks_added: Number(summaryRaw.bookmarks_added) || 0,
notes_created: Number(summaryRaw.notes_created) || 0,
courses_completed: Number(summaryRaw.courses_completed) || 0,
github_commits: Number(summaryRaw.github_commits) || 0,
},
analytics: Array.isArray(raw.analytics) ? raw.analytics : [],
productivity_metrics: Array.isArray(raw.productivity_metrics) ? raw.productivity_metrics : [],
learning_analytics: Array.isArray(raw.learning_analytics) ? raw.learning_analytics : [],
github_analytics: Array.isArray(raw.github_analytics) ? raw.github_analytics : [],
goals: Array.isArray(raw.goals) ? raw.goals : [],
habit_analytics: Array.isArray(raw.habit_analytics) ? raw.habit_analytics : [],
};
};
const fetchAnalytics = async () => {
try {
setLoading(true);
setError(null);
const token = localStorage.getItem('token');
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:8080'}/api/v1/analytics/dashboard?days=${selectedPeriod()}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to fetch analytics');
}
const data = await response.json();
setAnalytics(normalizeAnalyticsData(data));
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
setAnalytics(createFallbackAnalyticsData());
} finally {
setLoading(false);
}
};
onMount(() => {
fetchAnalytics();
});
const formatHours = (hours: number) => {
const h = Math.floor(hours);
const m = Math.round((hours - h) * 60);
return `${h}h ${m}m`;
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString();
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'urgent': return 'text-destructive';
case 'high': return 'text-orange-500';
case 'medium': return 'text-yellow-500';
case 'low': return 'text-muted-foreground';
default: return 'text-gray-500';
}
};
// Component render
return (
<div class="p-6 space-y-6">
<div class="flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold">Analytics & Insights</h1>
<p class="text-muted-foreground">Track your productivity and progress</p>
</div>
<div class="flex gap-2">
<select
value={selectedPeriod()}
onChange={(e) => {
setSelectedPeriod(e.target.value);
haptics.selection();
}}
class="px-3 py-2 border rounded-md bg-background"
>
<option value="7">Last 7 days</option>
<option value="30">Last 30 days</option>
<option value="90">Last 90 days</option>
</select>
<Button onClick={() => {
fetchAnalytics();
haptics.selection();
}}>Refresh</Button>
</div>
</div>
<Show when={error()}>
<div class="bg-destructive/15 border border-destructive/20 rounded-md p-4">
<p class="text-destructive">{error()}</p>
</div>
</Show>
<Show when={loading()}>
<div class="text-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
<p class="mt-2 text-muted-foreground">Loading analytics...</p>
</div>
</Show>
<Show when={analytics()}>
<div class="space-y-6">
{/* Summary Cards */}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-muted-foreground">Hours Tracked</p>
<p class="text-2xl font-bold">{formatHours(analytics()!.summary.hours_tracked)}</p>
</div>
<IconClock class="h-8 w-8 text-blue-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-muted-foreground">Tasks Completed</p>
<p class="text-2xl font-bold">{analytics()!.summary.tasks_completed}</p>
</div>
<IconChecklist class="h-8 w-8 text-primary" />
</div>
</CardContent>
</Card>
<Card>
<CardContent class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-muted-foreground">Bookmarks Added</p>
<p class="text-2xl font-bold">{analytics()!.summary.bookmarks_added}</p>
</div>
<IconBookmarks class="h-8 w-8 text-purple-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-muted-foreground">GitHub Commits</p>
<p class="text-2xl font-bold">{analytics()!.summary.github_commits}</p>
</div>
<IconGitBranch class="h-8 w-8 text-orange-500" />
</div>
</CardContent>
</Card>
</div>
{/* Goals Progress */}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<IconTarget class="h-5 w-5" />
Active Goals
</CardTitle>
<CardDescription>Track your goal progress</CardDescription>
</CardHeader>
<CardContent>
<div class="space-y-4">
<For each={analytics()!.goals.filter(g => g.status === 'active').slice(0, 5)}>
{(goal) => (
<div class="space-y-2">
<div class="flex justify-between items-center">
<div class="flex-1">
<h4 class="font-medium">{goal.title}</h4>
<p class="text-sm text-muted-foreground">
{goal.current_value} / {goal.target_value} {goal.unit}
</p>
</div>
<div class="flex items-center gap-2">
<span class={`text-sm font-medium ${getPriorityColor(goal.priority)}`}>
{goal.priority}
</span>
<span class="text-sm font-medium">{Math.round(goal.progress)}%</span>
</div>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div
class="bg-blue-500 h-2 rounded-full transition-all duration-300"
style={`width: ${goal.progress}%`}
></div>
</div>
<p class="text-xs text-muted-foreground">
Deadline: {formatDate(goal.deadline)}
</p>
</div>
)}
</For>
<Show when={analytics()!.goals.filter(g => g.status === 'active').length === 0}>
<p class="text-muted-foreground text-center py-4">No active goals</p>
</Show>
</div>
</CardContent>
</Card>
{/* Habit Tracking */}
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<IconAward class="h-5 w-5" />
Habit Tracking
</CardTitle>
<CardDescription>Your daily habits and streaks</CardDescription>
</CardHeader>
<CardContent>
<div class="space-y-4">
<For each={analytics()!.habit_analytics.slice(0, 5)}>
{(habit) => (
<div class="flex justify-between items-center p-3 border rounded-lg">
<div>
<h4 class="font-medium">{habit.habit_name}</h4>
<p class="text-sm text-muted-foreground">
{habit.frequency} {Math.round(habit.completion_rate)}% completion
</p>
</div>
<div class="text-right">
<div class="flex items-center gap-1">
<IconBulb class="h-4 w-4 text-orange-500" />
<span class="font-medium">{habit.streak} day streak</span>
</div>
<p class="text-xs text-muted-foreground">
Best: {habit.best_streak} days
</p>
</div>
</div>
)}
</For>
<Show when={analytics()!.habit_analytics.length === 0}>
<p class="text-muted-foreground text-center py-4">No habits tracked</p>
</Show>
</div>
</CardContent>
</Card>
</div>
{/* Learning Progress */}
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<IconBrain class="h-5 w-5" />
Learning Progress
</CardTitle>
<CardDescription>Your course progress and achievements</CardDescription>
</CardHeader>
<CardContent>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<For each={analytics()!.learning_analytics.slice(0, 6)}>
{(course) => (
<div class="border rounded-lg p-4">
<h4 class="font-medium truncate">{course.course.title}</h4>
<p class="text-sm text-muted-foreground mb-2">
{course.modules_completed}/{course.total_modules} modules
</p>
<div class="w-full bg-gray-200 rounded-full h-2 mb-2">
<div
class="bg-primary h-2 rounded-full transition-all duration-300"
style={`width: ${course.progress}%`}
></div>
</div>
<div class="flex justify-between text-xs text-muted-foreground">
<span>{Math.round(course.progress)}% complete</span>
<span>{course.streak_days} day streak</span>
</div>
</div>
)}
</For>
<Show when={analytics()!.learning_analytics.length === 0}>
<div class="col-span-full text-center py-8">
<p class="text-muted-foreground">No courses in progress</p>
</div>
</Show>
</div>
</CardContent>
</Card>
{/* GitHub Activity */}
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<IconGitBranch class="h-5 w-5" />
GitHub Activity
</CardTitle>
<CardDescription>Your contribution summary</CardDescription>
</CardHeader>
<CardContent>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="text-center">
<p class="text-2xl font-bold">{analytics()!.summary.github_commits}</p>
<p class="text-sm text-muted-foreground">Commits</p>
</div>
<div class="text-center">
<p class="text-2xl font-bold">
{analytics()!.github_analytics.reduce((sum, day) => sum + day.pull_requests, 0)}
</p>
<p class="text-sm text-muted-foreground">Pull Requests</p>
</div>
<div class="text-center">
<p class="text-2xl font-bold">
{analytics()!.github_analytics.reduce((sum, day) => sum + day.issues_opened, 0)}
</p>
<p class="text-sm text-muted-foreground">Issues Opened</p>
</div>
<div class="text-center">
<p class="text-2xl font-bold">
{analytics()!.github_analytics.reduce((sum, day) => sum + day.reviews, 0)}
</p>
<p class="text-sm text-muted-foreground">Reviews</p>
</div>
</div>
<div class="space-y-2">
<For each={analytics()!.github_analytics.slice(0, 7)}>
{(day) => (
<div class="flex justify-between items-center p-2 border rounded">
<span class="text-sm">{formatDate(day.date)}</span>
<div class="flex gap-4 text-sm">
<span>{day.commits} commits</span>
<span>{day.pull_requests} PRs</span>
<span>{day.issues_opened} issues</span>
</div>
</div>
)}
</For>
</div>
</CardContent>
</Card>
{/* Productivity Insights */}
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<IconChartLine class="h-5 w-5" />
Productivity Insights
</CardTitle>
<CardDescription>Key insights and patterns</CardDescription>
</CardHeader>
<CardContent>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 class="font-medium mb-3">Daily Activity</h4>
<div class="space-y-2">
<For each={analytics()!.analytics.slice(0, 7)}>
{(day) => (
<div class="flex justify-between items-center">
<span class="text-sm">{formatDate(day.date)}</span>
<div class="flex items-center gap-2">
<span class="text-sm">{formatHours(day.hours_tracked)}</span>
<span class="text-sm text-muted-foreground">
{day.tasks_completed} tasks
</span>
<Show when={day.productivity_score > 0}>
<span class="text-xs px-2 py-1 bg-primary/10 text-primary rounded">
{Math.round(day.productivity_score)}%
</span>
</Show>
</div>
</div>
)}
</For>
</div>
</div>
<div>
<h4 class="font-medium mb-3">Key Metrics</h4>
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-sm text-muted-foreground">Average Daily Hours</span>
<span class="text-sm font-medium">
{formatHours(analytics()!.summary.hours_tracked / analytics()!.period.days)}
</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-muted-foreground">Tasks per Day</span>
<span class="text-sm font-medium">
{(analytics()!.summary.tasks_completed / analytics()!.period.days).toFixed(1)}
</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-muted-foreground">Study Streak</span>
<span class="text-sm font-medium">
{Math.max(...analytics()!.analytics.map(a => a.study_streak))} days
</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-muted-foreground">Average Productivity</span>
<span class="text-sm font-medium">
{Math.round(
analytics()!.analytics.reduce((sum, a) => sum + a.productivity_score, 0) /
analytics()!.analytics.length
)}%
</span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</Show>
</div>
);
};