Add orchestrator, manual backup is functional 🎉

This commit is contained in:
SebClem 2024-04-19 16:22:30 +02:00
parent 18446ca610
commit bb68885872
Signed by: sebclem
GPG Key ID: 5A4308F6A359EA50
26 changed files with 597 additions and 236 deletions

View File

@ -0,0 +1,18 @@
import express from "express";
import { doBackupWorkflow } from "../services/orchestrator.js";
import { WorkflowType } from "../types/services/orchecstrator.js";
import logger from "../config/winston.js";
const actionRouter = express.Router();
actionRouter.post("/backup", (req, res) => {
doBackupWorkflow(WorkflowType.MANUAL)
.then(() => {
logger.info("All good !");
})
.catch((reason) => {
logger.error("Something wrong !");
});
});
export default actionRouter;

View File

@ -1,17 +1,18 @@
import express from "express"
import express from "express";
import configRouter from "./config.js";
import homeAssistant from "./homeAssistant.js"
import homeAssistant from "./homeAssistant.js";
import messageRouter from "./messages.js";
import webdavRouter from "./webdav.js";
import statusRouter from "./status.js";
import actionRouter from "./action.js";
const router = express.Router();
router.use("/homeAssistant", homeAssistant)
router.use("/homeAssistant", homeAssistant);
router.use("/config", configRouter);
router.use("/webdav", webdavRouter);
router.use("/messages", messageRouter);
router.use('/status', statusRouter);
router.use("/status", statusRouter);
router.use("/action", actionRouter);
export default router;

View File

@ -1,8 +1,13 @@
import fs from "fs";
import Joi from "joi";
import logger from "../config/winston.js";
import { type BackupConfig, BackupType } from "../types/services/backupConfig.js";
import {
type BackupConfig,
BackupType,
} from "../types/services/backupConfig.js";
import backupConfigValidation from "../types/services/backupConfigValidation.js";
import { DateTime } from "luxon";
import { WorkflowType } from "../types/services/orchecstrator.js";
const backupConfigPath = "/data/backupConfigV2.json";
@ -52,10 +57,32 @@ export function templateToRegexp(template: string) {
let regexp = template.replace("{date}", "(?<date>\\d{4}-\\d{2}-\\d{2})");
regexp = regexp.replace("{hour}", "(?<hour>\\d{4})");
regexp = regexp.replace("{hour_12}", "(?<hour12>\\d{4}(AM|PM))");
regexp = regexp.replace("{type}", "(?<type>Auto|Manual|)")
regexp = regexp.replace("{type_low}", "(?<type>auto|manual|)")
regexp = regexp.replace("{type}", "(?<type>Auto|Manual|)");
regexp = regexp.replace("{type_low}", "(?<type>auto|manual|)");
return regexp.replace(
"{ha_version}",
"(?<version>\\d+\\.\\d+\\.\\d+(b\\d+)?)"
);
}
export function getFormatedName(
workflowType: WorkflowType,
ha_version: string
) {
const setting = getBackupConfig();
let template = setting.nameTemplate;
template = template.replace(
"{type_low}",
workflowType == WorkflowType.MANUAL ? "manual" : "auto"
);
template = template.replace(
"{type}",
workflowType == WorkflowType.MANUAL ? "Manual" : "Auto"
);
template = template.replace("{ha_version}", ha_version);
const now = DateTime.now().setLocale("en");
template = template.replace("{hour_12}", now.toFormat("hhmma"));
template = template.replace("{hour}", now.toFormat("HHmm"));
template = template.replace("{date}", now.toFormat("yyyy-MM-dd"));
return template;
}

View File

