Enhanced Filter Debug page with inline editing and improved SSH filter testing

This commit is contained in:
2026-01-16 11:39:51 +01:00
parent 3e5f3dbec6
commit b561c94e20
15 changed files with 775 additions and 29 deletions

View File

@@ -1982,6 +1982,31 @@ func ListFiltersHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"filters": filters})
}
func GetFilterContentHandler(c *gin.Context) {
config.DebugLog("----------------------------")
config.DebugLog("GetFilterContentHandler called (handlers.go)")
filterName := c.Param("filter")
conn, err := resolveConnector(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
content, filePath, err := conn.GetFilterConfig(c.Request.Context(), filterName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get filter content: " + err.Error()})
return
}
// Remove comments for display in Filter Debug page only
content = fail2ban.RemoveComments(content)
c.JSON(http.StatusOK, gin.H{
"content": content,
"filterPath": filePath,
})
}
func TestFilterHandler(c *gin.Context) {
config.DebugLog("----------------------------")
config.DebugLog("TestFilterHandler called (handlers.go)") // entry point
@@ -1991,15 +2016,16 @@ func TestFilterHandler(c *gin.Context) {
return
}
var req struct {
FilterName string `json:"filterName"`
LogLines []string `json:"logLines"`
FilterName string `json:"filterName"`
LogLines []string `json:"logLines"`
FilterContent string `json:"filterContent"` // Optional: if provided, use this instead of reading from file
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
return
}
output, filterPath, err := conn.TestFilter(c.Request.Context(), req.FilterName, req.LogLines)
output, filterPath, err := conn.TestFilter(c.Request.Context(), req.FilterName, req.LogLines, req.FilterContent)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to test filter: " + err.Error()})
return

View File

@@ -62,6 +62,7 @@ func RegisterRoutes(r *gin.Engine, hub *Hub) {
// Filter debugger endpoints
api.GET("/filters", ListFiltersHandler)
api.GET("/filters/:filter/content", GetFilterContentHandler)
api.POST("/filters/test", TestFilterHandler)
api.POST("/filters", CreateFilterHandler)
api.DELETE("/filters/:filter", DeleteFilterHandler)

View File

@@ -43,9 +43,28 @@ function loadFilters() {
select.setAttribute('data-listener-added', 'true');
select.addEventListener('change', function() {
if (deleteBtn) deleteBtn.disabled = !select.value;
// Load filter content when a filter is selected
if (select.value) {
loadFilterContent(select.value);
} else {
const filterContentTextarea = document.getElementById('filterContentTextarea');
const editBtn = document.getElementById('editFilterContentBtn');
if (filterContentTextarea) {
filterContentTextarea.value = '';
filterContentTextarea.readOnly = true;
filterContentTextarea.classList.add('bg-gray-50');
filterContentTextarea.classList.remove('bg-white');
}
if (editBtn) editBtn.classList.add('hidden');
updateFilterContentHints(false);
}
});
}
if (deleteBtn) deleteBtn.disabled = !select.value;
// If a filter is already selected (e.g., first one by default), load its content
if (select.value) {
loadFilterContent(select.value);
}
}
})
.catch(err => {
@@ -54,9 +73,93 @@ function loadFilters() {
.finally(() => showLoading(false));
}
function loadFilterContent(filterName) {
const filterContentTextarea = document.getElementById('filterContentTextarea');
const editBtn = document.getElementById('editFilterContentBtn');
if (!filterContentTextarea) return;
showLoading(true);
fetch(withServerParam('/api/filters/' + encodeURIComponent(filterName) + '/content'), {
headers: serverHeaders()
})
.then(res => res.json())
.then(data => {
if (data.error) {
showToast('Error loading filter content: ' + data.error, 'error');
filterContentTextarea.value = '';
filterContentTextarea.readOnly = true;
if (editBtn) editBtn.classList.add('hidden');
updateFilterContentHints(false);
return;
}
filterContentTextarea.value = data.content || '';
filterContentTextarea.readOnly = true; // Keep it readonly by default
filterContentTextarea.classList.add('bg-gray-50');
filterContentTextarea.classList.remove('bg-white');
if (editBtn) editBtn.classList.remove('hidden');
updateFilterContentHints(false);
})
.catch(err => {
showToast('Error loading filter content: ' + err, 'error');
filterContentTextarea.value = '';
filterContentTextarea.readOnly = true;
if (editBtn) editBtn.classList.add('hidden');
updateFilterContentHints(false);
})
.finally(() => showLoading(false));
}
function toggleFilterContentEdit() {
const filterContentTextarea = document.getElementById('filterContentTextarea');
const editBtn = document.getElementById('editFilterContentBtn');
if (!filterContentTextarea) return;
if (filterContentTextarea.readOnly) {
// Make editable
filterContentTextarea.readOnly = false;
filterContentTextarea.classList.remove('bg-gray-50');
filterContentTextarea.classList.add('bg-white');
if (editBtn) {
editBtn.textContent = t('filter_debug.cancel_edit', 'Cancel');
editBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700');
editBtn.classList.add('bg-gray-600', 'hover:bg-gray-700');
}
updateFilterContentHints(true);
} else {
// Make readonly
filterContentTextarea.readOnly = true;
filterContentTextarea.classList.add('bg-gray-50');
filterContentTextarea.classList.remove('bg-white');
if (editBtn) {
editBtn.textContent = t('filter_debug.edit_filter', 'Edit');
editBtn.classList.remove('bg-gray-600', 'hover:bg-gray-700');
editBtn.classList.add('bg-blue-600', 'hover:bg-blue-700');
}
updateFilterContentHints(false);
}
}
function updateFilterContentHints(isEditable) {
const readonlyHint = document.querySelector('p[data-i18n="filter_debug.filter_content_hint_readonly"]');
const editableHint = document.getElementById('filterContentHintEditable');
if (isEditable) {
if (readonlyHint) readonlyHint.classList.add('hidden');
if (editableHint) editableHint.classList.remove('hidden');
} else {
if (readonlyHint) readonlyHint.classList.remove('hidden');
if (editableHint) editableHint.classList.add('hidden');
}
if (typeof updateTranslations === 'function') {
updateTranslations();
}
}
function testSelectedFilter() {
const filterName = document.getElementById('filterSelect').value;
const lines = document.getElementById('logLinesTextarea').value.split('\n').filter(line => line.trim() !== '');
const filterContentTextarea = document.getElementById('filterContentTextarea');
if (!filterName) {
showToast('Please select a filter.', 'info');
@@ -74,13 +177,24 @@ function testSelectedFilter() {
testResultsEl.innerHTML = '';
showLoading(true);
const requestBody = {
filterName: filterName,
logLines: lines
};
// Only include filter content if textarea is editable (not readonly)
// If readonly, test the original filter from server
if (filterContentTextarea && !filterContentTextarea.readOnly) {
const filterContent = filterContentTextarea.value.trim();
if (filterContent) {
requestBody.filterContent = filterContent;
}
}
fetch(withServerParam('/api/filters/test'), {
method: 'POST',
headers: serverHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({
filterName: filterName,
logLines: lines
})
body: JSON.stringify(requestBody)
})
.then(res => res.json())
.then(data => {
@@ -122,6 +236,7 @@ function renderTestResults(output, filterPath) {
function showFilterSection() {
const testResultsEl = document.getElementById('testResults');
const filterContentTextarea = document.getElementById('filterContentTextarea');
if (!currentServerId) {
var notice = document.getElementById('filterNotice');
if (notice) {
@@ -130,6 +245,10 @@ function showFilterSection() {
}
document.getElementById('filterSelect').innerHTML = '';
document.getElementById('logLinesTextarea').value = '';
if (filterContentTextarea) {
filterContentTextarea.value = '';
filterContentTextarea.readOnly = true;
}
testResultsEl.innerHTML = '';
testResultsEl.classList.add('hidden');
document.getElementById('deleteFilterBtn').disabled = true;
@@ -139,12 +258,37 @@ function showFilterSection() {
testResultsEl.innerHTML = '';
testResultsEl.classList.add('hidden');
document.getElementById('logLinesTextarea').value = '';
// Add change listener to enable/disable delete button
const editBtn = document.getElementById('editFilterContentBtn');
if (filterContentTextarea) {
filterContentTextarea.value = '';
filterContentTextarea.readOnly = true;
filterContentTextarea.classList.add('bg-gray-50');
filterContentTextarea.classList.remove('bg-white');
}
if (editBtn) editBtn.classList.add('hidden');
updateFilterContentHints(false);
// Add change listener to enable/disable delete button and load filter content
const filterSelect = document.getElementById('filterSelect');
const deleteBtn = document.getElementById('deleteFilterBtn');
filterSelect.addEventListener('change', function() {
deleteBtn.disabled = !filterSelect.value;
});
if (!filterSelect.hasAttribute('data-listener-added')) {
filterSelect.setAttribute('data-listener-added', 'true');
filterSelect.addEventListener('change', function() {
deleteBtn.disabled = !filterSelect.value;
if (filterSelect.value) {
loadFilterContent(filterSelect.value);
} else {
const editBtn = document.getElementById('editFilterContentBtn');
if (filterContentTextarea) {
filterContentTextarea.value = '';
filterContentTextarea.readOnly = true;
filterContentTextarea.classList.add('bg-gray-50');
filterContentTextarea.classList.remove('bg-white');
}
if (editBtn) editBtn.classList.add('hidden');
updateFilterContentHints(false);
}
});
}
}
function openCreateFilterModal() {
@@ -234,6 +378,16 @@ function deleteFilter() {
document.getElementById('testResults').innerHTML = '';
document.getElementById('testResults').classList.add('hidden');
document.getElementById('logLinesTextarea').value = '';
const filterContentTextarea = document.getElementById('filterContentTextarea');
const editBtn = document.getElementById('editFilterContentBtn');
if (filterContentTextarea) {
filterContentTextarea.value = '';
filterContentTextarea.readOnly = true;
filterContentTextarea.classList.add('bg-gray-50');
filterContentTextarea.classList.remove('bg-white');
}
if (editBtn) editBtn.classList.add('hidden');
updateFilterContentHints(false);
})
.catch(function(err) {
console.error('Error deleting filter:', err);

View File

@@ -163,6 +163,18 @@
</div>
</div>
<!-- Textarea for filter content (readonly by default, editable with Edit button) -->
<div class="mb-4">
<div class="flex items-center justify-between mb-2">
<label for="filterContentTextarea" class="block text-sm font-medium text-gray-700" data-i18n="filter_debug.filter_content">Filter Content</label>
<button type="button" id="editFilterContentBtn" onclick="toggleFilterContentEdit()" class="text-sm px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 hidden" data-i18n="filter_debug.edit_filter">Edit</button>
</div>
<p class="text-xs text-gray-500 mb-2" data-i18n="filter_debug.filter_content_hint_readonly">Filter content is shown read-only. Click 'Edit' to modify for testing. Changes are temporary and not saved.</p>
<p class="text-xs text-gray-500 mb-2 hidden" id="filterContentHintEditable" data-i18n="filter_debug.filter_content_hint">Edit the filter regex below for testing. Changes are temporary and not saved.</p>
<textarea id="filterContentTextarea" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 h-40 font-mono text-sm bg-gray-50"
placeholder="Filter content will appear here when a filter is selected..." readonly></textarea>
</div>
<!-- Textarea for log lines to test -->
<div class="mb-4">
<label for="logLinesTextarea" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="filter_debug.log_lines">Log Lines</label>