Add unique IP count and duplicate detection to sites page

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 <noreply@anthropic.com>
This commit is contained in:
2025-11-21 16:03:53 -06:00
parent 9bd2f67150
commit f24bd11dfd
4 changed files with 81 additions and 9 deletions

View File

@@ -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)

View File

@@ -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,

View File

@@ -26,8 +26,11 @@
</div>
<div class="col-md-4">
<div class="stat-card">
<div class="stat-value" id="total-ips">-</div>
<div class="stat-label">Total IPs</div>
<div class="stat-value" id="unique-ips">-</div>
<div class="stat-label">Unique IPs</div>
<div class="stat-sublabel" id="duplicate-ips-label" style="display: none; font-size: 0.75rem; color: #fbbf24;">
(<span id="duplicate-ips">0</span> duplicates)
</div>
</div>
</div>
<div class="col-md-4">
@@ -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)

View File

@@ -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
```