From 0a73c5cb37c557568647684382440d95de7bf3ab Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 18 Mar 2026 10:33:18 +0100 Subject: [PATCH] fix: use recette status log --- components/BackupRun.vue | 16 +- components/StatusSite.vue | 75 +++++++-- server/api/version-status.get.ts | 185 ++++++++++++++++++---- server/config/version-status-targets.json | 5 - 4 files changed, 230 insertions(+), 51 deletions(-) delete mode 100644 server/config/version-status-targets.json diff --git a/components/BackupRun.vue b/components/BackupRun.vue index 6252d7d..4e91e77 100644 --- a/components/BackupRun.vue +++ b/components/BackupRun.vue @@ -107,6 +107,12 @@ type ScriptResult = { downloadFolders: string[] } +type ApiErrorLike = { + data?: { + statusMessage?: string + } +} + const emit = defineEmits<{ result: [payload: ScriptResult] }>() @@ -171,7 +177,15 @@ const runScript = async (key: string) => { downloadFolders: data.downloadFolders || [] }) } catch (error: unknown) { - message.value = (error as any)?.data?.statusMessage || "Erreur execution script" + const errorMessage = + typeof error === "object" && + error !== null && + "data" in error && + typeof (error as ApiErrorLike).data?.statusMessage === "string" + ? (error as ApiErrorLike).data?.statusMessage + : null + + message.value = errorMessage || "Erreur execution script" emit("result", { key, label: scripts.value.find((item) => item.key === key)?.label || key, diff --git a/components/StatusSite.vue b/components/StatusSite.vue index 1616b83..825e7cd 100644 --- a/components/StatusSite.vue +++ b/components/StatusSite.vue @@ -24,17 +24,27 @@ v-else :key="`${row.label}-${row.url}`" class="status-row" - :class="row.status === 200 ? 'row-ok' : 'row-error'" + :class="row.ok ? 'row-ok' : 'row-error'" > -
- - - {{ row.label }} +
+
+ + + {{ row.label }} + +
+

+ {{ row.detail }} +

+
+
+ + {{ statusLabel(row) }} + + + {{ formatCheckedAt(row.checkedAt) }}
- - {{ statusLabel(row.status) }} -
@@ -51,6 +61,7 @@ interface StatusRow { ok: boolean status: number checkedAt: string + detail: string error?: string } @@ -74,10 +85,24 @@ const loading = ref(true) const initialized = ref(false) let timer: ReturnType | null = null -const statusLabel = (status: number) => { - if (status === 200) return "HTTP 200" - if (status === 0) return "Injoignable" - return `KO (${status})` +const statusLabel = (row: StatusRow) => { + if (row.ok) return "OK" + if (row.status === 0) return "DOWN" + return `KO (${row.status})` +} + +const formatCheckedAt = (checkedAt: string) => { + const date = new Date(checkedAt) + + if (Number.isNaN(date.getTime())) { + return checkedAt + } + + return date.toLocaleTimeString("fr-FR", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit" + }) } const checkStatus = async () => { @@ -95,6 +120,7 @@ const checkStatus = async () => { ok: false, status: 0, checkedAt: new Date().toISOString(), + detail: "Lecture du statut impossible", error: error instanceof Error ? error.message : String(error) } ] @@ -149,6 +175,7 @@ onBeforeUnmount(() => { display: flex; align-items: center; justify-content: space-between; + gap: 1rem; min-height: 3.2rem; padding: 0.85rem 1rem; border-radius: 14px; @@ -157,6 +184,30 @@ onBeforeUnmount(() => { transition: all 0.2s ease; } +.status-copy { + min-width: 0; +} + +.status-detail { + margin: 0.35rem 0 0; + color: rgb(var(--m-muted)); + font-size: 0.78rem; + line-height: 1.4; +} + +.status-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.2rem; + flex-shrink: 0; +} + +.status-time { + font-size: 0.72rem; + color: rgb(var(--m-muted)); +} + .row-ok { border-color: rgb(var(--m-success) / 0.08); } diff --git a/server/api/version-status.get.ts b/server/api/version-status.get.ts index 5449949..fd76bc5 100644 --- a/server/api/version-status.get.ts +++ b/server/api/version-status.get.ts @@ -1,41 +1,160 @@ -import targets from "../config/version-status-targets.json" +import { readFile } from "node:fs/promises" +import { join } from "node:path" -const REQUEST_TIMEOUT_MS = 5000 +type StatusEntry = { + checkedAt: string + status: "OK" | "DOWN" + host: string + detail: string +} + +type StatusResult = { + label: string + url: string + ok: boolean + status: number + checkedAt: string + detail: string +} + +const DEFAULT_RECETTE_SCRIPTS_DIR = "/home/malio/Malio-ops/RecetteScripts" + +function parseEnvFile(content: string) { + const values: Record = {} + + for (const rawLine of content.split("\n")) { + const line = rawLine.trim() + + if (!line || line.startsWith("#")) { + continue + } + + const separatorIndex = line.indexOf("=") + if (separatorIndex === -1) { + continue + } + + const key = line.slice(0, separatorIndex).trim() + const value = line.slice(separatorIndex + 1).trim() + + if (!key) { + continue + } + + values[key] = value.replace(/^['"]|['"]$/g, "") + } + + return values +} + +function getLogFileName(date: Date) { + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, "0") + const day = String(date.getDate()).padStart(2, "0") + + return `app_health_${year}-${month}-${day}.log` +} + +function parseStatusLine(line: string): StatusEntry | null { + const parts = line.split(" | ") + if (parts.length < 4) { + return null + } + + const [checkedAt, status, host, ...detailParts] = parts + if ((status !== "OK" && status !== "DOWN") || !host) { + return null + } + + return { + checkedAt, + status, + host, + detail: detailParts.join(" | ") + } +} + +function buildStatusResult(entry: StatusEntry): StatusResult { + return { + label: entry.host, + url: `http://${entry.host}/`, + ok: entry.status === "OK", + status: entry.status === "OK" ? 200 : 0, + checkedAt: entry.checkedAt, + detail: entry.detail + } +} export default defineEventHandler(async () => { - const results = await Promise.all( - targets.map(async (target) => { - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS) + const recetteScriptsDir = process.env.RECETTE_SCRIPTS_DIR || DEFAULT_RECETTE_SCRIPTS_DIR + const envFilePath = join(recetteScriptsDir, ".env") - try { - const response = await fetch(target.url, { - method: "GET", - headers: { Accept: "application/json" }, - signal: controller.signal - }) + try { + const envFileContent = await readFile(envFilePath, "utf8") + const envValues = parseEnvFile(envFileContent) + const logDir = envValues.APP_LOG_DIR - return { - label: target.label, - url: target.url, - ok: response.status === 200, - status: response.status, - checkedAt: new Date().toISOString() - } - } catch (error) { - return { - label: target.label, - url: target.url, - ok: false, - status: 0, - checkedAt: new Date().toISOString(), - error: error instanceof Error ? error.message : String(error) - } - } finally { - clearTimeout(timeoutId) + if (!logDir) { + throw createError({ + statusCode: 500, + statusMessage: "Variable APP_LOG_DIR manquante" + }) + } + + const logFilePath = join(logDir, getLogFileName(new Date())) + const logFileContent = await readFile(logFilePath, "utf8") + const latestEntriesByHost = new Map() + + for (const line of logFileContent.split("\n")) { + const entry = parseStatusLine(line) + if (!entry) { + continue } - }) - ) - return { results } + latestEntriesByHost.set(entry.host, entry) + } + + const configuredHosts = (envValues.APP_URLS || "") + .split(/\s+/) + .map((host) => host.trim()) + .filter(Boolean) + + const orderedResults = configuredHosts + .map((host) => latestEntriesByHost.get(host)) + .filter((entry): entry is StatusEntry => Boolean(entry)) + .map(buildStatusResult) + + const remainingResults = Array.from(latestEntriesByHost.entries()) + .filter(([host]) => !configuredHosts.includes(host)) + .map(([, entry]) => buildStatusResult(entry)) + + const results = [...orderedResults, ...remainingResults] + + if (results.length === 0) { + throw createError({ + statusCode: 503, + statusMessage: "Aucun statut disponible" + }) + } + + return { + results + } + } catch (error) { + console.error("Erreur lecture status recette:", error) + + if ( + typeof error === "object" && + error !== null && + "statusCode" in error && + "statusMessage" in error + ) { + throw error + } + + throw createError({ + statusCode: 500, + statusMessage: "Erreur lors de l'opération" + }) + } }) diff --git a/server/config/version-status-targets.json b/server/config/version-status-targets.json deleted file mode 100644 index 922b910..0000000 --- a/server/config/version-status-targets.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - { "label": "Ferme", "url": "http://ferme.malio-dev.fr/api/version" }, - { "label": "SIRH", "url": "http://sirh.malio-dev.fr/api/version" }, - { "label": "Inventory", "url": "http://inventory.malio-dev.fr/api/health" } -]