Merge branch 'bootstrap'

This commit is contained in:
Sebastien Clement 2021-01-09 00:28:02 +01:00
commit 84c3027d97
22 changed files with 11738 additions and 2748 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -13,7 +13,7 @@ Auto backup can be configure via the Home Assistant web interface.
### Features ### Features
- Auto Backup : Configure this add-on to automaticaly backup your HassIO instance. - Auto Backup : Configure this add-on to automaticaly backup your HassIO instance.
- Auto Clean : You can specify the maximum number of local snapshots and (__ONLY__) auto backup snapshots. - Auto Clean : You can specify the maximum number of local snapshots and (__ONLY__) auto backup snapshots.
- ~~Restore backups.~~ (Coming Soon) - Upload backed-up snapshot to Home assistant.
> __Info:__ > __Info:__
> Auto Clean is executed after every upload and every day at 00h30 > Auto Clean is executed after every upload and every day at 00h30

View File

@ -13,7 +13,7 @@ Auto backup can be configured via the web interface.
### Features ### Features
- Auto Backup : Configure this add-on to automaticly backup your HassIO instance. - Auto Backup : Configure this add-on to automaticly backup your HassIO instance.
- Auto Clean : You can specify the maximum number of local snapshots and (__ONLY__) auto backed-up snapshots. - Auto Clean : You can specify the maximum number of local snapshots and (__ONLY__) auto backed-up snapshots.
- ~~Restore backups.~~ (Coming Soon) - Upload backed-up snapshot to Home assistant.
> __Info:__ > __Info:__
> Auto Clean is executed after every upload and every day at 00h30 > Auto Clean is executed after every upload and every day at 00h30

View File

