Add LDAP avatar photos, UX polish, and TDS component upgrades
Lint / Python (flake8) (push) Successful in 1m13s
Lint / JS (eslint) (push) Successful in 9s
Security / Python Security (bandit) (push) Failing after 45s
Test / Python Tests (pytest) (push) Successful in 57s
Lint / Notify on failure (push) Has been skipped
Lint / Deploy (push) Successful in 5s
Lint / Python (flake8) (push) Successful in 1m13s
Lint / JS (eslint) (push) Successful in 9s
Security / Python Security (bandit) (push) Failing after 45s
Test / Python Tests (pytest) (push) Successful in 57s
Lint / Notify on failure (push) Has been skipped
Lint / Deploy (push) Successful in 5s
- Add /api/avatar endpoint querying lldap for user jpegPhoto; disk cache with sentinel pattern avoids repeat LDAP hits for users without photos - Add ldap3 dependency and ldap config block to config.json - Wire lt-avatar img overlay in base.html with capture-phase error fallback (lt-avatar-img-err) to reveal initials when image is absent - Fix lt-avatar CSS shim: position:relative + absolute inset on img (local base.css was missing these; added to style.css) - Replace all empty-state paragraphs with proper lt-empty-state markup (icon + title + body) across index, suppressions, inspector, app.js - Add lt-spinner--cyan next to refresh button; shows during refreshAll() - Replace inspector panel-section-title with lt-divider throughout - Add data-tooltip attributes to SFP DOM metrics, TX/RX/Carrier/Duplex/ Auto-neg/Error labels in links.html and inspector panel - Add tooltips to events table column headers (Sev, First Seen, Failures) - Fix links.html host panel timestamp (was reading sample.updated which is always undefined; now uses data.updated) - Fix UniFi status text casing (Online→ONLINE to match server render) - Remove dead topo-status-* class manipulation from updateTopology() - Always render alert-count-badge; toggle display:none when count is 0 - Fix double UniFi get_devices() call in monitor.py run loop - Fix chip-critical animation (was using green pulse-glow; now red) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import hashlib
|
||||
import ipaddress
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
@@ -15,7 +16,7 @@ import uuid
|
||||
from datetime import datetime, timezone
|
||||
from functools import wraps
|
||||
|
||||
from flask import Flask, jsonify, render_template, request
|
||||
from flask import Flask, jsonify, make_response, render_template, request, send_file
|
||||
|
||||
import db
|
||||
import diagnose
|
||||
@@ -169,6 +170,7 @@ def index():
|
||||
last_check=last_check,
|
||||
suppressions=suppressions,
|
||||
recent_resolved=recent_resolved,
|
||||
daemon_ok=_daemon_ok(last_check),
|
||||
)
|
||||
|
||||
|
||||
@@ -442,6 +444,82 @@ def api_diagnose_poll(job_id: str):
|
||||
return jsonify({'status': job['status'], 'result': job.get('result')})
|
||||
|
||||
|
||||
@app.route('/api/avatar')
|
||||
@require_auth
|
||||
def api_avatar():
|
||||
"""Serve the current user's LDAP avatar photo (JPEG), cached to disk."""
|
||||
username = request.headers.get('Remote-User', '').strip()
|
||||
if not username:
|
||||
return '', 404
|
||||
|
||||
ldap_cfg = _config().get('ldap', {})
|
||||
if not ldap_cfg.get('host') or not ldap_cfg.get('bind_dn'):
|
||||
return '', 404
|
||||
|
||||
# Build a safe cache filename from the username (alphanumeric + - _ .)
|
||||
safe_name = re.sub(r'[^a-zA-Z0-9._-]', '_', username)
|
||||
cache_dir = ldap_cfg.get('cache_dir', '/tmp/gandalf_avatars')
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
cache_file = os.path.join(cache_dir, f'user_{safe_name}.jpg')
|
||||
sentinel = os.path.join(cache_dir, f'user_{safe_name}.none')
|
||||
cache_ttl = int(ldap_cfg.get('cache_ttl', 3600))
|
||||
|
||||
now = time.time()
|
||||
|
||||
# Serve cached image if fresh
|
||||
if os.path.exists(cache_file) and now - os.path.getmtime(cache_file) < cache_ttl:
|
||||
return send_file(cache_file, mimetype='image/jpeg',
|
||||
max_age=cache_ttl, conditional=True)
|
||||
|
||||
# Skip LDAP if we already know this user has no avatar
|
||||
if os.path.exists(sentinel) and now - os.path.getmtime(sentinel) < cache_ttl:
|
||||
return '', 404
|
||||
|
||||
# Query lldap
|
||||
avatar_data = None
|
||||
try:
|
||||
import ldap3
|
||||
server = ldap3.Server(ldap_cfg['host'], port=int(ldap_cfg.get('port', 3890)))
|
||||
conn = ldap3.Connection(server,
|
||||
user=ldap_cfg['bind_dn'],
|
||||
password=ldap_cfg.get('bind_pw', ''),
|
||||
auto_bind=True, receive_timeout=5)
|
||||
safe_uid = ldap3.utils.conv.escape_filter_chars(username)
|
||||
conn.search(ldap_cfg.get('user_base', 'ou=people,dc=example,dc=com'),
|
||||
f'(uid={safe_uid})', attributes=['avatar'])
|
||||
if conn.entries and conn.entries[0]['avatar'].value:
|
||||
avatar_data = conn.entries[0]['avatar'].value
|
||||
conn.unbind()
|
||||
except ImportError:
|
||||
logger.error('ldap3 not installed — run: pip install ldap3')
|
||||
return '', 404
|
||||
except Exception as e:
|
||||
logger.error(f'LDAP avatar lookup failed for {username}: {e}')
|
||||
return '', 404
|
||||
|
||||
if not avatar_data or len(avatar_data) < 100:
|
||||
open(sentinel, 'w').close()
|
||||
return '', 404
|
||||
|
||||
# Validate JPEG magic bytes (FF D8 FF)
|
||||
if isinstance(avatar_data, str):
|
||||
avatar_data = avatar_data.encode('latin-1')
|
||||
if avatar_data[:3] != b'\xFF\xD8\xFF':
|
||||
logger.warning(f'Non-JPEG avatar data for {username}')
|
||||
open(sentinel, 'w').close()
|
||||
return '', 404
|
||||
|
||||
with open(cache_file, 'wb') as f:
|
||||
f.write(avatar_data)
|
||||
if os.path.exists(sentinel):
|
||||
os.unlink(sentinel)
|
||||
|
||||
resp = make_response(avatar_data)
|
||||
resp.headers['Content-Type'] = 'image/jpeg'
|
||||
resp.headers['Cache-Control'] = f'private, max-age={cache_ttl}'
|
||||
return resp
|
||||
|
||||
|
||||
@app.route('/health')
|
||||
def health():
|
||||
"""Health check endpoint (no auth). Checks DB and monitor freshness."""
|
||||
|
||||
Reference in New Issue
Block a user