Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d63b02fc4e | ||
| 7ed9382e73 | |||
| 539cbdd2f1 | |||
|
|
0d985b62b1 | ||
| 4d76d2697b | |||
| 0863dfad2e | |||
| 440fffc605 | |||
| aad7a0a928 |
35
.github/workflows/release.yml
vendored
35
.github/workflows/release.yml
vendored
@@ -1,25 +1,24 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- develop
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22.x
|
||||||
|
cache: npm
|
||||||
|
|
||||||
- run: npm install
|
- run: npm ci
|
||||||
|
|
||||||
- run: npx semantic-release
|
- run: npx semantic-release
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"@semantic-release/commit-analyzer",
|
"@semantic-release/commit-analyzer",
|
||||||
"@semantic-release/release-notes-generator",
|
"@semantic-release/release-notes-generator",
|
||||||
"@semantic-release/changelog",
|
"@semantic-release/changelog",
|
||||||
"@semantic-release/github",
|
|
||||||
[
|
[
|
||||||
"@semantic-release/git",
|
"@semantic-release/git",
|
||||||
{
|
{
|
||||||
@@ -17,4 +16,4 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
13
CHANGELOG.md
Normal file
13
CHANGELOG.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# [1.2.0](https://gitea.malio.fr/MALIO-DEV/Supervisor/compare/v1.1.0...v1.2.0) (2026-03-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* show git tag version ([539cbdd](https://gitea.malio.fr/MALIO-DEV/Supervisor/commit/539cbdd2f1fa73eddab8adb6e2cc0683e6c424aa))
|
||||||
|
|
||||||
|
# [1.1.0](https://gitea.malio.fr/MALIO-DEV/Supervisor/compare/v1.0.0...v1.1.0) (2026-03-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **backup:** add backup scripts workflow ([0863dfa](https://gitea.malio.fr/MALIO-DEV/Supervisor/commit/0863dfad2e3c6272a012c30820381a2610e22d1b))
|
||||||
201
components/BackupRun.vue
Normal file
201
components/BackupRun.vue
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<template>
|
||||||
|
<div class="backup-card card-glow">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">Run Script</h2>
|
||||||
|
<span class="font-mono text-[10px] text-m-muted tracking-widest uppercase">Scripts</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="status-box">
|
||||||
|
Chargement des scripts...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="backup-list">
|
||||||
|
<button
|
||||||
|
v-for="item in scripts"
|
||||||
|
:key="item.key"
|
||||||
|
type="button"
|
||||||
|
class="backup-btn"
|
||||||
|
:class="{ 'backup-btn-active': active === item.key }"
|
||||||
|
:disabled="runningKey !== null"
|
||||||
|
@click="runScript(item.key)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2.5">
|
||||||
|
<IconifyIcon :icon="item.icon" class="text-base text-m-accent" />
|
||||||
|
<span class="font-display text-sm font-semibold uppercase tracking-wide">
|
||||||
|
{{ item.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<IconifyIcon
|
||||||
|
:icon="runningKey === item.key ? 'mdi:loading' : 'mdi:play'"
|
||||||
|
class="text-lg text-m-muted transition-transform duration-200"
|
||||||
|
:class="{
|
||||||
|
'translate-x-0.5 !text-m-accent': active === item.key,
|
||||||
|
'animate-spin': runningKey === item.key
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="message" class="status-box" :class="statusClass">
|
||||||
|
<p class="status-title">{{ message }}</p>
|
||||||
|
<pre v-if="output" class="status-output">{{ output }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from "vue"
|
||||||
|
import { Icon as IconifyIcon } from "@iconify/vue"
|
||||||
|
|
||||||
|
type BackupScript = {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type BackupScriptListResponse = {
|
||||||
|
scripts: BackupScript[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type BackupScriptRunResponse = {
|
||||||
|
ok: boolean
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
output: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const active = ref<string | null>(null)
|
||||||
|
const loading = ref(true)
|
||||||
|
const runningKey = ref<string | null>(null)
|
||||||
|
const scripts = ref<BackupScript[]>([])
|
||||||
|
const output = ref("")
|
||||||
|
const message = ref("")
|
||||||
|
const isError = ref(false)
|
||||||
|
|
||||||
|
const statusClass = computed(() => (isError.value ? "status-error" : "status-success"))
|
||||||
|
|
||||||
|
const loadScripts = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = await $fetch<BackupScriptListResponse>("/api/backup-script")
|
||||||
|
scripts.value = data.scripts
|
||||||
|
} catch (error) {
|
||||||
|
isError.value = true
|
||||||
|
message.value = `Erreur chargement scripts: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runScript = async (key: string) => {
|
||||||
|
active.value = key
|
||||||
|
runningKey.value = key
|
||||||
|
output.value = ""
|
||||||
|
message.value = ""
|
||||||
|
isError.value = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await $fetch<BackupScriptRunResponse>("/api/backup-script", {
|
||||||
|
method: "POST",
|
||||||
|
body: { key }
|
||||||
|
})
|
||||||
|
message.value = `${data.label} execute`
|
||||||
|
output.value = data.output
|
||||||
|
} catch (error: any) {
|
||||||
|
isError.value = true
|
||||||
|
message.value = error?.data?.statusMessage || "Erreur execution script"
|
||||||
|
output.value = ""
|
||||||
|
} finally {
|
||||||
|
runningKey.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadScripts)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.backup-card {
|
||||||
|
background: rgb(var(--m-secondary));
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
transition: background-color 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgb(var(--m-text));
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.625rem 0.875rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgb(var(--m-tertiary));
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: rgb(var(--m-text));
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-btn:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-btn:hover {
|
||||||
|
border-color: rgb(var(--m-accent) / 0.15);
|
||||||
|
background: rgb(var(--m-accent) / 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-btn-active {
|
||||||
|
border-color: rgb(var(--m-accent) / 0.25);
|
||||||
|
background: rgb(var(--m-accent) / 0.08);
|
||||||
|
box-shadow: 0 0 12px -4px rgb(var(--m-accent) / 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-box {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.875rem;
|
||||||
|
background: rgb(var(--m-tertiary));
|
||||||
|
color: rgb(var(--m-text));
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success {
|
||||||
|
border: 1px solid rgb(var(--m-accent) / 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
border: 1px solid rgb(255 99 99 / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-output {
|
||||||
|
margin: 0.75rem 0 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
color: rgb(var(--m-muted));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-layout">
|
<div class="page-layout">
|
||||||
<aside class="sidebar">
|
<aside class="sidebar" aria-label="Navigation principale">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<div class="logo-container">
|
<div class="logo-container">
|
||||||
<img
|
<img
|
||||||
@@ -9,16 +9,48 @@
|
|||||||
class="logo"
|
class="logo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="brand-copy">
|
||||||
|
<p class="brand-title">Supervisor</p>
|
||||||
|
</div>
|
||||||
<div class="sidebar-divider" />
|
<div class="sidebar-divider" />
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<slot name="sidebar" />
|
<slot name="sidebar" />
|
||||||
|
<nav class="sidebar-nav" aria-label="Sections">
|
||||||
|
<p class="nav-label">Navigation</p>
|
||||||
|
<NuxtLink
|
||||||
|
v-for="item in navItems"
|
||||||
|
:key="item.to"
|
||||||
|
v-slot="{ href, navigate, isExactActive }"
|
||||||
|
:to="item.to"
|
||||||
|
custom
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
class="nav-link"
|
||||||
|
:class="{ 'nav-link-active': isExactActive }"
|
||||||
|
:aria-current="isExactActive ? 'page' : undefined"
|
||||||
|
@click="navigate(); isMenuOpen = false"
|
||||||
|
>
|
||||||
|
<span class="nav-link-main">
|
||||||
|
<span class="nav-icon">
|
||||||
|
<IconifyIcon :icon="item.icon" class="text-lg" />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span class="nav-title">{{ item.label }}</span>
|
||||||
|
<span class="nav-caption">{{ item.caption }}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="nav-pill">{{ item.short }}</span>
|
||||||
|
</a>
|
||||||
|
</NuxtLink>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
<div class="sidebar-divider" />
|
<div class="sidebar-divider" />
|
||||||
<div class="footer-row">
|
<div class="footer-row">
|
||||||
<p class="font-mono text-[10px] tracking-widest uppercase text-white/40">
|
<p class="font-mono text-[10px] tracking-widest uppercase text-white/40">
|
||||||
Supervisor v1.0
|
Supervisor {{ appVersion }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,7 +62,7 @@
|
|||||||
|
|
||||||
<div v-if="isMenuOpen" class="mobile-menu-backdrop" @click="isMenuOpen = false" />
|
<div v-if="isMenuOpen" class="mobile-menu-backdrop" @click="isMenuOpen = false" />
|
||||||
|
|
||||||
<aside v-if="isMenuOpen" class="mobile-sidebar">
|
<aside v-if="isMenuOpen" class="mobile-sidebar" aria-label="Navigation mobile">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<div class="logo-container">
|
<div class="logo-container">
|
||||||
<img
|
<img
|
||||||
@@ -39,16 +71,59 @@
|
|||||||
class="logo"
|
class="logo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="brand-copy">
|
||||||
|
<p class="brand-kicker">Control Center</p>
|
||||||
|
<p class="brand-title">Supervisor</p>
|
||||||
|
<p class="brand-description">
|
||||||
|
Tableau de bord interne pour le monitoring et les sauvegardes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class="sidebar-divider" />
|
<div class="sidebar-divider" />
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<slot name="sidebar" />
|
<slot name="sidebar" />
|
||||||
|
<nav class="sidebar-nav" aria-label="Sections mobiles">
|
||||||
|
<p class="nav-label">Navigation</p>
|
||||||
|
<NuxtLink
|
||||||
|
v-for="item in navItems"
|
||||||
|
:key="`mobile-${item.to}`"
|
||||||
|
v-slot="{ href, navigate, isExactActive }"
|
||||||
|
:to="item.to"
|
||||||
|
custom
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
class="nav-link"
|
||||||
|
:class="{ 'nav-link-active': isExactActive }"
|
||||||
|
:aria-current="isExactActive ? 'page' : undefined"
|
||||||
|
@click="navigate(); isMenuOpen = false"
|
||||||
|
>
|
||||||
|
<span class="nav-link-main">
|
||||||
|
<span class="nav-icon">
|
||||||
|
<IconifyIcon :icon="item.icon" class="text-lg" />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span class="nav-title">{{ item.label }}</span>
|
||||||
|
<span class="nav-caption">{{ item.caption }}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="nav-pill">{{ item.short }}</span>
|
||||||
|
</a>
|
||||||
|
</NuxtLink>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
<div class="sidebar-divider" />
|
<div class="sidebar-divider" />
|
||||||
|
<div class="status-card">
|
||||||
|
<p class="status-label">Environnement</p>
|
||||||
|
<p class="status-value">Production</p>
|
||||||
|
<p class="status-description">
|
||||||
|
Navigation rapide vers les vues principales de supervision.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class="footer-row">
|
<div class="footer-row">
|
||||||
<p class="font-mono text-[10px] tracking-widest uppercase text-white/40">
|
<p class="font-mono text-[10px] tracking-widest uppercase text-white/40">
|
||||||
Supervisor v1.0
|
Supervisor {{ appVersion }}
|
||||||
</p>
|
</p>
|
||||||
<button class="close-button" type="button" @click="isMenuOpen = false">
|
<button class="close-button" type="button" @click="isMenuOpen = false">
|
||||||
<IconifyIcon icon="mdi:close" class="text-xl" />
|
<IconifyIcon icon="mdi:close" class="text-xl" />
|
||||||
@@ -68,7 +143,26 @@ import { ref } from "vue"
|
|||||||
import { Icon as IconifyIcon } from "@iconify/vue"
|
import { Icon as IconifyIcon } from "@iconify/vue"
|
||||||
import logoSrc from '~/assets/LOGO_CARRE_BLANC.png'
|
import logoSrc from '~/assets/LOGO_CARRE_BLANC.png'
|
||||||
|
|
||||||
|
const {
|
||||||
|
public: { appVersion }
|
||||||
|
} = useRuntimeConfig()
|
||||||
const isMenuOpen = ref(false)
|
const isMenuOpen = ref(false)
|
||||||
|
const navItems = [
|
||||||
|
{
|
||||||
|
to: "/",
|
||||||
|
label: "Monitoring",
|
||||||
|
caption: "Etat global et disponibilite",
|
||||||
|
short: "MON",
|
||||||
|
icon: "mdi:chart-box-outline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: "/backup",
|
||||||
|
label: "Backup",
|
||||||
|
caption: "Scripts et fichiers archives",
|
||||||
|
short: "BKP",
|
||||||
|
icon: "mdi:database-arrow-up-outline"
|
||||||
|
}
|
||||||
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -110,6 +204,35 @@ const isMenuOpen = ref(false)
|
|||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brand-copy {
|
||||||
|
padding: 0.5rem 0 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-kicker {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgb(var(--m-accent));
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-title {
|
||||||
|
margin: 0.45rem 0 0;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 1.45rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-description {
|
||||||
|
margin: 0.55rem 0 0;
|
||||||
|
color: rgb(255 255 255 / 0.58);
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
@@ -124,11 +247,12 @@ const isMenuOpen = ref(false)
|
|||||||
|
|
||||||
.sidebar-content {
|
.sidebar-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 1rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-footer {
|
.sidebar-footer {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-divider {
|
.sidebar-divider {
|
||||||
@@ -149,6 +273,139 @@ const isMenuOpen = ref(false)
|
|||||||
padding: 0.5rem 1.5rem 0.75rem;
|
padding: 0.5rem 1.5rem 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0.75rem 0.25rem;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgb(255 255 255 / 0.38);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.85rem 0.9rem;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
border-color 0.2s ease,
|
||||||
|
transform 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background: rgb(255 255 255 / 0.06);
|
||||||
|
border-color: rgb(var(--m-accent) / 0.14);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:focus-visible {
|
||||||
|
outline: 2px solid rgb(var(--m-accent));
|
||||||
|
outline-offset: 2px;
|
||||||
|
background: rgb(255 255 255 / 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-active {
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgb(var(--m-accent) / 0.16), rgb(255 255 255 / 0.04));
|
||||||
|
border-color: rgb(var(--m-accent) / 0.24);
|
||||||
|
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgb(255 255 255 / 0.06);
|
||||||
|
color: rgb(var(--m-accent));
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title,
|
||||||
|
.nav-caption {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
font-size: 0.96rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-caption {
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: rgb(255 255 255 / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-pill {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0.28rem 0.45rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: rgb(255 255 255 / 0.62);
|
||||||
|
background: rgb(255 255 255 / 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
margin: 0 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgb(var(--m-accent) / 0.14);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top right, rgb(var(--m-accent) / 0.14), transparent 30%),
|
||||||
|
rgb(255 255 255 / 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgb(255 255 255 / 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-description {
|
||||||
|
margin: 0.45rem 0 0;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: rgb(255 255 255 / 0.54);
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
background: rgb(var(--m-bg));
|
background: rgb(var(--m-bg));
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -208,6 +465,11 @@ const isMenuOpen = ref(false)
|
|||||||
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.38);
|
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.38);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-content {
|
||||||
|
padding-right: 0.9rem;
|
||||||
|
padding-left: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
.close-button {
|
.close-button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -219,5 +481,9 @@ const isMenuOpen = ref(false)
|
|||||||
background: rgba(255, 255, 255, 0.06);
|
background: rgba(255, 255, 255, 0.06);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
padding: 0.8rem 0.85rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,32 @@
|
|||||||
|
import { execSync } from "node:child_process"
|
||||||
import tailwindcss from "@tailwindcss/vite"
|
import tailwindcss from "@tailwindcss/vite"
|
||||||
|
|
||||||
|
const getRepoVersion = () => {
|
||||||
|
try {
|
||||||
|
const tags = execSync(
|
||||||
|
"git for-each-ref --sort=-version:refname --format='%(refname:short)' refs/tags",
|
||||||
|
{ encoding: "utf8" }
|
||||||
|
)
|
||||||
|
.split("\n")
|
||||||
|
.map((tag) => tag.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
return tags[0] || "dev"
|
||||||
|
} catch {
|
||||||
|
return "dev"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2025-07-15',
|
compatibilityDate: "2025-07-15",
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
css: ["~/assets/css/main.css"],
|
css: ["~/assets/css/main.css"],
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
appVersion: getRepoVersion()
|
||||||
|
}
|
||||||
|
},
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()]
|
plugins: [tailwindcss()]
|
||||||
}
|
}
|
||||||
|
|||||||
278
pages/backup.vue
Normal file
278
pages/backup.vue
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
<template>
|
||||||
|
<NuxtLayout name="default">
|
||||||
|
<div class="dashboard-container">
|
||||||
|
<header class="dashboard-header">
|
||||||
|
<div class="header-copy">
|
||||||
|
<p class="section-kicker">Operations</p>
|
||||||
|
<h1 class="font-display text-3xl font-bold tracking-tight text-m-text">
|
||||||
|
Backup
|
||||||
|
</h1>
|
||||||
|
<p class="header-description">
|
||||||
|
Centralisez la selection des dossiers, l'execution des scripts et le telechargement
|
||||||
|
des fichiers depuis une seule vue.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<section class="grid-left" aria-label="Commandes de sauvegarde">
|
||||||
|
<BackupButtonSee
|
||||||
|
class="animate-fade-in-up backup-selector"
|
||||||
|
style="animation-delay: 120ms"
|
||||||
|
@select="selectedBackup = $event"
|
||||||
|
/>
|
||||||
|
<BackupRun
|
||||||
|
class="animate-fade-in-up"
|
||||||
|
style="animation-delay: 180ms"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="grid-middle" aria-labelledby="backup-files-title">
|
||||||
|
<div class="files-panel animate-fade-in-up" style="animation-delay: 240ms">
|
||||||
|
<div class="files-panel-header">
|
||||||
|
<div>
|
||||||
|
<p class="section-kicker">Fichiers</p>
|
||||||
|
<h2 id="backup-files-title" class="files-panel-title">
|
||||||
|
Historique des sauvegardes
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p class="files-panel-meta">
|
||||||
|
{{ selectedBackup ? `Source ${selectedBackup}` : "En attente de selection" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BackupList
|
||||||
|
class="backup-list-mobile"
|
||||||
|
:folder="selectedBackup"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NuxtLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue"
|
||||||
|
import BackupRun from "~/components/BackupRun.vue"
|
||||||
|
|
||||||
|
definePageMeta({ layout: false })
|
||||||
|
|
||||||
|
const selectedBackup = ref<string | null>(null)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard-container {
|
||||||
|
padding: 2rem 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(260px, 320px);
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: end;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-copy {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-kicker {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgb(var(--m-accent));
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-description {
|
||||||
|
max-width: 62ch;
|
||||||
|
margin-top: 0.9rem;
|
||||||
|
color: rgb(var(--m-muted));
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-card {
|
||||||
|
padding: 1.25rem;
|
||||||
|
border-radius: 16px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgb(var(--m-secondary)), rgb(var(--m-tertiary)));
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-label {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgb(var(--m-muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-value {
|
||||||
|
margin: 0.65rem 0 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgb(var(--m-text));
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-description {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
color: rgb(var(--m-muted));
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-panel {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top right, rgb(var(--m-accent) / 0.12), transparent 28%),
|
||||||
|
linear-gradient(180deg, rgb(var(--m-secondary)), rgb(var(--m-secondary) / 0.82));
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-panel::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border: 1px solid rgb(var(--m-accent) / 0.08);
|
||||||
|
border-radius: inherit;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-title {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: clamp(1.5rem, 2.2vw, 2rem);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.15;
|
||||||
|
color: rgb(var(--m-text));
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-description {
|
||||||
|
max-width: 68ch;
|
||||||
|
margin: 0.85rem 0 0;
|
||||||
|
color: rgb(var(--m-muted));
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-step {
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid rgb(var(--m-accent) / 0.08);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgb(var(--m-bg) / 0.24);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-index {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
color: rgb(var(--m-accent));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgb(var(--m-text));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-text {
|
||||||
|
margin: 0.45rem 0 0;
|
||||||
|
color: rgb(var(--m-muted));
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 300px minmax(0, 1fr);
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-left,
|
||||||
|
.grid-middle {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-panel {
|
||||||
|
padding: 1.25rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: rgb(var(--m-secondary) / 0.4);
|
||||||
|
border: 1px solid rgb(var(--m-accent) / 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-panel-title {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgb(var(--m-text));
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-panel-meta {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgb(var(--m-muted));
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1180px) {
|
||||||
|
.dashboard-header,
|
||||||
|
.dashboard-grid,
|
||||||
|
.workflow-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-panel-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-panel-meta {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.dashboard-container {
|
||||||
|
padding: 4.5rem 1.25rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-panel,
|
||||||
|
.selection-card,
|
||||||
|
.files-panel {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-grid {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -30,20 +30,10 @@
|
|||||||
<div class="dashboard-grid">
|
<div class="dashboard-grid">
|
||||||
<div class="grid-left">
|
<div class="grid-left">
|
||||||
<StatusSite class="animate-fade-in-up" style="animation-delay: 100ms" />
|
<StatusSite class="animate-fade-in-up" style="animation-delay: 100ms" />
|
||||||
<BackupButtonSee
|
|
||||||
class="animate-fade-in-up backup-selector"
|
|
||||||
style="animation-delay: 200ms"
|
|
||||||
@select="selectedBackup = $event"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid-middle">
|
<div class="grid-middle">
|
||||||
<Speedtest class="animate-fade-in-up speedtest-card-mobile" style="animation-delay: 150ms" />
|
<Speedtest class="animate-fade-in-up speedtest-card-mobile" style="animation-delay: 150ms" />
|
||||||
<BackupList
|
|
||||||
class="animate-fade-in-up backup-list-mobile"
|
|
||||||
style="animation-delay: 250ms"
|
|
||||||
:folder="selectedBackup"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
18
server/api/backup-script.get.ts
Normal file
18
server/api/backup-script.get.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import scripts from "../config/backup-script.json"
|
||||||
|
|
||||||
|
type BackupScript = {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
icon?: string
|
||||||
|
command: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler(() => {
|
||||||
|
return {
|
||||||
|
scripts: (scripts as BackupScript[]).map(({ key, label, icon }) => ({
|
||||||
|
key,
|
||||||
|
label,
|
||||||
|
icon: icon || "mdi:play-circle-outline"
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
55
server/api/backup-script.post.ts
Normal file
55
server/api/backup-script.post.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { exec } from "node:child_process"
|
||||||
|
import scripts from "../config/backup-script.json"
|
||||||
|
|
||||||
|
type BackupScript = {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
command: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCommand(command: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
exec(command, { timeout: 10 * 60 * 1000 }, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
reject(stderr || error.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve(stdout || stderr)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const body = await readBody<{ key?: string }>(event)
|
||||||
|
const key = typeof body?.key === "string" ? body.key : null
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: "Clé de script manquante"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = (scripts as BackupScript[]).find((item) => item.key === key)
|
||||||
|
if (!script) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
statusMessage: "Script introuvable"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const output = await runCommand(script.command)
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
key: script.key,
|
||||||
|
label: script.label,
|
||||||
|
output: output.trim()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: `Erreur execution script: ${String(error)}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
20
server/config/backup-script.json
Normal file
20
server/config/backup-script.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "backup-bdd-recette",
|
||||||
|
"label": "Backup BDD recette",
|
||||||
|
"icon": "mdi:database-export",
|
||||||
|
"command": "ssh malio-b@192.168.0.179 'cd /home/malio-b/Malio-ops/RecetteScripts && bash backup-bdd-recette.sh && exit'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "check-statut-recette",
|
||||||
|
"label": "Check statut recette",
|
||||||
|
"icon": "mdi:server-network",
|
||||||
|
"command": "ssh malio-b@192.168.0.179 'cd /home/malio-b/Malio-ops/RecetteScripts && bash check-statut-recette.sh && exit'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "backup-vaultwarden",
|
||||||
|
"label": "Backup vaultwarden",
|
||||||
|
"icon": "mdi:data",
|
||||||
|
"command": "ssh malio-b@192.168.0.179 'cd /home/malio-b/Malio-ops/BackupVaultWarden && bash backup-vaultwarden.sh && exit'"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
{
|
{
|
||||||
"key": "remote",
|
"key": "remote",
|
||||||
"label": "Serveur distant",
|
"label": "Serveur distant",
|
||||||
"command": "ssh malio-b@192.168.0.179 'cd /home/malio-b/Scripts-Serveur && bash check_storage.sh && exit'"
|
"command": "ssh malio-b@192.168.0.179 'cd /home/malio-b/Malio-ops/CheckStorage && bash check-storage.sh && exit'"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "local",
|
"key": "local",
|
||||||
|
|||||||
Reference in New Issue
Block a user