mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-11 21:33:00 +00:00
fix(web): unify operation history tracking for monorepo plugin operations (#240)
The operation history UI was reading from the wrong data source (operation_queue instead of operation_history), install/update records lacked version details, toggle operations used a type name that didn't match UI filters, and the Clear History button was non-functional. - Switch GET /plugins/operation/history to read from OperationHistory audit log with return type hint and targeted exception handling - Add DELETE /plugins/operation/history endpoint; wire up Clear button - Add _get_plugin_version helper with specific exception handling (FileNotFoundError, PermissionError, json.JSONDecodeError) and structured logging with plugin_id/path context - Record plugin version, branch, and commit details on install/update - Record install failures in the direct (non-queue) code path - Replace "toggle" operation type with "enable"/"disable" - Add normalizeStatus() in JS to map completed→success, error→failed so status filter works regardless of server-side convention - Truncate commit SHAs to 7 chars in details display - Fix HTML filter options, operation type colors, duplicate JS init Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -167,6 +167,13 @@ class OperationHistory:
|
|||||||
|
|
||||||
return history[:limit]
|
return history[:limit]
|
||||||
|
|
||||||
|
def clear_history(self) -> None:
|
||||||
|
"""Clear all operation history records."""
|
||||||
|
with self._lock:
|
||||||
|
self._history.clear()
|
||||||
|
self._save_history()
|
||||||
|
self.logger.info("Operation history cleared")
|
||||||
|
|
||||||
def _save_history(self) -> None:
|
def _save_history(self) -> None:
|
||||||
"""Save history to file."""
|
"""Save history to file."""
|
||||||
if not self.history_file:
|
if not self.history_file:
|
||||||
|
|||||||
@@ -54,6 +54,23 @@ SYSTEM_FONTS = frozenset([
|
|||||||
|
|
||||||
api_v3 = Blueprint('api_v3', __name__)
|
api_v3 = Blueprint('api_v3', __name__)
|
||||||
|
|
||||||
|
def _get_plugin_version(plugin_id: str) -> str:
|
||||||
|
"""Read the installed version from a plugin's manifest.json.
|
||||||
|
|
||||||
|
Returns the version string on success, or '' if the manifest
|
||||||
|
cannot be read (missing, corrupt, permission denied, etc.).
|
||||||
|
"""
|
||||||
|
manifest_path = Path(api_v3.plugin_store_manager.plugins_dir) / plugin_id / "manifest.json"
|
||||||
|
try:
|
||||||
|
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||||
|
manifest = json.load(f)
|
||||||
|
return manifest.get('version', '')
|
||||||
|
except (FileNotFoundError, PermissionError, OSError) as e:
|
||||||
|
logger.warning("[PluginVersion] Could not read manifest for %s at %s: %s", plugin_id, manifest_path, e)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.warning("[PluginVersion] Invalid JSON in manifest for %s at %s: %s", plugin_id, manifest_path, e)
|
||||||
|
return ''
|
||||||
|
|
||||||
def _ensure_cache_manager():
|
def _ensure_cache_manager():
|
||||||
"""Ensure cache manager is initialized."""
|
"""Ensure cache manager is initialized."""
|
||||||
global cache_manager
|
global cache_manager
|
||||||
@@ -2114,10 +2131,9 @@ def toggle_plugin():
|
|||||||
# Log operation
|
# Log operation
|
||||||
if api_v3.operation_history:
|
if api_v3.operation_history:
|
||||||
api_v3.operation_history.record_operation(
|
api_v3.operation_history.record_operation(
|
||||||
"toggle",
|
"enable" if enabled else "disable",
|
||||||
plugin_id=plugin_id,
|
plugin_id=plugin_id,
|
||||||
status="success" if enabled else "disabled",
|
status="success"
|
||||||
details={"enabled": enabled}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# If plugin is loaded, also call its lifecycle methods
|
# If plugin is loaded, also call its lifecycle methods
|
||||||
@@ -2143,8 +2159,9 @@ def toggle_plugin():
|
|||||||
from src.web_interface.errors import WebInterfaceError
|
from src.web_interface.errors import WebInterfaceError
|
||||||
error = WebInterfaceError.from_exception(e, ErrorCode.PLUGIN_OPERATION_CONFLICT)
|
error = WebInterfaceError.from_exception(e, ErrorCode.PLUGIN_OPERATION_CONFLICT)
|
||||||
if api_v3.operation_history:
|
if api_v3.operation_history:
|
||||||
|
toggle_type = "enable" if ('data' in locals() and data.get('enabled')) else "disable"
|
||||||
api_v3.operation_history.record_operation(
|
api_v3.operation_history.record_operation(
|
||||||
"toggle",
|
toggle_type,
|
||||||
plugin_id=data.get('plugin_id') if 'data' in locals() else None,
|
plugin_id=data.get('plugin_id') if 'data' in locals() else None,
|
||||||
status="failed",
|
status="failed",
|
||||||
error=str(e)
|
error=str(e)
|
||||||
@@ -2188,36 +2205,54 @@ def get_operation_status(operation_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@api_v3.route('/plugins/operation/history', methods=['GET'])
|
@api_v3.route('/plugins/operation/history', methods=['GET'])
|
||||||
def get_operation_history():
|
def get_operation_history() -> Response:
|
||||||
"""Get operation history"""
|
"""Get operation history from the audit log."""
|
||||||
try:
|
if not api_v3.operation_history:
|
||||||
if not api_v3.operation_queue:
|
|
||||||
return error_response(
|
|
||||||
ErrorCode.SYSTEM_ERROR,
|
|
||||||
'Operation queue not initialized',
|
|
||||||
status_code=500
|
|
||||||
)
|
|
||||||
|
|
||||||
limit = request.args.get('limit', 50, type=int)
|
|
||||||
plugin_id = request.args.get('plugin_id')
|
|
||||||
|
|
||||||
history = api_v3.operation_queue.get_operation_history(limit=limit)
|
|
||||||
|
|
||||||
# Filter by plugin_id if provided
|
|
||||||
if plugin_id:
|
|
||||||
history = [op for op in history if op.plugin_id == plugin_id]
|
|
||||||
|
|
||||||
return success_response(data=[op.to_dict() for op in history])
|
|
||||||
except Exception as e:
|
|
||||||
from src.web_interface.errors import WebInterfaceError
|
|
||||||
error = WebInterfaceError.from_exception(e, ErrorCode.SYSTEM_ERROR)
|
|
||||||
return error_response(
|
return error_response(
|
||||||
error.error_code,
|
ErrorCode.SYSTEM_ERROR,
|
||||||
error.message,
|
'Operation history not initialized',
|
||||||
details=error.details,
|
|
||||||
status_code=500
|
status_code=500
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
limit = request.args.get('limit', 50, type=int)
|
||||||
|
plugin_id = request.args.get('plugin_id')
|
||||||
|
operation_type = request.args.get('operation_type')
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
return error_response(ErrorCode.INVALID_INPUT, f'Invalid query parameter: {e}', status_code=400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
history = api_v3.operation_history.get_history(
|
||||||
|
limit=limit,
|
||||||
|
plugin_id=plugin_id,
|
||||||
|
operation_type=operation_type
|
||||||
|
)
|
||||||
|
except (AttributeError, RuntimeError) as e:
|
||||||
|
from src.web_interface.errors import WebInterfaceError
|
||||||
|
error = WebInterfaceError.from_exception(e, ErrorCode.SYSTEM_ERROR)
|
||||||
|
return error_response(error.error_code, error.message, details=error.details, status_code=500)
|
||||||
|
|
||||||
|
return success_response(data=[record.to_dict() for record in history])
|
||||||
|
|
||||||
|
@api_v3.route('/plugins/operation/history', methods=['DELETE'])
|
||||||
|
def clear_operation_history() -> Response:
|
||||||
|
"""Clear operation history."""
|
||||||
|
if not api_v3.operation_history:
|
||||||
|
return error_response(
|
||||||
|
ErrorCode.SYSTEM_ERROR,
|
||||||
|
'Operation history not initialized',
|
||||||
|
status_code=500
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
api_v3.operation_history.clear_history()
|
||||||
|
except (OSError, RuntimeError) as e:
|
||||||
|
from src.web_interface.errors import WebInterfaceError
|
||||||
|
error = WebInterfaceError.from_exception(e, ErrorCode.SYSTEM_ERROR)
|
||||||
|
return error_response(error.error_code, error.message, details=error.details, status_code=500)
|
||||||
|
|
||||||
|
return success_response(message='Operation history cleared')
|
||||||
|
|
||||||
@api_v3.route('/plugins/state', methods=['GET'])
|
@api_v3.route('/plugins/state', methods=['GET'])
|
||||||
def get_plugin_state():
|
def get_plugin_state():
|
||||||
"""Get plugin state from state manager"""
|
"""Get plugin state from state manager"""
|
||||||
@@ -2608,13 +2643,16 @@ def update_plugin():
|
|||||||
{'last_updated': datetime.now()}
|
{'last_updated': datetime.now()}
|
||||||
)
|
)
|
||||||
if api_v3.operation_history:
|
if api_v3.operation_history:
|
||||||
|
version = _get_plugin_version(plugin_id)
|
||||||
api_v3.operation_history.record_operation(
|
api_v3.operation_history.record_operation(
|
||||||
"update",
|
"update",
|
||||||
plugin_id=plugin_id,
|
plugin_id=plugin_id,
|
||||||
status="success",
|
status="success",
|
||||||
details={
|
details={
|
||||||
"last_updated": updated_last_updated,
|
"version": version,
|
||||||
"commit": updated_commit
|
"previous_commit": current_commit[:7] if current_commit else None,
|
||||||
|
"commit": updated_commit[:7] if updated_commit else None,
|
||||||
|
"branch": updated_branch
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2649,7 +2687,11 @@ def update_plugin():
|
|||||||
"update",
|
"update",
|
||||||
plugin_id=plugin_id,
|
plugin_id=plugin_id,
|
||||||
status="failed",
|
status="failed",
|
||||||
error=error_msg
|
error=error_msg,
|
||||||
|
details={
|
||||||
|
"previous_commit": current_commit[:7] if current_commit else None,
|
||||||
|
"branch": current_branch
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
@@ -2874,10 +2916,12 @@ def install_plugin():
|
|||||||
|
|
||||||
# Record in history
|
# Record in history
|
||||||
if api_v3.operation_history:
|
if api_v3.operation_history:
|
||||||
|
version = _get_plugin_version(plugin_id)
|
||||||
api_v3.operation_history.record_operation(
|
api_v3.operation_history.record_operation(
|
||||||
"install",
|
"install",
|
||||||
plugin_id=plugin_id,
|
plugin_id=plugin_id,
|
||||||
status="success"
|
status="success",
|
||||||
|
details={"version": version, "branch": branch}
|
||||||
)
|
)
|
||||||
|
|
||||||
branch_msg = f" (branch: {branch})" if branch else ""
|
branch_msg = f" (branch: {branch})" if branch else ""
|
||||||
@@ -2896,7 +2940,8 @@ def install_plugin():
|
|||||||
"install",
|
"install",
|
||||||
plugin_id=plugin_id,
|
plugin_id=plugin_id,
|
||||||
status="failed",
|
status="failed",
|
||||||
error=error_msg
|
error=error_msg,
|
||||||
|
details={"branch": branch}
|
||||||
)
|
)
|
||||||
|
|
||||||
raise Exception(error_msg)
|
raise Exception(error_msg)
|
||||||
@@ -2926,7 +2971,13 @@ def install_plugin():
|
|||||||
if api_v3.plugin_state_manager:
|
if api_v3.plugin_state_manager:
|
||||||
api_v3.plugin_state_manager.set_plugin_installed(plugin_id)
|
api_v3.plugin_state_manager.set_plugin_installed(plugin_id)
|
||||||
if api_v3.operation_history:
|
if api_v3.operation_history:
|
||||||
api_v3.operation_history.record_operation("install", plugin_id=plugin_id, status="success")
|
version = _get_plugin_version(plugin_id)
|
||||||
|
api_v3.operation_history.record_operation(
|
||||||
|
"install",
|
||||||
|
plugin_id=plugin_id,
|
||||||
|
status="success",
|
||||||
|
details={"version": version, "branch": branch}
|
||||||
|
)
|
||||||
|
|
||||||
branch_msg = f" (branch: {branch})" if branch else ""
|
branch_msg = f" (branch: {branch})" if branch else ""
|
||||||
return success_response(message=f'Plugin {plugin_id} installed successfully{branch_msg}')
|
return success_response(message=f'Plugin {plugin_id} installed successfully{branch_msg}')
|
||||||
@@ -2938,6 +2989,15 @@ def install_plugin():
|
|||||||
if not plugin_info:
|
if not plugin_info:
|
||||||
error_msg += ' (plugin not found in registry)'
|
error_msg += ' (plugin not found in registry)'
|
||||||
|
|
||||||
|
if api_v3.operation_history:
|
||||||
|
api_v3.operation_history.record_operation(
|
||||||
|
"install",
|
||||||
|
plugin_id=plugin_id,
|
||||||
|
status="failed",
|
||||||
|
error=error_msg,
|
||||||
|
details={"branch": branch}
|
||||||
|
)
|
||||||
|
|
||||||
return error_response(
|
return error_response(
|
||||||
ErrorCode.PLUGIN_INSTALL_FAILED,
|
ErrorCode.PLUGIN_INSTALL_FAILED,
|
||||||
error_msg,
|
error_msg,
|
||||||
|
|||||||
@@ -20,17 +20,14 @@
|
|||||||
<option value="uninstall">Uninstall</option>
|
<option value="uninstall">Uninstall</option>
|
||||||
<option value="enable">Enable</option>
|
<option value="enable">Enable</option>
|
||||||
<option value="disable">Disable</option>
|
<option value="disable">Disable</option>
|
||||||
<option value="config_save">Config Save</option>
|
<option value="configure">Configure</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- Filter by Status -->
|
<!-- Filter by Status -->
|
||||||
<select id="history-status-filter" class="form-control text-sm">
|
<select id="history-status-filter" class="form-control text-sm">
|
||||||
<option value="">All Statuses</option>
|
<option value="">All Statuses</option>
|
||||||
<option value="completed">Completed</option>
|
|
||||||
<option value="success">Success</option>
|
<option value="success">Success</option>
|
||||||
<option value="failed">Failed</option>
|
<option value="failed">Failed</option>
|
||||||
<option value="error">Error</option>
|
|
||||||
<option value="in_progress">In Progress</option>
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- Search -->
|
<!-- Search -->
|
||||||
@@ -101,6 +98,16 @@
|
|||||||
let allHistory = [];
|
let allHistory = [];
|
||||||
let filteredHistory = [];
|
let filteredHistory = [];
|
||||||
|
|
||||||
|
// Normalize status values so both server-side conventions match filter values.
|
||||||
|
// The server may return "completed" or "success" for successful operations,
|
||||||
|
// and "error" or "failed" for failures.
|
||||||
|
function normalizeStatus(status) {
|
||||||
|
const s = (status || '').toLowerCase();
|
||||||
|
if (s === 'completed' || s === 'success') return 'success';
|
||||||
|
if (s === 'error' || s === 'failed') return 'failed';
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
// Load operation history
|
// Load operation history
|
||||||
async function loadHistory() {
|
async function loadHistory() {
|
||||||
try {
|
try {
|
||||||
@@ -130,7 +137,7 @@
|
|||||||
filteredHistory = allHistory.filter(record => {
|
filteredHistory = allHistory.filter(record => {
|
||||||
if (pluginFilter && record.plugin_id !== pluginFilter) return false;
|
if (pluginFilter && record.plugin_id !== pluginFilter) return false;
|
||||||
if (typeFilter && record.operation_type !== typeFilter) return false;
|
if (typeFilter && record.operation_type !== typeFilter) return false;
|
||||||
if (statusFilter && record.status !== statusFilter) return false;
|
if (statusFilter && normalizeStatus(record.status) !== statusFilter) return false;
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
const searchable = [
|
const searchable = [
|
||||||
record.operation_type,
|
record.operation_type,
|
||||||
@@ -171,13 +178,28 @@
|
|||||||
tbody.innerHTML = pageData.map(record => {
|
tbody.innerHTML = pageData.map(record => {
|
||||||
const timestamp = new Date(record.timestamp || record.created_at || Date.now());
|
const timestamp = new Date(record.timestamp || record.created_at || Date.now());
|
||||||
const timeStr = timestamp.toLocaleString();
|
const timeStr = timestamp.toLocaleString();
|
||||||
const statusClass = getStatusClass(record.status);
|
const normalized = normalizeStatus(record.status);
|
||||||
|
const statusClass = getStatusClass(normalized);
|
||||||
const operationType = record.operation_type || 'unknown';
|
const operationType = record.operation_type || 'unknown';
|
||||||
const pluginId = record.plugin_id || '-';
|
const pluginId = record.plugin_id || '-';
|
||||||
const user = record.user || '-';
|
const user = record.user || '-';
|
||||||
const details = record.details ? JSON.stringify(record.details, null, 2) : '';
|
|
||||||
const error = record.error ? `<div class="text-red-600 text-xs mt-1">${escapeHtml(record.error)}</div>` : '';
|
const error = record.error ? `<div class="text-red-600 text-xs mt-1">${escapeHtml(record.error)}</div>` : '';
|
||||||
|
|
||||||
|
// Build a concise details summary
|
||||||
|
let detailsSummary = '';
|
||||||
|
const d = record.details;
|
||||||
|
if (d) {
|
||||||
|
const parts = [];
|
||||||
|
if (d.version) parts.push(`v${d.version}`);
|
||||||
|
if (d.branch) parts.push(`branch: ${d.branch}`);
|
||||||
|
if (d.commit) parts.push(`commit: ${String(d.commit).substring(0, 7)}`);
|
||||||
|
if (d.previous_commit && d.commit && d.previous_commit !== d.commit) {
|
||||||
|
parts.push(`from: ${String(d.previous_commit).substring(0, 7)}`);
|
||||||
|
}
|
||||||
|
if (d.preserve_config !== undefined) parts.push(d.preserve_config ? 'config preserved' : 'config removed');
|
||||||
|
detailsSummary = parts.length > 0 ? `<span class="text-xs text-gray-500">${escapeHtml(parts.join(' | '))}</span>` : '';
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr class="hover:bg-gray-50">
|
<tr class="hover:bg-gray-50">
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${escapeHtml(timeStr)}</td>
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${escapeHtml(timeStr)}</td>
|
||||||
@@ -189,12 +211,12 @@
|
|||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${escapeHtml(pluginId)}</td>
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${escapeHtml(pluginId)}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
<span class="px-2 py-1 text-xs font-medium rounded ${statusClass}">
|
<span class="px-2 py-1 text-xs font-medium rounded ${statusClass}">
|
||||||
${escapeHtml(record.status || 'unknown')}
|
${escapeHtml(normalized)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${escapeHtml(user)}</td>
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${escapeHtml(user)}</td>
|
||||||
<td class="px-6 py-4 text-sm text-gray-500">
|
<td class="px-6 py-4 text-sm text-gray-500">
|
||||||
${details ? `<details class="cursor-pointer"><summary class="text-blue-600 hover:text-blue-800">View Details</summary><pre class="mt-2 text-xs bg-gray-100 p-2 rounded overflow-auto max-h-32">${escapeHtml(details)}</pre></details>` : '-'}
|
${detailsSummary || '-'}
|
||||||
${error}
|
${error}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -215,14 +237,8 @@
|
|||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
function getStatusClass(status) {
|
function getStatusClass(status) {
|
||||||
const statusLower = (status || '').toLowerCase();
|
if (status === 'success') return 'bg-green-100 text-green-800';
|
||||||
if (statusLower === 'success' || statusLower === 'completed') {
|
if (status === 'failed') return 'bg-red-100 text-red-800';
|
||||||
return 'bg-green-100 text-green-800';
|
|
||||||
} else if (statusLower === 'failed' || statusLower === 'error') {
|
|
||||||
return 'bg-red-100 text-red-800';
|
|
||||||
} else if (statusLower === 'in_progress' || statusLower === 'pending') {
|
|
||||||
return 'bg-yellow-100 text-yellow-800';
|
|
||||||
}
|
|
||||||
return 'bg-gray-100 text-gray-800';
|
return 'bg-gray-100 text-gray-800';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,10 +250,12 @@
|
|||||||
return 'bg-purple-100 text-purple-800';
|
return 'bg-purple-100 text-purple-800';
|
||||||
} else if (typeLower === 'uninstall') {
|
} else if (typeLower === 'uninstall') {
|
||||||
return 'bg-red-100 text-red-800';
|
return 'bg-red-100 text-red-800';
|
||||||
} else if (typeLower === 'enable' || typeLower === 'disable') {
|
} else if (typeLower === 'enable') {
|
||||||
return 'bg-yellow-100 text-yellow-800';
|
|
||||||
} else if (typeLower === 'config_save') {
|
|
||||||
return 'bg-green-100 text-green-800';
|
return 'bg-green-100 text-green-800';
|
||||||
|
} else if (typeLower === 'disable') {
|
||||||
|
return 'bg-yellow-100 text-yellow-800';
|
||||||
|
} else if (typeLower === 'configure') {
|
||||||
|
return 'bg-teal-100 text-teal-800';
|
||||||
}
|
}
|
||||||
return 'bg-gray-100 text-gray-800';
|
return 'bg-gray-100 text-gray-800';
|
||||||
}
|
}
|
||||||
@@ -297,10 +315,20 @@
|
|||||||
|
|
||||||
const clearBtn = document.getElementById('clear-history-btn');
|
const clearBtn = document.getElementById('clear-history-btn');
|
||||||
if (clearBtn) {
|
if (clearBtn) {
|
||||||
clearBtn.addEventListener('click', () => {
|
clearBtn.addEventListener('click', async () => {
|
||||||
if (confirm('Are you sure you want to clear the operation history? This cannot be undone.')) {
|
if (confirm('Are you sure you want to clear the operation history? This cannot be undone.')) {
|
||||||
// TODO: Implement clear history API endpoint
|
try {
|
||||||
showError('Clear history not yet implemented');
|
const response = await fetch('/api/v3/plugins/operation/history', { method: 'DELETE' });
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.status === 'success') {
|
||||||
|
allHistory = [];
|
||||||
|
applyFilters();
|
||||||
|
} else {
|
||||||
|
showError(data.message || 'Failed to clear history');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError('Error clearing history');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -352,13 +380,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
setupEventListeners();
|
|
||||||
populatePluginFilter();
|
|
||||||
loadHistory();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Also initialize if script runs after DOM is ready
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
@@ -372,4 +393,3 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user