mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-10-22 22:11:15 +00:00
fix(web): run manual backups in standalone viewer (#280)
This commit is contained in:
1 parent
a861e848af
commit
3b1edea8af
3 files changed
+35
-57
No files matched your search
@@ -276,16 +276,13 @@ log_max_bytes = 5242880 # 5 MB per file
|
||||
log_backup_count = 3 # number of rotated files to keep
|
||||
```
|
||||
|
||||
**Database Backup** — schedule automatic backups from the Config tab:
|
||||
**Database Backup**: schedule automatic backups from the Config tab. The viewer stores these settings in the database, not in a `[Maintenance]` section of `config.ini`:
|
||||
|
||||
```ini
|
||||
[Maintenance]
|
||||
db_backup_enabled = true
|
||||
db_backup_schedule = daily # daily | weekly | manual
|
||||
db_backup_time = 02:00 # HH:MM local time
|
||||
db_backup_retention_count = 7
|
||||
db_backup_dir = /data/backups
|
||||
```
|
||||
- Enabled
|
||||
- Schedule: daily, weekly, or manual
|
||||
- Time: local `HH:MM`
|
||||
- Retention count
|
||||
- Backup directory
|
||||
|
||||
The **Maintenance Status** card in the Config tab shows the last backup time, next scheduled run, and log rotation status.
|
||||
|
||||
@@ -453,15 +450,8 @@ When `json_logging = true` each log line is a JSON object:
|
||||
```
|
||||
|
||||
### Maintenance
|
||||
```ini
|
||||
[Maintenance]
|
||||
db_backup_enabled = false
|
||||
db_backup_schedule = daily # daily | weekly | manual
|
||||
db_backup_time = 02:00 # HH:MM local time
|
||||
db_backup_retention_count = 7
|
||||
db_backup_dir = /data/backups
|
||||
email_attach_log = false # attach current log file (≤ 5 MB) to nightly email before rotation
|
||||
```
|
||||
|
||||
Configure database backups and nightly-email log attachments in the Web Viewer Config tab. These settings are stored in the SQLite `bot_metadata` table and do not use a `[Maintenance]` section in `config.ini`.
|
||||
|
||||
### Notifications
|
||||
```ini
|
||||
|
||||
@@ -54,6 +54,7 @@ from modules.db_retention import (
|
||||
retention_delete_settings,
|
||||
)
|
||||
from modules.ini_writer import IniValueError, update_ini_values
|
||||
from modules.maintenance import MaintenanceRunner
|
||||
from modules.scheduled_message_admin import (
|
||||
SECTION as SCHEDULED_MESSAGES_SECTION,
|
||||
)
|
||||
@@ -634,6 +635,14 @@ class BotDataViewer:
|
||||
# Now set db_manager on the minimal bot for RepeaterManager
|
||||
minimal_bot.db_manager = self.db_manager
|
||||
|
||||
# The viewer runs as a separate process, so it cannot call the bot's
|
||||
# MessageScheduler directly. MaintenanceRunner only needs this small
|
||||
# bot facade for manual database backups.
|
||||
self._maintenance_runner = MaintenanceRunner(
|
||||
minimal_bot,
|
||||
get_current_time=datetime.now,
|
||||
)
|
||||
|
||||
# The viewer only needs RepeaterManager for the manual geocode
|
||||
# endpoint, so defer its setup until that endpoint is actually used.
|
||||
self._repeater_manager_bot = minimal_bot
|
||||
@@ -2200,12 +2209,11 @@ class BotDataViewer:
|
||||
def api_maintenance_backup_now():
|
||||
"""Trigger an immediate DB backup outside the normal schedule."""
|
||||
try:
|
||||
bot = getattr(self, 'bot', None)
|
||||
scheduler = getattr(bot, 'scheduler', None) if bot else None
|
||||
if scheduler is None or not hasattr(scheduler, 'run_db_backup'):
|
||||
return jsonify({'success': False, 'error': 'Scheduler not available'}), 503
|
||||
scheduler.run_db_backup()
|
||||
# Read outcome written by _run_db_backup
|
||||
runner = getattr(self, '_maintenance_runner', None)
|
||||
if runner is None:
|
||||
return jsonify({'success': False, 'error': 'Maintenance runner not available'}), 503
|
||||
runner.run_db_backup()
|
||||
# Read the outcome written by MaintenanceRunner.
|
||||
path = self.db_manager.get_metadata('maint.status.db_backup_path') or ''
|
||||
outcome = self.db_manager.get_metadata('maint.status.db_backup_outcome') or ''
|
||||
if outcome.startswith('error'):
|
||||
|
||||
+13
-33
@@ -2773,59 +2773,39 @@ class TestDbBackupDirValidation:
|
||||
class TestBackupNowRoute:
|
||||
"""Tests for POST /api/maintenance/backup_now endpoint."""
|
||||
|
||||
def test_returns_503_when_no_scheduler(self, viewer):
|
||||
"""Returns 503 when bot/scheduler is not attached."""
|
||||
# viewer fixture has no bot attached
|
||||
with viewer.app.test_client() as c:
|
||||
resp = c.post("/api/maintenance/backup_now")
|
||||
def test_returns_503_when_no_maintenance_runner(self, viewer):
|
||||
"""Returns 503 when the viewer's maintenance runner is unavailable."""
|
||||
with patch.object(viewer, '_maintenance_runner', None):
|
||||
with viewer.app.test_client() as c:
|
||||
resp = c.post("/api/maintenance/backup_now")
|
||||
assert resp.status_code == 503
|
||||
data = resp.get_json()
|
||||
assert data["success"] is False
|
||||
assert "error" in data # Error details sanitized in 5xx responses
|
||||
|
||||
def test_returns_200_on_successful_backup(self, viewer, tmp_path):
|
||||
"""Returns 200 with success=True and path when backup succeeds."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
"""The standalone viewer performs a real SQLite backup."""
|
||||
backup_dir = tmp_path / "backups"
|
||||
viewer.db_manager.set_metadata('maint.db_backup_dir', str(backup_dir))
|
||||
|
||||
mock_scheduler = MagicMock()
|
||||
|
||||
def fake_run_db_backup():
|
||||
# Simulate what run_db_backup writes to metadata
|
||||
viewer.db_manager.set_metadata(
|
||||
'maint.status.db_backup_path', str(tmp_path / "test.db")
|
||||
)
|
||||
viewer.db_manager.set_metadata('maint.status.db_backup_outcome', 'ok')
|
||||
|
||||
mock_scheduler.run_db_backup = fake_run_db_backup
|
||||
mock_bot = MagicMock()
|
||||
mock_bot.scheduler = mock_scheduler
|
||||
|
||||
with patch.object(viewer, 'bot', mock_bot, create=True):
|
||||
with viewer.app.test_client() as c:
|
||||
resp = c.post("/api/maintenance/backup_now")
|
||||
with viewer.app.test_client() as c:
|
||||
resp = c.post("/api/maintenance/backup_now")
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert data["success"] is True
|
||||
assert "test.db" in data["path"]
|
||||
assert Path(data["path"]).is_file()
|
||||
assert Path(data["path"]).parent == backup_dir
|
||||
|
||||
def test_returns_500_on_backup_error(self, viewer):
|
||||
"""Returns 500 with success=False when backup writes an error outcome."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
mock_scheduler = MagicMock()
|
||||
|
||||
def fake_run_db_backup():
|
||||
viewer.db_manager.set_metadata('maint.status.db_backup_path', '')
|
||||
viewer.db_manager.set_metadata(
|
||||
'maint.status.db_backup_outcome', 'error: cannot create dir'
|
||||
)
|
||||
|
||||
mock_scheduler.run_db_backup = fake_run_db_backup
|
||||
mock_bot = MagicMock()
|
||||
mock_bot.scheduler = mock_scheduler
|
||||
|
||||
with patch.object(viewer, 'bot', mock_bot, create=True):
|
||||
with patch.object(viewer._maintenance_runner, 'run_db_backup', fake_run_db_backup):
|
||||
with viewer.app.test_client() as c:
|
||||
resp = c.post("/api/maintenance/backup_now")
|
||||
|
||||
|
||||
Reference in new issue
Block a user