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.

Files changed (152) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/centrifugo/serializers/channels.py +3 -0
  3. django_cfg/apps/centrifugo/serializers/publishes.py +2 -0
  4. django_cfg/apps/centrifugo/views/admin_api.py +1 -1
  5. django_cfg/apps/centrifugo/views/monitoring.py +116 -6
  6. django_cfg/apps/centrifugo/views/testing_api.py +1 -1
  7. django_cfg/apps/dashboard/__init__.py +8 -0
  8. django_cfg/apps/dashboard/api/__init__.py +27 -0
  9. django_cfg/apps/dashboard/api/serializers.py +165 -0
  10. django_cfg/apps/dashboard/api/viewsets.py +257 -0
  11. django_cfg/apps/dashboard/apps.py +23 -0
  12. django_cfg/apps/dashboard/services/__init__.py +11 -0
  13. django_cfg/apps/dashboard/services/statistics_service.py +235 -0
  14. django_cfg/apps/dashboard/services/system_health_service.py +280 -0
  15. django_cfg/apps/dashboard/urls.py +23 -0
  16. django_cfg/apps/frontend/JWT_AUTO_INJECTION.md +224 -0
  17. django_cfg/apps/frontend/views.py +121 -7
  18. django_cfg/apps/tasks/api/serializers.py +82 -0
  19. django_cfg/apps/tasks/api/views.py +571 -0
  20. django_cfg/apps/urls.py +2 -1
  21. django_cfg/core/builders/apps_builder.py +1 -0
  22. django_cfg/middleware/README.md +12 -0
  23. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +1 -1
  24. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +7 -10
  25. django_cfg/modules/django_client/core/parser/openapi30.py +26 -1
  26. django_cfg/modules/django_client/core/parser/openapi31.py +26 -1
  27. django_cfg/pyproject.toml +1 -1
  28. django_cfg/static/frontend/admin/404.html +1 -1
  29. django_cfg/static/frontend/admin/500.html +1 -1
  30. django_cfg/static/frontend/admin/_next/static/-Zk0eDB7OJOEFrFyR5BwZ/_buildManifest.js +1 -0
  31. django_cfg/static/frontend/admin/_next/static/chunks/{43076.55dd23b6cd68edb0.js → 20695.a7d37b6c40ad3f58.js} +1 -1
  32. django_cfg/static/frontend/admin/_next/static/chunks/{25033.d626f78bc99bc4a1.js → 25033.ee3e206d5a2877b6.js} +2 -2
  33. django_cfg/static/frontend/admin/_next/static/chunks/{25892.964150a58f94ce06.js → 25892.5cbed319f9226fdc.js} +1 -1
  34. django_cfg/static/frontend/admin/_next/static/chunks/{2d7a934f.dfef67639279d59d.js → 2d7a934f.329c61f23af1a7ec.js} +1 -1
  35. django_cfg/static/frontend/admin/_next/static/chunks/{30649.00c679812a56aee3.js → 30649.963cfb7268b5864a.js} +1 -1
  36. django_cfg/static/frontend/admin/_next/static/chunks/{30875.784491146c38dbcb.js → 30875.82c3741757b8aa32.js} +1 -1
  37. django_cfg/static/frontend/admin/_next/static/chunks/{32163.ab0ca435b3f26c04.js → 32163.109a03a7252f1508.js} +1 -1
  38. django_cfg/static/frontend/admin/_next/static/chunks/43076-4be6a9794e9c3e8b.js +1 -0
  39. django_cfg/static/frontend/admin/_next/static/chunks/{49978.fb8ba7ee52ffe666.js → 49978.db5a86a8eb233f35.js} +1 -1
  40. django_cfg/static/frontend/admin/_next/static/chunks/50314-79c02212788f1ec7.js +1 -0
  41. django_cfg/static/frontend/admin/_next/static/chunks/{50319.f786248384877960.js → 50319.fd78c7f7e3f1966e.js} +1 -1
  42. django_cfg/static/frontend/admin/_next/static/chunks/{52908.b690e323d8f8efdd.js → 52908.da5b850b0bc0970c.js} +1 -1
  43. django_cfg/static/frontend/admin/_next/static/chunks/{53710.80ca863525d137db.js → 53710.7176bbee6c7b78be.js} +1 -1
  44. django_cfg/static/frontend/admin/_next/static/chunks/{57982.251fed8d58adcf53.js → 57982.2c90b33b0934522a.js} +2 -2
  45. django_cfg/static/frontend/admin/_next/static/chunks/{60181.86e18057c4caaa97.js → 60181.c94d78d10eb5da37.js} +1 -1
  46. django_cfg/static/frontend/admin/_next/static/chunks/{60374.bde0ec1249aa79c6.js → 60374.5d80cfc45439b2b0.js} +1 -1
  47. django_cfg/static/frontend/admin/_next/static/chunks/{6884.7b1db804c88280ed.js → 6884.624d563508cf6db4.js} +1 -1
  48. django_cfg/static/frontend/admin/_next/static/chunks/{69436.9515b854cdf4b57a.js → 69436.be44021e3d7c99c7.js} +1 -1
  49. django_cfg/static/frontend/admin/_next/static/chunks/{70628.00cdd98f672e684f.js → 70628.58e8c38a66543d5e.js} +1 -1
  50. django_cfg/static/frontend/admin/_next/static/chunks/{73218.a826c2248612b37f.js → 73218.d712e7bd678e23a8.js} +1 -1
  51. django_cfg/static/frontend/admin/_next/static/chunks/{76334.64fbaa923d9ac293.js → 76334.f43f2d8b4bbf8dd6.js} +1 -1
  52. django_cfg/static/frontend/admin/_next/static/chunks/{7799.2b280f8ddf067d49.js → 7799.1575cc212bc750c7.js} +1 -1
  53. django_cfg/static/frontend/admin/_next/static/chunks/{80574.620a8a5b4eb91c25.js → 80574.92638dd7b9979664.js} +1 -1
  54. django_cfg/static/frontend/admin/_next/static/chunks/{81127.a0603c3394892d4e.js → 81127.3ead500eec887152.js} +1 -1
  55. django_cfg/static/frontend/admin/_next/static/chunks/82296-a2c8d38f62224be5.js +1 -0
  56. django_cfg/static/frontend/admin/_next/static/chunks/{8383.eb6188b22c453e14.js → 8383.e25a442df26b2e26.js} +1 -1
  57. django_cfg/static/frontend/admin/_next/static/chunks/{85833.35e6ca25ac32a7d2.js → 85833.b0dead4fbcbfdd1b.js} +1 -1
  58. django_cfg/static/frontend/admin/_next/static/chunks/{95365.fc9d7653a78839d0.js → 95365.2b430045fc2e5acf.js} +1 -1
  59. django_cfg/static/frontend/admin/_next/static/chunks/{96424.0793b94836eb13a6.js → 96424.11d76570e9a94b85.js} +1 -1
  60. django_cfg/static/frontend/admin/_next/static/chunks/pages/404-c283223d1afd02a2.js +1 -0
  61. django_cfg/static/frontend/admin/_next/static/chunks/pages/500-389d6d3e1f2f7fda.js +1 -0
  62. django_cfg/static/frontend/admin/_next/static/chunks/pages/_app-f25bec36bbdc9625.js +272 -0
  63. django_cfg/static/frontend/admin/_next/static/chunks/pages/_error-5291033275c26d09.js +1 -0
  64. django_cfg/static/frontend/admin/_next/static/chunks/pages/index-d7bc30185f52cbca.js +1 -0
  65. django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{cookies-24588bf5551f30df.js → cookies-b39c7f22c066e2c6.js} +1 -1
  66. django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{privacy-354dae34a4c4da59.js → privacy-5aedad0cf3a4f80f.js} +1 -1
  67. django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{security-0a5d7fa591ebb1ae.js → security-dbd854d0d5d483e2.js} +1 -1
  68. django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{terms-c3d80322f52dc112.js → terms-f3e1d2b9e5edf12f.js} +1 -1
  69. django_cfg/static/frontend/admin/_next/static/chunks/pages/private/centrifugo-22532c65971225eb.js +1 -0
  70. django_cfg/static/frontend/admin/_next/static/chunks/pages/private/profile-e93a65e8e7d9022b.js +1 -0
  71. django_cfg/static/frontend/admin/_next/static/chunks/pages/private/ui-669e8f2a785beba2.js +1 -0
  72. django_cfg/static/frontend/admin/_next/static/chunks/pages/private-a8a9ba76f2c75354.js +1 -0
  73. django_cfg/static/frontend/admin/_next/static/chunks/{webpack-905bba30877f6490.js → webpack-92add5f95c66e349.js} +1 -1
  74. django_cfg/static/frontend/admin/_next/static/css/78d677ac1677c210.css +3 -0
  75. django_cfg/static/frontend/admin/auth.html +1 -1
  76. django_cfg/static/frontend/admin/index.html +1 -1
  77. django_cfg/static/frontend/admin/legal/cookies.html +1 -1
  78. django_cfg/static/frontend/admin/legal/privacy.html +1 -1
  79. django_cfg/static/frontend/admin/legal/security.html +1 -1
  80. django_cfg/static/frontend/admin/legal/terms.html +1 -1
  81. django_cfg/static/frontend/admin/private/centrifugo.html +1 -0
  82. django_cfg/static/frontend/admin/private/profile.html +1 -0
  83. django_cfg/static/frontend/admin/private/ui.html +1 -0
  84. django_cfg/static/frontend/admin/private.html +1 -1
  85. django_cfg/templates/admin/index.html +97 -63
  86. django_cfg/templates/admin_old/index.html +80 -0
  87. django_cfg/templatetags/django_cfg.py +57 -10
  88. {django_cfg-1.4.82.dist-info → django_cfg-1.4.84.dist-info}/METADATA +1 -1
  89. {django_cfg-1.4.82.dist-info → django_cfg-1.4.84.dist-info}/RECORD +142 -122
  90. django_cfg/static/frontend/admin/_next/static/chunks/pages/404-7cdad2942c3fb179.js +0 -1
  91. django_cfg/static/frontend/admin/_next/static/chunks/pages/500-6cdb27b00678364f.js +0 -1
  92. django_cfg/static/frontend/admin/_next/static/chunks/pages/_app-9c5ca2471de6b000.js +0 -272
  93. django_cfg/static/frontend/admin/_next/static/chunks/pages/_error-b8071a05cabe1c2d.js +0 -1
  94. django_cfg/static/frontend/admin/_next/static/chunks/pages/index-bf88192a30e013a9.js +0 -1
  95. django_cfg/static/frontend/admin/_next/static/chunks/pages/private-2f58633ddf63a5bc.js +0 -1
  96. django_cfg/static/frontend/admin/_next/static/chunks/pages/ui-73632f2d9c6b11ab.js +0 -1
  97. django_cfg/static/frontend/admin/_next/static/css/e201974f9a4d64e6.css +0 -3
  98. django_cfg/static/frontend/admin/_next/static/qEBrQJUidlI_maQ4xQnI0/_buildManifest.js +0 -1
  99. django_cfg/static/frontend/admin/ui.html +0 -92
  100. /django_cfg/static/frontend/admin/_next/static/{qEBrQJUidlI_maQ4xQnI0 → -Zk0eDB7OJOEFrFyR5BwZ}/_ssgManifest.js +0 -0
  101. /django_cfg/templates/{admin → admin_old}/components/action_grid.html +0 -0
  102. /django_cfg/templates/{admin → admin_old}/components/card.html +0 -0
  103. /django_cfg/templates/{admin → admin_old}/components/data_table.html +0 -0
  104. /django_cfg/templates/{admin → admin_old}/components/metric_card.html +0 -0
  105. /django_cfg/templates/{admin → admin_old}/components/modal.html +0 -0
  106. /django_cfg/templates/{admin → admin_old}/components/progress_bar.html +0 -0
  107. /django_cfg/templates/{admin → admin_old}/components/section_header.html +0 -0
  108. /django_cfg/templates/{admin → admin_old}/components/stat_item.html +0 -0
  109. /django_cfg/templates/{admin → admin_old}/components/stats_grid.html +0 -0
  110. /django_cfg/templates/{admin → admin_old}/components/status_badge.html +0 -0
  111. /django_cfg/templates/{admin → admin_old}/components/user_avatar.html +0 -0
  112. /django_cfg/templates/{admin → admin_old}/constance/change_list.html +0 -0
  113. /django_cfg/templates/{admin → admin_old}/constance/includes/default_value.html +0 -0
  114. /django_cfg/templates/{admin → admin_old}/constance/includes/fieldset_header.html +0 -0
  115. /django_cfg/templates/{admin → admin_old}/constance/includes/results_list.html +0 -0
  116. /django_cfg/templates/{admin → admin_old}/constance/includes/setting_row.html +0 -0
  117. /django_cfg/templates/{admin → admin_old}/constance/includes/table_headers.html +0 -0
  118. /django_cfg/templates/{admin → admin_old}/examples/component_class_example.html +0 -0
  119. /django_cfg/templates/{admin → admin_old}/import_export/change_list_export.html +0 -0
  120. /django_cfg/templates/{admin → admin_old}/import_export/change_list_import.html +0 -0
  121. /django_cfg/templates/{admin → admin_old}/import_export/change_list_import_export.html +0 -0
  122. /django_cfg/templates/{admin → admin_old}/index_new.html +0 -0
  123. /django_cfg/templates/{admin → admin_old}/layouts/base_dashboard.html +0 -0
  124. /django_cfg/templates/{admin → admin_old}/layouts/dashboard_with_tabs.html +0 -0
  125. /django_cfg/templates/{admin → admin_old}/sections/commands_section.html +0 -0
  126. /django_cfg/templates/{admin → admin_old}/sections/documentation_section.html +0 -0
  127. /django_cfg/templates/{admin → admin_old}/sections/overview_section.html +0 -0
  128. /django_cfg/templates/{admin → admin_old}/sections/stats_section.html +0 -0
  129. /django_cfg/templates/{admin → admin_old}/sections/system_section.html +0 -0
  130. /django_cfg/templates/{admin → admin_old}/sections/widgets_section.html +0 -0
  131. /django_cfg/templates/{admin → admin_old}/snippets/components/activity_tracker.html +0 -0
  132. /django_cfg/templates/{admin → admin_old}/snippets/components/charts_section.html +0 -0
  133. /django_cfg/templates/{admin → admin_old}/snippets/components/django_commands.html +0 -0
  134. /django_cfg/templates/{admin → admin_old}/snippets/components/quick_actions.html +0 -0
  135. /django_cfg/templates/{admin → admin_old}/snippets/components/recent_activity_improved.html +0 -0
  136. /django_cfg/templates/{admin → admin_old}/snippets/components/recent_users_table.html +0 -0
  137. /django_cfg/templates/{admin → admin_old}/snippets/components/stats_cards.html +0 -0
  138. /django_cfg/templates/{admin → admin_old}/snippets/components/stats_tiles.html +0 -0
  139. /django_cfg/templates/{admin → admin_old}/snippets/components/system_health.html +0 -0
  140. /django_cfg/templates/{admin → admin_old}/snippets/components/system_metrics.html +0 -0
  141. /django_cfg/templates/{admin → admin_old}/snippets/components/user_permissions.html +0 -0
  142. /django_cfg/templates/{admin → admin_old}/snippets/tabs/app_stats_tab.html +0 -0
  143. /django_cfg/templates/{admin → admin_old}/snippets/tabs/commands_tab.html +0 -0
  144. /django_cfg/templates/{admin → admin_old}/snippets/tabs/documentation_tab.html +0 -0
  145. /django_cfg/templates/{admin → admin_old}/snippets/tabs/overview_tab.html +0 -0
  146. /django_cfg/templates/{admin → admin_old}/snippets/tabs/stats_tab.html +0 -0
  147. /django_cfg/templates/{admin → admin_old}/snippets/tabs/users_tab.html +0 -0
  148. /django_cfg/templates/{admin → admin_old}/snippets/tabs/widgets_tab.html +0 -0
  149. /django_cfg/templates/{admin → admin_old}/snippets/zones/zones_table.html +0 -0
  150. {django_cfg-1.4.82.dist-info → django_cfg-1.4.84.dist-info}/WHEEL +0 -0
  151. {django_cfg-1.4.82.dist-info → django_cfg-1.4.84.dist-info}/entry_points.txt +0 -0
  152. {django_cfg-1.4.82.dist-info → django_cfg-1.4.84.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.4.82"
35
+ __version__ = "1.4.84"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -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,8 @@
1
+ """
2
+ Dashboard Application
3
+
4
+ Provides RESTful API endpoints for dashboard statistics and metrics.
5
+ Designed to work with Next.js frontend without ORM dependencies.
6
+ """
7
+
8
+ default_app_config = 'django_cfg.apps.dashboard.apps.DashboardConfig'
@@ -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")