database backup

deepsource-autofix-76c6eb20
Dan Gowans 2023-01-25 10:32:43 -05:00
parent 5eb61df31d
commit 94af5ba3fe
35 changed files with 368 additions and 131 deletions

3
.gitignore vendored
View File

@ -6,6 +6,9 @@ cypress/downloads/
cypress/screenshots/ cypress/screenshots/
cypress/videos/ cypress/videos/
data/backups/*
!data/backups/README.md
data/sessions/ data/sessions/
node_modules/ node_modules/

View File

@ -1,17 +1,27 @@
import { testAdmin } from '../../../test/_globals.js'; import { testAdmin } from '../../../test/_globals.js';
import { logout, login, ajaxDelayMillis } from '../../support/index.js'; import { logout, login, ajaxDelayMillis } from '../../support/index.js';
describe('Admin - Cleanup Database', () => { describe('Admin - Database Maintenance', () => {
beforeEach('Loads page', () => { beforeEach('Loads page', () => {
logout(); logout();
login(testAdmin); login(testAdmin);
cy.visit('/admin/cleanup'); cy.visit('/admin/database');
cy.location('pathname').should('equal', '/admin/cleanup'); cy.location('pathname').should('equal', '/admin/database');
}); });
afterEach(logout); afterEach(logout);
it('Has no detectable accessibility issues', () => { it('Has no detectable accessibility issues', () => {
cy.injectAxe(); cy.injectAxe();
cy.checkA11y(); cy.checkA11y();
}); });
it('Backs up the database', () => {
cy.get("button[data-cy='backup']").click();
cy.get('.modal').should('be.visible').should('contain.text', 'Backup');
cy.get(".modal button[data-cy='ok']").click();
cy.wait(ajaxDelayMillis);
cy.get('.modal')
.should('contain.text', 'Backed Up')
.should('contain.text', 'Success');
cy.get(".modal button[data-cy='ok']").click();
});
it('Cleans up the database', () => { it('Cleans up the database', () => {
cy.get("button[data-cy='cleanup']").click(); cy.get("button[data-cy='cleanup']").click();
cy.get('.modal').should('be.visible').should('contain.text', 'Cleanup'); cy.get('.modal').should('be.visible').should('contain.text', 'Cleanup');

View File

@ -4,12 +4,12 @@ import { testAdmin } from '../../../test/_globals.js'
import { logout, login, ajaxDelayMillis } from '../../support/index.js' import { logout, login, ajaxDelayMillis } from '../../support/index.js'
describe('Admin - Cleanup Database', () => { describe('Admin - Database Maintenance', () => {
beforeEach('Loads page', () => { beforeEach('Loads page', () => {
logout() logout()
login(testAdmin) login(testAdmin)
cy.visit('/admin/cleanup') cy.visit('/admin/database')
cy.location('pathname').should('equal', '/admin/cleanup') cy.location('pathname').should('equal', '/admin/database')
}) })
afterEach(logout) afterEach(logout)
@ -19,6 +19,22 @@ describe('Admin - Cleanup Database', () => {
cy.checkA11y() cy.checkA11y()
}) })
it('Backs up the database', () => {
cy.get("button[data-cy='backup']").click()
cy.get('.modal').should('be.visible').should('contain.text', 'Backup')
cy.get(".modal button[data-cy='ok']").click()
cy.wait(ajaxDelayMillis)
cy.get('.modal')
.should('contain.text', 'Backed Up')
.should('contain.text', 'Success')
cy.get(".modal button[data-cy='ok']").click()
})
it('Cleans up the database', () => { it('Cleans up the database', () => {
cy.get("button[data-cy='cleanup']").click() cy.get("button[data-cy='cleanup']").click()

View File

@ -15,7 +15,7 @@ describe('Update User', () => {
cy.get("a[href*='/admin']").should('not.exist'); cy.get("a[href*='/admin']").should('not.exist');
}); });
it('Redirects to Dashboard when attempting to access admin area', () => { it('Redirects to Dashboard when attempting to access admin area', () => {
cy.visit('/admin/cleanup'); cy.visit('/admin/tables');
cy.wait(200); cy.wait(200);
cy.location('pathname').should('equal', '/dashboard/'); cy.location('pathname').should('equal', '/dashboard/');
}); });

View File

@ -24,7 +24,7 @@ describe('Update User', () => {
}) })
it('Redirects to Dashboard when attempting to access admin area', () => { it('Redirects to Dashboard when attempting to access admin area', () => {
cy.visit('/admin/cleanup') cy.visit('/admin/tables')
cy.wait(200) cy.wait(200)
cy.location('pathname').should('equal', '/dashboard/') cy.location('pathname').should('equal', '/dashboard/')
}) })

View File

@ -25,4 +25,9 @@ describe('Login Page', () => {
matchCase: false matchCase: false
}); });
}); });
it('Redirects to login when attempting to access dashboard', () => {
cy.visit('/dashboard');
cy.wait(200);
cy.location('pathname').should('contain', '/login');
});
}); });

View File

@ -32,4 +32,10 @@ describe('Login Page', () => {
matchCase: false matchCase: false
}) })
}) })
it('Redirects to login when attempting to access dashboard', () => {
cy.visit('/dashboard')
cy.wait(200)
cy.location('pathname').should('contain', '/login')
})
}) })

View File

@ -0,0 +1,3 @@
# Placeholder
This is a placeholder file to assist with the creation of this folder.

View File

@ -2,3 +2,4 @@ export declare const useTestDatabases: boolean;
export declare const lotOccupancyDBLive = "data/lotOccupancy.db"; export declare const lotOccupancyDBLive = "data/lotOccupancy.db";
export declare const lotOccupancyDBTesting = "data/lotOccupancy-testing.db"; export declare const lotOccupancyDBTesting = "data/lotOccupancy-testing.db";
export declare const lotOccupancyDB: string; export declare const lotOccupancyDB: string;
export declare const backupFolder = "data/backups";

View File

@ -11,3 +11,4 @@ export const lotOccupancyDBTesting = 'data/lotOccupancy-testing.db';
export const lotOccupancyDB = useTestDatabases export const lotOccupancyDB = useTestDatabases
? lotOccupancyDBTesting ? lotOccupancyDBTesting
: lotOccupancyDBLive; : lotOccupancyDBLive;
export const backupFolder = 'data/backups';

View File

@ -19,3 +19,5 @@ export const lotOccupancyDBTesting = 'data/lotOccupancy-testing.db'
export const lotOccupancyDB = useTestDatabases export const lotOccupancyDB = useTestDatabases
? lotOccupancyDBTesting ? lotOccupancyDBTesting
: lotOccupancyDBLive : lotOccupancyDBLive
export const backupFolder = 'data/backups'

View File

@ -1,6 +0,0 @@
export function handler(_request, response) {
response.render('admin-cleanup', {
headTitle: 'Database Cleanup'
});
}
export default handler;

View File

@ -0,0 +1,6 @@
export function handler(_request, response) {
response.render('admin-database', {
headTitle: 'Database Maintenance'
});
}
export default handler;

View File

@ -1,8 +1,8 @@
import type { Request, Response } from 'express' import type { Request, Response } from 'express'
export function handler(_request: Request, response: Response): void { export function handler(_request: Request, response: Response): void {
response.render('admin-cleanup', { response.render('admin-database', {
headTitle: 'Database Cleanup' headTitle: 'Database Maintenance'
}) })
} }

View File

@ -0,0 +1,3 @@
import type { Request, Response } from 'express';
export declare function handler(_request: Request, response: Response): Promise<void>;
export default handler;

View File

@ -0,0 +1,19 @@
import { backupDatabase } from '../../helpers/functions.database.js';
export async function handler(_request, response) {
const backupDatabasePath = await backupDatabase();
if (typeof backupDatabasePath === 'string') {
const backupDatabasePathSplit = backupDatabasePath.split(/[/\\]/g);
const fileName = backupDatabasePathSplit[backupDatabasePathSplit.length - 1];
response.json({
success: true,
fileName
});
}
else {
response.json({
success: false,
errorMessage: 'Unable to write backup file.'
});
}
}
export default handler;

View File

@ -0,0 +1,28 @@
import type { Request, Response } from 'express'
import { backupDatabase } from '../../helpers/functions.database.js'
export async function handler(
_request: Request,
response: Response
): Promise<void> {
const backupDatabasePath = await backupDatabase()
if (typeof backupDatabasePath === 'string') {
const backupDatabasePathSplit = backupDatabasePath.split(/[/\\]/g)
const fileName = backupDatabasePathSplit[backupDatabasePathSplit.length - 1]
response.json({
success: true,
fileName
})
} else {
response.json({
success: false,
errorMessage: 'Unable to write backup file.'
})
}
}
export default handler

View File

@ -0,0 +1 @@
export declare const backupDatabase: () => Promise<string | false>;

View File

@ -0,0 +1,17 @@
import fs from 'node:fs/promises';
import { lotOccupancyDB as databasePath, backupFolder } from '../data/databasePaths.js';
export const backupDatabase = async () => {
const databasePathSplit = databasePath.split(/[/\\]/g);
const backupDatabasePath = backupFolder +
'/' +
databasePathSplit[databasePathSplit.length - 1] +
'.' +
Date.now().toString();
try {
await fs.copyFile(databasePath, backupDatabasePath);
return backupDatabasePath;
}
catch {
return false;
}
};

View File

@ -0,0 +1,23 @@
import fs from 'node:fs/promises'
import {
lotOccupancyDB as databasePath,
backupFolder
} from '../data/databasePaths.js'
export const backupDatabase = async (): Promise<string | false> => {
const databasePathSplit = databasePath.split(/[/\\]/g)
const backupDatabasePath =
backupFolder +
'/' +
databasePathSplit[databasePathSplit.length - 1] +
'.' +
Date.now().toString()
try {
await fs.copyFile(databasePath, backupDatabasePath)
return backupDatabasePath
} catch {
return false
}
}

View File

@ -1,38 +0,0 @@
"use strict";
/* eslint-disable @typescript-eslint/no-non-null-assertion, unicorn/prefer-module */
Object.defineProperty(exports, "__esModule", { value: true });
(() => {
const los = exports.los;
function doCleanup() {
cityssm.postJSON(los.urlPrefix + '/admin/doCleanupDatabase', {}, (responseJSON) => {
var _a;
if (responseJSON.success) {
bulmaJS.alert({
title: 'Database Cleaned Up Successfully',
message: `${responseJSON.inactivedRecordCount} records inactivated,
${responseJSON.purgedRecordCount} permanently deleted.`,
contextualColorName: 'success'
});
}
else {
bulmaJS.alert({
title: 'Error Cleaning Database',
message: (_a = responseJSON.errorMessage) !== null && _a !== void 0 ? _a : '',
contextualColorName: 'danger'
});
}
});
}
document
.querySelector('#button--cleanupDatabase')
.addEventListener('click', () => {
bulmaJS.confirm({
title: 'Cleanup Database',
message: 'Are you sure you want to cleanup up the database?',
okButton: {
text: 'Yes, Cleanup Database',
callbackFunction: doCleanup
}
});
});
})();

