django-cfg 1.4.82__py3-none-any.whl → 1.4.84__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/centrifugo/serializers/channels.py +3 -0
- django_cfg/apps/centrifugo/serializers/publishes.py +2 -0
- django_cfg/apps/centrifugo/views/admin_api.py +1 -1
- django_cfg/apps/centrifugo/views/monitoring.py +116 -6
- django_cfg/apps/centrifugo/views/testing_api.py +1 -1
- django_cfg/apps/dashboard/__init__.py +8 -0
- django_cfg/apps/dashboard/api/__init__.py +27 -0
- django_cfg/apps/dashboard/api/serializers.py +165 -0
- django_cfg/apps/dashboard/api/viewsets.py +257 -0
- django_cfg/apps/dashboard/apps.py +23 -0
- django_cfg/apps/dashboard/services/__init__.py +11 -0
- django_cfg/apps/dashboard/services/statistics_service.py +235 -0
- django_cfg/apps/dashboard/services/system_health_service.py +280 -0
- django_cfg/apps/dashboard/urls.py +23 -0
- django_cfg/apps/frontend/JWT_AUTO_INJECTION.md +224 -0
- django_cfg/apps/frontend/views.py +121 -7
- django_cfg/apps/tasks/api/serializers.py +82 -0
- django_cfg/apps/tasks/api/views.py +571 -0
- django_cfg/apps/urls.py +2 -1
- django_cfg/core/builders/apps_builder.py +1 -0
- django_cfg/middleware/README.md +12 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +1 -1
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +7 -10
- django_cfg/modules/django_client/core/parser/openapi30.py +26 -1
- django_cfg/modules/django_client/core/parser/openapi31.py +26 -1
- 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/-Zk0eDB7OJOEFrFyR5BwZ/_buildManifest.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{43076.55dd23b6cd68edb0.js → 20695.a7d37b6c40ad3f58.js} +1 -1
- 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/43076-4be6a9794e9c3e8b.js +1 -0
- 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-79c02212788f1ec7.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/{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/82296-a2c8d38f62224be5.js +1 -0
- 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/{96424.0793b94836eb13a6.js → 96424.11d76570e9a94b85.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/404-c283223d1afd02a2.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/500-389d6d3e1f2f7fda.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/_app-f25bec36bbdc9625.js +272 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/_error-5291033275c26d09.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/index-d7bc30185f52cbca.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{cookies-24588bf5551f30df.js → cookies-b39c7f22c066e2c6.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{privacy-354dae34a4c4da59.js → privacy-5aedad0cf3a4f80f.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{security-0a5d7fa591ebb1ae.js → security-dbd854d0d5d483e2.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{terms-c3d80322f52dc112.js → terms-f3e1d2b9e5edf12f.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/centrifugo-22532c65971225eb.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/profile-e93a65e8e7d9022b.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/ui-669e8f2a785beba2.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private-a8a9ba76f2c75354.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{webpack-905bba30877f6490.js → webpack-92add5f95c66e349.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/css/78d677ac1677c210.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 -0
- django_cfg/static/frontend/admin/private/profile.html +1 -0
- 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 +97 -63
- django_cfg/templates/admin_old/index.html +80 -0
- django_cfg/templatetags/django_cfg.py +57 -10
- {django_cfg-1.4.82.dist-info → django_cfg-1.4.84.dist-info}/METADATA +1 -1
- {django_cfg-1.4.82.dist-info → django_cfg-1.4.84.dist-info}/RECORD +142 -122
- django_cfg/static/frontend/admin/_next/static/chunks/pages/404-7cdad2942c3fb179.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/500-6cdb27b00678364f.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/_app-9c5ca2471de6b000.js +0 -272
- django_cfg/static/frontend/admin/_next/static/chunks/pages/_error-b8071a05cabe1c2d.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/index-bf88192a30e013a9.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-73632f2d9c6b11ab.js +0 -1
- django_cfg/static/frontend/admin/_next/static/css/e201974f9a4d64e6.css +0 -3
- django_cfg/static/frontend/admin/_next/static/qEBrQJUidlI_maQ4xQnI0/_buildManifest.js +0 -1
- django_cfg/static/frontend/admin/ui.html +0 -92
- /django_cfg/static/frontend/admin/_next/static/{qEBrQJUidlI_maQ4xQnI0 → -Zk0eDB7OJOEFrFyR5BwZ}/_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}/sections/commands_section.html +0 -0
- /django_cfg/templates/{admin → admin_old}/sections/documentation_section.html +0 -0
- /django_cfg/templates/{admin → admin_old}/sections/overview_section.html +0 -0
- /django_cfg/templates/{admin → admin_old}/sections/stats_section.html +0 -0
- /django_cfg/templates/{admin → admin_old}/sections/system_section.html +0 -0
- /django_cfg/templates/{admin → admin_old}/sections/widgets_section.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.82.dist-info → django_cfg-1.4.84.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.82.dist-info → django_cfg-1.4.84.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.82.dist-info → django_cfg-1.4.84.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Channel statistics serializers for Centrifugo monitoring API.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
5
7
|
from pydantic import BaseModel, Field
|
|
6
8
|
|
|
7
9
|
|
|
@@ -14,6 +16,7 @@ class ChannelStatsSerializer(BaseModel):
|
|
|
14
16
|
failed: int = Field(description="Failed publishes")
|
|
15
17
|
avg_duration_ms: float = Field(description="Average duration")
|
|
16
18
|
avg_acks: float = Field(description="Average ACKs received")
|
|
19
|
+
last_activity_at: Optional[str] = Field(default=None, description="Last activity timestamp (ISO format)")
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
class ChannelListSerializer(BaseModel):
|
|
@@ -11,6 +11,8 @@ class RecentPublishesSerializer(BaseModel):
|
|
|
11
11
|
publishes: list[dict] = Field(description="List of recent publishes")
|
|
12
12
|
count: int = Field(description="Number of publishes returned")
|
|
13
13
|
total_available: int = Field(description="Total publishes available")
|
|
14
|
+
offset: int = Field(default=0, description="Current offset for pagination")
|
|
15
|
+
has_more: bool = Field(default=False, description="Whether more results are available")
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
__all__ = ["RecentPublishesSerializer"]
|
|
@@ -41,7 +41,7 @@ class CentrifugoAdminAPIViewSet(viewsets.ViewSet):
|
|
|
41
41
|
All requests are authenticated via Django session and proxied to Centrifugo.
|
|
42
42
|
"""
|
|
43
43
|
|
|
44
|
-
authentication_classes = [SessionAuthentication]
|
|
44
|
+
# authentication_classes = [SessionAuthentication]
|
|
45
45
|
permission_classes = [IsAdminUser]
|
|
46
46
|
|
|
47
47
|
def __init__(self, *args, **kwargs):
|
|
@@ -4,10 +4,11 @@ Centrifugo Monitoring ViewSet.
|
|
|
4
4
|
Provides REST API endpoints for monitoring Centrifugo publish statistics.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from datetime import datetime
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
8
|
|
|
9
9
|
from django.db import models
|
|
10
|
-
from django.db.models import Avg, Count
|
|
10
|
+
from django.db.models import Avg, Count, Max
|
|
11
|
+
from django.db.models.functions import TruncHour, TruncDay
|
|
11
12
|
from django_cfg.modules.django_logging import get_logger
|
|
12
13
|
from drf_spectacular.types import OpenApiTypes
|
|
13
14
|
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
|
@@ -41,7 +42,7 @@ class CentrifugoMonitorViewSet(viewsets.ViewSet):
|
|
|
41
42
|
- Channel-level statistics
|
|
42
43
|
"""
|
|
43
44
|
|
|
44
|
-
authentication_classes = [SessionAuthentication]
|
|
45
|
+
# authentication_classes = [SessionAuthentication]
|
|
45
46
|
permission_classes = [IsAdminUser]
|
|
46
47
|
|
|
47
48
|
@extend_schema(
|
|
@@ -144,6 +145,20 @@ class CentrifugoMonitorViewSet(viewsets.ViewSet):
|
|
|
144
145
|
description="Filter by channel name",
|
|
145
146
|
required=False,
|
|
146
147
|
),
|
|
148
|
+
OpenApiParameter(
|
|
149
|
+
name="status",
|
|
150
|
+
type=OpenApiTypes.STR,
|
|
151
|
+
location=OpenApiParameter.QUERY,
|
|
152
|
+
description="Filter by status (success, failed, timeout, pending, partial)",
|
|
153
|
+
required=False,
|
|
154
|
+
),
|
|
155
|
+
OpenApiParameter(
|
|
156
|
+
name="offset",
|
|
157
|
+
type=OpenApiTypes.INT,
|
|
158
|
+
location=OpenApiParameter.QUERY,
|
|
159
|
+
description="Offset for pagination (default: 0)",
|
|
160
|
+
required=False,
|
|
161
|
+
),
|
|
147
162
|
],
|
|
148
163
|
responses={
|
|
149
164
|
200: RecentPublishesSerializer,
|
|
@@ -158,14 +173,24 @@ class CentrifugoMonitorViewSet(viewsets.ViewSet):
|
|
|
158
173
|
count = min(count, 200) # Max 200
|
|
159
174
|
|
|
160
175
|
channel = request.GET.get("channel")
|
|
176
|
+
status_filter = request.GET.get("status") # NEW: status filter
|
|
177
|
+
offset = int(request.GET.get("offset", 0)) # NEW: offset for pagination
|
|
161
178
|
|
|
162
179
|
queryset = CentrifugoLog.objects.all()
|
|
163
180
|
|
|
164
181
|
if channel:
|
|
165
182
|
queryset = queryset.filter(channel=channel)
|
|
166
183
|
|
|
184
|
+
# NEW: Filter by status
|
|
185
|
+
if status_filter and status_filter in ["success", "failed", "timeout", "pending", "partial"]:
|
|
186
|
+
queryset = queryset.filter(status=status_filter)
|
|
187
|
+
|
|
188
|
+
# Get total count before slicing
|
|
189
|
+
total = queryset.count()
|
|
190
|
+
|
|
191
|
+
# NEW: Apply offset and limit
|
|
167
192
|
publishes_list = list(
|
|
168
|
-
queryset.order_by("-created_at")[:count].values(
|
|
193
|
+
queryset.order_by("-created_at")[offset:offset + count].values(
|
|
169
194
|
"message_id",
|
|
170
195
|
"channel",
|
|
171
196
|
"status",
|
|
@@ -187,12 +212,12 @@ class CentrifugoMonitorViewSet(viewsets.ViewSet):
|
|
|
187
212
|
if pub["completed_at"]:
|
|
188
213
|
pub["completed_at"] = pub["completed_at"].isoformat()
|
|
189
214
|
|
|
190
|
-
total = queryset.count()
|
|
191
|
-
|
|
192
215
|
response_data = {
|
|
193
216
|
"publishes": publishes_list,
|
|
194
217
|
"count": len(publishes_list),
|
|
195
218
|
"total_available": total,
|
|
219
|
+
"offset": offset, # NEW: for pagination
|
|
220
|
+
"has_more": (offset + count) < total, # NEW: pagination helper
|
|
196
221
|
}
|
|
197
222
|
|
|
198
223
|
serializer = RecentPublishesSerializer(**response_data)
|
|
@@ -228,6 +253,89 @@ class CentrifugoMonitorViewSet(viewsets.ViewSet):
|
|
|
228
253
|
400: {"description": "Invalid parameters"},
|
|
229
254
|
},
|
|
230
255
|
)
|
|
256
|
+
@extend_schema(
|
|
257
|
+
tags=["Centrifugo Monitoring"],
|
|
258
|
+
summary="Get publish timeline",
|
|
259
|
+
description="Returns hourly or daily breakdown of publish counts for charts.",
|
|
260
|
+
parameters=[
|
|
261
|
+
OpenApiParameter(
|
|
262
|
+
name="hours",
|
|
263
|
+
type=OpenApiTypes.INT,
|
|
264
|
+
location=OpenApiParameter.QUERY,
|
|
265
|
+
description="Time period in hours (default: 24)",
|
|
266
|
+
required=False,
|
|
267
|
+
),
|
|
268
|
+
OpenApiParameter(
|
|
269
|
+
name="interval",
|
|
270
|
+
type=OpenApiTypes.STR,
|
|
271
|
+
location=OpenApiParameter.QUERY,
|
|
272
|
+
description="Time interval: 'hour' or 'day' (default: hour)",
|
|
273
|
+
required=False,
|
|
274
|
+
),
|
|
275
|
+
],
|
|
276
|
+
responses={
|
|
277
|
+
200: {"description": "Timeline data"},
|
|
278
|
+
400: {"description": "Invalid parameters"},
|
|
279
|
+
},
|
|
280
|
+
)
|
|
281
|
+
@action(detail=False, methods=["get"], url_path="timeline")
|
|
282
|
+
def timeline(self, request):
|
|
283
|
+
"""Get publish timeline breakdown for charts."""
|
|
284
|
+
try:
|
|
285
|
+
hours = int(request.GET.get("hours", 24))
|
|
286
|
+
hours = min(max(hours, 1), 168)
|
|
287
|
+
interval = request.GET.get("interval", "hour")
|
|
288
|
+
|
|
289
|
+
if interval not in ["hour", "day"]:
|
|
290
|
+
interval = "hour"
|
|
291
|
+
|
|
292
|
+
# Determine truncation function
|
|
293
|
+
trunc_func = TruncHour if interval == "hour" else TruncDay
|
|
294
|
+
|
|
295
|
+
# Get timeline data
|
|
296
|
+
timeline_data = (
|
|
297
|
+
CentrifugoLog.objects.recent(hours)
|
|
298
|
+
.annotate(period=trunc_func("created_at"))
|
|
299
|
+
.values("period")
|
|
300
|
+
.annotate(
|
|
301
|
+
count=Count("id"),
|
|
302
|
+
successful=Count("id", filter=models.Q(status="success")),
|
|
303
|
+
failed=Count("id", filter=models.Q(status="failed")),
|
|
304
|
+
timeout=Count("id", filter=models.Q(status="timeout")),
|
|
305
|
+
)
|
|
306
|
+
.order_by("period")
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
timeline_list = []
|
|
310
|
+
for item in timeline_data:
|
|
311
|
+
timeline_list.append({
|
|
312
|
+
"timestamp": item["period"].isoformat(),
|
|
313
|
+
"count": item["count"],
|
|
314
|
+
"successful": item["successful"],
|
|
315
|
+
"failed": item["failed"],
|
|
316
|
+
"timeout": item["timeout"],
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
response_data = {
|
|
320
|
+
"timeline": timeline_list,
|
|
321
|
+
"period_hours": hours,
|
|
322
|
+
"interval": interval,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return Response(response_data)
|
|
326
|
+
|
|
327
|
+
except ValueError as e:
|
|
328
|
+
logger.warning(f"Timeline validation error: {e}")
|
|
329
|
+
return Response(
|
|
330
|
+
{"error": str(e)}, status=status.HTTP_400_BAD_REQUEST
|
|
331
|
+
)
|
|
332
|
+
except Exception as e:
|
|
333
|
+
logger.error(f"Timeline error: {e}", exc_info=True)
|
|
334
|
+
return Response(
|
|
335
|
+
{"error": "Internal server error"},
|
|
336
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
337
|
+
)
|
|
338
|
+
|
|
231
339
|
@action(detail=False, methods=["get"], url_path="channels")
|
|
232
340
|
def channels(self, request):
|
|
233
341
|
"""Get statistics per channel."""
|
|
@@ -245,6 +353,7 @@ class CentrifugoMonitorViewSet(viewsets.ViewSet):
|
|
|
245
353
|
failed=Count("id", filter=models.Q(status="failed")),
|
|
246
354
|
avg_duration_ms=Avg("duration_ms"),
|
|
247
355
|
avg_acks=Avg("acks_received"),
|
|
356
|
+
last_activity_at=Max("created_at"), # NEW: last activity timestamp
|
|
248
357
|
)
|
|
249
358
|
.order_by("-total")
|
|
250
359
|
)
|
|
@@ -259,6 +368,7 @@ class CentrifugoMonitorViewSet(viewsets.ViewSet):
|
|
|
259
368
|
failed=stats["failed"],
|
|
260
369
|
avg_duration_ms=round(stats["avg_duration_ms"] or 0, 2),
|
|
261
370
|
avg_acks=round(stats["avg_acks"] or 0, 2),
|
|
371
|
+
last_activity_at=stats["last_activity_at"].isoformat() if stats["last_activity_at"] else None, # NEW
|
|
262
372
|
)
|
|
263
373
|
)
|
|
264
374
|
|
|
@@ -103,7 +103,7 @@ class CentrifugoTestingAPIViewSet(viewsets.ViewSet):
|
|
|
103
103
|
publishing, and manual ACK management.
|
|
104
104
|
"""
|
|
105
105
|
|
|
106
|
-
authentication_classes = [SessionAuthentication]
|
|
106
|
+
# authentication_classes = [SessionAuthentication]
|
|
107
107
|
permission_classes = [IsAdminUser]
|
|
108
108
|
|
|
109
109
|
def __init__(self, *args, **kwargs):
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dashboard API
|
|
3
|
+
|
|
4
|
+
RESTful API endpoints for dashboard data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .serializers import (
|
|
8
|
+
ActivityEntrySerializer,
|
|
9
|
+
DashboardOverviewSerializer,
|
|
10
|
+
QuickActionSerializer,
|
|
11
|
+
StatCardSerializer,
|
|
12
|
+
SystemHealthItemSerializer,
|
|
13
|
+
SystemHealthSerializer,
|
|
14
|
+
SystemMetricsSerializer,
|
|
15
|
+
)
|
|
16
|
+
from .viewsets import DashboardViewSet
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
'DashboardViewSet',
|
|
20
|
+
'StatCardSerializer',
|
|
21
|
+
'SystemHealthItemSerializer',
|
|
22
|
+
'SystemHealthSerializer',
|
|
23
|
+
'QuickActionSerializer',
|
|
24
|
+
'ActivityEntrySerializer',
|
|
25
|
+
'SystemMetricsSerializer',
|
|
26
|
+
'DashboardOverviewSerializer',
|
|
27
|
+
]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dashboard API Serializers
|
|
3
|
+
|
|
4
|
+
Simple serializers for dashboard endpoints - no nested serializers to avoid allOf.
|
|
5
|
+
Returns plain dict structures for TypeScript generation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from rest_framework import serializers
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StatCardSerializer(serializers.Serializer):
|
|
12
|
+
"""
|
|
13
|
+
Serializer for dashboard statistics cards.
|
|
14
|
+
|
|
15
|
+
Maps to StatCard Pydantic model.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
title = serializers.CharField(help_text="Card title")
|
|
19
|
+
value = serializers.CharField(help_text="Main value to display")
|
|
20
|
+
icon = serializers.CharField(help_text="Material icon name")
|
|
21
|
+
change = serializers.CharField(required=False, allow_null=True, help_text="Change indicator (e.g., '+12%')")
|
|
22
|
+
change_type = serializers.ChoiceField(
|
|
23
|
+
choices=['positive', 'negative', 'neutral'],
|
|
24
|
+
default='neutral',
|
|
25
|
+
help_text="Change type"
|
|
26
|
+
)
|
|
27
|
+
description = serializers.CharField(required=False, allow_null=True, help_text="Additional description")
|
|
28
|
+
color = serializers.CharField(default='primary', help_text="Card color theme")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SystemHealthItemSerializer(serializers.Serializer):
|
|
32
|
+
"""
|
|
33
|
+
Serializer for system health status items.
|
|
34
|
+
|
|
35
|
+
Maps to SystemHealthItem Pydantic model.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
component = serializers.CharField(help_text="Component name")
|
|
39
|
+
status = serializers.ChoiceField(
|
|
40
|
+
choices=['healthy', 'warning', 'error', 'unknown'],
|
|
41
|
+
help_text="Health status"
|
|
42
|
+
)
|
|
43
|
+
description = serializers.CharField(help_text="Status description")
|
|
44
|
+
last_check = serializers.CharField(help_text="Last check time (ISO format)")
|
|
45
|
+
health_percentage = serializers.IntegerField(
|
|
46
|
+
required=False,
|
|
47
|
+
allow_null=True,
|
|
48
|
+
min_value=0,
|
|
49
|
+
max_value=100,
|
|
50
|
+
help_text="Health percentage (0-100)"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SystemHealthSerializer(serializers.Serializer):
|
|
55
|
+
"""Serializer for overall system health status."""
|
|
56
|
+
|
|
57
|
+
overall_status = serializers.ChoiceField(
|
|
58
|
+
choices=['healthy', 'warning', 'error', 'unknown'],
|
|
59
|
+
help_text="Overall system health status"
|
|
60
|
+
)
|
|
61
|
+
overall_health_percentage = serializers.IntegerField(
|
|
62
|
+
min_value=0,
|
|
63
|
+
max_value=100,
|
|
64
|
+
help_text="Overall health percentage"
|
|
65
|
+
)
|
|
66
|
+
components = SystemHealthItemSerializer(many=True, help_text="Health status of individual components")
|
|
67
|
+
timestamp = serializers.CharField(help_text="Check timestamp (ISO format)")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class QuickActionSerializer(serializers.Serializer):
|
|
71
|
+
"""
|
|
72
|
+
Serializer for quick action buttons.
|
|
73
|
+
|
|
74
|
+
Maps to QuickAction Pydantic model.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
title = serializers.CharField(help_text="Action title")
|
|
78
|
+
description = serializers.CharField(help_text="Action description")
|
|
79
|
+
icon = serializers.CharField(help_text="Material icon name")
|
|
80
|
+
link = serializers.CharField(help_text="Action URL")
|
|
81
|
+
color = serializers.ChoiceField(
|
|
82
|
+
choices=['primary', 'success', 'warning', 'danger', 'secondary'],
|
|
83
|
+
default='primary',
|
|
84
|
+
help_text="Button color theme"
|
|
85
|
+
)
|
|
86
|
+
category = serializers.CharField(default='general', help_text="Action category")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ActivityEntrySerializer(serializers.Serializer):
|
|
90
|
+
"""Serializer for recent activity entries."""
|
|
91
|
+
|
|
92
|
+
id = serializers.IntegerField(help_text="Activity ID")
|
|
93
|
+
user = serializers.CharField(help_text="User who performed the action")
|
|
94
|
+
action = serializers.CharField(help_text="Action type (created, updated, deleted, etc.)")
|
|
95
|
+
resource = serializers.CharField(help_text="Resource affected")
|
|
96
|
+
timestamp = serializers.CharField(help_text="Activity timestamp (ISO format)")
|
|
97
|
+
icon = serializers.CharField(help_text="Material icon name")
|
|
98
|
+
color = serializers.CharField(help_text="Icon color")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class SystemMetricsSerializer(serializers.Serializer):
|
|
102
|
+
"""Serializer for system performance metrics."""
|
|
103
|
+
|
|
104
|
+
cpu_usage = serializers.FloatField(help_text="CPU usage percentage")
|
|
105
|
+
memory_usage = serializers.FloatField(help_text="Memory usage percentage")
|
|
106
|
+
disk_usage = serializers.FloatField(help_text="Disk usage percentage")
|
|
107
|
+
network_in = serializers.CharField(help_text="Network incoming bandwidth")
|
|
108
|
+
network_out = serializers.CharField(help_text="Network outgoing bandwidth")
|
|
109
|
+
response_time = serializers.CharField(help_text="Average response time")
|
|
110
|
+
uptime = serializers.CharField(help_text="System uptime")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class UserStatisticsSerializer(serializers.Serializer):
|
|
114
|
+
"""Serializer for user statistics."""
|
|
115
|
+
|
|
116
|
+
total_users = serializers.IntegerField(help_text="Total number of users")
|
|
117
|
+
active_users = serializers.IntegerField(help_text="Active users (last 30 days)")
|
|
118
|
+
new_users = serializers.IntegerField(help_text="New users (last 7 days)")
|
|
119
|
+
superusers = serializers.IntegerField(help_text="Number of superusers")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class AppStatisticsSerializer(serializers.Serializer):
|
|
123
|
+
"""Serializer for application-specific statistics."""
|
|
124
|
+
|
|
125
|
+
app_name = serializers.CharField(help_text="Application name")
|
|
126
|
+
statistics = serializers.DictField(
|
|
127
|
+
child=serializers.IntegerField(),
|
|
128
|
+
help_text="Application statistics"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class DashboardOverviewSerializer(serializers.Serializer):
|
|
133
|
+
"""
|
|
134
|
+
Main serializer for dashboard overview endpoint.
|
|
135
|
+
Uses DictField to avoid allOf generation in OpenAPI.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
stat_cards = serializers.ListField(
|
|
139
|
+
child=serializers.DictField(),
|
|
140
|
+
help_text="Dashboard statistics cards"
|
|
141
|
+
)
|
|
142
|
+
system_health = serializers.ListField(
|
|
143
|
+
child=serializers.DictField(),
|
|
144
|
+
help_text="System health status"
|
|
145
|
+
)
|
|
146
|
+
quick_actions = serializers.ListField(
|
|
147
|
+
child=serializers.DictField(),
|
|
148
|
+
help_text="Quick action buttons"
|
|
149
|
+
)
|
|
150
|
+
recent_activity = serializers.ListField(
|
|
151
|
+
child=serializers.DictField(),
|
|
152
|
+
help_text="Recent activity entries"
|
|
153
|
+
)
|
|
154
|
+
system_metrics = serializers.DictField(help_text="System performance metrics")
|
|
155
|
+
user_statistics = serializers.DictField(help_text="User statistics")
|
|
156
|
+
timestamp = serializers.CharField(help_text="Data timestamp (ISO format)")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class APIResponseSerializer(serializers.Serializer):
|
|
160
|
+
"""Standard API response wrapper."""
|
|
161
|
+
|
|
162
|
+
success = serializers.BooleanField(help_text="Operation success status")
|
|
163
|
+
message = serializers.CharField(required=False, help_text="Success message")
|
|
164
|
+
error = serializers.CharField(required=False, help_text="Error message")
|
|
165
|
+
data = serializers.DictField(required=False, help_text="Response data")
|