This commit is contained in:
2026-03-22 12:17:01 +01:00
parent 7afca6ed04
commit a30e83a724
210 changed files with 318 additions and 2 deletions

View File

@@ -47,4 +47,21 @@ export const settingsApi = {
}),
backupSamba: () =>
client.post<{ ok: boolean; fichier: string; chemin: string }>('/api/settings/backup/samba').then(r => r.data),
resizeAllImages: () =>
client.post<{ ok: boolean; redimensionnees: number; ignorees: number; erreurs: number; message?: string }>(
'/api/settings/images/resize-all'
).then(r => r.data),
restoreBackup: (file: File, overwrite: boolean) => {
const form = new FormData()
form.append('file', file)
form.append('overwrite', String(overwrite))
return client.post<{
ok: boolean
uploads_copies: number
uploads_ignores: number
db_restauree: boolean
db_lignes_ajoutees: number
erreurs: number
}>('/api/settings/backup/restore', form).then(r => r.data)
},
}

View File

@@ -138,6 +138,52 @@
<div v-if="backupMsg" class="text-[10px] text-center font-bold text-aqua animate-bounce">{{ backupMsg }}</div>
</div>
<!-- Restauration -->
<div class="mt-6 pt-5 border-t border-bg-hard space-y-4">
<div class="flex items-center gap-2">
<span class="text-base">🔁</span>
<span class="text-[10px] font-black uppercase tracking-widest text-text-muted">Restaurer une sauvegarde</span>
</div>
<label class="flex items-center gap-3 cursor-pointer group">
<div class="relative">
<input v-model="restoreOverwrite" type="checkbox" class="sr-only peer" />
<div class="w-8 h-4 bg-bg-hard rounded-full peer peer-checked:bg-red/70 transition-colors"></div>
<div class="absolute left-1 top-1 w-2 h-2 bg-text-muted peer-checked:bg-bg peer-checked:translate-x-4 rounded-full transition-all"></div>
</div>
<span class="text-[10px] text-text-muted group-hover:text-text transition-colors leading-tight">
Écraser les données existantes<br>
<span class="text-[9px] italic">Si décoché : ajoute uniquement les nouveaux éléments</span>
</span>
</label>
<div class="flex gap-2">
<label class="flex-1 cursor-pointer">
<div :class="[
'py-2 px-3 rounded-xl border text-[10px] font-bold text-center truncate transition-all',
restoreFile ? 'border-yellow/50 text-yellow bg-yellow/10' : 'border-bg-soft text-text-muted hover:border-yellow/30'
]">
{{ restoreFile ? restoreFile.name : 'Choisir un fichier .zip' }}
</div>
<input type="file" accept=".zip" class="hidden" @change="onRestoreFileSelected" />
</label>
<button
:disabled="!restoreFile || restoringBackup"
@click="confirmAndRestore"
:class="[
'btn-primary !py-2 !px-4 text-xs shrink-0 transition-all',
restoreOverwrite ? '!bg-red !text-bg' : '!bg-yellow !text-bg'
]"
>
{{ restoringBackup ? '' : '' }}
</button>
</div>
<div v-if="restoreMsg" :class="['text-[10px] font-bold p-2 rounded-lg text-center', restoreError ? 'text-red bg-red/10' : 'text-green bg-green/10']">
{{ restoreMsg }}
</div>
</div>
</section>
<!-- Section Images -->
@@ -175,7 +221,21 @@
</div>
</div>
<div class="mt-8 pt-4 border-t border-bg-hard flex items-center justify-end gap-3">
<div class="mt-6 space-y-3">
<button
class="btn-outline w-full py-3 flex items-center justify-center gap-2 border-orange/30 text-orange hover:bg-orange/10 text-xs"
:disabled="resizingAll || imageMaxWidth === 0"
@click="resizeAllImages"
>
<span>{{ resizingAll ? '⏳' : '🔄' }}</span>
<span class="font-black uppercase tracking-widest text-[10px]">{{ resizingAll ? 'Traitement...' : 'Appliquer à la bibliothèque existante' }}</span>
</button>
<div v-if="resizeAllMsg" :class="['text-[10px] text-center font-bold p-2 rounded-lg', resizeAllError ? 'text-red bg-red/10' : 'text-green bg-green/10']">
{{ resizeAllMsg }}
</div>
</div>
<div class="mt-4 pt-4 border-t border-bg-hard flex items-center justify-end gap-3">
<span v-if="imageSavedMsg" class="text-[10px] font-bold text-aqua">{{ imageSavedMsg }}</span>
<button
class="btn-primary !bg-orange !text-bg !py-2 !px-6 text-xs"
@@ -313,6 +373,45 @@ const imageMaxWidthLabel = computed(() => {
})
const savingImage = ref(false)
const imageSavedMsg = ref('')
const resizingAll = ref(false)
const resizeAllMsg = ref('')
const resizeAllError = ref(false)
// --- Restauration ---
const restoreFile = ref<File | null>(null)
const restoreOverwrite = ref(true)
const restoringBackup = ref(false)
const restoreMsg = ref('')
const restoreError = ref(false)
function onRestoreFileSelected(e: Event) {
const input = e.target as HTMLInputElement
restoreFile.value = input.files?.[0] ?? null
}
async function confirmAndRestore() {
if (!restoreFile.value) return
const mode = restoreOverwrite.value ? 'ÉCRASER les données existantes' : 'ajouter uniquement les nouveaux éléments'
if (!window.confirm(`Restaurer "${restoreFile.value.name}" ?\n\nMode : ${mode}.\n\nCette opération est irréversible.`)) return
restoringBackup.value = true
restoreMsg.value = ''
restoreError.value = false
try {
const res = await settingsApi.restoreBackup(restoreFile.value, restoreOverwrite.value)
const parts = [`${res.uploads_copies} fichier(s) restauré(s)`]
if (res.uploads_ignores) parts.push(`${res.uploads_ignores} ignoré(s)`)
if (res.db_restauree) parts.push(restoreOverwrite.value ? 'BDD remplacée' : `${res.db_lignes_ajoutees} ligne(s) BDD ajoutée(s)`)
if (res.erreurs) parts.push(`${res.erreurs} erreur(s)`)
restoreMsg.value = parts.join(' · ')
restoreFile.value = null
} catch (err: any) {
restoreError.value = true
restoreMsg.value = err?.response?.data?.detail || 'Erreur lors de la restauration.'
} finally {
restoringBackup.value = false
setTimeout(() => { restoreMsg.value = '' }, 6000)
}
}
// --- Samba ---
const samba = ref({ serveur: '', partage: '', sous_dossier: '', utilisateur: '', motdepasse: '' })
@@ -421,6 +520,26 @@ async function loadSettings() {
}
}
async function resizeAllImages() {
resizingAll.value = true
resizeAllMsg.value = ''
resizeAllError.value = false
try {
const res = await settingsApi.resizeAllImages()
if (res.message) {
resizeAllMsg.value = res.message
} else {
resizeAllMsg.value = `${res.redimensionnees} redimensionnée(s), ${res.ignorees} ignorée(s)${res.erreurs ? `, ${res.erreurs} erreur(s)` : ''}`
}
} catch (err: any) {
resizeAllError.value = true
resizeAllMsg.value = err?.response?.data?.detail || 'Erreur lors du redimensionnement.'
} finally {
resizingAll.value = false
setTimeout(() => { resizeAllMsg.value = '' }, 5000)
}
}
async function saveImageSettings() {
savingImage.value = true
imageSavedMsg.value = ''