View File

@ -0,0 +1,68 @@
"use strict";
/* eslint-disable @typescript-eslint/no-non-null-assertion, unicorn/prefer-module */
Object.defineProperty(exports, "__esModule", { value: true });
(() => {
var _a, _b;
const los = exports.los;
function doBackup() {
cityssm.postJSON(los.urlPrefix + '/admin/doBackupDatabase', {}, (responseJSON) => {
var _a;
if (responseJSON.success) {
bulmaJS.alert({
title: 'Database Backed Up Successfully',
message: `Backed up to ${responseJSON.fileName}`,
contextualColorName: 'success'
});
}
else {
bulmaJS.alert({
title: 'Error Backing Up Database',
message: (_a = responseJSON.errorMessage) !== null && _a !== void 0 ? _a : '',
contextualColorName: 'danger'
});
}
});
}
function doCleanup() {
cityssm.postJSON(los.urlPrefix + '/admin/doCleanupDatabase', {}, (responseJSON) => {
var _a;
if (responseJSON.success) {
bulmaJS.alert({
title: 'Database Cleaned Up Successfully',
message: `${responseJSON.inactivedRecordCount} records inactivated,
${responseJSON.purgedRecordCount} permanently deleted.`,
contextualColorName: 'success'
});
}
else {
bulmaJS.alert({
title: 'Error Cleaning Database',
message: (_a = responseJSON.errorMessage) !== null && _a !== void 0 ? _a : '',
contextualColorName: 'danger'
});
}
});
}
(_a = document
.querySelector('#button--cleanupDatabase')) === null || _a === void 0 ? void 0 : _a.addEventListener('click', () => {
bulmaJS.confirm({
title: 'Cleanup Database',
message: 'Are you sure you want to cleanup up the database?',
okButton: {
text: 'Yes, Cleanup Database',
callbackFunction: doCleanup
}
});
});
(_b = document
.querySelector('#button--backupDatabase')) === null || _b === void 0 ? void 0 : _b.addEventListener('click', () => {
bulmaJS.confirm({
title: 'Backup Database',
message: 'Are you sure you want to backup up the database?',
okButton: {
text: 'Yes, Backup Database',
callbackFunction: doBackup
}
});
});
})();

