@@ -1901,6 +1901,7 @@ <h2 id="modalTitle">Add Custom Rule</h2>
19011901 }
19021902
19031903 function isMonitoredAlert ( a ) {
1904+ if ( a . blocked === false ) return true ;
19041905 const d = ( a . details || '' ) . toLowerCase ( ) ;
19051906 return d . includes ( 'monitored' ) || d . includes ( 'observed' ) || d . includes ( 'action=allow' ) ;
19061907 }
@@ -1953,13 +1954,16 @@ <h2 id="modalTitle">Add Custom Rule</h2>
19531954 const d = await fetchJSON ( '/api/user-rules' ) ;
19541955 exemptedIds = new Set ( d . rules . exempted_rule_ids || [ ] ) ;
19551956 heuristicOverrides = d . rules . heuristic_overrides || { } ;
1957+ ruleModeOverrides = d . rules . rule_mode_overrides || { } ;
19561958 pendingExemptions = null ;
19571959 pendingHeuristics = null ;
1960+ pendingRuleModes = null ;
19581961 renderRules ( ) ;
19591962 }
19601963 catch {
19611964 exemptedIds = new Set ( ) ;
19621965 heuristicOverrides = { } ;
1966+ ruleModeOverrides = { } ;
19631967 }
19641968 }
19651969
@@ -2016,7 +2020,8 @@ <h2 id="modalTitle">Add Custom Rule</h2>
20162020 const platLabel = plat === 'both' ? '' : '(' + plat + ')' ;
20172021 const sev = r . severity ? `<span class="rule-tag severity-${ r . severity . toLowerCase ( ) } ">${ esc ( r . severity ) } </span>` : '' ;
20182022 const desc = r . description ? `<div style="font-size:12px;color:var(--text-muted);margin-top:4px;width:100%;">${ esc ( r . description ) } </div>` : '' ;
2019- html += `<div class="rule-item">
2023+ const mode = ( pendingRuleModes ?? ruleModeOverrides ) [ r . id ] || ( exemptedIds . has ( r . id ) ? 'disabled' : 'enforce' ) ;
2024+ html += `<div class="rule-item" style="${ mode === 'disabled' ? 'opacity:0.5;' : '' } ">
20202025 <div class="rule-info" style="flex-wrap:wrap;">
20212026 <span class="rule-tag custom">${ r . id } </span>
20222027 <span style="font-size:16px;flex-shrink:0;">${ typeIcons [ r . type ] || '📌' } </span>
@@ -2025,7 +2030,8 @@ <h2 id="modalTitle">Add Custom Rule</h2>
20252030 <span style="font-size:11px;color:var(--text-muted);flex-shrink:0;">${ r . type } ${ platLabel } </span>
20262031 ${ desc }
20272032 </div>
2028- <div class="rule-actions">
2033+ <div class="rule-actions" style="display:flex; align-items:center; gap:8px;">
2034+ <div class="tri-toggle mode-${ mode } "><div class="tri-slider"></div><div class="tri-btn" data-val="disabled" onclick="toggleRuleMode('${ r . id } ', 'disabled')">🚫 Off</div><div class="tri-btn" data-val="alert" onclick="toggleRuleMode('${ r . id } ', 'alert')">👀 Alert</div><div class="tri-btn" data-val="enforce" onclick="toggleRuleMode('${ r . id } ', 'enforce')">🛡️ Enforce</div></div>
20292035 <button class="btn btn-ghost btn-sm" onclick="editCustomRule('${ r . id } ')">Edit</button>
20302036 <button class="btn btn-danger btn-sm" onclick="deleteCustomRule('${ r . id } ')">Delete</button>
20312037 </div>
@@ -2036,6 +2042,8 @@ <h2 id="modalTitle">Add Custom Rule</h2>
20362042
20372043 // ── Render System Rules ──
20382044 let activeRuleCategory = 'security' ;
2045+ let ruleModeOverrides = { } ;
2046+ let pendingRuleModes = null ;
20392047 function setRuleCategory ( cat ) {
20402048 activeRuleCategory = cat ;
20412049 document . getElementById ( 'btnCatSecurity' ) . style . background = cat === 'security' ? 'var(--accent)' : 'transparent' ;
@@ -2045,18 +2053,29 @@ <h2 id="modalTitle">Add Custom Rule</h2>
20452053 renderRules ( ) ;
20462054 }
20472055
2056+ function getSecurityRuleMode ( r ) {
2057+ const cur = pendingRuleModes ?? ruleModeOverrides ;
2058+ if ( cur [ r . id ] ) return cur [ r . id ] ;
2059+ return exemptedIds . has ( r . id ) ? 'disabled' : 'enforce' ;
2060+ }
2061+
20482062 function renderRules ( ) {
20492063 const c = document . getElementById ( 'rulesContainer' ) ;
2050- const curExempts = pendingExemptions || exemptedIds ;
2051- const curHeuristics = pendingHeuristics || heuristicOverrides ;
2064+ const curExempts = pendingExemptions ?? exemptedIds ;
2065+ const curHeuristics = pendingHeuristics ?? heuristicOverrides ;
2066+ const curRuleModes = pendingRuleModes ?? ruleModeOverrides ;
20522067 const q = ( document . getElementById ( 'ruleSearch' ) ?. value || '' ) . toLowerCase ( ) ;
20532068
20542069 const visible = allRuleItems . filter ( r => {
20552070 if ( r . type !== activeRuleCategory ) return false ;
20562071 return shouldShowRule ( r ) ;
20572072 } ) ;
20582073
2059- document . getElementById ( 'exemptionCount' ) . textContent = `${ curExempts . size } exempted` ;
2074+ const exemptCount = [ ...visible ] . filter ( r => {
2075+ const m = activeRuleCategory === 'security' ? getSecurityRuleMode ( r ) : ( curHeuristics [ r . id ] || r . defaultAction || 'alert' ) ;
2076+ return m === 'disabled' ;
2077+ } ) . length ;
2078+ document . getElementById ( 'exemptionCount' ) . textContent = `${ exemptCount } off` ;
20602079
20612080 if ( ! visible . length ) {
20622081 c . innerHTML = '<div class="empty-state"><p>No rules match your search for this category.</p></div>' ;
@@ -2073,18 +2092,17 @@ <h2 id="modalTitle">Add Custom Rule</h2>
20732092 let html = '' ;
20742093 for ( const [ cat , items ] of Object . entries ( groups ) ) {
20752094 if ( activeRuleCategory === 'security' ) {
2076- const ec = items . filter ( r => curExempts . has ( r . id ) ) . length ;
2077- html += `<div class="rule-group"><div class="rule-group-title">${ ec ? `${ cat } (${ items . length } ) · ${ ec } exempted` : `${ cat } (${ items . length } )` } </div>` ;
2095+ html += `<div class="rule-group"><div class="rule-group-title">${ cat } (${ items . length } )</div>` ;
20782096 for ( const r of items ) {
2079- const ex = curExempts . has ( r . id ) ;
2097+ const mode = getSecurityRuleMode ( r ) ;
20802098 const typeBadge = ruleTypeBadge ( r . id ) ;
2081- html += `<div class="rule-item" style="${ ex ? 'opacity:0.5;' : '' } "><div class="rule-info">${ typeBadge } <span class="rule-tag ${ ex ? 'exempted' : '' } ">${ r . id } </span><span class="rule-value" title="${ esc ( r . val ) } ">${ esc ( r . val ) } </span></div><label class="toggle"><input type="checkbox" ${ ex ? '' : 'checked' } onchange="toggleExemption ('${ r . id } ',this.checked )"><span class="toggle-slider"></span ></label ></div>` ;
2099+ html += `<div class="rule-item" style="${ mode === 'disabled' ? 'opacity:0.5;' : '' } "><div class="rule-info">${ typeBadge } <span class="rule-tag ${ mode === 'disabled' ? 'exempted' : '' } ">${ r . id } </span><span class="rule-value" title="${ esc ( r . val ) } ">${ esc ( r . val ) } </span></div><div class="rule-actions" style="min-width:100px;"><div class="tri-toggle mode- ${ mode } "><div class="tri-slider"></div><div class="tri-btn" data-val="disabled" onclick="toggleRuleMode(' ${ r . id } ', 'disabled')">🚫 Off</div><div class="tri-btn" data-val="alert" onclick="toggleRuleMode ('${ r . id } ', 'alert' )">👀 Alert</div><div class="tri-btn" data-val="enforce" onclick="toggleRuleMode(' ${ r . id } ', 'enforce')">🛡️ Enforce</div ></div></div ></div>` ;
20822100 }
20832101 html += '</div>' ;
20842102 } else {
20852103 // Heuristics
20862104 const rids = items . map ( r => r . id ) . join ( ',' ) ;
2087- html += `<details class="rule-group-details" open style="padding-bottom:12px; margin-bottom: 8px;">
2105+ html += `<details class="rule-group-details" style="padding-bottom:12px; margin-bottom: 8px;">
20882106 <summary class="rule-group-title" style="display:flex; justify-content:space-between; align-items:center;">
20892107 <span><span class="group-arrow">▶</span>${ cat } (${ items . length } )</span>
20902108 <div style="display:flex; gap:8px; align-items:center; font-size:12px; font-weight:normal;">
@@ -2141,6 +2159,20 @@ <h2 id="modalTitle">Add Custom Rule</h2>
21412159 renderRules ( ) ;
21422160 }
21432161
2162+ function toggleRuleMode ( id , mode ) {
2163+ if ( ! pendingRuleModes ) pendingRuleModes = { ...ruleModeOverrides } ;
2164+ pendingRuleModes [ id ] = mode ;
2165+ if ( mode === 'disabled' ) {
2166+ if ( ! pendingExemptions ) pendingExemptions = new Set ( exemptedIds ) ;
2167+ pendingExemptions . add ( id ) ;
2168+ } else {
2169+ if ( ! pendingExemptions ) pendingExemptions = new Set ( exemptedIds ) ;
2170+ pendingExemptions . delete ( id ) ;
2171+ }
2172+ document . getElementById ( 'saveBanner' ) . classList . add ( 'visible' ) ;
2173+ renderRules ( ) ;
2174+ }
2175+
21442176 function toggleHeuristic ( id , mode ) {
21452177 if ( ! pendingHeuristics ) pendingHeuristics = { ...heuristicOverrides } ;
21462178 pendingHeuristics [ id ] = mode ;
@@ -2161,16 +2193,25 @@ <h2 id="modalTitle">Add Custom Rule</h2>
21612193
21622194 async function saveExemptions ( ) {
21632195 try {
2196+ const body = { } ;
21642197 if ( pendingExemptions ) {
2165- await fetch ( '/api/user-rules' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( { exempted_rule_ids : Array . from ( pendingExemptions ) } ) } ) ;
2198+ body . exempted_rule_ids = Array . from ( pendingExemptions ) ;
21662199 exemptedIds = new Set ( pendingExemptions ) ;
21672200 pendingExemptions = null ;
21682201 }
2202+ if ( pendingRuleModes ) {
2203+ body . rule_mode_overrides = { ...ruleModeOverrides , ...pendingRuleModes } ;
2204+ ruleModeOverrides = body . rule_mode_overrides ;
2205+ pendingRuleModes = null ;
2206+ }
21692207 if ( pendingHeuristics ) {
2170- await fetch ( '/api/heuristic-overrides' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( { overrides : pendingHeuristics } ) } ) ;
2171- heuristicOverrides = { ... pendingHeuristics } ;
2208+ body . heuristic_overrides = { ... heuristicOverrides , ... pendingHeuristics } ;
2209+ heuristicOverrides = body . heuristic_overrides ;
21722210 pendingHeuristics = null ;
21732211 }
2212+ if ( Object . keys ( body ) . length ) {
2213+ await fetch ( '/api/user-rules' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( body ) } ) ;
2214+ }
21742215 document . getElementById ( 'saveBanner' ) . classList . remove ( 'visible' ) ;
21752216 renderRules ( ) ;
21762217 showToast ( "Changes applied." , 'success' ) ;
@@ -2180,6 +2221,7 @@ <h2 id="modalTitle">Add Custom Rule</h2>
21802221 function cancelExemptions ( ) {
21812222 pendingExemptions = null ;
21822223 pendingHeuristics = null ;
2224+ pendingRuleModes = null ;
21832225 document . getElementById ( 'saveBanner' ) . classList . remove ( 'visible' ) ;
21842226 renderRules ( ) ;
21852227 }
@@ -2190,10 +2232,7 @@ <h2 id="modalTitle">Add Custom Rule</h2>
21902232 toggleHeuristic ( id , 'disabled' ) ;
21912233 } else {
21922234 setRuleCategory ( 'security' ) ;
2193- if ( ! pendingExemptions ) pendingExemptions = new Set ( exemptedIds ) ;
2194- pendingExemptions . add ( id ) ;
2195- document . getElementById ( 'saveBanner' ) . classList . add ( 'visible' ) ;
2196- renderRules ( ) ;
2235+ toggleRuleMode ( id , 'disabled' ) ;
21972236 }
21982237 document . querySelectorAll ( '.tab' ) . forEach ( t => t . classList . remove ( 'active' ) ) ;
21992238 document . querySelectorAll ( '.tab-panel' ) . forEach ( p => p . classList . remove ( 'active' ) ) ;
0 commit comments