@ -27,6 +27,28 @@ app.use(express.static(path.join(__dirname, "public")));
app.use("/", indexRouter); app.use("/", indexRouter);
app.use("/api", apiRouter); app.use("/api", apiRouter);
/*
-----------------------------------------------------------
Library statics
----------------------------------------------------------
*/
// Boootstrap JS Files
app.use('/js/bootstrap.min.js', express.static(path.join(__dirname, '/node_modules/bootstrap/dist/js/bootstrap.min.js')))
// Fontawesome Files
app.use('/css/fa-all.min.css', express.static(path.join(__dirname, '/node_modules/@fortawesome/fontawesome-free/css/all.min.css')))
app.use('/webfonts/', express.static(path.join(__dirname, '/node_modules/@fortawesome/fontawesome-free/webfonts')))
// Jquery JS Files
app.use('/js/jquery.min.js', express.static(path.join(__dirname, '/node_modules/jquery/dist/jquery.min.js')))
/*
-----------------------------------------------------------
Error handler
----------------------------------------------------------
*/
// catch 404 and forward to error handler // catch 404 and forward to error handler
app.use(function (req, res, next) { app.use(function (req, res, next) {
next(createError(404)); next(createError(404));
@ -43,6 +65,12 @@ app.use(function (err, req, res, next) {
res.render("error"); res.render("error");
}); });
/*
-----------------------------------------------------------
Init app
----------------------------------------------------------
*/
const fs = require("fs"); const fs = require("fs");
const newlog = require("./config/winston"); const newlog = require("./config/winston");
if (!fs.existsSync("/data")) fs.mkdirSync("/data"); if (!fs.existsSync("/data")) fs.mkdirSync("/data");

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,9 @@
"start": "node --inspect=0.0.0.0:9226 ./bin/www " "start": "node --inspect=0.0.0.0:9226 ./bin/www "
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"app-root-path": "^3.0.0", "app-root-path": "^3.0.0",
"bootstrap": "^5.0.0-beta1",
"cookie-parser": "^1.4.5", "cookie-parser": "^1.4.5",
"cron": "^1.8.2", "cron": "^1.8.2",
"debug": "~2.6.9", "debug": "~2.6.9",
@ -15,16 +17,10 @@
"form-data": "^3.0.0", "form-data": "^3.0.0",
"got": "^11.8.1", "got": "^11.8.1",
"http-errors": "~1.6.3", "http-errors": "~1.6.3",
"jquery": "^3.5.1",
"moment": "^2.29.1", "moment": "^2.29.1",
"morgan": "~1.9.1", "morgan": "~1.9.1",
"webdav": "^3.6.2", "webdav": "^3.6.2",
"winston": "^3.3.3" "winston": "^3.3.3"
},
"devDependencies": {
"eslint": "^7.17.0",
"eslint-config-eslint": "^6.0.0",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0"
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,15 @@
.navbar {
background-color: #0091ea;
}
#header-box {
min-height: 160px;
}
.toast-container {
position: fixed;
z-index: 1055;
margin: 5px;
top: 58px;
right: 0;
}

View File

@ -0,0 +1,448 @@
var last_status = "";
var last_local_snap = "";
var last_manu_back = "";
var last_auto_back = "";
const default_toast_timeout = 10000;
let loadingModal;
let nextcloud_setting_modal;
let backup_setting_modal;
document.addEventListener('DOMContentLoaded', function () {
$.ajaxSetup({ traditional: true });
updateLocalSnaps();
update_status();
loadingModal = new bootstrap.Modal(document.getElementById('loading-modal'), {
keyboard: false,
backdrop: 'static'
});
nextcloud_setting_modal = new bootstrap.Modal(document.getElementById('modal-settings-nextcloud'));
backup_setting_modal = new bootstrap.Modal(document.getElementById('modal-settings-backup'));
setInterval(update_status, 500);
setInterval(updateLocalSnaps, 5000);
listeners();
});
function updateDynamicListeners() {
$('.local-snap-listener').click(function () {
let id = this.getAttribute('data-id');
console.log(id);
});
let manual_back_list = $('.manual-back-list');
manual_back_list.unbind();
manual_back_list.click(function () {
let id = this.getAttribute('data-id');
let name = this.getAttribute('data-name');
manualBackup(id, name);
})
$('.restore').click(function () {
let to_restore = this.getAttribute('data-id');
console.log(to_restore)
restore(to_restore)
})
}
function updateLocalSnaps() {
let needUpdate = false;
$.get('./api/formated-local-snap', (data) => {
if (JSON.stringify(data) === last_local_snap)
return;
last_local_snap = JSON.stringify(data);
needUpdate = true;
let local_snaps = $('#local_snaps');
local_snaps.empty();
local_snaps.html(data);
}).always(() => {
updateManuBackup(needUpdate);
});
}
function updateManuBackup(prevUpdate) {
let needUpdate = false;
$.get('./api/formated-backup-manual', (data) => {
if (JSON.stringify(data) === last_manu_back)
return;
last_manu_back = JSON.stringify(data);
needUpdate = true;
let manual_backups = $('#manual_backups')
manual_backups.empty();
manual_backups.html(data);
}).always(() => {
updateAutoBackup(prevUpdate || needUpdate);
});
}
function updateAutoBackup(prevUpdate) {
let needUpdate = false;
$.get('./api/formated-backup-auto', (data) => {
if (JSON.stringify(data) === last_auto_back)
return;
needUpdate = true;
last_auto_back = JSON.stringify(data);
let auto_backups = $('#auto_backups')
auto_backups.empty();
auto_backups.html(data);
}).always(() => {
if (prevUpdate || needUpdate)
updateDynamicListeners();
});
}
function update_status() {
$.get('./api/status', (data) => {
if (JSON.stringify(data) !== last_status) {
last_status = JSON.stringify(data);
let buttons = $('#btn-backup-now, #btn-clean-now');
switch (data.status) {
case "error":
printStatus('Error', data.message);
buttons.removeClass("disabled");
break;
case "idle":
printStatus('Idle', "Waiting for next backup.");
buttons.removeClass("disabled");
break;
case "download":
printStatusWithBar('Downloading Snapshot', data.progress);
buttons.addClass("disabled");
break;
case "download-b":
printStatusWithBar('Downloading Backup', data.progress);
buttons.addClass("disabled");
break;
case "upload":
printStatusWithBar('Uploading Snapshot', data.progress);
buttons.addClass("disabled");
break;
case "upload-b":
printStatusWithBar('Uploading Snapshot', data.progress);
buttons.addClass("disabled");
break;
case "creating":
printStatusWithBar('Creating Snapshot', data.progress);
buttons.addClass("disabled");
break;
}
if (data.last_backup != null) {
let last_back_status = $('#last_back_status');
if (last_back_status.html() !== data.last_backup)
last_back_status.html(data.last_backup);
}
if (data.next_backup != null) {
let next_back_status = $('#next_back_status');
if (next_back_status.html() !== data.next_backup)
next_back_status.html(data.next_backup);
}
}
});
}
function printStatus(status, secondLine) {
let status_jq = $('#status');
status_jq.empty();
status_jq.html(status);
let status_s_l_jq = $('#status-second-line');
status_s_l_jq.empty();
status_s_l_jq.removeClass('text-center');
status_s_l_jq.html(secondLine);
$('#progress').addClass("invisible");
}
function printStatusWithBar(status, progress) {
let status_jq = $('#status')
status_jq.empty();
status_jq.html(status);
let secondLine = $('#status-second-line')
secondLine.empty();
secondLine.html(progress === -1 ? "Waiting..." : (Math.round(progress * 100) + "%"));
secondLine.addClass("text-center");
let progressDiv = $('#progress');
progressDiv.removeClass("invisible");
if (progress === -1) {
progressDiv.children().css('width', "100%");
progressDiv.children().addClass('progress-bar-striped progress-bar-animated');
} else {
progressDiv.children().removeClass('progress-bar-striped progress-bar-animated');
progressDiv.children().css('width', (progress * 100) + "%");
}
}
function listeners() {
$('#btn-backup-now').click(backupNow);
$('#btn-clean-now').click(cleanNow);
$('#trigger-backup-settings').click(getBackupSettings);
$('#cron-drop-settings').change(updateDropVisibility);
$('#save-backup-settings').click(sendBackupSettings);
$('#trigger-nextcloud-settings').click(getNextcloudSettings);
$('#save-nextcloud-settings').click(sendNextcloudSettings);
$('#ssl').change(function () {
let div = $('#self_signed').parent().parent();
if ($('#ssl').is(':checked'))
div.removeClass("invisible")
else
div.addClass("invisible");
});
}
function restore(id) {
loadingModal.show();
$.post('./api/restore', { path: id })
.done(() => {
console.log("Restore cmd send !");
create_toast("success", "Command send !", default_toast_timeout);
})
.fail((error) => {
console.log(error);
create_toast("error", "Can't send command !", default_toast_timeout);
})
.always(() => loadingModal.hide())
}
function sendNextcloudSettings() {
loadingModal.show();
nextcloud_setting_modal.hide();
let ssl = $('#ssl').is(':checked')
let self_signed = $('#self_signed').is(':checked')
let hostname = $('#hostname').val();
let username = $('#username').val();
let password = $('#password').val();
let back_dir = $('#back-dir').val();
$.post('./api/nextcloud-settings', {
ssl: ssl,
host: hostname,
username: username,
password: password,
back_dir: back_dir,
self_signed: self_signed
})
.done(() => {
console.log('Saved');
$('#nextcloud_settings_message').parent().addClass("d-none");
create_toast("success", "Nextcloud settings saved !", default_toast_timeout);
0
})
.fail((data) => {
let nextcloud_settings_message = $('#nextcloud_settings_message')
if (data.status === 406) {
console.log(data.responseJSON.message);
nextcloud_settings_message.html(data.responseJSON.message);
} else {
nextcloud_settings_message.html("Invalid Settings.");
}
nextcloud_settings_message.parent().removeClass("d-none");
nextcloud_setting_modal.show();
create_toast("error", "Invalid Nextcloud settings !", default_toast_timeout);
console.log('Fail');
}).always(() => {
loadingModal.hide();
})
}
function manualBackup(id, name) {
$.post(`./api/manual-backup?id=${id}&name=${name}`)
.done(() => {
console.log("manual bk cmd send !");
create_toast("success", "Command send !", default_toast_timeout);
})
.fail((error) => {
console.log(error);
create_toast("error", "Can't send command !", default_toast_timeout);
})
}
function getNextcloudSettings() {
loadingModal.show();
$.get('./api/nextcloud-settings', (data) => {
$('#ssl').prop("checked", data.ssl === "true");
let sef_signed_jq = $('#self_signed')
if (data.ssl === "true") {
let div = sef_signed_jq.parent().parent();
div.removeClass("invisible");
}
sef_signed_jq.prop('checked', data.self_signed === "true")
$('#hostname').val(data.host);
$('#username').val(data.username);
$('#password').val(data.password);
$('#back-dir').val(data.back_dir);
loadingModal.hide();
nextcloud_setting_modal.show();
});
}
function backupNow() {
loadingModal.show();
$.post('./api/new-backup')
.done(() => {
create_toast("success", "Command send !", default_toast_timeout);
})
.fail((error) => {
console.log(error);
create_toast("error", "Can't send command !", default_toast_timeout);
})
.always(() => {
loadingModal.hide();
})
}
function cleanNow() {
loadingModal.show();
$.post('./api/clean-now')
.done(() => {
create_toast("success", "Command send !", default_toast_timeout);
})
.fail((error) => {
console.log(error);
create_toast("error", "Can't send command !", default_toast_timeout);
})
.always(() => {
loadingModal.hide();
})
}
function getBackupSettings() {
loadingModal.show();
$.get('./api/backup-settings', (data) => {
if (JSON.stringify(data) === "{}") {
data = {
cron_base: "0",
cron_hour: "00:00",
cron_weekday: "0",
cron_month_day: "1",
auto_clean_local: false,
auto_clean_local_keep: 5,
auto_clean_backup: false,
auto_clean_backup_keep: 5,
}
}
$('#cron-drop-settings').val(data.settings.cron_base);
$('#name-template').val(data.settings.name_template);
$('#timepicker').val(data.settings.cron_hour);
$('#cron-drop-day-month-read').val(data.settings.cron_month_day);
$('#cron-drop-day-month').val(data.settings.cron_month_day);
$('#auto_clean_local').prop('checked', data.settings.auto_clean_local === "true");
$('#local-snap-keep').val(data.settings.auto_clean_local_keep);
$('#auto_clean_backup').prop('checked', data.settings.auto_clean_backup === "true");
$('#backup-snap-keep').val(data.settings.auto_clean_backup_keep);
$('#cron-drop-day').val(data.settings.cron_weekday);
let folder_html = ""
for (let thisFolder of data.folders) {
let exclude = data.settings.exclude_folder.includes(thisFolder.slug);
folder_html += `<li class="list-group-item"><div class="form-check"><input class="form-check-input folders-box" type="checkbox" id="${thisFolder.slug}" ${exclude ? "" : "checked"}><label class="form-label mb-0" for="${thisFolder.slug}">${thisFolder.name}</label></div></li>`
}
$("#folders-div").html(folder_html);
let addons_html = ""
for (let thisAddon of data.addonList) {
let exclude = data.settings.exclude_addon.includes(thisAddon.slug);
addons_html += `<li class="list-group-item"><div class="form-check"><input class="form-check-input addons-box" type="checkbox" id="${thisAddon.slug}" ${exclude ? "" : "checked"}><label class="form-label mb-0" for="${thisAddon.slug}">${thisAddon.name}</label></div></li>`
}
$("#addons-div").html(addons_html);
updateDropVisibility();
loadingModal.hide();
backup_setting_modal.show();
});
}
function updateDropVisibility() {
let cronBase = $("#cron-drop-settings").val();
let timepicker = $('#timepicker');
let cron_drop_day = $('#cron-drop-day');
let cron_drop_day_mount = $('#cron-drop-day-month');
switch (cronBase) {
case "3":
timepicker.parent().parent().removeClass("d-none");
cron_drop_day.parent().parent().addClass("d-none");
cron_drop_day_mount.parent().parent().removeClass("d-none");
break;
case "2":
timepicker.parent().parent().removeClass("d-none");
cron_drop_day.parent().parent().removeClass("d-none");
cron_drop_day_mount.parent().parent().addClass("d-none");
break;
case "1":
timepicker.parent().parent().removeClass("d-none");
cron_drop_day.parent().parent().addClass("d-none");
cron_drop_day_mount.parent().parent().addClass("d-none");
break;
case "0":
timepicker.parent().parent().addClass("d-none");
cron_drop_day.parent().parent().addClass("d-none");
cron_drop_day_mount.parent().parent().addClass("d-none");
break;
}
}
function sendBackupSettings() {
let cron_month_day = $('#cron-drop-day-month').val();
let cron_weekday = $('#cron-drop-day').val();
let cron_hour = $('#timepicker').val();
let cron_base = $('#cron-drop-settings').val();
let auto_clean_local = $("#auto_clean_local").is(':checked');
let auto_clean_backup = $("#auto_clean_backup").is(':checked');
let auto_clean_local_keep = $("#local-snap-keep").val();
let auto_clean_backup_keep = $("#backup-snap-keep").val();
let name_template = $('#name-template').val();
let excluded_folders_nodes = document.querySelectorAll('.folders-box:not(:checked)');
let exclude_folder = [""];
for (let i of excluded_folders_nodes) {
exclude_folder.push(i.id);
}
let excluded_addons_nodes = document.querySelectorAll('.addons-box:not(:checked)');
let exclude_addon = [""];
for (let i of excluded_addons_nodes) {
exclude_addon.push(i.id);
}
loadingModal.show();
backup_setting_modal.hide();
$.post('./api/backup-settings',
{
name_template: name_template,
cron_base: cron_base,
cron_hour: cron_hour,
cron_weekday: cron_weekday,
cron_month_day: cron_month_day,
auto_clean_local: auto_clean_local,
auto_clean_local_keep: auto_clean_local_keep,
auto_clean_backup: auto_clean_backup,
auto_clean_backup_keep: auto_clean_backup_keep,
exclude_addon: exclude_addon,
exclude_folder: exclude_folder
})
.done(() => {
create_toast("success", "Backup settings saved !", default_toast_timeout);
})
.fail(() => {
create_toast("error", "Can't save backup settings !", default_toast_timeout);
backup_setting_modal.show();
})
.always(() => {
loadingModal.hide();
});
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,30 @@
function create_toast(type, message, delay) {
let toast_class;
let icon_class;
switch (type) {
case 'error':
toast_class = 'bg-danger';
icon_class = 'fa-exclamation-triangle'
break;
default:
toast_class = `bg-${type}`
icon_class = 'fa-check-square'
}
let toast_id = Date.now().toString();
let toast_html = `<div id="${toast_id}" class="toast d-flex align-items-center text-white ${toast_class}" role="alert" aria-live="assertive" aria-atomic="true">`
toast_html += `<div class="toast-body h6 mb-0 align-middle"><i class="fas ${icon_class} me-2 h5 mb-0"></i><span>${message}</span></div>`
toast_html += `<button type="button" class="btn-close btn-close-white ms-auto me-2" data-bs-dismiss="toast" aria-label="Close"></button></div>`
$('#toast-container').prepend(toast_html);
let toast_dom = document.getElementById(toast_id)
let toast = new bootstrap.Toast(toast_dom, {
animation: true,
autohide: delay !== -1,
delay: delay
});
toast_dom.addEventListener('hidden.bs.toast', function () {
this.remove();
});
toast.show();
return toast;
}

View File

@ -30,6 +30,7 @@ router.get("/formated-local-snap", function (req, res, next) {
return -1; return -1;
} }
}); });
res.render("localSnaps", { snaps: snaps, moment: moment }); res.render("localSnaps", { snaps: snaps, moment: moment });
}, },
(err) => { (err) => {
@ -48,6 +49,10 @@ router.get("/formated-local-snap", function (req, res, next) {
if (moment(a.lastmod).isBefore(moment(b.lastmod))) return 1; if (moment(a.lastmod).isBefore(moment(b.lastmod))) return 1;
else return -1; else return -1;
}); });
//TODO Remove this when bug is fixed, etag contain '&quot;' at start and end ?
for(let backup of contents){
backup.etag = backup.etag.replace(/&quot;/g, '');
}
res.render("backupSnaps", { backups: contents, moment: moment, humanFileSize: humanFileSize }); res.render("backupSnaps", { backups: contents, moment: moment, humanFileSize: humanFileSize });
}) })
.catch(() => { .catch(() => {
@ -64,6 +69,10 @@ router.get("/formated-local-snap", function (req, res, next) {
if (moment(a.lastmod).isBefore(moment(b.lastmod))) return 1; if (moment(a.lastmod).isBefore(moment(b.lastmod))) return 1;
else return -1; else return -1;
}); });
//TODO Remove this when bug is fixed, etag contain '&quot;' at start and end ?
for(let backup of contents){
backup.etag = backup.etag.replace(/&quot;/g, '');
}
res.render("backupSnaps", { backups: contents, moment: moment, humanFileSize: humanFileSize }); res.render("backupSnaps", { backups: contents, moment: moment, humanFileSize: humanFileSize });
}) })
.catch(() => { .catch(() => {

View File

@ -0,0 +1,52 @@
$body-bg: #222222;
$dark: #292929;
$secondary: #343a40;
$accent: #b58e51;
$enable-shadows: true;
$btn-box-shadow: none;
$component-active-bg: $accent;
$input-color: $accent;
$input-bg: $secondary;
$input-border-color: $secondary;
$form-select-indicator-color: $accent;
$form-switch-color: $accent;
$form-switch-bg-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='#{$form-switch-color}'/></svg>");
$form-check-input-border: 1px solid $accent !default;
$list-group-action-color: $accent;
$list-group-bg: $secondary;
$list-group-hover-bg: $secondary;
$list-group-action-hover-color: #adb5bd;
$alert-bg-scale: 0%;
$alert-border-scale: -10%;
$alert-color-scale: -100%;
@import "../node_modules/bootstrap/scss/bootstrap";
.bg-accent {
background-color: $accent;
}
.text-accent {
color: $accent;
}
input[type="time"]::-webkit-calendar-picker-indicator {
filter: invert(63%) sepia(23%) saturate(819%) hue-rotate(358deg) brightness(88%) contrast(91%);
height: 30px;
margin: -12px;
width: 32px;
}
input[type="time"]::-webkit-calendar-picker-indicator:hover {
cursor: pointer;
}

View File

@ -1,54 +1,66 @@
<% if (locals.backups) { %> <% if (locals.backups) { %>
<div class="collection"> <div class="list-group ">
<% for(const index in backups) { %> <% for(const index in backups) { %>
<a class="collection-item modal-trigger" href="#modal-<%=backups[index].etag%>" <a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
data-id="<%= backups[index].etag %>"> href="#"
<div><%= backups[index].basename%><div class="secondary-content hide-on-small-and-down"> data-id="<%= backups[index].etag %>"
<%= moment(backups[index].lastmod).format('MMM D, YYYY HH:mm') %></div> data-bs-toggle="modal"
</div> data-bs-target="#modal-<%= backups[index].etag %>">
</a> <%= backups[index].basename %>
<span class="badge bg-primary">
<%= moment(backups[index].lastmod).format('MMM D, YYYY HH:mm') %>
</span>
</a>
<div id="modal-<%=backups[index].etag%>" class="modal modal-fixed-footer blue-grey darken-4 white-text"> <div id="modal-<%= backups[index].etag %>" class="modal fade">
<div class="modal-content"> <div class="modal-dialog modal-lg">
<div class="row"> <div class="modal-content bg-dark">
<div class="col s12 center"> <div class="modal-header border-secondary">
<h4>Backup Detail</h2> <h5 class="modal-title" id="exampleModalLabel"> Backup Detail</h5>
</div> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
</div> aria-label="Close"></button>
<div class="row"> </div>
<div class="col s12 center divider"> <div class="modal-body">
</div>
</div> <div class="mb-3">
<div class="row"> <label for="name-<%= backups[index].etag %>" class="form-label">Name</label>
<input disabled type="text" class="form-control bg-secondary border-dark text-accent"
<div class="input-field col s12"> id="name-<%= backups[index].etag %>"
<input disabled type="text" id="name-<%=backups[index].etag%>" value="<%= backups[index].basename %>" /> value="<%= backups[index].basename %>"/>
<label for="name-<%=backups[index].etag%>" class="white-text active">Name</label> </div>
</div>
<div class="input-field col s12"> <div class="mb-3">
<input disabled type="text" id="date-<%=backups[index].etag%>" <label for="date-<%= backups[index].etag %>" class="form-label">Date</label>
value="<%=moment(backups[index].lastmod).format('MMM D, YYYY HH:mm')%>" /> <input disabled type="text" class="form-control bg-secondary border-dark text-accent"
<label for="date-<%=backups[index].etag%>" class="white-text active">Date</label> id="date-<%= backups[index].etag %>"
</div> value="<%= moment(backups[index].lastmod).format('MMM D, YYYY HH:mm') %>"/>
<div class="input-field col s12">
<input disabled type="text" id="size-<%=backups[index].etag%>" </div>
value="<%=humanFileSize(backups[index].size, false)%>" /> <div class="input-field col s12">
<label for="size-<%=backups[index].etag%>" class="white-text active">Size</label> <label for="size-<%= backups[index].etag %>" class="form-label">Size</label>
<input disabled type="text" class="form-control bg-secondary border-dark text-accent"
id="size-<%= backups[index].etag %>"
value="<%= humanFileSize(backups[index].size, false) %>"/>
</div>
</div>
<div class="modal-footer border-secondary">
<button data-bs-dismiss="modal" class="btn btn-danger">Close</button>
<button class="btn btn-success restore"
data-id="<%= backups[index].filename %>"
data-name='<%= backups[index].basename ? backups[index].basename : backups[index].etag %>'
data-bs-dismiss="modal">
Upload to HA
</button>
</div>
</div>
</div> </div>
</div> </div>
</div>
<div class="modal-footer blue-grey darken-4"> <% } %>
<a href="#!" class="waves-effect waves-green btn green restore modal-close" data-id="<%=backups[index].filename%>" data-name='<%= backups[index].basename ? backups[index].basename : backups[index].etag %>'>Upload to HA</a>
<a href="#!" class="modal-close waves-effect waves-green btn red">Close</a>
</div>
</div> </div>
<% } %>
</div>
<% } %> <% } %>

View File

@ -2,741 +2,134 @@
<html> <html>
<head> <head>
<!-- Required meta tags --> <!-- Required meta tags -->
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Nextcloud Backup</title> <title>Nextcloud Backup</title>
<!-- <link rel='stylesheet' href='/stylesheets/style.css' /> --> <link rel='stylesheet' href='./css/style.css'/>
<link rel="stylesheet" href="./css/materialize.min.css"> <link rel="stylesheet" href="./css/custom_bootstrap.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link rel="stylesheet" href="./css/fa-all.min.css">
<style>
.modal input[disabled] {
color: #757575 !important;
border-color: #616161 !important;
}
.modal .input-field span.helper-text {
color: #9e9e9e;
}
.modal div.row:last-child {
margin-bottom: 0;
}
.modal div.col:last-child {
margin-bottom: 0;
}
@media (min-width: 601px) {
.header-box {
height: 150px;
}
}
.header-box .col {
height: 100%;
}
.header-box .col .card {
height: 100%;
}
.header-box .card-content {
padding-top: 10px;
}
.header-box .card-content h5 {
margin-top: 10px;
}
ul.dropdown-content a:hover {
background-color: #101619 !important;
}
ul.dropdown-content li:hover {
background-color: #101619 !important;
}
/* change autocomplete color */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px #263238 inset !important;
-webkit-text-fill-color: white !important;
}
.select-dropdown {
color: white;
}
</style>
</head> </head>
<body class="blue-grey darken-4"> <body class="">
<nav class=" light-blue accent-4"> <nav class="navbar navbar-dark navbar-expand">
<div class="nav-wrapper container"> <div class="container">
<a href="#" class="brand-logo"><img src="./images/Nextcloud_Logo.svg" height="54" <a href="#" class="navbar-brand p-0">
style="margin: 5px"> <img src="./images/Nextcloud_Logo.svg" height="40">
</a> <span class="align-middle d-none d-sm-inline">Nextcloud Backup</span>
<ul class="right"> </a>
<li id="setting-trigger"> <ul class="navbar-nav">
<a class="dropdown-trigger" href="#" data-target="dropdown-settings"> <li class="nav-item dropdown" id="setting-trigger">
<i class="material-icons">settings</i> <a class="btn btn-outline-light bg-transparent nav-link px-2 text-white" href="#" id="dropdown-settings"
</a> role="button"
</li> data-bs-toggle="dropdown" aria-expanded="false">
</ul> <i class="fas fa-cogs"></i>
<div style="height: 64px; display: table; margin-left: 130px;" class="hide-on-med-and-down"> </a>
<h4 style="display: table-cell; vertical-align: middle;">Nextcloud Backup</h4> <ul class="dropdown-menu dropdown-menu-dark dropdown-menu-end" aria-labelledby="dropdown-settings">
</div> <li><a href="#" id="trigger-nextcloud-settings"
class="modal-trigger dropdown-item">Nextcloud</a>
</li>
<li><a href="#" id="trigger-backup-settings" class="dropdown-item">Backup</a></li>
</ul>
</li>
</ul>
</div> </div>
</nav> </nav>
<ul id="dropdown-settings" class="dropdown-content blue-grey darken-4">
<li><a href="#modal-settings-nextcloud" id="trigger-nextcloud-settings" class="modal-trigger center">Nextcloud</a>
</li> <div class="container">
<li><a href="#" id="trigger-backup-settings" class="center">Backup</a></li> <div class="row mt-3 mb-3" id="header-box">
</ul> <div class="col-12 col-md-3 mb-3 mb-md-0">
<div class="container "> <div class="card text-white bg-dark h-100 shadow-sm border-secondary">
<div class="card-header fw-bold h5 border-secondary border-bottom">Status</div>
<div class="row header-box"> <div class="card-body">
<div class="col s12 m3"> <h6 id="status" class="white-text"></h6>
<div class="card cyan darken-3"> <div id="status-second-line" class="truncate mb-2 text-accent"></div>
<div class="card-content"> <div class="progress bg-secondary invisible" id="progress">
<span class="card-title white-text" style="font-weight: bold;">Status </span> <div class="progress-bar bg-success" role="progressbar" style="width: 15%" aria-valuemin="0"
<div class="divider"></div> aria-valuemax="100"></div>
<h6 id="status" class="white-text"></h6> </div>
<div id="status-second-line" class="truncate tooltipped" data-position="bottom" data-tooltip=""></div> </div>
<div class="progress hide" id="progress">
<div class="determinate light-green darken-2" style="width: 0%"></div>
</div> </div>
</div>
</div> </div>
</div>
<div class="col-12 col-md-3 mb-3 mb-md-0">
<div class="col s12 m3"> <div class="card text-white bg-dark h-100 shadow-sm border-secondary">
<div class="card cyan darken-3"> <div class="card-header fw-bold h5 border-secondary border-bottom">Last Backup</div>
<div class="card-content"> <div class="card-body">
<span class="card-title white-text" style="font-weight: bold;">Last Backup</span> <h6 class="white-text" id="last_back_status"></h6>
<div class="divider"></div> </div>
<h6 class="white-text" id="last_back_status"></h6>
</div>
</div>
</div>
<div class="col s12 m3">
<div class="card cyan darken-3 ">
<div class="card-content">
<span class="card-title white-text" style="font-weight: bold;">Next Backup </span>
<div class="divider"></div>
<h6 class="white-text" id="next_back_status"></h6>
</div>
</div>
</div>
<div class="col s12 m3">
<div class="card cyan darken-3">
<div class="card-content">
<span class="card-title white-text" style="font-weight: bold;">Manual </span>
<div class="divider"></div>
<div style="width: 100%;" class="center">
<a class="btn green center waves-effect waves-light" id="btn-backup-now"
style="margin-top: 7px; display: block;">Backup
Now</a>
<a class="btn center teal darken-4 waves-effect waves-light" id="btn-clean-now"
style="margin-top: 7px; display: block;">Clean Now</a>
</div> </div>
</div>
</div> </div>
</div> <div class="col-12 col-md-3 mb-3 mb-md-0">
<div class="card text-white bg-dark h-100 shadow-sm border-secondary">
<div class="card-header fw-bold h5 border-secondary border-bottom">Next Backup</div>
<div class="card-body">
<h6 class="white-text" id="next_back_status"></h6>
</div>
</div>
</div>
<div class="col-12 col-md-3">
<div class="card text-white bg-dark h-100 shadow-sm border-secondary">
<div class="card-header fw-bold h5 border-secondary border-bottom">Manual</div>
<div class="card-body">
<div class="w-100">
<a class="btn btn-success d-block mb-2" id="btn-backup-now">
Backup Now
</a>
<a class="btn btn-danger d-block" id="btn-clean-now">
Clean Now
</a>
</div>
</div>
</div>
</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col s12 m12 l6 "> <div class="col-12 col-md-12 col-lg-6 mb-3 mb-lg-0">
<div class="card cyan darken-3"> <div class="card text-white bg-dark h-100 shadow-sm border-secondary">
<div class="card-content"> <div class="card-header fw-bold h4 text-center border-secondary border-bottom">Local Snapshots</div>
<span class="card-title center white-text">Local Snapshots</span> <div class="card-body">
<div id="local_snaps"></div> <div id="local_snaps"></div>
</div> </div>
</div>
</div> </div>
</div> <div class="col-12 col-md-12 col-lg-6 ">
<div class="col s12 m12 l6 "> <div class="card text-white bg-dark h-100 shadow-sm border-secondary">
<div class="card cyan darken-3"> <div class="card-header fw-bold h4 text-center border-secondary border-bottom">Snapshots in Nextcloud
<div class="card-content"> </div>
<span class="card-title center white-text">Snapshots in Nextcloud</span> <div class="card-body">
<span class="card-title center white-text">Auto</span> <h5 class="card-title text-center fw-bold border-bottom border-secondary pb-2">Auto</h5>
<span class="card-title center white-text divider"></span> <div id="auto_backups"></div>
<div id="auto_backups"></div> <h5 class="card-title text-center fw-bold border-bottom border-secondary mt-3 pb-2">Manual</h5>
<span class="card-title center white-text">Manual</span> <div id="manual_backups"></div>
<span class="card-title center white-text divider"></span> </div>
<div id="manual_backups"></div> </div>
</div>
</div> </div>
</div>
</div> </div>
</div> </div>
<%- include('modals/nextcloud-settings-modal') %> <%- include('modals/nextcloud-settings-modal') %>
<%- include('modals/backup-settings-modal') %>
<%- include('modals/backup-settings-modal') %> <%- include('modals/loading-modal') %>
<div id="toast-container" class="toast-container" aria-live="polite" aria-atomic="true">
<!-- <%- include('modals/restore-modal.ejs') %> --> </div>
<div id="modal-loading" class="modal blue-grey darken-4 white-text">
<div class="modal-content ">
<div class="row">
<div class="col s12 center">
<h4>Loading</h4>
</div>
</div>
<div class="row valign-wrapper" style="height: 150px;">
<div class="col s12 center">
<div class="preloader-wrapper big active">
<div class="spinner-layer spinner-blue">
<div class="circle-clipper left">
<div class="circle"></div>
</div>
<div class="gap-patch">
<div class="circle"></div>
</div>
<div class="circle-clipper right">
<div class="circle"></div>
</div>
</div>
<div class="spinner-layer spinner-red">
<div class="circle-clipper left">
<div class="circle"></div>
</div>
<div class="gap-patch">
<div class="circle"></div>
</div>
<div class="circle-clipper right">
<div class="circle"></div>
</div>
</div>
<div class="spinner-layer spinner-yellow">
<div class="circle-clipper left">
<div class="circle"></div>
</div>
<div class="gap-patch">
<div class="circle"></div>
</div>
<div class="circle-clipper right">
<div class="circle"></div>
</div>
</div>
<div class="spinner-layer spinner-green">
<div class="circle-clipper left">
<div class="circle"></div>
</div>
<div class="gap-patch">
<div class="circle"></div>
</div>
<div class="circle-clipper right">
<div class="circle"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body> </body>
<script src="./js/materialize.min.js"></script> <script src="./js/bootstrap.min.js"></script>
<script src="./js/jquery-3.4.1.min.js"></script> <script src="./js/jquery.min.js"></script>
<script src="./js/index.js"></script> <script src="./js/index.js"></script>
<script src="./js/toast.js"></script>
<script> <script>
var last_status = "";
var last_local_snap = "";
var last_manu_back = "";
var last_auto_back = "";
var loadingModal = null;
document.addEventListener('DOMContentLoaded', function() {
$.ajaxSetup({traditional: true});
updateLocalSnaps();
update_status();
let tooltips = document.querySelectorAll('.tooltipped');
M.Tooltip.init(tooltips, {});
let drops = document.querySelectorAll('.dropdown-trigger');
M.Dropdown.init(drops, { constrainWidth: false, coverTrigger: false, alignment: 'right', onOpenStart: () => $('#setting-trigger').addClass('active'), onCloseEnd: () => $('#setting-trigger').removeClass('active') });
setInterval(update_status, 500);
setInterval(updateLocalSnaps, 5000);
listeners();
});
function updateDynamicListeners() {
var elems = document.querySelectorAll('.collapsible');
M.Collapsible.init(elems, { accordion: true });
var modals = document.querySelectorAll('.modal:not(#modal-loading)');
M.Modal.init(modals, { dismissible: true });
let loadingModals = document.querySelectorAll('#modal-loading');
M.Modal.init(loadingModals, { dismissible: false });
loadingModal = M.Modal.getInstance(document.querySelector('#modal-loading'));
$('.local-snap-listener').click(function() {
let id = this.getAttribute('data-id');
console.log(id);
});
$('.manual-back-list').unbind();
$('.manual-back-list').click(function() {
let id = this.getAttribute('data-id');
let name = this.getAttribute('data-name');
manualBackup(id, name);
})
$('.restore').click(function(){
let to_restore = this.getAttribute('data-id');
console.log(to_restore)
restore(to_restore)
})
}
function updateLocalSnaps() {
let needUpdate = false;
$.get('./api/formated-local-snap', (data) => {
if (JSON.stringify(data) === last_local_snap)
return;
last_local_snap = JSON.stringify(data);
needUpdate = true;
$('#local_snaps').empty();
$('#local_snaps').html(data);
}).always(() => {
updateManuBackup(needUpdate);
});
}
function updateManuBackup(prevUpdate) {
let needUpdate = false;
$.get('./api/formated-backup-manual', (data) => {
if (JSON.stringify(data) === last_manu_back)
return;
last_manu_back = JSON.stringify(data);
needUpdate = true;
$('#manual_backups').empty();
$('#manual_backups').html(data);
}).always(() => {
updateAutoBackup(prevUpdate || needUpdate);
});
}
function updateAutoBackup(prevUpdate) {
let needUpdate = false;
$.get('./api/formated-backup-auto', (data) => {
if (JSON.stringify(data) === last_auto_back)
return;
needUpdate = true;
last_auto_back = JSON.stringify(data);
$('#auto_backups').empty();
$('#auto_backups').html(data);
}).always(() => {
if (prevUpdate || needUpdate)
updateDynamicListeners();
});
}
function update_status() {
$.get('./api/status', (data) => {
if (JSON.stringify(data) !== last_status) {
last_status = JSON.stringify(data);
switch (data.status) {
case "error":
printStatus('Error', data.message);
$('#btn-backup-now, #btn-clean-now').removeClass("disabled");
break;
case "idle":
printStatus('Idle', "Waiting for next backup.");
$('#btn-backup-now, #btn-clean-now').removeClass("disabled");
break;
case "download":
printStatusWithBar('Downloading Snapshot', data.progress);
$('#btn-backup-now, #btn-clean-now').addClass("disabled");
break;
case "download-b":
printStatusWithBar('Downloading Backup', data.progress);
$('#btn-backup-now, #btn-clean-now').addClass("disabled");
break;
case "upload":
printStatusWithBar('Uploading Snapshot', data.progress);
$('#btn-backup-now, #btn-clean-now').addClass("disabled");
break;
case "upload-b":
printStatusWithBar('Uploading Snapshot', data.progress);
$('#btn-backup-now, #btn-clean-now').addClass("disabled");
break;
case "creating":
printStatusWithBar('Creating Snapshot', data.progress);
$('#btn-backup-now, #btn-clean-now').addClass("disabled");
break;
}
if (data.last_backup != null) {
if ($('#last_back_status').html() != data.last_backup)
$('#last_back_status').html(data.last_backup);
}
if (data.next_backup != null) {
if ($('#next_back_status').html() != data.next_backup)
$('#next_back_status').html(data.next_backup);
}
}
});
}
function printStatus(status, secondLine) {
$('#status').empty();
$('#status').html(status);
$('#status-second-line').empty();
$('#status-second-line').removeClass('center');
$('#status-second-line').html(secondLine);
$('#status-second-line').attr('data-tooltip', secondLine)
$('#progress').addClass("hide");
}
function printStatusWithBar(status, progress) {
$('#status').empty();
$('#status').html(status);
let secondLine = $('#status-second-line')
secondLine.empty();
secondLine.html(progress == -1 ? "Waiting..." : (Math.round(progress * 100) + "%"));
secondLine.addClass("center");
secondLine.attr('data-tooltip', Math.round(progress * 100) + "%");
let progressDiv = $('#progress');
progressDiv.removeClass("hide");
if (progress == -1) {
progressDiv.children().removeClass('determinate');
progressDiv.children().addClass('indeterminate');
}
else {
progressDiv.children().removeClass('indeterminate');
progressDiv.children().addClass('determinate');
progressDiv.children().css('width', (progress * 100) + "%");
}
}
function listeners() {
$('#save-nextcloud-settings').click(sendNextcloudSettings);
$('#trigger-nextcloud-settings').click(getNextcloudSettings);
$('#trigger-backup-settings').click(getBackupSettings);
$('#btn-backup-now').click(backupNow);
$('#btn-clean-now').click(cleanNow);
$('#save-backup-settings').click(sendBackupSettings);
$('#cron-drop-day-month').on('input', function() {
$('#cron-drop-day-month-read').val($(this).val());
});
$('#local-snap-keep').on('input', function() {
$('#local-snap-keep-read').val($(this).val());
});
$('#backup-snap-keep').on('input', function() {
$('#backup-snap-keep-read').val($(this).val());
});
$('#ssl').change(function(){
let div = $('#self_signed').parent().parent().parent();
if($('#ssl').is(':checked'))
div.removeClass("hide")
else
div.addClass("hide");
});
$('#confirm-restore').click(function(){
restore(to_restore);
});
}
function restore(id){
loadingModal.open();
$.post('./api/restore', {path: id})
.done((data)=>{
console.log("Restore cmd send !");
M.toast({ html: '<i class="material-icons" style="margin-right:10px">check_box</i> Command send !', classes: "green" });
}).fail((error) => {
console.log(error);
M.toast({ html: '<i class="material-icons" style="margin-right:10px">warning</i> Can\'t send command !', classes: "red" });
})
.always(()=> loadingModal.close())
}
function sendNextcloudSettings() {
loadingModal.open();
let ssl = $('#ssl').is(':checked')
let self_signed = $('#self_signed').is(':checked')
let hostname = $('#hostname').val();
let username = $('#username').val();
let password = $('#password').val();
let back_dir = $('#back-dir').val();
$.post('./api/nextcloud-settings', { ssl: ssl, host: hostname, username: username, password: password, back_dir: back_dir, self_signed: self_signed })
.done((data) => {
console.log('Saved');
$('#nextcloud_settings_message').parent().addClass("hide");
M.toast({ html: '<i class="material-icons" style="margin-right:10px">check_box</i> Nextcloud settings saved !', classes: "green" });
M.Modal.getInstance(document.querySelector('#modal-settings-nextcloud')).close();
}).fail((data) => {
debugger;
if (data.status == 406) {
console.log(data.responseJSON.message);
$('#nextcloud_settings_message').html(data.responseJSON.message);
}
else {
$('#nextcloud_settings_message').html("Invalid Settings.");
}
$('#nextcloud_settings_message').parent().removeClass("hide");
M.toast({ html: '<i class="material-icons" style="margin-right:10px">warning</i> Invalid Nextcloud settings !', classes: "red" });
console.log('Fail');
}).always(() => {
loadingModal.close();
})
}
function manualBackup(id, name) {
$.post('./api/manual-backup?id=' + id + '&name=' + name)
.done((data)=>{
console.log("manual bk cmd send !");
M.toast({ html: '<i class="material-icons" style="margin-right:10px">check_box</i> Command send !', classes: "green" });
}).fail((error) => {
console.log(error);
M.toast({ html: '<i class="material-icons" style="margin-right:10px">warning</i> Can\'t send command !', classes: "red" });
})
}
function getNextcloudSettings() {
loadingModal.open();
$.get('./api/nextcloud-settings', (data) => {
$('#ssl').prop("checked", data.ssl == "true");
if(data.ssl == "true"){
let div = $('#self_signed').parent().parent().parent();
div.removeClass("hide");
}
$('#self_signed').prop('checked', data.self_signed == "true")
$('#hostname').val(data.host);
$('#hostname + label').removeClass("active");
$('#hostname + label').addClass("active");
$('#username').val(data.username);
$('#username + label').removeClass("active");
$('#username + label').addClass("active");
$('#password').val(data.password);
$('#password + label').removeClass("active");
$('#password + label').addClass("active");
$('#back-dir').val(data.back_dir);
$('#back-dir + label').removeClass("active");
$('#back-dir + label').addClass("active");
loadingModal.close();
});
}
function backupNow() {
loadingModal.open();
$.post('./api/new-backup')
.done(() => {
M.toast({ html: '<i class="material-icons" style="margin-right:10px">check_box</i> Command send !', classes: "green" });
}).fail((error) => {
console.log(error);
M.toast({ html: '<i class="material-icons" style="margin-right:10px">warning</i> Can\'t send command !', classes: "red" });
})
.always(() => {
loadingModal.close();
})
}
function cleanNow() {
loadingModal.open();
$.post('./api/clean-now')
.done(() => {
M.toast({ html: '<i class="material-icons" style="margin-right:10px">check_box</i> Command send !', classes: "green" });
}).fail((error) => {
console.log(error);
M.toast({ html: '<i class="material-icons" style="margin-right:10px">warning</i> Can\'t send command !', classes: "red" });
})
.always(() => {
loadingModal.close();
})
}
function getBackupSettings() {
loadingModal.open();
$.get('./api/backup-settings', (data) => {
if (JSON.stringify(data) == "{}") {
data = {
cron_base: "0",
cron_hour: "00:00",
cron_weekday: "0",
cron_month_day: "1",
auto_clean_local: false,
auto_clean_local_keep: 5,
auto_clean_backup: false,
auto_clean_backup_keep: 5,
}
}
changeSelect('#cron-drop-settings', data.settings.cron_base);
$('#cron-drop-settings').change(updateDropVisibility);
$('#name-template').val(data.settings.name_template);
$('#name-template + label').removeClass("active");
$('#name-template + label').addClass("active");
let timepicker = document.querySelector('#timepicker');
$('#timepicker').val(data.settings.cron_hour);
$('#timepicker + label').removeClass("active");
$('#timepicker + label').addClass("active");
if (M.Timepicker.getInstance(timepicker) != null)
M.Timepicker.getInstance(timepicker).destroy();
M.Timepicker.init(timepicker, { defaultTime: data.settings.cron_hour, twelveHour: false, container: 'body' });
$('#cron-drop-day-month-read').val(data.settings.cron_month_day);
$('#cron-drop-day-month').val(data.settings.cron_month_day);
$('#cron-drop-day-month-read + label').removeClass("active");
$('#cron-drop-day-month-read + label').addClass("active");
$('#auto_clean_local').prop('checked', data.settings.auto_clean_local == "true");
$('#local-snap-keep').val(data.settings.auto_clean_local_keep);
$('#auto_clean_backup').prop('checked', data.settings.auto_clean_backup == "true");
$('#backup-snap-keep').val(data.settings.auto_clean_backup_keep);
$('#backup-snap-keep + label').removeClass("active");
$('#backup-snap-keep + label').addClass("active");
$('#local-snap-keep + label').removeClass("active");
$('#local-snap-keep + label').addClass("active");
changeSelect('#cron-drop-day', data.settings.cron_weekday);
let folder_html = ""
for(let index in data.folders){
let thisFolder = data.folders[index];
let exclude = data.settings.exclude_folder.includes(thisFolder.slug);
folder_html += `<li><label><input type="checkbox" class="folders-box" id="${thisFolder.slug}" ${exclude ? "" : "checked=checked"}/><span>${thisFolder.name}</span></label></li>`
}
$("#folders-div").html(folder_html);
let addons_html = ""
for(let index in data.addonList){
let thisAddon = data.addonList[index];
let exclude = data.settings.exclude_addon.includes(thisAddon.slug);
addons_html += `<li><label><input type="checkbox" class="addons-box" id="${thisAddon.slug}" ${exclude ? "" : "checked=checked"}/><span>${thisAddon.name}</span></label></li>`
}
$("#addons-div").html(addons_html);
updateDropVisibility();
loadingModal.close();
M.Modal.getInstance(document.querySelector("#modal-settings-backup")).open()
});
}
function updateDropVisibility() {
let cronBase = $("#cron-drop-settings").val();
switch (cronBase) {
case "3":
$('#timepicker').parent().parent().removeClass("hide");
$('#cron-drop-day').parent().parent().parent().addClass("hide");
$('#cron-drop-day-month').parent().parent().parent().removeClass("hide");
break;
case "2":
$('#timepicker').parent().parent().removeClass("hide");
$('#cron-drop-day').parent().parent().parent().removeClass("hide");
$('#cron-drop-day-month').parent().parent().parent().addClass("hide");
break;
case "1":
$('#timepicker').parent().parent().removeClass("hide");
$('#cron-drop-day').parent().parent().parent().addClass("hide");
$('#cron-drop-day-month').parent().parent().parent().addClass("hide");
break;
case "0":
$('#timepicker').parent().parent().addClass("hide");
$('#cron-drop-day').parent().parent().parent().addClass("hide");
$('#cron-drop-day-month').parent().parent().parent().addClass("hide");
break;
}
}
function sendBackupSettings() {
let cron_month_day = $('#cron-drop-day-month').val();
let cron_weekday = $('#cron-drop-day').val();
let cron_hour = $('#timepicker').val();
let cron_base = $('#cron-drop-settings').val();
let auto_clean_local = $("#auto_clean_local").is(':checked');
let auto_clean_backup = $("#auto_clean_backup").is(':checked');
let auto_clean_local_keep = $("#local-snap-keep").val();
let auto_clean_backup_keep = $("#backup-snap-keep").val();
let name_template = $('#name-template').val();
let excluded_folders_nodes = document.querySelectorAll('.folders-box:not(:checked)');
let exclude_folder = [""];
for(let i of excluded_folders_nodes){
exclude_folder.push(i.id);
}
let excluded_addons_nodes = document.querySelectorAll('.addons-box:not(:checked)');
let exclude_addon = [""];
for(let i of excluded_addons_nodes){
exclude_addon.push(i.id);
}
loadingModal.open();
$.post('./api/backup-settings',
{
name_template: name_template,
cron_base: cron_base,
cron_hour: cron_hour,
cron_weekday: cron_weekday,
cron_month_day: cron_month_day,
auto_clean_local: auto_clean_local,
auto_clean_local_keep: auto_clean_local_keep,
auto_clean_backup: auto_clean_backup,
auto_clean_backup_keep: auto_clean_backup_keep,
exclude_addon: exclude_addon,
exclude_folder: exclude_folder
})
.done(() => {
M.toast({ html: '<i class="material-icons" style="margin-right:10px">check_box</i> Backup settings saved !', classes: "green" });
M.Modal.getInstance(document.querySelector('#modal-settings-backup')).close();
}).fail(() => {
M.toast({ html: '<i class="material-icons" style="margin-right:10px">warning</i> Can\'t save backup settings !', classes: "red" });
}).always(() => {
loadingModal.close();
});
}
function changeSelect(selector, value) {
let selectBaseRaw = document.querySelector(selector);
if (M.FormSelect.getInstance(selectBaseRaw) != null)
M.FormSelect.getInstance(selectBaseRaw).destroy();
$(selector + ' option[selected]').removeAttr('selected');
$(selector + ' option[value=' + value + ']').attr('selected', "true");
M.FormSelect.init(selectBaseRaw, {});
}
</script> </script>
</html> </html>

View File

@ -1,61 +1,71 @@
<% if (locals.snaps) { %> <% if (locals.snaps) { %>
<div class="collection"> <div class="list-group">
<% for(const index in snaps) { %> <% for(const index in snaps) { %>
<a class="collection-item local-snap-listener modal-trigger" href="#modal-<%=snaps[index].slug%>" <a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
data-id="<%= snaps[index].slug %>"> href="#"
<div><%= snaps[index].name ? snaps[index].name : snaps[index].slug %><div class="secondary-content hide-on-small-and-down"> data-id="<%= snaps[index].slug %>"
<%= moment(snaps[index].date).format('MMM D, YYYY HH:mm') %></div> data-bs-toggle="modal"
</div> data-bs-target="#modal-<%= snaps[index].slug %>">
</a> <%= snaps[index].name ? snaps[index].name : snaps[index].slug %>
<span class="badge bg-primary">
<%= moment(snaps[index].date).format('MMM D, YYYY HH:mm') %>
</span>
</a>
<div id="modal-<%= snaps[index].slug %>" class="modal fade">
<div class="modal-dialog modal-lg">
<div class="modal-content bg-dark">
<div class="modal-header border-secondary">
<h5 class="modal-title" id="exampleModalLabel"> Snapshot Detail</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="name-<%= snaps[index].slug %>" class="form-label">Name</label>
<input disabled type="text" class="form-control bg-secondary border-dark text-accent" id="name-<%= snaps[index].slug %>"
value="<%= snaps[index].name ? snaps[index].name : snaps[index].slug %>"/>
</div>
<div class="mb-3">
<label for="date-<%= snaps[index].slug %>" class="form-label">Date</label>
<input disabled type="text" class="form-control bg-secondary border-dark text-accent" id="date-<%= snaps[index].slug %>"
value="<%= moment(snaps[index].date).format('MMM D, YYYY HH:mm') %>"/>
</div>
<div class="mb-3">
<label for="protected-<%= snaps[index].slug %>"
class="form-label">Protected</label>
<input disabled type="text" class="form-control bg-secondary border-dark text-accent" id="protected-<%= snaps[index].slug %>"
value="<%= snaps[index].protected %>"/>
</div>
<div >
<label for="type-<%= snaps[index].slug %>" class="form-label">Type</label>
<input disabled type="text" class="form-control bg-secondary border-dark text-accent" id="type-<%= snaps[index].slug %>"
value="<%= snaps[index].type %>"/>
</div>
</div>
<div class="modal-footer border-secondary">
<button data-bs-dismiss="modal" class="btn btn-danger">Close</button>
<button class="btn btn-success restore"
data-id="<%= snaps[index].slug %>"
data-name='<%= snaps[index].name ? snaps[index].name : snaps[index].slug %>'
data-bs-dismiss="modal">
Backup now
</button>
</div>
</div>
<div id="modal-<%=snaps[index].slug%>" class="modal modal-fixed-footer blue-grey darken-4 white-text">
<div class="modal-content">
<div class="row">
<div class="col s12 center">
<h4>Snapshot Detail</h2>
</div> </div>
</div> </div>
<div class="row">
<div class="col s12 center divider">
</div>
</div>
<div class="row">
<div class="input-field col s12">
<input disabled type="text" id="name-<%=snaps[index].slug%>" value="<%= snaps[index].name ? snaps[index].name : snaps[index].slug %>" />
<label for="name-<%=snaps[index].slug%>" class="white-text active">Name</label>
</div>
<% } %>
<div class="input-field col s12">
<input disabled type="text" id="date-<%=snaps[index].slug%>"
value="<%=moment(snaps[index].date).format('MMM D, YYYY HH:mm')%>" />
<label for="date-<%=snaps[index].slug%>" class="white-text active">Date</label>
</div>
<div class="input-field col s12">
<input disabled type="text" id="protected-<%=snaps[index].slug%>"
value="<%=snaps[index].protected%>" />
<label for="protected-<%=snaps[index].slug%>" class="white-text active">Protected</label>
</div>
<div class="input-field col s12">
<input disabled type="text" id="type-<%=snaps[index].slug%>" value="<%=snaps[index].type%>" />
<label for="type-<%=snaps[index].slug%>" class="white-text active">Type</label>
</div>
</div>
</div> </div>
<div class="modal-footer blue-grey darken-4">
<a href="#!" class="waves-effect waves-green btn green manual-back-list modal-close" data-id="<%=snaps[index].slug%>" data-name='<%= snaps[index].name ? snaps[index].name : snaps[index].slug %>'</a>Backup now</a>
<a href="#!" class="modal-close waves-effect waves-green btn red">Close</a>
</div>
</div>
<% } %>
</div>
<% } %> <% } %>

View File

@ -1,169 +1,146 @@
<div id="modal-settings-backup" class="modal modal-fixed-footer blue-grey darken-4 white-text"> <div id="modal-settings-backup" class="modal fade">
<div class="modal-content"> <div class="modal-dialog modal-lg">
<div class="row" style="margin-bottom: 5px;"> <div class="modal-content bg-dark text-white">
<div class="col s12 center" > <div class="modal-header border-secondary">
<h4>Backup Settings</h2> <h5 class="modal-title" id="exampleModalLabel">Backup Settings</h5>
</div> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
</div> aria-label="Close"></button>
<div class="row" >
<div class="col s12 center divider">
</div>
</div>
<div class="row">
<div class="input-field col offset-xl2 xl8 offset-l1 l10 m12 s12">
<input id="name-template" type="text" class="white-text">
<label for="name-template">Backup name</label>
<span class="helper-text">You can find all available variables <a target="_blank"
href="https://github.com/Sebclem/hassio-nextcloud-backup/blob/master/naming_template.md">here</a></span>
</div>
</div>
<div class="row">
<div class="input-field col offset-xl2 xl8 offset-l1 l10 m12 s12">
<select id="cron-drop-settings">
<option value="0">Disable</option>
<option value="1">Daily</option>
<option value="2">Weekly</option>
<option value="3">Monthly</option>
</select>
<label>Auto Backup</label>
</div>
</div>
<div class="row hide">
<div class="input-field col offset-xl2 xl8 offset-l1 l10 m12 s12">
<input type="text" class="timepicker white-text" readonly="true" id="timepicker">
<label>Hour </label>
</div>
</div>
<div class="row hide">
<div class="input-field col offset-xl2 xl8 offset-l1 l10 m12 s12">
<select id="cron-drop-day">
<option value="1">Monday</option>
<option value="2">Tuesday</option>
<option value="3">Wednesday</option>
<option value="4">Thursday</option>
<option value="5">Friday</option>
<option value="6">Saturday</option>
<option value="0">Sunday</option>
</select>
<label>Day</label>
</div>
</div>
<div class="row hide">
<div class="input-field col s2">
<input type="text" class="white-text" disabled readonly="true" id="cron-drop-day-month-read">
<label class="white-text active">Day of month</label>
</div>
<div class="input-field col s10">
<p class="range-field">
<input type="range" id="cron-drop-day-month" min="1" max="28" style="border: none;" />
</p>
</div>
</div>
<div class="row">
<div class="col offset-xl1 xl5 l6 m12 s12">
<div class="row">
<div class="col s12 center">
<h5>Folders</h5>
</div>
</div> </div>
<div class="row"> <div class="modal-body">
<div class="col offset-s1 s11 xl10 offset-xl2" > <div class="row">
<ul id="folders-div"> <div class="col-12 col-lg-10 offset-lg-1">
<label for="name-template" class="form-label">Backup name</label>
<input id="name-template" type="text" class="form-control" aria-describedby="help-template">
<span id="help-template" class="form-text">
You can find all available variables
<a target="_blank"
href="https://github.com/Sebclem/hassio-nextcloud-backup/blob/master/naming_template.md">
here
</a>
</span>
</div>
</div>
</ul> <div class="row mt-2">
</div> <div class="col-12 col-lg-10 offset-lg-1">
</div> <label for="cron-drop-settings" class="form-label">Auto Backup</label>
</div> <select id="cron-drop-settings" class="form-select">
<div class="col xl5 l6 m12 s12" > <option value="0">Disable</option>
<div class="row"> <option value="1">Daily</option>
<div class="col s12 center"> <option value="2">Weekly</option>
<h5>Addons</h5> <option value="3">Monthly</option>
</div> </select>
</div> </div>
<div class="row"> </div>
<div class="col offset-s1 s11 xl10 offset-xl2"> <div class="row d-none mt-2">
<ul id="addons-div"> <div class="col-12 col-lg-10 offset-lg-1">
<label for="timepicker" class="form-label">Hour </label>
<input type="time" class="form-control" id="timepicker">
</div>
</div>
<div class="row d-none mt-2">
<div class="col-12 col-lg-10 offset-lg-1">
<label for="cron-drop-day" class="form-label">Day</label>
<select id="cron-drop-day" class="form-select">
<option value="1">Monday</option>
<option value="2">Tuesday</option>
<option value="3">Wednesday</option>
<option value="4">Thursday</option>
<option value="5">Friday</option>
<option value="6">Saturday</option>
<option value="0">Sunday</option>
</select>
</div>
</div>
<div class="row d-none mt-2">
<div class="col-12 col-lg-10 offset-lg-1">
<label for="cron-drop-day-month" class="form-label">Day of month</label>
<input type="number" class="form-control" id="cron-drop-day-month" min="1" max="28">
</div>
</div>
<div class="row mt-3">
<div class="col-12 col-lg-6">
<div class="row">
<div class="col-12 text-center">
<h5>Folders</h5>
</div>
</div>
<div class="row">
<div class="col-12">
<ul id="folders-div" class="list-group">
</ul>
</div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="row">
<div class="col-12 text-center">
<h5>Addons</h5>
</div>
</div>
<div class="row">
<div class="col-12">
<ul id="addons-div" class="list-group">
</ul>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12 text-center">
<h4>Auto Clean Settings</h4>
</div>
</div>
<div class="row mt-3">
<div class="col-12 col-md-5 offset-md-1">
<div class="row">
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input" id="auto_clean_local" type="checkbox">
<label class="form-check-label" for="auto_clean_local">Auto Clean Local Snapshots</label>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-12">
<label for="local-snap-keep" class="form-label">Local snapshot to keep</label>
<input type="number" class="form-control" id="local-snap-keep" min="0">
</div>
</div>
</div>
<div class="col-12 col-md-5">
<div class="row">
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input" id="auto_clean_backup" type="checkbox">
<label class="form-check-label" for="auto_clean_backup">Auto Clean Nextcloud Snapshots</label>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-12">
<label for="backup-snap-keep" class="form-label">Nextcloud snapshot to keep</label>
<input type="number" class="form-control" id="backup-snap-keep" min="0">
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer border-secondary">
<button data-bs-dismiss="modal" class="btn btn-danger"><b>Cancel</b></button>
<button class="btn btn-success" id="save-backup-settings"><b>Save</b></button>
</ul>
</div>
</div> </div>
</div>
</div> </div>
<div class="row" style="margin-bottom: 5px;"> </div>
<div class="col s12 center"> </div>
<h4>Auto Clean Settings</h2>
</div>
</div>
<div class="row">
<div class="col s12 center divider">
</div>
</div>
<div class="row">
<div class="col offset-xl1 xl5 l6 m12 s12">
<div class="row" style="margin-bottom: 10px;">
<div class="col s12">
<div style="color: #9e9e9e; display: inline;">Auto Clean Local Snapshots</div>
<div class="switch" style="display: inline;">
<label>
<input id="auto_clean_local" type="checkbox">
<span class="lever"></span>
</label>
</div>
</div>
</div>
<div class="row">
<div class="input-field col s12">
<input type="number" class="white-text" id="local-snap-keep">
<label class="active">Local snapshot to keep</label>
</div>
</div>
</div>
<div class="col xl5 l6 m12 s12" >
<div class="row" style="margin-bottom: 10px;">
<div class="col s12">
<div style="color: #9e9e9e; display: inline;">Auto Clean Nextcloud Snapshots</div>
<div class="switch" style="display: inline;">
<label>
<input id="auto_clean_backup" type="checkbox">
<span class="lever"></span>
</label>
</div>
</div>
</div>
<div class="row">
<div class="input-field col s12">
<input type="number" class="white-text" id="backup-snap-keep">
<label class="active">Nextcloud snapshot to keep</label>
</div>
</div>
</div>
</div>
<div class="row" style="margin-bottom: 10px;">
</div>
<div class="row">
</div>
</div>
<div class="modal-footer blue-grey darken-4">
<a href="#" class="modal-close waves-effect btn red"><b>Cancel</b></a>
<a href="#" class="btn green waves-effect" style="margin-left: 5px;" id="save-backup-settings"><b>Save</b></a>
</div>
</div>

View File

@ -0,0 +1,17 @@
<div class="modal" id="loading-modal" tabindex="-1"
aria-hidden="true">
<div class="modal-dialog border-secondary">
<div class="modal-content bg-dark text-white">
<div class="modal-header border-secondary d-flex justify-content-center">
<h3 class="modal-title fw-bold">Loading</h4>
</div>
<div class="modal-body text-center">
<div class="spinner-border text-accent border-5" style="width: 5rem; height: 5rem" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,73 +1,73 @@
<div id="modal-settings-nextcloud" class="modal modal-fixed-footer blue-grey darken-4 white-text"> <div id="modal-settings-nextcloud" class="modal fade" xmlns="http://www.w3.org/1999/html">
<div class="modal-content"> <div class="modal-dialog modal-lg">
<div class="row"> <div class="modal-content bg-dark text-white">
<div class="col s12 center"> <div class="modal-header border-secondary">
<h4>Nextcloud Settings</h4> <h5 class="modal-title" id="exampleModalLabel">Nextcloud Settings</h5>
</div> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
</div> aria-label="Close"></button>
<div class="row"> </div>
<div class="col s12 center divider"> <div class="modal-body">
</div> <div class="row d-none">
</div> <div class="col-12 col-md-10 offset-md-1 text-center alert alert-danger" role="alert" id="nextcloud_settings_message">
<div class="row hide">
<div class="col s12 center red-text" id="nextcloud_settings_message">
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-4">
<div class="form-check form-switch">
<input class="form-check-input" id="ssl" type="checkbox">
<label class="form-check-label" for="ssl">SSL</label>
</div>
</div>
<div class="col-sm-12 col-md-8 invisible">
<div class="form-check form-switch">
<input class="form-check-input" id="self_signed" type="checkbox">
<label class="form-check-label" for="ssl">Accept Self-signed certificate</label>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<label for="hostname" class="form-label">Hostname</label>
<input id="hostname" type="text" class="form-control" aria-describedby="hostname-help">
<div id="hostname-help" class="form-text">exemple.com:8080</span>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12 col-md-6">
<label for="username" class="form-label">Username</label>
<input id="username" type="text" class="form-control">
</div>
<div class="col-12 col-md-6">
<label for="password" class="form-label">Password</label>
<input id="password" type="password" class="form-control" aria-describedby="password-help">
<div id="password-help" class="form-text">
!!! Use App Password !!! See
<a target="_blank"
href="https://github.com/Sebclem/hassio-nextcloud-backup#nextcloud-config">
doc
</a>
for more information.
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<label for="back-dir" class="form-label">Backup Directory</label>
<input id="back-dir" type="text" class="form-control" aria-describedby="dir-help">
<div id="dir-help" class="form-text">Default: /Hassio Backup/</div>
</div>
</div>
</div>
<div class="modal-footer border-secondary">
<button data-bs-dismiss="modal" class="btn btn-danger"><b>Cancel</b></button>
<button class="btn btn-success" id="save-nextcloud-settings"><b>Save</b></button>
</div>
</div> </div>
</div>
<div class="row" style="margin-bottom: 10px;">
<div class="col m4 s12 " style="margin-bottom: 10px;">
<div style="color: #9e9e9e; display: inline;">SSL</div>
<div class="switch" style="display: inline;">
<label>
<input id="ssl" type="checkbox">
<span class="lever"></span>
</label>
</div>
</div>
<div class="col m8 s12 hide" style="margin-bottom: 10px;">
<div style="color: #9e9e9e; display: inline;">Accept Self-signed certificate</div>
<div class="switch" style="display: inline;">
<label>
<input id="self_signed" type="checkbox">
<span class="lever"></span>
</label>
</div>
</div>
</div>
<div class="row">
<div class="input-field col s12">
<input id="hostname" type="text" class="white-text">
<label for="hostname">Hostname</label>
<span class="helper-text">exemple.com:8080</span>
</div>
</div>
<div class="row">
<div class="input-field col m6 s12">
<input id="username" type="text" class="white-text">
<label for="username">Username</label>
</div>
<div class="input-field col m6 s12">
<input id="password" type="password" class="white-text">
<label for="password">Password</label>
<span class="helper-text">!!! Use App Password !!! See <a target="_blank"
href="https://github.com/Sebclem/hassio-nextcloud-backup#nextcloud-config">doc</a> for more
information.</span>
</div>
</div>
<div class="row">
<div class="input-field col s12">
<input id="back-dir" type="text" class="white-text">
<label for="back-dir">Backup Directory</label>
<span class="helper-text">Default: /Hassio Backup/</span>
</div>
</div>
</div> </div>
<div class="modal-footer blue-grey darken-4"> </div>
<a href="#" class="modal-close waves-effect btn red"><b>Cancel</b></a>
<a href="#" class="btn green waves-effect" style="margin-left: 5px;" id="save-nextcloud-settings"><b>Save</b></a>
</div>
</div>