From da540f81ae3b2fb619392d05f21435e10248f85611d2fff75931479305b41048 Mon Sep 17 00:00:00 2001 From: larssand Date: Mon, 6 Jul 2026 17:05:27 +0200 Subject: [PATCH] fix reset state --- src/fgai/dashboard.py | 21 +++++++++++++++++++++ src/fgai/feedback.py | 16 ++++++++++++++++ tests/test_dashboard.py | 7 +++++++ tests/test_feedback.py | 25 +++++++++++++++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 tests/test_feedback.py diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py index c28f36d..a88a20f 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -1224,6 +1224,27 @@ def serve_dashboard(host: str, port: int, status_file: str, *, image_dir: str | except (ValueError, json.JSONDecodeError) as exc: self._send(400, "application/json", json.dumps({"error": str(exc)}).encode("utf-8")) return + if self.path == "/api/feedback/clear" and self._is_loopback_client(): + try: + payload = json.loads(self.rfile.read(min(int(self.headers.get("Content-Length", "0")), 16_384)).decode("utf-8")) + status = str(payload.get("status", "") or "") + self._send(200, "application/json", json.dumps(FeedbackStore().clear(status=status or None)).encode("utf-8")) + except (ValueError, json.JSONDecodeError) as exc: + self._send(400, "application/json", json.dumps({"error": str(exc)}).encode("utf-8")) + return + if self.path == "/api/review-state/clear" and self._is_loopback_client(): + try: + payload = json.loads(self.rfile.read(min(int(self.headers.get("Content-Length", "0")), 16_384)).decode("utf-8") or "{}") + clear_incidents = bool(payload.get("incidents", True)) + clear_feedback = bool(payload.get("feedback", True)) + result = { + "incidents": IncidentStore().clear() if clear_incidents else {"removed": 0, "remaining": len(IncidentStore().entries()), "status": "skipped"}, + "feedback": FeedbackStore().clear() if clear_feedback else {"removed": 0, "remaining": len(FeedbackStore().entries()), "status": "skipped"}, + } + self._send(200, "application/json", json.dumps(result).encode("utf-8")) + except (ValueError, json.JSONDecodeError) as exc: + self._send(400, "application/json", json.dumps({"error": str(exc)}).encode("utf-8")) + return if self.path == "/api/incidents" and self._is_loopback_client(): try: payload = json.loads(self.rfile.read(min(int(self.headers.get("Content-Length", "0")), 16_384)).decode("utf-8")) diff --git a/src/fgai/feedback.py b/src/fgai/feedback.py index 3c7f312..b8dd901 100644 --- a/src/fgai/feedback.py +++ b/src/fgai/feedback.py @@ -25,3 +25,19 @@ class FeedbackStore: entries = [value for value in self.entries() if (value.get("stream_id"), value.get("entity"), value.get("field"), value.get("value", "")) != (entry["stream_id"], entry["entity"], entry["field"], entry["value"])] entries.append(entry); self.path.parent.mkdir(parents=True, exist_ok=True); self.path.write_text(json.dumps(entries, indent=2), encoding="utf-8") return entry + + def clear(self, *, status: str | None = None) -> dict[str, object]: + entries = self.entries() + if status is None: + removed = len(entries) + entries = [] + else: + status = status.lower() + if status not in {"false_positive", "expected", "confirmed"}: + raise ValueError("invalid feedback status") + before = len(entries) + entries = [item for item in entries if str(item.get("status", "")).lower() != status] + removed = before - len(entries) + self.path.parent.mkdir(parents=True, exist_ok=True) + self.path.write_text(json.dumps(entries, indent=2), encoding="utf-8") + return {"removed": removed, "remaining": len(entries), "status": status or "all"} diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 4f34695..181ab9c 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -1,7 +1,9 @@ import json +import inspect import unittest from unittest.mock import patch +import fgai.dashboard as dashboard from fgai.dashboard import HTML, _ollama_models @@ -62,6 +64,11 @@ class DashboardTests(unittest.TestCase): self.assertIn('id="clearAllIncidents"', HTML) self.assertIn("/api/incidents/clear", HTML) + def test_dashboard_exposes_review_state_reset_endpoints(self): + source = inspect.getsource(dashboard) + self.assertIn("/api/feedback/clear", source) + self.assertIn("/api/review-state/clear", source) + def test_dashboard_does_not_clear_streams_when_picker_is_unloaded(self): self.assertIn("const streamValues = Object.values(window.streamSelection || {})", HTML) self.assertIn("if (streamValues.length)", HTML) diff --git a/tests/test_feedback.py b/tests/test_feedback.py new file mode 100644 index 0000000..6e7536a --- /dev/null +++ b/tests/test_feedback.py @@ -0,0 +1,25 @@ +import tempfile +import unittest +from pathlib import Path + +from fgai.feedback import FeedbackStore + + +class FeedbackTests(unittest.TestCase): + def test_feedback_store_can_clear_by_status_or_all(self): + with tempfile.TemporaryDirectory() as directory: + store = FeedbackStore(str(Path(directory) / "feedback.json")) + store.add({"status": "expected", "entity": "alice", "stream_id": "windows", "field": "username"}) + store.add({"status": "confirmed", "entity": "bob", "stream_id": "vpn", "field": "username"}) + + expected = store.clear(status="expected") + self.assertEqual(expected, {"removed": 1, "remaining": 1, "status": "expected"}) + self.assertEqual(store.entries()[0]["status"], "confirmed") + + all_items = store.clear() + self.assertEqual(all_items, {"removed": 1, "remaining": 0, "status": "all"}) + self.assertEqual(store.entries(), []) + + +if __name__ == "__main__": + unittest.main()