diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py
index 4f10913..b0e7fe8 100644
--- a/src/fgai/dashboard.py
+++ b/src/fgai/dashboard.py
@@ -7,6 +7,7 @@ from pathlib import Path
from .config import ConfigStore
from .graylog_mcp import GraylogMcpClient
from .metrics import prometheus_metrics
+from .feedback import FeedbackStore
HTML = """
@@ -155,8 +156,12 @@ async function refresh() {
const relatedRows = (data.cross_source_correlations || []).flatMap(correlation => (correlation.samples || []).map(sample => ({source_ip: correlation.source_ip, ...sample})));
const fieldRows = Object.entries(data.field_deviations || {}).flatMap(([entity, deviations]) => (deviations || []).map(item => ({entity, ...item})));
document.getElementById('fieldDeviations').innerHTML = table(fieldRows, [
- {label:'Entity', key:'entity'}, {label:'Stream', key:'stream_id'}, {label:'Field', key:'field'}, {label:'Score', key:'score'}, {label:'Evidence', key:'reason'}
+ {label:'Entity', key:'entity'}, {label:'Stream', key:'stream_id'}, {label:'Field', key:'field'}, {label:'Score', key:'score'}, {label:'Review', render:r => esc(r.feedback || 'unreviewed')}, {label:'Evidence', key:'reason'}, {label:'Action', render:r => ` `}
]);
+ document.querySelectorAll('.feedback').forEach(button => button.addEventListener('click', async () => {
+ await fetch('/api/feedback', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({status:button.dataset.status, entity:button.dataset.entity, stream_id:button.dataset.stream, field:button.dataset.field})});
+ refresh();
+ }));
document.getElementById('relatedActivity').innerHTML = table(relatedRows, [
{label:'Source IP', key:'source_ip'}, {label:'Stream', key:'stream'}, {label:'Time', key:'timestamp'},
{label:'Type', key:'type'}, {label:'Action', key:'action'}, {label:'Severity', key:'severity'},
@@ -258,6 +263,9 @@ def serve_dashboard(host: str, port: int, status_file: str, *, image_dir: str |
if self.path == "/api/config":
self._send(200, "application/json", json.dumps(config_store.public()).encode("utf-8"))
return
+ if self.path == "/api/feedback":
+ self._send(200, "application/json", json.dumps(FeedbackStore().entries()).encode("utf-8"))
+ return
if self.path == "/api/graylog/streams":
config = config_store.read()
try:
@@ -310,6 +318,13 @@ def serve_dashboard(host: str, port: int, status_file: str, *, image_dir: str |
self._send(404, "text/plain; charset=utf-8", b"not found")
def do_POST(self) -> None:
+ if self.path == "/api/feedback" 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"))
+ self._send(200, "application/json", json.dumps(FeedbackStore().add(payload)).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/config" or not self._is_loopback_client():
self._send(403, "application/json", b'{"error":"configuration is local-only"}')
return