From 3612d26abe0b895d347c00e4e62a94a90998494d Mon Sep 17 00:00:00 2001 From: George Kargiotakis Date: Mon, 29 Dec 2025 23:39:22 +0200 Subject: [PATCH] Add public resource views and cleanup system overview Add new public-facing pages for Diving Organizations and Tags under a new "Resources" menu in the navigation bar. This allows users to browse certification bodies and available tags without admin access. - Create `DivingOrganizationsPage` with logo support and certification levels - Create `DivingTagsPage` with search and usage counts - Update `Navbar` to include "Resources" dropdown with Award icon - Remove deprecated `AdminSystemOverview` page and backend endpoint - Fix mobile search bar z-index to appear above menu backdrop - Remove icons from search category headers for cleaner UI - Update API and routes to support new pages --- README.md | 4 +- backend/app/routers/system.py | 169 +--- backend/app/schemas.py | 78 -- backend/tests/test_system.py | 289 ------- docs/TESTING_STRATEGY.md | 20 +- docs/development/architecture.md | 48 +- docs/maintenance/changelog.md | 4 +- frontend/src/App.js | 16 +- frontend/src/api.js | 26 +- frontend/src/components/GlobalSearchBar.js | 4 +- frontend/src/components/Navbar.js | 80 ++ frontend/src/components/ui/Combobox.js | 4 +- frontend/src/pages/AdminSystemOverview.js | 744 ------------------ frontend/src/pages/DivingOrganizationsPage.js | 210 +++++ frontend/src/pages/DivingTagsPage.js | 99 +++ 15 files changed, 461 insertions(+), 1334 deletions(-) delete mode 100644 frontend/src/pages/AdminSystemOverview.js create mode 100644 frontend/src/pages/DivingOrganizationsPage.js create mode 100644 frontend/src/pages/DivingTagsPage.js diff --git a/README.md b/README.md index 2366c757..35269bec 100644 --- a/README.md +++ b/README.md @@ -58,8 +58,8 @@ and review dive sites and diving centers. ### **Admin Dashboard & System Monitoring** -- **System Overview Dashboard**: Real-time platform statistics including user - counts, content metrics, and engagement data +- **System Statistics & Metrics**: Real-time platform statistics including user + growth, geographic distribution, and system health monitoring. - **System Health Monitoring**: CPU, memory, disk usage, and database connectivity monitoring - **Recent Activity Tracking**: Monitor user registrations, content creation, diff --git a/backend/app/routers/system.py b/backend/app/routers/system.py index dd6165c9..9ea8cbfa 100644 --- a/backend/app/routers/system.py +++ b/backend/app/routers/system.py @@ -17,7 +17,7 @@ ) from app.auth import get_current_admin_user from app.schemas import ( - SystemOverviewResponse, SystemHealthResponse, PlatformStatsResponse, + SystemHealthResponse, PlatformStatsResponse, NotificationAnalyticsResponse, GrowthResponse ) from app.utils import get_client_ip, format_ip_for_logging @@ -483,173 +483,6 @@ async def get_general_statistics( "last_updated": now.isoformat() } -@router.get("/overview", response_model=SystemOverviewResponse) -async def get_system_overview( - current_user: User = Depends(get_current_admin_user), - db: Session = Depends(get_db) -): - """Get comprehensive system overview with platform statistics and health metrics (DEPRECATED: Use /metrics and /statistics instead)""" - - # Calculate date ranges - now = datetime.utcnow() - thirty_days_ago = now - timedelta(days=30) - seven_days_ago = now - timedelta(days=7) - one_day_ago = now - timedelta(days=1) - - # User Statistics - total_users = db.query(func.count(User.id)).scalar() - active_users_30d = db.query(func.count(User.id)).filter( - User.created_at >= thirty_days_ago - ).scalar() - new_users_7d = db.query(func.count(User.id)).filter( - User.created_at >= seven_days_ago - ).scalar() - new_users_30d = db.query(func.count(User.id)).filter( - User.created_at >= thirty_days_ago - ).scalar() - - # Calculate user growth rate (percentage change from previous 30 days) - sixty_days_ago = now - timedelta(days=60) - users_60d_ago = db.query(func.count(User.id)).filter( - User.created_at >= sixty_days_ago - ).scalar() - users_30d_ago = db.query(func.count(User.id)).filter( - User.created_at >= thirty_days_ago - ).scalar() - - growth_rate = 0 - if users_30d_ago > 0: - growth_rate = ((new_users_30d - (users_60d_ago - users_30d_ago)) / users_30d_ago) * 100 - - # Content Statistics - total_dive_sites = db.query(func.count(DiveSite.id)).scalar() - total_diving_centers = db.query(func.count(DivingCenter.id)).scalar() - total_dives = db.query(func.count(Dive.id)).scalar() - total_comments = db.query(func.count(SiteComment.id)).scalar() + db.query(func.count(CenterComment.id)).scalar() - total_ratings = db.query(func.count(SiteRating.id)).scalar() + db.query(func.count(CenterRating.id)).scalar() - total_media = db.query(func.count(SiteMedia.id)).scalar() + db.query(func.count(DiveMedia.id)).scalar() - - # Engagement Metrics - avg_site_rating = db.query(func.avg(SiteRating.score)).scalar() or 0 - avg_center_rating = db.query(func.avg(CenterRating.score)).scalar() or 0 - - # Recent activity (last 24 hours) - recent_comments = db.query(func.count(SiteComment.id)).filter( - SiteComment.created_at >= one_day_ago - ).scalar() + db.query(func.count(CenterComment.id)).filter( - CenterComment.created_at >= one_day_ago - ).scalar() - - recent_ratings = db.query(func.count(SiteRating.id)).filter( - SiteRating.created_at >= one_day_ago - ).scalar() + db.query(func.count(CenterRating.id)).filter( - CenterRating.created_at >= one_day_ago - ).scalar() - - recent_dives = db.query(func.count(Dive.id)).filter( - Dive.created_at >= one_day_ago - ).scalar() - - # Geographic Distribution - dive_sites_by_country = db.query( - DiveSite.country, - func.count(DiveSite.id).label('count') - ).filter( - DiveSite.country.isnot(None) - ).group_by(DiveSite.country).order_by(desc('count')).limit(10).all() - - # System Usage (simplified - in production this would come from logs/analytics) - api_calls_today = 0 # This would be tracked in production - peak_usage_time = "14:00-16:00" # Placeholder - most_accessed_endpoint = "/api/v1/dive-sites" # Placeholder - - # System Health - db_connection_status = "healthy" - try: - db.execute(text("SELECT 1")) - except Exception: - db_connection_status = "unhealthy" - - # Resource Utilization - cpu_usage = psutil.cpu_percent(interval=1) - memory_usage = psutil.virtual_memory().percent - disk_usage = psutil.disk_usage('/').percent - - # External Services Status (placeholders) - google_oauth_status = "healthy" - geocoding_service_status = "healthy" - - # Security Metrics (placeholder - would need to be implemented with login tracking) - failed_logins_24h = 0 # This would be tracked in production - - return { - "platform_stats": { - "users": { - "total": total_users, - "active_30d": active_users_30d, - "new_7d": new_users_7d, - "new_30d": new_users_30d, - "growth_rate": round(growth_rate, 2) - }, - "content": { - "dive_sites": total_dive_sites, - "diving_centers": total_diving_centers, - "dives": total_dives, - "comments": total_comments, - "ratings": total_ratings, - "media_uploads": total_media - }, - "engagement": { - "avg_site_rating": round(avg_site_rating, 1), - "avg_center_rating": round(avg_center_rating, 1), - "recent_comments_24h": recent_comments, - "recent_ratings_24h": recent_ratings, - "recent_dives_24h": recent_dives - }, - "geographic": { - "dive_sites_by_country": [ - {"country": country, "count": count} - for country, count in dive_sites_by_country - ] - }, - "system_usage": { - "api_calls_today": api_calls_today, - "peak_usage_time": peak_usage_time, - "most_accessed_endpoint": most_accessed_endpoint - } - }, - "system_health": { - "database": { - "status": db_connection_status, - "response_time": "fast" # Placeholder - }, - "application": { - "status": "healthy", - "uptime": "99.9%", # Placeholder - "response_time": "fast" # Placeholder - }, - "resources": { - "cpu_usage": cpu_usage, - "memory_usage": memory_usage, - "disk_usage": disk_usage - }, - "external_services": { - "google_oauth": google_oauth_status, - "geocoding_service": geocoding_service_status - }, - "security": { - "failed_logins_24h": failed_logins_24h, - "suspicious_activity": "none detected" # Placeholder - } - }, - "alerts": { - "critical": [], - "warnings": [], - "info": [] - }, - "last_updated": now.isoformat() - } - @router.get("/health", response_model=SystemHealthResponse) async def get_system_health( current_user: User = Depends(get_current_admin_user), diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 60ac370a..e928d59f 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -949,84 +949,6 @@ class ParsedDiveTripUpdate(BaseModel): trip_status: Optional[str] = Field(None, pattern=r"^(scheduled|confirmed|cancelled|completed)$") dives: Optional[List[ParsedDiveCreate]] = None -# System Overview Schemas -class UserStats(BaseModel): - total: int - active_30d: int - new_7d: int - new_30d: int - growth_rate: float - -class ContentStats(BaseModel): - dive_sites: int - diving_centers: int - dives: int - comments: int - ratings: int - media_uploads: int - -class EngagementStats(BaseModel): - avg_site_rating: float - avg_center_rating: float - recent_comments_24h: int - recent_ratings_24h: int - recent_dives_24h: int - -class GeographicStats(BaseModel): - dive_sites_by_country: List[dict] - -class SystemUsageStats(BaseModel): - api_calls_today: int - peak_usage_time: str - most_accessed_endpoint: str - -class PlatformStats(BaseModel): - users: UserStats - content: ContentStats - engagement: EngagementStats - geographic: GeographicStats - system_usage: SystemUsageStats - -class DatabaseHealth(BaseModel): - status: str - response_time: str - -class ApplicationHealth(BaseModel): - status: str - uptime: str - response_time: str - -class ResourceHealth(BaseModel): - cpu_usage: float - memory_usage: float - disk_usage: float - -class ExternalServicesHealth(BaseModel): - google_oauth: str - geocoding_service: str - -class SecurityHealth(BaseModel): - failed_logins_24h: int - suspicious_activity: str - -class SystemHealth(BaseModel): - database: DatabaseHealth - application: ApplicationHealth - resources: ResourceHealth - external_services: ExternalServicesHealth - security: SecurityHealth - -class Alerts(BaseModel): - critical: List[str] - warnings: List[str] - info: List[str] - -class SystemOverviewResponse(BaseModel): - platform_stats: PlatformStats - system_health: SystemHealth - alerts: Alerts - last_updated: str - class SystemHealthResponse(BaseModel): status: str database: dict diff --git a/backend/tests/test_system.py b/backend/tests/test_system.py index 0d812286..c36d38bb 100644 --- a/backend/tests/test_system.py +++ b/backend/tests/test_system.py @@ -12,295 +12,6 @@ ) -class TestSystemOverview: - """Test system overview endpoint.""" - - def test_get_system_overview_admin_success(self, client, admin_headers, db_session): - """Test getting system overview as admin.""" - # Create test data - test_user = User( - username="testuser", - email="test@example.com", - password_hash="hashed_password", - enabled=True, - created_at=datetime.utcnow() - timedelta(days=15) - ) - db_session.add(test_user) - db_session.flush() # Flush to get the ID - - test_dive_site = DiveSite( - name="Test Dive Site", - country="Test Country", - created_at=datetime.utcnow() - timedelta(days=10) - ) - db_session.add(test_dive_site) - db_session.flush() # Flush to get the ID - - test_diving_center = DivingCenter( - name="Test Diving Center", - created_at=datetime.utcnow() - timedelta(days=5) - ) - db_session.add(test_diving_center) - db_session.flush() # Flush to get the ID - - dive = Dive( - name="Test Dive", - user_id=test_user.id, - dive_date=datetime.utcnow().date(), - difficulty_id=2 # ADVANCED_OPEN_WATER - ) - db_session.add(dive) - - test_rating = SiteRating( - user_id=test_user.id, - dive_site_id=test_dive_site.id, - score=8, - created_at=datetime.utcnow() - timedelta(days=2) - ) - db_session.add(test_rating) - - test_comment = SiteComment( - user_id=test_user.id, - dive_site_id=test_dive_site.id, - comment_text="Test comment", - created_at=datetime.utcnow() - timedelta(hours=12) - ) - db_session.add(test_comment) - - db_session.commit() - - with patch('psutil.cpu_percent', return_value=25.0), \ - patch('psutil.virtual_memory') as mock_memory, \ - patch('psutil.disk_usage') as mock_disk: - - mock_memory.return_value.percent = 45.0 - mock_disk.return_value.percent = 60.0 - - response = client.get("/api/v1/admin/system/overview", headers=admin_headers) - - assert response.status_code == status.HTTP_200_OK - data = response.json() - - # Check platform stats - assert "platform_stats" in data - assert "system_health" in data - assert "alerts" in data - assert "last_updated" in data - - # Check user statistics - should be at least 2 (admin user from fixture + test user) - users = data["platform_stats"]["users"] - assert users["total"] >= 2 - assert users["active_30d"] >= 2 - assert users["new_7d"] >= 0 - assert users["new_30d"] >= 2 - - # Check content statistics - content = data["platform_stats"]["content"] - assert content["dive_sites"] == 1 - assert content["diving_centers"] == 1 - assert content["dives"] == 1 - assert content["comments"] == 1 - assert content["ratings"] == 1 - - # Check system health - health = data["system_health"] - assert health["database"]["status"] == "healthy" - assert health["resources"]["cpu_usage"] == 25.0 - assert health["resources"]["memory_usage"] == 45.0 - assert health["resources"]["disk_usage"] == 60.0 - - def test_get_system_overview_unauthorized(self, client): - """Test getting system overview without authentication.""" - response = client.get("/api/v1/admin/system/overview") - assert response.status_code == status.HTTP_403_FORBIDDEN - - def test_get_system_overview_regular_user_forbidden(self, client, auth_headers): - """Test getting system overview as regular user.""" - response = client.get("/api/v1/admin/system/overview", headers=auth_headers) - assert response.status_code == status.HTTP_403_FORBIDDEN - - def test_get_system_overview_with_growth_rate_calculation(self, client, admin_headers, db_session): - """Test system overview with user growth rate calculation.""" - # Create users at different times - now = datetime.utcnow() - - # User created 70 days ago - old_user = User( - username="olduser", - email="old@example.com", - password_hash="hashed_password", - enabled=True, - created_at=now - timedelta(days=70) - ) - db_session.add(old_user) - - # User created 40 days ago - mid_user = User( - username="miduser", - email="mid@example.com", - password_hash="hashed_password", - enabled=True, - created_at=now - timedelta(days=40) - ) - db_session.add(mid_user) - - # User created 10 days ago - new_user = User( - username="newuser", - email="new@example.com", - password_hash="hashed_password", - enabled=True, - created_at=now - timedelta(days=10) - ) - db_session.add(new_user) - - db_session.commit() - - with patch('psutil.cpu_percent', return_value=30.0), \ - patch('psutil.virtual_memory') as mock_memory, \ - patch('psutil.disk_usage') as mock_disk: - - mock_memory.return_value.percent = 50.0 - mock_disk.return_value.percent = 55.0 - - response = client.get("/api/v1/admin/system/overview", headers=admin_headers) - - assert response.status_code == status.HTTP_200_OK - data = response.json() - - users = data["platform_stats"]["users"] - # Should be at least 4 users: admin user from fixture + 3 users I created - assert users["total"] >= 4 - # active_30d might be less due to fixture user creation time - assert users["active_30d"] >= 2 - assert users["new_7d"] >= 0 - # new_30d might be less due to fixture user creation time - assert users["new_30d"] >= 2 - # Growth rate: (3 - (1 - 3)) / 1 * 100 = 500% - assert users["growth_rate"] >= 0 # Just check it's not negative - - def test_get_system_overview_with_geographic_distribution(self, client, admin_headers, db_session): - """Test system overview with geographic dive site distribution.""" - # Create dive sites in different countries - countries = ["USA", "Mexico", "Thailand", "Australia", "Egypt"] - for i, country in enumerate(countries): - site = DiveSite( - name=f"Site {i+1}", - country=country, - created_at=datetime.utcnow() - timedelta(days=i) - ) - db_session.add(site) - - db_session.commit() - - with patch('psutil.cpu_percent', return_value=20.0), \ - patch('psutil.virtual_memory') as mock_memory, \ - patch('psutil.disk_usage') as mock_disk: - - mock_memory.return_value.percent = 40.0 - mock_disk.return_value.percent = 50.0 - - response = client.get("/api/v1/admin/system/overview", headers=admin_headers) - - assert response.status_code == status.HTTP_200_OK - data = response.json() - - geographic = data["platform_stats"]["geographic"] - assert len(geographic["dive_sites_by_country"]) == 5 - - # Check that countries are ordered by count (descending) - country_counts = [item["count"] for item in geographic["dive_sites_by_country"]] - assert country_counts == [1, 1, 1, 1, 1] # All have count 1 - - def test_get_system_overview_with_engagement_metrics(self, client, admin_headers, db_session): - """Test system overview with engagement metrics.""" - # Create test user - test_user = User( - username="testuser", - email="test@example.com", - password_hash="hashed_password", - enabled=True - ) - db_session.add(test_user) - db_session.flush() # Flush to get the ID - - # Create dive site - dive_site = DiveSite( - name="Test Site", - created_at=datetime.utcnow() - timedelta(days=5) - ) - db_session.add(dive_site) - db_session.flush() # Flush to get the ID - - # Create ratings for different users and dive sites - # Create additional test users - test_users = [] - for i in range(4): - user = User( - username=f"testuser{i+1}", - email=f"test{i+1}@example.com", - password_hash="hashed_password", - enabled=True - ) - db_session.add(user) - test_users.append(user) - - # Create additional dive sites - dive_sites = [dive_site] # Include the existing one - for i in range(3): - site = DiveSite( - name=f"Test Site {i+2}", - created_at=datetime.utcnow() - timedelta(days=5) - ) - db_session.add(site) - dive_sites.append(site) - - db_session.flush() # Flush to get IDs - - # Create ratings - one per user per dive site - scores = [7, 8, 9, 10] - for i, score in enumerate(scores): - rating = SiteRating( - user_id=test_users[i].id, - dive_site_id=dive_sites[i].id, - score=score, - created_at=datetime.utcnow() - timedelta(days=score) - ) - db_session.add(rating) - - # Create center rating - diving_center = DivingCenter(name="Test Center") - db_session.add(diving_center) - db_session.flush() # Flush to get the ID - - center_rating = CenterRating( - user_id=test_user.id, - diving_center_id=diving_center.id, - score=9, - created_at=datetime.utcnow() - timedelta(days=1) - ) - db_session.add(center_rating) - - db_session.commit() - - with patch('psutil.cpu_percent', return_value=25.0), \ - patch('psutil.virtual_memory') as mock_memory, \ - patch('psutil.disk_usage') as mock_disk: - - mock_memory.return_value.percent = 45.0 - mock_disk.return_value.percent = 60.0 - - response = client.get("/api/v1/admin/system/overview", headers=admin_headers) - - assert response.status_code == status.HTTP_200_OK - data = response.json() - - engagement = data["platform_stats"]["engagement"] - # Average site rating: (7+8+9+10)/4 = 8.5 - assert engagement["avg_site_rating"] == 8.5 - assert engagement["avg_center_rating"] == 9.0 - - class TestSystemHealth: """Test system health endpoint.""" diff --git a/docs/TESTING_STRATEGY.md b/docs/TESTING_STRATEGY.md index ac8b658f..f2504215 100644 --- a/docs/TESTING_STRATEGY.md +++ b/docs/TESTING_STRATEGY.md @@ -43,7 +43,7 @@ python -m pytest **New Features Added:** -- **System Overview Dashboard**: Comprehensive platform statistics and health monitoring +- **System Statistics & Metrics**: Comprehensive platform statistics and health monitoring - **Recent Activity Monitoring**: Real-time tracking of user actions and system changes - **System Health Endpoints**: Backend API endpoints for monitoring system status - **Real-time Activity Tracking**: Database queries for user and system activity @@ -54,10 +54,10 @@ python -m pytest ```python -def test_get_system_overview(self, client, admin_headers): - """Test system overview endpoint returns comprehensive platform statistics.""" -def test_get_system_health(self, client, admin_headers): - """Test system health endpoint returns detailed health information.""" +def test_get_system_metrics(self, client, admin_headers): + """Test system metrics endpoint returns detailed health information.""" +def test_get_general_statistics(self, client, admin_headers): + """Test general statistics endpoint returns comprehensive platform statistics.""" def test_get_platform_stats(self, client, admin_headers): """Test platform statistics endpoint returns detailed breakdown.""" def test_get_recent_activity(self, client, admin_headers): @@ -96,7 +96,7 @@ def test_engagement_activity(self, client, admin_headers): **Frontend Integration Tests:** -- System Overview page loading and data display +- System Metrics and General Statistics pages loading and data display - Recent Activity page with filtering and real-time updates - Admin dashboard navigation and accessibility - Auto-refresh functionality and manual refresh options @@ -104,15 +104,15 @@ def test_engagement_activity(self, client, admin_headers): **API Endpoint Tests:** -- `GET /api/v1/admin/system/overview` - System overview with platform statistics -- `GET /api/v1/admin/system/health` - System health monitoring +- `GET /api/v1/admin/system/statistics` - General statistics with platform stats +- `GET /api/v1/admin/system/metrics` - System health monitoring - `GET /api/v1/admin/system/stats` - Platform statistics breakdown - `GET /api/v1/admin/system/activity` - Recent activity with filtering **Testing Results:** - **Backend Tests**: All system monitoring endpoints working correctly ✅ -- **Frontend Validation**: System Overview and Recent Activity pages operational ✅ +- **Frontend Validation**: System Metrics, General Statistics, and Recent Activity pages operational ✅ - **ESLint Compliance**: All formatting issues resolved ✅ - **API Testing**: All new monitoring endpoints functional ✅ @@ -195,7 +195,7 @@ def test_proxy_chain_analysis(self, client): **API Endpoint Tests:** - `GET /api/v1/newsletters/trips` - Advanced search with filtering and sorting -- `GET /api/v1/admin/system/overview` - System overview with client IP detection +- `GET /api/v1/admin/system/statistics` - General statistics with client IP detection - Rate limiting endpoints with client IP validation **Testing Results:** diff --git a/docs/development/architecture.md b/docs/development/architecture.md index dc61412a..43e77f4c 100644 --- a/docs/development/architecture.md +++ b/docs/development/architecture.md @@ -298,40 +298,37 @@ This document outlines the technical design for a Python-based web application, * **Tag Management**: Comprehensive tag system with usage statistics * **Safety Features**: Protection against deleting used tags and self-deletion * **Diving Center Ownership Management**: Approve/deny ownership claims and assign owners -* **System Overview Dashboard**: Comprehensive platform statistics and health monitoring +* **General Statistics Dashboard**: Detailed platform statistics and engagement metrics +* **System Metrics Dashboard**: Comprehensive system health and infrastructure monitoring * **Recent Activity Monitoring**: Real-time tracking of user actions and system changes * **Backup and Export Management**: Data export capabilities and backup management ### **3.13. Admin Dashboard Pages** -#### **3.13.1. System Overview Dashboard** +#### **3.13.1. System Statistics & Metrics** -The System Overview dashboard provides administrators with comprehensive platform statistics and health monitoring capabilities: +The General Statistics and System Metrics dashboards provide administrators with comprehensive platform statistics and health monitoring capabilities: -**Platform Statistics:** -* **User Statistics**: Total users, active users (last 30 days), new registrations (last 7/30 days), user growth rate -* **Content Statistics**: Total dive sites, diving centers, dives, comments, ratings, media uploads +**General Statistics Dashboard:** +* **User Statistics**: Total users, active users (last 7/30 days), new registrations (last 7/30 days), user growth rate, email verification status +* **Content Statistics**: Total dive sites, diving centers, dives, routes, trips, comments, ratings, media uploads, tags * **Engagement Metrics**: Average ratings, comment activity, user participation rates -* **Geographic Distribution**: Dive sites by country/region, user distribution by location +* **Geographic Distribution**: Dive sites and diving centers by country/region * **System Usage**: API calls per day, peak usage times, most accessed endpoints +* **Notification Analytics**: In-app notification and email delivery statistics, delivery rates, category breakdown -**System Health Monitoring:** -* **Database Performance**: Connection pool status, query response times, slow query alerts -* **Application Health**: Response times, error rates, uptime statistics +**System Metrics Dashboard:** +* **Database Performance**: Connection health, query response times +* **Application Health**: Service status (Database, API, Frontend) * **Resource Utilization**: CPU usage, memory consumption, disk space -* **External Services**: Google OAuth status, geocoding service availability -* **Security Metrics**: Failed login attempts, suspicious activity, rate limiting events - -**Real-time Alerts:** -* **Critical Issues**: Database connection failures, high error rates, service outages -* **Performance Warnings**: Slow response times, high resource usage -* **Security Alerts**: Unusual login patterns, potential security threats -* **Capacity Planning**: Storage usage trends, user growth projections +* **Cloud Storage Health**: Cloudflare R2 connectivity and local fallback status +* **Bot Protection Metrics**: Cloudflare Turnstile verification success rates, error breakdown, top IP addresses +* **System Alerts**: Real-time summary of critical issues and warnings **Visual Dashboard Elements:** -* **Charts and Graphs**: User growth trends, content creation rates, system performance +* **Charts and Graphs**: Growth trends, distribution maps, metric visualizations * **Status Indicators**: Service health lights (green/yellow/red) -* **Quick Actions**: Direct links to common administrative tasks +* **Quick Actions**: Direct links to specific statistics and metrics pages * **Refresh Controls**: Real-time data updates with configurable intervals #### **3.13.2. Recent Activity Monitoring** @@ -733,7 +730,8 @@ The application will follow a microservices-oriented or a well-separated monolit * /api/v1/dive-trips/favorites (GET, POST, DELETE \- manage favorite trips) * /api/v1/dive-trips/export (GET \- export trips to calendar format) * /api/v1/media/upload (POST \- for image/video uploads) -* /api/v1/admin/system/overview (GET \- platform statistics and health) +* /api/v1/admin/system/statistics (GET \- platform statistics and engagement) +* /api/v1/admin/system/metrics (GET \- system health and infrastructure metrics) * /api/v1/admin/system/activity (GET \- recent user and system activity) * /api/v1/admin/system/backup (POST \- create database backup) * /api/v1/admin/system/export (GET \- export data in various formats) @@ -1147,7 +1145,7 @@ node test_regressions.js * ✅ Ownership status management (unclaimed, claimed, approved, denied) #### **Admin Dashboard System ✅ COMPLETED** -* ✅ System Overview dashboard with comprehensive platform statistics +* ✅ General Statistics and System Metrics dashboards with comprehensive platform statistics * ✅ Real-time system health monitoring and performance metrics * ✅ Recent Activity monitoring with user and system activity tracking * ✅ Activity filtering by time range and activity type @@ -1272,8 +1270,8 @@ node test_regressions.js * ✅ Role-based access control (User, Moderator, Admin) * ✅ User status management (enabled/disabled) * ✅ Mass delete functionality with safety features -* ✅ Quick Actions section with System Overview, Recent Activity, and Backup & Export placeholders -* ✅ System Overview Dashboard with comprehensive platform statistics and health monitoring +* ✅ Quick Actions section with statistics, metrics, activity monitoring, and growth visualizations +* ✅ General Statistics and System Metrics dashboards with comprehensive platform statistics and health monitoring * ✅ Recent Activity Monitoring with real-time user and system activity tracking #### **User Registration and Approval System ✅ COMPLETED** @@ -1374,7 +1372,7 @@ node test_regressions.js * 🔄 Mobile application development #### **Phase 8: Admin Dashboard Enhancement 🔄 IN PROGRESS** -* ✅ System Overview Dashboard with comprehensive platform statistics and health monitoring +* ✅ General Statistics and System Metrics dashboards with comprehensive platform statistics and health monitoring * ✅ Recent Activity Monitoring with real-time user and system activity tracking * 🔄 Backup and Export Management (placeholder UI exists, actual functionality pending) * 🔄 Advanced analytics and reporting features (basic stats implemented, advanced features pending) diff --git a/docs/maintenance/changelog.md b/docs/maintenance/changelog.md index 9ffb0479..b045dc8a 100644 --- a/docs/maintenance/changelog.md +++ b/docs/maintenance/changelog.md @@ -1343,7 +1343,7 @@ Fixed critical issues with API protocol handling and search endpoint redirects. #### **Admin Dashboard Enhancement** -- **System Overview Dashboard**: Comprehensive platform statistics and health +- **General Statistics & System Metrics**: Comprehensive platform statistics and health monitoring - Platform statistics (users, content, engagement, geographic distribution) - System health monitoring (database, application, resources, external @@ -1372,7 +1372,7 @@ services) #### **Frontend Admin Interface** -- **Admin Dashboard Integration**: Clickable cards for System Overview and +- **Admin Dashboard Integration**: Clickable cards for General Statistics, System Metrics, and Recent Activity - **Navigation Updates**: Added admin menu links for new monitoring pages - **Real-time Updates**: Auto-refresh functionality with manual refresh options diff --git a/frontend/src/App.js b/frontend/src/App.js index 157cb4c2..59adc875 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -24,7 +24,6 @@ import AdminNotificationPreferences from './pages/AdminNotificationPreferences'; import AdminOwnershipRequests from './pages/AdminOwnershipRequests'; import AdminRecentActivity from './pages/AdminRecentActivity'; import AdminSystemMetrics from './pages/AdminSystemMetrics'; -import AdminSystemOverview from './pages/AdminSystemOverview'; import AdminTags from './pages/AdminTags'; import AdminUsers from './pages/AdminUsers'; import API from './pages/API'; @@ -43,6 +42,8 @@ import DiveSites from './pages/DiveSites'; import DiveTrips from './pages/DiveTrips'; import DivingCenterDetail from './pages/DivingCenterDetail'; import DivingCenters from './pages/DivingCenters'; +import DivingOrganizationsPage from './pages/DivingOrganizationsPage'; +import DivingTagsPage from './pages/DivingTagsPage'; import EditDive from './pages/EditDive'; import EditDiveSite from './pages/EditDiveSite'; import EditDivingCenter from './pages/EditDivingCenter'; @@ -156,6 +157,11 @@ function App() { } /> } /> + } /> + } + /> } /> } /> } /> @@ -384,14 +390,6 @@ function App() { } /> - - - - } - /> { return response.data; }; +export const getTagsWithCounts = async () => { + const response = await api.get('/api/v1/tags/with-counts'); + return response.data; +}; + export const createTag = async tagData => { const response = await api.post('/api/v1/tags/', tagData); return response.data; @@ -538,6 +543,22 @@ export const deleteTag = async tagId => { return response.data; }; +// Diving Organizations API functions +export const getDivingOrganizations = async (params = {}) => { + const response = await api.get('/api/v1/diving-organizations/', { params }); + return response.data; +}; + +export const getDivingOrganization = async identifier => { + const response = await api.get(`/api/v1/diving-organizations/${identifier}`); + return response.data; +}; + +export const getDivingOrganizationLevels = async identifier => { + const response = await api.get(`/api/v1/diving-organizations/${identifier}/levels`); + return response.data; +}; + // Newsletter API functions export const uploadNewsletter = async (file, useOpenai = true) => { const formData = new FormData(); @@ -656,11 +677,6 @@ export const confirmImportDives = async divesData => { }; // System Overview API functions -export const getSystemOverview = async () => { - const response = await api.get('/api/v1/admin/system/overview'); - return response.data; -}; - export const getSystemHealth = async () => { const response = await api.get('/api/v1/admin/system/health'); return response.data; diff --git a/frontend/src/components/GlobalSearchBar.js b/frontend/src/components/GlobalSearchBar.js index f8e827cb..8aa3a863 100644 --- a/frontend/src/components/GlobalSearchBar.js +++ b/frontend/src/components/GlobalSearchBar.js @@ -20,6 +20,7 @@ const GlobalSearchBar = ({ className = '', inputClassName = '', placeholder = 'Search dives, sites, centers...', + popoverClassName, }) => { const [query, setQuery] = useState(''); const [results, setResults] = useState(null); @@ -68,7 +69,6 @@ const GlobalSearchBar = ({ const IconComponent = ENTITY_ICONS[group.icon_name] || Search; return { label: group.entity_type.replace(/_/g, ' '), - icon: , options: group.results.map(item => ({ value: `${group.entity_type}-${item.id}`, label: item.name, @@ -129,6 +129,7 @@ const GlobalSearchBar = ({ query.length >= 3 ? `No results found for "${query}"` : 'Type at least 3 characters...' } error={error} + popoverClassName={popoverClassName} /> ); }; @@ -137,6 +138,7 @@ GlobalSearchBar.propTypes = { className: PropTypes.string, inputClassName: PropTypes.string, placeholder: PropTypes.string, + popoverClassName: PropTypes.string, }; export default GlobalSearchBar; diff --git a/frontend/src/components/Navbar.js b/frontend/src/components/Navbar.js index b13ea528..78cbe1a5 100644 --- a/frontend/src/components/Navbar.js +++ b/frontend/src/components/Navbar.js @@ -25,6 +25,7 @@ import { Code, Bell, BarChart3, + Award, } from 'lucide-react'; import React, { useState, useEffect } from 'react'; import { createPortal } from 'react-dom'; @@ -44,6 +45,7 @@ const Navbar = () => { const location = useLocation(); const { isMobile, navbarVisible } = useResponsiveScroll(); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [showMobileResourcesDropdown, setShowMobileResourcesDropdown] = useState(false); const [showMobileInfoDropdown, setShowMobileInfoDropdown] = useState(false); const [showMobileAdminDropdown, setShowMobileAdminDropdown] = useState(false); @@ -58,6 +60,7 @@ const Navbar = () => { const closeMobileMenu = () => { setIsMobileMenuOpen(false); + setShowMobileResourcesDropdown(false); setShowMobileInfoDropdown(false); setShowMobileAdminDropdown(false); }; @@ -213,6 +216,30 @@ const Navbar = () => { Dive Trips + + + Resources + + + } + items={[ + { + type: 'item', + label: 'Diving Organizations', + icon: , + onClick: () => navigate('/resources/diving-organizations'), + }, + { + type: 'item', + label: 'Tags', + icon: , + onClick: () => navigate('/resources/tags'), + }, + ]} + /> + @@ -293,6 +320,12 @@ const Navbar = () => { icon: , onClick: () => navigate('/admin/diving-centers'), }, + { + type: 'item', + label: 'Diving Organizations', + icon: , + onClick: () => navigate('/admin/diving-organizations'), + }, { type: 'item', label: 'Tags', @@ -428,6 +461,7 @@ const Navbar = () => { className='flex-1' inputClassName='bg-white text-gray-900' placeholder='Search dives, sites, centers...' + popoverClassName='z-[100000]' /> + + {showMobileResourcesDropdown && ( +
+ + + Diving Organizations + + + + Tags + +
+ )} + +
-
- - ); - } - - return ( -
- {/* Header */} -
-
-
-

System Overview

-

Platform statistics and system health monitoring

- {systemOverview?.last_updated && ( -

- Last updated: {new Date(systemOverview.last_updated).toLocaleString()} -

- )} -
- -
-
- - {/* System Health Status */} -
-

System Health

-
- {/* Overall Status */} -
-
-
-

Overall Status

-

- {systemHealth?.status || 'Unknown'} -

-
-
- {getStatusIcon(systemHealth?.status)} -
-
-
- - {/* Database */} -
-
-
-

Database

-

- {systemHealth?.database?.healthy ? 'Healthy' : 'Unhealthy'} -

-

- {systemHealth?.database?.response_time_ms}ms -

-
- -
-
- - {/* CPU Usage */} -
-
-
-

CPU Usage

-

- {formatPercentage(systemHealth?.resources?.cpu?.usage_percent)} -

-

{systemHealth?.resources?.cpu?.cores} cores

-
- -
-
- - {/* Memory Usage */} -
-
-
-

Memory Usage

-

- {formatPercentage(systemHealth?.resources?.memory?.usage_percent)} -

-

- {systemHealth?.resources?.memory?.available_gb}GB available -

-
- -
-
-
-
- - {/* Storage Health Status */} -
-

Storage Health

- {storageLoading ? ( -
-
-
- - Loading storage health status... -
-
-
- ) : storageError ? ( -
-
-
- - Failed to load storage health status -
-
-
- ) : ( -
- {/* R2 Configuration Status */} -
-
-
-

R2 Configuration

-

- {storageHealth?.r2_available ? 'Configured' : 'Not Configured'} -

-

- {storageHealth?.r2_available - ? 'Environment variables present' - : 'Missing credentials'} -

-
-
- {storageHealth?.r2_available ? ( - - ) : ( - - )} -
-
-
- - {/* R2 Connectivity Status */} -
-
-
-

R2 Connectivity

-

- {storageHealth?.r2_connectivity ? 'Connected' : 'Disconnected'} -

-

- {storageHealth?.r2_connectivity ? 'Bucket accessible' : 'Cannot reach R2'} -

-
-
- {storageHealth?.r2_connectivity ? ( - - ) : ( - - )} -
-
-
- - {/* Local Storage Status */} -
-
-
-

Local Storage

-

- {storageHealth?.local_storage_available ? 'Available' : 'Unavailable'} -

-

- {storageHealth?.local_storage_writable ? 'Writable' : 'Read-only'} -

-
-
- {storageHealth?.local_storage_available ? ( - - ) : ( - - )} -
-
-
- - {/* Active Storage Mode */} -
-
-
-

Active Storage

-

- {storageHealth?.r2_connectivity ? 'R2 Cloud' : 'Local Only'} -

-

- {storageHealth?.r2_connectivity - ? 'Using cloud storage' - : 'Using local fallback'} -

-
-
- {storageHealth?.r2_connectivity ? ( - - ) : ( - - )} -
-
-
-
- )} -
- - {/* Platform Statistics */} -
-

Platform Statistics

- - {/* User Statistics */} -
-
-
-
-

Total Users

-

- {formatNumber(systemOverview?.platform_stats?.users?.total)} -

-
- {systemOverview?.platform_stats?.users?.growth_rate > 0 ? ( - - ) : ( - - )} - 0 - ? 'text-green-600' - : 'text-red-600' - }`} - > - {systemOverview?.platform_stats?.users?.growth_rate}% - -
-
- -
-
- -
-
-
-

Dive Sites

-

- {formatNumber(systemOverview?.platform_stats?.content?.dive_sites)} -

-

Total locations

-
- -
-
- -
-
-
-

Diving Centers

-

- {formatNumber(systemOverview?.platform_stats?.content?.diving_centers)} -

-

Registered centers

-
- -
-
- -
-
-
-

Dives Logged

-

- {formatNumber(systemOverview?.platform_stats?.content?.dives)} -

-

User dives

-
- -
-
-
- - {/* Engagement Statistics */} -
-
-
-
-

Avg Site Rating

-

- {systemOverview?.platform_stats?.engagement?.avg_site_rating}/10 -

-

Dive sites

-
- -
-
- -
-
-
-

Avg Center Rating

-

- {systemOverview?.platform_stats?.engagement?.avg_center_rating}/10 -

-

Diving centers

-
- -
-
- -
-
-
-

Comments (24h)

-

- {formatNumber(systemOverview?.platform_stats?.engagement?.recent_comments_24h)} -

-

Recent activity

-
- -
-
- -
-
-
-

Media Uploads

-

- {formatNumber(systemOverview?.platform_stats?.content?.media_uploads)} -

-

Photos & videos

-
- -
-
-
- - {/* Geographic Distribution */} -
-

- - Geographic Distribution -

-
- {systemOverview?.platform_stats?.geographic?.dive_sites_by_country - ?.slice(0, 6) - .map((item, index) => ( -
- {item.country} - {item.count} sites -
- ))} -
-
- - {/* System Usage */} -
-

- - System Usage -

-
-
-

API Calls Today

-

- {formatNumber(systemOverview?.platform_stats?.system_usage?.api_calls_today)} -

-
-
-

Peak Usage Time

-

- {systemOverview?.platform_stats?.system_usage?.peak_usage_time} -

-
-
-

Most Accessed Endpoint

-

- {systemOverview?.platform_stats?.system_usage?.most_accessed_endpoint} -

-
-
-
-
- - {/* Turnstile Statistics */} -
-

Turnstile Bot Protection

- - {/* Turnstile Overview Cards */} -
-
-
-
-

Success Rate

-

- {turnstileStats?.success_rate - ? formatPercentage(turnstileStats.success_rate * 100) - : 'N/A'} -

-

Verification success

-
- -
-
- -
-
-
-

Response Time

-

- {turnstileStats?.average_response_time_ms - ? `${turnstileStats.average_response_time_ms.toFixed(1)}ms` - : 'N/A'} -

-

Average verification

-
- -
-
- -
-
-
-

Total Events

-

- {formatNumber(turnstileStats?.total_events || 0)} -

-

Verification attempts

-
- -
-
- -
-
-
-

Status

-

- {turnstileStats?.monitoring_active ? 'Active' : 'Inactive'} -

-

Monitoring system

-
- -
-
-
- - {/* Error Breakdown */} - {turnstileStats?.error_breakdown && - Object.keys(turnstileStats.error_breakdown).length > 0 && ( -
-

- - Error Breakdown (Last 24h) -

-
- {Object.entries(turnstileStats.error_breakdown).map(([errorCode, count]) => ( -
- {errorCode} - {count} occurrences -
- ))} -
-
- )} - - {/* Top IP Addresses */} - {turnstileStats?.top_ips && turnstileStats.top_ips.length > 0 && ( -
-

- - Top IP Addresses (Last 24h) -

-
- {turnstileStats.top_ips.map(([ip, count], index) => ( -
- {ip} - {count} requests -
- ))} -
-
- )} - - {/* No Data Message */} - {(!turnstileStats || turnstileStats.total_events === 0) && ( -
- -

No Turnstile Data Available

-

- Turnstile verification events will appear here once users start using the - authentication system. -

-
- )} -
- - {/* Alerts */} - {systemOverview?.alerts && ( -
-

System Alerts

-
- {systemOverview.alerts.critical?.length > 0 && ( -
-

Critical Alerts

-
    - {systemOverview.alerts.critical.map((alert, index) => ( -
  • - - {alert} -
  • - ))} -
-
- )} - - {systemOverview.alerts.warnings?.length > 0 && ( -
-

Warnings

-
    - {systemOverview.alerts.warnings.map((alert, index) => ( -
  • - - {alert} -
  • - ))} -
-
- )} - - {systemOverview.alerts.info?.length > 0 && ( -
-

Information

-
    - {systemOverview.alerts.info.map((alert, index) => ( -
  • - - {alert} -
  • - ))} -
-
- )} - - {!systemOverview.alerts.critical?.length && - !systemOverview.alerts.warnings?.length && - !systemOverview.alerts.info?.length && ( -
-

- - No active alerts. All systems are operating normally. -

-
- )} -
-
- )} -
- ); -}; - -export default AdminSystemOverview; diff --git a/frontend/src/pages/DivingOrganizationsPage.js b/frontend/src/pages/DivingOrganizationsPage.js new file mode 100644 index 00000000..50b635b4 --- /dev/null +++ b/frontend/src/pages/DivingOrganizationsPage.js @@ -0,0 +1,210 @@ +import { Award, ChevronDown, ChevronUp, Search, ExternalLink, Globe } from 'lucide-react'; +import { useState } from 'react'; +import { useQuery } from 'react-query'; + +import { getDivingOrganizations, getDivingOrganizationLevels } from '../api'; +import usePageTitle from '../hooks/usePageTitle'; + +const CertificationLevelsList = ({ organizationId, identifier }) => { + const { data: levels, isLoading } = useQuery( + ['organization-levels', organizationId], + () => getDivingOrganizationLevels(identifier), + { + staleTime: 60 * 60 * 1000, // 1 hour + } + ); + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!levels || levels.length === 0) { + return
No certification levels found.
; + } + + return ( +
+

+ Certification Levels +

+
+ {levels.map(level => ( +
+ {level.name} + {level.abbreviation} +
+ ))} +
+
+ ); +}; + +const OrganizationLogo = ({ org }) => { + const [imageError, setImageError] = useState(false); + + if (org.logo_url && !imageError) { + return ( +
+ {`${org.name} setImageError(true)} + /> +
+ ); + } + + return ( +
+ {org.acronym || org.name.substring(0, 2).toUpperCase()} +
+ ); +}; + +const OrganizationCard = ({ org }) => { + const [isExpanded, setIsExpanded] = useState(false); + + return ( +
+
setIsExpanded(!isExpanded)}> +
+
+ +
+

{org.name}

+ {org.acronym &&

{org.acronym}

} +
+
+
+ +
+ {isExpanded ? : } +
+
+
+ + {org.website && ( +
e.stopPropagation()} + > + + + Visit Website + + +
+ )} +
+ + {isExpanded && } +
+ ); +}; + +const DivingOrganizationsPage = () => { + usePageTitle('Divemap - Diving Organizations'); + const [searchTerm, setSearchTerm] = useState(''); + + const { data: organizations, isLoading } = useQuery( + 'public-organizations', + () => getDivingOrganizations({ limit: 100 }), // Get all reasonable amount + { + staleTime: 60 * 60 * 1000, // 1 hour + } + ); + + const filteredOrgs = organizations?.filter(org => { + const term = searchTerm.toLowerCase(); + return ( + org.name.toLowerCase().includes(term) || + (org.acronym && org.acronym.toLowerCase().includes(term)) + ); + }); + + return ( +
+
+
+
+
+

+ + Diving Organizations +

+

+ Browse recognized diving organizations and their certification levels. +

+
+
+
+ +
+ setSearchTerm(e.target.value)} + /> +
+
+
+ + {isLoading ? ( +
+
+
+ ) : ( +
+ {filteredOrgs?.length > 0 ? ( +
+ {filteredOrgs.map(org => ( + + ))} +
+ ) : ( +
+ +

No organizations found

+

+ {searchTerm + ? `No organizations matching "${searchTerm}"` + : 'There are no diving organizations in the system yet.'} +

+
+ )} +
+ )} +
+
+ ); +}; + +export default DivingOrganizationsPage; diff --git a/frontend/src/pages/DivingTagsPage.js b/frontend/src/pages/DivingTagsPage.js new file mode 100644 index 00000000..976b24d6 --- /dev/null +++ b/frontend/src/pages/DivingTagsPage.js @@ -0,0 +1,99 @@ +import { Tags, Search, Hash } from 'lucide-react'; +import { useState } from 'react'; +import { useQuery } from 'react-query'; + +import { getTagsWithCounts } from '../api'; +import usePageTitle from '../hooks/usePageTitle'; + +const DivingTagsPage = () => { + usePageTitle('Divemap - Diving Tags'); + const [searchTerm, setSearchTerm] = useState(''); + + const { data: tags, isLoading } = useQuery('public-tags', getTagsWithCounts, { + staleTime: 5 * 60 * 1000, // 5 minutes + }); + + const filteredTags = tags?.filter( + tag => + tag.name.toLowerCase().includes(searchTerm.toLowerCase()) || + (tag.description && tag.description.toLowerCase().includes(searchTerm.toLowerCase())) + ); + + return ( +
+
+
+
+
+

+ + Diving Tags +

+

+ Explore the tags used to categorize dive sites and experiences. +

+
+
+
+ +
+ setSearchTerm(e.target.value)} + /> +
+
+
+ + {isLoading ? ( +
+
+
+ ) : ( +
+ {filteredTags?.length > 0 ? ( +
+ {filteredTags.map(tag => ( +
+
+
+ + + +

{tag.name}

+
+ + {tag.dive_site_count} {tag.dive_site_count === 1 ? 'site' : 'sites'} + +
+ {tag.description && ( +

{tag.description}

+ )} +
+ ))} +
+ ) : ( +
+ +

No tags found

+

+ {searchTerm + ? `No tags matching "${searchTerm}"` + : 'There are no tags in the system yet.'} +

+
+ )} +
+ )} +
+
+ ); +}; + +export default DivingTagsPage;