View File

@ -11,6 +11,32 @@ declare const bulmaJS: BulmaJS
;(() => { ;(() => {
const los = exports.los as globalTypes.LOS const los = exports.los as globalTypes.LOS
function doBackup(): void {
cityssm.postJSON(
los.urlPrefix + '/admin/doBackupDatabase',
{},
(responseJSON: {
success: boolean
errorMessage?: string
fileName?: string
}) => {
if (responseJSON.success) {
bulmaJS.alert({
title: 'Database Backed Up Successfully',
message: `Backed up to ${responseJSON.fileName!}`,
contextualColorName: 'success'
})
} else {
bulmaJS.alert({
title: 'Error Backing Up Database',
message: responseJSON.errorMessage ?? '',
contextualColorName: 'danger'
})
}
}
)
}
function doCleanup(): void { function doCleanup(): void {
cityssm.postJSON( cityssm.postJSON(
los.urlPrefix + '/admin/doCleanupDatabase', los.urlPrefix + '/admin/doCleanupDatabase',
@ -40,8 +66,8 @@ declare const bulmaJS: BulmaJS
} }
document document
.querySelector('#button--cleanupDatabase')! .querySelector('#button--cleanupDatabase')
.addEventListener('click', () => { ?.addEventListener('click', () => {
bulmaJS.confirm({ bulmaJS.confirm({
title: 'Cleanup Database', title: 'Cleanup Database',
message: 'Are you sure you want to cleanup up the database?', message: 'Are you sure you want to cleanup up the database?',
@ -51,4 +77,17 @@ declare const bulmaJS: BulmaJS
} }
}) })
}) })
document
.querySelector('#button--backupDatabase')
?.addEventListener('click', () => {
bulmaJS.confirm({
title: 'Backup Database',
message: 'Are you sure you want to backup up the database?',
okButton: {
text: 'Yes, Backup Database',
callbackFunction: doBackup
}
})
})
})() })()

