improve find fileds for default profiles

This commit is contained in:
larssand
2026-07-02 09:51:25 +02:00
parent 53437a3952
commit 7c0b482a22
6 changed files with 167 additions and 15 deletions

View File

@@ -189,11 +189,17 @@ function profileDiscoveryDetails(row) {
}
const selected = d.selected_fields || {};
const top = (d.top_fields || []).slice(0,8).map(item => `${item.field} ${(Number(item.coverage || 0)*100).toFixed(0)}%/${item.unique_values}`).join(', ');
const shared = (d.shared_fields || row.shared_fields || []).slice(0,8).map(item => `${item.field} (${item.streams} streams)`).join(', ');
const shared = (d.shared_fields || row.shared_fields || []).slice(0,8).map(sharedFieldLabel).join(', ');
const rejected = (d.rejected_fields || []).slice(0,8).map(item => `${item.field}: ${item.reason}`).join('; ');
const reasons = (d.reasons || []).join('; ');
return `<details data-detail-id="${esc(detailId)}"><summary>${esc(d.field_count)} fields analyzed</summary><p><b>Selected</b><br>Entity: ${esc((selected.entity || []).join(', ') || '-')}<br>Time: ${esc((selected.time || []).join(', ') || '-')}<br>Categorical: ${esc((selected.categorical || []).join(', ') || '-')}<br>Numeric: ${esc((selected.numeric || []).join(', ') || '-')}</p><p><b>Shared across streams</b><br>${esc(shared || '-')}</p><p><b>Top fields</b><br>${esc(top || '-')}</p><p><b>Rejected</b><br>${esc(rejected || '-')}</p><p>${esc(reasons || '')}</p></details>`;
}
function sharedFieldLabel(item) {
const aliases = (item.aliases || []).filter(alias => alias !== item.field).slice(0,4);
const suffix = aliases.length ? ` via ${aliases.join('/')}` : '';
const kind = item.kind ? ` ${item.kind}` : '';
return `${item.field}${kind} (${item.streams} streams${suffix})`;
}
function profileAdvisorLabel(row, advisor) {
const rowAdvisor = row.profile_advisor || {};
if (rowAdvisor.status === 'heuristic' && rowAdvisor.error) return 'heuristic (advisor error)';
@@ -346,8 +352,9 @@ async function refresh() {
{label:'Baseline fields', render:r => esc([...(r.categorical_fields || []), ...(r.numeric_fields || [])].slice(0,8).join(', ') || '-')},
{label:'Detectors', render:r => esc(Object.keys(r.detectors || {}).join(', ') || '-')},
{label:'Common denominators', render:r => esc((r.common_fields || []).slice(0,5).map(item => `${item.field} ${(item.coverage*100).toFixed(0)}%`).join(', ') || '-')},
{label:'Shared fields', render:r => esc((r.shared_fields || []).slice(0,4).map(sharedFieldLabel).join(', ') || '-')},
{label:'Discovery', render:r => profileDiscoveryDetails(r)},
{label:'Action', render:r => r.profile_exists ? '<button type="button" disabled>Applied</button>' : `<button type="button" class="apply-suggested-profile" data-stream-id="${esc(r.stream_id)}">Apply profile</button>`}
{label:'Action', render:r => `<button type="button" class="apply-suggested-profile" data-stream-id="${esc(r.stream_id)}">${r.profile_exists ? 'Update profile' : 'Apply profile'}</button>`}
], 'profile-suggestions') : '<p class="muted">No missing stream profiles. Enable "show existing profiles" to inspect already configured profiles.</p>';
const llmText = llm.text ? esc(llm.text).replace(/\\n/g, '<br>') : esc(llm.error || 'LLM assessment disabled or waiting for first run.');
document.getElementById('llmAssessment').innerHTML = `<div>Status: <code>${esc(llm.status || 'unknown')}</code></div><p>${llmText}</p>`;
@@ -552,6 +559,22 @@ function renderStreamPicker(errorText='') {
document.getElementById('enableVisibleStreams').addEventListener('click', () => { streams.forEach(stream => { window.streamSelection[stream.id].enabled = true; }); renderStreamPicker(); });
document.getElementById('disableVisibleStreams').addEventListener('click', () => { streams.forEach(stream => { window.streamSelection[stream.id].enabled = false; }); renderStreamPicker(); });
}
function mergeProfile(existing, recommended) {
const mergeList = (left, right) => [...new Set([...(left || []), ...(right || [])].filter(Boolean))];
const detectors = {...(recommended.detectors || {}), ...(existing.detectors || {})};
return {
...recommended,
...existing,
name: existing.name || recommended.name,
entity_field: (existing.entity_field || recommended.entity_field || (recommended.entity_fields || [])[0] || ''),
entity_fields: mergeList(existing.entity_fields || (existing.entity_field ? [existing.entity_field] : []), recommended.entity_fields || (recommended.entity_field ? [recommended.entity_field] : [])),
timestamp_field: existing.timestamp_field || recommended.timestamp_field || 'timestamp',
categorical_fields: mergeList(existing.categorical_fields, recommended.categorical_fields),
numeric_fields: mergeList(existing.numeric_fields, recommended.numeric_fields),
detectors,
field_weights: {...(recommended.field_weights || {}), ...(existing.field_weights || {})}
};
}
async function applySuggestedProfile(streamId) {
const notice = document.getElementById('profileApplyResult');
const button = document.querySelector(`.apply-suggested-profile[data-stream-id="${CSS.escape(streamId)}"]`);
@@ -565,7 +588,8 @@ async function applySuggestedProfile(streamId) {
notice.textContent = `Applying profile for ${suggestion.stream_name || streamId}...`;
const configResponse = await fetch('/api/config', {cache: 'no-store'});
const config = await configResponse.json();
const profile = suggestion.profile;
const existingProfile = (config.graylog_stream_profiles || []).find(item => String(item.stream_id) === String(suggestion.profile.stream_id));
const profile = existingProfile ? mergeProfile(existingProfile, suggestion.profile) : suggestion.profile;
const payload = {
graylog_stream_profiles: [...(config.graylog_stream_profiles || []).filter(item => item.stream_id !== profile.stream_id), profile],
graylog_streams: config.graylog_streams || [],
@@ -601,7 +625,7 @@ async function applySuggestedProfile(streamId) {
notice.textContent = `Could not apply profile: ${result.error || response.statusText || response.status}`;
return;
}
notice.textContent = `Applied recommended profile for ${suggestion.stream_name || streamId}. Monitor will use it on the next cycle.`;
notice.textContent = `${existingProfile ? 'Updated' : 'Applied'} recommended profile for ${suggestion.stream_name || streamId}. Monitor will use it on the next cycle.`;
document.getElementById('settingsResult').textContent = notice.textContent;
window.streamProfiles = result.graylog_stream_profiles || payload.graylog_stream_profiles;
loadSettings();