mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-13 22:15:10 +00:00
fix(admin): second review pass
Correctness - /api/remote never recorded source.created, so URL, GitHub and connector sources had a source.deleted with no matching creation. All three creation paths now go through one _audit_source_created helper. - The prompt-cache rate divided cached tokens by a whole bucket's prompt tokens. A bucket is a day and mixes calls whose provider reports a cache breakdown with calls whose provider does not, so filtering buckets in the client could not separate them and the rate was understated by however much traffic ran on a non-reporting provider. The denominator is now computed in SQL over the reporting rows. - The outcome pill matched values nothing writes. Guardrails emit triggered / not_evaluated and the device feed emits dispatched; the map had blocked / denied / allowed, so a guardrail that fired rendered neutral grey -- the one signal the merged feed exists to surface. Fixtures were seeding the fictional values, so the tests passed on it too. - Stream duration_ms timed the consumer. stream_token_usage is a generator, so start-to-exhaustion includes the agent loop's tool handling and the SSE client's pace; a slow browser recorded ~30s for a sub-second call. It now accumulates only the time spent inside next(). Safety - Activity filters failed open: an unknown facet or unparseable timestamp was dropped, and no filter means every row, so a typo widened an audit view and on the export streamed the full history. Both are now a 400. - The search term was interpolated into an ILIKE pattern, so "100%" matched everything and "q1_report" matched more than it should. Escaped. - 0034 set actor_id NOT NULL with no default. A previous-release process inserting mid-rollout would raise, and in admin/routes.py that insert shares the request transaction, so a role grant beside it would roll back too. Noise and dead code - The per-user panel is a security panel: data-plane events file under the actor, so an active account's routine deletes pushed a denied login out of the 20-row window. It now excludes them; the Activity tab shows everything. - device_audit_log had no created_at-leading index, so the merged feed sequentially scanned that branch every page (migration 0036). - conversation.deleted_all no longer records when nothing was deleted, and agent.updated no longer records an empty field list. - Dropped by_model from /admin/usage (no consumer; an extra aggregate per page load), the duplicate filter surface on AuthEventsRepository that nothing called, and the unreachable FLOW_LABELS.schedule entry. - Type hints on record_event's conn and the remaining unannotated helpers.
This commit is contained in:
1 parent
2ea9d66789
commit
25c82003d7
33 files changed
+635
-206
No files matched your search
@@ -43,23 +43,38 @@ _EXPORT_CHUNK = 1_000
|
||||
_MAX_SEARCH_LENGTH = 200
|
||||
|
||||
|
||||
class _InvalidFilter(ValueError):
|
||||
"""A filter argument the caller got wrong. Surfaces as a 400."""
|
||||
|
||||
|
||||
def _facet_list(name: str, allowed: tuple[str, ...]) -> Optional[list[str]]:
|
||||
"""Parse a repeated/comma-separated query arg, dropping unknown values.
|
||||
"""Parse a repeated/comma-separated query arg into known facet values.
|
||||
|
||||
Rejects unknown values rather than dropping them. Dropping would leave the
|
||||
request with no filter on that facet, and "no filter" means *everything* --
|
||||
so a typo or a stale chip would silently widen an audit view instead of
|
||||
narrowing it, and on the export would stream the whole history.
|
||||
|
||||
Args:
|
||||
name: Query parameter name.
|
||||
allowed: The permitted values; anything else is ignored rather than
|
||||
rejected, so an old bookmark degrades instead of erroring.
|
||||
allowed: The permitted values.
|
||||
|
||||
Returns:
|
||||
The distinct valid values in the order given, or None when absent.
|
||||
The distinct values in the order given, or None when absent.
|
||||
|
||||
Raises:
|
||||
_InvalidFilter: If any supplied value is not in ``allowed``.
|
||||
"""
|
||||
raw: list[str] = []
|
||||
for value in request.args.getlist(name):
|
||||
raw.extend(part.strip() for part in value.split(","))
|
||||
raw.extend(part.strip() for part in value.split(",") if part.strip())
|
||||
seen: list[str] = []
|
||||
for value in raw:
|
||||
if value in allowed and value not in seen:
|
||||
if value not in allowed:
|
||||
raise _InvalidFilter(
|
||||
f"Unknown {name}: {value!r}. Expected one of {', '.join(allowed)}."
|
||||
)
|
||||
if value not in seen:
|
||||
seen.append(value)
|
||||
return seen or None
|
||||
|
||||
@@ -76,14 +91,21 @@ def _event_list() -> Optional[list[str]]:
|
||||
|
||||
|
||||
def _time_arg(name: str) -> Optional[datetime]:
|
||||
"""Parse an ISO-8601 timestamp query arg, or None when absent/unparseable."""
|
||||
"""Parse an ISO-8601 timestamp query arg, or None when absent.
|
||||
|
||||
Raises:
|
||||
_InvalidFilter: If present but unparseable. Silently ignoring it would
|
||||
drop the time bound and return the full history.
|
||||
"""
|
||||
raw = (request.args.get(name) or "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
parsed = datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return None
|
||||
except ValueError as exc:
|
||||
raise _InvalidFilter(
|
||||
f"{name} must be an ISO-8601 timestamp, got {raw!r}."
|
||||
) from exc
|
||||
# A naive bound is read as UTC, matching how the feed stores timestamps.
|
||||
return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
|
||||
|
||||
@@ -122,7 +144,10 @@ class AdminActivityResource(Resource):
|
||||
def get(self):
|
||||
"""Merged audit feed, newest first, with filters and a total."""
|
||||
page, page_size, offset = _page()
|
||||
filters = _filters()
|
||||
try:
|
||||
filters = _filters()
|
||||
except _InvalidFilter as exc:
|
||||
return make_response(jsonify({"success": False, "message": str(exc)}), 400)
|
||||
with db_readonly() as conn:
|
||||
repo = ActivityRepository(conn)
|
||||
total = repo.count(**filters)
|
||||
@@ -183,7 +208,10 @@ class AdminActivityExportResource(Resource):
|
||||
jsonify({"success": False, "message": "Invalid format"}), 400
|
||||
)
|
||||
limit = max(1, min(_EXPORT_MAX_ROWS, _int_arg("max_rows", _EXPORT_MAX_ROWS)))
|
||||
filters = _filters()
|
||||
try:
|
||||
filters = _filters()
|
||||
except _InvalidFilter as exc:
|
||||
return make_response(jsonify({"success": False, "message": str(exc)}), 400)
|
||||
stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
||||
generator = _csv_rows if fmt == "csv" else _ndjson_rows
|
||||
return Response(
|
||||
|
||||
Reference in new issue
Block a user