Merge branch 'develop' into feat/system-metrics

# Conflicts:
#	pages/index.vue
This commit is contained in:
2026-03-13 10:24:14 +01:00
24 changed files with 2795 additions and 811 deletions

View File

@@ -22,6 +22,13 @@
</div>
</div>
<div v-else-if="errorMessage" class="empty-state error-state">
<IconifyIcon icon="mdi:alert-circle-outline" class="text-3xl text-m-error/70" />
<p class="mt-2 font-mono text-xs text-m-error/80">
{{ errorMessage }}
</p>
</div>
<div v-else-if="backups.length === 0" class="empty-state">
<IconifyIcon icon="mdi:file-hidden" class="text-3xl text-m-muted/40" />
<p class="mt-2 font-mono text-xs text-m-muted/50">
@@ -55,6 +62,7 @@
import {Icon as IconifyIcon} from "@iconify/vue"
import CircleSkeleton from "~/components/skeleton/CircleSkeleton.vue"
import TextSkeleton from "~/components/skeleton/TextSkeleton.vue"
import { apiFetch, downloadApiFile } from "~/composables/useApiAuth"
const props = defineProps<{
folder: string | null
@@ -62,31 +70,42 @@ const props = defineProps<{
const backups = ref<string[]>([])
const loading = ref(false)
const errorMessage = ref("")
const title = computed(() => {
if (!props.folder) return "Fichiers"
return `Backup — ${props.folder.toUpperCase()}`
})
const downloadBackup = (file: string) => {
const downloadBackup = async (file: string) => {
if (!props.folder) return
const url = `/api/download?folder=${encodeURIComponent(props.folder)}&file=${encodeURIComponent(file)}`
window.location.href = url
errorMessage.value = ""
try {
await downloadApiFile(url, file)
} catch (error) {
console.error("Erreur telechargement backup:", error)
errorMessage.value = "Erreur lors de l'opération"
}
}
watch(() => props.folder, async (folder) => {
if (!folder) {
loading.value = false
backups.value = []
errorMessage.value = ""
return
}
loading.value = true
errorMessage.value = ""
try {
const data = await $fetch<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`)
const data = await apiFetch<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`)
backups.value = data
} catch (error) {
console.error("Erreur récupération backups:", error)
backups.value = []
errorMessage.value = "Erreur lors de l'opération"
} finally {
loading.value = false
}
@@ -120,6 +139,12 @@ watch(() => props.folder, async (folder) => {
padding: 2.5rem 1rem;
}
.error-state {
border-radius: 8px;
border: 1px solid rgb(var(--m-error) / 0.12);
background: rgb(var(--m-error) / 0.06);
}
.file-list {
display: flex;
flex-direction: column;

View File

@@ -79,6 +79,8 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue"
import { Icon as IconifyIcon } from "@iconify/vue"
import { apiFetch } from "~/composables/useApiAuth"
import { useApiAuthHeader } from "~/composables/useApiAuth"
type BackupScript = {
key: string
@@ -118,6 +120,7 @@ const scripts = ref<BackupScript[]>([])
const output = ref<string>("")
const message = ref<string>("")
const isError = ref(false)
const apiAuthHeader = useApiAuthHeader()
const statusClass = computed(() => (isError.value ? "status-error" : "status-success"))
@@ -134,12 +137,12 @@ const loadScripts = async () => {
downloadFolders: []
})
try {
const data = await $fetch<BackupScriptListResponse>("/api/backup-script")
const data = await apiFetch<BackupScriptListResponse>("/api/backup-script")
scripts.value = data.scripts
} catch (error) {
} catch {
scripts.value = []
isError.value = true
message.value = `Erreur chargement scripts: ${error instanceof Error ? error.message : String(error)}`
message.value = "Erreur lors de l'opération"
emit("result", {
key: null,
label: "",
@@ -160,7 +163,7 @@ const runScript = async (key: string) => {
isError.value = false
try {
const data = await $fetch<BackupScriptRunResponse>("/api/backup-script", {
const data = await apiFetch<BackupScriptRunResponse>("/api/backup-script", {
method: "POST",
body: { key }
})
@@ -173,8 +176,20 @@ const runScript = async (key: string) => {
isError: false,
downloadFolders: data.downloadFolders || []
})
} catch (error: any) {
} catch (error: unknown) {
isError.value = true
const statusMessage =
typeof error === "object" &&
error !== null &&
"data" in error &&
typeof error.data === "object" &&
error.data !== null &&
"statusMessage" in error.data &&
typeof error.data.statusMessage === "string"
? error.data.statusMessage
: null
message.value = statusMessage || "Erreur lors de l'opération"
message.value = error?.data?.statusMessage || "Erreur execution script"
output.value = ""
emit("result", {

View File

@@ -1,6 +1,11 @@
<script setup>
import {Icon as IconifyIcon} from "@iconify/vue"
const { data: messages } = await useFetch('/api/discord/messages')
import { apiFetch } from "~/composables/useApiAuth"
const { data: messages, error } = await useFetch('/api/discord/messages', {
$fetch: apiFetch,
server: false
})
</script>
<template>
@@ -13,7 +18,14 @@ const { data: messages } = await useFetch('/api/discord/messages')
<span class="font-mono text-[10px] text-m-muted tracking-widest uppercase">Messages</span>
</div>
<div v-if="!messages || messages.length === 0" class="empty-state">
<div v-if="error" class="empty-state error-state">
<IconifyIcon icon="mdi:alert-circle-outline" class="text-3xl text-m-error/70" />
<p class="mt-2 font-mono text-xs text-m-error/80">
Erreur lors de l'opération
</p>
</div>
<div v-else-if="!messages || messages.length === 0" class="empty-state">
<IconifyIcon icon="mdi:chat-outline" class="text-3xl text-m-muted/40" />
<p class="mt-2 font-mono text-xs text-m-muted/50">
Aucun message
@@ -74,6 +86,12 @@ const { data: messages } = await useFetch('/api/discord/messages')
padding: 2rem 1rem;
}
.error-state {
border-radius: 8px;
border: 1px solid rgb(var(--m-error) / 0.12);
background: rgb(var(--m-error) / 0.06);
}
.message-list {
display: flex;
flex-direction: column;

View File

@@ -4,8 +4,8 @@
<h2 class="card-title">Speedtest</h2>
<button
class="reload-btn"
@click="runTests"
:disabled="isTesting"
@click="runTests"
>
<IconifyIcon
icon="mdi:reload"
@@ -36,17 +36,23 @@
</div>
</div>
</div>
<p v-if="errorMessage" class="error-text" role="status" aria-live="polite">
{{ errorMessage }}
</p>
</div>
</template>
<script setup lang="ts">
import {computed, ref} from "vue"
import {Icon as IconifyIcon} from "@iconify/vue"
import { apiRequest } from "~/composables/useApiAuth"
const ping = ref<number | null>(null)
const download = ref<number | null>(null)
const upload = ref<number | null>(null)
const isTesting = ref(false)
const errorMessage = ref("")
const metrics = computed(() => [
{ label: "Download", icon: "mdi:arrow-down-bold", value: download.value, unit: "Mbps" },
@@ -56,7 +62,10 @@ const metrics = computed(() => [
async function testDownload() {
const start = performance.now()
const res = await fetch('/api/download')
const res = await apiRequest('/api/download')
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
const blob = await res.blob()
const end = performance.now()
const size = blob.size
@@ -68,7 +77,10 @@ async function testUpload() {
const size = 5 * 1024 * 1024
const data = new Uint8Array(size)
const start = performance.now()
await fetch('/api/upload', { method: 'POST', body: data })
const response = await apiRequest('/api/upload', { method: 'POST', body: data })
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const end = performance.now()
const seconds = (end - start) / 1000
upload.value = Math.round((size * 8) / seconds / 1000000)
@@ -76,7 +88,10 @@ async function testUpload() {
async function testPing() {
const start = performance.now()
await fetch('/api/ping')
const response = await fetch('/api/ping')
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const end = performance.now()
ping.value = Math.round(end - start)
}
@@ -86,11 +101,15 @@ async function runTests() {
download.value = null
upload.value = null
ping.value = null
errorMessage.value = ""
try {
await testDownload()
await testUpload()
await testPing()
} catch (error) {
console.error("Erreur speedtest:", error)
errorMessage.value = "Erreur lors de l'opération"
} finally {
isTesting.value = false
}
@@ -189,4 +208,15 @@ async function runTests() {
letter-spacing: 0.1em;
color: rgb(var(--m-muted));
}
.error-text {
margin-top: 0.75rem;
border-radius: 8px;
border: 1px solid rgb(var(--m-error) / 0.12);
background: rgb(var(--m-error) / 0.06);
padding: 0.75rem 0.875rem;
font-family: var(--font-mono);
font-size: 0.75rem;
color: rgb(var(--m-error));
}
</style>

View File

@@ -20,8 +20,8 @@
</template>
<div
v-else
v-for="row in rows"
v-else
:key="`${row.label}-${row.url}`"
class="status-row"
:class="row.status === 200 ? 'row-ok' : 'row-error'"
@@ -43,6 +43,7 @@
import CircleSkeleton from "~/components/skeleton/CircleSkeleton.vue"
import TextSkeleton from "~/components/skeleton/TextSkeleton.vue"
import {onBeforeUnmount, onMounted, ref} from "vue"
import { apiFetch } from "~/composables/useApiAuth"
interface StatusRow {
label: string
@@ -84,7 +85,7 @@ const checkStatus = async () => {
loading.value = true
}
try {
const data = await $fetch<StatusResponse>(props.endpoint)
const data = await apiFetch<StatusResponse>(props.endpoint)
rows.value = data.results
} catch (error) {
rows.value = [