diff --git a/src/fgai/baseline.py b/src/fgai/baseline.py
index 766c63d..e60ecf6 100644
--- a/src/fgai/baseline.py
+++ b/src/fgai/baseline.py
@@ -156,7 +156,8 @@ class BaselineStore:
reason = f"{field} value deviates from its stream baseline"
deviation = abs(current_value - mean(history))
if deviation > (pstdev(history) or 1.0) * 3:
- output[entity].append({"field": field, "stream_id": stream, "score": 15, "reason": reason})
+ samples = sorted({event.fields.get(field, "") for event in events if event.fields.get("fgai_stream_id") == stream and event.fields.get(str(getattr(profiles.get(stream), "entity_field", "")).lower()) == entity and event.fields.get(field)})[:5]
+ output[entity].append({"field": field, "stream_id": stream, "score": 15, "reason": reason, "current": round(current_value, 2), "baseline": round(mean(history), 2), "sample_values": samples})
# Detect selected categorical values that have not appeared for this entity in prior data.
for event in events:
profile = profiles.get(event.fields.get("fgai_stream_id", ""))
diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py
index 12db6c5..28fe8f3 100644
--- a/src/fgai/dashboard.py
+++ b/src/fgai/dashboard.py
@@ -154,12 +154,15 @@ async function refresh() {
]);
const reputationRows = Object.entries(data.reputation || {}).map(([ip, intel]) => ({ip, ...intel}));
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 streamTitles = Object.fromEntries((configuration.graylog_streams || []).map(item => [item.id, item.title || item.id]));
+ const fieldRows = Object.entries(data.field_deviations || {}).flatMap(([entity, deviations]) => (deviations || []).map(item => ({entity, stream_title: streamTitles[item.stream_id] || item.stream_id, ...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:'Review', render:r => esc(r.feedback || 'unreviewed')}, {label:'Evidence', key:'reason'}, {label:'Action', render:r => ` `}
+ {label:'Entity', key:'entity'}, {label:'Stream', key:'stream_title'}, {label:'Field', key:'field'}, {label:'Score', key:'score'}, {label:'Review', render:r => esc(r.feedback || 'unreviewed')}, {label:'Evidence', render:r => esc(`${r.reason}; current ${r.current ?? '-'} vs baseline ${r.baseline ?? '-'}; values: ${(r.sample_values || []).join(', ') || '-'}`)}, {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})});
+ const note = prompt('Review note (optional):') || '';
+ const days = prompt('Expiry in days (0 = no expiry):', '0') || '0';
+ 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, note, expires_at: Number(days) > 0 ? Math.floor(Date.now()/1000) + Number(days) * 86400 : 0})});
refresh();
}));
document.getElementById('relatedActivity').innerHTML = table(relatedRows, [