diff --git a/.env.example b/.env.example
index dd23ec7..234a0b4 100644
--- a/.env.example
+++ b/.env.example
@@ -15,3 +15,6 @@ SESSION_SECRET=change_me_to_long_random_secret
ENABLE_TELEGRAM_NOTIFICATIONS=false
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
+
+# Optional container timezone for scheduler/logs
+TZ=UTC
diff --git a/.gitignore b/.gitignore
index e6dd91c..a798117 100644
--- a/.gitignore
+++ b/.gitignore
@@ -166,3 +166,4 @@ temp/
.cache/
audit.log
backups/
+settings.json
diff --git a/Dockerfile b/Dockerfile
index c597732..2498b5a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,6 +4,9 @@ FROM node:20-alpine
# Рабочая директория
WORKDIR /app
+# pg_dump for database backups
+RUN apk add --no-cache postgresql-client
+
# Копируем package.json
COPY package*.json ./
diff --git a/docker-compose.yml b/docker-compose.yml
index effaa7c..7a56db4 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -38,6 +38,9 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
+ - ./backups:/app/backups
+ - ./audit.log:/app/audit.log
+ - ./settings.json:/app/settings.json
depends_on:
db:
diff --git a/public/assets/app.js b/public/assets/app.js
index 1900041..f735b13 100644
--- a/public/assets/app.js
+++ b/public/assets/app.js
@@ -10,6 +10,7 @@
this.currentContainer = '';
this.logStream = null;
this.logsBuffer = [];
+ this.currentSettings = null;
this.currentPage = 1;
this.limit = 10;
this.editingRecord = null;
@@ -152,6 +153,7 @@
document.getElementById('logsButton').classList.toggle('hidden', !this.getPermissions().canViewLogs);
document.getElementById('usersButton').classList.toggle('hidden', !this.getPermissions().canManageUsers);
document.getElementById('backupsButton').classList.toggle('hidden', !this.getPermissions().canManageUsers);
+ document.getElementById('settingsButton').classList.toggle('hidden', !this.getPermissions().canManageUsers);
document.getElementById('auditButton').classList.toggle('hidden', !this.getPermissions().canManageUsers);
document.querySelector('button[onclick="app.showSQLPanel()"]').style.display = this.getPermissions().canRunSql ? '' : 'none';
document.querySelector('button[onclick="app.showCreateTableModal()"]').style.display = this.getPermissions().canCreate ? '' : 'none';
@@ -1226,6 +1228,11 @@
document.getElementById('backupsModal').classList.remove('hidden');
}
+ async showSettingsModal() {
+ await this.loadSettings();
+ document.getElementById('settingsModal').classList.remove('hidden');
+ }
+
async loadBackups() {
try {
const response = await fetch('/api/backups');
@@ -1239,7 +1246,7 @@
${backup.filename}
-
${backup.createdAt} · ${backup.size} bytes
+
${backup.createdAt} - ${backup.kind} - ${backup.size} bytes
Download
@@ -1267,6 +1274,66 @@
}
}
+ async loadSettings() {
+ try {
+ const response = await fetch('/api/settings');
+ const settings = await response.json();
+ if (!response.ok) {
+ throw new Error(settings.error || 'Failed to load settings');
+ }
+
+ this.currentSettings = settings;
+ document.getElementById('settingsBackupsEnabled').checked = Boolean(settings.backups?.enabled);
+ document.getElementById('settingsBackupTime').value = `${String(settings.backups?.hour ?? 3).padStart(2, '0')}:${String(settings.backups?.minute ?? 0).padStart(2, '0')}`;
+ document.getElementById('settingsKeepLast').value = settings.backups?.keepLast ?? 14;
+ document.getElementById('settingsIncludeAppSnapshot').checked = settings.backups?.includeAppSnapshot !== false;
+ document.getElementById('settingsTelegramEnabled').checked = Boolean(settings.telegram?.enabled);
+ document.getElementById('settingsTelegramToken').value = settings.telegram?.botToken || '';
+ document.getElementById('settingsTelegramChatId').value = settings.telegram?.chatId || '';
+ } catch (err) {
+ this.showToast(err.message, 'error');
+ }
+ }
+
+ async saveSettings() {
+ const [hour, minute] = (document.getElementById('settingsBackupTime').value || '03:00').split(':').map(Number);
+ const payload = {
+ backups: {
+ enabled: document.getElementById('settingsBackupsEnabled').checked,
+ hour,
+ minute,
+ keepLast: Number(document.getElementById('settingsKeepLast').value || 14),
+ includeAppSnapshot: document.getElementById('settingsIncludeAppSnapshot').checked,
+ },
+ telegram: {
+ enabled: document.getElementById('settingsTelegramEnabled').checked,
+ botToken: document.getElementById('settingsTelegramToken').value.trim(),
+ chatId: document.getElementById('settingsTelegramChatId').value.trim(),
+ },
+ };
+
+ try {
+ const response = await fetch('/api/settings', {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Request-Source': 'WEB',
+ },
+ body: JSON.stringify(payload),
+ });
+ const result = await response.json();
+ if (!response.ok) {
+ throw new Error(result.error || 'Failed to save settings');
+ }
+
+ this.currentSettings = result.settings;
+ this.showToast('Settings saved', 'success');
+ this.closeModal('settingsModal');
+ } catch (err) {
+ this.showToast(err.message, 'error');
+ }
+ }
+
async loadAuditLog() {
try {
const response = await fetch('/api/audit');
diff --git a/public/index.html b/public/index.html
index 45dea56..11de856 100644
--- a/public/index.html
+++ b/public/index.html
@@ -74,6 +74,10 @@
Backups
+