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 (
|
from .utils import (
|
||||||
scan_notes_fast_walk,
|
scan_notes_fast_walk,
|
||||||
|
ensure_index_built,
|
||||||
get_note_content,
|
get_note_content,
|
||||||
save_note,
|
save_note,
|
||||||
delete_note,
|
delete_note,
|
||||||
|
|
@ -1558,55 +1559,33 @@ async def get_index_stats():
|
||||||
@api_router.get("/stats", tags=["Stats"])
|
@api_router.get("/stats", tags=["Stats"])
|
||||||
@limiter.limit("30/minute")
|
@limiter.limit("30/minute")
|
||||||
async def get_stats(request: Request):
|
async def get_stats(request: Request):
|
||||||
"""
|
"""At-a-glance counts for dashboard widgets (Homepage etc.).
|
||||||
Get application statistics at a glance.
|
|
||||||
|
|
||||||
Designed for dashboard widgets (e.g., Homepage) - lightweight and cached.
|
All vault aggregates are read from the in-memory index — no file walk on
|
||||||
Returns counts of notes, folders, tags, templates, media, and other metadata.
|
the request path. Templates / plugins / version are looked up directly."""
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
notes_dir = config['storage']['notes_dir']
|
notes_dir = config['storage']['notes_dir']
|
||||||
|
ensure_index_built(notes_dir)
|
||||||
# Get notes and folders (cached)
|
s = note_index.summary()
|
||||||
notes, folders = scan_notes_fast_walk(notes_dir, include_media=True)
|
|
||||||
|
templates_count = len(get_templates(notes_dir))
|
||||||
# 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
|
|
||||||
enabled_plugins = sum(1 for p in plugin_manager.plugins.values() if p.enabled)
|
enabled_plugins = sum(1 for p in plugin_manager.plugins.values() if p.enabled)
|
||||||
|
|
||||||
# Read version
|
|
||||||
version = "unknown"
|
version = "unknown"
|
||||||
version_file = Path(__file__).parent.parent / "VERSION"
|
version_file = Path(__file__).parent.parent / "VERSION"
|
||||||
if version_file.exists():
|
if version_file.exists():
|
||||||
version = version_file.read_text().strip()
|
version = version_file.read_text().strip()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"notes_count": len(note_items),
|
"notes_count": s["notes_count"],
|
||||||
"folders_count": len(folders),
|
"folders_count": s["folders_count"],
|
||||||
"tags_count": len(all_tags),
|
"tags_count": s["tags_count"],
|
||||||
"templates_count": len(templates),
|
"templates_count": templates_count,
|
||||||
"media_count": len(media_items),
|
"media_count": s["media_count"],
|
||||||
"total_size_bytes": total_size,
|
"total_size_bytes": s["total_size_bytes"],
|
||||||
"last_modified": last_modified,
|
"last_modified": s["last_modified"],
|
||||||
"plugins_enabled": enabled_plugins,
|
"plugins_enabled": enabled_plugins,
|
||||||
"version": version
|
"version": version,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to get stats"))
|
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to get stats"))
|
||||||
|
|
|
||||||
|
|
@ -499,6 +499,34 @@ class NoteIndex:
|
||||||
break
|
break
|
||||||
return result
|
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]:
|
def stats(self) -> Dict[str, Any]:
|
||||||
"""Snapshot of counters + size metrics."""
|
"""Snapshot of counters + size metrics."""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
|
|
@ -968,6 +996,10 @@ def try_get_extraction(
|
||||||
return _index.try_get_extraction(rel_path, mtime)
|
return _index.try_get_extraction(rel_path, mtime)
|
||||||
|
|
||||||
|
|
||||||
|
def summary() -> Dict[str, Any]:
|
||||||
|
return _index.summary()
|
||||||
|
|
||||||
|
|
||||||
def stats() -> Dict[str, Any]:
|
def stats() -> Dict[str, Any]:
|
||||||
return _index.stats()
|
return _index.stats()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -167,11 +167,12 @@ def _scan_cache_invalidate() -> None:
|
||||||
_SCAN_WALK_CACHE.clear()
|
_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.
|
"""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():
|
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(
|
def get_tags_and_links_cached(
|
||||||
|
|
@ -545,7 +546,7 @@ def search_notes(notes_dir: str, query: str) -> List[Dict]:
|
||||||
from html import escape
|
from html import escape
|
||||||
results: List[Dict] = []
|
results: List[Dict] = []
|
||||||
|
|
||||||
_ensure_index_built(notes_dir)
|
ensure_index_built(notes_dir)
|
||||||
note_index.ensure_search_index(notes_dir)
|
note_index.ensure_search_index(notes_dir)
|
||||||
candidates = note_index.get_search_candidates(query)
|
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]:
|
def get_all_tags(notes_dir: str) -> Dict[str, int]:
|
||||||
"""All tags in the vault with note counts."""
|
"""All tags in the vault with note counts."""
|
||||||
_ensure_index_built(notes_dir)
|
ensure_index_built(notes_dir)
|
||||||
return note_index.get_all_tags()
|
return note_index.get_all_tags()
|
||||||
|
|
||||||
|
|
||||||
def get_notes_by_tag(notes_dir: str, tag: str) -> List[Dict]:
|
def get_notes_by_tag(notes_dir: str, tag: str) -> List[Dict]:
|
||||||
"""All notes carrying `tag` (case-insensitive)."""
|
"""All notes carrying `tag` (case-insensitive)."""
|
||||||
_ensure_index_built(notes_dir)
|
ensure_index_built(notes_dir)
|
||||||
idx = note_index.get_index()
|
idx = note_index.get_index()
|
||||||
records = [idx.get_note_record(p) for p in note_index.get_paths_for_tag(tag.lower())]
|
records = [idx.get_note_record(p) for p in note_index.get_paths_for_tag(tag.lower())]
|
||||||
matching = [
|
matching = [
|
||||||
|
|
@ -1151,7 +1152,7 @@ def get_backlinks(notes_dir: str, target_note_path: str) -> List[Dict]:
|
||||||
Path(target_path).stem.lower(),
|
Path(target_path).stem.lower(),
|
||||||
}
|
}
|
||||||
|
|
||||||
_ensure_index_built(notes_dir)
|
ensure_index_built(notes_dir)
|
||||||
idx = note_index.get_index()
|
idx = note_index.get_index()
|
||||||
candidates = note_index.get_backlink_candidates(target_path)
|
candidates = note_index.get_backlink_candidates(target_path)
|
||||||
records = [(p, idx.get_note_record(p)) for p in candidates]
|
records = [(p, idx.get_note_record(p)) for p in candidates]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue