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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 536 additions and 59 deletions

View file

@ -76,7 +76,7 @@ def generate_typescript_types() -> None:
schema_path = PROJECT_DIR / "mealie" / "schema"
types_dir = PROJECT_DIR / "frontend" / "types" / "api-types"
ignore_dirs = ["__pycache__", "static"]
ignore_dirs = ["__pycache__", "static", "_mealie"]
skipped_files: list[Path] = []
skipped_dirs: list[Path] = []

View file

@ -4,6 +4,7 @@ import { AdminUsersApi } from "./admin/admin-users";
import { AdminGroupsApi } from "./admin/admin-groups";
import { AdminBackupsApi } from "./admin/admin-backups";
import { AdminMaintenanceApi } from "./admin/admin-maintenance";
import { AdminAnalyticsApi } from "./admin/admin-analytics";
import { ApiRequestInstance } from "~/types/api";
export class AdminAPI {
@ -13,6 +14,7 @@ export class AdminAPI {
public groups: AdminGroupsApi;
public backups: AdminBackupsApi;
public maintenance: AdminMaintenanceApi;
public analytics: AdminAnalyticsApi;
constructor(requests: ApiRequestInstance) {
this.about = new AdminAboutAPI(requests);
@ -21,6 +23,7 @@ export class AdminAPI {
this.groups = new AdminGroupsApi(requests);
this.backups = new AdminBackupsApi(requests);
this.maintenance = new AdminMaintenanceApi(requests);
this.analytics = new AdminAnalyticsApi(requests);
Object.freeze(this);
}

View file

@ -0,0 +1,14 @@
import { BaseAPI } from "../_base";
import { MealieAnalytics } from "~/types/api-types/analytics";
const prefix = "/api";
const routes = {
base: `${prefix}/admin/analytics`,
};
export class AdminAnalyticsApi extends BaseAPI {
async getAnalytics() {
return await this.requests.get<MealieAnalytics>(routes.base);
}
}

View file

@ -1,11 +1,14 @@
import { BaseAPI } from "../_base";
import { SuccessResponse } from "~/types/api-types/response";
import { MaintenanceSummary } from "~/types/api-types/admin";
import { MaintenanceLogs, MaintenanceStorageDetails, MaintenanceSummary } from "~/types/api-types/admin";
const prefix = "/api";
const routes = {
base: `${prefix}/admin/maintenance`,
storage: `${prefix}/admin/maintenance/storage`,
logs: (lines: number) => `${prefix}/admin/maintenance/logs?lines=${lines}`,
cleanTemp: `${prefix}/admin/maintenance/clean/temp`,
cleanImages: `${prefix}/admin/maintenance/clean/images`,
cleanRecipeFolders: `${prefix}/admin/maintenance/clean/recipe-folders`,
cleanLogFile: `${prefix}/admin/maintenance/clean/logs`,
@ -16,6 +19,14 @@ export class AdminMaintenanceApi extends BaseAPI {
return this.requests.get<MaintenanceSummary>(routes.base);
}
async getStorageDetails() {
return await this.requests.get<MaintenanceStorageDetails>(routes.storage);
}
async cleanTemp() {
return await this.requests.post<SuccessResponse>(routes.cleanTemp, {});
}
async cleanImages() {
return await this.requests.post<SuccessResponse>(routes.cleanImages, {});
}
@ -27,4 +38,8 @@ export class AdminMaintenanceApi extends BaseAPI {
async cleanLogFile() {
return await this.requests.post<SuccessResponse>(routes.cleanLogFile, {});
}
async logs(lines: number) {
return await this.requests.get<MaintenanceLogs>(routes.logs(lines));
}
}

View file

@ -44,21 +44,21 @@ export default defineComponent({
});
const topLinks: SidebarLinks = [
// {
// icon: $globals.icons.viewDashboard,
// to: "/admin/dashboard",
// title: i18n.t("sidebar.dashboard"),
// },
{
icon: $globals.icons.cog,
to: "/admin/site-settings",
title: i18n.t("sidebar.site-settings"),
},
{
icon: $globals.icons.cog,
icon: $globals.icons.wrench,
to: "/admin/maintenance",
title: "Maintenance",
},
// {
// icon: $globals.icons.chart,
// to: "/admin/analytics",
// title: "Analytics",
// },
{
icon: $globals.icons.user,
to: "/admin/manage/users",

View file

@ -0,0 +1,137 @@
<template>
<v-container fluid class="md-container">
<BannerExperimental></BannerExperimental>
<BaseCardSectionTitle title="Site Analytics">
Your instance of Mealie can send anonymous usage statistics to the Mealie project team. This is done to help us
gauge the usage of mealie, provide public statistics and to help us improve the user experience.
<p class="pt-4 pb-0 mb-0">
Your installation creates a UUID that is used to identify your installation,
<strong> this is randomly generated using the UUID4 implementation in python</strong>. This UUID is stored on
our analytics server and used to ensure your data is only counted once.
</p>
</BaseCardSectionTitle>
<section>
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.chart" title="Analytics Settings">
When you opt into analytics your install will register itself with the Analytics API to count the installation
and register your generated anonymous installation ID
</BaseCardSectionTitle>
<v-card-text>
<v-switch v-model="state.analyticsEnabled" label="Collect Anonymous Analytics" />
</v-card-text>
</section>
<section class="my-8">
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.chart" title="Analytics Data">
This is a list of all the data that is sent to the Mealie project team.
</BaseCardSectionTitle>
<v-card class="ma-2">
<template v-for="(value, idx) in data">
<v-list-item :key="`item-${idx}`">
<v-list-item-title class="py-2">
<div>{{ value.text }}</div>
<v-list-item-subtitle class="text-end"> {{ getValue(value.valueKey) }} </v-list-item-subtitle>
</v-list-item-title>
</v-list-item>
<v-divider :key="`divider-${idx}`" class="mx-2"></v-divider>
</template>
</v-card>
</section>
</v-container>
</template>
<script lang="ts">
import { defineComponent, reactive, useAsync } from "@nuxtjs/composition-api";
import { useAdminApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils";
import { MealieAnalytics } from "~/types/api-types/analytics";
type DisplayData = {
text: string;
valueKey: keyof MealieAnalytics;
};
export default defineComponent({
layout: "admin",
setup() {
const adminApi = useAdminApi();
const state = reactive({
analyticsEnabled: false,
});
const analyticsData = useAsync(async () => {
const { data } = await adminApi.analytics.getAnalytics();
return data;
}, useAsyncKey());
function getValue(key: keyof MealieAnalytics) {
if (!analyticsData.value) {
return "";
}
return analyticsData.value[key];
}
const data: DisplayData[] = [
{
text: "Installation Id",
valueKey: "installationId",
},
{
text: "Version",
valueKey: "version",
},
{
text: "Database",
valueKey: "databaseType",
},
{
text: "Using Email",
valueKey: "usingEmail",
},
{
text: "Using LDAP",
valueKey: "usingLdap",
},
{
text: "API Tokens",
valueKey: "apiTokens",
},
{
text: "Users",
valueKey: "users",
},
{
text: "Recipes",
valueKey: "recipes",
},
{
text: "Groups",
valueKey: "groups",
},
{
text: "Shopping Lists",
valueKey: "shoppingLists",
},
{
text: "Cookbooks",
valueKey: "cookbooks",
},
];
return {
data,
state,
analyticsData,
getValue,
};
},
head() {
return {
title: "Analytics",
};
},
});
</script>
<style scoped></style>

View file

@ -1,51 +1,69 @@
<template>
<v-container fluid class="narrow-container">
<BaseDialog v-model="state.storageDetails" title="Storage Details" :icon="$globals.icons.folderOutline">
<div class="py-2">
<template v-for="(value, key, idx) in storageDetails">
<v-list-item :key="`item-${key}`">
<v-list-item-title>
<div>{{ storageDetailsText(key) }}</div>
</v-list-item-title>
<v-list-item-subtitle class="text-end"> {{ value }} </v-list-item-subtitle>
</v-list-item>
<v-divider v-if="idx != 4" :key="`divider-${key}`" class="mx-2"></v-divider>
</template>
</div>
</BaseDialog>
<BasePageTitle divider>
<template #title> Site Maintenance </template>
</BasePageTitle>
<BannerExperimental />
<div class="d-flex justify-end">
<ButtonLink to="/admin/maintenance/logs" text="Logs" :icon="$globals.icons.file" />
</div>
<section>
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.cog" title="Summary"> </BaseCardSectionTitle>
<div class="mb-6 ml-2">
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.wrench" title="Summary"> </BaseCardSectionTitle>
<div class="mb-6 ml-2 d-flex" style="gap: 0.3rem">
<BaseButton color="info" @click="getSummary">
<template #icon> {{ $globals.icons.tools }} </template>
Get Summary
</BaseButton>
<BaseButton color="info" @click="openDetails">
<template #icon> {{ $globals.icons.folderOutline }} </template>
Details
</BaseButton>
</div>
<v-card class="ma-2" :loading="state.fetchingInfo">
<template v-for="(value, idx) in info">
<v-list-item :key="`item-${idx}`">
<v-list-item-title>
<v-list-item-title class="py-2">
<div>{{ value.name }}</div>
<v-list-item-subtitle class="text-end"> {{ value.value }} </v-list-item-subtitle>
</v-list-item-title>
<v-list-item-subtitle class="text-end"> {{ value.value }} </v-list-item-subtitle>
</v-list-item>
<v-divider :key="`divider-${idx}`" class="mx-2"></v-divider>
</template>
</v-card>
</section>
<section>
<BaseCardSectionTitle class="pb-0 mt-8" :icon="$globals.icons.cog" title="Actions">
<BaseCardSectionTitle class="pb-0 mt-8" :icon="$globals.icons.wrench" title="Actions">
Maintenance actions are <b> destructive </b> and should be used with caution. Performing any of these actions is
<b> irreversible </b>.
</BaseCardSectionTitle>
<v-card class="ma-2" :loading="state.actionLoading">
<template v-for="(action, idx) in actions">
<v-list-item :key="`item-${idx}`">
<v-list-item :key="`item-${idx}`" class="py-1">
<v-list-item-title>
<div>{{ action.name }}</div>
<v-list-item-subtitle>
<v-list-item-subtitle class="wrap-word">
{{ action.subtitle }}
</v-list-item-subtitle>
</v-list-item-title>
<v-list-item-action>
<BaseButton color="info" @click="action.handler">
<template #icon> {{ $globals.icons.robot }}</template>
Run
</BaseButton>
</v-list-item-action>
<BaseButton color="info" @click="action.handler">
<template #icon> {{ $globals.icons.robot }}</template>
Run
</BaseButton>
</v-list-item>
<v-divider :key="`divider-${idx}`" class="mx-2"></v-divider>
</template>
@ -57,18 +75,23 @@
<script lang="ts">
import { computed, ref, defineComponent, reactive } from "@nuxtjs/composition-api";
import { useAdminApi } from "~/composables/api";
import { MaintenanceSummary } from "~/types/api-types/admin";
import { MaintenanceStorageDetails, MaintenanceSummary } from "~/types/api-types/admin";
export default defineComponent({
layout: "admin",
setup() {
const state = reactive({
storageDetails: false,
storageDetailsLoading: false,
fetchingInfo: false,
actionLoading: false,
});
const adminApi = useAdminApi();
// ==========================================================================
// General Info
const infoResults = ref<MaintenanceSummary>({
dataDirSize: "unknown",
logFileSize: "unknown",
@ -111,6 +134,39 @@ export default defineComponent({
];
});
// ==========================================================================
// Storage Details
const storageTitles: { [key: string]: string } = {
tempDirSize: "Temporary Directory (.temp)",
backupsDirSize: "Backups Directory (backups)",
groupsDirSize: "Groups Directory (groups)",
recipesDirSize: "Recipes Directory (recipes)",
userDirSize: "User Directory (user)",
};
function storageDetailsText(key: string) {
return storageTitles[key] ?? "unknown";
}
const storageDetails = ref<MaintenanceStorageDetails | null>(null);
async function openDetails() {
state.storageDetailsLoading = true;
state.storageDetails = true;
const { data } = await adminApi.maintenance.getStorageDetails();
if (data) {
storageDetails.value = data;
}
state.storageDetailsLoading = true;
}
// ==========================================================================
// Actions
async function handleDeleteLogFile() {
state.actionLoading = true;
await adminApi.maintenance.cleanLogFile();
@ -129,6 +185,12 @@ export default defineComponent({
state.actionLoading = false;
}
async function handleCleanTemp() {
state.actionLoading = true;
await adminApi.maintenance.cleanTemp();
state.actionLoading = false;
}
const actions = [
{
name: "Delete Log Files",
@ -140,6 +202,11 @@ export default defineComponent({
handler: handleCleanDirectories,
subtitle: "Removes all the recipe folders that are not valid UUIDs",
},
{
name: "Clean Temporary Files",
handler: handleCleanTemp,
subtitle: "Removes all files and folders in the .temp directory",
},
{
name: "Clean Images",
handler: handleCleanImages,
@ -148,6 +215,9 @@ export default defineComponent({
];
return {
storageDetailsText,
openDetails,
storageDetails,
state,
info,
getSummary,
@ -162,4 +232,9 @@ export default defineComponent({
});
</script>
<style scoped></style>
<style scoped>
.wrap-word {
white-space: normal;
word-wrap: break-word;
}
</style>

View file

@ -0,0 +1,100 @@
<template>
<v-container fluid>
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.cog" title="Summary"> </BaseCardSectionTitle>
<div class="mb-6 ml-2 d-flex" style="gap: 0.8rem">
<BaseButton color="info" :loading="state.loading" @click="refreshLogs">
<template #icon> {{ $globals.icons.refreshCircle }} </template>
Refresh Logs
</BaseButton>
<AppButtonCopy :copy-text="copyText" />
<div class="ml-auto" style="max-width: 150px">
<v-text-field v-model="state.lines" type="number" label="Tail Lines" hide-details dense outlined>
</v-text-field>
</div>
</div>
<v-card outlined>
<v-virtual-scroll
v-scroll="scrollOptions"
:bench="20"
:items="logs.logs"
height="800"
item-height="20"
class="keep-whitespace log-container"
>
<template #default="{ item }">
<p class="log-text">
{{ item }}
</p>
</template>
</v-virtual-scroll>
</v-card>
</v-container>
</template>
<script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api";
import { computed, onMounted, reactive } from "vue-demi";
import { useAdminApi } from "~/composables/api";
export default defineComponent({
layout: "admin",
setup() {
const adminApi = useAdminApi();
const state = reactive({
loading: false,
lines: 500,
autoRefresh: true,
});
const scrollOptions = reactive({
enable: true,
always: false,
smooth: false,
notSmoothOnInit: true,
});
const logs = ref({
logs: [] as string[],
});
async function refreshLogs() {
state.loading = true;
const { data } = await adminApi.maintenance.logs(state.lines);
if (data) {
logs.value = data;
}
state.loading = false;
}
onMounted(() => {
refreshLogs();
});
const copyText = computed(() => {
return logs.value.logs.join("") || "";
});
return {
copyText,
scrollOptions,
state,
refreshLogs,
logs,
};
},
head: {
title: "Mealie Logs",
},
});
</script>
<style>
.log-text {
font: 0.8rem Inconsolata, monospace;
}
.log-container {
background-color: var(--v-background-base) !important;
}
.keep-whitespace {
white-space: pre;
}
</style>

View file

@ -1,29 +0,0 @@
<template>
<v-container fluid>
<BaseCardSectionTitle title="Data Migrations">
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Alias error provident, eveniet, laboriosam assumenda
earum amet quaerat vel consequatur molestias sed enim. Adipisci a consequuntur dolor culpa expedita voluptatem
praesentium optio iste atque, ea reiciendis iure non aut suscipit modi ducimus ratione, quam numquam quaerat
distinctio illum nemo. Dicta, doloremque!
</BaseCardSectionTitle>
</v-container>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
layout: "admin",
setup() {
return {};
},
head() {
return {
title: this.$t("settings.migrations") as string,
};
},
});
</script>
<style scoped>
</style>

View file

@ -182,7 +182,7 @@
event: 'randomDinner',
},
{
icon: $globals.icons.bolwMixOutline,
icon: $globals.icons.bowlMixOutline,
text: 'Random Side',
event: 'randomSide',
},

View file

@ -190,6 +190,16 @@ export interface ImportJob {
force?: boolean;
rebase?: boolean;
}
export interface MaintenanceLogs {
logs: string[];
}
export interface MaintenanceStorageDetails {
tempDirSize: string;
backupsDirSize: string;
groupsDirSize: string;
recipesDirSize: string;
userDirSize: string;
}
export interface MaintenanceSummary {
dataDirSize: string;
logFileSize: string;

View file

@ -0,0 +1,20 @@
/* tslint:disable */
/* eslint-disable */
/**
/* This file was automatically generated from pydantic models by running pydantic2ts.
/* Do not modify it by hand - just update the pydantic models and then re-run the script
*/
export interface MealieAnalytics {
installationId: string;
version: string;
databaseType: string;
usingEmail: boolean;
usingLdap: boolean;
apiTokens: number;
users: number;
groups: number;
recipes: number;
shoppingLists: number;
cookbooks: number;
}

View file

@ -3,7 +3,9 @@ export interface Icon {
primary: string;
// General
bolwMixOutline: string;
chart: string;
wrench: string;
bowlMixOutline: string;
foods: string;
units: string;
alert: string;

View file

@ -105,14 +105,19 @@ import {
mdiArrowRightBold,
mdiChevronRight,
mdiBowlMixOutline,
mdiWrench,
mdiChartLine,
} from "@mdi/js";
export const icons = {
// Primary
primary: mdiSilverwareVariant,
wrench: mdiWrench,
chart: mdiChartLine,
// General
bolwMixOutline: mdiBowlMixOutline,
bowlMixOutline: mdiBowlMixOutline,
foods: mdiFoodApple,
units: mdiBeakerOutline,
alert: mdiAlert,

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,
)