mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-05 14:55:08 +00:00
feat(web): show which plugins use each font; warn before deleting one (#619)
* feat(web): show which plugins use each font, warn before deleting one
The Fonts tab lists font files from the web process's own scan, and the
plugins that register fonts run in the display process, so the tab had no
way to say whether a font was in use before deleting it.
The display service now publishes {catalog key: [plugin ids]} to the
shared cache (font_usage_snapshot, src/font_usage.py), built from the
loaded plugins' FontManager.register_manager_font() registrations. A
daemon thread checks every 10 s and writes only when the usage changed
(plus a daily refresh so cache cleanup cannot expire it); it never raises.
Families, aliases (press_start, four_by_six, ...) and paths are resolved
through FontManager's catalog to the file stem the Fonts tab keys rows by;
fonts outside assets/fonts are left out. Unloading a plugin drops its
registrations (new FontManager.forget_manager_fonts).
GET /api/v3/fonts/catalog merges used_by into each row per request (the
5-minute scan cache is copied, never edited): a list of plugin ids, or
null when the display service has not reported. The tab shows a Used by
column ("unknown" / "-" / ids, rendered as text) and deleting an in-use
font names the plugins in the confirmation, from a fresh read. The server
still refuses only system fonts. Catalog fetches bypass the browser's
5-second API cache, which otherwise served the pre-delete list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: call forget_manager_fonts through a hasattr check pylint can follow
getattr(..., None) then callable() is fine at runtime, but pylint's E1102
("not callable") can't see through it, and Codacy fails the check on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,46 @@ from web_interface.blueprints.api_v3 import (
|
||||
)
|
||||
|
||||
|
||||
def _catalog_response(catalog):
|
||||
"""The catalog response, with each font's ``used_by`` merged in now.
|
||||
|
||||
Usage comes from the display service (src/font_usage.py) and changes
|
||||
independently of the files, so it is read per request and never stored
|
||||
in the 5-minute ``fonts_catalog`` cache: entries are copied, not edited.
|
||||
``used_by`` is a list of plugin ids, empty when no loaded plugin
|
||||
registered the font, and None when the display service has not reported.
|
||||
"""
|
||||
from src.font_usage import read_font_usage
|
||||
from web_interface.blueprints.api_v3.display import _cache_manager
|
||||
try:
|
||||
usage = read_font_usage(_cache_manager())
|
||||
except Exception:
|
||||
logger.debug("[FontCatalog] Could not read font usage", exc_info=True)
|
||||
usage = None
|
||||
|
||||
used_by = {}
|
||||
if usage is not None:
|
||||
# The snapshot keys fonts by file stem, as the catalog does; match
|
||||
# case-insensitively too, since FontManager lower-cases families.
|
||||
by_lower = {key.lower(): key for key in catalog}
|
||||
for key, plugin_ids in usage['fonts'].items():
|
||||
row = key if key in catalog else by_lower.get(key.lower())
|
||||
if row is not None:
|
||||
used_by.setdefault(row, set()).update(plugin_ids)
|
||||
|
||||
merged = {
|
||||
key: dict(info, used_by=(sorted(used_by.get(key, ())) if usage is not None else None))
|
||||
for key, info in catalog.items()
|
||||
}
|
||||
return jsonify({'status': 'success', 'data': {
|
||||
'catalog': merged,
|
||||
'font_usage': {
|
||||
'available': usage is not None,
|
||||
'generated_at': usage['generated_at'] if usage is not None else None,
|
||||
},
|
||||
}})
|
||||
|
||||
|
||||
@api_v3.route('/fonts/catalog', methods=['GET'])
|
||||
def get_fonts_catalog():
|
||||
"""Get fonts catalog"""
|
||||
@@ -18,7 +58,7 @@ def get_fonts_catalog():
|
||||
from web_interface.cache import get_cached, set_cached
|
||||
cached_result = get_cached('fonts_catalog', ttl_seconds=300)
|
||||
if cached_result is not None:
|
||||
return jsonify({'status': 'success', 'data': {'catalog': cached_result}})
|
||||
return _catalog_response(cached_result)
|
||||
except ImportError:
|
||||
# Cache not available, continue without caching
|
||||
get_cached = None
|
||||
@@ -116,7 +156,7 @@ def get_fonts_catalog():
|
||||
except Exception:
|
||||
logger.error("[FontCatalog] Failed to cache fonts_catalog", exc_info=True)
|
||||
|
||||
return jsonify({'status': 'success', 'data': {'catalog': catalog}})
|
||||
return _catalog_response(catalog)
|
||||
except Exception as e:
|
||||
logger.error("%s failed", request.path, exc_info=True)
|
||||
return jsonify({'status': 'error',
|
||||
|
||||
Reference in New Issue
Block a user