View File

@ -1 +0,0 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),(()=>{const e=exports.los;function a(){cityssm.postJSON(e.urlPrefix+"/admin/doCleanupDatabase",{},e=>{var a;e.success?bulmaJS.alert({title:"Database Cleaned Up Successfully",message:`${e.inactivedRecordCount} records inactivated,\n ${e.purgedRecordCount} permanently deleted.`,contextualColorName:"success"}):bulmaJS.alert({title:"Error Cleaning Database",message:null!==(a=e.errorMessage)&&void 0!==a?a:"",contextualColorName:"danger"})})}document.querySelector("#button--cleanupDatabase").addEventListener("click",()=>{bulmaJS.confirm({title:"Cleanup Database",message:"Are you sure you want to cleanup up the database?",okButton:{text:"Yes, Cleanup Database",callbackFunction:a}})})})();

View File

@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),(()=>{var e,a;const t=exports.los;function s(){cityssm.postJSON(t.urlPrefix+"/admin/doBackupDatabase",{},e=>{var a;e.success?bulmaJS.alert({title:"Database Backed Up Successfully",message:`Backed up to ${e.fileName}`,contextualColorName:"success"}):bulmaJS.alert({title:"Error Backing Up Database",message:null!==(a=e.errorMessage)&&void 0!==a?a:"",contextualColorName:"danger"})})}function u(){cityssm.postJSON(t.urlPrefix+"/admin/doCleanupDatabase",{},e=>{var a;e.success?bulmaJS.alert({title:"Database Cleaned Up Successfully",message:`${e.inactivedRecordCount} records inactivated,\n ${e.purgedRecordCount} permanently deleted.`,contextualColorName:"success"}):bulmaJS.alert({title:"Error Cleaning Database",message:null!==(a=e.errorMessage)&&void 0!==a?a:"",contextualColorName:"danger"})})}null===(e=document.querySelector("#button--cleanupDatabase"))||void 0===e||e.addEventListener("click",()=>{bulmaJS.confirm({title:"Cleanup Database",message:"Are you sure you want to cleanup up the database?",okButton:{text:"Yes, Cleanup Database",callbackFunction:u}})}),null===(a=document.querySelector("#button--backupDatabase"))||void 0===a||a.addEventListener("click",()=>{bulmaJS.confirm({title:"Backup Database",message:"Are you sure you want to backup up the database?",okButton:{text:"Yes, Backup Database",callbackFunction:s}})})})();

