hm-o2e-tool/index.html
Vadym Samoilenko 37d402d563 Fix MSAL CDN path — use lib/ instead of dist/
UMD browser bundle is at lib/msal-browser.min.js, not dist/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 14:13:54 +00:00

2485 lines
No EOL
107 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OMG Static - H&M</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #dc3545 0%, #b02a37 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
font-size: 1.1em;
}
.form-section {
padding: 30px;
}
.dropdown-container {
margin-bottom: 30px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #333;
font-size: 1.1em;
}
select {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 16px;
background-color: white;
transition: border-color 0.3s ease;
}
select:focus {
outline: none;
border-color: #dc3545;
}
.variables-container {
margin: 30px 0;
min-height: 100px;
}
.variable-group {
margin-bottom: 20px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #dc3545;
}
.variable-group label {
margin-bottom: 5px;
font-size: 1em;
color: #555;
}
.variable-group input, .variable-group select {
margin-bottom: 10px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.variable-group input:focus, .variable-group select:focus {
border-color: #dc3545;
}
.submit-container {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.submit-btn {
background: linear-gradient(135deg, #dc3545 0%, #b02a37 100%);
color: white;
border: none;
padding: 15px 40px;
font-size: 18px;
border-radius: 6px;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(220, 53, 69, 0.4);
}
.submit-btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.loading {
display: none;
margin-top: 20px;
text-align: center;
color: #dc3545;
}
.result {
margin-top: 20px;
padding: 15px;
border-radius: 4px;
display: none;
white-space: pre-wrap;
font-family: monospace;
font-size: 12px;
max-height: 300px;
overflow-y: auto;
}
.result.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.result.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.debug-log {
margin-top: 10px;
padding: 10px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
font-family: monospace;
font-size: 11px;
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
display: none;
}
.toggle-debug {
margin-top: 10px;
padding: 5px 10px;
background: #6c757d;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
}
.toggle-debug:hover {
background: #545b62;
}
.ratio-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.ratio-table th,
.ratio-table td {
padding: 8px 12px;
text-align: left;
border: 1px solid #ddd;
}
.ratio-table th {
background-color: #f8f9fa;
font-weight: bold;
color: #333;
}
.ratio-table tr:nth-child(even) {
background-color: #f9f9f9;
}
.ratio-table input {
width: 80px;
padding: 4px 8px;
border: 1px solid #ccc;
border-radius: 3px;
text-align: center;
}
.ratio-table input:focus {
border-color: #dc3545;
outline: none;
}
.ratio-table-container {
margin-top: 15px;
max-height: 400px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
}
@media (max-width: 600px) {
.header h1 {
font-size: 2em;
}
.form-section {
padding: 20px;
}
}
#authOverlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.auth-box {
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 40px;
text-align: center;
max-width: 400px;
width: 100%;
}
.auth-box h2 {
margin-bottom: 15px;
color: #333;
}
.auth-box p {
color: #666;
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
margin-top: 10px;
justify-content: center;
}
.user-info span {
font-size: 0.9em;
opacity: 0.9;
}
.user-info button {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid rgba(255, 255, 255, 0.5);
padding: 6px 14px;
border-radius: 4px;
cursor: pointer;
font-size: 0.85em;
}
.user-info button:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>
<script src="https://cdn.jsdelivr.net/npm/@azure/msal-browser@3/lib/msal-browser.min.js"></script>
</head>
<body>
<div id="authOverlay">
<div class="auth-box">
<h2>OMG Static &ndash; H&amp;M</h2>
<p id="authStatus">Loading...</p>
</div>
</div>
<div class="container" style="display:none">
<div class="header">
<h1>OMG Static - H&M <small style="opacity: 0.7; font-size: 0.5em;">v1.01</small></h1>
<p>Select an action and configure the required parameters</p>
<div class="user-info" id="userInfo" style="display:none">
<span id="userName"></span>
<button id="logoutBtn">Sign out</button>
</div>
</div>
<div class="form-section">
<div class="dropdown-container">
<label for="actionSelect">Select Action:</label>
<select id="actionSelect">
<option value="">-- Select an action --</option>
<option value="relink">Relink Images Search and update missing images</option>
<option value="image-status">Image Status Check for missing images</option>
<option value="export-indd">Export INDD Export indd files from OMG Static</option>
<option value="export-pdf">Export PDF Exports and renames R100 files for OMG</option>
<option value="ratio-check">Ratio Check Export a indd for the minimum and maximum output</option>
</select>
</div>
<div class="variables-container" id="variablesContainer">
<p style="text-align: center; color: #999; font-style: italic;">Please select an action to see the required variables</p>
</div>
<div class="submit-container">
<button class="submit-btn" id="submitBtn" disabled>Submit</button>
<div class="loading" id="loading">Processing request...</div>
<div class="result" id="result"></div>
<button class="toggle-debug" id="toggleDebug" style="display: none;">Show Debug Log</button>
<div class="debug-log" id="debugLog"></div>
</div>
</div>
</div>
<script>
const actionSelect = document.getElementById('actionSelect');
const variablesContainer = document.getElementById('variablesContainer');
const submitBtn = document.getElementById('submitBtn');
const loading = document.getElementById('loading');
const result = document.getElementById('result');
const toggleDebug = document.getElementById('toggleDebug');
const debugLog = document.getElementById('debugLog');
let debugMessages = [];
let appConfig = null;
let msalInstance = null;
let currentAccount = null;
// Load configuration on page load
async function loadConfig() {
try {
const response = await fetch('config.php', {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Config load failed: ${response.status} ${response.statusText}`);
}
appConfig = await response.json();
logDebug('Configuration loaded successfully');
} catch (error) {
console.error('Failed to load configuration:', error);
showResult('Error: Failed to load application configuration. Please check server setup.', 'error');
submitBtn.disabled = true;
}
}
async function initAuth(config) {
const msalConfig = {
auth: {
clientId: config.azure.clientId,
authority: `https://login.microsoftonline.com/${config.azure.tenantId}`,
redirectUri: config.azure.redirectUri
},
cache: { cacheLocation: 'sessionStorage', storeAuthStateInCookie: false }
};
msalInstance = new msal.PublicClientApplication(msalConfig);
await msalInstance.initialize();
const response = await msalInstance.handleRedirectPromise();
if (response) {
currentAccount = response.account;
} else {
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) currentAccount = accounts[0];
}
if (!currentAccount) {
document.getElementById('authStatus').textContent = 'Redirecting to Microsoft login...';
await msalInstance.loginRedirect({ scopes: ['openid', 'profile', 'email'] });
return false;
}
return true;
}
function showApp(account) {
document.getElementById('authOverlay').style.display = 'none';
document.querySelector('.container').style.display = 'block';
document.getElementById('userName').textContent = account.name || account.username;
document.getElementById('userInfo').style.display = 'flex';
document.getElementById('logoutBtn').addEventListener('click', () => {
msalInstance.logoutRedirect({ account: currentAccount });
});
}
// Initialize application
document.addEventListener('DOMContentLoaded', async function() {
await loadConfig();
const authenticated = await initAuth(appConfig);
if (!authenticated) return;
showApp(currentAccount);
initializeApp();
});
// Helper function to build API URLs with config
function buildApiUrl(command, params = {}) {
if (!appConfig) {
throw new Error('Configuration not loaded');
}
const baseParams = {
command: command,
authDomain: appConfig.api.authDomain,
authUsername: encodeURIComponent(appConfig.api.authUsername),
authPassword: encodeURIComponent(appConfig.api.authPassword),
clientId: appConfig.api.clientId
};
const allParams = { ...baseParams, ...params };
const paramString = Object.entries(allParams)
.map(([key, value]) => `${key}=${value}`)
.join('&');
return `${appConfig.api.baseUrl}?${paramString}`;
}
function logDebug(message, data = null) {
const timestamp = new Date().toLocaleTimeString();
const logEntry = `[${timestamp}] ${message}`;
debugMessages.push(logEntry);
if (data) {
debugMessages.push(JSON.stringify(data, null, 2));
}
debugLog.textContent = debugMessages.join('\n');
debugLog.scrollTop = debugLog.scrollHeight;
console.log(logEntry, data);
}
function clearDebugLog() {
debugMessages = [];
debugLog.textContent = '';
}
function initializeApp() {
toggleDebug.addEventListener('click', function() {
if (debugLog.style.display === 'none' || debugLog.style.display === '') {
debugLog.style.display = 'block';
toggleDebug.textContent = 'Hide Debug Log';
} else {
debugLog.style.display = 'none';
toggleDebug.textContent = 'Show Debug Log';
}
});
const actionVariables = {
'relink': [
{ name: 'linkMethod', label: 'Link Method', type: 'radio', required: true, options: ['Link by Folder', 'Link by File'] },
{ name: 'folderId', label: 'Folder ID', type: 'text', required: true, placeholder: 'Enter folder ID', conditional: 'Link by Folder' },
{ name: 'documentId', label: 'Document ID', type: 'text', required: true, placeholder: 'Enter document ID', conditional: 'Link by File' }
],
'image-status': [
{ name: 'folderId', label: 'Folder ID', type: 'text', required: true, placeholder: 'Enter folder ID' }
],
'export-indd': [
{ name: 'folderId', label: 'Folder ID', type: 'text', required: true, placeholder: 'Enter folder ID' }
],
'export-pdf': [
{ name: 'folderId', label: 'Folder ID', type: 'text', required: true, placeholder: 'Enter folder ID' }
],
'ratio-check': [
{ name: 'folderId', label: 'Folder ID', type: 'text', required: true, placeholder: 'Enter folder ID' },
{ name: 'ratioRules', label: 'Ratio Rules', type: 'ratio-table', required: false }
]
};
actionSelect.addEventListener('change', function() {
const selectedAction = this.value;
if (selectedAction && actionVariables[selectedAction]) {
generateVariableInputs(actionVariables[selectedAction]);
submitBtn.disabled = false;
} else {
variablesContainer.innerHTML = '<p style="text-align: center; color: #999; font-style: italic;">Please select an action to see the required variables</p>';
submitBtn.disabled = true;
}
hideResult();
});
function generateVariableInputs(variables) {
variablesContainer.innerHTML = '';
variables.forEach(variable => {
const variableGroup = document.createElement('div');
variableGroup.className = 'variable-group';
// Handle conditional fields - initially hide them
if (variable.conditional) {
variableGroup.style.display = 'none';
variableGroup.setAttribute('data-conditional', variable.conditional);
}
const label = document.createElement('label');
label.textContent = variable.label + (variable.required ? ' *' : '');
label.setAttribute('for', variable.name);
let input;
if (variable.type === 'select') {
input = document.createElement('select');
input.innerHTML = '<option value="">-- Select --</option>';
variable.options.forEach(option => {
const optionElement = document.createElement('option');
optionElement.value = option;
optionElement.textContent = option;
input.appendChild(optionElement);
});
} else if (variable.type === 'radio') {
// Create radio button group
const radioContainer = document.createElement('div');
radioContainer.style.marginTop = '10px';
radioContainer.style.display = 'flex';
radioContainer.style.gap = '20px';
variable.options.forEach(option => {
const radioWrapper = document.createElement('div');
radioWrapper.style.display = 'flex';
radioWrapper.style.alignItems = 'center';
const radioInput = document.createElement('input');
radioInput.type = 'radio';
radioInput.name = variable.name;
radioInput.value = option;
radioInput.id = `${variable.name}_${option.replace(/\\s+/g, '_')}`;
const radioLabel = document.createElement('label');
radioLabel.setAttribute('for', radioInput.id);
radioLabel.textContent = option;
radioLabel.style.marginLeft = '8px';
radioLabel.style.fontWeight = 'normal';
// Add event listener for conditional field showing
radioInput.addEventListener('change', function() {
if (this.checked) {
handleConditionalFields(variable.name, this.value);
}
});
radioWrapper.appendChild(radioInput);
radioWrapper.appendChild(radioLabel);
radioContainer.appendChild(radioWrapper);
});
input = radioContainer;
} else if (variable.type === 'ratio-table') {
input = createRatioTable();
} else {
input = document.createElement('input');
input.type = variable.type;
input.placeholder = variable.placeholder || '';
input.id = variable.name;
input.name = variable.name;
input.required = variable.required;
}
variableGroup.appendChild(label);
variableGroup.appendChild(input);
variablesContainer.appendChild(variableGroup);
});
}
function createRatioTable() {
const ratios = [
{ ratio: '020', minDefault: '015', maxDefault: '035' },
{ ratio: '050', minDefault: '036', maxDefault: '058' },
{ ratio: '067', minDefault: '059', maxDefault: '076' },
{ ratio: '100', minDefault: '077', maxDefault: '110' },
{ ratio: '120', minDefault: '111', maxDefault: '130' },
{ ratio: '140', minDefault: '131', maxDefault: '170' },
{ ratio: '200', minDefault: '171', maxDefault: '227' },
{ ratio: '254', minDefault: '228', maxDefault: '277' },
{ ratio: '300', minDefault: '278', maxDefault: '344' },
{ ratio: '388', minDefault: '345', maxDefault: '414' },
{ ratio: '440', minDefault: '415', maxDefault: '530' },
{ ratio: '600', minDefault: '531', maxDefault: '620' }
];
const container = document.createElement('div');
container.className = 'ratio-table-container';
const table = document.createElement('table');
table.className = 'ratio-table';
// Create header
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
['Ratio', 'Minimum Ratio', 'Maximum Ratio'].forEach(header => {
const th = document.createElement('th');
th.textContent = header;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// Create body
const tbody = document.createElement('tbody');
ratios.forEach(ratioData => {
const row = document.createElement('tr');
// Ratio column (read-only)
const ratioCell = document.createElement('td');
ratioCell.textContent = ratioData.ratio;
ratioCell.style.fontWeight = 'bold';
row.appendChild(ratioCell);
// Minimum ratio column (editable)
const minCell = document.createElement('td');
const minInput = document.createElement('input');
minInput.type = 'text';
minInput.value = ratioData.minDefault;
minInput.name = `ratio_${ratioData.ratio}_min`;
minInput.setAttribute('data-ratio', ratioData.ratio);
minInput.setAttribute('data-type', 'min');
minCell.appendChild(minInput);
row.appendChild(minCell);
// Maximum ratio column (editable)
const maxCell = document.createElement('td');
const maxInput = document.createElement('input');
maxInput.type = 'text';
maxInput.value = ratioData.maxDefault;
maxInput.name = `ratio_${ratioData.ratio}_max`;
maxInput.setAttribute('data-ratio', ratioData.ratio);
maxInput.setAttribute('data-type', 'max');
maxCell.appendChild(maxInput);
row.appendChild(maxCell);
tbody.appendChild(row);
});
table.appendChild(tbody);
container.appendChild(table);
return container;
}
function handleConditionalFields(radioName, selectedValue) {
// Hide all conditional fields for this radio group first
const conditionalFields = variablesContainer.querySelectorAll('[data-conditional]');
conditionalFields.forEach(field => {
field.style.display = 'none';
// Clear values in hidden fields
const inputs = field.querySelectorAll('input, select');
inputs.forEach(input => input.value = '');
});
// Show the field that matches the selected value
const targetField = variablesContainer.querySelector(`[data-conditional="${selectedValue}"]`);
if (targetField) {
targetField.style.display = 'block';
}
}
submitBtn.addEventListener('click', async function() {
const selectedAction = actionSelect.value;
if (!selectedAction) return;
const formData = collectFormData();
if (!validateFormData(formData)) {
showResult('Please fill in all required fields.', 'error');
return;
}
await submitData(selectedAction, formData);
});
} // end initializeApp
function collectFormData() {
const formData = {};
const inputs = variablesContainer.querySelectorAll('input, select');
inputs.forEach(input => {
if (input.name) {
if (input.type === 'radio') {
if (input.checked) {
formData[input.name] = input.value;
}
} else if (input.name.startsWith('ratio_')) {
// Handle ratio table inputs specially
const ratio = input.getAttribute('data-ratio');
const type = input.getAttribute('data-type');
if (!formData.ratioRules) {
formData.ratioRules = {};
}
if (!formData.ratioRules[ratio]) {
formData.ratioRules[ratio] = {};
}
formData.ratioRules[ratio][type] = input.value;
} else {
formData[input.name] = input.value;
}
}
});
return formData;
}
function validateFormData(formData) {
const selectedAction = actionSelect.value;
if (!selectedAction || !actionVariables[selectedAction]) return false;
const requiredFields = actionVariables[selectedAction].filter(v => v.required);
for (let field of requiredFields) {
// Skip validation for conditional fields that are not currently shown
if (field.conditional) {
const conditionalField = variablesContainer.querySelector(`[data-conditional="${field.conditional}"]`);
if (!conditionalField || conditionalField.style.display === 'none') {
continue; // Skip validation for hidden conditional fields
}
}
if (!formData[field.name] || formData[field.name].trim() === '') {
return false;
}
}
return true;
}
async function submitData(action, data) {
submitBtn.disabled = true;
loading.style.display = 'block';
hideResult();
try {
if (action === 'export-indd') {
await handleExportINDD(data);
} else if (action === 'export-pdf') {
await handleExportPDF(data);
} else if (action === 'relink') {
await handleRelink(data);
} else if (action === 'image-status') {
await handleImageStatus(data);
} else if (action === 'ratio-check') {
await handleRatioCheck(data);
} else {
const apiEndpoint = getApiEndpoint(action);
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: action,
parameters: data
})
});
if (response.ok) {
const result = await response.json();
showResult(`Request submitted successfully! Response: ${JSON.stringify(result, null, 2)}`, 'success');
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
}
} catch (error) {
console.error('API call failed:', error);
showResult(`Error: ${error.message}. Check console for details.`, 'error');
} finally {
submitBtn.disabled = false;
loading.style.display = 'none';
}
}
async function handleExportINDD(data) {
const { folderId } = data;
if (!appConfig) {
throw new Error('Configuration not loaded');
}
// Step 1: Get document list
const listUrl = buildApiUrl('document.list', {
folderId: folderId,
include: 'id%2Cname'
});
showResult('Step 1: Fetching document list...', 'success');
const listResponse = await fetch(listUrl);
if (!listResponse.ok) {
throw new Error(`Failed to fetch document list: ${listResponse.status} ${listResponse.statusText}`);
}
const listText = await listResponse.text();
let documents = [];
// Check if response is XML
if (listText.trim().startsWith('<?xml')) {
// Parse XML response
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(listText, 'text/xml');
// Check for API errors
const errorElement = xmlDoc.querySelector('error');
if (errorElement) {
throw new Error(`API Error: ${errorElement.textContent}`);
}
// Extract documents from XML
const documentElements = xmlDoc.querySelectorAll('document');
documents = Array.from(documentElements).map(doc => ({
id: doc.querySelector('id')?.textContent || '',
name: doc.querySelector('name')?.textContent || ''
}));
} else {
// Try to parse as JSON (fallback)
try {
const listData = JSON.parse(listText);
if (listData.data && Array.isArray(listData.data)) {
documents = listData.data;
} else {
throw new Error('Invalid JSON response format');
}
} catch (jsonError) {
throw new Error(`Unable to parse API response. Raw response: ${listText.substring(0, 200)}...`);
}
}
showResult(`Step 1 Complete: Found ${documents.length} documents. Starting exports...`, 'success');
// Step 2: Export each document
const results = [];
let successCount = 0;
let errorCount = 0;
for (let i = 0; i < documents.length; i++) {
const doc = documents[i];
const { id, name } = doc;
showResult(`Exporting ${i + 1}/${documents.length}: ${name}...`, 'success');
try {
const exportUrl = buildApiUrl('document.export.indd', {
id: id,
result: 'asset',
assetProjectId: appConfig.assets.inddProjectId,
assetFolderIdentifier: appConfig.assets.inddFolderIdentifier,
assetName: encodeURIComponent(name),
assetOverwrite: '1'
});
const exportResponse = await fetch(exportUrl);
if (exportResponse.ok) {
const exportText = await exportResponse.text();
let exportResult;
// Handle XML or JSON response
if (exportText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(exportText, 'text/xml');
// Check for errors in XML
const errorElement = xmlDoc.querySelector('error');
if (errorElement) {
results.push({ id, name, status: 'error', error: `API Error: ${errorElement.textContent}` });
errorCount++;
continue;
}
// Extract success info from XML
const successElement = xmlDoc.querySelector('success');
exportResult = {
success: successElement ? successElement.textContent : 'Export completed',
format: 'xml'
};
} else {
try {
exportResult = JSON.parse(exportText);
} catch (jsonError) {
exportResult = { message: 'Export completed', rawResponse: exportText.substring(0, 100) };
}
}
results.push({ id, name, status: 'success', result: exportResult });
successCount++;
} else {
results.push({ id, name, status: 'error', error: `${exportResponse.status} ${exportResponse.statusText}` });
errorCount++;
}
} catch (error) {
results.push({ id, name, status: 'error', error: error.message });
errorCount++;
}
// Add a small delay between requests to avoid overwhelming the server
if (i < documents.length - 1) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
// Final result summary
const resultMessage = `Export Complete!
Total Documents: ${documents.length}
Successful: ${successCount}
Failed: ${errorCount}`;
if (errorCount === 0) {
showResult(resultMessage, 'success');
} else {
showResult(resultMessage, errorCount === documents.length ? 'error' : 'success');
}
}
async function handleExportPDF(data) {
const { folderId } = data;
if (!appConfig) {
throw new Error('Configuration not loaded');
}
// Step 1: Get document list
const listUrl = buildApiUrl('document.list', {
folderId: folderId,
include: 'id%2Cname'
});
showResult('Step 1: Fetching document list...', 'success');
const listResponse = await fetch(listUrl);
if (!listResponse.ok) {
throw new Error(`Failed to fetch document list: ${listResponse.status} ${listResponse.statusText}`);
}
const listText = await listResponse.text();
let documents = [];
// Check if response is XML
if (listText.trim().startsWith('<?xml')) {
// Parse XML response
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(listText, 'text/xml');
// Check for API errors
const errorElement = xmlDoc.querySelector('error');
if (errorElement) {
throw new Error(`API Error: ${errorElement.textContent}`);
}
// Extract documents from XML
const documentElements = xmlDoc.querySelectorAll('document');
documents = Array.from(documentElements).map(doc => ({
id: doc.querySelector('id')?.textContent || '',
name: doc.querySelector('name')?.textContent || ''
}));
} else {
// Try to parse as JSON (fallback)
try {
const listData = JSON.parse(listText);
if (listData.data && Array.isArray(listData.data)) {
documents = listData.data;
} else {
throw new Error('Invalid JSON response format');
}
} catch (jsonError) {
throw new Error(`Unable to parse API response. Raw response: ${listText.substring(0, 200)}...`);
}
}
// Filter documents that contain R100 in the name
const r100Documents = documents.filter(doc => doc.name.includes('R100'));
showResult(`Step 1 Complete: Found ${documents.length} total documents, ${r100Documents.length} contain R100. Starting PDF exports...`, 'success');
if (r100Documents.length === 0) {
showResult('No documents containing R100 found in this folder.', 'success');
return;
}
// Step 2: Export each R100 document as PDF
const results = [];
let successCount = 0;
let errorCount = 0;
for (let i = 0; i < r100Documents.length; i++) {
const doc = r100Documents[i];
const { id, name } = doc;
// Replace R100 with RATIOS in the asset name
const assetName = name.replace('R100', 'RATIOS');
showResult(`Exporting PDF ${i + 1}/${r100Documents.length}: ${assetName}...`, 'success');
try {
const exportUrl = buildApiUrl('document.export.pdf', {
id: id,
presetId: appConfig.assets.pdfPresetId,
optionAddRegMarks: '1',
result: 'asset',
assetProjectId: appConfig.assets.pdfProjectId,
assetFolderIdentifier: appConfig.assets.pdfFolderIdentifier,
assetName: encodeURIComponent(assetName),
assetOverwrite: '1'
});
const exportResponse = await fetch(exportUrl);
if (exportResponse.ok) {
const exportText = await exportResponse.text();
let exportResult;
// Handle XML or JSON response
if (exportText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(exportText, 'text/xml');
// Check for errors in XML
const errorElement = xmlDoc.querySelector('error');
if (errorElement) {
results.push({ id, name, assetName, status: 'error', error: `API Error: ${errorElement.textContent}` });
errorCount++;
continue;
}
// Extract success info from XML
const successElement = xmlDoc.querySelector('success');
exportResult = {
success: successElement ? successElement.textContent : 'PDF Export completed',
format: 'xml'
};
} else {
try {
exportResult = JSON.parse(exportText);
} catch (jsonError) {
exportResult = { message: 'PDF Export completed', rawResponse: exportText.substring(0, 100) };
}
}
results.push({ id, name, assetName, status: 'success', result: exportResult });
successCount++;
} else {
results.push({ id, name, assetName, status: 'error', error: `${exportResponse.status} ${exportResponse.statusText}` });
errorCount++;
}
} catch (error) {
results.push({ id, name, assetName, status: 'error', error: error.message });
errorCount++;
}
// Add a small delay between requests to avoid overwhelming the server
if (i < r100Documents.length - 1) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
// Final result summary
const resultMessage = `PDF Export Complete!
R100 Documents Found: ${r100Documents.length}
Successful: ${successCount}
Failed: ${errorCount}`;
if (errorCount === 0) {
showResult(resultMessage, 'success');
} else {
showResult(resultMessage, errorCount === r100Documents.length ? 'error' : 'success');
}
}
async function handleRelink(data) {
const { linkMethod, folderId, documentId } = data;
if (!appConfig) {
throw new Error('Configuration not loaded');
}
if (linkMethod === 'Link by Folder') {
await handleLinkByFolder(folderId);
}
if (linkMethod === 'Link by File') {
await handleLinkByFile(documentId);
}
}
async function handleImageStatus(data) {
const { folderId } = data;
if (!appConfig) {
throw new Error('Configuration not loaded');
}
clearDebugLog();
toggleDebug.style.display = 'block';
showResult('Step 1: Getting document list from folder...', 'success');
logDebug(`Starting Image Status check for folder ID: ${folderId}`);
// Step 1: Get document list from folder
const documentListUrl = buildApiUrl('document.list', {
folderId: folderId,
include: 'id'
});
logDebug('API Call: document.list', { url: documentListUrl.replace(encodeURIComponent(appConfig.api.authPassword), '***') });
const documentListResponse = await fetch(documentListUrl);
if (!documentListResponse.ok) {
logDebug(`Document list API failed: ${documentListResponse.status} ${documentListResponse.statusText}`);
throw new Error(`Failed to fetch document list: ${documentListResponse.status} ${documentListResponse.statusText}`);
}
const documentListText = await documentListResponse.text();
let documents = [];
logDebug('Document list response received', { responseLength: documentListText.length, startsWithXML: documentListText.trim().startsWith('<?xml') });
if (documentListText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(documentListText, 'text/xml');
const errorElement = xmlDoc.querySelector('error');
if (errorElement) {
logDebug(`Document list API returned error: ${errorElement.textContent}`);
throw new Error(`API Error: ${errorElement.textContent}`);
}
const documentElements = xmlDoc.querySelectorAll('document');
documents = Array.from(documentElements).map(doc => ({
id: doc.querySelector('id')?.textContent || ''
}));
logDebug(`Found ${documents.length} documents in folder`, {
documentIds: documents.map(doc => doc.id)
});
} else {
logDebug('Non-XML response received', { response: documentListText.substring(0, 500) });
throw new Error('Invalid response format from document list API');
}
if (documents.length === 0) {
showResult('No documents found in the specified folder.', 'success');
return;
}
showResult(`Found ${documents.length} documents. Checking image status for each document...`, 'success');
const documentStatusResults = [];
// Process each document to get image status
for (let i = 0; i < documents.length; i++) {
const document = documents[i];
const documentId = document.id;
showResult(`Checking document ${i + 1}/${documents.length} (ID: ${documentId})...`, 'success');
logDebug(`Checking image status for document ${i + 1}/${documents.length}`, { documentId });
try {
// Get image list with linkState for this document
const imageListUrl = buildApiUrl('document.image.list', {
id: documentId,
include: 'linkState'
});
logDebug('API Call: document.image.list', { url: imageListUrl.replace(encodeURIComponent(appConfig.api.authPassword), '***'), documentId });
const imageListResponse = await fetch(imageListUrl);
if (!imageListResponse.ok) {
logDebug(`Image list API failed for document ${documentId}: ${imageListResponse.status} ${imageListResponse.statusText}`);
throw new Error(`Failed to fetch image list for document ${documentId}: ${imageListResponse.status} ${imageListResponse.statusText}`);
}
const imageListText = await imageListResponse.text();
let images = [];
logDebug(`Image list response received for document ${documentId}`, {
responseLength: imageListText.length,
startsWithXML: imageListText.trim().startsWith('<?xml')
});
if (imageListText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(imageListText, 'text/xml');
const errorElement = xmlDoc.querySelector('error');
if (errorElement) {
logDebug(`Image list API returned error for document ${documentId}: ${errorElement.textContent}`);
throw new Error(`Image List API Error for document ${documentId}: ${errorElement.textContent}`);
}
const imageElements = xmlDoc.querySelectorAll('image');
images = Array.from(imageElements).map(img => ({
linkState: img.querySelector('linkState')?.textContent || 'Unknown'
}));
logDebug(`Parsed ${images.length} images from document ${documentId}`, {
linkStates: images.map(img => img.linkState)
});
} else {
logDebug(`Non-XML response received for document ${documentId}`, { response: imageListText.substring(0, 500) });
throw new Error(`Invalid response format from image list API for document ${documentId}`);
}
// Count images by link state
const linkedImages = images.filter(img => img.linkState === 'NORMAL').length;
const missingImages = images.filter(img => img.linkState === 'MISSING').length;
const totalImages = images.length;
documentStatusResults.push({
documentId: documentId,
status: 'success',
totalImages: totalImages,
linkedImages: linkedImages,
missingImages: missingImages,
otherImages: totalImages - linkedImages - missingImages
});
logDebug(`Document ${documentId} image status completed`, {
totalImages,
linkedImages,
missingImages
});
} catch (error) {
logDebug(`Error checking image status for document ${documentId}: ${error.message}`, { error: error.stack });
documentStatusResults.push({
documentId: documentId,
status: 'error',
error: error.message
});
}
// Small delay between requests to avoid overwhelming the server
if (i < documents.length - 1) {
await new Promise(resolve => setTimeout(resolve, appConfig.performance.statusCheckDelay));
}
}
// Calculate totals
const successfulChecks = documentStatusResults.filter(doc => doc.status === 'success');
const totalLinkedImages = successfulChecks.reduce((sum, doc) => sum + doc.linkedImages, 0);
const totalMissingImages = successfulChecks.reduce((sum, doc) => sum + doc.missingImages, 0);
const totalAllImages = successfulChecks.reduce((sum, doc) => sum + doc.totalImages, 0);
const errorCount = documentStatusResults.filter(doc => doc.status === 'error').length;
// Final summary
const resultMessage = `Image Status Check Complete!
Total Documents Checked: ${documents.length}
Successful Checks: ${successfulChecks.length}
Failed Checks: ${errorCount}
Overall Totals:
- Total Images: ${totalAllImages}
- Linked Images (Normal): ${totalLinkedImages}
- Missing Images: ${totalMissingImages}
Document Details:
${documentStatusResults.map(doc =>
doc.status === 'success'
? `Document ${doc.documentId}: ${doc.linkedImages} linked, ${doc.missingImages} missing (${doc.totalImages} total)`
: `Document ${doc.documentId}: ERROR - ${doc.error}`
).join('\n')}`;
showResult(resultMessage, 'success');
}
async function handleLinkByFile(documentId) {
clearDebugLog();
toggleDebug.style.display = 'block';
showResult('Step 1: Opening document session...', 'success');
logDebug(`Starting relink process for document ID: ${documentId}`);
// Step 1: Open document session
const sessionOpenUrl = buildApiUrl('document.session.open', { documentId: documentId });
logDebug('API Call: document.session.open', { url: sessionOpenUrl.replace(encodeURIComponent(appConfig.api.authPassword), '***') });
const sessionOpenResponse = await fetch(sessionOpenUrl);
if (!sessionOpenResponse.ok) {
logDebug(`Session open API failed: ${sessionOpenResponse.status} ${sessionOpenResponse.statusText}`);
throw new Error(`Failed to open document session: ${sessionOpenResponse.status} ${sessionOpenResponse.statusText}`);
}
const sessionOpenText = await sessionOpenResponse.text();
let editSessionId = '';
logDebug('Session open response received', { responseLength: sessionOpenText.length, startsWithXML: sessionOpenText.trim().startsWith('<?xml') });
if (sessionOpenText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(sessionOpenText, 'text/xml');
const errorElement = xmlDoc.querySelector('error');
if (errorElement) {
logDebug(`Session open API returned error: ${errorElement.textContent}`);
throw new Error(`Session Open API Error: ${errorElement.textContent}`);
}
// Extract editSession ID
editSessionId = xmlDoc.querySelector('editSession')?.textContent || '';
if (!editSessionId) {
logDebug('No editSession ID found in response', { response: sessionOpenText });
throw new Error('No editSession ID received from session open API');
}
logDebug('Document session opened successfully', { editSessionId });
} else {
logDebug('Non-XML response received from session open', { response: sessionOpenText.substring(0, 500) });
throw new Error('Invalid response format from session open API');
}
let successCount = 0;
let errorCount = 0;
let images = [];
let missingImages = [];
const results = [];
try {
showResult('Step 2: Getting image list from document...', 'success');
// Step 2: Get document image list
const imageListUrl = buildApiUrl('document.image.list', { id: documentId });
logDebug('API Call: document.image.list', { url: imageListUrl.replace(encodeURIComponent(appConfig.api.authPassword), '***') });
const imageListResponse = await fetch(imageListUrl);
if (!imageListResponse.ok) {
logDebug(`Image list API failed: ${imageListResponse.status} ${imageListResponse.statusText}`);
throw new Error(`Failed to fetch image list: ${imageListResponse.status} ${imageListResponse.statusText}`);
}
const imageListText = await imageListResponse.text();
logDebug('Image list response received', { responseLength: imageListText.length, startsWithXML: imageListText.trim().startsWith('<?xml') });
if (imageListText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(imageListText, 'text/xml');
const errorElement = xmlDoc.querySelector('error');
if (errorElement) {
logDebug(`Image list API returned error: ${errorElement.textContent}`);
throw new Error(`API Error: ${errorElement.textContent}`);
}
const imageElements = xmlDoc.querySelectorAll('image');
images = Array.from(imageElements).map(img => {
// Extract filename from absolutePath since name tag contains user info
const absolutePath = img.querySelector('absolutePath')?.textContent || '';
const decodedAbsolutePath = decodeURIComponent(absolutePath);
const pathParts = decodedAbsolutePath.split('/');
const actualImageName = pathParts[pathParts.length - 1];
return {
id: img.querySelector('id')?.textContent || '',
name: actualImageName,
absolutePath: absolutePath,
linkState: img.querySelector('linkState')?.textContent || ''
};
});
logDebug(`Parsed ${images.length} images from document`, {
images: images.map(img => ({
id: img.id,
name: img.name,
linkState: img.linkState,
absolutePath: img.absolutePath.substring(0, 50) + '...'
}))
});
} else {
logDebug('Non-XML response received', { response: imageListText.substring(0, 500) });
}
showResult(`Found ${images.length} images in document. Checking link states...`, 'success');
// Only process MISSING images
missingImages = images.filter(img => img.linkState === 'MISSING');
logDebug('Image link state analysis', {
totalImages: images.length,
normalImages: images.filter(img => img.linkState === 'Normal').length,
missingImages: missingImages.length,
linkStates: [...new Set(images.map(img => img.linkState))]
});
if (missingImages.length > 0) {
showResult(`Found ${missingImages.length} missing images. Starting relink process...`, 'success');
logDebug('Starting relink process for missing images', {
missingImages: missingImages.map(img => ({
id: img.id,
name: img.name,
linkState: img.linkState
}))
});
// Process each missing image
for (let i = 0; i < missingImages.length; i++) {
const image = missingImages[i];
showResult(`Processing ${i + 1}/${missingImages.length}: ${image.name} (${image.linkState})...`, 'success');
logDebug(`Processing image ${i + 1}/${missingImages.length}`, {
id: image.id,
name: image.name,
linkState: image.linkState,
absolutePath: image.absolutePath
});
try {
logDebug('Processing image for relink', {
name: image.name,
absolutePath: image.absolutePath,
linkState: image.linkState
});
if (image.name === 'H&M-logo_sRGB.ai') {
logDebug('Detected H&M sRGB logo - using configured asset identifier', {
imageId: image.id,
imageName: image.name
});
const relinkSuccess = await relinkSpecificImage(documentId, image.id, appConfig.assets.hmSRGBLogoAsset, editSessionId);
results.push({
name: image.name,
imageId: image.id,
status: relinkSuccess ? 'success' : 'failed',
method: 'H&M sRGB logo'
});
if (relinkSuccess) {
successCount++;
} else {
errorCount++;
}
} else if (image.name === 'H&M-logo_CMYK_coated.eps') {
logDebug('Detected H&M CMYK logo - using configured asset identifier', {
imageId: image.id,
imageName: image.name
});
const relinkSuccess = await relinkSpecificImage(documentId, image.id, appConfig.assets.hmCMYKLogoAsset, editSessionId);
results.push({
name: image.name,
imageId: image.id,
status: relinkSuccess ? 'success' : 'failed',
method: 'H&M CMYK logo'
});
if (relinkSuccess) {
successCount++;
} else {
errorCount++;
}
} else {
logDebug('Processing generic image through asset lookup process');
const result = await relinkGenericImage(documentId, image, editSessionId);
results.push(result);
if (result.status === 'success') {
successCount++;
} else {
errorCount++;
}
}
} catch (error) {
logDebug(`Error processing image: ${error.message}`, { error: error.stack });
results.push({ name: image.name, status: 'error', error: error.message });
errorCount++;
}
// Configurable delay between requests to avoid timeouts
if (i < missingImages.length - 1) {
await new Promise(resolve => setTimeout(resolve, appConfig.performance.relinkDelay));
}
}
} else {
logDebug('No missing images found');
}
} finally {
// Always save and close the document session
showResult('Saving and closing document session...', 'success');
logDebug('Saving and closing document session', { editSessionId });
try {
// Save session
const sessionSaveUrl = buildApiUrl('document.session.save', {
documentId: documentId,
editSession: editSessionId
});
logDebug('API Call: document.session.save', { url: sessionSaveUrl.replace(encodeURIComponent(appConfig.api.authPassword), '***') });
const sessionSaveResponse = await fetch(sessionSaveUrl);
if (!sessionSaveResponse.ok) {
logDebug(`Session save API failed: ${sessionSaveResponse.status} ${sessionSaveResponse.statusText}`);
} else {
logDebug('Session save response received');
}
// Close session
const sessionCloseUrl = buildApiUrl('document.session.close', {
documentId: documentId,
editSession: editSessionId
});
logDebug('API Call: document.session.close', { url: sessionCloseUrl.replace(encodeURIComponent(appConfig.api.authPassword), '***') });
const sessionCloseResponse = await fetch(sessionCloseUrl);
if (!sessionCloseResponse.ok) {
logDebug(`Session close API failed: ${sessionCloseResponse.status} ${sessionCloseResponse.statusText}`);
} else {
logDebug('Document session saved and closed successfully');
}
} catch (sessionError) {
logDebug(`Session management error: ${sessionError.message}`);
}
}
// Final summary
const resultMessage = `Relink Complete!
Total Images: ${images.length}
Missing Images: ${missingImages.length}
Successfully Relinked: ${successCount}
Failed: ${errorCount}`;
showResult(resultMessage, errorCount > 0 ? 'error' : 'success');
}
async function handleLinkByFolder(folderId) {
clearDebugLog();
toggleDebug.style.display = 'block';
showResult('Step 1: Getting document list from folder...', 'success');
logDebug(`Starting Link by Folder process for folder ID: ${folderId}`);
// Step 1: Get document list from folder
const documentListUrl = buildApiUrl('document.list', {
folderId: folderId,
include: 'id'
});
logDebug('API Call: document.list', { url: documentListUrl.replace(encodeURIComponent(appConfig.api.authPassword), '***') });
const documentListResponse = await fetch(documentListUrl);
if (!documentListResponse.ok) {
logDebug(`Document list API failed: ${documentListResponse.status} ${documentListResponse.statusText}`);
throw new Error(`Failed to fetch document list: ${documentListResponse.status} ${documentListResponse.statusText}`);
}
const documentListText = await documentListResponse.text();
let documents = [];
logDebug('Document list response received', { responseLength: documentListText.length, startsWithXML: documentListText.trim().startsWith('<?xml') });
if (documentListText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(documentListText, 'text/xml');
const errorElement = xmlDoc.querySelector('error');
if (errorElement) {
logDebug(`Document list API returned error: ${errorElement.textContent}`);
throw new Error(`API Error: ${errorElement.textContent}`);
}
const documentElements = xmlDoc.querySelectorAll('document');
documents = Array.from(documentElements).map(doc => ({
id: doc.querySelector('id')?.textContent || ''
}));
logDebug(`Found ${documents.length} documents in folder`, {
documentIds: documents.map(doc => doc.id)
});
} else {
logDebug('Non-XML response received', { response: documentListText.substring(0, 500) });
throw new Error('Invalid response format from document list API');
}
if (documents.length === 0) {
showResult('No documents found in the specified folder.', 'success');
return;
}
showResult(`Found ${documents.length} documents. Starting relink process for each document...`, 'success');
let totalSuccessCount = 0;
let totalErrorCount = 0;
let processedDocuments = 0;
const documentResults = [];
// Process each document
for (let i = 0; i < documents.length; i++) {
const document = documents[i];
const documentId = document.id;
showResult(`Processing document ${i + 1}/${documents.length} (ID: ${documentId})...`, 'success');
logDebug(`Processing document ${i + 1}/${documents.length}`, { documentId });
try {
// Use the existing handleLinkByFile function for each document
const result = await processDocumentRelink(documentId);
documentResults.push({
documentId: documentId,
status: 'completed',
successCount: result.successCount,
errorCount: result.errorCount,
totalImages: result.totalImages,
missingImages: result.missingImages
});
totalSuccessCount += result.successCount;
totalErrorCount += result.errorCount;
processedDocuments++;
logDebug(`Document ${documentId} completed`, {
successCount: result.successCount,
errorCount: result.errorCount,
missingImages: result.missingImages
});
} catch (error) {
logDebug(`Error processing document ${documentId}: ${error.message}`, { error: error.stack });
documentResults.push({
documentId: documentId,
status: 'error',
error: error.message
});
totalErrorCount++;
}
// Small delay between documents to avoid overwhelming the server
if (i < documents.length - 1) {
showResult(`Completed document ${documentId}. Waiting before processing next document...`, 'success');
await new Promise(resolve => setTimeout(resolve, appConfig.performance.documentDelay));
}
}
// Final summary
const resultMessage = `Link by Folder Complete!
Total Documents: ${documents.length}
Successfully Processed: ${processedDocuments}
Total Images Relinked: ${totalSuccessCount}
Total Failures: ${totalErrorCount}
Document Details:
${documentResults.map(doc =>
doc.status === 'completed'
? `Document ${doc.documentId}: ${doc.successCount} relinked, ${doc.errorCount} failed (${doc.missingImages} missing images)`
: `Document ${doc.documentId}: ERROR - ${doc.error}`
).join('\n')}`;
if (totalErrorCount === 0) {
showResult(resultMessage, 'success');
} else {
showResult(resultMessage, 'success');
}
}
async function handleRatioCheck(data) {
const { folderId, ratioRules } = data;
if (!appConfig) {
throw new Error('Configuration not loaded');
}
clearDebugLog();
toggleDebug.style.display = 'block';
showResult('Step 1: Getting document list from folder...', 'success');
logDebug(`Starting Ratio Check for folder ID: ${folderId}`);
logDebug('Ratio Rules:', ratioRules);
// Step 1: Get document list from folder with dimensions
const documentListUrl = buildApiUrl('document.list', {
folderId: folderId,
include: 'id%2Cname%2Cwidth%2Cheight'
});
logDebug('API Call: document.list', { url: documentListUrl.replace(encodeURIComponent(appConfig.api.authPassword), '***') });
const documentListResponse = await fetch(documentListUrl);
if (!documentListResponse.ok) {
logDebug(`Document list API failed: ${documentListResponse.status} ${documentListResponse.statusText}`);
throw new Error(`Failed to fetch document list: ${documentListResponse.status} ${documentListResponse.statusText}`);
}
const documentListText = await documentListResponse.text();
let documents = [];
logDebug('Document list response received', { responseLength: documentListText.length, startsWithXML: documentListText.trim().startsWith('<?xml') });
if (documentListText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(documentListText, 'text/xml');
const errorElement = xmlDoc.querySelector('error');
if (errorElement) {
logDebug(`Document list API returned error: ${errorElement.textContent}`);
throw new Error(`API Error: ${errorElement.textContent}`);
}
const documentElements = xmlDoc.querySelectorAll('document');
documents = Array.from(documentElements).map(doc => ({
id: doc.querySelector('id')?.textContent || '',
name: doc.querySelector('name')?.textContent || '',
width: parseInt(doc.querySelector('width')?.textContent || '0'),
height: parseInt(doc.querySelector('height')?.textContent || '0')
}));
logDebug(`Found ${documents.length} documents in folder`, {
documentIds: documents.map(doc => ({ id: doc.id, name: doc.name, dimensions: `${doc.width}x${doc.height}` }))
});
} else {
logDebug('Non-XML response received', { response: documentListText.substring(0, 500) });
throw new Error('Invalid response format from document list API');
}
if (documents.length === 0) {
showResult('No documents found in the specified folder.', 'success');
return;
}
// Step 2: Find documents for each ratio
const ratioDocuments = {};
const ratioKeys = Object.keys(ratioRules);
ratioKeys.forEach(ratioKey => {
const targetPattern = `R${ratioKey}.indd`;
const matchingDocs = documents.filter(doc => doc.name.endsWith(targetPattern));
if (matchingDocs.length > 0) {
ratioDocuments[ratioKey] = matchingDocs[0]; // Take first match
logDebug(`Found document for ratio ${ratioKey}:`, matchingDocs[0]);
} else {
logDebug(`No document found for ratio ${ratioKey} (looking for pattern: ${targetPattern})`);
}
});
const foundRatios = Object.keys(ratioDocuments);
showResult(`Found ${foundRatios.length} matching documents. Starting ratio processing...`, 'success');
if (foundRatios.length === 0) {
showResult('No documents found matching the ratio patterns (R020.indd, R050.indd, etc.)', 'error');
return;
}
// Step 3: Process each ratio document
let totalSuccessCount = 0;
let totalErrorCount = 0;
const processingResults = [];
for (let i = 0; i < foundRatios.length; i++) {
const ratioKey = foundRatios[i];
const document = ratioDocuments[ratioKey];
const ratioConfig = ratioRules[ratioKey];
showResult(`Processing ratio ${ratioKey} (${i + 1}/${foundRatios.length}): ${document.name}...`, 'success');
logDebug(`Processing ratio ${ratioKey}`, { document, ratioConfig });
try {
const result = await processRatioDocument(document, ratioKey, ratioConfig);
processingResults.push({
ratio: ratioKey,
documentName: document.name,
status: 'completed',
minSuccess: result.minSuccess,
maxSuccess: result.maxSuccess,
errors: result.errors
});
totalSuccessCount += (result.minSuccess ? 1 : 0) + (result.maxSuccess ? 1 : 0);
totalErrorCount += result.errors.length;
} catch (error) {
logDebug(`Error processing ratio ${ratioKey}: ${error.message}`, { error: error.stack });
processingResults.push({
ratio: ratioKey,
documentName: document.name,
status: 'error',
error: error.message
});
totalErrorCount++;
}
// Delay between ratios
if (i < foundRatios.length - 1) {
showResult(`Completed ratio ${ratioKey}. Waiting before processing next ratio...`, 'success');
await new Promise(resolve => setTimeout(resolve, appConfig.performance.documentDelay));
}
}
// Final summary
const resultMessage = `Ratio Check Complete!
Ratios Processed: ${foundRatios.length}
Total Exports: ${totalSuccessCount}
Total Failures: ${totalErrorCount}
Processing Details:
${processingResults.map(result => {
if (result.status === 'completed') {
const minStatus = result.minSuccess ? '✓' : '✗';
const maxStatus = result.maxSuccess ? '✓' : '✗';
const errorText = result.errors.length > 0 ? ` (${result.errors.length} errors)` : '';
return `Ratio ${result.ratio}: Min ${minStatus} Max ${maxStatus}${errorText}`;
} else {
return `Ratio ${result.ratio}: ERROR - ${result.error}`;
}
}).join('\n')}`;
if (totalErrorCount === 0) {
showResult(resultMessage, 'success');
} else {
showResult(resultMessage, 'success');
}
}
async function processRatioDocument(document, ratioKey, ratioConfig) {
const { id: documentId, name: documentName, height: originalHeight } = document;
const { min: minRatio, max: maxRatio } = ratioConfig;
logDebug(`Processing document ${documentId} for ratio ${ratioKey}`, {
documentName,
originalHeight,
minRatio,
maxRatio
});
// Step 1: Open document session
const sessionOpenUrl = buildApiUrl('document.session.open', { documentId: documentId });
logDebug('API Call: document.session.open', { url: sessionOpenUrl.replace(encodeURIComponent(appConfig.api.authPassword), '***') });
const sessionOpenResponse = await fetch(sessionOpenUrl);
if (!sessionOpenResponse.ok) {
throw new Error(`Failed to open document session: ${sessionOpenResponse.status} ${sessionOpenResponse.statusText}`);
}
const sessionOpenText = await sessionOpenResponse.text();
let editSessionId = '';
if (sessionOpenText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(sessionOpenText, 'text/xml');
const errorElement = xmlDoc.querySelector('error');
if (errorElement) {
throw new Error(`Session Open API Error: ${errorElement.textContent}`);
}
editSessionId = xmlDoc.querySelector('editSession')?.textContent || '';
if (!editSessionId) {
throw new Error('No editSession ID received from session open API');
}
logDebug('Document session opened successfully', { editSessionId });
} else {
throw new Error('Invalid response format from session open API');
}
const results = { minSuccess: false, maxSuccess: false, errors: [] };
try {
// Process minimum ratio
logDebug(`Processing minimum ratio: ${minRatio}`);
try {
await processRatioExport(documentId, editSessionId, documentName, originalHeight, ratioKey, minRatio, 'min');
results.minSuccess = true;
logDebug('Minimum ratio export successful');
} catch (error) {
results.errors.push(`Min ratio error: ${error.message}`);
logDebug('Minimum ratio export failed', { error: error.message });
}
// Process maximum ratio
logDebug(`Processing maximum ratio: ${maxRatio}`);
try {
await processRatioExport(documentId, editSessionId, documentName, originalHeight, ratioKey, maxRatio, 'max');
results.maxSuccess = true;
logDebug('Maximum ratio export successful');
} catch (error) {
results.errors.push(`Max ratio error: ${error.message}`);
logDebug('Maximum ratio export failed', { error: error.message });
}
} finally {
// Always close the session WITHOUT saving so the master reverts to original state
try {
const sessionCloseUrl = buildApiUrl('document.session.close', {
documentId: documentId,
editSession: editSessionId
});
logDebug('API Call: document.session.close (no save - preserving master)', { url: sessionCloseUrl.replace(encodeURIComponent(appConfig.api.authPassword), '***') });
const sessionCloseResponse = await fetch(sessionCloseUrl);
if (!sessionCloseResponse.ok) {
logDebug(`Session close API failed: ${sessionCloseResponse.status} ${sessionCloseResponse.statusText}`);
} else {
logDebug(`Document session ${editSessionId} closed successfully (no save - master preserved)`);
}
} catch (closeError) {
logDebug(`Error closing session: ${closeError.message}`);
}
}
return results;
}
async function processRatioExport(documentId, editSessionId, documentName, originalHeight, ratioKey, ratioValue, ratioType) {
// Calculate new width based on ratio
const ratioDecimal = parseInt(ratioValue) / 100;
const newWidth = Math.round(originalHeight * ratioDecimal * 100) / 100; // Round to 2 decimal places
logDebug(`Calculated dimensions for ratio ${ratioValue}:`, {
originalHeight,
ratioDecimal,
newWidth
});
// Step 1: Resize document
const resizeUrl = buildApiUrl('document.resize', {
id: documentId,
editSession: editSessionId,
width: newWidth.toString(),
height: originalHeight.toString()
});
logDebug('API Call: document.resize', {
url: resizeUrl.replace(encodeURIComponent(appConfig.api.authPassword), '***'),
width: newWidth,
height: originalHeight
});
const resizeResponse = await fetch(resizeUrl);
if (!resizeResponse.ok) {
throw new Error(`Failed to resize document: ${resizeResponse.status} ${resizeResponse.statusText}`);
}
const resizeText = await resizeResponse.text();
logDebug('Resize response received', { responseLength: resizeText.length });
// Validate resize response
if (resizeText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(resizeText, 'text/xml');
const errorElement = xmlDoc.querySelector('error');
if (errorElement) {
throw new Error(`Resize API Error: ${errorElement.textContent}`);
}
}
// Step 2: Export document
const baseName = documentName.replace('.indd', '');
const exportAssetName = `${baseName}-${ratioValue}.indd`;
const exportUrl = buildApiUrl('document.export.indd', {
editSession: editSessionId,
id: documentId,
result: 'asset',
assetProjectId: appConfig.assets.inddProjectId,
assetFolderIdentifier: appConfig.assets.ratioCheckFolderIdentifier,
assetName: encodeURIComponent(exportAssetName),
assetOverwrite: '1'
});
logDebug('API Call: document.export.indd', {
url: exportUrl.replace(encodeURIComponent(appConfig.api.authPassword), '***'),
assetName: exportAssetName
});
const exportResponse = await fetch(exportUrl);
if (!exportResponse.ok) {
throw new Error(`Failed to export document: ${exportResponse.status} ${exportResponse.statusText}`);
}
const exportText = await exportResponse.text();
logDebug('Export response received', { responseLength: exportText.length });
// Validate export response
if (exportText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(exportText, 'text/xml');
const errorElement = xmlDoc.querySelector('error');
if (errorElement) {
throw new Error(`Export API Error: ${errorElement.textContent}`);
}
const successElement = xmlDoc.querySelector('success');
if (!successElement) {
throw new Error('No success response received from export API');
}
logDebug(`Successfully exported ${exportAssetName}`);
} else {
logDebug('Non-XML export response', { response: exportText.substring(0, 200) });
}
}
// Helper function that extracts the core relink logic without the UI setup
async function processDocumentRelink(documentId) {
logDebug(`Starting relink process for document ID: ${documentId}`);
// Step 1: Open document session
const sessionOpenUrl = buildApiUrl('document.session.open', { documentId: documentId });
const sessionOpenResponse = await fetch(sessionOpenUrl);
if (!sessionOpenResponse.ok) {
throw new Error(`Failed to open document session: ${sessionOpenResponse.status} ${sessionOpenResponse.statusText}`);
}
const sessionOpenText = await sessionOpenResponse.text();
let editSessionId = '';
if (sessionOpenText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(sessionOpenText, 'text/xml');
const errorElement = xmlDoc.querySelector('error');
if (errorElement) {
throw new Error(`Session Open API Error: ${errorElement.textContent}`);
}
editSessionId = xmlDoc.querySelector('editSession')?.textContent || '';
if (!editSessionId) {
throw new Error('No editSession ID received from session open API');
}
} else {
throw new Error('Invalid response format from session open API');
}
let successCount = 0;
let errorCount = 0;
let images = [];
let missingImages = [];
try {
// Step 2: Get document image list
const imageListUrl = buildApiUrl('document.image.list', { id: documentId });
const imageListResponse = await fetch(imageListUrl);
if (!imageListResponse.ok) {
throw new Error(`Failed to fetch image list: ${imageListResponse.status} ${imageListResponse.statusText}`);
}
const imageListText = await imageListResponse.text();
if (imageListText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(imageListText, 'text/xml');
const errorElement = xmlDoc.querySelector('error');
if (errorElement) {
throw new Error(`API Error: ${errorElement.textContent}`);
}
const imageElements = xmlDoc.querySelectorAll('image');
images = Array.from(imageElements).map(img => {
const absolutePath = img.querySelector('absolutePath')?.textContent || '';
const decodedAbsolutePath = decodeURIComponent(absolutePath);
const pathParts = decodedAbsolutePath.split('/');
const actualImageName = pathParts[pathParts.length - 1];
return {
id: img.querySelector('id')?.textContent || '',
name: actualImageName,
absolutePath: absolutePath,
linkState: img.querySelector('linkState')?.textContent || ''
};
});
}
// Only process MISSING images
missingImages = images.filter(img => img.linkState === 'MISSING');
if (missingImages.length > 0) {
// Process each missing image
for (let i = 0; i < missingImages.length; i++) {
const image = missingImages[i];
try {
if (image.name === 'H&M-logo_sRGB.ai') {
const relinkSuccess = await relinkSpecificImage(documentId, image.id, appConfig.assets.hmSRGBLogoAsset, editSessionId);
if (relinkSuccess) {
successCount++;
} else {
errorCount++;
}
} else if (image.name === 'H&M-logo_CMYK_coated.eps') {
const relinkSuccess = await relinkSpecificImage(documentId, image.id, appConfig.assets.hmCMYKLogoAsset, editSessionId);
if (relinkSuccess) {
successCount++;
} else {
errorCount++;
}
} else {
const result = await relinkGenericImage(documentId, image, editSessionId);
if (result.status === 'success') {
successCount++;
} else {
errorCount++;
}
}
} catch (error) {
errorCount++;
}
// Configurable delay between requests to avoid timeouts
if (i < missingImages.length - 1) {
await new Promise(resolve => setTimeout(resolve, appConfig.performance.relinkDelay));
}
}
}
} finally {
// Always save and close the document session
await closeDocumentSession(editSessionId, documentId);
}
return {
successCount,
errorCount,
totalImages: images.length,
missingImages: missingImages.length
};
}
// Helper function to save and close document session
async function closeDocumentSession(editSessionId, documentId) {
try {
// Save session
const sessionSaveUrl = buildApiUrl('document.session.save', {
documentId: documentId,
editSession: editSessionId
});
const sessionSaveResponse = await fetch(sessionSaveUrl);
if (!sessionSaveResponse.ok) {
throw new Error(`Failed to save document session: ${sessionSaveResponse.status} ${sessionSaveResponse.statusText}`);
}
// Close session
const sessionCloseUrl = buildApiUrl('document.session.close', {
documentId: documentId,
editSession: editSessionId
});
const sessionCloseResponse = await fetch(sessionCloseUrl);
if (!sessionCloseResponse.ok) {
throw new Error(`Failed to close document session: ${sessionCloseResponse.status} ${sessionCloseResponse.statusText}`);
}
logDebug(`Document session ${editSessionId} saved and closed successfully for document ${documentId}`);
} catch (sessionError) {
logDebug(`Session management error for document ${documentId}: ${sessionError.message}`);
throw sessionError; // Re-throw to handle at document level
}
}
async function relinkSpecificImage(documentId, frameId, assetIdentifier, editSessionId) {
const relinkUrl = buildApiUrl('document.image.relink', {
id: documentId,
frameId: frameId,
assetProjectId: appConfig.assets.inddProjectId,
assetIdentifier: assetIdentifier,
editSession: editSessionId
});
logDebug('Relink API call', {
documentId,
frameId,
assetIdentifier,
url: relinkUrl.replace(encodeURIComponent(appConfig.api.authPassword), '***')
});
const response = await fetch(relinkUrl);
if (!response.ok) {
logDebug(`Relink API HTTP error: ${response.status} ${response.statusText}`);
throw new Error(`Failed to relink image: ${response.status} ${response.statusText}`);
}
const responseText = await response.text();
logDebug('Relink API response received', {
documentId,
frameId,
assetIdentifier,
responseLength: responseText.length,
startsWithXML: responseText.trim().startsWith('<?xml'),
response: responseText.substring(0, 200) + '...'
});
if (responseText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(responseText, 'text/xml');
const errorElement = xmlDoc.querySelector('error');
if (errorElement) {
logDebug(`Relink API returned error: ${errorElement.textContent}`, {
documentId,
frameId,
assetIdentifier
});
throw new Error(`Relink API Error: ${errorElement.textContent}`);
}
const successElement = xmlDoc.querySelector('success');
if (!successElement) {
logDebug('Relink API did not return success element', {
documentId,
frameId,
assetIdentifier,
fullResponse: responseText
});
throw new Error('No success response received from relink API');
}
logDebug('Relink successful - success element found', {
documentId,
frameId,
assetIdentifier
});
return true; // Successfully relinked
}
logDebug('Invalid response format from relink API', { response: responseText.substring(0, 500) });
throw new Error('Invalid response format from relink API');
}
async function relinkGenericImage(documentId, image, editSessionId) {
try {
logDebug(`Processing generic image: ${image.name}`, {
name: image.name,
absolutePath: image.absolutePath
});
// Step 1: Get document info to find folder ID
const docInfoUrl = buildApiUrl('document.info', {
id: documentId,
include: 'folderId'
});
const docInfoResponse = await fetch(docInfoUrl);
const docInfoText = await docInfoResponse.text();
let folderId = '';
if (docInfoText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(docInfoText, 'text/xml');
folderId = xmlDoc.querySelector('folderId')?.textContent || '';
}
if (!folderId) {
return { name: image.name, status: 'error', error: 'Could not get folder ID' };
}
// Step 2: Get folder info to find folder name
const folderInfoUrl = buildApiUrl('document.folder.info', {
id: folderId,
include: 'name'
});
const folderInfoResponse = await fetch(folderInfoUrl);
const folderInfoText = await folderInfoResponse.text();
let folderName = '';
if (folderInfoText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(folderInfoText, 'text/xml');
folderName = xmlDoc.querySelector('name')?.textContent || '';
}
if (!folderName) {
return { name: image.name, status: 'error', error: 'Could not get folder name' };
}
// Step 3: Find matching campaign folder in assets
const assetListUrl = buildApiUrl('asset.list', {
projectId: appConfig.assets.inddProjectId,
include: 'name%2Cidentifier'
});
const assetListResponse = await fetch(assetListUrl);
const assetListText = await assetListResponse.text();
let campaignFolder = null;
if (assetListText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(assetListText, 'text/xml');
const assetElements = xmlDoc.querySelectorAll('asset');
for (let asset of assetElements) {
const name = asset.querySelector('name')?.textContent || '';
if (name === folderName) {
campaignFolder = {
name: name,
identifier: asset.querySelector('identifier')?.textContent || ''
};
break;
}
}
}
if (!campaignFolder) {
return { name: image.name, status: 'error', error: `No matching campaign folder found for: ${folderName}` };
}
// Step 4: Look for Links folder within campaign
const campaignAssetsUrl = buildApiUrl('asset.list', {
projectId: appConfig.assets.inddProjectId,
folderIdentifier: campaignFolder.identifier,
include: 'name%2Cidentifier'
});
const campaignAssetsResponse = await fetch(campaignAssetsUrl);
const campaignAssetsText = await campaignAssetsResponse.text();
let linksFolder = null;
if (campaignAssetsText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(campaignAssetsText, 'text/xml');
const assetElements = xmlDoc.querySelectorAll('asset');
for (let asset of assetElements) {
const name = asset.querySelector('name')?.textContent || '';
if (name === 'Links') {
linksFolder = {
name: name,
identifier: asset.querySelector('identifier')?.textContent || ''
};
break;
}
}
}
if (!linksFolder) {
return { name: image.name, status: 'error', error: 'Links folder not found in campaign' };
}
// Step 5: Get RGB/CMYK folders
const linksAssetsUrl = buildApiUrl('asset.list', {
projectId: appConfig.assets.inddProjectId,
folderIdentifier: linksFolder.identifier,
include: 'name%2Cidentifier'
});
const linksAssetsResponse = await fetch(linksAssetsUrl);
const linksAssetsText = await linksAssetsResponse.text();
let targetFolder = null;
const useCMYK = image.name.endsWith('39L_TAC330.tif');
const targetFolderName = useCMYK ? 'CMYK' : 'RGB';
if (linksAssetsText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(linksAssetsText, 'text/xml');
const assetElements = xmlDoc.querySelectorAll('asset');
for (let asset of assetElements) {
const name = asset.querySelector('name')?.textContent || '';
if (name === targetFolderName) {
targetFolder = {
name: name,
identifier: asset.querySelector('identifier')?.textContent || ''
};
break;
}
}
}
if (!targetFolder) {
return { name: image.name, status: 'error', error: `${targetFolderName} folder not found` };
}
// Step 6: Find matching image in target folder
const targetAssetsUrl = buildApiUrl('asset.list', {
projectId: appConfig.assets.inddProjectId,
folderIdentifier: targetFolder.identifier,
include: 'name%2Cidentifier'
});
const targetAssetsResponse = await fetch(targetAssetsUrl);
const targetAssetsText = await targetAssetsResponse.text();
let matchingAsset = null;
if (targetAssetsText.trim().startsWith('<?xml')) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(targetAssetsText, 'text/xml');
const assetElements = xmlDoc.querySelectorAll('asset');
for (let asset of assetElements) {
const assetName = asset.querySelector('name')?.textContent || '';
// Primary matching using extracted filename from absolutePath
if (assetName === image.name ||
assetName.includes(image.name) ||
image.name.includes(assetName) ||
// Also check filename without extension
assetName.includes(image.name.split('.')[0]) ||
image.name.includes(assetName.split('.')[0])) {
matchingAsset = {
name: assetName,
identifier: asset.querySelector('identifier')?.textContent || ''
};
logDebug(`Found matching asset: ${assetName} for image: ${image.name}`);
break;
}
}
}
if (!matchingAsset) {
return { name: image.name, status: 'error', error: `No matching asset found in ${targetFolderName} folder` };
}
// Step 7: Relink the image
const relinkSuccess = await relinkSpecificImage(documentId, image.id, matchingAsset.identifier, editSessionId);
return {
name: image.name,
status: relinkSuccess ? 'success' : 'failed',
method: `Found in ${targetFolderName} folder`,
matchedAsset: matchingAsset.name
};
} catch (error) {
return { name: image.name, status: 'error', error: error.message };
}
}
function getApiEndpoint(action) {
const endpoints = {
'relink': '/api/relink-images',
'export-indd': '/api/export-indd',
'export-pdf': '/api/export-pdf',
'ratio-check': '/api/ratio-check'
};
return endpoints[action] || '/api/generic';
}
function showResult(message, type) {
result.textContent = message;
result.className = `result ${type}`;
result.style.display = 'block';
}
function hideResult() {
result.style.display = 'none';
}
</script>
</body>
</html>