From f24bd11dfd80cc2f003dd6c98c9dbe9e17d8a245 Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Fri, 21 Nov 2025 16:03:53 -0600 Subject: [PATCH] Add unique IP count and duplicate detection to sites page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sites page previously showed total IP count which included duplicates across multiple sites, leading to inflated numbers. Now displays unique IP count as the primary metric with duplicate count shown when present. - Add get_global_ip_stats() method to SiteService for unique/duplicate counts - Update /api/sites?all=true endpoint to include IP statistics - Update sites.html to display unique IPs with optional duplicate indicator - Update API documentation with new response fields 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/web/api/sites.py | 8 +++++++- app/web/services/site_service.py | 28 ++++++++++++++++++++++++++++ app/web/templates/sites.html | 23 +++++++++++++++++------ docs/API_REFERENCE.md | 31 +++++++++++++++++++++++++++++-- 4 files changed, 81 insertions(+), 9 deletions(-) diff --git a/app/web/api/sites.py b/app/web/api/sites.py index 6440c3e..c1ccc5a 100644 --- a/app/web/api/sites.py +++ b/app/web/api/sites.py @@ -36,9 +36,15 @@ def list_sites(): if request.args.get('all', '').lower() == 'true': site_service = SiteService(current_app.db_session) sites = site_service.list_all_sites() + ip_stats = site_service.get_global_ip_stats() logger.info(f"Listed all sites (count={len(sites)})") - return jsonify({'sites': sites}) + return jsonify({ + 'sites': sites, + 'total_ips': ip_stats['total_ips'], + 'unique_ips': ip_stats['unique_ips'], + 'duplicate_ips': ip_stats['duplicate_ips'] + }) # Get and validate query parameters page = request.args.get('page', 1, type=int) diff --git a/app/web/services/site_service.py b/app/web/services/site_service.py index f4a4fb7..739e60d 100644 --- a/app/web/services/site_service.py +++ b/app/web/services/site_service.py @@ -228,6 +228,34 @@ class SiteService: return [self._site_to_dict(site) for site in sites] + def get_global_ip_stats(self) -> Dict[str, int]: + """ + Get global IP statistics across all sites. + + Returns: + Dictionary with: + - total_ips: Total count of IP entries (including duplicates) + - unique_ips: Count of distinct IP addresses + - duplicate_ips: Number of duplicate entries (total - unique) + """ + # Total IP entries + total_ips = ( + self.db.query(func.count(SiteIP.id)) + .scalar() or 0 + ) + + # Unique IP addresses + unique_ips = ( + self.db.query(func.count(func.distinct(SiteIP.ip_address))) + .scalar() or 0 + ) + + return { + 'total_ips': total_ips, + 'unique_ips': unique_ips, + 'duplicate_ips': total_ips - unique_ips + } + def bulk_add_ips_from_cidr(self, site_id: int, cidr: str, expected_ping: Optional[bool] = None, expected_tcp_ports: Optional[List[int]] = None, diff --git a/app/web/templates/sites.html b/app/web/templates/sites.html index 074ba3b..0684fa1 100644 --- a/app/web/templates/sites.html +++ b/app/web/templates/sites.html @@ -26,8 +26,11 @@
-
-
-
Total IPs
+
-
+
Unique IPs
+
@@ -499,7 +502,7 @@ async function loadSites() { const data = await response.json(); sitesData = data.sites || []; - updateStats(); + updateStats(data.unique_ips, data.duplicate_ips); renderSites(sitesData); document.getElementById('sites-loading').style.display = 'none'; @@ -514,12 +517,20 @@ async function loadSites() { } // Update summary stats -function updateStats() { +function updateStats(uniqueIps, duplicateIps) { const totalSites = sitesData.length; - const totalIps = sitesData.reduce((sum, site) => sum + (site.ip_count || 0), 0); document.getElementById('total-sites').textContent = totalSites; - document.getElementById('total-ips').textContent = totalIps; + document.getElementById('unique-ips').textContent = uniqueIps || 0; + + // Show duplicate count if there are any + if (duplicateIps && duplicateIps > 0) { + document.getElementById('duplicate-ips').textContent = duplicateIps; + document.getElementById('duplicate-ips-label').style.display = 'block'; + } else { + document.getElementById('duplicate-ips-label').style.display = 'none'; + } + document.getElementById('sites-in-use').textContent = '-'; // Will be updated async // Count sites in use (async) diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 1bcc7ad..10f4aff 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -117,7 +117,7 @@ Retrieve a paginated list of all sites. | `per_page` | integer | No | 20 | Items per page (1-100) | | `all` | string | No | - | Set to "true" to return all sites without pagination | -**Success Response (200 OK):** +**Success Response (200 OK) - Paginated:** ```json { "sites": [ @@ -139,13 +139,40 @@ Retrieve a paginated list of all sites. } ``` +**Success Response (200 OK) - All Sites (all=true):** +```json +{ + "sites": [ + { + "id": 1, + "name": "Production DC", + "description": "Production datacenter servers", + "ip_count": 25, + "created_at": "2025-11-19T10:30:00Z", + "updated_at": "2025-11-19T10:30:00Z" + } + ], + "total_ips": 100, + "unique_ips": 85, + "duplicate_ips": 15 +} +``` + +**Response Fields (all=true):** + +| Field | Type | Description | +|-------|------|-------------| +| `total_ips` | integer | Total count of IP entries across all sites (including duplicates) | +| `unique_ips` | integer | Count of distinct IP addresses | +| `duplicate_ips` | integer | Number of duplicate IP entries (total_ips - unique_ips) | + **Usage Example:** ```bash # List first page curl -X GET http://localhost:5000/api/sites \ -b cookies.txt -# Get all sites (for dropdowns) +# Get all sites with global IP stats curl -X GET "http://localhost:5000/api/sites?all=true" \ -b cookies.txt ```