View File

@ -57,7 +57,8 @@ import handler_doUpdateLotOccupantType from '../handlers/admin-post/doUpdateLotO
import handler_doMoveLotOccupantTypeUp from '../handlers/admin-post/doMoveLotOccupantTypeUp.js'; import handler_doMoveLotOccupantTypeUp from '../handlers/admin-post/doMoveLotOccupantTypeUp.js';
import handler_doMoveLotOccupantTypeDown from '../handlers/admin-post/doMoveLotOccupantTypeDown.js'; import handler_doMoveLotOccupantTypeDown from '../handlers/admin-post/doMoveLotOccupantTypeDown.js';
import handler_doDeleteLotOccupantType from '../handlers/admin-post/doDeleteLotOccupantType.js'; import handler_doDeleteLotOccupantType from '../handlers/admin-post/doDeleteLotOccupantType.js';
import handler_cleanup from '../handlers/admin-get/cleanup.js'; import handler_database from '../handlers/admin-get/database.js';
import handler_doBackupDatabase from '../handlers/admin-post/doBackupDatabase.js';
import handler_doCleanupDatabase from '../handlers/admin-post/doCleanupDatabase.js'; import handler_doCleanupDatabase from '../handlers/admin-post/doCleanupDatabase.js';
import handler_ntfyStartup from '../handlers/admin-get/ntfyStartup.js'; import handler_ntfyStartup from '../handlers/admin-get/ntfyStartup.js';
export const router = Router(); export const router = Router();
@ -119,7 +120,8 @@ router.post('/doUpdateLotOccupantType', handler_doUpdateLotOccupantType);
router.post('/doMoveLotOccupantTypeUp', handler_doMoveLotOccupantTypeUp); router.post('/doMoveLotOccupantTypeUp', handler_doMoveLotOccupantTypeUp);
router.post('/doMoveLotOccupantTypeDown', handler_doMoveLotOccupantTypeDown); router.post('/doMoveLotOccupantTypeDown', handler_doMoveLotOccupantTypeDown);
router.post('/doDeleteLotOccupantType', handler_doDeleteLotOccupantType); router.post('/doDeleteLotOccupantType', handler_doDeleteLotOccupantType);
router.get('/cleanup', handler_cleanup); router.get('/database', handler_database);
router.post('/doBackupDatabase', handler_doBackupDatabase);
router.post('/doCleanupDatabase', handler_doCleanupDatabase); router.post('/doCleanupDatabase', handler_doCleanupDatabase);
router.get('/ntfyStartup', handler_ntfyStartup); router.get('/ntfyStartup', handler_ntfyStartup);
export default router; export default router;

