[#379] Ajout page monitoring #1
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
54
AGENTS.md
Normal file
54
AGENTS.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
This repository is a Nuxt 4 application.
|
||||
- `pages/`: route pages (for example `pages/index.vue`).
|
||||
- `components/`: reusable Vue components (for example `DiskSidebarWidget.vue`, `ApiStatusBubble.vue`).
|
||||
- `layouts/`: shared page shells (`layouts/default.vue`).
|
||||
- `server/api/`: Nitro server endpoints (for example `disk.get.ts`, `version-status.get.ts`).
|
||||
- `assets/css/`: global styles and theme tokens (`main.css`, `malio.css`).
|
||||
- `public/`: static files served as-is.
|
||||
|
||||
Keep feature logic close to usage: UI in `components/`, page composition in `pages/`, backend checks in `server/api/`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
Use npm scripts from `package.json`:
|
||||
- `npm run dev`: start local dev server with hot reload.
|
||||
- `npm run build`: production build (client + server).
|
||||
- `npm run preview`: run the built app locally.
|
||||
- `npm run generate`: static generation when needed.
|
||||
|
||||
There is no dedicated test script currently. At minimum, run `npm run build` before opening a PR.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- Language: TypeScript + Vue SFCs (`<script setup lang="ts">`).
|
||||
- Indentation: 2 spaces.
|
||||
- Prefer double quotes in TS files to match current codebase.
|
||||
- Components: PascalCase file names (`ApiStatusBubble.vue`).
|
||||
- API handlers: kebab-case + HTTP suffix (`version-status.get.ts`).
|
||||
- Use Tailwind utility classes and project color tokens (`text-m-error`, `bg-m-success`, `text-m-primary`).
|
||||
|
||||
## Testing Guidelines
|
||||
No automated framework is configured yet. Use these checks:
|
||||
- Build validation: `npm run build`.
|
||||
- Manual smoke test: open `/`, verify API status cards refresh and disk sidebar renders.
|
||||
- For new endpoints, validate response shape in browser/network tab or `curl`.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
Git history is not available in this workspace snapshot, so use this convention:
|
||||
- Commit format: `type(scope): short summary`.
|
||||
- Example: `feat(api): add labeled multi-endpoint status check`.
|
||||
- Types: `feat`, `fix`, `refactor`, `style`, `docs`, `chore`.
|
||||
|
||||
PRs should include:
|
||||
- What changed and why.
|
||||
- Affected paths (e.g. `server/api/version-status.get.ts`).
|
||||
- UI screenshots for visual changes.
|
||||
- Verification steps (commands run, expected result).
|
||||
|
||||
## Security & Configuration Notes
|
||||
- Do not hardcode secrets in source.
|
||||
- Prefer environment variables for private endpoints/tokens.
|
||||
- Keep external API checks server-side (`server/api/*`) to avoid exposing sensitive details in the client.
|
||||
|
||||
Ne lance pas de build si je le dit pas
|
||||
5
app.vue
Normal file
5
app.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
27
assets/css/main.css
Normal file
27
assets/css/main.css
Normal file
@@ -0,0 +1,27 @@
|
||||
@import "tailwindcss";
|
||||
@import "./malio.css";
|
||||
|
||||
@theme {
|
||||
--color-panel: #0f172a;
|
||||
--color-panel-soft: #1e293b;
|
||||
--color-m-primary: rgb(var(--m-primary));
|
||||
--color-m-secondary: rgb(var(--m-secondary));
|
||||
--color-m-tertiary: rgb(var(--m-tertiary));
|
||||
--color-m-border: rgb(var(--m-border));
|
||||
--color-m-text: rgb(var(--m-text));
|
||||
--color-m-muted: rgb(var(--m-muted));
|
||||
--color-m-bg: rgb(var(--m-bg));
|
||||
--color-m-error: rgb(var(--m-error));
|
||||
--color-m-success: rgb(var(--m-success));
|
||||
/* Alias pour la faute de frappe courante "m-succes" */
|
||||
--color-m-succes: rgb(var(--m-success));
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.bg-grid {
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(148, 163, 184, 0.12) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(148, 163, 184, 0.12) 1px, transparent 1px);
|
||||
background-size: 32px 32px;
|
||||
}
|
||||
}
|
||||
19
assets/css/malio.css
Normal file
19
assets/css/malio.css
Normal file
@@ -0,0 +1,19 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
/* Couleurs en RGB “space separated” pour Tailwind */
|
||||
--m-primary: 34 39 131; /* Couleur principal*/
|
||||
--m-secondary: 48 73 152; /* Couleur secondaire */
|
||||
--m-tertiary: 243 244 248; /* Couleur tertiaire (background) */
|
||||
--m-border: 203 213 225; /* Couleur des bordures */
|
||||
--m-text: 15 23 42; /* Couleur du texte */
|
||||
--m-muted: 100 116 139; /* Couleur pour les éléments désactivés ou secondaires */
|
||||
--m-bg: 243 244 248; /* Couleur de fond générale */
|
||||
|
||||
--m-error: 212 0 21; /* rouge pour les erreurs */
|
||||
--m-success: 15 200 73; /* vert pour les succès */
|
||||
}
|
||||
}
|
||||
50
components/DiagramStorage.vue
Normal file
50
components/DiagramStorage.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<section class="flex flex-col items-center p-4">
|
||||
<p class="text-center text-xl font-semibold uppercase">{{ hostName }}</p>
|
||||
<div class="relative h-[140px] w-[140px]" :class="statusColorClass">
|
||||
<svg class="h-full w-full -rotate-90" viewBox="0 0 120 120" aria-label="Pourcentage restant">
|
||||
<circle
|
||||
class="fill-none stroke-[rgba(255,255,255,0.22)] [stroke-width:10]"
|
||||
cx="60"
|
||||
cy="60"
|
||||
:r="chartRadius"
|
||||
/>
|
||||
<circle
|
||||
class="fill-none stroke-[currentColor] [stroke-linecap:round] [stroke-width:10] transition-[stroke-dashoffset] duration-300"
|
||||
cx="60"
|
||||
cy="60"
|
||||
:r="chartRadius"
|
||||
:style="{ strokeDasharray: `${chartCircumference}`, strokeDashoffset: `${chartOffset}` }"
|
||||
/>
|
||||
</svg>
|
||||
<div class="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<strong class="text-2xl leading-none">{{ remainingPercentText }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-1 text-center text-sm font-semibold">{{ usedText }} / {{ totalText }}</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
hostName: string
|
||||
statusColorClass: string
|
||||
chartRadius: number
|
||||
chartCircumference: number
|
||||
chartOffset: number
|
||||
remainingPercentText: string
|
||||
usedText: string
|
||||
totalText: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.m-success {
|
||||
color: rgb(var(--m-success));
|
||||
}
|
||||
|
||||
.m-error {
|
||||
color: rgb(var(--m-error));
|
||||
}
|
||||
</style>
|
||||
131
components/Speedtest.vue
Normal file
131
components/Speedtest.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="bg-m-secondary w-[509px] h-[184px] mx-4 rounded-md shadow-md/50 shadow-black p-2">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<p class="font-bold text-3xl text-m-tertiary">
|
||||
Speedtest
|
||||
</p>
|
||||
<IconifyIcon
|
||||
icon="mdi:reload"
|
||||
class="bg-m-tertiary text-2xl text-black rounded-md shadow-md/50 mr-1 cursor-pointer"
|
||||
@Click="runTests"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="bg-m-tertiary w-[153px] h-[120px] rounded-md shadow-md/50 shadow-m-black">
|
||||
<div class="flex items-center justify-center">
|
||||
<IconifyIcon
|
||||
icon="mdi:download"
|
||||
class="text-m-primary text-2xl mt-2 ml-1"
|
||||
/>
|
||||
<p class="font-bold uppercase text-xl text-m-text mt-2 mr-1">
|
||||
download
|
||||
</p>
|
||||
</div>
|
||||
<div class="mx-2 flex flex-col items-center justify-center">
|
||||
<span class="text-4xl">
|
||||
{{ download !== null ? `${download}` : "..." }}
|
||||
</span>
|
||||
<p class="font-bold text-xl leading-tight">
|
||||
Mbps
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-m-tertiary w-[153px] h-[120px] rounded-md shadow-md/50 shadow-m-black">
|
||||
<div class="flex items-center justify-center">
|
||||
<IconifyIcon
|
||||
icon="mdi:upload"
|
||||
class="text-m-primary text-2xl mt-2 ml-1"
|
||||
/>
|
||||
<p class="font-bold uppercase text-xl text-m-text mt-2 mr-1">
|
||||
upload
|
||||
</p>
|
||||
</div>
|
||||
<div class="mx-2 flex flex-col items-center justify-center">
|
||||
<span class="text-4xl">
|
||||
{{ upload !== null ? `${upload}` : "..." }}
|
||||
</span>
|
||||
<p class="font-bold text-xl leading-tight">
|
||||
Mbps
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-m-tertiary w-[153px] h-[120px] rounded-md shadow-md/50 shadow-m-black">
|
||||
<div class="flex items-center justify-center">
|
||||
<IconifyIcon
|
||||
icon="mdi:wifi"
|
||||
class="text-m-primary text-2xl mt-2 ml-1"
|
||||
/>
|
||||
<p class="font-bold uppercase text-xl text-m-text mt-2 mr-1">
|
||||
ping
|
||||
</p>
|
||||
</div>
|
||||
<div class="mx-2 flex flex-col items-center justify-center">
|
||||
<span class="text-4xl">
|
||||
{{ ping !== null ? `${ping}` : "..." }}
|
||||
</span>
|
||||
<p class="font-bold text-xl leading-tight">
|
||||
Ms
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from "vue";
|
||||
import {Icon as IconifyIcon} from "@iconify/vue"
|
||||
|
||||
const ping = ref<number | null>(null)
|
||||
const download = ref<number | null>(null)
|
||||
const upload = ref<number | null>(null)
|
||||
|
||||
async function testDownload() {
|
||||
const start = performance.now()
|
||||
|
||||
const res = await fetch('/api/download')
|
||||
const blob = await res.blob()
|
||||
|
||||
const end = performance.now()
|
||||
|
||||
const size = blob.size
|
||||
const seconds = (end - start) / 1000
|
||||
download.value = Math.round((size * 8) / seconds / 1000000)
|
||||
}
|
||||
|
||||
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 end = performance.now()
|
||||
|
||||
const seconds = (end - start) / 1000
|
||||
upload.value = Math.round((size * 8) / seconds / 1000000)
|
||||
}
|
||||
|
||||
|
||||
async function testPing() {
|
||||
const start = performance.now()
|
||||
|
||||
await fetch('/api/ping')
|
||||
|
||||
const end = performance.now()
|
||||
|
||||
ping.value = Math.round(end - start)
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
await testDownload()
|
||||
await testUpload()
|
||||
await testPing()
|
||||
}
|
||||
onMounted(() => {
|
||||
runTests()
|
||||
})
|
||||
</script>
|
||||
98
components/StatusSite.vue
Normal file
98
components/StatusSite.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="bg-m-secondary w-[250px] h-auto rounded-md mx-4 shadow-md/50 shadow-black pb-4">
|
||||
<p class="font-bold text-3xl text-m-tertiary my-1 mx-3">
|
||||
Status
|
||||
</p>
|
||||
<div
|
||||
class="bg-m-tertiary w-[200px] h-auto rounded-md shadow-md/50 shadow-m-black mx-[25px] mb-3"
|
||||
v-for="row in rows"
|
||||
:key="`${row.label}-${row.url}`"
|
||||
>
|
||||
<p class="font-bold text-xl text-m-text mt-2 mx-2 mb-1">
|
||||
{{ row.label }}
|
||||
</p>
|
||||
<div class="mx-2 flex items-center">
|
||||
<span
|
||||
class="inline-block h-[24px] w-[24px] rounded-full mr-2"
|
||||
:class="statusClass(row.status)"
|
||||
/>
|
||||
<span class="font-semibold text-lg">
|
||||
{{ statusLabel(row.status) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {onBeforeUnmount, onMounted, ref} from "vue"
|
||||
|
||||
interface StatusRow {
|
||||
label: string
|
||||
url: string
|
||||
ok: boolean
|
||||
status: number
|
||||
checkedAt: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface StatusResponse {
|
||||
results: StatusRow[]
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
endpoint?: string
|
||||
refreshMs?: number
|
||||
}>(),
|
||||
{
|
||||
endpoint: "/api/version-status",
|
||||
refreshMs: 30000
|
||||
}
|
||||
)
|
||||
|
||||
const rows = ref<StatusRow[]>([])
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const statusClass = (status: number) => {
|
||||
if (status === 200) return "bg-m-success"
|
||||
if (status === 0) return "bg-m-error"
|
||||
return "bg-m-error"
|
||||
}
|
||||
|
||||
const statusLabel = (status: number) => {
|
||||
if (status === 200) return "HTTP 200"
|
||||
if (status === 0) return "Injoignable"
|
||||
return `KO (HTTP ${status})`
|
||||
}
|
||||
|
||||
const checkStatus = async () => {
|
||||
try {
|
||||
const data = await $fetch<StatusResponse>(props.endpoint)
|
||||
rows.value = data.results
|
||||
} catch (error) {
|
||||
rows.value = [
|
||||
{
|
||||
label: "Erreur",
|
||||
url: props.endpoint,
|
||||
ok: false,
|
||||
status: 0,
|
||||
checkedAt: new Date().toISOString(),
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkStatus()
|
||||
timer = setInterval(checkStatus, props.refreshMs)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
41
layouts/default.vue
Normal file
41
layouts/default.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="page-layout">
|
||||
<aside class="sidebar">
|
||||
<div class="flex items-center gap-3 px-14 py-4">
|
||||
<div class="avatar">
|
||||
<div class="h-[155px] w-[155px]">
|
||||
<img
|
||||
:src="logoSrc"
|
||||
alt="Logo Malio"
|
||||
class="h-full w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="sidebar" />
|
||||
</aside>
|
||||
<main class="content">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import logoSrc from '~/assets/LOGO_CARRE_BLANC.png'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: rgb(var(--m-primary));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: rgb(var(--m-tertiary));
|
||||
}
|
||||
</style>
|
||||
11
nuxt.config.ts
Normal file
11
nuxt.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: { enabled: true },
|
||||
css: ["~/assets/css/main.css"],
|
||||
vite: {
|
||||
plugins: [tailwindcss()]
|
||||
}
|
||||
})
|
||||
10312
package-lock.json
generated
Normal file
10312
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "disk-monitor",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"nuxt": "^4.3.1",
|
||||
"vue": "^3.5.29",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"tailwindcss": "^4.2.1"
|
||||
}
|
||||
}
|
||||
163
pages/index.vue
Normal file
163
pages/index.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<NuxtLayout name="default">
|
||||
<template #sidebar>
|
||||
<div class="flex flex-col gap-">
|
||||
<DiagramStorage
|
||||
v-for="item in diagramItems"
|
||||
:key="item.key"
|
||||
:host-name="item.hostName"
|
||||
:status-color-class="item.statusColorClass"
|
||||
:chart-radius="chartRadius"
|
||||
:chart-circumference="chartCircumference"
|
||||
:chart-offset="item.chartOffset"
|
||||
:remaining-percent-text="item.remainingPercentText"
|
||||
:used-text="item.usedText"
|
||||
:total-text="item.totalText"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<p class="font-bold text-4xl my-6 mx-4">Écran de monitoring</p>
|
||||
<div class="flex">
|
||||
<StatusSite/>
|
||||
<Speedtest/>
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Speedtest from "~/components/Speedtest.vue";
|
||||
|
||||
definePageMeta({layout: false})
|
||||
import {computed, onMounted, ref} from "vue"
|
||||
|
||||
type SourceKey = "remote" | "local"
|
||||
type DiskCommandResult = { ok: boolean; output: string }
|
||||
type DiskApiResponse = {
|
||||
remote?: string | DiskCommandResult
|
||||
local?: string | DiskCommandResult
|
||||
}
|
||||
|
||||
const rawResults = ref<Record<SourceKey, string>>({
|
||||
remote: "",
|
||||
local: ""
|
||||
})
|
||||
const loading = ref(false)
|
||||
const chartRadius = 52
|
||||
const chartCircumference = 2 * Math.PI * chartRadius
|
||||
|
||||
const getHostName = (output: string, fallback: string) => {
|
||||
const hostMatch = output.match(/Name:\s*(.+)/i)
|
||||
return hostMatch?.[1]?.trim() || fallback
|
||||
}
|
||||
|
||||
const getDiskValues = (output: string) => {
|
||||
if (!output || output.startsWith("Erreur:")) return null
|
||||
|
||||
const availableLine = output
|
||||
.split("\n")
|
||||
.find((line) => line.toLowerCase().includes("espace disponible"))
|
||||
const usageLine = output
|
||||
.split("\n")
|
||||
.find((line) => line.toLowerCase().includes("espace utilise / espace total"))
|
||||
|
||||
const availableRaw = availableLine?.match(/:\s*(\d+(?:[.,]\d+)?)\s*GB/i)?.[1]
|
||||
const usedRaw = usageLine?.match(/:\s*(\d+(?:[.,]\d+)?)\s*GB/i)?.[1]
|
||||
const totalRaw = usageLine?.match(/\/\s*(\d+(?:[.,]\d+)?)\s*GB/i)?.[1]
|
||||
|
||||
const availableGb = availableRaw ? Number.parseFloat(availableRaw.replace(",", ".")) : null
|
||||
const usedGb = usedRaw ? Number.parseFloat(usedRaw.replace(",", ".")) : null
|
||||
const totalGb = totalRaw ? Number.parseFloat(totalRaw.replace(",", ".")) : null
|
||||
|
||||
if (
|
||||
availableGb === null ||
|
||||
usedGb === null ||
|
||||
totalGb === null ||
|
||||
!Number.isFinite(availableGb) ||
|
||||
!Number.isFinite(usedGb) ||
|
||||
!Number.isFinite(totalGb) ||
|
||||
totalGb <= 0
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {availableGb, usedGb, totalGb}
|
||||
}
|
||||
|
||||
const getOutputText = (entry: unknown) => {
|
||||
if (typeof entry === "string") return entry
|
||||
if (entry && typeof entry === "object" && "output" in entry) {
|
||||
const output = (entry as DiskCommandResult).output
|
||||
return typeof output === "string" ? output : String(output)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
const diagramItems = computed(() => {
|
||||
return [
|
||||
{ key: "remote" as const, fallbackHost: "Serveur distant", output: rawResults.value.remote },
|
||||
{ key: "local" as const, fallbackHost: "Machine locale", output: rawResults.value.local }
|
||||
].map((item) => {
|
||||
const diskValues = getDiskValues(item.output)
|
||||
const remainingPercent =
|
||||
diskValues === null
|
||||
? null
|
||||
: Math.max(
|
||||
0,
|
||||
Math.min(100, Math.round((diskValues.availableGb / diskValues.totalGb) * 100))
|
||||
)
|
||||
|
||||
const chartOffset = chartCircumference - ((remainingPercent ?? 0) / 100) * chartCircumference
|
||||
const statusColorClass =
|
||||
remainingPercent !== null && remainingPercent <= 30 ? "m-error" : "m-success"
|
||||
|
||||
return {
|
||||
key: item.key,
|
||||
hostName: getHostName(item.output, item.fallbackHost),
|
||||
statusColorClass,
|
||||
chartOffset,
|
||||
remainingPercentText:
|
||||
loading.value ? "..." : remainingPercent === null ? "--%" : `${remainingPercent}%`,
|
||||
usedText: loading.value ? "..." : diskValues ? `${diskValues.usedGb.toFixed(2)} GB` : "--",
|
||||
totalText: loading.value ? "..." : diskValues ? `${diskValues.totalGb.toFixed(2)} GB` : "--"
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
const runScript = async () => {
|
||||
loading.value = true
|
||||
rawResults.value = {
|
||||
remote: "",
|
||||
local: ""
|
||||
}
|
||||
|
||||
try {
|
||||
const output = await $fetch<DiskApiResponse | string>("/api/disk")
|
||||
|
||||
if (typeof output === "string") {
|
||||
rawResults.value = {
|
||||
remote: output,
|
||||
local: "Erreur: sortie locale indisponible"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
rawResults.value = {
|
||||
remote: getOutputText(output.remote),
|
||||
local: getOutputText(output.local)
|
||||
}
|
||||
} catch (error) {
|
||||
const message = `Erreur: ${error instanceof Error ? error.message : String(error)}`
|
||||
rawResults.value = {
|
||||
remote: message,
|
||||
local: message
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
runScript()
|
||||
})
|
||||
</script>
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-Agent: *
|
||||
Disallow:
|
||||
45
server/api/disk.get.ts
Normal file
45
server/api/disk.get.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { exec } from "child_process"
|
||||
|
||||
const remoteCommand =
|
||||
process.env.DISK_REMOTE_COMMAND ||
|
||||
"ssh malio-b@192.168.0.179 'cd /home/malio-b/Scripts-Serveur && bash check_storage.sh && exit'"
|
||||
|
||||
const localCommand =
|
||||
process.env.DISK_LOCAL_COMMAND ||
|
||||
"bash /home/kevin/check_storage.sh"
|
||||
|
||||
function runCommand(command: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(stderr || error.message)
|
||||
return
|
||||
}
|
||||
resolve(stdout)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
const [remoteResult, localResult] = await Promise.allSettled([
|
||||
runCommand(remoteCommand),
|
||||
runCommand(localCommand)
|
||||
])
|
||||
|
||||
return {
|
||||
remote: {
|
||||
ok: remoteResult.status === "fulfilled",
|
||||
output:
|
||||
remoteResult.status === "fulfilled"
|
||||
? remoteResult.value
|
||||
: `Erreur: ${String(remoteResult.reason)}`
|
||||
},
|
||||
local: {
|
||||
ok: localResult.status === "fulfilled",
|
||||
output:
|
||||
localResult.status === "fulfilled"
|
||||
? localResult.value
|
||||
: `Erreur: ${String(localResult.reason)}`
|
||||
}
|
||||
}
|
||||
})
|
||||
25
server/api/download.get.ts
Normal file
25
server/api/download.get.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Readable } from "node:stream"
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
const size = 128 * 1024 * 1024
|
||||
let sent = 0
|
||||
|
||||
const stream = new Readable({
|
||||
read(chunkSize) {
|
||||
if (sent >= size) {
|
||||
this.push(null)
|
||||
return
|
||||
}
|
||||
|
||||
const remaining = size - sent
|
||||
const chunk = Buffer.alloc(Math.min(chunkSize, remaining), "a")
|
||||
sent += chunk.length
|
||||
this.push(chunk)
|
||||
}
|
||||
})
|
||||
|
||||
setHeader(event, "Content-Type", "application/octet-stream")
|
||||
setHeader(event, "Content-Length", size)
|
||||
|
||||
return stream
|
||||
})
|
||||
3
server/api/ping.get.ts
Normal file
3
server/api/ping.get.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default defineEventHandler(() => {
|
||||
return { ok: true }
|
||||
})
|
||||
11
server/api/upload.post.ts
Normal file
11
server/api/upload.post.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const req = event.node.req
|
||||
|
||||
let received = 0
|
||||
|
||||
for await (const chunk of req) {
|
||||
received += chunk.length
|
||||
}
|
||||
|
||||
return { received }
|
||||
})
|
||||
37
server/api/version-status.get.ts
Normal file
37
server/api/version-status.get.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export default defineEventHandler(async () => {
|
||||
const targets = [
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
const results = await Promise.all(
|
||||
targets.map(async (target) => {
|
||||
try {
|
||||
const response = await fetch(target.url, {
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json" }
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return { results }
|
||||
})
|
||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.server.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.shared.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user