stats endpoint perf boost + bugfix
This commit is contained in:
parent
f40239784f
commit
e9b88516ae
|
|
@ -24,6 +24,7 @@ from slowapi.errors import RateLimitExceeded
|
|||
|
||||
from .utils import (
|
||||
scan_notes_fast_walk,
|
||||
ensure_index_built,
|
||||
get_note_content,
|
||||
save_note,
|
||||
delete_note,
|
||||
|
|
@ -1558,55 +1559,33 @@ async def get_index_stats():
|
|||
@api_router.get("/stats", tags=["Stats"])
|
||||
@limiter.limit("30/minute")
|
||||
async def get_stats(request: Request):
|
||||
"""
|
||||
Get application statistics at a glance.
|
||||
"""At-a-glance counts for dashboard widgets (Homepage etc.).
|
||||
|
||||
Designed for dashboard widgets (e.g., Homepage) - lightweight and cached.
|
||||
Returns counts of notes, folders, tags, templates, media, and other metadata.
|
||||
"""
|
||||
All vault aggregates are read from the in-memory index — no file walk on
|
||||
the request path. Templates / plugins / version are looked up directly."""
|
||||
try:
|
||||
notes_dir = config['storage']['notes_dir']
|
||||
|
||||
# Get notes and folders (cached)
|
||||
notes, folders = scan_notes_fast_walk(notes_dir, include_media=True)
|
||||
|
||||
# Separate notes from media
|
||||
note_items = [n for n in notes if n.get('type') == 'note']
|
||||
media_items = [n for n in notes if n.get('type') != 'note']
|
||||
|
||||
# Count unique tags
|
||||
all_tags = set()
|
||||
for note in note_items:
|
||||
all_tags.update(note.get('tags', []))
|
||||
|
||||
# Get templates count
|
||||
templates = get_templates(notes_dir)
|
||||
|
||||
# Calculate total size
|
||||
total_size = sum(n.get('size', 0) for n in notes)
|
||||
|
||||
# Get last modified (notes are already sorted by modified desc)
|
||||
last_modified = note_items[0].get('modified') if note_items else None
|
||||
|
||||
# Count enabled plugins
|
||||
ensure_index_built(notes_dir)
|
||||
s = note_index.summary()
|
||||
|
||||
templates_count = len(get_templates(notes_dir))
|
||||
enabled_plugins = sum(1 for p in plugin_manager.plugins.values() if p.enabled)
|
||||
|
||||
# Read version
|
||||
|
||||
version = "unknown"
|
||||
version_file = Path(__file__).parent.parent / "VERSION"
|
||||
if version_file.exists():
|
||||
version = version_file.read_text().strip()
|
||||
|
||||
|
||||
return {
|
||||
"notes_count": len(note_items),
|
||||
"folders_count": len(folders),
|
||||
"tags_count": len(all_tags),
|
||||
"templates_count": len(templates),
|
||||
"media_count": len(media_items),
|
||||
"total_size_bytes": total_size,
|
||||
"last_modified": last_modified,
|
||||
"notes_count": s["notes_count"],
|
||||
"folders_count": s["folders_count"],
|
||||
"tags_count": s["tags_count"],
|
||||
"templates_count": templates_count,
|
||||
"media_count": s["media_count"],
|
||||
"total_size_bytes": s["total_size_bytes"],
|
||||
"last_modified": s["last_modified"],
|
||||
"plugins_enabled": enabled_plugins,
|
||||
"version": version
|
||||
"version": version,
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to get stats"))
|
||||
|
|
|
|||
|
|
@ -499,6 +499,34 @@ class NoteIndex:
|
|||
break
|
||||
return result
|
||||
|
||||
def summary(self) -> Dict[str, Any]:
|
||||
"""Aggregate counts + total size + last-modified note. Powers /api/stats
|
||||
without a vault scan — every field is computed from records already in
|
||||
memory."""
|
||||
with self._lock:
|
||||
notes_count = 0
|
||||
media_count = 0
|
||||
total_size = 0
|
||||
last_modified: Optional[str] = None
|
||||
last_mtime = -1.0
|
||||
for rec in self._notes.values():
|
||||
total_size += rec.size
|
||||
if rec.type == "note":
|
||||
notes_count += 1
|
||||
if rec.mtime > last_mtime:
|
||||
last_mtime = rec.mtime
|
||||
last_modified = rec.modified
|
||||
else:
|
||||
media_count += 1
|
||||
return {
|
||||
"notes_count": notes_count,
|
||||
"media_count": media_count,
|
||||
"folders_count": len(self._folders),
|
||||
"tags_count": len(self._tags_backward),
|
||||
"total_size_bytes": total_size,
|
||||
"last_modified": last_modified,
|
||||
}
|
||||
|
||||
def stats(self) -> Dict[str, Any]:
|
||||
"""Snapshot of counters + size metrics."""
|
||||
with self._lock:
|
||||
|
|
@ -968,6 +996,10 @@ def try_get_extraction(
|
|||
return _index.try_get_extraction(rel_path, mtime)
|
||||
|
||||
|
||||
def summary() -> Dict[str, Any]:
|
||||
return _index.summary()
|
||||
|
||||
|
||||
def stats() -> Dict[str, Any]:
|
||||
return _index.stats()
|
||||
|
||||
|
|
|
|||
|
|
@ -167,11 +167,12 @@ def _scan_cache_invalidate() -> None:
|
|||
_SCAN_WALK_CACHE.clear()
|
||||
|
||||
|
||||
def _ensure_index_built(notes_dir: str) -> None:
|
||||
def ensure_index_built(notes_dir: str) -> None:
|
||||
"""Trigger a fresh scan when the NoteIndex hasn't been populated yet.
|
||||
Idempotent: the scan short-circuits when nothing changed in the vault."""
|
||||
Scans with include_media=True so every index consumer (stats, notes,
|
||||
backlinks, search) shares one fingerprint and never thrashes."""
|
||||
if not note_index.get_index().is_built():
|
||||
scan_notes_fast_walk(notes_dir, use_cache=False, include_media=False)
|
||||
scan_notes_fast_walk(notes_dir, use_cache=False, include_media=True)
|
||||
|
||||
|
||||
def get_tags_and_links_cached(
|
||||
|
|
@ -545,7 +546,7 @@ def search_notes(notes_dir: str, query: str) -> List[Dict]:
|
|||
from html import escape
|
||||
results: List[Dict] = []
|
||||
|
||||
_ensure_index_built(notes_dir)
|
||||
ensure_index_built(notes_dir)
|
||||
note_index.ensure_search_index(notes_dir)
|
||||
candidates = note_index.get_search_candidates(query)
|
||||
|
||||
|
|
@ -875,13 +876,13 @@ def parse_tags(content: str) -> List[str]:
|
|||
|
||||
def get_all_tags(notes_dir: str) -> Dict[str, int]:
|
||||
"""All tags in the vault with note counts."""
|
||||
_ensure_index_built(notes_dir)
|
||||
ensure_index_built(notes_dir)
|
||||
return note_index.get_all_tags()
|
||||
|
||||
|
||||
def get_notes_by_tag(notes_dir: str, tag: str) -> List[Dict]:
|
||||
"""All notes carrying `tag` (case-insensitive)."""
|
||||
_ensure_index_built(notes_dir)
|
||||
ensure_index_built(notes_dir)
|
||||
idx = note_index.get_index()
|
||||
records = [idx.get_note_record(p) for p in note_index.get_paths_for_tag(tag.lower())]
|
||||
matching = [
|
||||
|
|
@ -1151,7 +1152,7 @@ def get_backlinks(notes_dir: str, target_note_path: str) -> List[Dict]:
|
|||
Path(target_path).stem.lower(),
|
||||
}
|
||||
|
||||
_ensure_index_built(notes_dir)
|
||||
ensure_index_built(notes_dir)
|
||||
idx = note_index.get_index()
|
||||
candidates = note_index.get_backlink_candidates(target_path)
|
||||
records = [(p, idx.get_note_record(p)) for p in candidates]
|
||||
|
|
|
|||
Loading…
Reference in New Issue