feat(sofiia-console): add runbooks index status endpoint

GET /api/runbooks/status returns docs_root, indexed_files, indexed_chunks, last_indexed_at, fts_available; docs_index_meta table and set on rebuild

Made-with: Cursor
This commit is contained in:
Apple
2026-03-03 04:35:18 -08:00
parent ef3ff80645
commit 63fec4371a
5 changed files with 85 additions and 2 deletions

View File

@@ -374,6 +374,10 @@ CREATE VIRTUAL TABLE IF NOT EXISTS docs_chunks_fts USING fts5(
content,
content=''
);
CREATE TABLE IF NOT EXISTS docs_index_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL DEFAULT ''
);
-- ── Graph Intelligence (Hygiene + Reflection) ──────────────────────────────
-- These ADD COLUMN statements are idempotent (IF NOT EXISTS requires SQLite 3.37+).

View File

@@ -9,11 +9,12 @@ import asyncio
import logging
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
from . import db as _db
from .docs_store import clear_docs_index, insert_docs_file
from .docs_store import clear_docs_index, insert_docs_file, set_docs_index_meta
logger = logging.getLogger(__name__)
@@ -52,6 +53,8 @@ async def rebuild_index(docs_root: Optional[Path] = None) -> int:
mtime = path.stat().st_mtime
await insert_docs_file(path_key, mtime, content)
count += 1
last_indexed_at = datetime.now(timezone.utc).isoformat()
await set_docs_index_meta(str(root), last_indexed_at, sha="")
logger.info("Docs index rebuilt: %s files from %s", count, root)
return count

View File

@@ -37,15 +37,63 @@ def _extract_title(content: str, path: str) -> str:
async def clear_docs_index() -> None:
"""Remove all docs_files, docs_chunks, and FTS rows."""
"""Remove all docs_files, docs_chunks, FTS rows, and index meta."""
conn = await _db.get_db()
await conn.execute("DELETE FROM docs_chunks_fts")
await conn.execute("DELETE FROM docs_chunks")
await conn.execute("DELETE FROM docs_files")
await conn.execute("DELETE FROM docs_index_meta")
await conn.commit()
logger.info("Docs index cleared.")
async def set_docs_index_meta(docs_root: str, last_indexed_at: str, sha: str = "") -> None:
"""Write meta after rebuild. last_indexed_at: ISO or epoch string."""
conn = await _db.get_db()
for key, value in [("docs_root", docs_root), ("last_indexed_at", last_indexed_at), ("sha", sha)]:
await conn.execute(
"INSERT OR REPLACE INTO docs_index_meta(key, value) VALUES (?,?)",
(key, value),
)
await conn.commit()
async def get_docs_index_status() -> Dict[str, Any]:
"""Return indexed_files, indexed_chunks, last_indexed_at, docs_root, fts_available."""
conn = await _db.get_db()
files_row = None
chunks_row = None
async with conn.execute("SELECT COUNT(*) FROM docs_files") as cur:
files_row = await cur.fetchone()
async with conn.execute("SELECT COUNT(*) FROM docs_chunks") as cur:
chunks_row = await cur.fetchone()
indexed_files = int(files_row[0]) if files_row else 0
indexed_chunks = int(chunks_row[0]) if chunks_row else 0
meta = {}
async with conn.execute("SELECT key, value FROM docs_index_meta") as cur:
async for row in cur:
meta[row[0]] = row[1]
last_indexed_at = meta.get("last_indexed_at") or None
docs_root = meta.get("docs_root") or ""
fts_available = False
if indexed_chunks > 0:
try:
async with conn.execute("SELECT 1 FROM docs_chunks_fts LIMIT 1") as cur:
fts_available = (await cur.fetchone()) is not None
except Exception:
pass
return {
"docs_root": docs_root,
"indexed_files": indexed_files,
"indexed_chunks": indexed_chunks,
"last_indexed_at": last_indexed_at,
"fts_available": fts_available,
}
async def insert_docs_file(path: str, mtime: float, content: str) -> None:
"""Register one file and its chunks. Caller ensures path is normalized."""
conn = await _db.get_db()

View File

@@ -15,6 +15,14 @@ from . import docs_store as store
runbooks_router = APIRouter(prefix="/api/runbooks", tags=["runbooks-docs"])
@runbooks_router.get("/status")
async def runbooks_status(
_auth: str = Depends(require_auth),
):
"""Return docs index status: indexed_files, indexed_chunks, last_indexed_at, docs_root, fts_available."""
return await store.get_docs_index_status()
def _safe_path(path: str) -> bool:
"""Reject path traversal and non-docs paths."""
if not path or ".." in path or path.startswith("/"):

View File

@@ -93,3 +93,23 @@ def test_runbooks_raw_400_for_invalid_path(sofiia_client):
"""Raw returns 400 for path traversal attempt."""
r = sofiia_client.get("/api/runbooks/raw?path=../../../etc/passwd")
assert r.status_code == 400
def test_runbooks_status_after_rebuild(sofiia_module, tmp_path, tmp_docs_with_rehearsal, monkeypatch):
"""After rebuild, status shows indexed_files > 0, indexed_chunks > 0, last_indexed_at set."""
import app.docs_index as docs_index_mod
import app.docs_store as docs_store_mod
monkeypatch.setenv("SOFIIA_DATA_DIR", str(tmp_path / "sofiia-data"))
loop = asyncio.get_event_loop()
async def run():
await docs_index_mod.rebuild_index(tmp_docs_with_rehearsal)
return await docs_store_mod.get_docs_index_status()
status = loop.run_until_complete(run())
assert status["indexed_files"] >= 1, status
assert status["indexed_chunks"] >= 1, status
assert status.get("last_indexed_at") is not None, status
assert "docs_root" in status
assert "fts_available" in status