This commit is contained in:
Raven Scott
2025-11-24 15:17:04 -05:00
parent 074d8df3cc
commit 50857e4c02
14 changed files with 382 additions and 221 deletions
+80 -56
View File
@@ -4,6 +4,7 @@ import { startTerminal, appendTerminalOutput } from './libs/terminal.js';
import { startDockerTerminal, cleanUpDockerTerminal } from './libs/dockerTerminal.js';
import { fetchTemplates, displayTemplateList, openDeployModal } from './libs/templateDeploy.js';
import { showContainerSkeleton, createProgressBar, updateProgressBar, removeProgressBar, showOperationStatus } from './libs/loadingStates.js';
import { closeAllModals, showStatusIndicator, hideStatusIndicator, showAlert } from './libs/uiUtils.js';
// DOM Elements - Cache frequently accessed elements
const containerList = document.getElementById('container-list');
@@ -39,14 +40,7 @@ function stopStatsInterval() {
}
}
function closeAllModals() {
// Find and hide all open modals
const modals = document.querySelectorAll('.modal.show'); // Adjust selector if necessary
modals.forEach(modal => {
const modalInstance = bootstrap.Modal.getInstance(modal); // Get Bootstrap modal instance
modalInstance.hide(); // Close the modal
});
}
// Utility functions are now imported from uiUtils.js
document.addEventListener('DOMContentLoaded', () => {
@@ -346,52 +340,7 @@ function initContainerFiltering() {
// Initialize the app
console.log('[INFO] Client app initialized');
// Show Status Indicator
// Modify showStatusIndicator to recreate it dynamically
function showStatusIndicator(message = 'Processing...') {
const statusIndicator = document.createElement('div');
statusIndicator.id = 'status-indicator';
statusIndicator.className = 'position-fixed top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center bg-dark bg-opacity-75';
statusIndicator.innerHTML = `
<div class="text-center">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3 text-light">${message}</p>
</div>
`;
document.body.appendChild(statusIndicator);
}
function hideStatusIndicator() {
const statusIndicator = document.getElementById('status-indicator');
if (statusIndicator) {
console.log('[DEBUG] Hiding status indicator');
statusIndicator.remove();
} else {
console.error('[ERROR] Status indicator element not found!');
}
}
// Show Alert
// Show alert message
function showAlert(type, message) {
const alertBox = document.createElement('div');
alertBox.className = `alert alert-${type}`;
alertBox.textContent = message;
// Use cached DOM element
if (alertContainer) {
alertContainer.appendChild(alertBox);
setTimeout(() => {
if (alertContainer.contains(alertBox)) {
alertContainer.removeChild(alertBox);
}
}, 5000);
} else {
console.warn('[WARN] Alert container not found.');
}
}
// Utility functions are now imported from uiUtils.js
@@ -999,6 +948,67 @@ function debounce(func, wait) {
};
}
// Container filtering and sorting state
let containerFilterState = {
search: '',
status: 'all',
sort: 'name-asc',
allContainers: []
};
// Filter and sort containers
function filterAndSortContainers(containers) {
let filtered = [...containers];
// Apply search filter
if (containerFilterState.search) {
const searchLower = containerFilterState.search.toLowerCase();
filtered = filtered.filter(container => {
const name = container.Names[0]?.replace(/^\//, '') || '';
const image = container.Image || '';
return name.toLowerCase().includes(searchLower) ||
image.toLowerCase().includes(searchLower);
});
}
// Apply status filter
if (containerFilterState.status !== 'all') {
filtered = filtered.filter(container => {
const state = container.State?.toLowerCase() || '';
return state === containerFilterState.status.toLowerCase();
});
}
// Apply sorting
const [sortField, sortOrder] = containerFilterState.sort.split('-');
filtered.sort((a, b) => {
let aVal, bVal;
switch (sortField) {
case 'name':
aVal = (a.Names[0]?.replace(/^\//, '') || '').toLowerCase();
bVal = (b.Names[0]?.replace(/^\//, '') || '').toLowerCase();
break;
case 'cpu':
aVal = smoothedStats[a.Id]?.cpu || 0;
bVal = smoothedStats[b.Id]?.cpu || 0;
break;
case 'memory':
aVal = smoothedStats[a.Id]?.memory || 0;
bVal = smoothedStats[b.Id]?.memory || 0;
break;
default:
return 0;
}
if (aVal < bVal) return sortOrder === 'asc' ? -1 : 1;
if (aVal > bVal) return sortOrder === 'asc' ? 1 : -1;
return 0;
});
return filtered;
}
// Render the container list with optimized DOM manipulation
function renderContainers(containers, topicId) {
if (!window.activePeer || !connections[topicId] || window.activePeer !== connections[topicId].peer) {
@@ -1041,8 +1051,22 @@ function renderContainers(containers, topicId) {
<td>${name}</td>
<td>${image}</td>
<td>${container.State || 'Unknown'}</td>
<td class="cpu">0</td>
<td class="memory">0</td>
<td class="cpu">
<div class="stats-container">
<span class="stats-value">0.00%</span>
<div class="stats-bar-container">
<div class="stats-bar cpu-bar" style="width: 0%"></div>
</div>
</div>
</td>
<td class="memory">
<div class="stats-container">
<span class="stats-value">0.00 MB</span>
<div class="stats-bar-container">
<div class="stats-bar memory-bar" style="width: 0%"></div>
</div>
</div>
</td>
<td class="ip-address">${ipAddress}</td>
<td>
<div class="btn-group btn-group-sm">
+2
View File
@@ -84,3 +84,5 @@ export const CONFIG = {
},
};
+1 -1
View File
@@ -178,7 +178,7 @@ const BLOCKED_PATTERNS = [
/&&/, // Command chaining
/\|\|/, // OR operator
/;/, // Command separator
/>/.*</, // Redirection
/>.*</, // Redirection (e.g., >file or <file)
/2>&1/, // Stderr redirection
];
+2
View File
@@ -158,3 +158,5 @@ function getStatusColor(status) {
return colors[status] || 'secondary';
}
+181 -153
View File
@@ -1,13 +1,13 @@
// Import dependencies first (ES6 imports must be at top)
import { showOperationStatus } from './loadingStates.js';
import { closeAllModals, showStatusIndicator, hideStatusIndicator, showAlert } from './uiUtils.js';
// DOM Elements
const templateList = document.getElementById('template-list');
const templateSearchInput = document.getElementById('template-search-input');
const templateDeployModal = new bootstrap.Modal(document.getElementById('templateDeployModalUnique'));
const deployForm = document.getElementById('deploy-form');
let templates = [];
let localTemplates = [];
// Import template storage
import * as templateStorage from './templateStorage.js';
// Array item counters for unique IDs
let portCounter = 0;
@@ -24,96 +24,27 @@ let sysctlCounter = 0;
let ulimitCounter = 0;
let tmpfsCounter = 0;
function closeAllModals() {
const modals = document.querySelectorAll('.modal.show');
modals.forEach(modal => {
const modalInstance = bootstrap.Modal.getInstance(modal);
if (modalInstance) {
modalInstance.hide();
} else {
// If no instance exists, create one and hide it
const newInstance = new bootstrap.Modal(modal);
newInstance.hide();
}
});
// Also explicitly close the deploy modal if it exists
// Utility functions are now imported from uiUtils.js
// Also explicitly close the deploy modal if needed
function closeDeployModal() {
if (templateDeployModal) {
templateDeployModal.hide();
}
}
// Show status indicator
function showStatusIndicator(message = 'Processing...') {
const statusIndicator = document.createElement('div');
statusIndicator.id = 'status-indicator';
statusIndicator.className = 'position-fixed top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center bg-dark bg-opacity-75';
statusIndicator.innerHTML = `
<div class="text-center">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3 text-light">${message}</p>
</div>
`;
document.body.appendChild(statusIndicator);
}
// Hide status indicator
function hideStatusIndicator() {
const statusIndicator = document.getElementById('status-indicator');
if (statusIndicator) {
statusIndicator.remove();
}
}
// Show alert message
function showAlert(type, message) {
const alertBox = document.createElement('div');
alertBox.className = `alert alert-${type}`;
alertBox.textContent = message;
const container = document.querySelector('#alert-container');
if (container) {
container.appendChild(alertBox);
setTimeout(() => {
if (container.contains(alertBox)) {
container.removeChild(alertBox);
}
}, 5000);
}
}
// Fetch templates from the URL
async function fetchTemplates() {
try {
// Load local templates
localTemplates = Object.entries(templateStorage.loadTemplates()).map(([name, config]) => ({
title: name,
description: `Local template saved on ${new Date(config.savedAt).toLocaleDateString()}`,
logo: '',
image: config.image || '',
ports: config.ports || [],
volumes: config.volumes || [],
env: config.env || [],
...config
}));
// Fetch remote templates
const response = await fetch('https://raw.githubusercontent.com/Lissy93/portainer-templates/main/templates.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
templates = data.templates || [];
// Combine local and remote templates
displayTemplateList([...localTemplates, ...templates]);
templates = data.templates || []; // Update global templates
displayTemplateList(templates);
} catch (error) {
console.error('[ERROR] Failed to fetch templates:', error.message);
// Still show local templates even if remote fetch fails
displayTemplateList(localTemplates);
showAlert('warning', 'Failed to load remote templates. Showing local templates only.');
showAlert('danger', 'Failed to load templates.');
}
}
@@ -130,52 +61,19 @@ templateSearchInput.addEventListener('input', () => {
// Display templates in the list
function displayTemplateList(templates) {
templateList.innerHTML = '';
// Add section header for local templates
if (localTemplates.length > 0) {
const localHeader = document.createElement('li');
localHeader.className = 'list-group-item bg-secondary';
localHeader.innerHTML = '<strong>Local Templates</strong>';
templateList.appendChild(localHeader);
}
templates.forEach((template, index) => {
const isLocal = index < localTemplates.length;
templates.forEach(template => {
const listItem = document.createElement('li');
listItem.className = 'list-group-item d-flex justify-content-between align-items-center';
listItem.innerHTML = `
<div>
${template.logo ? `<img src="${template.logo}" alt="Logo" class="me-2" style="width: 24px; height: 24px;">` : ''}
<img src="${template.logo || ''}" alt="Logo" class="me-2" style="width: 24px; height: 24px;">
<span>${template.title}</span>
${isLocal ? '<span class="badge bg-info ms-2">Local</span>' : ''}
</div>
<div>
${isLocal ? `<button class="btn btn-danger btn-sm me-2 delete-template-btn" data-template="${template.title}">Delete</button>` : ''}
<button class="btn btn-primary btn-sm deploy-btn">Deploy</button>
</div>
<button class="btn btn-primary btn-sm deploy-btn">Deploy</button>
`;
listItem.querySelector('.deploy-btn').addEventListener('click', () => openDeployModal(template));
if (isLocal) {
listItem.querySelector('.delete-template-btn').addEventListener('click', (e) => {
e.stopPropagation();
if (confirm(`Delete template "${template.title}"?`)) {
templateStorage.deleteTemplate(template.title);
fetchTemplates(); // Refresh list
}
});
}
templateList.appendChild(listItem);
});
// Add section header for remote templates
if (localTemplates.length > 0 && templates.length > localTemplates.length) {
const remoteHeader = document.createElement('li');
remoteHeader.className = 'list-group-item bg-secondary mt-2';
remoteHeader.innerHTML = '<strong>Remote Templates</strong>';
templateList.appendChild(remoteHeader);
}
}
// Array management functions
@@ -734,19 +632,80 @@ async function deployDockerContainer(payload) {
console.log('[INFO] Sending deployment command to the server...');
return new Promise((resolve, reject) => {
window.handlePeerResponse = (response) => {
if (response.success && response.message.includes('deployed successfully')) {
console.log('[INFO] Deployment response received:', response.message);
resolve(response);
} else if (response.error) {
reject(new Error(response.error));
// Store the original handler to restore it later
const originalHandler = window.handlePeerResponse;
let timeoutId = null;
let isResolved = false;
// Create a deployment-specific handler
const deploymentHandler = (response) => {
// Only process responses related to deployment
if (isResolved) {
// If already resolved, pass to original handler if it exists
if (typeof originalHandler === 'function') {
originalHandler(response);
}
return;
}
// Check if this is a deployment response
const isDeploymentResponse =
(response.success && response.message && response.message.includes('deployed successfully')) ||
(response.error && (response.message || response.error).includes('deploy') || response.error.includes('Container'));
if (isDeploymentResponse) {
// Clear timeout
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
// Restore original handler
window.handlePeerResponse = originalHandler;
isResolved = true;
if (response.success && response.message && response.message.includes('deployed successfully')) {
console.log('[INFO] Deployment response received:', response.message);
resolve(response);
} else if (response.error) {
// Safely extract error message
const errorMessage = typeof response.error === 'string'
? response.error
: (response.error?.message || response.error?.toString() || 'Unknown deployment error');
reject(new Error(errorMessage));
} else {
// Unexpected response format
reject(new Error('Unexpected response format from server'));
}
} else {
// Not a deployment response, pass to original handler if it exists
if (typeof originalHandler === 'function') {
originalHandler(response);
}
}
};
sendCommand('deployContainer', payload);
// Set the deployment handler
window.handlePeerResponse = deploymentHandler;
setTimeout(() => {
reject(new Error('Deployment timed out. No response from server.'));
// Use window.sendCommand to avoid TDZ issues
if (typeof window.sendCommand === 'function') {
window.sendCommand('deployContainer', payload);
} else {
// Restore original handler before rejecting
window.handlePeerResponse = originalHandler;
reject(new Error('sendCommand is not available. Please ensure app.js is loaded.'));
return;
}
// Set timeout with cleanup
timeoutId = setTimeout(() => {
if (!isResolved) {
// Restore original handler
window.handlePeerResponse = originalHandler;
isResolved = true;
reject(new Error('Deployment timed out. No response from server.'));
}
}, 60000); // Increased timeout for complex deployments
});
}
@@ -755,50 +714,119 @@ async function deployDockerContainer(payload) {
deployForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = collectFormData();
let formData;
try {
formData = collectFormData();
} catch (collectError) {
console.error('[ERROR] Failed to collect form data:', collectError);
console.error('[ERROR] Collect error stack:', collectError.stack);
showAlert('danger', 'Failed to collect form data. Check console for details.');
return;
}
// Validate
const errors = validateFormData(formData);
let errors = [];
try {
errors = validateFormData(formData);
} catch (validateError) {
console.error('[ERROR] Failed to validate form data:', validateError);
console.error('[ERROR] Validate error stack:', validateError.stack);
showAlert('danger', 'Failed to validate form data. Check console for details.');
return;
}
if (errors.length > 0) {
showAlert('danger', errors.join(' '));
return;
}
// Ensure formData is initialized before use
if (!formData || typeof formData !== 'object') {
console.error('[ERROR] Invalid form data:', formData);
showAlert('danger', 'Invalid form data. Please check your input and try again.');
return;
}
try {
showStatusIndicator('Deploying container...');
showOperationStatus(`Deploying ${formData.containerName}`, 'processing');
const successResponse = await deployDockerContainer(formData);
hideStatusIndicator();
showOperationStatus(`Deploying ${formData.containerName}`, 'completed');
// Safely get container name with fallback
const containerName = (formData && formData.containerName) ? String(formData.containerName) : 'container';
// Close all modals including the deploy modal
closeAllModals();
if (templateDeployModal) {
templateDeployModal.hide();
showStatusIndicator('Deploying container...');
try {
if (typeof showOperationStatus === 'function') {
showOperationStatus(`Deploying ${containerName}`, 'processing');
}
} catch (e) {
console.warn('[WARN] Failed to show operation status:', e);
}
// Deploy container with proper error handling
const successResponse = await deployDockerContainer(formData);
// Ensure we have a valid response
if (!successResponse || typeof successResponse !== 'object') {
throw new Error('Invalid response from deployment function');
}
showAlert('success', successResponse.message || 'Container deployed successfully!');
} catch (error) {
console.error('[ERROR] Failed to deploy container:', error.message);
hideStatusIndicator();
showOperationStatus(`Deploying ${formData.containerName}`, 'failed');
showAlert('danger', error.message);
try {
if (typeof showOperationStatus === 'function') {
showOperationStatus(`Deploying ${containerName}`, 'completed');
}
} catch (e) {
console.warn('[WARN] Failed to show operation status:', e);
}
// Close all modals including the deploy modal
if (typeof closeAllModals === 'function') {
closeAllModals();
}
closeDeployModal();
// Safely extract success message
const successMessage = (successResponse && successResponse.message)
? String(successResponse.message)
: 'Container deployed successfully!';
showAlert('success', successMessage);
} catch (error) {
// Safely extract error message
const errorMessage = (error && error.message)
? String(error.message)
: 'Failed to deploy container. Check console for details.';
console.error('[ERROR] Failed to deploy container:', errorMessage);
if (error && error.stack) {
console.error('[ERROR] Full error stack:', error.stack);
}
if (error) {
console.error('[ERROR] Error details:', error);
}
// Ensure status indicator is hidden
try {
hideStatusIndicator();
} catch (e) {
console.warn('[WARN] Failed to hide status indicator:', e);
}
// Safely get container name for error status
try {
const containerName = (formData && formData.containerName)
? String(formData.containerName)
: 'container';
if (typeof showOperationStatus === 'function') {
showOperationStatus(`Deploying ${containerName}`, 'failed');
}
} catch (e) {
console.warn('[WARN] Failed to show operation status:', e);
}
showAlert('danger', errorMessage);
}
});
// Add save template functionality
window.saveCurrentTemplate = function() {
const formData = collectFormData();
const templateName = prompt('Enter a name for this template:');
if (templateName && templateName.trim()) {
if (templateStorage.saveTemplate(templateName.trim(), formData)) {
showAlert('success', `Template "${templateName}" saved successfully!`);
fetchTemplates(); // Refresh template list
} else {
showAlert('danger', 'Failed to save template.');
}
}
};
// Save template functionality removed to match working version
// Initialize templates on load
document.addEventListener('DOMContentLoaded', fetchTemplates);
+13 -6
View File
@@ -2,9 +2,16 @@
* Local template storage and management
*/
import { CONFIG } from '../config.js';
// Storage key for templates (removed CONFIG dependency to avoid TDZ errors)
const STORAGE_KEY = 'peardock_templates';
const STORAGE_KEY = CONFIG.STORAGE.TEMPLATES_KEY;
/**
* Get the storage key for templates
* @returns {string} - Storage key
*/
function getStorageKey() {
return STORAGE_KEY;
}
/**
* Save template to local storage
@@ -19,7 +26,7 @@ export function saveTemplate(name, template) {
savedAt: new Date().toISOString(),
version: templates[name]?.version ? templates[name].version + 1 : 1
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(templates));
localStorage.setItem(getStorageKey(), JSON.stringify(templates));
return true;
} catch (err) {
console.error('[ERROR] Failed to save template:', err);
@@ -33,7 +40,7 @@ export function saveTemplate(name, template) {
*/
export function loadTemplates() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
const stored = localStorage.getItem(getStorageKey());
return stored ? JSON.parse(stored) : {};
} catch (err) {
console.error('[ERROR] Failed to load templates:', err);
@@ -51,7 +58,7 @@ export function deleteTemplate(name) {
const templates = loadTemplates();
if (templates[name]) {
delete templates[name];
localStorage.setItem(STORAGE_KEY, JSON.stringify(templates));
localStorage.setItem(getStorageKey(), JSON.stringify(templates));
return true;
}
return false;
@@ -93,7 +100,7 @@ export function importTemplates(json) {
const existing = loadTemplates();
const merged = { ...existing, ...imported };
localStorage.setItem(STORAGE_KEY, JSON.stringify(merged));
localStorage.setItem(getStorageKey(), JSON.stringify(merged));
return true;
} catch (err) {
console.error('[ERROR] Failed to import templates:', err);
+80
View File
@@ -0,0 +1,80 @@
/**
* Shared UI utility functions
* Used by both app.js and templateDeploy.js to avoid code duplication
*/
/**
* Close all open Bootstrap modals
*/
export function closeAllModals() {
// Find and hide all open modals
const modals = document.querySelectorAll('.modal.show');
modals.forEach(modal => {
const modalInstance = bootstrap.Modal.getInstance(modal);
if (modalInstance) {
modalInstance.hide();
} else {
// If no instance exists, create one and hide it
const newInstance = new bootstrap.Modal(modal);
newInstance.hide();
}
});
}
/**
* Show status indicator overlay
* @param {string} message - Message to display
*/
export function showStatusIndicator(message = 'Processing...') {
const statusIndicator = document.createElement('div');
statusIndicator.id = 'status-indicator';
statusIndicator.className = 'position-fixed top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center bg-dark bg-opacity-75';
statusIndicator.innerHTML = `
<div class="text-center">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3 text-light">${message}</p>
</div>
`;
document.body.appendChild(statusIndicator);
}
/**
* Hide status indicator overlay
*/
export function hideStatusIndicator() {
const statusIndicator = document.getElementById('status-indicator');
if (statusIndicator) {
console.log('[DEBUG] Hiding status indicator');
statusIndicator.remove();
} else {
console.error('[ERROR] Status indicator element not found!');
}
}
/**
* Show alert message
* @param {string} type - Alert type (success, danger, warning, info)
* @param {string} message - Message to display
*/
export function showAlert(type, message) {
const alertBox = document.createElement('div');
alertBox.className = `alert alert-${type}`;
alertBox.textContent = message;
// Get alert container dynamically to avoid dependency on module-specific variables
const container = document.getElementById('alert-container') || document.querySelector('#alert-container');
if (container) {
container.appendChild(alertBox);
setTimeout(() => {
if (container.contains(alertBox)) {
container.removeChild(alertBox);
}
}, 5000);
} else {
console.warn('[WARN] Alert container not found.');
}
}
+1 -2
View File
@@ -10,7 +10,6 @@ import dotenv from 'dotenv';
import { spawn } from 'child_process';
import * as validation from './utils/validation.js';
import rateLimiter from './utils/rateLimiter.js';
import * as containerConfig from './utils/containerConfig.js';
import { createErrorResponse, sanitizeErrorMessage } from '../utils/errorHandler.js';
import logger from './utils/logger.js';
@@ -577,7 +576,7 @@ swarm.on('connection', (peer) => {
// Create the container
logger.info('Creating container', { name: args.containerName });
const container = await docker.createContainer(config);
const container = await docker.createContainer(containerConfig);
// Connect to custom network if specified
if (args.customNetwork && args.networkMode !== 'container' && args.networkMode !== 'host' && args.networkMode !== 'none') {
+2
View File
@@ -156,3 +156,5 @@ const logger = new Logger({
export default logger;
+2
View File
@@ -134,3 +134,5 @@ const rateLimiter = new RateLimiter();
export default rateLimiter;
+2
View File
@@ -234,3 +234,5 @@ export {
validateNumber
};
+2
View File
@@ -48,3 +48,5 @@ test('sanitizeErrorMessage - truncates long messages', (t) => {
t.ok(sanitized.endsWith('...'));
});
+2
View File
@@ -74,3 +74,5 @@ test('validateNumber - invalid numbers', (t) => {
t.equal(validation.validateNumber(null, 0, 10), null);
});
+12 -3
View File
@@ -2,7 +2,16 @@
* Centralized error handling utility
*/
import { CONFIG } from '../config.js';
// Removed CONFIG import to avoid TDZ errors - use constant directly
const UNKNOWN_ERROR_CODE = 'UNKNOWN_ERROR';
/**
* Get the unknown error code
* @returns {string} - Unknown error code
*/
function getUnknownErrorCode() {
return UNKNOWN_ERROR_CODE;
}
/**
* Error types
@@ -23,7 +32,7 @@ export class AppError extends Error {
super(message);
this.name = 'AppError';
this.type = type;
this.code = code || CONFIG.ERROR_CODES.UNKNOWN_ERROR;
this.code = code || getUnknownErrorCode();
this.details = details;
this.timestamp = new Date().toISOString();
}
@@ -69,7 +78,7 @@ export function createErrorResponse(error, includeDetails = false) {
return {
error: error.message || 'An unexpected error occurred',
code: error.code || CONFIG.ERROR_CODES.UNKNOWN_ERROR,
code: error.code || getUnknownErrorCode(),
type: errorType,
};
}