fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled

This commit is contained in:
2025-11-24 21:28:25 +01:00
parent 4eb7134853
commit 77abc65cd7
1327 changed files with 91915 additions and 9909 deletions

View File

@@ -0,0 +1,299 @@
/**
* Duplicate Management Module
*
* Handles duplicate asset management (merge, delete)
*/
export class DuplicateManagement {
constructor() {
this.init();
}
init() {
this.setupDeleteDuplicate();
this.setupDeleteGroup();
this.setupMergeGroup();
this.setupRefresh();
}
setupDeleteDuplicate() {
document.addEventListener('click', async (e) => {
const button = e.target.closest('[data-delete-duplicate]');
if (!button) return;
e.preventDefault();
const assetId = button.dataset.assetId;
if (!confirm(`Are you sure you want to delete this duplicate asset?`)) {
return;
}
await this.deleteAsset(assetId, button);
});
}
setupDeleteGroup() {
document.addEventListener('click', async (e) => {
const button = e.target.closest('[data-delete-group]');
if (!button) return;
e.preventDefault();
const sha256 = button.dataset.sha256;
const groupCard = button.closest('.duplicate-group');
const assetCount = groupCard?.querySelectorAll('.duplicate-asset-card').length || 0;
if (!confirm(`Are you sure you want to delete all ${assetCount} duplicate assets in this group?`)) {
return;
}
await this.deleteGroup(sha256, button);
});
}
setupMergeGroup() {
document.addEventListener('click', async (e) => {
const button = e.target.closest('[data-merge-group]');
if (!button) return;
e.preventDefault();
const sha256 = button.dataset.sha256;
const groupCard = button.closest('.duplicate-group');
const assets = Array.from(groupCard?.querySelectorAll('[data-asset-id]') || [])
.map(el => el.dataset.assetId);
if (assets.length < 2) {
if (window.Toast) {
window.Toast.warning('Need at least 2 assets to merge');
} else {
alert('Need at least 2 assets to merge');
}
return;
}
// Show merge dialog
const keepAssetId = await this.showMergeDialog(assets);
if (!keepAssetId) {
return;
}
await this.mergeGroup(sha256, assets, keepAssetId, button);
});
}
setupRefresh() {
document.addEventListener('click', (e) => {
const button = e.target.closest('[data-refresh-duplicates]');
if (!button) return;
e.preventDefault();
window.location.reload();
});
}
async deleteAsset(assetId, button) {
const originalText = button.textContent;
button.disabled = true;
button.textContent = 'Deleting...';
try {
const response = await fetch(`/admin/api/assets/${assetId}`, {
method: 'DELETE',
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
if (window.Toast) {
window.Toast.success('Duplicate asset deleted');
}
// Remove asset card
button.closest('.duplicate-asset-card')?.remove();
// Check if group is now empty or has only one asset
const groupCard = button.closest('.duplicate-group');
const remainingAssets = groupCard?.querySelectorAll('.duplicate-asset-card').length || 0;
if (remainingAssets <= 1) {
groupCard?.remove();
}
} catch (error) {
console.error('Failed to delete asset:', error);
if (window.Toast) {
window.Toast.error(`Failed to delete asset: ${error.message}`);
} else {
alert(`Failed to delete asset: ${error.message}`);
}
} finally {
button.disabled = false;
button.textContent = originalText;
}
}
async deleteGroup(sha256, button) {
const groupCard = button.closest('.duplicate-group');
const assetCards = groupCard?.querySelectorAll('.duplicate-asset-card') || [];
const assetIds = Array.from(assetCards).map(card =>
card.querySelector('[data-asset-id]')?.dataset.assetId
).filter(Boolean);
button.disabled = true;
button.textContent = 'Deleting...';
try {
// Delete all assets in parallel
const deletePromises = assetIds.map(id =>
fetch(`/admin/api/assets/${id}`, {
method: 'DELETE',
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
})
);
await Promise.all(deletePromises);
if (window.Toast) {
window.Toast.success(`Deleted ${assetIds.length} duplicate assets`);
}
// Remove group card
groupCard?.remove();
} catch (error) {
console.error('Failed to delete group:', error);
if (window.Toast) {
window.Toast.error(`Failed to delete group: ${error.message}`);
} else {
alert(`Failed to delete group: ${error.message}`);
}
} finally {
button.disabled = false;
button.textContent = 'Delete All';
}
}
async showMergeDialog(assetIds) {
return new Promise((resolve) => {
const dialog = document.createElement('div');
dialog.className = 'merge-dialog';
dialog.innerHTML = `
<div class="merge-dialog__overlay"></div>
<div class="merge-dialog__content">
<h3>Merge Duplicate Assets</h3>
<p>Select which asset to keep. All other duplicates will be deleted.</p>
<div class="merge-dialog__assets" id="merge-assets-list"></div>
<div class="merge-dialog__actions">
<button class="btn btn--secondary" data-merge-cancel>Cancel</button>
<button class="btn btn--primary" data-merge-confirm disabled>Merge</button>
</div>
</div>
`;
document.body.appendChild(dialog);
const assetsList = dialog.querySelector('#merge-assets-list');
let selectedAssetId = null;
// Load asset details and create radio buttons
assetIds.forEach(assetId => {
const assetCard = document.querySelector(`[data-asset-id="${assetId}"]`)?.closest('.duplicate-asset-card');
if (!assetCard) return;
const assetIdEl = assetCard.querySelector('.asset-id code');
const assetBucket = assetCard.querySelector('.asset-bucket');
const assetPreview = assetCard.querySelector('.asset-preview-small')?.innerHTML;
const radio = document.createElement('div');
radio.className = 'merge-asset-option';
radio.innerHTML = `
<input type="radio" name="keep-asset" value="${assetId}" id="asset-${assetId}">
<label for="asset-${assetId}">
<div class="merge-asset-preview">${assetPreview || ''}</div>
<div class="merge-asset-info">
<strong>${assetIdEl?.textContent || assetId}</strong>
<small>${assetBucket?.textContent || ''}</small>
</div>
</label>
`;
radio.querySelector('input').addEventListener('change', (e) => {
if (e.target.checked) {
selectedAssetId = e.target.value;
dialog.querySelector('[data-merge-confirm]').disabled = false;
}
});
assetsList.appendChild(radio);
});
const cleanup = () => {
document.body.removeChild(dialog);
};
dialog.querySelector('[data-merge-confirm]').addEventListener('click', () => {
cleanup();
resolve(selectedAssetId);
});
dialog.querySelector('[data-merge-cancel]').addEventListener('click', () => {
cleanup();
resolve(null);
});
dialog.querySelector('.merge-dialog__overlay').addEventListener('click', () => {
cleanup();
resolve(null);
});
});
}
async mergeGroup(sha256, assetIds, keepAssetId, button) {
button.disabled = true;
button.textContent = 'Merging...';
try {
// Delete all assets except the one to keep
const deleteIds = assetIds.filter(id => id !== keepAssetId);
const deletePromises = deleteIds.map(id =>
fetch(`/admin/api/assets/${id}`, {
method: 'DELETE',
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
})
);
await Promise.all(deletePromises);
if (window.Toast) {
window.Toast.success(`Merged ${deleteIds.length} duplicates into one asset`);
}
// Remove group card
button.closest('.duplicate-group')?.remove();
} catch (error) {
console.error('Failed to merge group:', error);
if (window.Toast) {
window.Toast.error(`Failed to merge group: ${error.message}`);
} else {
alert(`Failed to merge group: ${error.message}`);
}
} finally {
button.disabled = false;
button.textContent = 'Merge Group';
}
}
}
// Auto-initialize
document.addEventListener('DOMContentLoaded', () => {
if (document.querySelector('.duplicates-list')) {
new DuplicateManagement();
}
});