mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-30 17:53:31 -04:00 
			
		
		
		
	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:
		| @@ -76,7 +76,7 @@ def generate_typescript_types() -> None: | |||||||
|     schema_path = PROJECT_DIR / "mealie" / "schema" |     schema_path = PROJECT_DIR / "mealie" / "schema" | ||||||
|     types_dir = PROJECT_DIR / "frontend" / "types" / "api-types" |     types_dir = PROJECT_DIR / "frontend" / "types" / "api-types" | ||||||
|  |  | ||||||
|     ignore_dirs = ["__pycache__", "static"] |     ignore_dirs = ["__pycache__", "static", "_mealie"] | ||||||
|  |  | ||||||
|     skipped_files: list[Path] = [] |     skipped_files: list[Path] = [] | ||||||
|     skipped_dirs: list[Path] = [] |     skipped_dirs: list[Path] = [] | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import { AdminUsersApi } from "./admin/admin-users"; | |||||||
| import { AdminGroupsApi } from "./admin/admin-groups"; | import { AdminGroupsApi } from "./admin/admin-groups"; | ||||||
| import { AdminBackupsApi } from "./admin/admin-backups"; | import { AdminBackupsApi } from "./admin/admin-backups"; | ||||||
| import { AdminMaintenanceApi } from "./admin/admin-maintenance"; | import { AdminMaintenanceApi } from "./admin/admin-maintenance"; | ||||||
|  | import { AdminAnalyticsApi } from "./admin/admin-analytics"; | ||||||
| import { ApiRequestInstance } from "~/types/api"; | import { ApiRequestInstance } from "~/types/api"; | ||||||
|  |  | ||||||
| export class AdminAPI { | export class AdminAPI { | ||||||
| @@ -13,6 +14,7 @@ export class AdminAPI { | |||||||
|   public groups: AdminGroupsApi; |   public groups: AdminGroupsApi; | ||||||
|   public backups: AdminBackupsApi; |   public backups: AdminBackupsApi; | ||||||
|   public maintenance: AdminMaintenanceApi; |   public maintenance: AdminMaintenanceApi; | ||||||
|  |   public analytics: AdminAnalyticsApi; | ||||||
|  |  | ||||||
|   constructor(requests: ApiRequestInstance) { |   constructor(requests: ApiRequestInstance) { | ||||||
|     this.about = new AdminAboutAPI(requests); |     this.about = new AdminAboutAPI(requests); | ||||||
| @@ -21,6 +23,7 @@ export class AdminAPI { | |||||||
|     this.groups = new AdminGroupsApi(requests); |     this.groups = new AdminGroupsApi(requests); | ||||||
|     this.backups = new AdminBackupsApi(requests); |     this.backups = new AdminBackupsApi(requests); | ||||||
|     this.maintenance = new AdminMaintenanceApi(requests); |     this.maintenance = new AdminMaintenanceApi(requests); | ||||||
|  |     this.analytics = new AdminAnalyticsApi(requests); | ||||||
|  |  | ||||||
|     Object.freeze(this); |     Object.freeze(this); | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								frontend/api/admin/admin-analytics.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/api/admin/admin-analytics.ts
									
									
									
									
									
										Normal 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); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,11 +1,14 @@ | |||||||
| import { BaseAPI } from "../_base"; | import { BaseAPI } from "../_base"; | ||||||
| import { SuccessResponse } from "~/types/api-types/response"; | 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 prefix = "/api"; | ||||||
|  |  | ||||||
| const routes = { | const routes = { | ||||||
|   base: `${prefix}/admin/maintenance`, |   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`, |   cleanImages: `${prefix}/admin/maintenance/clean/images`, | ||||||
|   cleanRecipeFolders: `${prefix}/admin/maintenance/clean/recipe-folders`, |   cleanRecipeFolders: `${prefix}/admin/maintenance/clean/recipe-folders`, | ||||||
|   cleanLogFile: `${prefix}/admin/maintenance/clean/logs`, |   cleanLogFile: `${prefix}/admin/maintenance/clean/logs`, | ||||||
| @@ -16,6 +19,14 @@ export class AdminMaintenanceApi extends BaseAPI { | |||||||
|     return this.requests.get<MaintenanceSummary>(routes.base); |     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() { |   async cleanImages() { | ||||||
|     return await this.requests.post<SuccessResponse>(routes.cleanImages, {}); |     return await this.requests.post<SuccessResponse>(routes.cleanImages, {}); | ||||||
|   } |   } | ||||||
| @@ -27,4 +38,8 @@ export class AdminMaintenanceApi extends BaseAPI { | |||||||
|   async cleanLogFile() { |   async cleanLogFile() { | ||||||
|     return await this.requests.post<SuccessResponse>(routes.cleanLogFile, {}); |     return await this.requests.post<SuccessResponse>(routes.cleanLogFile, {}); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   async logs(lines: number) { | ||||||
|  |     return await this.requests.get<MaintenanceLogs>(routes.logs(lines)); | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -44,21 +44,21 @@ export default defineComponent({ | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     const topLinks: SidebarLinks = [ |     const topLinks: SidebarLinks = [ | ||||||
|       // { |  | ||||||
|       //   icon: $globals.icons.viewDashboard, |  | ||||||
|       //   to: "/admin/dashboard", |  | ||||||
|       //   title: i18n.t("sidebar.dashboard"), |  | ||||||
|       // }, |  | ||||||
|       { |       { | ||||||
|         icon: $globals.icons.cog, |         icon: $globals.icons.cog, | ||||||
|         to: "/admin/site-settings", |         to: "/admin/site-settings", | ||||||
|         title: i18n.t("sidebar.site-settings"), |         title: i18n.t("sidebar.site-settings"), | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         icon: $globals.icons.cog, |         icon: $globals.icons.wrench, | ||||||
|         to: "/admin/maintenance", |         to: "/admin/maintenance", | ||||||
|         title: "Maintenance", |         title: "Maintenance", | ||||||
|       }, |       }, | ||||||
|  |       // { | ||||||
|  |       //   icon: $globals.icons.chart, | ||||||
|  |       //   to: "/admin/analytics", | ||||||
|  |       //   title: "Analytics", | ||||||
|  |       // }, | ||||||
|       { |       { | ||||||
|         icon: $globals.icons.user, |         icon: $globals.icons.user, | ||||||
|         to: "/admin/manage/users", |         to: "/admin/manage/users", | ||||||
|   | |||||||
							
								
								
									
										137
									
								
								frontend/pages/admin/analytics.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								frontend/pages/admin/analytics.vue
									
									
									
									
									
										Normal 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> | ||||||
| @@ -1,51 +1,69 @@ | |||||||
| <template> | <template> | ||||||
|   <v-container fluid class="narrow-container"> |   <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> |     <BasePageTitle divider> | ||||||
|       <template #title> Site Maintenance </template> |       <template #title> Site Maintenance </template> | ||||||
|     </BasePageTitle> |     </BasePageTitle> | ||||||
| 
 | 
 | ||||||
|     <BannerExperimental /> |     <div class="d-flex justify-end"> | ||||||
|  |       <ButtonLink to="/admin/maintenance/logs" text="Logs" :icon="$globals.icons.file" /> | ||||||
|  |     </div> | ||||||
| 
 | 
 | ||||||
|     <section> |     <section> | ||||||
|       <BaseCardSectionTitle class="pb-0" :icon="$globals.icons.cog" title="Summary"> </BaseCardSectionTitle> |       <BaseCardSectionTitle class="pb-0" :icon="$globals.icons.wrench" title="Summary"> </BaseCardSectionTitle> | ||||||
|       <div class="mb-6 ml-2"> |       <div class="mb-6 ml-2 d-flex" style="gap: 0.3rem"> | ||||||
|         <BaseButton color="info" @click="getSummary"> |         <BaseButton color="info" @click="getSummary"> | ||||||
|           <template #icon> {{ $globals.icons.tools }} </template> |           <template #icon> {{ $globals.icons.tools }} </template> | ||||||
|           Get Summary |           Get Summary | ||||||
|         </BaseButton> |         </BaseButton> | ||||||
|  |         <BaseButton color="info" @click="openDetails"> | ||||||
|  |           <template #icon> {{ $globals.icons.folderOutline }} </template> | ||||||
|  |           Details | ||||||
|  |         </BaseButton> | ||||||
|       </div> |       </div> | ||||||
|       <v-card class="ma-2" :loading="state.fetchingInfo"> |       <v-card class="ma-2" :loading="state.fetchingInfo"> | ||||||
|         <template v-for="(value, idx) in info"> |         <template v-for="(value, idx) in info"> | ||||||
|           <v-list-item :key="`item-${idx}`"> |           <v-list-item :key="`item-${idx}`"> | ||||||
|             <v-list-item-title> |             <v-list-item-title class="py-2"> | ||||||
|               <div>{{ value.name }}</div> |               <div>{{ value.name }}</div> | ||||||
|             </v-list-item-title> |  | ||||||
|               <v-list-item-subtitle class="text-end"> {{ value.value }} </v-list-item-subtitle> |               <v-list-item-subtitle class="text-end"> {{ value.value }} </v-list-item-subtitle> | ||||||
|  |             </v-list-item-title> | ||||||
|           </v-list-item> |           </v-list-item> | ||||||
|           <v-divider :key="`divider-${idx}`" class="mx-2"></v-divider> |           <v-divider :key="`divider-${idx}`" class="mx-2"></v-divider> | ||||||
|         </template> |         </template> | ||||||
|       </v-card> |       </v-card> | ||||||
|     </section> |     </section> | ||||||
|     <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 |         Maintenance actions are <b> destructive </b> and should be used with caution. Performing any of these actions is | ||||||
|         <b> irreversible </b>. |         <b> irreversible </b>. | ||||||
|       </BaseCardSectionTitle> |       </BaseCardSectionTitle> | ||||||
|       <v-card class="ma-2" :loading="state.actionLoading"> |       <v-card class="ma-2" :loading="state.actionLoading"> | ||||||
|         <template v-for="(action, idx) in actions"> |         <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> |             <v-list-item-title> | ||||||
|               <div>{{ action.name }}</div> |               <div>{{ action.name }}</div> | ||||||
|               <v-list-item-subtitle> |               <v-list-item-subtitle class="wrap-word"> | ||||||
|                 {{ action.subtitle }} |                 {{ action.subtitle }} | ||||||
|               </v-list-item-subtitle> |               </v-list-item-subtitle> | ||||||
|             </v-list-item-title> |             </v-list-item-title> | ||||||
|             <v-list-item-action> |  | ||||||
|             <BaseButton color="info" @click="action.handler"> |             <BaseButton color="info" @click="action.handler"> | ||||||
|               <template #icon> {{ $globals.icons.robot }}</template> |               <template #icon> {{ $globals.icons.robot }}</template> | ||||||
|               Run |               Run | ||||||
|             </BaseButton> |             </BaseButton> | ||||||
|             </v-list-item-action> |  | ||||||
|           </v-list-item> |           </v-list-item> | ||||||
|           <v-divider :key="`divider-${idx}`" class="mx-2"></v-divider> |           <v-divider :key="`divider-${idx}`" class="mx-2"></v-divider> | ||||||
|         </template> |         </template> | ||||||
| @@ -57,18 +75,23 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { computed, ref, defineComponent, reactive } from "@nuxtjs/composition-api"; | import { computed, ref, defineComponent, reactive } from "@nuxtjs/composition-api"; | ||||||
| import { useAdminApi } from "~/composables/api"; | import { useAdminApi } from "~/composables/api"; | ||||||
| import { MaintenanceSummary } from "~/types/api-types/admin"; | import { MaintenanceStorageDetails, MaintenanceSummary } from "~/types/api-types/admin"; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|   layout: "admin", |   layout: "admin", | ||||||
|   setup() { |   setup() { | ||||||
|     const state = reactive({ |     const state = reactive({ | ||||||
|  |       storageDetails: false, | ||||||
|  |       storageDetailsLoading: false, | ||||||
|       fetchingInfo: false, |       fetchingInfo: false, | ||||||
|       actionLoading: false, |       actionLoading: false, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const adminApi = useAdminApi(); |     const adminApi = useAdminApi(); | ||||||
| 
 | 
 | ||||||
|  |     // ========================================================================== | ||||||
|  |     // General Info | ||||||
|  | 
 | ||||||
|     const infoResults = ref<MaintenanceSummary>({ |     const infoResults = ref<MaintenanceSummary>({ | ||||||
|       dataDirSize: "unknown", |       dataDirSize: "unknown", | ||||||
|       logFileSize: "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() { |     async function handleDeleteLogFile() { | ||||||
|       state.actionLoading = true; |       state.actionLoading = true; | ||||||
|       await adminApi.maintenance.cleanLogFile(); |       await adminApi.maintenance.cleanLogFile(); | ||||||
| @@ -129,6 +185,12 @@ export default defineComponent({ | |||||||
|       state.actionLoading = false; |       state.actionLoading = false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     async function handleCleanTemp() { | ||||||
|  |       state.actionLoading = true; | ||||||
|  |       await adminApi.maintenance.cleanTemp(); | ||||||
|  |       state.actionLoading = false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     const actions = [ |     const actions = [ | ||||||
|       { |       { | ||||||
|         name: "Delete Log Files", |         name: "Delete Log Files", | ||||||
| @@ -140,6 +202,11 @@ export default defineComponent({ | |||||||
|         handler: handleCleanDirectories, |         handler: handleCleanDirectories, | ||||||
|         subtitle: "Removes all the recipe folders that are not valid UUIDs", |         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", |         name: "Clean Images", | ||||||
|         handler: handleCleanImages, |         handler: handleCleanImages, | ||||||
| @@ -148,6 +215,9 @@ export default defineComponent({ | |||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|  |       storageDetailsText, | ||||||
|  |       openDetails, | ||||||
|  |       storageDetails, | ||||||
|       state, |       state, | ||||||
|       info, |       info, | ||||||
|       getSummary, |       getSummary, | ||||||
| @@ -162,4 +232,9 @@ export default defineComponent({ | |||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style scoped></style> | <style scoped> | ||||||
|  | .wrap-word { | ||||||
|  |   white-space: normal; | ||||||
|  |   word-wrap: break-word; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										100
									
								
								frontend/pages/admin/maintenance/logs.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								frontend/pages/admin/maintenance/logs.vue
									
									
									
									
									
										Normal 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> | ||||||
| @@ -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> |  | ||||||
| @@ -182,7 +182,7 @@ | |||||||
|                   event: 'randomDinner', |                   event: 'randomDinner', | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                   icon: $globals.icons.bolwMixOutline, |                   icon: $globals.icons.bowlMixOutline, | ||||||
|                   text: 'Random Side', |                   text: 'Random Side', | ||||||
|                   event: 'randomSide', |                   event: 'randomSide', | ||||||
|                 }, |                 }, | ||||||
|   | |||||||
| @@ -190,6 +190,16 @@ export interface ImportJob { | |||||||
|   force?: boolean; |   force?: boolean; | ||||||
|   rebase?: boolean; |   rebase?: boolean; | ||||||
| } | } | ||||||
|  | export interface MaintenanceLogs { | ||||||
|  |   logs: string[]; | ||||||
|  | } | ||||||
|  | export interface MaintenanceStorageDetails { | ||||||
|  |   tempDirSize: string; | ||||||
|  |   backupsDirSize: string; | ||||||
|  |   groupsDirSize: string; | ||||||
|  |   recipesDirSize: string; | ||||||
|  |   userDirSize: string; | ||||||
|  | } | ||||||
| export interface MaintenanceSummary { | export interface MaintenanceSummary { | ||||||
|   dataDirSize: string; |   dataDirSize: string; | ||||||
|   logFileSize: string; |   logFileSize: string; | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								frontend/types/api-types/analytics.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								frontend/types/api-types/analytics.ts
									
									
									
									
									
										Normal 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; | ||||||
|  | } | ||||||
| @@ -3,7 +3,9 @@ export interface Icon { | |||||||
|   primary: string; |   primary: string; | ||||||
|  |  | ||||||
|   // General |   // General | ||||||
|   bolwMixOutline: string; |   chart: string; | ||||||
|  |   wrench: string; | ||||||
|  |   bowlMixOutline: string; | ||||||
|   foods: string; |   foods: string; | ||||||
|   units: string; |   units: string; | ||||||
|   alert: string; |   alert: string; | ||||||
|   | |||||||
| @@ -105,14 +105,19 @@ import { | |||||||
|   mdiArrowRightBold, |   mdiArrowRightBold, | ||||||
|   mdiChevronRight, |   mdiChevronRight, | ||||||
|   mdiBowlMixOutline, |   mdiBowlMixOutline, | ||||||
|  |   mdiWrench, | ||||||
|  |   mdiChartLine, | ||||||
| } from "@mdi/js"; | } from "@mdi/js"; | ||||||
|  |  | ||||||
| export const icons = { | export const icons = { | ||||||
|   // Primary |   // Primary | ||||||
|   primary: mdiSilverwareVariant, |   primary: mdiSilverwareVariant, | ||||||
|  |  | ||||||
|  |   wrench: mdiWrench, | ||||||
|  |   chart: mdiChartLine, | ||||||
|  |  | ||||||
|   // General |   // General | ||||||
|   bolwMixOutline: mdiBowlMixOutline, |   bowlMixOutline: mdiBowlMixOutline, | ||||||
|   foods: mdiFoodApple, |   foods: mdiFoodApple, | ||||||
|   units: mdiBeakerOutline, |   units: mdiBeakerOutline, | ||||||
|   alert: mdiAlert, |   alert: mdiAlert, | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ from mealie.routes._base.routers import AdminAPIRouter | |||||||
|  |  | ||||||
| from . import ( | from . import ( | ||||||
|     admin_about, |     admin_about, | ||||||
|  |     admin_analytics, | ||||||
|     admin_backups, |     admin_backups, | ||||||
|     admin_email, |     admin_email, | ||||||
|     admin_log, |     admin_log, | ||||||
| @@ -15,9 +16,10 @@ router = AdminAPIRouter(prefix="/admin") | |||||||
|  |  | ||||||
| router.include_router(admin_about.router, tags=["Admin: About"]) | router.include_router(admin_about.router, tags=["Admin: About"]) | ||||||
| router.include_router(admin_log.router, tags=["Admin: Log"]) | router.include_router(admin_log.router, tags=["Admin: Log"]) | ||||||
| router.include_router(admin_management_users.router) | router.include_router(admin_management_users.router, tags=["Admin: Manage Users"]) | ||||||
| router.include_router(admin_management_groups.router) | router.include_router(admin_management_groups.router, tags=["Admin: Manage Groups"]) | ||||||
| router.include_router(admin_email.router, tags=["Admin: Email"]) | router.include_router(admin_email.router, tags=["Admin: Email"]) | ||||||
| router.include_router(admin_server_tasks.router, tags=["Admin: Server Tasks"]) | router.include_router(admin_server_tasks.router, tags=["Admin: Server Tasks"]) | ||||||
| router.include_router(admin_backups.router, tags=["Admin: Backups"]) | router.include_router(admin_backups.router, tags=["Admin: Backups"]) | ||||||
| router.include_router(admin_maintenance.router, tags=["Admin: Maintenance"]) | router.include_router(admin_maintenance.router, tags=["Admin: Maintenance"]) | ||||||
|  | router.include_router(admin_analytics.router, tags=["Admin: Analytics"]) | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								mealie/routes/admin/admin_analytics.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								mealie/routes/admin/admin_analytics.py
									
									
									
									
									
										Normal 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() | ||||||
| @@ -10,6 +10,7 @@ from mealie.core.root_logger import LOGGER_FILE | |||||||
| from mealie.pkgs.stats import fs_stats | from mealie.pkgs.stats import fs_stats | ||||||
| from mealie.routes._base import BaseAdminController, controller | from mealie.routes._base import BaseAdminController, controller | ||||||
| from mealie.schema.admin import MaintenanceSummary | from mealie.schema.admin import MaintenanceSummary | ||||||
|  | from mealie.schema.admin.maintenance import MaintenanceLogs, MaintenanceStorageDetails | ||||||
| from mealie.schema.response import ErrorResponse, SuccessResponse | from mealie.schema.response import ErrorResponse, SuccessResponse | ||||||
|  |  | ||||||
| router = APIRouter(prefix="/maintenance") | router = APIRouter(prefix="/maintenance") | ||||||
| @@ -54,6 +55,16 @@ def clean_recipe_folders(root_dir: Path, dry_run: bool) -> int: | |||||||
|     return cleaned_dirs |     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) | @controller(router) | ||||||
| class AdminMaintenanceController(BaseAdminController): | class AdminMaintenanceController(BaseAdminController): | ||||||
|     @router.get("", response_model=MaintenanceSummary) |     @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), |             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) |     @router.post("/clean/images", response_model=SuccessResponse) | ||||||
|     def clean_images(self): |     def clean_images(self): | ||||||
|         """ |         """ | ||||||
| @@ -83,6 +109,16 @@ class AdminMaintenanceController(BaseAdminController): | |||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             raise HTTPException(status_code=500, detail=ErrorResponse.respond("Failed to clean images")) from 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) |     @router.post("/clean/recipe-folders", response_model=SuccessResponse) | ||||||
|     def clean_recipe_folders(self): |     def clean_recipe_folders(self): | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -1,2 +1,3 @@ | |||||||
|  | # GENERATED CODE - DO NOT MODIFY BY HAND | ||||||
| from .mealie_model import * | from .mealie_model import * | ||||||
| from .types import * | from .types import * | ||||||
|   | |||||||
| @@ -6,3 +6,15 @@ class MaintenanceSummary(MealieModel): | |||||||
|     log_file_size: str |     log_file_size: str | ||||||
|     cleanable_images: int |     cleanable_images: int | ||||||
|     cleanable_dirs: 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] | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								mealie/schema/analytics/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								mealie/schema/analytics/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | # GENERATED CODE - DO NOT MODIFY BY HAND | ||||||
|  | from .analytics import * | ||||||
							
								
								
									
										19
									
								
								mealie/schema/analytics/analytics.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								mealie/schema/analytics/analytics.py
									
									
									
									
									
										Normal 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 | ||||||
| @@ -11,5 +11,5 @@ from .recipe_nutrition import * | |||||||
| from .recipe_settings import * | from .recipe_settings import * | ||||||
| from .recipe_share_token import *  # type: ignore | from .recipe_share_token import *  # type: ignore | ||||||
| from .recipe_step import * | from .recipe_step import * | ||||||
| from .recipe_tool import * | from .recipe_tool import *  # type: ignore | ||||||
| from .request_helpers import * | from .request_helpers import * | ||||||
|   | |||||||
							
								
								
									
										33
									
								
								mealie/services/analytics/service_analytics.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								mealie/services/analytics/service_analytics.py
									
									
									
									
									
										Normal 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, | ||||||
|  |         ) | ||||||
		Reference in New Issue
	
	Block a user