Completed the feedback UI.
This commit is contained in:
@@ -7,6 +7,7 @@ from pathlib import Path
|
|||||||
from .config import ConfigStore
|
from .config import ConfigStore
|
||||||
from .graylog_mcp import GraylogMcpClient
|
from .graylog_mcp import GraylogMcpClient
|
||||||
from .metrics import prometheus_metrics
|
from .metrics import prometheus_metrics
|
||||||
|
from .feedback import FeedbackStore
|
||||||
|
|
||||||
|
|
||||||
HTML = """<!doctype html>
|
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 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})));
|
const fieldRows = Object.entries(data.field_deviations || {}).flatMap(([entity, deviations]) => (deviations || []).map(item => ({entity, ...item})));
|
||||||
document.getElementById('fieldDeviations').innerHTML = table(fieldRows, [
|
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, [
|
document.getElementById('relatedActivity').innerHTML = table(relatedRows, [
|
||||||
{label:'Source IP', key:'source_ip'}, {label:'Stream', key:'stream'}, {label:'Time', key:'timestamp'},
|
{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'},
|
{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":
|
if self.path == "/api/config":
|
||||||
self._send(200, "application/json", json.dumps(config_store.public()).encode("utf-8"))
|
self._send(200, "application/json", json.dumps(config_store.public()).encode("utf-8"))
|
||||||
return
|
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":
|
if self.path == "/api/graylog/streams":
|
||||||
config = config_store.read()
|
config = config_store.read()
|
||||||
try:
|
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")
|
self._send(404, "text/plain; charset=utf-8", b"not found")
|
||||||
|
|
||||||
def do_POST(self) -> None:
|
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():
|
if self.path != "/api/config" or not self._is_loopback_client():
|
||||||
self._send(403, "application/json", b'{"error":"configuration is local-only"}')
|
self._send(403, "application/json", b'{"error":"configuration is local-only"}')
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user