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
+
+ (0 duplicates)
+
@@ -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
```