Add clone support for scene and automation cards, update sync clock descriptions

- Scene clone: opens capture modal with prefilled name/description/targets
  instead of server-side duplication; removed backend clone endpoint
- Automation clone: opens editor with prefilled conditions, scene, logic,
  deactivation mode (webhook tokens stripped for uniqueness)
- Updated sync clock i18n descriptions to reflect speed-only-on-clock model
- Added entity card clone pattern documentation to server/CLAUDE.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 22:47:11 +03:00
parent bc5d8fdc9b
commit a330a8c0f0
9 changed files with 115 additions and 65 deletions

View File

@@ -185,6 +185,7 @@ function createAutomationCard(automation, sceneMap = new Map()) {
</div>
<div class="stream-card-props">${condPills}</div>`,
actions: `
<button class="btn btn-icon btn-secondary" onclick="cloneAutomation('${automation.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
<button class="btn btn-icon btn-secondary" onclick="openAutomationEditor('${automation.id}')" title="${t('automations.edit')}">${ICON_SETTINGS}</button>
<button class="btn btn-icon ${automation.enabled ? 'btn-warning' : 'btn-success'}" onclick="toggleAutomationEnabled('${automation.id}', ${!automation.enabled})" title="${automation.enabled ? t('automations.action.disable') : t('automations.status.active')}">
${automation.enabled ? ICON_PAUSE : ICON_START}
@@ -192,7 +193,7 @@ function createAutomationCard(automation, sceneMap = new Map()) {
});
}
export async function openAutomationEditor(automationId) {
export async function openAutomationEditor(automationId, cloneData) {
const modal = document.getElementById('automation-editor-modal');
const titleEl = document.getElementById('automation-editor-title');
const idInput = document.getElementById('automation-editor-id');
@@ -241,6 +242,26 @@ export async function openAutomationEditor(automationId) {
showToast(e.message, 'error');
return;
}
} else if (cloneData) {
// Clone mode — create with prefilled data
titleEl.innerHTML = `${ICON_AUTOMATION} ${t('automations.add')}`;
idInput.value = '';
nameInput.value = (cloneData.name || '') + ' (Copy)';
enabledInput.checked = cloneData.enabled !== false;
logicSelect.value = cloneData.condition_logic || 'or';
// Clone conditions (strip webhook tokens — they must be unique)
for (const c of (cloneData.conditions || [])) {
const clonedCond = { ...c };
if (clonedCond.condition_type === 'webhook') delete clonedCond.token;
addAutomationConditionRow(clonedCond);
}
_initSceneSelector('automation-scene', cloneData.scene_preset_id);
document.getElementById('automation-deactivation-mode').value = cloneData.deactivation_mode || 'none';
_onDeactivationModeChange();
_initSceneSelector('automation-fallback-scene', cloneData.deactivation_scene_preset_id);
} else {
titleEl.innerHTML = `${ICON_AUTOMATION} ${t('automations.add')}`;
idInput.value = '';
@@ -729,6 +750,18 @@ export function copyWebhookUrl(btn) {
});
}
export async function cloneAutomation(automationId) {
try {
const resp = await fetchWithAuth(`/automations/${automationId}`);
if (!resp.ok) throw new Error('Failed to load automation');
const automation = await resp.json();
openAutomationEditor(null, automation);
} catch (e) {
if (e.isAuth) return;
showToast(t('automations.error.clone_failed'), 'error');
}
}
export async function deleteAutomation(automationId, automationName) {
const msg = t('automations.delete.confirm').replace('{name}', automationName);
const confirmed = await showConfirm(msg);