Completed the feedback UI.

This commit is contained in:
larssand
2026-06-22 22:25:06 +02:00
parent 08d1c43799
commit a55325610c

View File

@@ -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 = """<!doctype 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 => `<button class="feedback" data-status="expected" data-entity="${esc(r.entity)}" data-stream="${esc(r.stream_id)}" data-field="${esc(r.field)}">Expected</button> <button class="feedback" data-status="false_positive" data-entity="${esc(r.entity)}" data-stream="${esc(r.stream_id)}" data-field="${esc(r.field)}">False positive</button> <button class="feedback" data-status="confirmed" data-entity="${esc(r.entity)}" data-stream="${esc(r.stream_id)}" data-field="${esc(r.field)}">Confirm</button>`}
]);
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