feat: enhance audit logging, add MCP settings, and production docker setup
continuous-integration/drone/push Build is passing

- Implemented comprehensive audit logging for Tasks, Users, Settings, Goals, Labels, AI Chat, and Rewards.
- Added Admin UI for MCP Server settings and Audit Logs.
- Created docker-compose-production.yml with Traefik configuration.
- Fixed backend bugs (missing storage methods, route closure).
- Added Audit Logging Guidelines.
This commit is contained in:
2025-12-15 15:53:31 +01:00
parent fdf321cde9
commit d1736c5991
35 changed files with 5165 additions and 499 deletions
+92 -45
View File
@@ -27,9 +27,9 @@ import ProjectTemplate from './components/ProjectTemplate';
import WeekListView from './components/WeekListView';
import Settings from './pages/settings';
import AchievementsPage from './pages/AchievementsPage';
import UnscheduledTasksPage from './pages/UnscheduledTasksPage';
import { Task, Label } from '@shared/schema';
import { useTimer } from './hooks/useTimer';
import { Plus } from 'lucide-react';
import { SidebarProvider, SidebarInset, SidebarTrigger } from "@/components/ui/sidebar"
import { AppSidebar } from "./components/AppSidebar"
@@ -45,16 +45,17 @@ import NotFound from "@/pages/not-found";
import { AiChat } from "@/components/AiChat";
import ForgotPasswordPage from "@/pages/ForgotPasswordPage";
import ResetPasswordPage from "@/pages/ResetPasswordPage";
import AiChatPage from "@/pages/AiChatPage";
import { useQuery } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { User } from "@shared/schema";
import { Loader2 } from "lucide-react";
function App() {
const { t } = useTranslation();
const [, setLocation] = useLocation();
const [tasks, setTasks] = useState<Task[]>([]);
const [labels, setLabels] = useState<Label[]>([]);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isTaskDetailsOpen, setIsTaskDetailsOpen] = useState(false);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
@@ -62,6 +63,8 @@ function App() {
const [showPomodoro, setShowPomodoro] = useState(false);
const [activePomodoroTaskId, setActivePomodoroTaskId] = useState<string | null>(null);
const queryClient = useQueryClient();
const { data: user, isLoading: isLoadingUser } = useQuery<User>({
queryKey: ["/api/user"],
retry: false,
@@ -71,32 +74,20 @@ function App() {
queryKey: ['/api/setup/status'],
});
// Fetch tasks and labels
useEffect(() => {
if (!user) return;
const fetchData = async () => {
try {
const tasksResponse = await fetch('/api/tasks');
if (tasksResponse.ok) {
const serverTasks: Task[] = await tasksResponse.json();
const normalizedTasks = serverTasks.map(task => ({
...task,
dueDate: task.dueDate ? new Date(task.dueDate) : null
}));
setTasks(normalizedTasks);
}
// Fetch tasks and labels using React Query
const { data: tasks = [] } = useQuery<Task[]>({
queryKey: ['/api/tasks'],
enabled: !!user,
select: (data) => data.map(task => ({
...task,
dueDate: task.dueDate ? new Date(task.dueDate) : null
}))
});
const labelsResponse = await fetch('/api/labels');
if (labelsResponse.ok) {
const serverLabels: Label[] = await labelsResponse.json();
setLabels(serverLabels);
}
} catch (error) {
console.error('Error fetching data:', error);
}
};
fetchData();
}, [user]);
const { data: labels = [] } = useQuery<Label[]>({
queryKey: ['/api/labels'],
enabled: !!user,
});
const handleCreateTask = async (newTask: Partial<Task>) => {
try {
@@ -122,17 +113,31 @@ function App() {
if (!response.ok) throw new Error('Failed to create task');
const createdTask: Task = await response.json();
const normalizedTask = {
...createdTask,
dueDate: createdTask.dueDate ? new Date(createdTask.dueDate) : null
};
setTasks(prev => [...prev, normalizedTask]);
// Update cache optimistically or invalidation
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => {
const normalizedTask = {
...createdTask,
dueDate: createdTask.dueDate ? new Date(createdTask.dueDate) : null
};
return [...old, normalizedTask];
});
// Invalidate to be sure
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
} catch (error) {
console.error('Error creating task:', error);
}
};
const handleTaskUpdate = async (taskId: string, updates: Partial<Task>) => {
// Optimistic Update
const previousTasks = queryClient.getQueryData<Task[]>(['/api/tasks']);
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) =>
old.map(task => task.id === taskId ? { ...task, ...updates } : task)
);
try {
const response = await fetch(`/api/tasks/${taskId}`, {
method: 'PATCH',
@@ -143,19 +148,28 @@ function App() {
if (!response.ok) throw new Error('Failed to update task');
const updatedTask: Task = await response.json();
const normalizedTask = {
...updatedTask,
dueDate: updatedTask.dueDate ? new Date(updatedTask.dueDate) : null
};
setTasks(prev => prev.map(task => task.id === taskId ? normalizedTask : task));
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => uniqueTasks(old, updatedTask));
} catch (error) {
console.error('Error updating task:', error);
setTasks(prev => prev.map(task => task.id === taskId ? { ...task, ...updates } : task));
// Rollback
if (previousTasks) {
queryClient.setQueryData(['/api/tasks'], previousTasks);
}
}
};
// Helper to merge updated task into array
const uniqueTasks = (tasks: Task[], updated: Task) => {
const normalized = { ...updated, dueDate: updated.dueDate ? new Date(updated.dueDate) : null };
return tasks.map(t => t.id === updated.id ? normalized : t);
};
const handleCreateFromTemplate = (templateId: string, startDate: Date, projectName: string) => {
console.log('Creating project from template:', { templateId, startDate, projectName });
// Assuming this might create tasks, we should invalidate
setTimeout(() => queryClient.invalidateQueries({ queryKey: ['/api/tasks'] }), 1000);
};
const { startTimer, stopTimer } = useTimer({
@@ -197,8 +211,14 @@ function App() {
const confirmTaskDelete = async () => {
if (!deleteTaskId) return;
const previousTasks = queryClient.getQueryData<Task[]>(['/api/tasks']);
const taskToDelete = tasks.find(task => task.id === deleteTaskId);
setTasks(prev => prev.filter(task => task.id !== deleteTaskId));
// Optimistic delete
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) =>
old.filter(task => task.id !== deleteTaskId)
);
if (selectedTask?.id === deleteTaskId) {
handleTaskDetailsClose();
@@ -208,9 +228,13 @@ function App() {
try {
const response = await fetch(`/api/tasks/${deleteTaskId}`, { method: 'DELETE' });
if (!response.ok && response.status !== 404) throw new Error('Failed to delete task');
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
} catch (error) {
console.error('Error deleting task:', error);
if (taskToDelete) setTasks(prev => [...prev, taskToDelete]);
// Rollback
if (previousTasks) {
queryClient.setQueryData(['/api/tasks'], previousTasks);
}
}
};
@@ -315,16 +339,36 @@ function App() {
<Route path="/templates">
<ProjectTemplate
onCreateFromTemplate={handleCreateFromTemplate}
onCreateTasks={(newTasks) => setTasks(prev => [...prev, ...newTasks])}
onCreateTasks={(newTasks) => {
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => [...old, ...newTasks]);
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
}}
onNavigateToSettings={() => setLocation('/settings')}
/>
</Route>
<Route path="/achievements">
<AchievementsPage user={user} />
{user ? <AchievementsPage user={user} /> : <AuthPage />}
</Route>
<Route path="/unscheduled">
{user ? (
<UnscheduledTasksPage
user={user}
onToggleCompletion={(taskId, currentStatus) => handleTaskStatusChange(taskId, currentStatus === 'done' ? 'todo' : 'done')}
onDelete={(id) => setDeleteTaskId(id)}
onUpdate={handleTaskUpdate}
onSelect={setSelectedTask}
/>
) : (
<AuthPage />
)}
</Route>
<Route path="/leaderboard">
<LeaderboardPage />
</Route>
<Route path="/ai">
<AiChatPage />
</Route>
<Route path="/settings">
<Settings onNavigateToTemplates={() => setLocation('/templates')} />
</Route>
@@ -370,7 +414,6 @@ function App() {
/>
<Toaster />
{user?.aiEnabled && <AiChat />}
<TaskCreationModal
isOpen={isCreateModalOpen}
@@ -384,6 +427,10 @@ function App() {
task={selectedTask}
onSave={handleTaskDetailsSave}
labels={labels}
onNavigate={(id) => {
const t = tasks.find(x => x.id === id);
if (t) setSelectedTask(t);
}}
/>
<AlertDialog open={deleteTaskId !== null} onOpenChange={(open) => !open && setDeleteTaskId(null)}>