diff --git a/docs/TODO.md b/docs/TODO.md index 7cd1282..6b006a4 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -16,9 +16,9 @@ --- -## Activity Log System (In Progress) +## Activity Log System (Complete ✅) -**Status:** Phase 5/8 Complete (Backend Complete ✅) +**Status:** Phase 8/8 Complete - Ready for Testing **Started:** 2025-12-02 **Commits:** `f9cdf2a` (Ph1), `c9beee9` (Ph2), `d83e416` (Ph3), `4dd6603` (Ph4), `d641e3f` (Ph5) **Admin User:** spotlight@radziel.com (password: Dance123!) @@ -78,22 +78,30 @@ Comprehensive activity logging system for admin monitoring with real-time stream - ✅ Fresh DB check for admin status on join - **File:** `backend/src/socket/index.js` -### Remaining Tasks (Frontend) - **Phase 6-7: Frontend Admin Page** -- [ ] Create `frontend/src/pages/admin/ActivityLogsPage.jsx` -- [ ] Stats dashboard (total logs, failures, by category) -- [ ] Filter UI (date range, action, username) -- [ ] Log table with pagination -- [ ] Real-time streaming toggle with auto-scroll -- [ ] Add navigation link for admins +- ✅ Created `frontend/src/pages/admin/ActivityLogsPage.jsx` (600+ lines) +- ✅ Stats dashboard (total logs, unique users, failures, 24h activity) +- ✅ Category breakdown visualization +- ✅ Filter UI (date range, category dropdown, action dropdown, username, status) +- ✅ Log table with pagination (50 per page) +- ✅ Real-time streaming toggle with Socket.IO +- ✅ Added admin API methods to `frontend/src/services/api.js` +- ✅ Added route `/admin/activity-logs` to `App.jsx` +- ✅ Added admin navigation link (desktop & mobile) with Shield icon +- ✅ Build successful (no errors) +- **Files:** + - `frontend/src/pages/admin/ActivityLogsPage.jsx` + - `frontend/src/services/api.js` + - `frontend/src/App.jsx` + - `frontend/src/components/layout/Navbar.jsx` -**Phase 8: Testing & Polish** +**Phase 8: Testing & Manual Verification** - [ ] Test all 14 action logging points - [ ] Test admin-only access enforcement - [ ] Test real-time streaming with multiple admins -- [ ] Mobile responsive design -- [ ] Documentation +- [ ] Test filtering combinations +- [ ] Test pagination +- [ ] Mobile responsive design verification ### Implementation Notes - **Fire-and-forget**: Logging never blocks requests or crashes app diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 85c37f5..b57fc41 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -18,6 +18,7 @@ import RatePartnerPage from './pages/RatePartnerPage'; import HistoryPage from './pages/HistoryPage'; import ProfilePage from './pages/ProfilePage'; import PublicProfilePage from './pages/PublicProfilePage'; +import ActivityLogsPage from './pages/admin/ActivityLogsPage'; import VerificationBanner from './components/common/VerificationBanner'; import InstallPWA from './components/pwa/InstallPWA'; @@ -199,6 +200,16 @@ function App() { } /> + {/* Admin Routes */} + + + + } + /> + {/* Public Profile - must be before home route */} { Profile + {user?.isAdmin && ( + + + Admin + + )} +
{user.username} @@ -201,6 +211,15 @@ const Navbar = ({ pageTitle = null }) => { > Profile + {user?.isAdmin && ( + setMenuOpen(false)} + className="flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-100" + > + Admin + + )} + + +
+ + + {/* Error */} + {error && ( +
+ +

{error}

+
+ )} + + {/* Stats Dashboard */} + {stats && ( +
+
+

Total Logs

+

{stats.totalLogs?.toLocaleString()}

+
+ +
+

Unique Users

+

{stats.uniqueUsers?.toLocaleString()}

+
+ +
+

Failed Actions

+

{stats.failedActions?.toLocaleString()}

+

Success: {stats.successRate}%

+
+ +
+

Last 24h

+

{stats.recentActivity24h?.toLocaleString()}

+
+
+ )} + + {/* Category Stats */} + {stats?.logsByCategory && ( +
+

Logs by Category

+
+ {stats.logsByCategory.map((cat) => ( +
+
+ {cat.category} + {cat.count.toLocaleString()} +
+ ))} +
+
+ )} + + {/* Filters */} +
+
+
+ +

Filters

+
+ +
+ +
+
+ + handleFilterChange('startDate', e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent" + /> +
+ +
+ + handleFilterChange('endDate', e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent" + /> +
+ +
+ + +
+ +
+ + +
+ +
+ + handleFilterChange('username', e.target.value)} + placeholder="Search username..." + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent" + /> +
+ +
+ + +
+
+
+ + {/* Results Info */} +
+

+ Showing {offset + 1}-{Math.min(offset + limit, total)} of {total.toLocaleString()} logs + {streaming && (Live)} +

+ +
+ + + Page {Math.floor(offset / limit) + 1} + + +
+
+ + {/* Logs Table */} +
+ {loading ? ( +
+ +
+ ) : logs.length === 0 ? ( +
+ +

No activity logs found

+
+ ) : ( +
+ + + + + + + + + + + + + {logs.map((log) => ( + + + + + + + + + ))} + +
+ Status + + Timestamp + + User + + Action + + Resource + + IP Address +
+ {log.success ? ( + + ) : ( + + )} + + {formatDate(log.createdAt)} + +
+ {log.username || 'system'} +
+ {log.userId && ( +
ID: {log.userId}
+ )} +
+ + {log.action} + + + {log.resource || '-'} + + {log.ipAddress || '-'} +
+
+ )} +
+ + {/* Pagination Bottom */} +
+

+ Total: {total.toLocaleString()} logs +

+ +
+ + +
+
+
+ + ); +}; + +export default ActivityLogsPage; diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 6cfbe26..176f50c 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -465,6 +465,33 @@ export const adminAPI = { const data = await fetchAPI(`/admin/events/${slug}/matching-runs/${runId}/suggestions?${params.toString()}`); return data; }, + + // Activity Logs API + async getActivityLogs({ startDate, endDate, action, category, username, userId, success, limit = 100, offset = 0 } = {}) { + const params = new URLSearchParams(); + if (startDate) params.append('startDate', startDate); + if (endDate) params.append('endDate', endDate); + if (action) params.append('action', action); + if (category) params.append('category', category); + if (username) params.append('username', username); + if (userId) params.append('userId', String(userId)); + if (success !== undefined) params.append('success', String(success)); + params.append('limit', String(limit)); + params.append('offset', String(offset)); + + const data = await fetchAPI(`/admin/activity-logs?${params.toString()}`); + return data.data; + }, + + async getActivityLogActions() { + const data = await fetchAPI('/admin/activity-logs/actions'); + return data.data; + }, + + async getActivityLogStats() { + const data = await fetchAPI('/admin/activity-logs/stats'); + return data.data; + }, }; export { ApiError };