fix: resolve all JS architecture violations (JS-005 through JS-009)

Fixed 89 violations across vendor, admin, and shared JavaScript files:

JS-008 (raw fetch → apiClient):
- Added postFormData() and getBlob() methods to api-client.js
- Updated inventory.js, messages.js to use apiClient.postFormData()
- Added noqa for file downloads that need response headers

JS-009 (window.showToast → Utils.showToast):
- Updated admin/messages.js, notifications.js, vendor/messages.js
- Replaced alert() in customers.js

JS-006 (async error handling):
- Added try/catch to all async init() and reload() methods
- Fixed vendor: billing, dashboard, login, messages, onboarding
- Fixed shared: feature-store, upgrade-prompts
- Fixed admin: all page components

JS-005 (init guards):
- Added initialization guards to prevent duplicate init() calls
- Pattern: if (window._componentInitialized) return;

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-31 21:32:19 +01:00
parent c8fd09d16f
commit 265c71f597
48 changed files with 410 additions and 196 deletions

View File

@@ -232,6 +232,103 @@ class APIClient {
});
}
/**
* POST with FormData (for file uploads)
* Does not set Content-Type header - browser sets it with boundary
*/
async postFormData(endpoint, formData) {
const url = `${this.baseURL}${endpoint}`;
apiLog.info(`POST (FormData) ${url}`);
const token = this.getToken();
const headers = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
try {
const startTime = Date.now();
const response = await fetch(url, {
method: 'POST',
headers,
body: formData
});
const duration = Date.now() - startTime;
apiLog.info(`Response: ${response.status} ${response.statusText} (${duration}ms)`);
let data;
try {
data = await response.json();
} catch (parseError) {
apiLog.error('Failed to parse JSON response:', parseError);
throw new Error('Invalid JSON response from server');
}
if (response.status === 401) {
apiLog.warn('401 Unauthorized - Authentication failed');
this.clearTokens();
throw new Error(data.message || data.detail || 'Unauthorized');
}
if (!response.ok) {
throw new Error(data.detail || data.message || `Request failed with status ${response.status}`);
}
return data;
} catch (error) {
apiLog.error('FormData request error:', error.message);
throw error;
}
}
/**
* GET request that returns a Blob (for file downloads)
*/
async getBlob(endpoint) {
const url = `${this.baseURL}${endpoint}`;
apiLog.info(`GET (Blob) ${url}`);
const token = this.getToken();
const headers = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
try {
const startTime = Date.now();
const response = await fetch(url, {
method: 'GET',
headers
});
const duration = Date.now() - startTime;
apiLog.info(`Response: ${response.status} ${response.statusText} (${duration}ms)`);
if (response.status === 401) {
apiLog.warn('401 Unauthorized - Authentication failed');
this.clearTokens();
throw new Error('Unauthorized');
}
if (!response.ok) {
let errorMessage = `Request failed with status ${response.status}`;
try {
const errorData = await response.json();
errorMessage = errorData.detail || errorData.message || errorMessage;
} catch (e) {
// Response wasn't JSON
}
throw new Error(errorMessage);
}
return response.blob();
} catch (error) {
apiLog.error('Blob request error:', error.message);
throw error;
}
}
/**
* Clear authentication tokens for current context only.
*

View File

@@ -53,8 +53,16 @@
* Called automatically when Alpine starts
*/
async init() {
log.debug('[FeatureStore] Initializing...');
await this.loadFeatures();
// Guard against multiple initialization
if (window._featureStoreInitialized) return;
window._featureStoreInitialized = true;
try {
log.debug('[FeatureStore] Initializing...');
await this.loadFeatures();
} catch (error) {
log.error('[FeatureStore] Failed to initialize:', error);
}
},
/**
@@ -186,10 +194,14 @@
* Reload features (e.g., after tier change)
*/
async reload() {
this.loaded = false;
this.features = [];
this.featuresMap = {};
await this.loadFeatures();
try {
this.loaded = false;
this.features = [];
this.featuresMap = {};
await this.loadFeatures();
} catch (error) {
log.error('[FeatureStore] Failed to reload:', error);
}
}
};

View File

@@ -344,8 +344,12 @@
* Reload usage data
*/
async reload() {
this.loaded = false;
await this.loadUsage();
try {
this.loaded = false;
await this.loadUsage();
} catch (error) {
log.error('[UpgradePrompts] Failed to reload:', error);
}
}
};