View File

@ -1,4 +1,4 @@
import { Router, RequestHandler } from 'express' import { Router, type RequestHandler } from 'express'
// Fee Management // Fee Management
@ -81,9 +81,10 @@ import handler_doMoveLotOccupantTypeUp from '../handlers/admin-post/doMoveLotOcc
import handler_doMoveLotOccupantTypeDown from '../handlers/admin-post/doMoveLotOccupantTypeDown.js' import handler_doMoveLotOccupantTypeDown from '../handlers/admin-post/doMoveLotOccupantTypeDown.js'
import handler_doDeleteLotOccupantType from '../handlers/admin-post/doDeleteLotOccupantType.js' import handler_doDeleteLotOccupantType from '../handlers/admin-post/doDeleteLotOccupantType.js'
// Cleanup // Database Maintenance
import handler_cleanup from '../handlers/admin-get/cleanup.js' import handler_database from '../handlers/admin-get/database.js'
import handler_doBackupDatabase from '../handlers/admin-post/doBackupDatabase.js'
import handler_doCleanupDatabase from '../handlers/admin-post/doCleanupDatabase.js' import handler_doCleanupDatabase from '../handlers/admin-post/doCleanupDatabase.js'
// Ntfy Startup // Ntfy Startup
@ -346,9 +347,11 @@ router.post(
handler_doDeleteLotOccupantType as RequestHandler handler_doDeleteLotOccupantType as RequestHandler
) )
// Cleanup // Database Maintenance
router.get('/cleanup', handler_cleanup) router.get('/database', handler_database)
router.post('/doBackupDatabase', handler_doBackupDatabase as RequestHandler)
router.post('/doCleanupDatabase', handler_doCleanupDatabase as RequestHandler) router.post('/doCleanupDatabase', handler_doCleanupDatabase as RequestHandler)

View File

