Phase 4: backups w/ volumes, notifications, settings & users, audit page (0.4.0)
- Backup/restore: per-stack tar.gz incl. named-volume snapshots (helper container), upload restore with rename/overwrite/conflict detection. - Notifications: ntfy/Discord/Slack/Gotify/generic webhooks, per-event subscriptions; wired into the update checker and stack lifecycle. - Settings page: update-check interval, webhook CRUD + test, user management (with last-admin safeguards). - Audit log page (searchable, paginated). - Mobile-responsive sidebar/layout. Multi-host agents and remote backup destinations (SFTP/S3) deferred. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
22d9864436
commit
8d19b09abd
@@ -1,7 +1,8 @@
|
||||
"""SQLModel table models. Importing this package registers all tables."""
|
||||
from models.audit import AuditLog
|
||||
from models.setting import Setting, Webhook
|
||||
from models.stack import Stack
|
||||
from models.template import Template
|
||||
from models.user import User
|
||||
|
||||
__all__ = ["User", "Stack", "AuditLog", "Template"]
|
||||
__all__ = ["User", "Stack", "AuditLog", "Template", "Setting", "Webhook"]
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
# Notification event names ----------------------------------------------------
|
||||
EVENT_UPDATE_AVAILABLE = "update_available"
|
||||
EVENT_STACK_START = "stack_start"
|
||||
EVENT_STACK_STOP = "stack_stop"
|
||||
EVENT_STACK_ERROR = "stack_error"
|
||||
EVENT_PULL_FAILED = "pull_failed"
|
||||
|
||||
ALL_EVENTS = [
|
||||
EVENT_UPDATE_AVAILABLE,
|
||||
EVENT_STACK_START,
|
||||
EVENT_STACK_STOP,
|
||||
EVENT_STACK_ERROR,
|
||||
EVENT_PULL_FAILED,
|
||||
]
|
||||
|
||||
WEBHOOK_TYPES = ["ntfy", "discord", "slack", "gotify", "generic"]
|
||||
|
||||
|
||||
class Setting(SQLModel, table=True):
|
||||
"""Simple key/value store for runtime-tunable settings (JSON-encoded value)."""
|
||||
|
||||
key: str = Field(primary_key=True)
|
||||
value: str # JSON-encoded
|
||||
|
||||
|
||||
class Webhook(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
name: str
|
||||
url: str
|
||||
type: str = "generic" # one of WEBHOOK_TYPES
|
||||
events: str = ",".join(ALL_EVENTS) # comma-separated subscribed events
|
||||
enabled: bool = True
|
||||
created_at: datetime = Field(default_factory=_now)
|
||||
|
||||
|
||||
# --- API schemas ---
|
||||
|
||||
|
||||
class WebhookCreate(SQLModel):
|
||||
name: str
|
||||
url: str
|
||||
type: str = "generic"
|
||||
events: list[str] = ALL_EVENTS
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
class WebhookUpdate(SQLModel):
|
||||
name: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
events: Optional[list[str]] = None
|
||||
enabled: Optional[bool] = None
|
||||
|
||||
|
||||
class WebhookRead(SQLModel):
|
||||
id: int
|
||||
name: str
|
||||
url: str
|
||||
type: str
|
||||
events: list[str]
|
||||
enabled: bool
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class SettingsRead(SQLModel):
|
||||
update_check_interval_minutes: int
|
||||
env_webhook_count: int
|
||||
available_events: list[str]
|
||||
webhook_types: list[str]
|
||||
|
||||
|
||||
class SettingsUpdate(SQLModel):
|
||||
update_check_interval_minutes: Optional[int] = None
|
||||
@@ -35,6 +35,12 @@ class UserCreate(SQLModel):
|
||||
role: str = "admin"
|
||||
|
||||
|
||||
class UserUpdate(SQLModel):
|
||||
password: Optional[str] = None
|
||||
role: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class LoginRequest(SQLModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
Reference in New Issue
Block a user