django-cfg 1.4.83__py3-none-any.whl → 1.4.85__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/dashboard/__init__.py +8 -0
- django_cfg/apps/dashboard/apps.py +23 -0
- django_cfg/apps/dashboard/serializers/__init__.py +55 -0
- django_cfg/apps/dashboard/serializers/activity.py +38 -0
- django_cfg/apps/dashboard/serializers/apizones.py +26 -0
- django_cfg/apps/dashboard/serializers/base.py +16 -0
- django_cfg/apps/dashboard/serializers/charts.py +44 -0
- django_cfg/apps/dashboard/serializers/commands.py +26 -0
- django_cfg/apps/dashboard/serializers/overview.py +34 -0
- django_cfg/apps/dashboard/serializers/statistics.py +46 -0
- django_cfg/apps/dashboard/serializers/system.py +58 -0
- django_cfg/apps/dashboard/services/__init__.py +20 -0
- django_cfg/apps/dashboard/services/apizones_service.py +119 -0
- django_cfg/apps/dashboard/services/charts_service.py +266 -0
- django_cfg/apps/dashboard/services/commands_service.py +142 -0
- django_cfg/apps/dashboard/services/statistics_service.py +393 -0
- django_cfg/apps/dashboard/services/system_health_service.py +280 -0
- django_cfg/apps/dashboard/urls.py +42 -0
- django_cfg/apps/dashboard/views/__init__.py +23 -0
- django_cfg/apps/dashboard/views/activity_views.py +83 -0
- django_cfg/apps/dashboard/views/apizones_views.py +73 -0
- django_cfg/apps/dashboard/views/charts_views.py +159 -0
- django_cfg/apps/dashboard/views/commands_views.py +73 -0
- django_cfg/apps/dashboard/views/overview_views.py +92 -0
- django_cfg/apps/dashboard/views/statistics_views.py +105 -0
- django_cfg/apps/dashboard/views/system_views.py +73 -0
- django_cfg/apps/frontend/views.py +5 -0
- django_cfg/apps/urls.py +2 -1
- django_cfg/core/builders/apps_builder.py +1 -0
- django_cfg/modules/django_client/core/parser/openapi30.py +12 -5
- django_cfg/modules/django_client/core/parser/openapi31.py +12 -5
- django_cfg/modules/django_unfold/callbacks/main.py +7 -6
- django_cfg/modules/django_unfold/dashboard.py +1 -36
- django_cfg/modules/django_unfold/models/config.py +102 -73
- django_cfg/modules/django_unfold/tailwind.py +31 -79
- django_cfg/pyproject.toml +1 -1
- django_cfg/static/frontend/admin/404.html +1 -1
- django_cfg/static/frontend/admin/500.html +1 -1
- django_cfg/static/frontend/admin/_next/static/BembwiEtlu4eFl3OX7n1k/_buildManifest.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/23004-faae121bbfecc163.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{25033.d626f78bc99bc4a1.js → 25033.ee3e206d5a2877b6.js} +2 -2
- django_cfg/static/frontend/admin/_next/static/chunks/{25892.964150a58f94ce06.js → 25892.5cbed319f9226fdc.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{2d7a934f.dfef67639279d59d.js → 2d7a934f.329c61f23af1a7ec.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{30649.00c679812a56aee3.js → 30649.963cfb7268b5864a.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{30875.784491146c38dbcb.js → 30875.82c3741757b8aa32.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{32163.ab0ca435b3f26c04.js → 32163.109a03a7252f1508.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{49978.fb8ba7ee52ffe666.js → 49978.db5a86a8eb233f35.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/50314-3b9d15242191c8bc.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{50319.f786248384877960.js → 50319.fd78c7f7e3f1966e.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{52908.b690e323d8f8efdd.js → 52908.da5b850b0bc0970c.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{53710.80ca863525d137db.js → 53710.7176bbee6c7b78be.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{57982.251fed8d58adcf53.js → 57982.2c90b33b0934522a.js} +2 -2
- django_cfg/static/frontend/admin/_next/static/chunks/{60181.86e18057c4caaa97.js → 60181.c94d78d10eb5da37.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{60374.bde0ec1249aa79c6.js → 60374.5d80cfc45439b2b0.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{64330.2ef79bccd7d4e363.js → 64330.41858e98c0e5173b.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/6766.8d01e44e83070e83.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{6884.7b1db804c88280ed.js → 6884.624d563508cf6db4.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{69436.9515b854cdf4b57a.js → 69436.be44021e3d7c99c7.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{70628.00cdd98f672e684f.js → 70628.58e8c38a66543d5e.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{73218.a826c2248612b37f.js → 73218.d712e7bd678e23a8.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{76334.64fbaa923d9ac293.js → 76334.f43f2d8b4bbf8dd6.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{7799.2b280f8ddf067d49.js → 7799.1575cc212bc750c7.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{80574.620a8a5b4eb91c25.js → 80574.92638dd7b9979664.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{81127.a0603c3394892d4e.js → 81127.3ead500eec887152.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{8383.eb6188b22c453e14.js → 8383.e25a442df26b2e26.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{85833.35e6ca25ac32a7d2.js → 85833.b0dead4fbcbfdd1b.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{95365.fc9d7653a78839d0.js → 95365.2b430045fc2e5acf.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{96168.eb7fdb721b9cdb00.js → 96168.b7197f890097df6e.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{96424.0793b94836eb13a6.js → 96424.11d76570e9a94b85.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/{404-c283223d1afd02a2.js → 404-cf71cd7b3cb005e5.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/{500-389d6d3e1f2f7fda.js → 500-ff19c7842e3df415.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/_app-f62e5528fbcbb6b3.js +272 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/{_error-5291033275c26d09.js → _error-87f3fdc2aa131e77.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/index-69f737d4802cc5b7.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{cookies-bb5507a122775f30.js → cookies-b39c7f22c066e2c6.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{privacy-f8a3d8db1a197be3.js → privacy-5aedad0cf3a4f80f.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{security-aba50addd2179f8f.js → security-dbd854d0d5d483e2.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{terms-4aa35cd30b5c08ad.js → terms-f3e1d2b9e5edf12f.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/centrifugo-f24beb6ed3955aa8.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/{profile-e93a65e8e7d9022b.js → profile-b8045f993287f1a7.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/ui-373fff8b42878e64.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private-fe9faa86ecdb0ce6.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{webpack-905bba30877f6490.js → webpack-7c456a65e96eb97e.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/css/5f9a37b6e6a72303.css +3 -0
- django_cfg/static/frontend/admin/auth.html +1 -1
- django_cfg/static/frontend/admin/index.html +1 -1
- django_cfg/static/frontend/admin/legal/cookies.html +1 -1
- django_cfg/static/frontend/admin/legal/privacy.html +1 -1
- django_cfg/static/frontend/admin/legal/security.html +1 -1
- django_cfg/static/frontend/admin/legal/terms.html +1 -1
- django_cfg/static/frontend/admin/private/centrifugo.html +1 -1
- django_cfg/static/frontend/admin/private/profile.html +1 -1
- django_cfg/static/frontend/admin/private/ui.html +1 -0
- django_cfg/static/frontend/admin/private.html +1 -1
- django_cfg/templates/admin/index.html +328 -62
- django_cfg/templates/admin/sections/commands_section.html +5 -549
- django_cfg/templates/admin/sections/documentation_section.html +5 -152
- django_cfg/templates/admin/sections/overview_section.html +5 -112
- django_cfg/templates/admin/sections/stats_section.html +5 -35
- django_cfg/templates/admin/sections/system_section.html +5 -99
- django_cfg/templates/admin/sections/widgets_section.html +10 -128
- django_cfg/templates/admin_old/index.html +80 -0
- django_cfg/templates/admin_old/sections/commands_section.html +549 -0
- django_cfg/templates/admin_old/sections/documentation_section.html +152 -0
- django_cfg/templates/admin_old/sections/overview_section.html +112 -0
- django_cfg/templates/admin_old/sections/stats_section.html +35 -0
- django_cfg/templates/admin_old/sections/system_section.html +99 -0
- django_cfg/templates/admin_old/sections/widgets_section.html +129 -0
- django_cfg/templates/unfold/layouts/skeleton.html +27 -0
- django_cfg/templatetags/django_cfg.py +53 -0
- {django_cfg-1.4.83.dist-info → django_cfg-1.4.85.dist-info}/METADATA +1 -1
- {django_cfg-1.4.83.dist-info → django_cfg-1.4.85.dist-info}/RECORD +160 -124
- django_cfg/static/frontend/admin/_next/static/chunks/6766.d62fed7cd4761148.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/_app-16701a4e1bc3e6ac.js +0 -272
- django_cfg/static/frontend/admin/_next/static/chunks/pages/index-88751d9f44a32105.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/centrifugo-1c5f00c26c77a47b.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private-2f58633ddf63a5bc.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/ui-0e6c0e35862789ec.js +0 -1
- django_cfg/static/frontend/admin/_next/static/css/806300fb98c42afb.css +0 -3
- django_cfg/static/frontend/admin/_next/static/ibMHm1p66p0UGKsKnDWxn/_buildManifest.js +0 -1
- django_cfg/static/frontend/admin/ui.html +0 -92
- /django_cfg/static/frontend/admin/_next/static/{ibMHm1p66p0UGKsKnDWxn → BembwiEtlu4eFl3OX7n1k}/_ssgManifest.js +0 -0
- /django_cfg/templates/{admin → admin_old}/components/action_grid.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/card.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/data_table.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/metric_card.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/modal.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/progress_bar.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/section_header.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/stat_item.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/stats_grid.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/status_badge.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/user_avatar.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/change_list.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/includes/default_value.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/includes/fieldset_header.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/includes/results_list.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/includes/setting_row.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/includes/table_headers.html +0 -0
- /django_cfg/templates/{admin → admin_old}/examples/component_class_example.html +0 -0
- /django_cfg/templates/{admin → admin_old}/import_export/change_list_export.html +0 -0
- /django_cfg/templates/{admin → admin_old}/import_export/change_list_import.html +0 -0
- /django_cfg/templates/{admin → admin_old}/import_export/change_list_import_export.html +0 -0
- /django_cfg/templates/{admin → admin_old}/index_new.html +0 -0
- /django_cfg/templates/{admin → admin_old}/layouts/base_dashboard.html +0 -0
- /django_cfg/templates/{admin → admin_old}/layouts/dashboard_with_tabs.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/activity_tracker.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/charts_section.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/django_commands.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/quick_actions.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/recent_activity_improved.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/recent_users_table.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/stats_cards.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/stats_tiles.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/system_health.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/system_metrics.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/user_permissions.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/app_stats_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/commands_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/documentation_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/overview_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/stats_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/users_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/widgets_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/zones/zones_table.html +0 -0
- {django_cfg-1.4.83.dist-info → django_cfg-1.4.85.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.83.dist-info → django_cfg-1.4.85.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.83.dist-info → django_cfg-1.4.85.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Statistics Service
|
|
3
|
+
|
|
4
|
+
Collects and aggregates statistics for dashboard display.
|
|
5
|
+
Uses Django ORM for real data collection.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from django.apps import apps
|
|
13
|
+
from django.contrib.auth import get_user_model
|
|
14
|
+
from django.utils import timezone
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StatisticsService:
|
|
20
|
+
"""
|
|
21
|
+
Service for collecting dashboard statistics.
|
|
22
|
+
|
|
23
|
+
%%PRIORITY:HIGH%%
|
|
24
|
+
%%AI_HINT: This service collects data from various Django models using ORM%%
|
|
25
|
+
|
|
26
|
+
TAGS: statistics, dashboard, service
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
"""Initialize statistics service."""
|
|
31
|
+
self.logger = logger
|
|
32
|
+
|
|
33
|
+
def _get_user_model(self):
|
|
34
|
+
"""Get the user model safely."""
|
|
35
|
+
return get_user_model()
|
|
36
|
+
|
|
37
|
+
def get_user_statistics(self) -> Dict[str, Any]:
|
|
38
|
+
"""
|
|
39
|
+
Get user-related statistics.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dictionary containing user stats:
|
|
43
|
+
- total_users: Total number of users
|
|
44
|
+
- active_users: Number of active users
|
|
45
|
+
- new_users: New users in last 7 days
|
|
46
|
+
- superusers: Number of superusers
|
|
47
|
+
|
|
48
|
+
%%AI_HINT: Uses real Django ORM queries from User model%%
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
User = self._get_user_model()
|
|
52
|
+
|
|
53
|
+
total_users = User.objects.count()
|
|
54
|
+
active_users = User.objects.filter(is_active=True).count()
|
|
55
|
+
new_users_7d = User.objects.filter(
|
|
56
|
+
date_joined__gte=timezone.now() - timedelta(days=7)
|
|
57
|
+
).count()
|
|
58
|
+
superusers = User.objects.filter(is_superuser=True).count()
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
'total_users': total_users,
|
|
62
|
+
'active_users': active_users,
|
|
63
|
+
'new_users': new_users_7d,
|
|
64
|
+
'superusers': superusers,
|
|
65
|
+
}
|
|
66
|
+
except Exception as e:
|
|
67
|
+
self.logger.error(f"Error getting user statistics: {e}")
|
|
68
|
+
return {
|
|
69
|
+
'total_users': 0,
|
|
70
|
+
'active_users': 0,
|
|
71
|
+
'new_users': 0,
|
|
72
|
+
'superusers': 0,
|
|
73
|
+
'error': str(e)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def get_app_statistics(self) -> Dict[str, Any]:
|
|
77
|
+
"""
|
|
78
|
+
Get application-specific statistics.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Dictionary with aggregated app statistics.
|
|
82
|
+
{
|
|
83
|
+
'apps': {app_label: {...stats...}},
|
|
84
|
+
'total_records': int,
|
|
85
|
+
'total_models': int,
|
|
86
|
+
'total_apps': int
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
%%AI_HINT: Real app introspection using Django apps registry%%
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
stats = {"apps": {}, "total_records": 0, "total_models": 0, "total_apps": 0}
|
|
93
|
+
|
|
94
|
+
# Get all installed apps
|
|
95
|
+
for app_config in apps.get_app_configs():
|
|
96
|
+
app_label = app_config.label
|
|
97
|
+
|
|
98
|
+
# Skip system apps
|
|
99
|
+
if app_label in ["admin", "contenttypes", "sessions", "auth"]:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
app_stats = self._get_app_stats(app_label)
|
|
103
|
+
if app_stats:
|
|
104
|
+
stats["apps"][app_label] = app_stats
|
|
105
|
+
stats["total_records"] += app_stats.get("total_records", 0)
|
|
106
|
+
stats["total_models"] += app_stats.get("model_count", 0)
|
|
107
|
+
stats["total_apps"] += 1
|
|
108
|
+
|
|
109
|
+
return stats
|
|
110
|
+
except Exception as e:
|
|
111
|
+
self.logger.error(f"Error getting app statistics: {e}")
|
|
112
|
+
return {'apps': {}, 'total_records': 0, 'total_models': 0, 'total_apps': 0, 'error': str(e)}
|
|
113
|
+
|
|
114
|
+
def _get_app_stats(self, app_label: str) -> Optional[Dict[str, Any]]:
|
|
115
|
+
"""Get statistics for a specific app."""
|
|
116
|
+
try:
|
|
117
|
+
app_config = apps.get_app_config(app_label)
|
|
118
|
+
# Convert generator to list to avoid len() error
|
|
119
|
+
models_list = list(app_config.get_models())
|
|
120
|
+
|
|
121
|
+
if not models_list:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
app_stats = {
|
|
125
|
+
"name": app_config.verbose_name or app_label.title(),
|
|
126
|
+
"models": {},
|
|
127
|
+
"total_records": 0,
|
|
128
|
+
"model_count": len(models_list),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for model in models_list:
|
|
132
|
+
try:
|
|
133
|
+
# Get model statistics
|
|
134
|
+
model_stats = self._get_model_stats(model)
|
|
135
|
+
if model_stats:
|
|
136
|
+
app_stats["models"][model._meta.model_name] = model_stats
|
|
137
|
+
app_stats["total_records"] += model_stats.get("count", 0)
|
|
138
|
+
except Exception:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
return app_stats
|
|
142
|
+
|
|
143
|
+
except Exception:
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
def _get_model_stats(self, model) -> Optional[Dict[str, Any]]:
|
|
147
|
+
"""Get statistics for a specific model."""
|
|
148
|
+
try:
|
|
149
|
+
# Get basic model info
|
|
150
|
+
model_stats = {
|
|
151
|
+
"name": model._meta.verbose_name_plural
|
|
152
|
+
or model._meta.verbose_name
|
|
153
|
+
or model._meta.model_name,
|
|
154
|
+
"count": model.objects.count(),
|
|
155
|
+
"fields_count": len(model._meta.fields),
|
|
156
|
+
"admin_url": f"admin:{model._meta.app_label}_{model._meta.model_name}_changelist",
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return model_stats
|
|
160
|
+
|
|
161
|
+
except Exception:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def get_stat_cards(self) -> List[Dict[str, Any]]:
|
|
165
|
+
"""
|
|
166
|
+
Get statistics cards for dashboard overview.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
List of stat card dictionaries ready for serialization.
|
|
170
|
+
Each card contains: title, value, icon, change, change_type
|
|
171
|
+
|
|
172
|
+
USED_BY: DashboardViewSet.overview endpoint
|
|
173
|
+
%%AI_HINT: Real data from User model with calculated changes%%
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
user_stats = self.get_user_statistics()
|
|
177
|
+
|
|
178
|
+
total_users = user_stats['total_users']
|
|
179
|
+
active_users = user_stats['active_users']
|
|
180
|
+
new_users_7d = user_stats['new_users']
|
|
181
|
+
superusers = user_stats['superusers']
|
|
182
|
+
|
|
183
|
+
cards = [
|
|
184
|
+
{
|
|
185
|
+
'title': 'Total Users',
|
|
186
|
+
'value': f"{total_users:,}",
|
|
187
|
+
'icon': 'people',
|
|
188
|
+
'change': f"+{new_users_7d}" if new_users_7d > 0 else None,
|
|
189
|
+
'change_type': 'positive' if new_users_7d > 0 else 'neutral',
|
|
190
|
+
'color': 'primary',
|
|
191
|
+
'description': 'Registered users',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
'title': 'Active Users',
|
|
195
|
+
'value': f"{active_users:,}",
|
|
196
|
+
'icon': 'person',
|
|
197
|
+
'change': (
|
|
198
|
+
f"{(active_users/total_users*100):.1f}%"
|
|
199
|
+
if total_users > 0
|
|
200
|
+
else "0%"
|
|
201
|
+
),
|
|
202
|
+
'change_type': (
|
|
203
|
+
'positive' if active_users > total_users * 0.7 else 'neutral'
|
|
204
|
+
),
|
|
205
|
+
'color': 'success',
|
|
206
|
+
'description': 'Currently active',
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
'title': 'New This Week',
|
|
210
|
+
'value': f"{new_users_7d:,}",
|
|
211
|
+
'icon': 'person_add',
|
|
212
|
+
'change_type': 'positive' if new_users_7d > 0 else 'neutral',
|
|
213
|
+
'color': 'info',
|
|
214
|
+
'description': 'Last 7 days',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
'title': 'Superusers',
|
|
218
|
+
'value': f"{superusers:,}",
|
|
219
|
+
'icon': 'admin_panel_settings',
|
|
220
|
+
'change': (
|
|
221
|
+
f"{(superusers/total_users*100):.1f}%" if total_users > 0 else "0%"
|
|
222
|
+
),
|
|
223
|
+
'change_type': 'neutral',
|
|
224
|
+
'color': 'warning',
|
|
225
|
+
'description': 'Administrative access',
|
|
226
|
+
},
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
return cards
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
self.logger.error(f"Error generating stat cards: {e}")
|
|
233
|
+
return []
|
|
234
|
+
|
|
235
|
+
def get_recent_activity(self, limit: int = 10) -> List[Dict[str, Any]]:
|
|
236
|
+
"""
|
|
237
|
+
Get recent activity entries for dashboard.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
limit: Maximum number of activity entries to return
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
List of recent activity entries
|
|
244
|
+
|
|
245
|
+
%%AI_HINT: Returns placeholder - connect to django-auditlog when available%%
|
|
246
|
+
"""
|
|
247
|
+
try:
|
|
248
|
+
# Note: This is a placeholder. Real implementation should connect to:
|
|
249
|
+
# - django-auditlog (if installed)
|
|
250
|
+
# - Custom activity/history tracking
|
|
251
|
+
# - Django admin logs (LogEntry model)
|
|
252
|
+
|
|
253
|
+
# Try to get Django admin logs as fallback
|
|
254
|
+
from django.contrib.admin.models import LogEntry
|
|
255
|
+
|
|
256
|
+
activities = []
|
|
257
|
+
log_entries = LogEntry.objects.select_related('user', 'content_type').order_by('-action_time')[:limit]
|
|
258
|
+
|
|
259
|
+
for entry in log_entries:
|
|
260
|
+
activities.append({
|
|
261
|
+
'id': entry.pk,
|
|
262
|
+
'user': entry.user.get_username() if entry.user else 'System',
|
|
263
|
+
'action': entry.get_action_flag_display().lower(),
|
|
264
|
+
'resource': str(entry),
|
|
265
|
+
'timestamp': entry.action_time.isoformat(),
|
|
266
|
+
'icon': self._get_activity_icon(entry.action_flag),
|
|
267
|
+
'color': self._get_activity_color(entry.action_flag),
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
return activities
|
|
271
|
+
|
|
272
|
+
except Exception as e:
|
|
273
|
+
self.logger.error(f"Error getting recent activity: {e}")
|
|
274
|
+
return []
|
|
275
|
+
|
|
276
|
+
def _get_activity_icon(self, action_flag: int) -> str:
|
|
277
|
+
"""Get icon for activity based on action flag."""
|
|
278
|
+
icons = {
|
|
279
|
+
1: 'add_circle', # ADDITION
|
|
280
|
+
2: 'edit', # CHANGE
|
|
281
|
+
3: 'delete', # DELETION
|
|
282
|
+
}
|
|
283
|
+
return icons.get(action_flag, 'circle')
|
|
284
|
+
|
|
285
|
+
def _get_activity_color(self, action_flag: int) -> str:
|
|
286
|
+
"""Get color for activity based on action flag."""
|
|
287
|
+
colors = {
|
|
288
|
+
1: 'success', # ADDITION
|
|
289
|
+
2: 'primary', # CHANGE
|
|
290
|
+
3: 'error', # DELETION
|
|
291
|
+
}
|
|
292
|
+
return colors.get(action_flag, 'default')
|
|
293
|
+
|
|
294
|
+
def get_recent_users(self, limit: int = 10) -> List[Dict[str, Any]]:
|
|
295
|
+
"""
|
|
296
|
+
Get recent users data.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
limit: Maximum number of users to return
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
List of recent user dictionaries with admin URLs
|
|
303
|
+
|
|
304
|
+
%%AI_HINT: Real data from User model with admin URLs%%
|
|
305
|
+
"""
|
|
306
|
+
try:
|
|
307
|
+
User = self._get_user_model()
|
|
308
|
+
recent_users = User.objects.select_related().order_by("-date_joined")[:limit]
|
|
309
|
+
|
|
310
|
+
return [
|
|
311
|
+
{
|
|
312
|
+
"id": user.id,
|
|
313
|
+
"username": user.username,
|
|
314
|
+
"email": user.email or "No email",
|
|
315
|
+
"date_joined": (
|
|
316
|
+
user.date_joined.strftime("%Y-%m-%d %H:%M")
|
|
317
|
+
if user.date_joined
|
|
318
|
+
else "Unknown"
|
|
319
|
+
),
|
|
320
|
+
"is_active": user.is_active,
|
|
321
|
+
"is_staff": user.is_staff,
|
|
322
|
+
"is_superuser": user.is_superuser,
|
|
323
|
+
"last_login": (
|
|
324
|
+
user.last_login.strftime("%Y-%m-%d %H:%M")
|
|
325
|
+
if user.last_login
|
|
326
|
+
else None
|
|
327
|
+
),
|
|
328
|
+
}
|
|
329
|
+
for user in recent_users
|
|
330
|
+
]
|
|
331
|
+
except Exception as e:
|
|
332
|
+
self.logger.error(f"Error getting recent users: {e}")
|
|
333
|
+
return []
|
|
334
|
+
|
|
335
|
+
def get_system_metrics(self) -> Dict[str, Any]:
|
|
336
|
+
"""
|
|
337
|
+
Get system performance metrics.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Dictionary with system metrics (CPU, memory, disk, etc.)
|
|
341
|
+
|
|
342
|
+
%%AI_HINT: Uses psutil for real system metrics when available%%
|
|
343
|
+
"""
|
|
344
|
+
try:
|
|
345
|
+
# Try to use psutil for real system metrics
|
|
346
|
+
try:
|
|
347
|
+
import psutil
|
|
348
|
+
import time
|
|
349
|
+
|
|
350
|
+
# Get CPU usage
|
|
351
|
+
cpu_percent = psutil.cpu_percent(interval=0.1)
|
|
352
|
+
|
|
353
|
+
# Get memory usage
|
|
354
|
+
memory = psutil.virtual_memory()
|
|
355
|
+
memory_percent = memory.percent
|
|
356
|
+
|
|
357
|
+
# Get disk usage for root partition
|
|
358
|
+
disk = psutil.disk_usage('/')
|
|
359
|
+
disk_percent = disk.percent
|
|
360
|
+
|
|
361
|
+
# Get network IO
|
|
362
|
+
net_io = psutil.net_io_counters()
|
|
363
|
+
|
|
364
|
+
# Get system uptime
|
|
365
|
+
boot_time = psutil.boot_time()
|
|
366
|
+
uptime_seconds = time.time() - boot_time
|
|
367
|
+
uptime_days = int(uptime_seconds // 86400)
|
|
368
|
+
uptime_hours = int((uptime_seconds % 86400) // 3600)
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
'cpu_usage': round(cpu_percent, 1),
|
|
372
|
+
'memory_usage': round(memory_percent, 1),
|
|
373
|
+
'disk_usage': round(disk_percent, 1),
|
|
374
|
+
'network_in': f"{net_io.bytes_recv / (1024**2):.1f} MB",
|
|
375
|
+
'network_out': f"{net_io.bytes_sent / (1024**2):.1f} MB",
|
|
376
|
+
'response_time': '< 100ms', # Placeholder - can be calculated from middleware
|
|
377
|
+
'uptime': f"{uptime_days} days, {uptime_hours} hours",
|
|
378
|
+
}
|
|
379
|
+
except ImportError:
|
|
380
|
+
self.logger.warning("psutil not installed, returning placeholder metrics")
|
|
381
|
+
# Fallback to placeholder data if psutil not available
|
|
382
|
+
return {
|
|
383
|
+
'cpu_usage': 0,
|
|
384
|
+
'memory_usage': 0,
|
|
385
|
+
'disk_usage': 0,
|
|
386
|
+
'network_in': 'N/A',
|
|
387
|
+
'network_out': 'N/A',
|
|
388
|
+
'response_time': 'N/A',
|
|
389
|
+
'uptime': 'N/A',
|
|
390
|
+
}
|
|
391
|
+
except Exception as e:
|
|
392
|
+
self.logger.error(f"Error getting system metrics: {e}")
|
|
393
|
+
return {'error': str(e)}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
System Health Service
|
|
3
|
+
|
|
4
|
+
Monitors system components health status.
|
|
5
|
+
Checks database, cache, queue, storage, and API availability.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any, Dict, List, Literal
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SystemHealthService:
|
|
16
|
+
"""
|
|
17
|
+
Service for monitoring system component health.
|
|
18
|
+
|
|
19
|
+
%%PRIORITY:HIGH%%
|
|
20
|
+
%%AI_HINT: Checks health of various system components%%
|
|
21
|
+
|
|
22
|
+
TAGS: health, monitoring, system, service
|
|
23
|
+
DEPENDS_ON: [django.db.connection, django.core.cache, redis]
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
"""Initialize system health service."""
|
|
28
|
+
self.logger = logger
|
|
29
|
+
|
|
30
|
+
def check_database_health(self) -> Dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Check database connectivity and health.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Health status dictionary with status, description, last_check
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
from django.db import connection
|
|
39
|
+
|
|
40
|
+
# Test database connection
|
|
41
|
+
with connection.cursor() as cursor:
|
|
42
|
+
cursor.execute("SELECT 1")
|
|
43
|
+
cursor.fetchone()
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
'component': 'database',
|
|
47
|
+
'status': 'healthy',
|
|
48
|
+
'description': 'Database connection is working',
|
|
49
|
+
'last_check': datetime.now().isoformat(),
|
|
50
|
+
'health_percentage': 100,
|
|
51
|
+
}
|
|
52
|
+
except Exception as e:
|
|
53
|
+
self.logger.error(f"Database health check failed: {e}")
|
|
54
|
+
return {
|
|
55
|
+
'component': 'database',
|
|
56
|
+
'status': 'error',
|
|
57
|
+
'description': f'Database error: {str(e)}',
|
|
58
|
+
'last_check': datetime.now().isoformat(),
|
|
59
|
+
'health_percentage': 0,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
def check_cache_health(self) -> Dict[str, Any]:
|
|
63
|
+
"""
|
|
64
|
+
Check cache (Redis/Memcached) connectivity and health.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Health status dictionary
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
from django.core.cache import cache
|
|
71
|
+
|
|
72
|
+
# Test cache by setting and getting a test value
|
|
73
|
+
test_key = 'health_check_test'
|
|
74
|
+
test_value = 'ok'
|
|
75
|
+
cache.set(test_key, test_value, timeout=10)
|
|
76
|
+
result = cache.get(test_key)
|
|
77
|
+
|
|
78
|
+
if result == test_value:
|
|
79
|
+
cache.delete(test_key)
|
|
80
|
+
return {
|
|
81
|
+
'component': 'cache',
|
|
82
|
+
'status': 'healthy',
|
|
83
|
+
'description': 'Cache is working correctly',
|
|
84
|
+
'last_check': datetime.now().isoformat(),
|
|
85
|
+
'health_percentage': 100,
|
|
86
|
+
}
|
|
87
|
+
else:
|
|
88
|
+
return {
|
|
89
|
+
'component': 'cache',
|
|
90
|
+
'status': 'warning',
|
|
91
|
+
'description': 'Cache test failed',
|
|
92
|
+
'last_check': datetime.now().isoformat(),
|
|
93
|
+
'health_percentage': 50,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
self.logger.error(f"Cache health check failed: {e}")
|
|
98
|
+
return {
|
|
99
|
+
'component': 'cache',
|
|
100
|
+
'status': 'error',
|
|
101
|
+
'description': f'Cache error: {str(e)}',
|
|
102
|
+
'last_check': datetime.now().isoformat(),
|
|
103
|
+
'health_percentage': 0,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def check_queue_health(self) -> Dict[str, Any]:
|
|
107
|
+
"""
|
|
108
|
+
Check task queue (Celery/Dramatiq) health.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Health status dictionary
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
# TODO: Add real queue health check
|
|
115
|
+
# Example: Check Redis connection, queue sizes, worker status
|
|
116
|
+
from django_cfg.modules.django_tasks import DjangoTasks
|
|
117
|
+
|
|
118
|
+
tasks = DjangoTasks()
|
|
119
|
+
redis_client = tasks.get_redis_client()
|
|
120
|
+
|
|
121
|
+
if redis_client and redis_client.ping():
|
|
122
|
+
return {
|
|
123
|
+
'component': 'queue',
|
|
124
|
+
'status': 'healthy',
|
|
125
|
+
'description': 'Queue system is operational',
|
|
126
|
+
'last_check': datetime.now().isoformat(),
|
|
127
|
+
'health_percentage': 100,
|
|
128
|
+
}
|
|
129
|
+
else:
|
|
130
|
+
return {
|
|
131
|
+
'component': 'queue',
|
|
132
|
+
'status': 'error',
|
|
133
|
+
'description': 'Queue system unavailable',
|
|
134
|
+
'last_check': datetime.now().isoformat(),
|
|
135
|
+
'health_percentage': 0,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
self.logger.error(f"Queue health check failed: {e}")
|
|
140
|
+
return {
|
|
141
|
+
'component': 'queue',
|
|
142
|
+
'status': 'error',
|
|
143
|
+
'description': f'Queue error: {str(e)}',
|
|
144
|
+
'last_check': datetime.now().isoformat(),
|
|
145
|
+
'health_percentage': 0,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
def check_storage_health(self) -> Dict[str, Any]:
|
|
149
|
+
"""
|
|
150
|
+
Check storage/file system health.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Health status dictionary
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
import os
|
|
157
|
+
from django.conf import settings
|
|
158
|
+
|
|
159
|
+
# Check if media directory is writable
|
|
160
|
+
media_root = getattr(settings, 'MEDIA_ROOT', None)
|
|
161
|
+
|
|
162
|
+
if media_root and os.path.exists(media_root) and os.access(media_root, os.W_OK):
|
|
163
|
+
return {
|
|
164
|
+
'component': 'storage',
|
|
165
|
+
'status': 'healthy',
|
|
166
|
+
'description': 'Storage is accessible and writable',
|
|
167
|
+
'last_check': datetime.now().isoformat(),
|
|
168
|
+
'health_percentage': 100,
|
|
169
|
+
}
|
|
170
|
+
else:
|
|
171
|
+
return {
|
|
172
|
+
'component': 'storage',
|
|
173
|
+
'status': 'warning',
|
|
174
|
+
'description': 'Storage may have limited access',
|
|
175
|
+
'last_check': datetime.now().isoformat(),
|
|
176
|
+
'health_percentage': 70,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
self.logger.error(f"Storage health check failed: {e}")
|
|
181
|
+
return {
|
|
182
|
+
'component': 'storage',
|
|
183
|
+
'status': 'error',
|
|
184
|
+
'description': f'Storage error: {str(e)}',
|
|
185
|
+
'last_check': datetime.now().isoformat(),
|
|
186
|
+
'health_percentage': 0,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
def get_all_health_checks(self) -> List[Dict[str, Any]]:
|
|
190
|
+
"""
|
|
191
|
+
Run all health checks and return aggregated results.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
List of health check results for all components
|
|
195
|
+
|
|
196
|
+
USED_BY: DashboardViewSet.system_health endpoint
|
|
197
|
+
"""
|
|
198
|
+
checks = [
|
|
199
|
+
self.check_database_health(),
|
|
200
|
+
self.check_cache_health(),
|
|
201
|
+
self.check_queue_health(),
|
|
202
|
+
self.check_storage_health(),
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
return checks
|
|
206
|
+
|
|
207
|
+
def get_overall_health_status(self) -> Dict[str, Any]:
|
|
208
|
+
"""
|
|
209
|
+
Get overall system health status.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Dictionary with overall status, percentage, and component details
|
|
213
|
+
"""
|
|
214
|
+
checks = self.get_all_health_checks()
|
|
215
|
+
|
|
216
|
+
# Calculate overall health percentage
|
|
217
|
+
total_health = sum(check.get('health_percentage', 0) for check in checks)
|
|
218
|
+
overall_percentage = total_health // len(checks) if checks else 0
|
|
219
|
+
|
|
220
|
+
# Determine overall status
|
|
221
|
+
statuses = [check.get('status') for check in checks]
|
|
222
|
+
if 'error' in statuses:
|
|
223
|
+
overall_status = 'error'
|
|
224
|
+
elif 'warning' in statuses:
|
|
225
|
+
overall_status = 'warning'
|
|
226
|
+
else:
|
|
227
|
+
overall_status = 'healthy'
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
'overall_status': overall_status,
|
|
231
|
+
'overall_health_percentage': overall_percentage,
|
|
232
|
+
'components': checks,
|
|
233
|
+
'timestamp': datetime.now().isoformat(),
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
def get_quick_actions(self) -> List[Dict[str, Any]]:
|
|
237
|
+
"""
|
|
238
|
+
Get quick action buttons for dashboard.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
List of quick action dictionaries
|
|
242
|
+
|
|
243
|
+
%%AI_HINT: Actions link to admin pages or trigger common tasks%%
|
|
244
|
+
"""
|
|
245
|
+
actions = [
|
|
246
|
+
{
|
|
247
|
+
'title': 'User Management',
|
|
248
|
+
'description': 'Manage users and permissions',
|
|
249
|
+
'icon': 'people',
|
|
250
|
+
'link': '/admin/auth/user/',
|
|
251
|
+
'color': 'primary',
|
|
252
|
+
'category': 'admin',
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
'title': 'View Logs',
|
|
256
|
+
'description': 'Check system logs',
|
|
257
|
+
'icon': 'description',
|
|
258
|
+
'link': '/admin/django_cfg/logs/',
|
|
259
|
+
'color': 'secondary',
|
|
260
|
+
'category': 'system',
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
'title': 'Clear Cache',
|
|
264
|
+
'description': 'Clear application cache',
|
|
265
|
+
'icon': 'refresh',
|
|
266
|
+
'link': '/cfg/admin/cache/clear/',
|
|
267
|
+
'color': 'warning',
|
|
268
|
+
'category': 'system',
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
'title': 'Run Backup',
|
|
272
|
+
'description': 'Create system backup',
|
|
273
|
+
'icon': 'backup',
|
|
274
|
+
'link': '/cfg/admin/backup/create/',
|
|
275
|
+
'color': 'success',
|
|
276
|
+
'category': 'system',
|
|
277
|
+
},
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
return actions
|