feat: admin maintenance and analytics stubs (#1107)

* add tail log viewer routes

* add log viewer

* add _mealie to ignore directories

* add detailed breakdown of storage

* generate types

* add dialog to view breakdown

* cleanup mobile UI

* move migrations page

* spelling

* init analytics page

* move route up

* add remove temp files function

* analytics API client

* stub out analytics pages

* generate types

* stub out analytics routes

* update names

* ignore types

* temporary remove analytics from sidebar
This commit is contained in:
Hayden
2022-03-29 08:25:28 -08:00
committed by GitHub
parent 6f309d7a89
commit 1a23f867da
23 changed files with 536 additions and 59 deletions

View File

@@ -2,6 +2,7 @@ from mealie.routes._base.routers import AdminAPIRouter
from . import (
admin_about,
admin_analytics,
admin_backups,
admin_email,
admin_log,
@@ -15,9 +16,10 @@ router = AdminAPIRouter(prefix="/admin")
router.include_router(admin_about.router, tags=["Admin: About"])
router.include_router(admin_log.router, tags=["Admin: Log"])
router.include_router(admin_management_users.router)
router.include_router(admin_management_groups.router)
router.include_router(admin_management_users.router, tags=["Admin: Manage Users"])
router.include_router(admin_management_groups.router, tags=["Admin: Manage Groups"])
router.include_router(admin_email.router, tags=["Admin: Email"])
router.include_router(admin_server_tasks.router, tags=["Admin: Server Tasks"])
router.include_router(admin_backups.router, tags=["Admin: Backups"])
router.include_router(admin_maintenance.router, tags=["Admin: Maintenance"])
router.include_router(admin_analytics.router, tags=["Admin: Analytics"])

View File

@@ -0,0 +1,20 @@
from functools import cached_property
from fastapi import APIRouter
from mealie.routes._base import BaseAdminController, controller
from mealie.schema.analytics.analytics import MealieAnalytics
from mealie.services.analytics.service_analytics import AnalyticsService
router = APIRouter(prefix="/analytics")
@controller(router)
class AdminAboutController(BaseAdminController):
@cached_property
def service(self) -> AnalyticsService:
return AnalyticsService(self.repos)
@router.get("", response_model=MealieAnalytics)
def get_analytics(self):
return self.service.calculate_analytics()

View File

@@ -10,6 +10,7 @@ from mealie.core.root_logger import LOGGER_FILE
from mealie.pkgs.stats import fs_stats
from mealie.routes._base import BaseAdminController, controller
from mealie.schema.admin import MaintenanceSummary
from mealie.schema.admin.maintenance import MaintenanceLogs, MaintenanceStorageDetails
from mealie.schema.response import ErrorResponse, SuccessResponse
router = APIRouter(prefix="/maintenance")
@@ -54,6 +55,16 @@ def clean_recipe_folders(root_dir: Path, dry_run: bool) -> int:
return cleaned_dirs
def tail_log(log_file: Path, n: int) -> list[str]:
try:
with open(log_file, "r") as f:
lines = f.readlines()
except FileNotFoundError:
return ["no log file found"]
return lines[-n:]
@controller(router)
class AdminMaintenanceController(BaseAdminController):
@router.get("", response_model=MaintenanceSummary)
@@ -72,6 +83,21 @@ class AdminMaintenanceController(BaseAdminController):
cleanable_dirs=clean_recipe_folders(self.deps.folders.RECIPE_DATA_DIR, dry_run=True),
)
@router.get("/logs", response_model=MaintenanceLogs)
def get_logs(self, lines: int = 200):
return MaintenanceLogs(logs=tail_log(LOGGER_FILE, lines))
@router.get("/storage", response_model=MaintenanceStorageDetails)
def get_storage_details(self):
return MaintenanceStorageDetails(
temp_dir_size=fs_stats.pretty_size(fs_stats.get_dir_size(self.deps.folders.TEMP_DIR)),
backups_dir_size=fs_stats.pretty_size(fs_stats.get_dir_size(self.deps.folders.BACKUP_DIR)),
groups_dir_size=fs_stats.pretty_size(fs_stats.get_dir_size(self.deps.folders.GROUPS_DIR)),
recipes_dir_size=fs_stats.pretty_size(fs_stats.get_dir_size(self.deps.folders.RECIPE_DATA_DIR)),
user_dir_size=fs_stats.pretty_size(fs_stats.get_dir_size(self.deps.folders.USER_DIR)),
)
@router.post("/clean/images", response_model=SuccessResponse)
def clean_images(self):
"""
@@ -83,6 +109,16 @@ class AdminMaintenanceController(BaseAdminController):
except Exception as e:
raise HTTPException(status_code=500, detail=ErrorResponse.respond("Failed to clean images")) from e
@router.post("/clean/temp", response_model=SuccessResponse)
def clean_temp(self):
try:
shutil.rmtree(self.deps.folders.TEMP_DIR)
self.deps.folders.TEMP_DIR.mkdir(parents=True, exist_ok=True)
except Exception as e:
raise HTTPException(status_code=500, detail=ErrorResponse.respond("Failed to clean temp")) from e
return SuccessResponse.respond("'.temp' directory cleaned")
@router.post("/clean/recipe-folders", response_model=SuccessResponse)
def clean_recipe_folders(self):
"""

View File

@@ -1,2 +1,3 @@
# GENERATED CODE - DO NOT MODIFY BY HAND
from .mealie_model import *
from .types import *

View File

@@ -6,3 +6,15 @@ class MaintenanceSummary(MealieModel):
log_file_size: str
cleanable_images: int
cleanable_dirs: int
class MaintenanceStorageDetails(MealieModel):
temp_dir_size: str
backups_dir_size: str
groups_dir_size: str
recipes_dir_size: str
user_dir_size: str
class MaintenanceLogs(MealieModel):
logs: list[str]

View File

@@ -0,0 +1,2 @@
# GENERATED CODE - DO NOT MODIFY BY HAND
from .analytics import *

View File

@@ -0,0 +1,19 @@
from pydantic import UUID4
from .._mealie import MealieModel
class MealieAnalytics(MealieModel):
installation_id: UUID4
version: str
database_type: str
using_email: bool
using_ldap: bool
api_tokens: int
users: int
groups: int
recipes: int
shopping_lists: int
cookbooks: int

View File

@@ -11,5 +11,5 @@ from .recipe_nutrition import *
from .recipe_settings import *
from .recipe_share_token import * # type: ignore
from .recipe_step import *
from .recipe_tool import *
from .recipe_tool import * # type: ignore
from .request_helpers import *

View File

@@ -0,0 +1,33 @@
import uuid
from mealie.core.settings.static import APP_VERSION
from mealie.repos.repository_factory import AllRepositories
from mealie.schema.analytics.analytics import MealieAnalytics
from mealie.services._base_service import BaseService
class AnalyticsService(BaseService):
def __init__(self, repos: AllRepositories):
self.repos = repos
super().__init__()
def _databate_type(self) -> str:
return "sqlite" if "sqlite" in self.settings.DB_URL else "postgres" # type: ignore
def calculate_analytics(self) -> MealieAnalytics:
return MealieAnalytics(
# Site Wide Analytics
installation_id=uuid.uuid4(),
version=APP_VERSION,
database_type=self._databate_type(),
# Optional Configs
using_ldap=self.settings.LDAP_ENABLED,
using_email=self.settings.SMTP_ENABLE,
# Stats
api_tokens=0,
users=0,
groups=0,
recipes=0,
shopping_lists=0,
cookbooks=0,
)