improved pagination

This commit is contained in:
2025-04-08 08:58:47 -04:00
parent 01ecccc928
commit de13c8b2f9
2 changed files with 214 additions and 147 deletions

View File

@@ -18,6 +18,14 @@
<option value="last_30_days" {% if time_range == 'last_30_days' %}selected{% endif %}>Last 30 Days</option> <option value="last_30_days" {% if time_range == 'last_30_days' %}selected{% endif %}>Last 30 Days</option>
<option value="all" {% if time_range == 'all' %}selected{% endif %}>All Time</option> <option value="all" {% if time_range == 'all' %}selected{% endif %}>All Time</option>
</select> </select>
<label for="per_page">Entries per page:</label>
<select name="per_page" id="per_page">
{% for option in [5,10, 25, 50, 100] %}
<option value="{{ option }}" {% if per_page == option %}selected{% endif %}>{{ option }}</option>
{% endfor %}
</select>
<button type="submit">Update</button> <button type="submit">Update</button>
</form> </form>
@@ -27,150 +35,188 @@
Auto-refresh every 30s Auto-refresh every 30s
</label> </label>
<span id="refresh-status"></span> <span id="refresh-status"></span>
</div> </div>
<div class="stats-container"> <div class="stats-container">
<!-- Access-Accept Card --> <!-- Access-Accept Card -->
<div class="card success-card"> <div class="card success-card">
<h2>Recent Access-Accept</h2> <h2>Recent Access-Accept</h2>
<table class="styled-table small-table"> <table class="styled-table small-table">
<thead> <thead>
<tr> <tr>
<th>MAC Address</th> <th>MAC Address</th>
<th>Description</th> <th>Description</th>
<th>Vendor</th> <th>Vendor</th>
<th>VLAN</th> <th>VLAN</th>
<th>Time</th> <th>Time</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for entry in accept_entries %} {% for entry in accept_entries %}
<tr> <tr>
<td>{{ entry.mac_address }}</td> <td>{{ entry.mac_address }}</td>
<td>{{ entry.description or '' }}</td> <td>{{ entry.description or '' }}</td>
<td class="vendor-cell" data-mac="{{ entry.mac_address }}">{{ entry.vendor or '...' }}</td> <td class="vendor-cell" data-mac="{{ entry.mac_address }}">{{ entry.vendor or '...' }}</td>
<td>{{ entry.vlan_id or '?' }}</td> <td>{{ entry.vlan_id or '?' }}</td>
<td>{{ entry.ago }}</td> <td>{{ entry.ago }}</td>
</tr> </tr>
{% endfor %}
</tbody>
</table>
{% if total_pages_accept > 1 %}
<div class="pagination">
{% for page in range(1, total_pages_accept + 1) %}
{% if page == page_accept %}
<span class="current-page">{{ page }}</span>
{% else %}
<a href="{{ url_for('stats.stats_page', page_accept=page, page_reject=page_reject, page_fallback=page_fallback, time_range=time_range) }}">{{ page }}</a>
{% endif %}
{% endfor %} {% endfor %}
</div> </tbody>
</table>
{% if pagination_accept.pages|length > 1 %}
<div class="pagination">
{% if pagination_accept.show_first %}
<a href="{{ url_for('stats.stats_page', page_accept=pagination_accept.first_page, page_reject=page_reject, page_fallback=page_fallback, per_page=per_page, time_range=time_range) }}">&laquo;</a>
{% endif %}
{% if pagination_accept.show_prev %}
<a href="{{ url_for('stats.stats_page', page_accept=pagination_accept.prev_page, page_reject=page_reject, page_fallback=page_fallback, per_page=per_page, time_range=time_range) }}">&lsaquo;</a>
{% endif %} {% endif %}
</div>
<!-- Access-Reject Card --> {% for page in pagination_accept.pages %}
<div class="card error-card"> {% if page == page_accept %}
<h2>Recent Access-Reject</h2> <span class="current-page">{{ page }}</span>
<table class="styled-table small-table"> {% else %}
<thead> <a href="{{ url_for('stats.stats_page', page_accept=page, page_reject=page_reject, page_fallback=page_fallback, per_page=per_page, time_range=time_range) }}">{{ page }}</a>
<tr> {% endif %}
<th>MAC Address</th> {% endfor %}
<th>Description</th>
<th>Vendor</th>
<th>Time</th>
</tr>
</thead>
<tbody>
{% for entry in reject_entries %}
<tr>
<td>{{ entry.mac_address }}</td>
<td>{{ entry.description or '' }}</td>
<td class="vendor-cell" data-mac="{{ entry.mac_address }}">{{ entry.vendor or '...' }}</td>
<td>{{ entry.ago }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if total_pages_reject > 1 %}
<div class="pagination">
{% for page in range(1, total_pages_reject + 1) %}
{% if page == page_reject %}
<span class="current-page">{{ page }}</span>
{% else %}
<a href="{{ url_for('stats.stats_page', page_accept=page_accept, page_reject=page, page_fallback=page_fallback, time_range=time_range) }}">{{ page }}</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
<!-- Access-Fallback Card --> {% if pagination_accept.show_next %}
<div class="card fallback-card"> <a href="{{ url_for('stats.stats_page', page_accept=pagination_accept.next_page, page_reject=page_reject, page_fallback=page_fallback, per_page=per_page, time_range=time_range) }}">&rsaquo;</a>
<h2>Recent Access-Fallback</h2> {% endif %}
<table class="styled-table small-table"> {% if pagination_accept.show_last %}
<thead> <a href="{{ url_for('stats.stats_page', page_accept=pagination_accept.last_page, page_reject=page_reject, page_fallback=page_fallback, per_page=per_page, time_range=time_range) }}">&raquo;</a>
<tr>
<th>MAC Address</th>
<th>Description</th>
<th>Vendor</th>
<th>Time</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for entry in fallback_entries %}
<tr>
<td>{{ entry.mac_address }}</td>
<td>
{% if not entry.already_exists %}
<input type="text" name="description" value="{{ entry.description or '' }}" placeholder="Description (optional)" form="form-{{ loop.index }}">
{% else %}
{{ entry.description or '' }}
{% endif %}
</td>
<td class="vendor-cell" data-mac="{{ entry.mac_address }}">{{ entry.vendor or '...' }}</td>
<td>{{ entry.ago }}</td>
<td>
{% if not entry.already_exists %}
<form method="POST" action="{{ url_for('stats.add') }}" class="inline-form" id="form-{{ loop.index }}">
<input type="hidden" name="mac_address" value="{{ entry.mac_address }}">
<select name="group_id" required>
<option value="">Assign to VLAN</option>
{% for group in available_groups %}
<option value="{{ group.vlan_id }}">
VLAN {{ group.vlan_id }}{% if group.description %} - {{ group.description }}{% endif %}
</option>
{% endfor %}
</select>
<button type="submit" title="Add">💾</button>
</form>
{% else %}
<span style="color: limegreen;">Already exists in VLAN {{ entry.existing_vlan or 'unknown' }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if total_pages_fallback > 1 %}
<div class="pagination">
{% for page in range(1, total_pages_fallback + 1) %}
{% if page == page_fallback %}
<span class="current-page">{{ page }}</span>
{% else %}
<a href="{{ url_for('stats.stats_page', page_accept=page_accept, page_reject=page_reject, page_fallback=page, time_range=time_range) }}">{{ page }}</a>
{% endif %}
{% endfor %}
</div>
{% endif %} {% endif %}
</div> </div>
{% endif %}
</div>
<!-- Access-Reject Card -->
<div class="card error-card">
<h2>Recent Access-Reject</h2>
<table class="styled-table small-table">
<thead>
<tr>
<th>MAC Address</th>
<th>Description</th>
<th>Vendor</th>
<th>Time</th>
</tr>
</thead>
<tbody>
{% for entry in reject_entries %}
<tr>
<td>{{ entry.mac_address }}</td>
<td>{{ entry.description or '' }}</td>
<td class="vendor-cell" data-mac="{{ entry.mac_address }}">{{ entry.vendor or '...' }}</td>
<td>{{ entry.ago }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if pagination_reject.pages|length > 1 %}
<div class="pagination">
{% if pagination_reject.show_first %}
<a href="{{ url_for('stats.stats_page', page_accept=page_accept, page_reject=pagination_reject.first_page, page_fallback=page_fallback, per_page=per_page, time_range=time_range) }}">&laquo;</a>
{% endif %}
{% if pagination_reject.show_prev %}
<a href="{{ url_for('stats.stats_page', page_accept=page_accept, page_reject=pagination_reject.prev_page, page_fallback=page_fallback, per_page=per_page, time_range=time_range) }}">&lsaquo;</a>
{% endif %}
{% for page in pagination_reject.pages %}
{% if page == page_reject %}
<span class="current-page">{{ page }}</span>
{% else %}
<a href="{{ url_for('stats.stats_page', page_accept=page_accept, page_reject=page, page_fallback=page_fallback, per_page=per_page, time_range=time_range) }}">{{ page }}</a>
{% endif %}
{% endfor %}
{% if pagination_reject.show_next %}
<a href="{{ url_for('stats.stats_page', page_accept=page_accept, page_reject=pagination_reject.next_page, page_fallback=page_fallback, per_page=per_page, time_range=time_range) }}">&rsaquo;</a>
{% endif %}
{% if pagination_reject.show_last %}
<a href="{{ url_for('stats.stats_page', page_accept=page_accept, page_reject=pagination_reject.last_page, page_fallback=page_fallback, per_page=per_page, time_range=time_range) }}">&raquo;</a>
{% endif %}
</div>
{% endif %}
</div>
<!-- Access-Fallback Card -->
<div class="card fallback-card">
<h2>Recent Access-Fallback</h2>
<table class="styled-table small-table">
<thead>
<tr>
<th>MAC Address</th>
<th>Description</th>
<th>Vendor</th>
<th>Time</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for entry in fallback_entries %}
<tr>
<td>{{ entry.mac_address }}</td>
<td>
{% if not entry.already_exists %}
<input type="text" name="description" value="{{ entry.description or '' }}" placeholder="Description (optional)" form="form-{{ loop.index }}">
{% else %}
{{ entry.description or '' }}
{% endif %}
</td>
<td class="vendor-cell" data-mac="{{ entry.mac_address }}">{{ entry.vendor or '...' }}</td>
<td>{{ entry.ago }}</td>
<td>
{% if not entry.already_exists %}
<form method="POST" action="{{ url_for('stats.add') }}" class="inline-form" id="form-{{ loop.index }}">
<input type="hidden" name="mac_address" value="{{ entry.mac_address }}">
<select name="group_id" required>
<option value="">Assign to VLAN</option>
{% for group in available_groups %}
<option value="{{ group.vlan_id }}">
VLAN {{ group.vlan_id }}{% if group.description %} - {{ group.description }}{% endif %}
</option>
{% endfor %}
</select>
<button type="submit" title="Add">💾</button>
</form>
{% else %}
<span style="color: limegreen;">Already exists in VLAN {{ entry.existing_vlan or 'unknown' }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if pagination_fallback.pages|length > 1 %}
<div class="pagination">
{% if pagination_fallback.show_first %}
<a href="{{ url_for('stats.stats_page', page_accept=page_accept, page_reject=page_reject, page_fallback=pagination_fallback.first_page, per_page=per_page, time_range=time_range) }}">&laquo;</a>
{% endif %}
{% if pagination_fallback.show_prev %}
<a href="{{ url_for('stats.stats_page', page_accept=page_accept, page_reject=page_reject, page_fallback=pagination_fallback.prev_page, per_page=per_page, time_range=time_range) }}">&lsaquo;</a>
{% endif %}
{% for page in pagination_fallback.pages %}
{% if page == page_fallback %}
<span class="current-page">{{ page }}</span>
{% else %}
<a href="{{ url_for('stats.stats_page', page_accept=page_accept, page_reject=page_reject, page_fallback=page, per_page=per_page, time_range=time_range) }}">{{ page }}</a>
{% endif %}
{% endfor %}
{% if pagination_fallback.show_next %}
<a href="{{ url_for('stats.stats_page', page_accept=page_accept, page_reject=page_reject, page_fallback=pagination_fallback.next_page, per_page=per_page, time_range=time_range) }}">&rsaquo;</a>
{% endif %}
{% if pagination_fallback.show_last %}
<a href="{{ url_for('stats.stats_page', page_accept=page_accept, page_reject=page_reject, page_fallback=pagination_fallback.last_page, per_page=per_page, time_range=time_range) }}">&raquo;</a>
{% endif %}
</div>
{% endif %}
</div>
</div> </div>
</div> </div>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// MAC vendor lookup
const queriedPrefixes = new Set(); const queriedPrefixes = new Set();
document.querySelectorAll('.vendor-cell').forEach(cell => { document.querySelectorAll('.vendor-cell').forEach(cell => {
const mac = cell.getAttribute('data-mac'); const mac = cell.getAttribute('data-mac');
@@ -200,7 +246,6 @@
} }
}); });
// Auto-refresh toggle logic
const refreshCheckbox = document.getElementById('auto-refresh-checkbox'); const refreshCheckbox = document.getElementById('auto-refresh-checkbox');
const refreshStatus = document.getElementById('refresh-status'); const refreshStatus = document.getElementById('refresh-status');
let intervalId = null; let intervalId = null;
@@ -209,7 +254,7 @@
refreshStatus.textContent = "Auto-refresh enabled"; refreshStatus.textContent = "Auto-refresh enabled";
intervalId = setInterval(() => { intervalId = setInterval(() => {
document.querySelector('form').submit(); document.querySelector('form').submit();
}, 30000); // 30 seconds }, 30000);
} }
function stopAutoRefresh() { function stopAutoRefresh() {
@@ -225,9 +270,8 @@
} }
}); });
// Default: start disabled
stopAutoRefresh(); stopAutoRefresh();
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -1,11 +1,11 @@
from flask import Blueprint, render_template, request, current_app, redirect, url_for, jsonify from flask import Blueprint, render_template, request, current_app, redirect, url_for, jsonify
from db_interface import get_latest_auth_logs, count_auth_logs, get_all_groups, get_vendor_info, get_user_by_mac, add_user, get_known_mac_vendors from db_interface import get_latest_auth_logs, count_auth_logs, get_all_groups, get_vendor_info, get_user_by_mac, add_user, get_known_mac_vendors
from math import ceil from math import ceil
import re
import pytz import pytz
import humanize import humanize
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from time import sleep from time import sleep
import threading
stats = Blueprint('stats', __name__) stats = Blueprint('stats', __name__)
@@ -21,33 +21,53 @@ def get_time_filter_delta(time_range):
"last_30_days": timedelta(days=30), "last_30_days": timedelta(days=30),
}.get(time_range) }.get(time_range)
def get_pagination_data(current_page, total_pages, max_display=7):
pagination = []
if total_pages <= max_display:
pagination = list(range(1, total_pages + 1))
else:
half = max_display // 2
if current_page <= half:
pagination = list(range(1, max_display + 1))
elif current_page >= total_pages - half:
pagination = list(range(total_pages - max_display + 1, total_pages + 1))
else:
pagination = list(range(current_page - half, current_page + half + 1))
return {
"pages": pagination,
"show_first": current_page > 1,
"show_last": current_page < total_pages,
"show_prev": current_page > 1,
"show_next": current_page < total_pages,
"prev_page": max(current_page - 1, 1),
"next_page": min(current_page + 1, total_pages),
"first_page": 1,
"last_page": total_pages
}
@stats.route('/stats', methods=['GET', 'POST']) @stats.route('/stats', methods=['GET', 'POST'])
def stats_page(): def stats_page():
time_range = request.form.get('time_range') or request.args.get('time_range') or 'last_minute' time_range = request.form.get('time_range') or request.args.get('time_range') or 'last_minute'
per_page = int(request.form.get('per_page') or request.args.get('per_page') or 25)
# Per-card pagination values
per_page = 25
page_accept = int(request.args.get('page_accept', 1)) page_accept = int(request.args.get('page_accept', 1))
page_reject = int(request.args.get('page_reject', 1)) page_reject = int(request.args.get('page_reject', 1))
page_fallback = int(request.args.get('page_fallback', 1)) page_fallback = int(request.args.get('page_fallback', 1))
# Timezone setup
tz_name = current_app.config.get('APP_TIMEZONE', 'UTC') tz_name = current_app.config.get('APP_TIMEZONE', 'UTC')
local_tz = pytz.timezone(tz_name) local_tz = pytz.timezone(tz_name)
# Accept pagination
total_accept = count_auth_logs('Access-Accept', time_range) total_accept = count_auth_logs('Access-Accept', time_range)
total_pages_accept = ceil(total_accept / per_page) total_pages_accept = ceil(total_accept / per_page)
offset_accept = (page_accept - 1) * per_page offset_accept = (page_accept - 1) * per_page
accept_entries = get_latest_auth_logs('Access-Accept', per_page, time_range, offset_accept) accept_entries = get_latest_auth_logs('Access-Accept', per_page, time_range, offset_accept)
# Reject pagination
total_reject = count_auth_logs('Access-Reject', time_range) total_reject = count_auth_logs('Access-Reject', time_range)
total_pages_reject = ceil(total_reject / per_page) total_pages_reject = ceil(total_reject / per_page)
offset_reject = (page_reject - 1) * per_page offset_reject = (page_reject - 1) * per_page
reject_entries = get_latest_auth_logs('Access-Reject', per_page, time_range, offset_reject) reject_entries = get_latest_auth_logs('Access-Reject', per_page, time_range, offset_reject)
# Fallback pagination
total_fallback = count_auth_logs('Accept-Fallback', time_range) total_fallback = count_auth_logs('Accept-Fallback', time_range)
total_pages_fallback = ceil(total_fallback / per_page) total_pages_fallback = ceil(total_fallback / per_page)
offset_fallback = (page_fallback - 1) * per_page offset_fallback = (page_fallback - 1) * per_page
@@ -64,40 +84,43 @@ def stats_page():
entry['ago'] = 'unknown' entry['ago'] = 'unknown'
vendor_info = get_vendor_info(entry['mac_address'], insert_if_found=False) vendor_info = get_vendor_info(entry['mac_address'], insert_if_found=False)
entry['vendor'] = vendor_info['vendor'] if vendor_info else None # placeholder entry['vendor'] = vendor_info['vendor'] if vendor_info else None
user = get_user_by_mac(entry['mac_address']) user = get_user_by_mac(entry['mac_address'])
entry['already_exists'] = user is not None entry['already_exists'] = user is not None
entry['existing_vlan'] = user['vlan_id'] if user else None entry['existing_vlan'] = user['vlan_id'] if user else None
entry['description'] = user['description'] if user else None entry['description'] = user['description'] if user else None
match = re.search(r'VLAN\s+(\d+)', entry.get('result', ''))
entry['vlan_id'] = match.group(1) if match else None
return entry return entry
# Enrich entries
accept_entries = [enrich(e) for e in accept_entries] accept_entries = [enrich(e) for e in accept_entries]
reject_entries = [enrich(e) for e in reject_entries] reject_entries = [enrich(e) for e in reject_entries]
fallback_entries = [enrich(e) for e in fallback_entries] fallback_entries = [enrich(e) for e in fallback_entries]
available_groups = get_all_groups() available_groups = get_all_groups()
return render_template( return render_template(
"stats.html", "stats.html",
time_range=time_range, time_range=time_range,
per_page=per_page,
accept_entries=accept_entries, accept_entries=accept_entries,
reject_entries=reject_entries, reject_entries=reject_entries,
fallback_entries=fallback_entries, fallback_entries=fallback_entries,
available_groups=available_groups, available_groups=available_groups,
page_accept=page_accept, page_accept=page_accept,
total_pages_accept=total_pages_accept, pagination_accept=get_pagination_data(page_accept, total_pages_accept),
page_reject=page_reject, page_reject=page_reject,
total_pages_reject=total_pages_reject, pagination_reject=get_pagination_data(page_reject, total_pages_reject),
page_fallback=page_fallback, page_fallback=page_fallback,
total_pages_fallback=total_pages_fallback pagination_fallback=get_pagination_data(page_fallback, total_pages_fallback)
) )
@stats.route('/add', methods=['POST']) @stats.route('/add', methods=['POST'])
def add(): def add():
mac = request.form['mac_address'] mac = request.form['mac_address']