|
| 1 | +<template> |
| 2 | + <div class="max-w-2xl space-y-6"> |
| 3 | + <!-- Header --> |
| 4 | + <div> |
| 5 | + <h3 class="text-sm font-semibold text-arena-100">Backup & Restore</h3> |
| 6 | + <p class="text-xs text-arena-500 mt-1">Download a full backup of your data or restore from a previous backup file.</p> |
| 7 | + </div> |
| 8 | + |
| 9 | + <!-- Backup --> |
| 10 | + <Card color="blue"> |
| 11 | + <div class="flex items-start gap-4"> |
| 12 | + <div class="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 bg-blue-500/10"> |
| 13 | + <Icon name="download" size="md" class="text-blue-400" /> |
| 14 | + </div> |
| 15 | + <div class="flex-1 min-w-0"> |
| 16 | + <h4 class="text-[13px] font-medium text-arena-100">Download Backup</h4> |
| 17 | + <p class="text-xs text-arena-500 mt-0.5"> |
| 18 | + Exports all agents, flows, skills, backends, clients, secrets, commands, memory providers, and conversations as a <code class="text-arena-400 bg-piedra-800/80 px-1 rounded">.tar.gz</code> archive. |
| 19 | + </p> |
| 20 | + <button |
| 21 | + @click="onBackup" |
| 22 | + :disabled="backupLoading" |
| 23 | + class="mt-3 px-4 py-1.5 bg-blue-500/15 hover:bg-blue-500/25 text-blue-300 text-xs font-medium rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed" |
| 24 | + > |
| 25 | + {{ backupLoading ? 'Downloading...' : 'Download Backup' }} |
| 26 | + </button> |
| 27 | + </div> |
| 28 | + </div> |
| 29 | + </Card> |
| 30 | + |
| 31 | + <!-- Restore --> |
| 32 | + <Card color="blue"> |
| 33 | + <div class="flex items-start gap-4"> |
| 34 | + <div class="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 bg-blue-500/10"> |
| 35 | + <Icon name="upload" size="md" class="text-blue-400" /> |
| 36 | + </div> |
| 37 | + <div class="flex-1 min-w-0"> |
| 38 | + <h4 class="text-[13px] font-medium text-arena-100">Restore from Backup</h4> |
| 39 | + <p class="text-xs text-arena-500 mt-0.5"> |
| 40 | + Upload a previously downloaded <code class="text-arena-400 bg-piedra-800/80 px-1 rounded">.tar.gz</code> backup to replace all current data. This action cannot be undone. |
| 41 | + </p> |
| 42 | + <div class="flex items-center gap-3 mt-3"> |
| 43 | + <button |
| 44 | + @click="triggerRestore" |
| 45 | + :disabled="restoreLoading" |
| 46 | + class="px-4 py-1.5 bg-blue-500/15 hover:bg-blue-500/25 text-blue-300 text-xs font-medium rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed" |
| 47 | + > |
| 48 | + {{ restoreLoading ? 'Restoring...' : 'Upload & Restore' }} |
| 49 | + </button> |
| 50 | + <span v-if="restoreFile" class="text-[11px] text-arena-400 truncate">{{ restoreFile.name }}</span> |
| 51 | + </div> |
| 52 | + <input ref="fileInput" type="file" accept=".tar.gz,.tgz" class="hidden" @change="onFileSelected" /> |
| 53 | + </div> |
| 54 | + </div> |
| 55 | + </Card> |
| 56 | + |
| 57 | + <!-- Warning --> |
| 58 | + <div class="flex items-start gap-3 p-3 rounded-lg bg-lava-500/5 border border-lava-500/10"> |
| 59 | + <svg class="w-4 h-4 text-lava-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 60 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> |
| 61 | + </svg> |
| 62 | + <div> |
| 63 | + <p class="text-xs font-medium text-lava-300">Restoring replaces everything</p> |
| 64 | + <p class="text-[11px] text-lava-400/70 mt-0.5">All current agents, flows, skills, conversations, secrets, and configuration will be overwritten. Consider downloading a backup first.</p> |
| 65 | + </div> |
| 66 | + </div> |
| 67 | + </div> |
| 68 | +</template> |
| 69 | + |
| 70 | +<script setup> |
| 71 | +import { ref, inject } from 'vue' |
| 72 | +import { backupApi } from '../../lib/api/index.js' |
| 73 | +import { useDataStore } from '../../lib/stores/data.js' |
| 74 | +import Card from '../../components/Card.vue' |
| 75 | +import Icon from '../../components/Icon.vue' |
| 76 | +
|
| 77 | +const store = useDataStore() |
| 78 | +const toast = inject('toast') |
| 79 | +const requestDelete = inject('requestDelete') |
| 80 | +
|
| 81 | +const backupLoading = ref(false) |
| 82 | +const restoreLoading = ref(false) |
| 83 | +const restoreFile = ref(null) |
| 84 | +const fileInput = ref(null) |
| 85 | +
|
| 86 | +async function onBackup() { |
| 87 | + backupLoading.value = true |
| 88 | + try { |
| 89 | + await backupApi.download() |
| 90 | + toast.success('Backup downloaded') |
| 91 | + } catch (e) { |
| 92 | + toast.error('Backup failed: ' + e.message) |
| 93 | + } finally { |
| 94 | + backupLoading.value = false |
| 95 | + } |
| 96 | +} |
| 97 | +
|
| 98 | +function triggerRestore() { |
| 99 | + fileInput.value.value = '' |
| 100 | + fileInput.value.click() |
| 101 | +} |
| 102 | +
|
| 103 | +function onFileSelected(e) { |
| 104 | + const file = e.target.files?.[0] |
| 105 | + if (!file) return |
| 106 | + restoreFile.value = file |
| 107 | +
|
| 108 | + requestDelete( |
| 109 | + 'This will replace ALL data (agents, flows, skills, conversations, secrets). Are you sure?', |
| 110 | + async () => { |
| 111 | + restoreLoading.value = true |
| 112 | + try { |
| 113 | + await backupApi.restore(file) |
| 114 | + toast.success('Backup restored — reloading data') |
| 115 | + store.refresh() |
| 116 | + } catch (err) { |
| 117 | + toast.error('Restore failed: ' + err.message) |
| 118 | + } finally { |
| 119 | + restoreLoading.value = false |
| 120 | + restoreFile.value = null |
| 121 | + } |
| 122 | + } |
| 123 | + ) |
| 124 | +} |
| 125 | +</script> |
0 commit comments