Vendoo 1.43.0 – Production Update
Manifestbasierter Production-Updater 3.3, Modulnavigation, optionales Produktbild Studio, Listing-Studio-Aktionsleiste sowie plattformübergreifende Windows-/Linux-Release-Gates.
This commit was merged in pull request #26.
This commit is contained in:
+230
-21
@@ -1,4 +1,4 @@
|
||||
const VENDOO_UI_BUILD = '1.41.2';
|
||||
const VENDOO_UI_BUILD = '1.43.0';
|
||||
|
||||
const state = {
|
||||
photos: [],
|
||||
@@ -282,6 +282,44 @@ function setupResponsiveContracts() {
|
||||
}
|
||||
}
|
||||
|
||||
const moduleUiState = new Map();
|
||||
|
||||
function isUiModuleEnabled(id) {
|
||||
return !id || moduleUiState.get(id) !== false;
|
||||
}
|
||||
|
||||
function applyModuleVisibility(root = document) {
|
||||
root.querySelectorAll?.('[data-module], [data-module-panel]').forEach(element => {
|
||||
const moduleId = element.dataset.module || element.dataset.modulePanel;
|
||||
const visible = isUiModuleEnabled(moduleId);
|
||||
element.classList.toggle('hidden', !visible);
|
||||
element.setAttribute('aria-hidden', visible ? 'false' : 'true');
|
||||
if ('disabled' in element && !visible) element.disabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
function applyModuleNavigation(payload = {}) {
|
||||
moduleUiState.clear();
|
||||
for (const module of payload.modules || []) moduleUiState.set(module.id, module.enabled === true);
|
||||
applyModuleVisibility(document);
|
||||
const active = document.querySelector('.tab-content.active');
|
||||
const activeButton = active ? document.querySelector(`.nav-item[data-tab="${active.id}"]`) : null;
|
||||
if (activeButton?.classList.contains('hidden')) switchToTab('dashboard');
|
||||
}
|
||||
|
||||
async function refreshModuleNavigation() {
|
||||
try {
|
||||
const payload = await api('GET', '/api/modules/navigation', null, { retry: false, timeoutMs: 10000 });
|
||||
applyModuleNavigation(payload);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
console.warn('[Vendoo] Modulnavigation konnte nicht geladen werden:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('vendoo:modules-changed', () => refreshModuleNavigation());
|
||||
|
||||
async function init() {
|
||||
document.documentElement.dataset.vendooBuild = VENDOO_UI_BUILD;
|
||||
console.info(`[Vendoo] UI-Build ${VENDOO_UI_BUILD}`);
|
||||
@@ -295,10 +333,16 @@ async function init() {
|
||||
if (meData.setupMode) { window.location.href = '/login.html'; return; }
|
||||
} catch {}
|
||||
|
||||
// Modulstatus immer zuerst laden: Ein deaktiviertes Modul darf weder seine
|
||||
// Navigation anzeigen noch den App-Start durch eine gesperrte API blockieren.
|
||||
const moduleNavigation = await api('GET', '/api/modules/navigation');
|
||||
applyModuleNavigation(moduleNavigation);
|
||||
const aiEnabled = isUiModuleEnabled('vendoo.ai');
|
||||
|
||||
const [platforms, settings, providers, templates] = await Promise.all([
|
||||
api('GET', '/api/platforms'),
|
||||
api('GET', '/api/settings'),
|
||||
api('GET', '/api/providers'),
|
||||
aiEnabled ? api('GET', '/api/providers') : Promise.resolve([]),
|
||||
api('GET', '/api/templates'),
|
||||
]);
|
||||
|
||||
@@ -1704,6 +1748,12 @@ function closeMobileSidebar({ restoreFocus = false } = {}) {
|
||||
}
|
||||
|
||||
function switchToTab(tabId, adminTab) {
|
||||
const requestedNav = document.querySelector(`.nav-item[data-tab="${tabId}"]`);
|
||||
if (requestedNav?.classList.contains('hidden')) {
|
||||
toast('Dieses Modul ist derzeit deaktiviert.');
|
||||
tabId = 'dashboard';
|
||||
adminTab = null;
|
||||
}
|
||||
if (tabId !== 'history' && listingEditLock.listingId) releaseListingLock({ quiet: true });
|
||||
document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
@@ -2464,7 +2514,8 @@ function showListing(listing, platformHtml) {
|
||||
document.getElementById('result-tags').value = tags;
|
||||
const plat = state.platforms.find(p => p.id === state.platform);
|
||||
document.getElementById('tags-label').textContent = plat?.fields.includes('hashtags') ? 'Hashtags' : 'Tags';
|
||||
document.getElementById('save-btn').textContent = listing.id ? 'Änderungen speichern' : 'Speichern & Hinzufügen';
|
||||
const saveButton = document.getElementById('save-btn');
|
||||
if (saveButton) { saveButton.textContent = listing.id ? 'Änderungen speichern' : 'Artikel speichern'; saveButton.disabled = false; }
|
||||
document.getElementById('generator-preview-empty')?.classList.add('hidden');
|
||||
document.getElementById('result-section').classList.remove('hidden');
|
||||
const previewStatus = document.getElementById('generator-preview-status');
|
||||
@@ -2568,7 +2619,8 @@ function resetGenerator() {
|
||||
document.getElementById('meta-section').classList.add('hidden');
|
||||
document.getElementById('variants-section').classList.add('hidden');
|
||||
document.getElementById('generator-preview-empty')?.classList.remove('hidden');
|
||||
document.getElementById('save-btn').textContent = 'Speichern & Hinzufügen';
|
||||
const saveButton = document.getElementById('save-btn');
|
||||
if (saveButton) { saveButton.textContent = 'Speichern'; saveButton.disabled = true; }
|
||||
document.getElementById('generate-btn').disabled = true;
|
||||
renderAiModelGrid('generator', []);
|
||||
setAiModelStatus('generator-ai-model-status', 'Wartet auf Produktfotos.', 'neutral');
|
||||
@@ -3397,13 +3449,13 @@ async function showDetail(id) {
|
||||
<button id="detail-download-photos" class="vd-button compact" type="button">Fotos herunterladen</button>
|
||||
</div>
|
||||
<p class="detail-photo-help">Vorschaubilder ziehen, um die Reihenfolge zu ändern. Das erste Bild ist das Cover.</p>
|
||||
<section class="ai-model-card detail-ai-model-card">
|
||||
<section class="ai-model-card detail-ai-model-card hidden" data-module-panel="vendoo.product-image-tools">
|
||||
<div class="ai-model-card-head">
|
||||
<div>
|
||||
<span class="generator-panel-index">AI</span>
|
||||
<div>
|
||||
<h4>Produktbild-Werkzeuge</h4>
|
||||
<p>Erzeuge Ghost-Mannequin- oder Flatlay-Varianten aus den aktuellen Listing-Fotos. Freie Prompt-Bilder werden im FLUX Studio erstellt.</p>
|
||||
<h4>Produktbild Studio</h4>
|
||||
<p>Optionales Modul für kontrollierte Ghost-Mannequin- und Flatlay-Varianten aus den aktuellen Listing-Fotos.</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="generator-beta">Safe</span>
|
||||
@@ -3514,6 +3566,8 @@ async function showDetail(id) {
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
applyModuleVisibility(document.getElementById('detail-content'));
|
||||
|
||||
const lockResult = await acquireListingLock(numericId);
|
||||
if (!userCan('listings.edit') || lockResult.readOnly) {
|
||||
document.querySelectorAll('#history-detail input, #history-detail textarea, #history-detail select').forEach(element => { element.disabled = true; });
|
||||
@@ -3545,16 +3599,18 @@ async function showDetail(id) {
|
||||
if (element) { element.disabled = true; element.setAttribute('aria-disabled', 'true'); }
|
||||
});
|
||||
}
|
||||
document.getElementById('detail-ai-model-btn')?.addEventListener('click', runDetailAiModelPhotos);
|
||||
document.getElementById('detail-ai-model-prompt-btn')?.addEventListener('click', () => openAiPromptModal('detail'));
|
||||
document.getElementById('detail-ai-model-mode')?.addEventListener('change', () => updateAiModeControls('detail'));
|
||||
updateAiModeControls('detail');
|
||||
if (isUiModuleEnabled('vendoo.product-image-tools')) {
|
||||
document.getElementById('detail-ai-model-btn')?.addEventListener('click', runDetailAiModelPhotos);
|
||||
document.getElementById('detail-ai-model-prompt-btn')?.addEventListener('click', () => openAiPromptModal('detail'));
|
||||
document.getElementById('detail-ai-model-mode')?.addEventListener('change', () => updateAiModeControls('detail'));
|
||||
updateAiModeControls('detail');
|
||||
renderAiModelGrid('detail', []);
|
||||
loadDetailAiModelPhotos(numericId);
|
||||
loadDetailAiModelGenerations(numericId);
|
||||
}
|
||||
renderDetailPhotos();
|
||||
renderAiModelGrid('detail', []);
|
||||
addQuillTooltips();
|
||||
refreshDetailPlatformPreview();
|
||||
loadDetailAiModelPhotos(numericId);
|
||||
loadDetailAiModelGenerations(numericId);
|
||||
|
||||
if (typeof QRious !== 'undefined') {
|
||||
new QRious({ element: document.getElementById('detail-qr-canvas'), value: JSON.stringify({ sku: skuVal, id: l.id }), size: 118, level: 'M' });
|
||||
@@ -8107,6 +8163,92 @@ async function loadOperationsExtensions() {
|
||||
catch (error) { const box = document.getElementById('operations-extension-clients'); if (box) box.innerHTML = `<div class="empty-state">${escapeHtml(error.message)}</div>`; }
|
||||
}
|
||||
|
||||
function deploymentStateLabel(state) {
|
||||
return ({
|
||||
checking: 'Prüfung', backup_pending: 'Backup geplant', backup_running: 'Backup läuft',
|
||||
backup_verified: 'Backup verifiziert', deploy_requested: 'Deployment angefordert',
|
||||
deploy_running: 'Deployment läuft', health_wait: 'Readiness wird geprüft',
|
||||
succeeded: 'Erfolgreich', failed: 'Fehlgeschlagen', rollback_required: 'Rollback/Prüfung erforderlich',
|
||||
})[state] || state || 'Unbekannt';
|
||||
}
|
||||
|
||||
function deploymentStateClass(state) {
|
||||
if (state === 'succeeded') return 'is-ok';
|
||||
if (state === 'failed' || state === 'rollback_required') return 'is-failed';
|
||||
return 'is-running';
|
||||
}
|
||||
|
||||
function renderProductionOverview(data) {
|
||||
const box = document.getElementById('deployment-production-overview');
|
||||
if (!box) return;
|
||||
const production = data.production || {};
|
||||
const readiness = production.readiness || {};
|
||||
const storage = production.storage || {};
|
||||
const lastBackup = production.last_backup;
|
||||
const flux = production.flux || {};
|
||||
const cards = [
|
||||
['Version', production.version || data.current_version || VENDOO_UI_BUILD, production.build_revision ? `Build ${String(production.build_revision).slice(0, 12)}` : 'Keine Build-Revision gemeldet', true],
|
||||
['Readiness', readiness.ok ? 'Bereit' : 'Nicht bereit', readiness.error || readiness.message || 'Interner Readiness-Check', !!readiness.ok],
|
||||
['Datenbank', production.database?.integrity === 'ok' ? 'Integrität OK' : String(production.database?.integrity || 'Unbekannt'), production.database?.path || 'Kein Pfad gemeldet', production.database?.integrity === 'ok'],
|
||||
['Persistente Daten', operationFormatBytes(storage.data?.bytes || 0), `${Number(storage.data?.files || 0)} Einträge · Uploads ${operationFormatBytes(storage.uploads?.bytes || 0)}`, true],
|
||||
['Letztes Backup', lastBackup ? (lastBackup.status === 'verified' ? 'Verifiziert' : lastBackup.status) : 'Nicht vorhanden', lastBackup ? `${operationFormatDate(lastBackup.created_at)} · ${operationFormatBytes(lastBackup.file_size)}` : 'Vor dem ersten Deployment erforderlich', lastBackup?.status === 'verified'],
|
||||
['FLUX Studio', flux.enabled ? 'Aktiv' : 'Deaktiviert', flux.enabled ? 'Produktionsgrenze prüfen' : 'Wie vorgesehen deaktiviert', !flux.enabled],
|
||||
];
|
||||
box.innerHTML = cards.map(([label, value, detail, ok]) => `<article class="deployment-production-item ${ok ? 'is-ok' : 'is-warning'}"><span data-vd-icon="${ok ? 'check-circle-2' : 'triangle-alert'}"></span><div><small>${escapeHtml(label)}</small><strong>${escapeHtml(String(value))}</strong><p>${escapeHtml(String(detail || ''))}</p></div></article>`).join('');
|
||||
|
||||
const checkBox = document.getElementById('deployment-production-check-result');
|
||||
const check = data.latest_production_check;
|
||||
if (checkBox && check) {
|
||||
checkBox.innerHTML = `<strong>${check.overall_status === 'passed' ? 'Produktionscheck bestanden' : check.overall_status === 'warning' ? 'Produktionscheck mit Hinweisen' : 'Produktionscheck fehlgeschlagen'}</strong><span>${Number(check.passed_count || 0)} bestanden · ${Number(check.warning_count || 0)} Hinweise · ${Number(check.failed_count || 0)} Fehler · ${operationFormatDate(check.created_at || check.verified_at)}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderDeploymentRuns(data) {
|
||||
const list = document.getElementById('deployment-run-list');
|
||||
if (!list) return;
|
||||
const runs = data.recent_runs || [];
|
||||
if (!runs.length) { list.innerHTML = '<div class="empty-state">Keine Deployment-Aufträge vorhanden.</div>'; return; }
|
||||
list.innerHTML = runs.map(run => {
|
||||
const lastEvent = run.events?.at?.(-1);
|
||||
const active = !['succeeded', 'failed'].includes(run.state);
|
||||
return `<article class="deployment-run ${deploymentStateClass(run.state)}">
|
||||
<div class="deployment-run-head"><span class="deployment-run-state">${escapeHtml(deploymentStateLabel(run.state))}</span><time>${operationFormatDate(run.created_at)}</time></div>
|
||||
<strong>${escapeHtml(run.current_version || '?')} → ${escapeHtml(run.target_version || '?')}</strong>
|
||||
<p>${escapeHtml(lastEvent?.message || run.error_message || run.reason || 'Deployment-Auftrag')}</p>
|
||||
<small>${escapeHtml(run.github_branch || 'main')}${run.backup_id ? ' · Backup verifiziert' : ''}${run.deployment_id ? ` · ID ${escapeHtml(String(run.deployment_id).slice(0, 18))}` : ''}</small>
|
||||
${active ? `<div class="deployment-run-actions">${run.state !== 'rollback_required' ? `<button type="button" class="secondary-btn compact" data-deployment-refresh="${escapeHtml(run.id)}">Status aktualisieren</button>` : ''}${data.trigger_mode === 'webhook' && ['deploy_requested','deploy_running','health_wait'].includes(run.state) ? `<button type="button" class="secondary-btn compact" data-deployment-confirm="${escapeHtml(run.id)}">Externen Abschluss bestätigen</button>` : ''}${run.state === 'rollback_required' ? `<button type="button" class="secondary-btn compact" data-deployment-resolve="${escapeHtml(run.id)}">Prüfung abschließen</button>` : ''}</div>` : ''}
|
||||
</article>`;
|
||||
}).join('');
|
||||
list.querySelectorAll('[data-deployment-refresh]').forEach(button => button.addEventListener('click', async () => {
|
||||
button.disabled = true;
|
||||
try { await api('POST', `/api/admin/deployment/runs/${encodeURIComponent(button.dataset.deploymentRefresh)}/refresh`); await loadDeploymentStatus(); }
|
||||
catch (error) { toast(error.message); }
|
||||
finally { button.disabled = false; }
|
||||
}));
|
||||
list.querySelectorAll('[data-deployment-confirm]').forEach(button => button.addEventListener('click', async () => {
|
||||
const confirmation = prompt('Nur bestätigen, wenn Coolify das Deployment wirklich abgeschlossen hat. Exakt CONFIRM eingeben:');
|
||||
if (confirmation !== 'CONFIRM') return;
|
||||
button.disabled = true;
|
||||
try {
|
||||
await api('POST', `/api/admin/deployment/runs/${encodeURIComponent(button.dataset.deploymentConfirm)}/confirm`, { confirmation, outcome: 'succeeded', message: 'Im Vendoo Update Center extern bestätigt.' });
|
||||
toast('Externer Abschluss wurde erfasst und Readiness erneut geprüft.');
|
||||
await loadDeploymentStatus();
|
||||
} catch (error) { toast(error.message); }
|
||||
finally { button.disabled = false; }
|
||||
}));
|
||||
list.querySelectorAll('[data-deployment-resolve]').forEach(button => button.addEventListener('click', async () => {
|
||||
const confirmation = prompt('Der Auftrag bleibt als fehlgeschlagen dokumentiert und die Deployment-Sperre wird aufgehoben. Exakt CONFIRM eingeben:');
|
||||
if (confirmation !== 'CONFIRM') return;
|
||||
button.disabled = true;
|
||||
try {
|
||||
await api('POST', `/api/admin/deployment/runs/${encodeURIComponent(button.dataset.deploymentResolve)}/confirm`, { confirmation, outcome: 'failed', message: 'Rollback-/Fehlerprüfung manuell abgeschlossen.' });
|
||||
toast('Prüfung abgeschlossen. Ein neuer Deployment-Auftrag ist wieder möglich.');
|
||||
await loadDeploymentStatus();
|
||||
} catch (error) { toast(error.message); }
|
||||
finally { button.disabled = false; }
|
||||
}));
|
||||
}
|
||||
|
||||
function renderDeploymentStatus() {
|
||||
const data = operationsState.deployment || {};
|
||||
const setValue = (id, value) => { const field = document.getElementById(id); if (field && document.activeElement !== field) field.value = value ?? ''; };
|
||||
@@ -8114,9 +8256,14 @@ function renderDeploymentStatus() {
|
||||
setValue('deployment-coolify-url', data.coolify_url || '');
|
||||
setValue('deployment-resource-uuid', data.resource_uuid || '');
|
||||
setValue('deployment-trigger-mode', data.trigger_mode || 'webhook');
|
||||
setValue('deployment-status-endpoint', data.deployment_status_endpoint || '/api/v1/deployments/{deployment_id}');
|
||||
setValue('deployment-timeout-seconds', data.deployment_timeout_seconds || 900);
|
||||
setValue('deployment-poll-seconds', data.health_poll_seconds || 15);
|
||||
setValue('deployment-github-repository', data.github_repository || 'Masterluke77/vendoo');
|
||||
setValue('deployment-github-branch', data.github_branch || 'main');
|
||||
setValue('deployment-update-channel', data.update_channel || 'stable');
|
||||
const target = document.getElementById('deployment-target-version');
|
||||
if (target && document.activeElement !== target && !target.value) target.value = data.active_run?.target_version || data.current_version || '';
|
||||
const backup = document.getElementById('deployment-backup-before'); if (backup) backup.checked = data.backup_before_deploy !== false;
|
||||
const uploads = document.getElementById('deployment-backup-uploads'); if (uploads) uploads.checked = data.backup_uploads === true;
|
||||
const webhook = document.getElementById('deployment-webhook-url'); if (webhook && document.activeElement !== webhook) { webhook.value = ''; webhook.placeholder = data.deploy_webhook?.configured ? `Gespeichert: ${data.deploy_webhook.masked || '••••••••'}` : 'Coolify Deploy-Webhook eintragen'; }
|
||||
@@ -8124,7 +8271,11 @@ function renderDeploymentStatus() {
|
||||
const status = document.getElementById('deployment-status');
|
||||
if (status) status.innerHTML = `<strong>${data.configured ? 'Coolify ist konfiguriert' : 'Coolify noch nicht vollständig konfiguriert'}</strong><span>Provider: ${escapeHtml(data.provider || 'none')} · Trigger: ${escapeHtml(data.trigger_mode || 'webhook')} · Docker-Socket: ${data.docker_socket_access ? 'vorhanden' : 'nicht freigegeben'}</span>`;
|
||||
const action = document.getElementById('deployment-action-result');
|
||||
if (action && data.last_deployment) action.innerHTML = `<strong>${data.last_deployment.accepted ? 'Letzter Deployment-Auftrag angenommen' : 'Letzter Deployment-Auftrag fehlgeschlagen'}</strong><span>${operationFormatDate(data.last_deployment.requested_at)} · Branch ${escapeHtml(data.last_deployment.github_branch || 'main')} · HTTP ${escapeHtml(String(data.last_deployment.http_status || '—'))}</span>`;
|
||||
const last = data.recent_runs?.[0] || data.production?.last_deployment;
|
||||
if (action && last) action.innerHTML = `<strong>${escapeHtml(deploymentStateLabel(last.state))}</strong><span>${operationFormatDate(last.created_at || last.requested_at)} · ${escapeHtml(last.current_version || data.current_version || '?')} → ${escapeHtml(last.target_version || '?')} · Branch ${escapeHtml(last.github_branch || 'main')}</span>`;
|
||||
renderProductionOverview(data);
|
||||
renderDeploymentRuns(data);
|
||||
window.VendooIcons?.render(document.getElementById('operations-updates-panel'));
|
||||
}
|
||||
|
||||
async function loadDeploymentStatus() {
|
||||
@@ -8140,6 +8291,9 @@ function deploymentSettingsPayload() {
|
||||
trigger_mode: document.getElementById('deployment-trigger-mode')?.value || 'webhook',
|
||||
deploy_method: 'GET',
|
||||
deploy_endpoint: '/api/v1/deploy?uuid={uuid}&force={force}',
|
||||
deployment_status_endpoint: document.getElementById('deployment-status-endpoint')?.value || '/api/v1/deployments/{deployment_id}',
|
||||
deployment_timeout_seconds: Number(document.getElementById('deployment-timeout-seconds')?.value || 900),
|
||||
health_poll_seconds: Number(document.getElementById('deployment-poll-seconds')?.value || 15),
|
||||
github_repository: document.getElementById('deployment-github-repository')?.value || 'Masterluke77/vendoo',
|
||||
github_branch: document.getElementById('deployment-github-branch')?.value || 'main',
|
||||
ghcr_image: 'ghcr.io/masterluke77/vendoo',
|
||||
@@ -8238,6 +8392,18 @@ function setupOperationsCenter() {
|
||||
finally { button.disabled = !operationsState.updateFile; button.textContent = 'Update prüfen und vorbereiten'; }
|
||||
});
|
||||
|
||||
document.getElementById('deployment-production-check')?.addEventListener('click', async event => {
|
||||
const button = event.currentTarget; const resultBox = document.getElementById('deployment-production-check-result');
|
||||
button.disabled = true; if (resultBox) resultBox.textContent = 'Produktionsgrenzen werden geprüft …';
|
||||
try {
|
||||
const result = await api('POST', '/api/admin/deployment/production-check');
|
||||
const details = (result.results || []).filter(item => item.status !== 'pass').map(item => `${item.title || item.name || item.id}: ${item.message || item.status}`).slice(0, 4).join(' · ');
|
||||
if (resultBox) resultBox.innerHTML = `<strong>${result.overall_status === 'passed' ? 'Produktionscheck bestanden' : result.overall_status === 'warning' ? 'Produktionscheck mit Hinweisen' : 'Produktionscheck fehlgeschlagen'}</strong><span>${Number(result.passed_count || 0)} bestanden · ${Number(result.warning_count || 0)} Hinweise · ${Number(result.failed_count || 0)} Fehler${details ? ` · ${escapeHtml(details)}` : ''}</span>`;
|
||||
await loadDeploymentStatus();
|
||||
} catch (error) { if (resultBox) resultBox.innerHTML = `<strong>Produktionscheck konnte nicht ausgeführt werden</strong><span>${escapeHtml(error.message)}</span>`; }
|
||||
finally { button.disabled = false; }
|
||||
});
|
||||
|
||||
document.getElementById('deployment-save')?.addEventListener('click', async event => {
|
||||
const button = event.currentTarget; button.disabled = true;
|
||||
try { operationsState.deployment = await api('PUT', '/api/admin/deployment', deploymentSettingsPayload()); renderDeploymentStatus(); toast('Coolify- und GitHub-Konfiguration gespeichert.'); }
|
||||
@@ -8252,7 +8418,12 @@ function setupOperationsCenter() {
|
||||
});
|
||||
document.getElementById('deployment-update-check')?.addEventListener('click', async event => {
|
||||
const button = event.currentTarget; const resultBox = document.getElementById('deployment-action-result'); button.disabled = true;
|
||||
try { const result = await api('POST', '/api/admin/deployment/updates/check'); if (resultBox) resultBox.innerHTML = `<strong>${result.update_available ? `Version ${escapeHtml(result.latest_version)} verfügbar` : result.configured ? 'Vendoo ist aktuell' : 'Keine Manifest-URL konfiguriert'}</strong><span>Installiert: ${escapeHtml(result.current_version || VENDOO_UI_BUILD)}${result.message ? ` · ${escapeHtml(result.message)}` : ''}</span>`; }
|
||||
try {
|
||||
const result = await api('POST', '/api/admin/deployment/updates/check');
|
||||
const target = document.getElementById('deployment-target-version');
|
||||
if (target && result.latest_version) target.value = result.latest_version;
|
||||
if (resultBox) resultBox.innerHTML = `<strong>${result.update_available ? `Version ${escapeHtml(result.latest_version)} verfügbar` : result.configured ? 'Vendoo ist aktuell' : 'Keine Manifest-URL konfiguriert'}</strong><span>Installiert: ${escapeHtml(result.current_version || VENDOO_UI_BUILD)}${result.latest_version ? ` · Freigegeben: ${escapeHtml(result.latest_version)}` : ''}${result.message ? ` · ${escapeHtml(result.message)}` : ''}</span>`;
|
||||
}
|
||||
catch (error) { if (resultBox) resultBox.innerHTML = `<strong>Updateprüfung fehlgeschlagen</strong><span>${escapeHtml(error.message)}</span>`; }
|
||||
finally { button.disabled = false; }
|
||||
});
|
||||
@@ -8261,9 +8432,11 @@ function setupOperationsCenter() {
|
||||
if (confirmation !== 'DEPLOY') return;
|
||||
const button = event.currentTarget; const resultBox = document.getElementById('deployment-action-result'); button.disabled = true; button.textContent = 'Deployment wird angefordert …';
|
||||
try {
|
||||
const result = await api('POST', '/api/admin/deployment/trigger', { confirmation, force: false, reason: 'update-center' });
|
||||
if (resultBox) resultBox.innerHTML = `<strong>Coolify-Deployment angenommen</strong><span>${escapeHtml(result.message || '')}${result.backup_file ? ` · Backup: ${escapeHtml(result.backup_file)}` : ''}</span>`;
|
||||
toast('Coolify-Deployment wurde angefordert.');
|
||||
const targetVersion = document.getElementById('deployment-target-version')?.value || operationsState.deployment?.current_version || VENDOO_UI_BUILD;
|
||||
const idempotencyKey = globalThis.crypto?.randomUUID?.() || `vendoo-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
const result = await api('POST', '/api/admin/deployment/trigger', { confirmation, force: false, reason: 'update-center', target_version: targetVersion, idempotency_key: idempotencyKey });
|
||||
if (resultBox) resultBox.innerHTML = `<strong>${result.reused ? 'Vorhandener Deployment-Auftrag erkannt' : 'Coolify-Deployment angenommen'}</strong><span>${escapeHtml(result.message || deploymentStateLabel(result.state))}${result.backup_id ? ` · Backup ${escapeHtml(String(result.backup_id).slice(0, 12))}` : ''}</span>`;
|
||||
toast(result.reused ? 'Der gleiche Deployment-Auftrag war bereits vorhanden.' : 'Coolify-Deployment wurde angefordert.');
|
||||
await loadDeploymentStatus();
|
||||
} catch (error) { if (resultBox) resultBox.innerHTML = `<strong>Deployment fehlgeschlagen</strong><span>${escapeHtml(error.message)}</span>`; toast(error.message); }
|
||||
finally { button.disabled = false; button.textContent = 'Coolify-Deployment starten'; }
|
||||
@@ -8291,7 +8464,7 @@ function setupAdmin() {
|
||||
document.getElementById('invite-user-btn')?.addEventListener('click', () => {
|
||||
document.getElementById('invite-email').value = '';
|
||||
document.getElementById('invite-name').value = '';
|
||||
document.getElementById('invite-role').value = 'editor';
|
||||
document.getElementById('invite-role').value = 'viewer';
|
||||
document.getElementById('invite-result-wrap').classList.add('hidden');
|
||||
document.getElementById('invite-result-password').value = '';
|
||||
const mlRadio = document.querySelector('input[name="invite-method"][value="magic_link"]');
|
||||
@@ -8442,13 +8615,48 @@ function exportAdminAudit() {
|
||||
window.location.href = `/api/admin/audit/export.csv?${params.toString()}`;
|
||||
}
|
||||
|
||||
async function loadAdminInvitations() {
|
||||
const container = document.getElementById('invitations-list');
|
||||
if (!container) return;
|
||||
try {
|
||||
const result = await api('GET', '/api/admin/users/invitations');
|
||||
const invitations = result.invitations || [];
|
||||
if (!invitations.length) { container.innerHTML = '<div class="empty-state">Noch keine Einladungen vorhanden.</div>'; return; }
|
||||
const statusLabel = status => ({ pending: 'Offen', expired: 'Abgelaufen', revoked_or_used: 'Verwendet oder widerrufen', revoked: 'Widerrufen' })[status] || status;
|
||||
container.innerHTML = invitations.map(invitation => {
|
||||
const actionable = ['pending', 'expired'].includes(invitation.status);
|
||||
return `<article class="invitation-admin-row ${invitation.status === 'pending' ? 'is-pending' : ''}">
|
||||
<div><strong>${escapeHtml(invitation.email)}</strong><span>${escapeHtml(ROLE_LABELS[invitation.role] || invitation.role || 'Leser')} · ${escapeHtml(statusLabel(invitation.status))}</span><small>Erstellt ${operationFormatDate(invitation.created_at)} · Ablauf ${operationFormatDate(invitation.expires_at)}</small></div>
|
||||
${actionable ? `<div class="invitation-admin-actions"><button type="button" class="small-btn" data-invitation-resend="${invitation.id}" title="Einladung erneut senden"><span data-vd-icon="mail"></span></button>${invitation.status === 'pending' ? `<button type="button" class="small-btn danger-btn" data-invitation-revoke="${invitation.id}" title="Einladung widerrufen"><span data-vd-icon="ban"></span></button>` : ''}</div>` : ''}
|
||||
</article>`;
|
||||
}).join('');
|
||||
container.querySelectorAll('[data-invitation-resend]').forEach(button => button.addEventListener('click', async () => {
|
||||
button.disabled = true;
|
||||
try {
|
||||
const result = await api('POST', `/api/admin/users/invitations/${encodeURIComponent(button.dataset.invitationResend)}/resend`);
|
||||
toast(result.consoleFallback ? 'Neue Einladung erstellt; Link wurde in der Server-Konsole ausgegeben.' : 'Einladung erneut gesendet.');
|
||||
await loadAdminInvitations();
|
||||
} catch (error) { toast(error.message); }
|
||||
finally { button.disabled = false; }
|
||||
}));
|
||||
container.querySelectorAll('[data-invitation-revoke]').forEach(button => button.addEventListener('click', async () => {
|
||||
if (!confirm('Diese Einladung sofort widerrufen? Der Link kann danach nicht mehr verwendet werden.')) return;
|
||||
button.disabled = true;
|
||||
try { await api('DELETE', `/api/admin/users/invitations/${encodeURIComponent(button.dataset.invitationRevoke)}`); toast('Einladung widerrufen.'); await loadAdminInvitations(); }
|
||||
catch (error) { toast(error.message); }
|
||||
finally { button.disabled = false; }
|
||||
}));
|
||||
globalThis.VendooIcons?.render?.(container);
|
||||
} catch (error) { container.innerHTML = `<div class="empty-state">${escapeHtml(error.message)}</div>`; }
|
||||
}
|
||||
|
||||
async function loadAdminUsers() {
|
||||
try {
|
||||
const users = await api('GET', '/api/admin/users');
|
||||
adminUsersCache = Array.isArray(users) ? users : [];
|
||||
const container = document.getElementById('users-list');
|
||||
if (!container) return;
|
||||
if (!adminUsersCache.length) { container.innerHTML = '<div class="empty-state">Keine User vorhanden</div>'; return; }
|
||||
if (!adminUsersCache.length) { container.innerHTML = '<div class="empty-state">Keine User vorhanden</div>'; await loadAdminInvitations(); return; }
|
||||
|
||||
container.innerHTML = adminUsersCache.map(u => {
|
||||
const initials = getInitials(u.name, u.email);
|
||||
@@ -8480,6 +8688,7 @@ async function loadAdminUsers() {
|
||||
if (user) confirmDeleteUser(user.id, user.email);
|
||||
}));
|
||||
globalThis.VendooIcons?.render?.(container);
|
||||
await loadAdminInvitations();
|
||||
} catch (err) { toast('Fehler: ' + err.message); }
|
||||
}
|
||||
|
||||
|
||||
+85
-54
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="vendoo-build" content="1.41.2">
|
||||
<meta name="vendoo-build" content="1.43.0">
|
||||
<title>Vendoo</title>
|
||||
<link rel="icon" href="/brand/favicons/favicon.ico" sizes="any">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/brand/favicons/favicon-16x16.png">
|
||||
@@ -13,9 +13,9 @@
|
||||
<link rel="manifest" href="/brand/favicons/site.webmanifest">
|
||||
<meta name="msapplication-config" content="/brand/favicons/browserconfig.xml">
|
||||
<meta name="theme-color" content="#FAF6F2">
|
||||
<link rel="stylesheet" href="style.css?v=1.41.2">
|
||||
<link rel="stylesheet" href="modules/theme-manager/theme-manager.css?v=1.41.2">
|
||||
<link rel="stylesheet" href="modules/module-manager/module-manager.css?v=1.41.2">
|
||||
<link rel="stylesheet" href="style.css?v=1.43.0">
|
||||
<link rel="stylesheet" href="modules/theme-manager/theme-manager.css?v=1.43.0">
|
||||
<link rel="stylesheet" href="modules/module-manager/module-manager.css?v=1.43.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
@@ -36,55 +36,55 @@
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
|
||||
<span>Dashboard</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="generator">
|
||||
<button class="nav-item" data-tab="generator" data-module="vendoo.ai">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/><rect x="3" y="3" width="18" height="18" rx="3"/></svg>
|
||||
<span>Neuer Artikel</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="today">
|
||||
<button class="nav-item" data-tab="today" data-module="vendoo.listings">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11.5 12 4l9 7.5"/><path d="M5.5 10.5V20h13v-9.5"/><path d="M9.5 20v-6h5v6"/></svg>
|
||||
<span>Heute</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="history">
|
||||
<button class="nav-item" data-tab="history" data-module="vendoo.listings">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 7h8M8 11h8M8 15h5"/></svg>
|
||||
<span>Artikel</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="quality-center">
|
||||
<button class="nav-item" data-tab="quality-center" data-module="vendoo.quality">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 20 7v5c0 5-3.4 8.3-8 10-4.6-1.7-8-5-8-10V7Z"/><path d="m8.5 12 2.2 2.2 4.8-5"/></svg>
|
||||
<span>Qualitätscenter</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="publish">
|
||||
<button class="nav-item" data-tab="publish" data-module="vendoo.publishing">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg>
|
||||
<span>Publish</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="inventory">
|
||||
<button class="nav-item" data-tab="inventory" data-module="vendoo.inventory">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m4 7 8-4 8 4-8 4Z"/><path d="m4 7 8 4 8-4v10l-8 4-8-4Z"/><path d="M12 11v10"/></svg>
|
||||
<span>Lager</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="templates">
|
||||
<button class="nav-item" data-tab="templates" data-module="vendoo.listings">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 8h8M8 12h8M8 16h5"/></svg>
|
||||
<span>Vorlagen</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="flux-studio">
|
||||
<button class="nav-item" data-tab="flux-studio" data-module="vendoo.flux-studio">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 9.8 8.2 4 10l5 3.2L8 19l4-3 4 3-1-5.8 5-3.2-5.8-1.8Z"/><path d="M18 3v4M16 5h4"/></svg>
|
||||
<span>FLUX Studio</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="media-library">
|
||||
<button class="nav-item" data-tab="media-library" data-module="vendoo.media">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="9" r="1.5"/><path d="m4 17 5-5 4 4 2-2 5 5"/></svg>
|
||||
<span>Bilder</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="image-factory">
|
||||
<button class="nav-item" data-tab="image-factory" data-module="vendoo.ai">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4z"/><path d="M14 17h6M17 14v6"/></svg>
|
||||
<span>Bildproduktion</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="extensions">
|
||||
<button class="nav-item" data-tab="extensions" data-module="vendoo.module-manager">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 3v4M15.5 3v4M6 7h12v4a6 6 0 0 1-12 0Z"/><path d="M12 17v4M9 21h6"/></svg>
|
||||
<span>Extensions</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="settings">
|
||||
<button class="nav-item" data-tab="settings" data-module="vendoo.settings">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.05.05-2.83 2.83-.05-.05a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1.04 1.57V21h-4v-.07a1.7 1.7 0 0 0-1.04-1.57 1.7 1.7 0 0 0-1.87.34l-.05.05-2.83-2.83.05-.05A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.57-1.04H3v-4h.07A1.7 1.7 0 0 0 4.64 8.9a1.7 1.7 0 0 0-.34-1.87l-.05-.05 2.83-2.83.05.05A1.7 1.7 0 0 0 9 4.54 1.7 1.7 0 0 0 10.04 3H10V3h4v.07A1.7 1.7 0 0 0 15.04 4.6a1.7 1.7 0 0 0 1.87-.34l.05-.05 2.83 2.83-.05.05A1.7 1.7 0 0 0 19.4 9c.24.63.85 1.04 1.53 1.04H21v4h-.07c-.68 0-1.29.41-1.53 1.04Z"/></svg>
|
||||
<span>Einstellungen</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="admin" data-admin-tab="users" id="nav-admin-main">
|
||||
<button class="nav-item" data-tab="admin" data-module="vendoo.users" data-admin-tab="users" id="nav-admin-main">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z"/><path d="M9 12h6M12 9v6"/></svg>
|
||||
<span>Admin</span>
|
||||
</button>
|
||||
@@ -92,11 +92,11 @@
|
||||
|
||||
<div class="nav-group nav-group-tools" id="nav-system-group">
|
||||
<div class="nav-divider"></div>
|
||||
<button class="nav-item nav-item-quiet" data-tab="fees">
|
||||
<button class="nav-item nav-item-quiet" data-tab="fees" data-module="vendoo.publishing">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 7h8M8 11h3M8 15h3M15 11v5M13 13.5h4"/></svg>
|
||||
<span>Gebühren</span>
|
||||
</button>
|
||||
<button class="nav-item nav-item-quiet" data-tab="trash">
|
||||
<button class="nav-item nav-item-quiet" data-tab="trash" data-module="vendoo.listings">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M9 7V4h6v3M7 7l1 14h8l1-14"/></svg>
|
||||
<span>Papierkorb</span>
|
||||
<span id="trash-badge" class="nav-badge hidden">0</span>
|
||||
@@ -145,7 +145,7 @@
|
||||
</button>
|
||||
|
||||
<div class="topbar-actions">
|
||||
<button class="topbar-btn primary" id="topbar-new-btn">
|
||||
<button class="topbar-btn primary" id="topbar-new-btn" data-module="vendoo.ai">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
<span>Neuer Artikel</span>
|
||||
</button>
|
||||
@@ -314,18 +314,39 @@
|
||||
<p>Fotos erfassen, AI gezielt steuern und das Ergebnis ohne Kontextwechsel fertigstellen.</p>
|
||||
</div>
|
||||
<div class="generator-heading-actions">
|
||||
<div class="generator-stepper" aria-label="Arbeitsfortschritt">
|
||||
<span class="generator-step is-active" id="generator-step-photos"><b>1</b> Fotos</span>
|
||||
<span class="generator-step" id="generator-step-ai"><b>2</b> AI</span>
|
||||
<span class="generator-step" id="generator-step-result"><b>3</b> Prüfen</span>
|
||||
</div>
|
||||
<button id="reset-generator-btn" class="vd-button secondary" type="button">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/></svg>
|
||||
Zurücksetzen
|
||||
Formular leeren
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="generator-commandbar" id="generator-commandbar">
|
||||
<div class="generator-command-progress">
|
||||
<div class="generator-stepper" aria-label="Arbeitsfortschritt">
|
||||
<span class="generator-step is-active" id="generator-step-photos"><b>1</b> Fotos</span>
|
||||
<span class="generator-step" id="generator-step-ai"><b>2</b> Generieren</span>
|
||||
<span class="generator-step" id="generator-step-result"><b>3</b> Prüfen</span>
|
||||
</div>
|
||||
<div class="generator-readiness">
|
||||
<span id="generator-readiness-dot"></span>
|
||||
<div>
|
||||
<strong id="generator-readiness-title">Fotos fehlen</strong>
|
||||
<small id="generator-readiness-copy">Lade mindestens ein Produktfoto hoch.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="generator-command-actions">
|
||||
<button id="generate-btn" class="vd-button primary generator-primary-action" disabled>
|
||||
<span aria-hidden="true">✦</span>
|
||||
Artikel generieren
|
||||
</button>
|
||||
<button id="save-btn" class="vd-button generator-save-action" disabled>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="generator-studio-layout">
|
||||
<section class="generator-panel media-workbench" aria-labelledby="generator-media-title">
|
||||
<div class="generator-panel-header">
|
||||
@@ -377,13 +398,13 @@
|
||||
</div>
|
||||
|
||||
|
||||
<section id="generator-ai-model-card" class="ai-model-card generator-ai-model-card">
|
||||
<section id="generator-ai-model-card" class="ai-model-card generator-ai-model-card hidden" data-module-panel="vendoo.product-image-tools">
|
||||
<div class="ai-model-card-head">
|
||||
<div>
|
||||
<span class="generator-panel-index">AI</span>
|
||||
<div>
|
||||
<h4>Produktbild-Werkzeuge</h4>
|
||||
<p>Erzeuge Ghost-Mannequin- oder Flatlay-Varianten aus dem vorhandenen Produktfoto. Freie Prompt-Bilder findest du im separaten FLUX Studio.</p>
|
||||
<h4>Produktbild Studio</h4>
|
||||
<p>Optionales Modul für kontrollierte Ghost-Mannequin- und Flatlay-Varianten. Aktivierung und Deaktivierung erfolgen im Modulmanager.</p>
|
||||
</div>
|
||||
</div>
|
||||
<span id="generator-ai-model-badge" class="generator-beta">Safe</span>
|
||||
@@ -533,19 +554,7 @@
|
||||
<p><strong>AI analysiert deine Fotos</strong><br><span id="generator-loading-copy">Produkt, Zustand und Marktplatzdaten werden vorbereitet.</span></p>
|
||||
</div>
|
||||
|
||||
<div class="generator-action-zone">
|
||||
<div class="generator-readiness">
|
||||
<span id="generator-readiness-dot"></span>
|
||||
<div>
|
||||
<strong id="generator-readiness-title">Fotos fehlen</strong>
|
||||
<small id="generator-readiness-copy">Lade mindestens ein Produktfoto hoch.</small>
|
||||
</div>
|
||||
</div>
|
||||
<button id="generate-btn" class="primary-btn generator-primary-action" disabled>
|
||||
<span>✦</span>
|
||||
Artikel generieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<aside class="generator-panel generator-preview-panel" aria-labelledby="generator-preview-title">
|
||||
@@ -649,7 +658,7 @@
|
||||
|
||||
<div class="result-actions generator-result-actions">
|
||||
<button id="copy-all-btn" class="secondary-btn">Alles kopieren</button>
|
||||
<button id="save-btn" class="primary-btn">Speichern & Hinzufügen</button>
|
||||
<span>Speichern ist dauerhaft oben erreichbar.</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -1122,6 +1131,10 @@
|
||||
<!-- Users Tab -->
|
||||
<div class="admin-panel active" id="admin-users-panel">
|
||||
<div id="users-list" class="users-list"></div>
|
||||
<section class="invitation-admin-section">
|
||||
<div class="security-panel-heading"><div><p class="eyebrow">Testzugänge</p><h3>Einladungen</h3><p>Offene, abgelaufene und widerrufene Einladungen. Standardrolle ist Leser; höhere Rechte müssen bewusst gewählt werden.</p></div></div>
|
||||
<div id="invitations-list" class="invitation-admin-list"><div class="empty-state">Einladungen werden geladen …</div></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Roles Tab -->
|
||||
@@ -1256,27 +1269,45 @@
|
||||
<div class="operation-card-actions"><button type="button" class="secondary-btn" id="operations-update-url-save">URL speichern</button><button type="button" class="secondary-btn" id="operations-update-check">Jetzt prüfen</button></div>
|
||||
<div id="operations-update-check-result" class="operation-result">Noch nicht geprüft.</div>
|
||||
</article>
|
||||
<article class="operations-card operations-production-card">
|
||||
<header><span data-vd-icon="activity"></span><div><h4>Produktionsstatus</h4><p>Live-Überblick über Version, Readiness, Datenbank, Speicher und FLUX-Grenze.</p></div></header>
|
||||
<div id="deployment-production-overview" class="deployment-production-overview"><div class="empty-state">Produktionsstatus wird geladen …</div></div>
|
||||
<div class="operation-card-actions"><button type="button" class="secondary-btn" id="deployment-production-check">Produktionscheck ausführen</button></div>
|
||||
<div id="deployment-production-check-result" class="operation-result">Noch kein Produktionscheck ausgeführt.</div>
|
||||
</article>
|
||||
<article class="operations-card operations-deployment-card">
|
||||
<header><span data-vd-icon="cloud-cog"></span><div><h4>Coolify & GitHub Deployment</h4><p>Neue GitHub-Versionen kontrolliert über Coolify ausrollen. Vendoo erhält keinen Docker-Socket.</p></div></header>
|
||||
<header><span data-vd-icon="cloud-cog"></span><div><h4>Coolify & GitHub Deployment</h4><p>Kontrolliertes Deployment ohne Docker-Socket und ohne selbständernden Produktivcontainer.</p></div></header>
|
||||
<label class="operation-field"><span>Provider</span><select id="deployment-provider"><option value="none">Nicht konfiguriert</option><option value="coolify">Coolify</option></select></label>
|
||||
<label class="operation-field"><span>Coolify URL</span><input id="deployment-coolify-url" type="url" placeholder="https://coolify.example.de"></label>
|
||||
<label class="operation-field"><span>Resource UUID</span><input id="deployment-resource-uuid" type="text" placeholder="Coolify Application UUID"></label>
|
||||
<label class="operation-field"><span>Trigger</span><select id="deployment-trigger-mode"><option value="webhook">Deploy-Webhook (empfohlen)</option><option value="api">Coolify API</option></select></label>
|
||||
<label class="operation-field"><span>Trigger</span><select id="deployment-trigger-mode"><option value="webhook">Deploy-Webhook</option><option value="api">Coolify API mit Statusabfrage</option></select></label>
|
||||
<label class="operation-field"><span>Deployment-Status-Endpunkt</span><input id="deployment-status-endpoint" type="text" value="/api/v1/deployments/{deployment_id}"></label>
|
||||
<div class="operation-inline-fields">
|
||||
<label class="operation-field"><span>Zeitlimit (Sek.)</span><input id="deployment-timeout-seconds" type="number" min="60" max="7200" value="900"></label>
|
||||
<label class="operation-field"><span>Prüfintervall (Sek.)</span><input id="deployment-poll-seconds" type="number" min="5" max="300" value="15"></label>
|
||||
</div>
|
||||
<label class="operation-field"><span>Deploy-Webhook</span><input id="deployment-webhook-url" type="password" autocomplete="new-password" placeholder="Nur neu eingeben, wenn er geändert werden soll"></label>
|
||||
<label class="operation-field"><span>API-Token</span><input id="deployment-api-token" type="password" autocomplete="new-password" placeholder="Nur für API-Modus"></label>
|
||||
<div class="operation-card-actions"><button type="button" class="secondary-btn" id="deployment-save">Konfiguration speichern</button><button type="button" class="secondary-btn" id="deployment-test">Verbindung prüfen</button></div>
|
||||
<div id="deployment-status" class="operation-result">Deployment-Konfiguration wird geladen …</div>
|
||||
</article>
|
||||
<article class="operations-card operations-deploy-action-card">
|
||||
<header><span data-vd-icon="rocket"></span><div><h4>GitHub-Stand ausrollen</h4><p>Coolify holt den konfigurierten Branch und ersetzt den laufenden Container erst nach erfolgreichem Build und Healthcheck.</p></div></header>
|
||||
<header><span data-vd-icon="rocket"></span><div><h4>GitHub-Stand kontrolliert ausrollen</h4><p>Vorab-Backup, Doppelstartschutz, Statusverfolgung, Readiness und Zielversion sind Bestandteil eines Auftrags.</p></div></header>
|
||||
<label class="operation-field"><span>GitHub Repository</span><input id="deployment-github-repository" type="text" value="Masterluke77/vendoo"></label>
|
||||
<label class="operation-field"><span>Produktionsbranch</span><input id="deployment-github-branch" type="text" value="main"></label>
|
||||
<label class="operation-field"><span>Update-Kanal</span><select id="deployment-update-channel"><option value="stable">Stable</option><option value="beta">Beta</option></select></label>
|
||||
<div class="operation-inline-fields">
|
||||
<label class="operation-field"><span>Update-Kanal</span><select id="deployment-update-channel"><option value="stable">Stable</option><option value="beta">Beta</option></select></label>
|
||||
<label class="operation-field"><span>Zielversion</span><input id="deployment-target-version" type="text" placeholder="z. B. 1.43.0"></label>
|
||||
</div>
|
||||
<label class="operation-check"><input id="deployment-backup-before" type="checkbox" checked><span>Vor dem Deployment ein verifiziertes Backup mit Konfiguration erstellen</span></label>
|
||||
<label class="operation-check"><input id="deployment-backup-uploads" type="checkbox"><span>Uploads zusätzlich in das Vorab-Backup aufnehmen</span></label>
|
||||
<div class="operation-card-actions"><button type="button" class="secondary-btn" id="deployment-update-check">Version prüfen</button><button type="button" class="primary-btn" id="deployment-trigger">Coolify-Deployment starten</button></div>
|
||||
<div id="deployment-action-result" class="operation-result">Noch kein Deployment aus dem Webinterface angefordert.</div>
|
||||
</article>
|
||||
<article class="operations-card operations-deployment-history-card">
|
||||
<header><span data-vd-icon="history"></span><div><h4>Deployment-Verlauf</h4><p>Persistente Aufträge und Ereignisse bleiben auch nach einem Neustart nachvollziehbar.</p></div></header>
|
||||
<div id="deployment-run-list" class="deployment-run-list"><div class="empty-state">Keine Deployment-Aufträge vorhanden.</div></div>
|
||||
</article>
|
||||
|
||||
<article class="operations-card operations-update-upload">
|
||||
<header><span data-vd-icon="package-open"></span><div><h4>Update-Paket vorbereiten</h4><p>Prüft Version, Pflichtdateien und verbotene Laufzeitdaten. Vorher wird automatisch gesichert.</p></div></header>
|
||||
@@ -1378,9 +1409,9 @@ PUBLIC_BASE_URL=https://vendoo.example.de</pre>
|
||||
<div class="form-group">
|
||||
<label>Rolle</label>
|
||||
<select id="invite-role" class="model-select">
|
||||
<option value="viewer">Leser</option>
|
||||
<option value="viewer" selected>Leser</option>
|
||||
<option value="publisher">Publisher</option>
|
||||
<option value="editor" selected>Editor</option>
|
||||
<option value="editor">Editor</option>
|
||||
<option value="manager">Manager</option>
|
||||
<option value="admin">Administrator</option>
|
||||
</select>
|
||||
@@ -2361,11 +2392,11 @@ PUBLIC_BASE_URL=https://vendoo.example.de</pre>
|
||||
<div id="toast" class="toast"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/qrious@4.0.2/dist/qrious.min.js"></script>
|
||||
<script src="design-system/theme-runtime.js?v=1.41.2"></script>
|
||||
<script src="vendoo-icons.js?v=1.41.2"></script>
|
||||
<script src="app.js?v=1.41.2"></script>
|
||||
<script type="module" src="modules/theme-manager/theme-manager.js?v=1.41.2"></script>
|
||||
<script type="module" src="modules/module-manager/module-manager.js?v=1.41.2"></script>
|
||||
<script src="design-system/theme-runtime.js?v=1.43.0"></script>
|
||||
<script src="vendoo-icons.js?v=1.43.0"></script>
|
||||
<script src="app.js?v=1.43.0"></script>
|
||||
<script type="module" src="modules/theme-manager/theme-manager.js?v=1.43.0"></script>
|
||||
<script type="module" src="modules/module-manager/module-manager.js?v=1.43.0"></script>
|
||||
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -106,6 +106,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/login.js?v=1.41.2" defer></script>
|
||||
<script src="/login.js?v=1.43.0" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -150,6 +150,7 @@ async function perform(action, id) {
|
||||
await request('DELETE', `/api/admin/modules/${encodeURIComponent(id)}`);
|
||||
}
|
||||
await load();
|
||||
window.dispatchEvent(new CustomEvent('vendoo:modules-changed', { detail: { action, id } }));
|
||||
globalThis.toast?.(action === 'enable' ? 'Modul aktiviert.' : action === 'disable' ? 'Modul deaktiviert.' : 'Modul gelöscht.');
|
||||
} catch (error) {
|
||||
globalThis.toast?.(error.message);
|
||||
@@ -168,6 +169,7 @@ async function install() {
|
||||
await request('POST', '/api/admin/modules/install', form, { multipart: true });
|
||||
input.value = '';
|
||||
await load();
|
||||
window.dispatchEvent(new CustomEvent('vendoo:modules-changed', { detail: { action: 'install' } }));
|
||||
globalThis.toast?.('Modulpaket geprüft und installiert.');
|
||||
} catch (error) {
|
||||
globalThis.toast?.(error.message);
|
||||
|
||||
+153
-1
@@ -7165,6 +7165,29 @@ button,input,select,textarea { max-width:100%; }
|
||||
.operation-card-actions { display: flex; gap: 7px; flex-wrap: wrap; }
|
||||
.operation-result { display: grid; gap: 3px; min-height: 52px; margin-top: 10px; padding: 10px; border-radius: 9px; background: var(--bg-page); color: var(--text-muted); font-size: 10px; }
|
||||
.operation-result strong { color: var(--text); font-size: 12px; }
|
||||
|
||||
.deployment-production-overview { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
|
||||
.deployment-production-item { display: grid; grid-template-columns: 28px minmax(0, 1fr); gap: 8px; align-items: start; padding: 9px; border: 1px solid var(--border); border-radius: 9px; background: var(--bg-page); min-width: 0; }
|
||||
.deployment-production-item > span { width: 28px; height: 28px; display: grid; place-items: center; border-radius: 8px; color: var(--warning, #d97706); background: color-mix(in srgb, var(--warning, #d97706) 10%, transparent); }
|
||||
.deployment-production-item.is-ok > span { color: var(--success, #20875d); background: color-mix(in srgb, var(--success, #20875d) 10%, transparent); }
|
||||
.deployment-production-item small, .deployment-production-item strong, .deployment-production-item p { display: block; min-width: 0; overflow-wrap: anywhere; }
|
||||
.deployment-production-item small { color: var(--text-muted); font-size: 8px; text-transform: uppercase; letter-spacing: .04em; }
|
||||
.deployment-production-item strong { margin-top: 2px; font-size: 11px; }
|
||||
.deployment-production-item p { margin-top: 2px; font-size: 9px; line-height: 1.35; }
|
||||
.deployment-run-list { display: grid; gap: 8px; max-height: 430px; overflow: auto; }
|
||||
.deployment-run { padding: 10px; border: 1px solid var(--border); border-left-width: 3px; border-radius: 10px; background: var(--bg-page); }
|
||||
.deployment-run.is-ok { border-left-color: var(--success, #20875d); }
|
||||
.deployment-run.is-failed { border-left-color: var(--danger, #c43f4b); }
|
||||
.deployment-run.is-running { border-left-color: var(--accent); }
|
||||
.deployment-run-head { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 5px; }
|
||||
.deployment-run-state { font-size: 9px; font-weight: 800; text-transform: uppercase; letter-spacing: .035em; color: var(--accent); }
|
||||
.deployment-run.is-ok .deployment-run-state { color: var(--success, #20875d); }
|
||||
.deployment-run.is-failed .deployment-run-state { color: var(--danger, #c43f4b); }
|
||||
.deployment-run time, .deployment-run small { color: var(--text-muted); font-size: 8px; }
|
||||
.deployment-run > strong { display: block; font-size: 12px; }
|
||||
.deployment-run > p { margin: 3px 0; font-size: 10px; line-height: 1.4; }
|
||||
.deployment-run-actions { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; }
|
||||
.secondary-btn.compact { min-height: 29px; padding: 0 9px; font-size: 9px; }
|
||||
.operations-instruction { display: flex; gap: 11px; padding: 13px; margin-top: 14px; border-radius: 12px; background: color-mix(in srgb, var(--accent) 7%, var(--bg-card)); }
|
||||
.operations-instruction > span { color: var(--accent); }
|
||||
.operations-instruction strong, .operations-instruction p { display: block; margin: 0; }
|
||||
@@ -7207,7 +7230,7 @@ button,input,select,textarea { max-width:100%; }
|
||||
.operations-pending .secondary-btn { width: 100%; }
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
.operation-inline-fields, .operations-extension-packages { grid-template-columns: minmax(0, 1fr); }
|
||||
.operation-inline-fields, .operations-extension-packages, .deployment-production-overview { grid-template-columns: minmax(0, 1fr); }
|
||||
.operations-backup-row { grid-template-columns: 32px minmax(0, 1fr); }
|
||||
.operation-backup-actions { grid-column: 1 / -1; justify-content: flex-end; }
|
||||
.operations-extension-client { grid-template-columns: 10px minmax(0, 1fr); }
|
||||
@@ -7379,3 +7402,132 @@ body[data-user-role="publisher"] .danger-zone { display: none !important; }
|
||||
/* 1.37.0 – Security Dashboard Foundation */
|
||||
.security-secret-list{display:flex;flex-wrap:wrap;gap:var(--vd-space-2,8px);margin-top:var(--vd-space-3,12px)}
|
||||
.security-secret-list .status-badge{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
|
||||
/* Vendoo 1.42.0 — kontrollierte Testeinladungen */
|
||||
.invitation-admin-section { margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--border); }
|
||||
.invitation-admin-list { display: grid; gap: 8px; }
|
||||
.invitation-admin-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: center; padding: 11px 12px; border: 1px solid var(--border); border-left: 3px solid var(--text-muted); border-radius: 11px; background: var(--bg-card); }
|
||||
.invitation-admin-row.is-pending { border-left-color: var(--accent); }
|
||||
.invitation-admin-row strong, .invitation-admin-row span, .invitation-admin-row small { display: block; min-width: 0; overflow-wrap: anywhere; }
|
||||
.invitation-admin-row strong { font-size: 12px; }
|
||||
.invitation-admin-row span { margin-top: 2px; color: var(--text-muted); font-size: 10px; }
|
||||
.invitation-admin-row small { margin-top: 2px; color: var(--text-muted); font-size: 9px; }
|
||||
.invitation-admin-actions { display: flex; gap: 6px; }
|
||||
@media (max-width: 620px) {
|
||||
.invitation-admin-row { grid-template-columns: minmax(0, 1fr); }
|
||||
.invitation-admin-actions { justify-content: flex-end; }
|
||||
}
|
||||
|
||||
|
||||
/* ==========================================================================
|
||||
Vendoo 1.43.0 – Module-aware Listing Studio
|
||||
========================================================================== */
|
||||
.generator-studio-page {
|
||||
--generator-gap: clamp(14px, 1.4vw, 22px);
|
||||
padding-bottom: 36px;
|
||||
}
|
||||
.generator-heading {
|
||||
margin-bottom: 12px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.generator-heading h2 { font-size: clamp(24px, 2.2vw, 34px); }
|
||||
.generator-heading > div:first-child > p:last-child { max-width: 720px; font-size: 13px; line-height: 1.55; }
|
||||
.generator-commandbar {
|
||||
position: sticky;
|
||||
top: calc(var(--vd-topbar-height) + 8px);
|
||||
z-index: 34;
|
||||
min-height: 72px;
|
||||
margin-bottom: var(--generator-gap);
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
border: 1px solid var(--vd-line);
|
||||
border-radius: var(--vd-radius-md, 14px);
|
||||
background: color-mix(in srgb, var(--vd-surface-raised) 92%, transparent);
|
||||
box-shadow: 0 12px 34px color-mix(in srgb, var(--vd-ink) 9%, transparent);
|
||||
backdrop-filter: blur(18px) saturate(1.15);
|
||||
}
|
||||
.generator-command-progress { min-width: 0; display: flex; align-items: center; gap: 18px; }
|
||||
.generator-command-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
|
||||
.generator-command-actions .vd-button { min-height: 44px; padding-inline: 18px; font-weight: 760; }
|
||||
.generator-save-action { min-width: 148px; border-color: var(--vd-brand); color: var(--vd-brand); background: var(--vd-surface-raised); }
|
||||
.generator-save-action:not(:disabled):hover { background: var(--vd-brand-soft); }
|
||||
.generator-commandbar .generator-readiness { min-width: 230px; grid-template-columns: 11px minmax(0, 1fr); }
|
||||
.generator-commandbar .generator-readiness > span { width: 9px; height: 9px; box-shadow: 0 0 0 5px color-mix(in srgb, var(--vd-warning) 14%, transparent); }
|
||||
.generator-commandbar .generator-readiness.is-ready > span { box-shadow: 0 0 0 5px color-mix(in srgb, var(--vd-success) 14%, transparent); }
|
||||
.generator-commandbar .generator-readiness strong { font-size: 12px; }
|
||||
.generator-commandbar .generator-readiness small { margin-top: 3px; font-size: 10px; }
|
||||
.generator-studio-layout {
|
||||
grid-template-columns: minmax(320px, .88fr) minmax(420px, 1.12fr);
|
||||
gap: var(--generator-gap);
|
||||
}
|
||||
.generator-panel {
|
||||
border-radius: var(--vd-radius-md, 14px);
|
||||
box-shadow: 0 8px 26px color-mix(in srgb, var(--vd-ink) 5%, transparent);
|
||||
}
|
||||
.generator-panel-header { min-height: 76px; padding: 16px 18px; }
|
||||
.generator-panel-header h3 { font-size: 15px; }
|
||||
.generator-panel-header p { margin-top: 4px; font-size: 11px; line-height: 1.45; }
|
||||
.generator-panel-index { width: 34px; height: 34px; font-size: 10px; }
|
||||
.generator-count, .generator-beta, .generator-preview-status { min-height: 26px; padding: 5px 10px; font-size: 10px; }
|
||||
.generator-photo-stage { margin: 16px 16px 12px; border-radius: 12px; }
|
||||
.generator-dropzone { margin-inline: 16px; min-height: 108px; padding: 16px; border-radius: 12px; }
|
||||
.generator-dropzone p { font-size: 11px; }
|
||||
.generator-dropzone p strong { font-size: 13px; }
|
||||
.generator-dropzone small { font-size: 10px; }
|
||||
.generator-photo-strip, .generator-image-tools { margin-inline: 16px; }
|
||||
.generator-photo-quality { margin: 14px 16px 4px; padding: 14px; border-radius: 12px; }
|
||||
.generator-photo-quality-head strong { font-size: 12px; }
|
||||
.generator-photo-quality li { font-size: 10px; }
|
||||
.generator-config-stack { padding: 14px; gap: 11px; }
|
||||
.generator-config-card, .generator-notes-card { padding: 14px; border-radius: 12px; }
|
||||
.generator-config-copy label, .generator-notes-head label { font-size: 12px; }
|
||||
.generator-config-copy span, .generator-notes-head span { font-size: 10px; }
|
||||
.generator-choice-group .toggle-btn { min-height: 34px; padding-inline: 12px; font-size: 10px; }
|
||||
.generator-config-card select { min-height: 40px; font-size: 11px; }
|
||||
.generator-notes-card textarea { min-height: 118px; font-size: 11px; }
|
||||
.generator-tone-row span { font-size: 9px; }
|
||||
.generator-loading { margin: 0 14px 14px; }
|
||||
.generator-action-zone { display: none; }
|
||||
.generator-preview-panel {
|
||||
grid-column: 1 / -1;
|
||||
position: static;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
.generator-preview-empty { min-height: 300px; }
|
||||
.generator-result { padding: 18px; }
|
||||
.generator-result .result-input { min-height: 42px; font-size: 13px; }
|
||||
.generator-result .ql-container.ql-snow { min-height: 280px; font-size: 13px; }
|
||||
.generator-meta-section .meta-input { min-height: 38px; font-size: 11px; }
|
||||
.generator-result-actions {
|
||||
position: static;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding-top: 14px;
|
||||
color: var(--vd-ink-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
.generator-result-actions button { min-width: 140px; }
|
||||
[data-module].hidden, [data-module-panel].hidden { display: none !important; }
|
||||
@media (max-width: 1100px) {
|
||||
.generator-commandbar { align-items: stretch; flex-direction: column; }
|
||||
.generator-command-progress, .generator-command-actions { width: 100%; justify-content: space-between; }
|
||||
.generator-studio-layout { grid-template-columns: 1fr; }
|
||||
.generator-preview-panel { grid-column: auto; }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.generator-commandbar { top: calc(var(--vd-topbar-height) + 4px); padding: 10px; }
|
||||
.generator-command-progress { align-items: stretch; flex-direction: column; gap: 10px; }
|
||||
.generator-commandbar .generator-readiness { min-width: 0; }
|
||||
.generator-command-actions { display: grid; grid-template-columns: 1fr 1fr; }
|
||||
.generator-command-actions .vd-button { min-width: 0; padding-inline: 10px; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.generator-command-actions { grid-template-columns: 1fr; }
|
||||
.generator-step { font-size: 9px; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user