feat(settings): per-option save, live worker sync, audit log, and rollback
A) Per-option Save Button
- Add per-setting Save button in settings.html (visible only when value changed)
- Button calls POST /api/settings/{key} directly; existing bulk Save retained
- Add Audit Log link in settings page header
B) Immediate Worker Sync
- New app/utils/settings_sync.py with notify_settings_updated() (Redis version key)
and register_settings_reload_signal() (Celery task_prerun handler)
- Register signal in celery_worker.py at startup
- All API write paths call notify_settings_updated() after successful saves
C) Audit Log
- Add SettingsAuditLog model (key, old_value, new_value, changed_by, changed_at, action)
- save_setting_to_db / delete_setting_from_db accept changed_by and write audit entries
- New get_audit_log() service function (masks sensitive values)
- New GET /api/settings/audit-log endpoint (admin-only)
- New GET /admin/settings/audit-log view + audit_log.html template
- Visible to all admins (per clarified requirement)
D) Config Rollback / History
- New get_setting_history() and rollback_setting() service functions
- New GET /api/settings/{key}/history endpoint
- New POST /api/settings/{key}/rollback/{history_id} endpoint
- Rollback buttons in audit_log.html with confirmation dialog
- Tests: 25 new tests covering audit log, rollback, worker sync helpers, and API endpoints
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
+45
-10
@@ -1,6 +1,7 @@
|
||||
# app/models.py
|
||||
|
||||
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, func
|
||||
from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Integer, String,
|
||||
Text, UniqueConstraint, func)
|
||||
|
||||
from app.database import Base
|
||||
|
||||
@@ -69,21 +70,33 @@ class FileProcessingStep(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
file_id = Column(Integer, ForeignKey(_FILES_ID_FK), nullable=False, index=True)
|
||||
step_name = Column(String, nullable=False, index=True) # e.g., "hash_file", "upload_to_dropbox"
|
||||
status = Column(String, nullable=False) # "pending", "in_progress", "success", "failure", "skipped"
|
||||
step_name = Column(
|
||||
String, nullable=False, index=True
|
||||
) # e.g., "hash_file", "upload_to_dropbox"
|
||||
status = Column(
|
||||
String, nullable=False
|
||||
) # "pending", "in_progress", "success", "failure", "skipped"
|
||||
started_at = Column(DateTime(timezone=True), nullable=True) # When step started
|
||||
completed_at = Column(DateTime(timezone=True), nullable=True) # When step finished (success/failure)
|
||||
completed_at = Column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
) # When step finished (success/failure)
|
||||
error_message = Column(Text, nullable=True) # Error message if status is "failure"
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
updated_at = Column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
__table_args__ = (UniqueConstraint("file_id", "step_name", name="unique_file_step"),)
|
||||
__table_args__ = (
|
||||
UniqueConstraint("file_id", "step_name", name="unique_file_step"),
|
||||
)
|
||||
|
||||
|
||||
class ProcessingLog(Base):
|
||||
__tablename__ = "processing_logs"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
file_id = Column(Integer, ForeignKey(_FILES_ID_FK), nullable=True) # Optional file association
|
||||
file_id = Column(
|
||||
Integer, ForeignKey(_FILES_ID_FK), nullable=True
|
||||
) # Optional file association
|
||||
task_id = Column(String, index=True) # Celery task ID
|
||||
step_name = Column(String) # e.g., "OCR", "convert_to_pdf", "upload_s3"
|
||||
status = Column(String) # "pending", "in_progress", "success", "failure"
|
||||
@@ -98,7 +111,29 @@ class ApplicationSettings(Base):
|
||||
__tablename__ = "application_settings"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
key = Column(String, unique=True, index=True, nullable=False) # Setting key (e.g., 'database_url')
|
||||
value = Column(String, nullable=True) # Setting value (stored as string, converted as needed)
|
||||
key = Column(
|
||||
String, unique=True, index=True, nullable=False
|
||||
) # Setting key (e.g., 'database_url')
|
||||
value = Column(
|
||||
String, nullable=True
|
||||
) # Setting value (stored as string, converted as needed)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
updated_at = Column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
|
||||
class SettingsAuditLog(Base):
|
||||
"""Audit log for all configuration changes made via the settings UI."""
|
||||
|
||||
__tablename__ = "settings_audit_log"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
key = Column(String, nullable=False, index=True) # Setting key that was changed
|
||||
old_value = Column(String, nullable=True) # Previous value (None if first-time set)
|
||||
new_value = Column(String, nullable=True) # New value (None if deleted)
|
||||
changed_by = Column(
|
||||
String, nullable=False
|
||||
) # Username of the admin who made the change
|
||||
changed_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||
action = Column(String, nullable=False) # "update" or "delete"
|
||||
|
||||
Reference in New Issue
Block a user