@ -11,7 +11,7 @@
"strictNullChecks": true "strictNullChecks": true
}, },
"files": [ "files": [
"public-typescript/adminCleanup.ts", "public-typescript/adminDatabase.ts",
"public-typescript/adminFees.ts", "public-typescript/adminFees.ts",
"public-typescript/adminLotTypes.ts", "public-typescript/adminLotTypes.ts",
"public-typescript/adminOccupancyTypes.ts", "public-typescript/adminOccupancyTypes.ts",

View File

@ -28,9 +28,9 @@
</a> </a>
</li> </li>
<li> <li>
<a class="<%= (headTitle === "Database Cleanup" ? "is-active" : "") %>" href="<%= urlPrefix %>/admin/cleanup"> <a class="<%= (headTitle === "Database Maintenance" ? "is-active" : "") %>" href="<%= urlPrefix %>/admin/database">
<span class="icon"><i class="fas fa-fw fa-broom" aria-hidden="true"></i></span> <span class="icon"><i class="fas fa-fw fa-database" aria-hidden="true"></i></span>
<span>Database Cleanup</span> <span>Database Maintenance</span>
</a> </a>
</li> </li>
<% if (configFunctions.getProperty("application.ntfyStartup")) { %> <% if (configFunctions.getProperty("application.ntfyStartup")) { %>

View File

@ -1,60 +0,0 @@
<%- include('_header'); -%>
<div class="columns">
<div class="column is-3 is-hidden-mobile">
<%- include('_menu-admin'); -%>
</div>
<div class="column">
<nav class="breadcrumb">
<ul>
<li><a href="<%= urlPrefix %>/dashboard">Home</a></li>
<li>
<a href="#">
<span class="icon is-small"><i class="fas fa-cog" aria-hidden="true"></i></span>
<span>Administrator Tools</span>
</a>
</li>
<li class="is-active">
<a href="#" aria-current="page">
Database Cleanup
</a>
</li>
</ul>
</nav>
<h1 class="title is-1">
Database Cleanup
</h1>
<div class="message is-warning">
<div class="message-header">
Important Note
</div>
<div class="message-body">
<p>
When records are deleted in this application, they are not removed entirely.
This gives systems administrators the ability to recover deleted records.
This also can leave a small amount of garbage behind in the database.
</p>
<p class="mt-2">
This process permanently deletes records that have already been deleted over <%= configFunctions.getProperty("settings.adminCleanup.recordDeleteAgeDays") %> days ago.
Note that no active records will be affected by the cleanup process.
</p>
</div>
</div>
<p class="has-text-right">
<button class="button is-success" id="button--cleanupDatabase" data-cy="cleanup" type="button">
<span class="icon"><i class="fas fa-broom" aria-hidden="true"></i></span>
<span>Cleanup Database</span>
</button>
</p>
</div>
</div>
<%- include('_footerA'); -%>
<script src="<%= urlPrefix %>/javascripts/adminCleanup.min.js"></script>
<%- include('_footerB'); -%>

View File

@ -0,0 +1,84 @@
<%- include('_header'); -%>
<div class="columns">
<div class="column is-3 is-hidden-mobile">
<%- include('_menu-admin'); -%>
</div>
<div class="column">
<nav class="breadcrumb">
<ul>
<li><a href="<%= urlPrefix %>/dashboard">Home</a></li>
<li>
<a href="#">
<span class="icon is-small"><i class="fas fa-cog" aria-hidden="true"></i></span>
<span>Administrator Tools</span>
</a>
</li>
<li class="is-active">
<a href="#" aria-current="page">
Database Maintenance
</a>
</li>
</ul>
</nav>
<h1 class="title is-1">
Database Maintenance
</h1>
<h2 class="title is-3">
Database Backup
</h2>
<div class="message is-info">
<div class="message-body">
<p>
Before making significant changes to the records in the database,
it is a good idea to back up first!
</p>
</div>
</div>
<p class="has-text-right">
<button class="button is-success" id="button--backupDatabase" data-cy="backup" type="button">
<span class="icon"><i class="fas fa-save" aria-hidden="true"></i></span>
<span>Backup Database</span>
</button>
</p>
<h2 class="title is-3">
Database Cleanup
</h2>
<div class="message is-warning">
<div class="message-header">
Important Note about Cleanup
</div>
<div class="message-body">
<p>
When records are deleted in this application, they are not removed entirely.
This gives systems administrators the ability to recover deleted records.
This also can leave a small amount of garbage behind in the database.
</p>
<p class="mt-2">
This process permanently deletes records that have already been deleted over
<%= configFunctions.getProperty("settings.adminCleanup.recordDeleteAgeDays") %> days ago.
Note that no active records will be affected by the cleanup process.
</p>
</div>
</div>
<p class="has-text-right">
<button class="button is-success" id="button--cleanupDatabase" data-cy="cleanup" type="button">
<span class="icon"><i class="fas fa-broom" aria-hidden="true"></i></span>
<span>Cleanup Database</span>
</button>
</p>
</div>
</div>
<%- include('_footerA'); -%>
<script src="<%= urlPrefix %>/javascripts/adminDatabase.min.js"></script>
<%- include('_footerB'); -%>

View File

@ -365,13 +365,14 @@
<div class="card-content"> <div class="card-content">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<i class="fas fa-3x fa-fw fa-broom" aria-hidden="true"></i> <i class="fas fa-3x fa-fw fa-database" aria-hidden="true"></i>
</div> </div>
<div class="media-content has-text-black"> <div class="media-content has-text-black">
<h2 class="title is-4 is-marginless"> <h2 class="title is-4 is-marginless">
<a href="<%= urlPrefix %>/admin/cleanup">Database Cleanup</a> <a href="<%= urlPrefix %>/admin/database">Database Maintenance</a>
</h2> </h2>
<p> <p>
Backup the database before making significant updates.
Permanently delete records that have been previously deleted from the database. Permanently delete records that have been previously deleted from the database.
</p> </p>
</div> </div>