log
This commit is contained in:
@@ -79,6 +79,16 @@ class Activity(Base):
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow)
|
||||
|
||||
|
||||
class SystemLogEntry(Base):
|
||||
__tablename__ = "system_log_entries"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, index=True)
|
||||
source: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
message: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
user_id: Mapped[int | None] = mapped_column(ForeignKey("sync_users.id", ondelete="SET NULL"))
|
||||
|
||||
|
||||
class SyncRun(Base):
|
||||
__tablename__ = "sync_runs"
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from sqlalchemy import and_, or_, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.models import Activity, ActivityStatus, SyncRun, SyncRunStatus, SyncUser, utcnow
|
||||
from app.db.models import Activity, ActivityStatus, SyncRun, SyncRunStatus, SystemLogEntry, SyncUser, utcnow
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -191,6 +191,26 @@ class ActivityRepository:
|
||||
)
|
||||
|
||||
|
||||
class SystemLogRepository:
|
||||
def __init__(self, session: Session) -> None:
|
||||
self.session = session
|
||||
|
||||
def add(self, *, source: str, message: str, user_id: int | None = None) -> SystemLogEntry:
|
||||
entry = SystemLogEntry(source=source, message=message[:2000], user_id=user_id)
|
||||
self.session.add(entry)
|
||||
self.session.commit()
|
||||
return entry
|
||||
|
||||
def list_recent(self, limit: int = 50) -> list[SystemLogEntry]:
|
||||
return list(
|
||||
self.session.scalars(
|
||||
select(SystemLogEntry)
|
||||
.order_by(SystemLogEntry.created_at.desc(), SystemLogEntry.id.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class SyncRunRepository:
|
||||
def __init__(self, session: Session) -> None:
|
||||
self.session = session
|
||||
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
from app.db.models import ActivityStatus, HealthState, SyncRunStatus
|
||||
from app.db.repositories import ActivityRepository, SyncRunRepository, UserRepository
|
||||
from app.db.repositories import ActivityRepository, SyncRunRepository, SystemLogRepository, UserRepository
|
||||
from app.fit.rewriter import FitFormatError
|
||||
from app.garmin.uploader import (
|
||||
GarminAuthError,
|
||||
@@ -76,7 +76,7 @@ class SyncManager:
|
||||
self._locks: dict[int, asyncio.Lock] = {}
|
||||
self._locks_guard = asyncio.Lock()
|
||||
|
||||
def _notify_action_required(self, user: Any, message: str | None) -> None:
|
||||
def _notify_action_required(self, session: Any, user: Any, message: str | None) -> None:
|
||||
if self.notifier is None or not user.notify_email_enabled or not user.notification_email:
|
||||
return
|
||||
try:
|
||||
@@ -85,10 +85,21 @@ class SyncManager:
|
||||
subject=f"MyWhoosh-Garmin Sync: action required for {user.name}",
|
||||
body=message or "Your sync requires attention. Check the dashboard for details.",
|
||||
)
|
||||
except Exception:
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"sync_user: failed to send action-required notification for user %s", user.id, exc_info=True
|
||||
)
|
||||
try:
|
||||
SystemLogRepository(session).add(
|
||||
source="email_notification",
|
||||
message=(
|
||||
f"Failed to email {user.notification_email} for user {user.id} ({user.name}): "
|
||||
f"{type(exc).__name__}: {exc}"
|
||||
),
|
||||
user_id=user.id,
|
||||
)
|
||||
except Exception:
|
||||
logger.warning("sync_user: failed to record email-notification failure in system log", exc_info=True)
|
||||
|
||||
async def _lock_for(self, user_id: int) -> asyncio.Lock:
|
||||
async with self._locks_guard:
|
||||
@@ -389,7 +400,7 @@ class SyncManager:
|
||||
)
|
||||
session.commit()
|
||||
if user.action_reason is not None and user.action_reason != previous_action_reason:
|
||||
self._notify_action_required(user, summary_error)
|
||||
self._notify_action_required(session, user, summary_error)
|
||||
return SyncOutcome(
|
||||
user_id=user.id,
|
||||
status=status.value,
|
||||
|
||||
@@ -5,7 +5,7 @@ from sqlalchemy import func, select
|
||||
from app.auth.admin import require_admin
|
||||
from app.auth.csrf import ensure_csrf_token, validate_csrf
|
||||
from app.db.models import Activity
|
||||
from app.db.repositories import ActivityRepository, UserRepository
|
||||
from app.db.repositories import ActivityRepository, SystemLogRepository, UserRepository
|
||||
from app.sync.manager import SyncAlreadyRunning
|
||||
from app.web.routes import templates
|
||||
|
||||
@@ -103,6 +103,7 @@ def system_page(request: Request):
|
||||
with request.app.state.session_factory() as session:
|
||||
user_count = len(UserRepository(session).list_all())
|
||||
activity_count = session.scalar(select(func.count()).select_from(Activity)) or 0
|
||||
log_entries = SystemLogRepository(session).list_recent(limit=50)
|
||||
return templates.TemplateResponse(request, "system.html", {
|
||||
"csrf_token": ensure_csrf_token(request),
|
||||
"app_version": APP_VERSION,
|
||||
@@ -111,4 +112,5 @@ def system_page(request: Request):
|
||||
"next_tick": scheduler.next_tick,
|
||||
"user_count": user_count,
|
||||
"activity_count": activity_count,
|
||||
"log_entries": log_entries,
|
||||
})
|
||||
|
||||
@@ -31,4 +31,30 @@
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit">Sync all now</button>
|
||||
</form>
|
||||
|
||||
<h2>System log</h2>
|
||||
<div class="card">
|
||||
{% if log_entries %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Source</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in log_entries %}
|
||||
<tr>
|
||||
<td>{{ entry.created_at }}</td>
|
||||
<td>{{ entry.source }}</td>
|
||||
<td>{{ entry.message }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="empty-state">No system log entries.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user