@ -13,7 +13,7 @@ import logger from "../config/winston.js";
import messageManager from "../tools/messageManager.js";
import * as settingsTools from "../tools/settingsTools.js";
import * as statusTools from "../tools/status.js";
import type { NewPartialBackupPayload } from "../types/services/ha_os_payload.js";
import type { NewBackupPayload } from "../types/services/ha_os_payload.js";
import type {
AddonData,
AddonModel,
@ -25,6 +25,7 @@ import type {
} from "../types/services/ha_os_response.js";
import { States, type Status } from "../types/status.js";
import { DateTime } from "luxon";
import { BackupType } from "../types/services/backupConfig.js";
const pipeline = promisify(stream.pipeline);
@ -81,29 +82,6 @@ function getAddonList(): Promise<Response<SupervisorResponse<AddonData>>> {
);
}
function getAddonToBackup(addons: AddonModel[]) {
const excluded_addon = settingsTools.getSettings().exclude_addon;
const slugs: string[] = [];
for (const addon of addons) {
if (!excluded_addon.includes(addon.slug)) slugs.push(addon.slug);
}
logger.debug("Addon to backup:");
logger.debug(slugs);
return slugs;
}
function getFolderToBackup() {
const excluded_folder = settingsTools.getSettings().exclude_folder;
const all_folder = getFolderList();
const slugs = [];
for (const folder of all_folder) {
if (!excluded_folder.includes(folder.slug)) slugs.push(folder.slug);
}
logger.debug("Folders to backup:");
logger.debug(slugs);
return slugs;
}
function getBackups(): Promise<Response<SupervisorResponse<BackupData>>> {
const option: OptionsOfJSONResponseBody = {
headers: { authorization: `Bearer ${token}` },
@ -232,25 +210,24 @@ function getBackupInfo(id: string) {
function createNewBackup(
name: string,
addonSlugs: string[],
folders: string[]
type: BackupType,
passwordEnable: boolean,
password?: string,
addonSlugs?: string[],
folders?: string[]
) {
const status = statusTools.getStatus();
status.status = States.BKUP_CREATION;
status.progress = -1;
statusTools.setStatus(status);
logger.info("Creating new snapshot...");
const body: NewPartialBackupPayload = {
const body: NewBackupPayload = {
name: name,
addons: addonSlugs,
folders: folders,
password: passwordEnable ? password : undefined,
addons: type == BackupType.PARTIAL ? addonSlugs : undefined,
folders: type == BackupType.PARTIAL ? folders : undefined,
};
const password_protected = settingsTools.getSettings().password_protected;
logger.debug(`Is password protected ? ${password_protected}`);
if (password_protected === "true") {
body.password = settingsTools.getSettings().password_protect_value;
}
const option: OptionsOfJSONResponseBody = {
headers: { authorization: `Bearer ${token}` },
responseType: "json",
@ -259,12 +236,12 @@ function createNewBackup(
},
json: body,
};
return got
.post<SupervisorResponse<{ slug: string }>>(
`http://hassio/backups/new/partial`,
option
)
.then(
const url =
type == BackupType.PARTIAL
? "http://hassio/backups/new/partial"
: "http://hassio/backups/new/full";
return got.post<SupervisorResponse<{ slug: string }>>(url, option).then(
(result) => {
logger.info(`Snapshot created with id ${result.body.data.slug}`);
const status = statusTools.getStatus();

View File

@ -0,0 +1,133 @@
import type { AddonModel } from "../types/services/ha_os_response.js";
import { WorkflowType } from "../types/services/orchecstrator.js";
import * as backupConfigService from "./backupConfigService.js";
import * as homeAssistantService from "./homeAssistantService.js";
import { getBackupFolder, getWebdavConfig } from "./webdavConfigService.js";
import * as webDavService from "./webdavService.js";
import * as statusTools from "../tools/status.js";
import { stat, unlinkSync } from "fs";
import logger from "../config/winston.js";
import { BackupType } from "../types/services/backupConfig.js";
import { DateTime } from "luxon";
import messageManager from "../tools/messageManager.js";
export function doBackupWorkflow(type: WorkflowType) {
let name = "";
let addonsToStartStop = [] as string[];
let addonInHa = [] as AddonModel[];
let tmpBackupFile = "";
const backupConfig = backupConfigService.getBackupConfig();
const webdavConfig = getWebdavConfig();
return homeAssistantService
.getVersion()
.then((value) => {
const version = value.body.data.version;
name = backupConfigService.getFormatedName(type, version);
return homeAssistantService.getAddonList();
})
.then((response) => {
addonInHa = response.body.data.addons;
addonsToStartStop = sanitizeAddonList(
backupConfig.autoStopAddon,
response.body.data.addons
);
return webDavService.checkWebdavLogin(webdavConfig);
})
.then(() => {
return homeAssistantService.stopAddons(addonsToStartStop);
})
.then((response) => {
if (backupConfig.backupType == BackupType.FULL) {
return homeAssistantService.createNewBackup(
name,
backupConfig.backupType,
backupConfig.password.enabled,
backupConfig.password.value
);
} else {
const addons = getAddonToBackup(
backupConfig.exclude?.addon as string[],
addonInHa
);
const folders = getFolderToBackup(
backupConfig.exclude?.folder as string[],
homeAssistantService.getFolderList()
);
return homeAssistantService.createNewBackup(
name,
backupConfig.backupType,
backupConfig.password.enabled,
backupConfig.password.value,
addons,
folders
);
}
})
.then((response) => {
response.body.data.slug;
return homeAssistantService.downloadSnapshot(response.body.data.slug);
})
.then((tmpFile) => {
tmpBackupFile = tmpFile;
return webDavService.webdavUploadFile(
tmpFile,
getBackupFolder(type, webdavConfig) + name,
webdavConfig
);
})
.then(() => {
logger.info("Backup workflow finished successfully !");
messageManager.info("Backup workflow finished successfully !");
const status = statusTools.getStatus();
status.last_backup.success = true;
status.last_backup.last_try = DateTime.now();
status.last_backup.last_success = DateTime.now();
statusTools.setStatus(status);
})
.catch(() => {
backupFail();
if (tmpBackupFile != "") {
unlinkSync(tmpBackupFile);
}
return Promise.reject();
});
}
// This methods remove addon that are no installed in HA from the conf array
function sanitizeAddonList(addonInConf: string[], addonInHA: AddonModel[]) {
addonInConf.filter((value) => addonInHA.some((v) => v.slug == value));
return addonInConf;
}
function getAddonToBackup(excludedAddon: string[], addonInHA: AddonModel[]) {
const slugs: string[] = [];
for (const addon of addonInHA) {
if (!excludedAddon.includes(addon.slug)) slugs.push(addon.slug);
}
logger.debug("Addon to backup:");
logger.debug(slugs);
return slugs;
}
function getFolderToBackup(
excludedFolder: string[],
folderInHA: { name: string; slug: string }[]
) {
const slugs = [];
for (const folder of folderInHA) {
if (!excludedFolder.includes(folder.slug)) slugs.push(folder.slug);
}
logger.debug("Folders to backup:");
logger.debug(slugs);
return slugs;
}
function backupFail() {
const status = statusTools.getStatus();
status.last_backup.success = false;
status.last_backup.last_try = DateTime.now();
statusTools.setStatus(status);
messageManager.error("Last backup as failed !");
}

View File

@ -4,9 +4,13 @@ import logger from "../config/winston.js";
import { default_root } from "../tools/pathTools.js";
import {
type WebdavConfig,
WebdavEndpointType
WebdavEndpointType,
} from "../types/services/webdavConfig.js";
import WebdavConfigValidation from "../types/services/webdavConfigValidation.js";
import { BackupType } from "../types/services/backupConfig.js";
import * as pathTools from "../tools/pathTools.js";
import { WorkflowType } from "../types/services/orchecstrator.js";
import e from "express";
const webdavConfigPath = "/data/webdavConfigV2.json";
const NEXTCLOUD_ENDPOINT = "/remote.php/dav/files/$username";
@ -46,12 +50,24 @@ export function getEndpoint(config: WebdavConfig) {
} else {
return "";
}
if (endpoint.endsWith("/")) {
return endpoint.slice(0, -1);
if (!endpoint.startsWith("/")) {
endpoint = "/" + endpoint;
}
if (!endpoint.endsWith("/")) {
return endpoint + "/";
}
return endpoint;
}
export function getBackupFolder(type: WorkflowType, config: WebdavConfig) {
const end = type == WorkflowType.AUTO ? pathTools.auto : pathTools.manual;
return config.backupDir.endsWith("/")
? config.backupDir + end
: config.backupDir + "/" + end;
}
export function getWebdavDefaultConfig(): WebdavConfig {
return {
url: "",

View File

@ -268,8 +268,7 @@ export function webdavUploadFile(
},
https: { rejectUnauthorized: !config.allowSelfSignedCerts },
};
const url =
config.url + getEndpoint(config) + config.backupDir + webdavPath;
const url = config.url + getEndpoint(config) + webdavPath;
logger.debug(`...URI: ${encodeURI(url)}`);
logger.debug(`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`);

View File

@ -1,4 +1,4 @@
export interface NewPartialBackupPayload {
export interface NewBackupPayload {
name?: string;
password?: string;
homeassistant?: boolean;

View File

@ -0,0 +1,4 @@
export enum WorkflowType {
AUTO,
MANUAL,
}

View File

@ -17,7 +17,7 @@ export interface Status {
last_backup: {
success?: boolean;
last_success?: DateTime;
message?: string;
last_try?: DateTime;
};
next_backup?: string;
webdav: {

View File

@ -7,6 +7,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
ActionComponent: typeof import('./src/components/statusBar/ActionComponent.vue')['default']
AlertManager: typeof import('./src/components/AlertManager.vue')['default']
BackupConfigAddon: typeof import('./src/components/settings/BackupConfig/BackupConfigAddon.vue')['default']
BackupConfigAutoBackup: typeof import('./src/components/settings/BackupConfig/BackupConfigAutoBackup.vue')['default']
@ -16,9 +17,11 @@ declare module 'vue' {
BackupConfigForm: typeof import('./src/components/settings/BackupConfigForm.vue')['default']
BackupConfigMenu: typeof import('./src/components/settings/BackupConfigMenu.vue')['default']
BackupConfigSecurity: typeof import('./src/components/settings/BackupConfig/BackupConfigSecurity.vue')['default']
BackupStatus: typeof import('./src/components/statusBar/BackupStatus.vue')['default']
CloudDeleteDialog: typeof import('./src/components/cloud/CloudDeleteDialog.vue')['default']
CloudList: typeof import('./src/components/cloud/CloudList.vue')['default']
CloudListItem: typeof import('./src/components/cloud/CloudListItem.vue')['default']
ConnectionStatus: typeof import('./src/components/statusBar/ConnectionStatus.vue')['default']
HaList: typeof import('./src/components/homeAssistant/HaList.vue')['default']
HaListItem: typeof import('./src/components/homeAssistant/HaListItem.vue')['default']
HaListItemContent: typeof import('./src/components/homeAssistant/HaListItemContent.vue')['default']
@ -26,7 +29,8 @@ declare module 'vue' {
NavbarComponent: typeof import('./src/components/NavbarComponent.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
StatusComponent: typeof import('./src/components/StatusComponent.vue')['default']
StatusBar: typeof import('./src/components/statusBar/StatusBar.vue')['default']
StatusComponent: typeof import('./src/components/statusBar/StatusComponent.vue')['default']
WebdavConfigForm: typeof import('./src/components/settings/WebdavConfigForm.vue')['default']
WebdavConfigMenu: typeof import('./src/components/settings/WebdavConfigMenu.vue')['default']
}

View File

@ -5,17 +5,13 @@
<webdav-config-menu @saved="cloudList?.refreshBackup"></webdav-config-menu>
<backup-config-menu></backup-config-menu>
<alert-manager></alert-manager>
<v-main class="mx-12">
<v-main class="mx-xl-16 mx-lg-10 mx-2">
<StatusBar @state-updated="refreshLists"></StatusBar>
<v-row>
<v-col cols="4" offset="1">
<status-component></status-component>
<v-col cols="12" lg="6">
<ha-list ref="haList"></ha-list>
</v-col>
</v-row>
<v-row>
<v-col cols="6">
<ha-list></ha-list>
</v-col>
<v-col cols="6">
<v-col cols="12" lg="6">
<cloud-list ref="cloudList"></cloud-list>
</v-col>
</v-row>
@ -32,8 +28,14 @@ import MessageBar from "./components/MessageBar.vue";
import NavbarComponent from "./components/NavbarComponent.vue";
import BackupConfigMenu from "./components/settings/BackupConfigMenu.vue";
import WebdavConfigMenu from "./components/settings/WebdavConfigMenu.vue";
import StatusComponent from "./components/StatusComponent.vue";
import StatusBar from "./components/statusBar/StatusBar.vue";
const cloudList = ref<InstanceType<typeof CloudList> | null>(null);
const haList = ref<InstanceType<typeof HaList> | null>(null);
function refreshLists() {
cloudList.value?.refreshBackup();
haList.value?.refreshBackup();
}
</script>
<style scoped></style>

View File

@ -1,130 +0,0 @@
<template>
<div>
<v-card class="mt-5" border elevation="10">
<v-card-title class="text-center">Status</v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-row>
<v-col cols="6">
<v-card variant="elevated" border>
<v-card-text class="align-center d-flex justify-center">
<span class="me-auto">Home Assistant</span>
<v-tooltip content-class="bg-black">
<template v-slot:activator="{ props }">
<v-chip
v-bind="props"
variant="elevated"
:prepend-icon="hassProps.icon"
:color="hassProps.color"
:text="hassProps.text"
>
</v-chip>
</template>
Last check:
{{
status?.hass.last_check
? DateTime.fromISO(status.hass.last_check).toLocaleString(
DateTime.DATETIME_MED
)
: "UNKNOWN"
}}
</v-tooltip>
</v-card-text>
</v-card>
</v-col>
<v-col cols="6">
<v-card variant="elevated" border>
<v-card-text class="align-center d-flex justify-center">
<span class="me-auto">Cloud</span>
<v-tooltip content-class="bg-black">
<template v-slot:activator="{ props }">
<v-chip
v-bind="props"
variant="elevated"
:prepend-icon="webdavProps.icon"
:color="webdavProps.color"
:text="webdavProps.text"
>
</v-chip>
</template>
<span>Login: </span>
<span :class="'text-' + webdavLoggedProps.color">
{{ webdavLoggedProps.text }}
</span>
<br />
<span>Folder: </span>
<span :class="'text-' + webdavFolderProps.color">
{{ webdavFolderProps.text }}
</span>
<p>
Last check:
{{
status?.webdav.last_check
? DateTime.fromISO(
status.webdav.last_check
).toLocaleString(DateTime.DATETIME_MED)
: "UNKNOWN"
}}
</p>
</v-tooltip>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</div>
</template>
<script setup lang="ts">
import { getStatus } from "@/services/statusService";
import { Status } from "@/types/status";
import { computed, ref, onBeforeUnmount } from "vue";
import { DateTime } from "luxon";
const status = ref<Status | undefined>(undefined);
function refreshStatus() {
getStatus().then((data) => {
status.value = data;
});
}
const webdavProps = computed(() => {
if (status.value?.webdav.logged_in && status.value?.webdav.folder_created) {
return { icon: "mdi-check", text: "Ok", color: "green" };
} else {
return { icon: "mdi-alert", text: "Fail", color: "red" };
}
});
const webdavLoggedProps = computed(() => {
if (status.value?.webdav.logged_in) {
return { text: "Ok", color: "green" };
} else {
return { text: "Fail", color: "red" };
}
});
const webdavFolderProps = computed(() => {
if (status.value?.webdav.folder_created) {
return { text: "Ok", color: "green" };
} else {
return { text: "Fail", color: "red" };
}
});
const hassProps = computed(() => {
if (status.value?.hass.ok) {
return { icon: "mdi-check", text: "Ok", color: "green" };
} else {
return { icon: "mdi-alert", text: "Fail", color: "red" };
}
});
refreshStatus();
const interval = setInterval(refreshStatus, 2000);
onBeforeUnmount(() => {
clearInterval(interval);
});
</script>

View File

@ -15,7 +15,7 @@
></v-btn>
</v-col>
</v-row>
<v-divider></v-divider>
<v-divider class="border-opacity-25"></v-divider>
<v-card-text>
<v-row>
<v-col>

View File

@ -90,7 +90,7 @@
</v-col>
</v-row>
</v-card-text>
<v-divider class="mx-4"></v-divider>
<v-divider class="mx-4 border-opacity-25"></v-divider>
<v-card-actions class="justify-center">
<v-tooltip text="Upload to Home Assitant" location="bottom">
<template v-slot:activator="{ props }">

View File

@ -15,7 +15,7 @@
></v-btn>
</v-col>
</v-row>
<v-divider></v-divider>
<v-divider class="border-opacity-25"></v-divider>
<v-card-text>
<v-row>
<v-col>
@ -60,15 +60,19 @@ const loading = ref<boolean>(true);
function refreshBackup() {
loading.value = true;
getBackups().then((value) => {
getBackups()
.then((value) => {
backups.value = value;
loading.value = false;
}).catch(()=> {
loading.value = false;
})
.catch(() => {
loading.value = false;
});
}
refreshBackup();
defineExpose({ refreshBackup });
// TODO Manage delete
</script>

View File

@ -120,7 +120,7 @@
<h3>Content</h3>
</v-col>
</v-row>
<v-divider class="mt-2"></v-divider>
<v-divider class="mt-2 border-opacity-25"></v-divider>
<v-row>
<v-col cols="12" lg="6">
<div class="text-center text-white mt-2">
@ -152,7 +152,7 @@
</v-row>
</template>
</v-card-text>
<v-divider class="mx-4"></v-divider>
<v-divider class="mx-4 border-opacity-25"></v-divider>
<v-card-actions class="justify-center">
<v-tooltip text="Upload to Cloud" location="bottom">
<template v-slot:activator="{ props }">

View File

@ -55,7 +55,7 @@
</v-col>
</v-row>
</v-fade-transition>
<v-divider class="my-4"></v-divider>
<v-divider class="my-4 border-opacity-25"></v-divider>
<v-row>
<v-col class="text-center">
<v-sheet border elevation="5" rounded class="py-1">
@ -78,7 +78,7 @@
<BackupConfigAutoStop :loading="loading"></BackupConfigAutoStop>
</v-col>
</v-row>
<v-divider class="my-4"></v-divider>
<v-divider class="my-4 border-opacity-25"></v-divider>
<v-row class="mb-10">
<v-col>
<BackupConfigSecurity :loading="loading"></BackupConfigSecurity>

View File

@ -8,7 +8,7 @@
>
<v-card>
<v-card-title class="text-center">Backup Settings</v-card-title>
<v-divider></v-divider>
<v-divider class="border-opacity-25"></v-divider>
<v-card-text>
<backup-config-form
ref="form"
@ -18,7 +18,7 @@
@loading="loading = true"
></backup-config-form>
</v-card-text>
<v-divider></v-divider>
<v-divider class="border-opacity-25"></v-divider>
<v-card-actions class="justify-end">
<v-btn
color="red"

View File

@ -0,0 +1,26 @@
<template>
<v-card border>
<v-card-title class="text-center">Action</v-card-title>
<v-divider class="border-opacity-25"></v-divider>
<v-card-text>
<v-btn color="success" @click="launchBackup">Backup Now</v-btn>
</v-card-text>
</v-card>
</template>
<script setup lang="ts">
import { backupNow } from "@/services/actionService";
import { useAlertStore } from "@/store/alert";
const alertStore = useAlertStore();
function launchBackup() {
backupNow()
.then(() => {
alertStore.add("success", "Backup workflow started !");
})
.catch(() => {
alertStore.add("error", "Fail to start backup workflow !");
});
}
</script>

View File

@ -0,0 +1,102 @@
<template>
<v-card border>
<v-card-item>
<v-card-title class="text-center">Backup</v-card-title>
</v-card-item>
<v-divider class="border-opacity-25"></v-divider>
<v-card-text>
<v-row>
<v-col xl="6" lg="12" sm="6" cols="12">
<div class="h-100 d-flex align-center">
<span class="me-auto">Last</span>
<v-tooltip content-class="bg-black">
<template v-slot:activator="{ props }">
<v-chip
v-bind="props"
variant="elevated"
:prepend-icon="lastBackupProps.icon"
:color="lastBackupProps.color"
:text="lastBackupProps.text"
>
</v-chip>
</template>
<p>
Last try:
{{
status?.last_backup.last_try
? DateTime.fromISO(
status?.last_backup.last_try
).toLocaleString(DateTime.DATETIME_MED)
: "Unknown"
}}
</p>
<p>
Last success:
{{
status?.last_backup.last_success
? DateTime.fromISO(
status?.last_backup.last_success
).toLocaleString(DateTime.DATETIME_MED)
: "Unknown"
}}
</p>
</v-tooltip>
</div>
</v-col>
<v-divider vertical class="border-opacity-25 mt-n1"></v-divider>
<v-col xl="6" lg="12" sm="6" cols="12">
<div class="h-100 d-flex align-center">
<span class="me-auto">Next</span>
</div>
</v-col>
</v-row>
<v-row>
<v-divider class="border-opacity-25 mx-n1"></v-divider>
</v-row>
<v-row>
<v-col>
<v-progress-linear
height="25"
:model-value="percent"
:indeterminate="
status?.progress == -1 && status.status != States.IDLE
"
class=""
color="success"
rounded
>
<strong>{{ status?.status }}</strong>
</v-progress-linear>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script setup lang="ts">
import { States, Status } from "@/types/status";
import { DateTime } from "luxon";
import { computed } from "vue";
const props = defineProps<{
status?: Status;
}>();
const percent = computed(() => {
if (props.status?.status == States.IDLE || !props.status?.progress) {
return 0;
} else {
return props.status.progress * 100;
}
});
const lastBackupProps = computed(() => {
if (props.status?.last_backup.success == undefined) {
return { icon: "mdi-help-circle", text: "Unknown", color: "" };
} else if (props.status?.last_backup.success) {
return { icon: "mdi-check", text: "Success", color: "green" };
} else {
return { icon: "mdi-alert", text: "Fail", color: "red" };
}
});
</script>

View File

@ -0,0 +1,126 @@
<template>
<v-card border elevation="10">
<v-card-item>
<v-card-title class="text-center">Status</v-card-title>
</v-card-item>
<v-divider class="border-opacity-25"></v-divider>
<v-card-text class="h-auto">
<v-row align-content="space-around">
<v-col xl="6" lg="12" sm="6" cols="12">
<div class="h-100 d-flex align-center">
<span class="me-auto">Home Assistant</span>
<v-tooltip content-class="bg-black">
<template v-slot:activator="{ props }">
<v-chip
v-bind="props"
variant="elevated"
:prepend-icon="hassProps.icon"
:color="hassProps.color"
:text="hassProps.text"
>
</v-chip>
</template>
Last check:
{{
status?.hass.last_check
? DateTime.fromISO(status.hass.last_check).toLocaleString(
DateTime.DATETIME_MED
)
: "UNKNOWN"
}}
</v-tooltip>
</div>
</v-col>
<v-divider vertical class="border-opacity-25 my-n1"></v-divider>
<v-col xl="6" lg="12" sm="6" cols="12">
<div class="h-100 d-flex align-center">
<span class="me-auto">Cloud</span>
<v-tooltip content-class="bg-black">
<template v-slot:activator="{ props }">
<v-chip
v-bind="props"
variant="elevated"
:prepend-icon="webdavProps.icon"
:color="webdavProps.color"
:text="webdavProps.text"
>
</v-chip>
</template>
<span>Login: </span>
<span :class="'text-' + webdavLoggedProps.color">
{{ webdavLoggedProps.text }}
</span>
<br />
<span>Folder: </span>
<span :class="'text-' + webdavFolderProps.color">
{{ webdavFolderProps.text }}
</span>
<p>
Last check:
{{
status?.webdav.last_check
? DateTime.fromISO(status.webdav.last_check).toLocaleString(
DateTime.DATETIME_MED
)
: "UNKNOWN"
}}
</p>
</v-tooltip>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script setup lang="ts">
import { Status } from "@/types/status";
import { DateTime } from "luxon";
import { computed } from "vue";
const props = defineProps<{
status?: Status;
}>();
const webdavProps = computed(() => {
if (
props.status?.webdav.logged_in == undefined ||
props.status?.webdav.folder_created == undefined
) {
return { icon: "mdi-help-circle", text: "Unknown", color: "" };
} else if (
props.status?.webdav.logged_in &&
props.status?.webdav.folder_created
) {
return { icon: "mdi-check", text: "Ok", color: "green" };
} else {
return { icon: "mdi-alert", text: "Fail", color: "red" };
}
});
const webdavLoggedProps = computed(() => {
if (props.status?.webdav.logged_in) {
return { text: "Ok", color: "green" };
} else {
return { text: "Fail", color: "red" };
}
});
const webdavFolderProps = computed(() => {
if (props.status?.webdav.folder_created) {
return { text: "Ok", color: "green" };
} else {
return { text: "Fail", color: "red" };
}
});
const hassProps = computed(() => {
if (props.status?.hass.ok == undefined) {
return { icon: "mdi-help-circle", text: "Unknown", color: "" };
} else if (props.status?.hass.ok) {
return { icon: "mdi-check", text: "Ok", color: "green" };
} else {
return { icon: "mdi-alert", text: "Fail", color: "red" };
}
});
</script>

View File

@ -0,0 +1,47 @@
<template>
<v-row class="mt-5 justify-space-around">
<v-col cols="12" lg="4" xxl="3">
<ConnectionStatus :status="status"></ConnectionStatus>
</v-col>
<v-col cols="12" lg="4" xxl="3">
<BackupStatus :status="status"></BackupStatus>
</v-col>
<v-col cols="12" sm="12" lg="4" xxl="3">
<ActionComponent></ActionComponent>
</v-col>
</v-row>
</template>
<script setup lang="ts">
import { getStatus } from "@/services/statusService";
import { States, Status } from "@/types/status";
import { computed, ref, onBeforeUnmount } from "vue";
import { DateTime } from "luxon";
import ConnectionStatus from "./ConnectionStatus.vue";
import BackupStatus from "./BackupStatus.vue";
import ActionComponent from "./ActionComponent.vue";
const status = ref<Status | undefined>(undefined);
let oldStatus: States | undefined = undefined;
const emit = defineEmits<{
(e: "stateUpdated", state: string): void;
}>();
function refreshStatus() {
getStatus().then((data) => {
status.value = data;
if (oldStatus != status.value.status) {
oldStatus = status.value.status;
emit("stateUpdated", status.value.status);
}
});
}
refreshStatus();
const interval = setInterval(refreshStatus, 500);
onBeforeUnmount(() => {
clearInterval(interval);
});
</script>

View File

@ -0,0 +1,5 @@
import kyClient from "./kyClient";
export function backupNow() {
return kyClient.post("action/backup");
}

View File

@ -17,7 +17,7 @@ export interface Status {
last_backup: {
success?: boolean;
last_success?: string;
message?: string;
last_try?: string;
};
next_backup?: string;
webdav: {