aimeat 1.5.0 → 1.6.1
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.
- package/dist/.env.example +15 -0
- package/dist/bin/aimeat.js +0 -0
- package/dist/locales/en.json +79 -2
- package/dist/locales/fi.json +79 -2
- package/dist/public/css/views/admin.css +97 -0
- package/dist/public/js/services/admin.js +7 -1
- package/dist/public/views/admin/federation-tab.js +60 -0
- package/dist/public/views/admin/stats-tab.js +480 -187
- package/dist/public/views/profile/access-tab.js +1 -0
- package/dist/src/auth/node-keys.d.ts.map +1 -1
- package/dist/src/auth/node-keys.js +3 -0
- package/dist/src/auth/node-keys.js.map +1 -1
- package/dist/src/config.d.ts +13 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +14 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/index.js +41 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/extensions.js +1 -1
- package/dist/src/mcp/extensions.js.map +1 -1
- package/dist/src/middleware/rate-limit.d.ts.map +1 -1
- package/dist/src/middleware/rate-limit.js +4 -1
- package/dist/src/middleware/rate-limit.js.map +1 -1
- package/dist/src/models/schemas.d.ts +45 -0
- package/dist/src/models/schemas.d.ts.map +1 -1
- package/dist/src/models/schemas.js +37 -1
- package/dist/src/models/schemas.js.map +1 -1
- package/dist/src/routes/actions.d.ts.map +1 -1
- package/dist/src/routes/actions.js +1 -0
- package/dist/src/routes/actions.js.map +1 -1
- package/dist/src/routes/admin-monitoring.d.ts.map +1 -1
- package/dist/src/routes/admin-monitoring.js +7 -0
- package/dist/src/routes/admin-monitoring.js.map +1 -1
- package/dist/src/routes/admin.d.ts.map +1 -1
- package/dist/src/routes/admin.js +33 -15
- package/dist/src/routes/admin.js.map +1 -1
- package/dist/src/routes/agents.js +2 -2
- package/dist/src/routes/agents.js.map +1 -1
- package/dist/src/routes/apps.d.ts.map +1 -1
- package/dist/src/routes/apps.js +5 -1
- package/dist/src/routes/apps.js.map +1 -1
- package/dist/src/routes/auth.d.ts.map +1 -1
- package/dist/src/routes/auth.js +19 -1
- package/dist/src/routes/auth.js.map +1 -1
- package/dist/src/routes/boards.js +2 -2
- package/dist/src/routes/boards.js.map +1 -1
- package/dist/src/routes/consent.d.ts.map +1 -1
- package/dist/src/routes/consent.js +2 -1
- package/dist/src/routes/consent.js.map +1 -1
- package/dist/src/routes/disputes.js +4 -4
- package/dist/src/routes/disputes.js.map +1 -1
- package/dist/src/routes/extensions.d.ts.map +1 -1
- package/dist/src/routes/extensions.js +72 -21
- package/dist/src/routes/extensions.js.map +1 -1
- package/dist/src/routes/federation-auth.d.ts.map +1 -1
- package/dist/src/routes/federation-auth.js +2 -62
- package/dist/src/routes/federation-auth.js.map +1 -1
- package/dist/src/routes/federation-peer.d.ts.map +1 -1
- package/dist/src/routes/federation-peer.js +13 -1
- package/dist/src/routes/federation-peer.js.map +1 -1
- package/dist/src/routes/flags.d.ts.map +1 -1
- package/dist/src/routes/flags.js +2 -1
- package/dist/src/routes/flags.js.map +1 -1
- package/dist/src/routes/foundry.d.ts.map +1 -1
- package/dist/src/routes/foundry.js +2 -2
- package/dist/src/routes/foundry.js.map +1 -1
- package/dist/src/routes/generator.d.ts.map +1 -1
- package/dist/src/routes/generator.js +2 -2
- package/dist/src/routes/generator.js.map +1 -1
- package/dist/src/routes/ghii.d.ts.map +1 -1
- package/dist/src/routes/ghii.js +67 -31
- package/dist/src/routes/ghii.js.map +1 -1
- package/dist/src/routes/memory.d.ts.map +1 -1
- package/dist/src/routes/memory.js +2 -0
- package/dist/src/routes/memory.js.map +1 -1
- package/dist/src/routes/owners.d.ts.map +1 -1
- package/dist/src/routes/owners.js +104 -8
- package/dist/src/routes/owners.js.map +1 -1
- package/dist/src/routes/packages.js +1 -1
- package/dist/src/routes/packages.js.map +1 -1
- package/dist/src/routes/stats.d.ts +13 -0
- package/dist/src/routes/stats.d.ts.map +1 -1
- package/dist/src/routes/stats.js +54 -4
- package/dist/src/routes/stats.js.map +1 -1
- package/dist/src/routes/storage-files.d.ts.map +1 -1
- package/dist/src/routes/storage-files.js +5 -1
- package/dist/src/routes/storage-files.js.map +1 -1
- package/dist/src/routes/upload.js +2 -2
- package/dist/src/routes/upload.js.map +1 -1
- package/dist/src/routes/wallet.d.ts.map +1 -1
- package/dist/src/routes/wallet.js +2 -1
- package/dist/src/routes/wallet.js.map +1 -1
- package/dist/src/routes/work.d.ts.map +1 -1
- package/dist/src/routes/work.js +2 -1
- package/dist/src/routes/work.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +3 -3
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
- package/dist/src/server-bootstrap/service-init.js +2 -0
- package/dist/src/server-bootstrap/service-init.js.map +1 -1
- package/dist/src/server-bootstrap/static-files.d.ts.map +1 -1
- package/dist/src/server-bootstrap/static-files.js +10 -7
- package/dist/src/server-bootstrap/static-files.js.map +1 -1
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +4 -2
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/capability-invoke.d.ts.map +1 -1
- package/dist/src/services/capability-invoke.js +5 -18
- package/dist/src/services/capability-invoke.js.map +1 -1
- package/dist/src/services/config-schema.d.ts.map +1 -1
- package/dist/src/services/config-schema.js +13 -0
- package/dist/src/services/config-schema.js.map +1 -1
- package/dist/src/services/consent.d.ts.map +1 -1
- package/dist/src/services/consent.js +1 -10
- package/dist/src/services/consent.js.map +1 -1
- package/dist/src/services/directory.d.ts.map +1 -1
- package/dist/src/services/directory.js +10 -4
- package/dist/src/services/directory.js.map +1 -1
- package/dist/src/services/email.d.ts +17 -4
- package/dist/src/services/email.d.ts.map +1 -1
- package/dist/src/services/email.js +30 -12
- package/dist/src/services/email.js.map +1 -1
- package/dist/src/services/extension-runtime.d.ts.map +1 -1
- package/dist/src/services/extension-runtime.js +5 -0
- package/dist/src/services/extension-runtime.js.map +1 -1
- package/dist/src/services/federation.d.ts +2 -0
- package/dist/src/services/federation.d.ts.map +1 -1
- package/dist/src/services/federation.js.map +1 -1
- package/dist/src/services/mailbox-notification.d.ts +17 -0
- package/dist/src/services/mailbox-notification.d.ts.map +1 -1
- package/dist/src/services/mailbox-notification.js +25 -0
- package/dist/src/services/mailbox-notification.js.map +1 -1
- package/dist/src/services/morsel.d.ts.map +1 -1
- package/dist/src/services/morsel.js +10 -9
- package/dist/src/services/morsel.js.map +1 -1
- package/dist/src/services/password.d.ts +1 -6
- package/dist/src/services/password.d.ts.map +1 -1
- package/dist/src/services/password.js +19 -10
- package/dist/src/services/password.js.map +1 -1
- package/dist/src/services/push.d.ts +16 -0
- package/dist/src/services/push.d.ts.map +1 -1
- package/dist/src/services/push.js +20 -0
- package/dist/src/services/push.js.map +1 -1
- package/dist/src/services/quota.d.ts.map +1 -1
- package/dist/src/services/quota.js +2 -1
- package/dist/src/services/quota.js.map +1 -1
- package/dist/src/services/scheduler.d.ts.map +1 -1
- package/dist/src/services/scheduler.js +17 -1
- package/dist/src/services/scheduler.js.map +1 -1
- package/dist/src/services/stats.d.ts +42 -2
- package/dist/src/services/stats.d.ts.map +1 -1
- package/dist/src/services/stats.js +176 -1
- package/dist/src/services/stats.js.map +1 -1
- package/dist/src/services/totp.js +2 -2
- package/dist/src/storage/interface.d.ts +6 -1
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.d.ts +10 -0
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +81 -2
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +10 -0
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +73 -2
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/consent.d.ts +1 -0
- package/dist/src/storage/providers/sqlite/repos/consent.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/consent.js +4 -0
- package/dist/src/storage/providers/sqlite/repos/consent.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +19 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/consent.repository.d.ts +1 -0
- package/dist/src/storage/repositories/consent.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/device-auth.repository.d.ts +1 -0
- package/dist/src/storage/repositories/device-auth.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/extension-instance.repository.d.ts +1 -0
- package/dist/src/storage/repositories/extension-instance.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/index.d.ts +1 -0
- package/dist/src/storage/repositories/index.d.ts.map +1 -1
- package/dist/src/storage/repositories/knowledge.repository.d.ts +2 -0
- package/dist/src/storage/repositories/knowledge.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/moderation.repository.d.ts +1 -0
- package/dist/src/storage/repositories/moderation.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/stats.repository.d.ts +12 -0
- package/dist/src/storage/repositories/stats.repository.d.ts.map +1 -0
- package/dist/src/storage/repositories/stats.repository.js +7 -0
- package/dist/src/storage/repositories/stats.repository.js.map +1 -0
- package/dist/src/storage/storage-factory.js +1 -0
- package/dist/src/storage/storage-factory.js.map +1 -1
- package/dist/src/utils/password-validation.d.ts +8 -0
- package/dist/src/utils/password-validation.d.ts.map +1 -0
- package/dist/src/utils/password-validation.js +25 -0
- package/dist/src/utils/password-validation.js.map +1 -0
- package/dist/src/utils/service-summary.d.ts +6 -1
- package/dist/src/utils/service-summary.d.ts.map +1 -1
- package/dist/src/utils/service-summary.js +6 -1
- package/dist/src/utils/service-summary.js.map +1 -1
- package/package.json +97 -77
- package/prisma/schema.prisma +26 -10
|
@@ -1,187 +1,480 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
</div>
|
|
107
|
-
`;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file stats-tab.js
|
|
3
|
+
* @description Admin dashboard Statistics tab with time range selector, notification
|
|
4
|
+
* delivery sections (email, push, mailbox), and per-day Chart.js charts.
|
|
5
|
+
* Manages its own data fetching so the period can be changed without reloading
|
|
6
|
+
* the entire dashboard.
|
|
7
|
+
* @structure
|
|
8
|
+
* - getDateRange(period) -- converts preset key to { from, to } date strings
|
|
9
|
+
* - successRate / rateColor -- helpers for delivery percentage display
|
|
10
|
+
* - sumByPrefix -- sums daily history keys matching a prefix
|
|
11
|
+
* - TimeRange -- period preset buttons + custom date inputs
|
|
12
|
+
* - BreakdownTable -- generic type-breakdown table (email / push)
|
|
13
|
+
* - StatsTab (default) -- main tab component
|
|
14
|
+
* - renderAllCharts -- loads Chart.js and draws all canvases
|
|
15
|
+
* @version-history
|
|
16
|
+
* v1.0.0 -- 2026-05-01 -- Initial stats tab with basic cards and Chart.js charts
|
|
17
|
+
* v2.0.0 -- 2026-05-21 -- Major rewrite: time range selector, email / push /
|
|
18
|
+
* mailbox notification sections, per-day charts, self-managed data fetching
|
|
19
|
+
* v2.0.1 -- 2026-05-21 -- i18n chart labels, chart cleanup on unmount, live
|
|
20
|
+
* badges on gauge cards, section header CSS classes, breakdown table th fix
|
|
21
|
+
* v2.0.2 -- 2026-05-21 -- Replace inline styles with CSS classes, i18n weekday
|
|
22
|
+
* labels, use periodCustom/periodFrom/periodTo i18n keys on date inputs
|
|
23
|
+
*/
|
|
24
|
+
import { h } from 'preact';
|
|
25
|
+
import { useState, useEffect, useRef, useCallback } from 'preact/hooks';
|
|
26
|
+
import htm from 'htm';
|
|
27
|
+
const html = htm.bind(h);
|
|
28
|
+
import { t } from '/js/i18n.js';
|
|
29
|
+
import { num, fmtUp, fmtBytes, StatsGrid, StatCard, EconRow } from './shared.js';
|
|
30
|
+
import * as api from '/js/services/admin.js';
|
|
31
|
+
|
|
32
|
+
/* ── Helpers ── */
|
|
33
|
+
|
|
34
|
+
/** Compute { from, to } date strings from a period preset key. */
|
|
35
|
+
function getDateRange(period) {
|
|
36
|
+
const today = new Date();
|
|
37
|
+
const fmt = d => d.toISOString().split('T')[0];
|
|
38
|
+
const to = fmt(today);
|
|
39
|
+
switch (period) {
|
|
40
|
+
case 'today': return { from: to, to };
|
|
41
|
+
case 'week': {
|
|
42
|
+
const mon = new Date(today);
|
|
43
|
+
mon.setDate(mon.getDate() - ((mon.getDay() + 6) % 7));
|
|
44
|
+
return { from: fmt(mon), to };
|
|
45
|
+
}
|
|
46
|
+
case '7d': {
|
|
47
|
+
const d = new Date(today); d.setDate(d.getDate() - 6);
|
|
48
|
+
return { from: fmt(d), to };
|
|
49
|
+
}
|
|
50
|
+
case '30d': {
|
|
51
|
+
const d = new Date(today); d.setDate(d.getDate() - 29);
|
|
52
|
+
return { from: fmt(d), to };
|
|
53
|
+
}
|
|
54
|
+
default: return null; // 'all' = no params
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function successRate(sent, failed) {
|
|
59
|
+
if (!sent && !failed) return null;
|
|
60
|
+
return ((sent / (sent + failed)) * 100).toFixed(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function rateColor(rate) {
|
|
64
|
+
if (rate === null) return 'var(--text-dim)';
|
|
65
|
+
const n = parseFloat(rate);
|
|
66
|
+
if (n >= 95) return '#22c55e';
|
|
67
|
+
if (n >= 80) return '#eab308';
|
|
68
|
+
return '#ef4444';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Sum all values in a day-object whose key starts with `prefix`. */
|
|
72
|
+
function sumByPrefix(dayData, prefix) {
|
|
73
|
+
let total = 0;
|
|
74
|
+
for (const [key, val] of Object.entries(dayData || {})) {
|
|
75
|
+
if (key.startsWith(prefix)) total += val;
|
|
76
|
+
}
|
|
77
|
+
return total;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* ── Sub-components ── */
|
|
81
|
+
|
|
82
|
+
function TimeRange({ period, setPeriod, customFrom, setCustomFrom, customTo, setCustomTo, onApply }) {
|
|
83
|
+
const presets = [
|
|
84
|
+
{ key: 'today', label: t('dashboard.periodToday') },
|
|
85
|
+
{ key: 'week', label: t('dashboard.periodThisWeek') },
|
|
86
|
+
{ key: '7d', label: t('dashboard.period7Days') },
|
|
87
|
+
{ key: '30d', label: t('dashboard.period30Days') },
|
|
88
|
+
{ key: 'all', label: t('dashboard.periodAll') },
|
|
89
|
+
];
|
|
90
|
+
return html`
|
|
91
|
+
<div class="adm-time-range">
|
|
92
|
+
<span class="adm-time-range-label">${t('dashboard.periodLabel')}:</span>
|
|
93
|
+
${presets.map(p => html`
|
|
94
|
+
<button class="adm-time-btn ${period === p.key ? 'active' : ''}"
|
|
95
|
+
onClick=${() => setPeriod(p.key)}>${p.label}</button>
|
|
96
|
+
`)}
|
|
97
|
+
<div class="adm-time-custom">
|
|
98
|
+
<span class="adm-text-dim adm-text-sm">${t('dashboard.periodCustom')}:</span>
|
|
99
|
+
<input type="date" value=${customFrom} aria-label=${t('dashboard.periodFrom')}
|
|
100
|
+
onInput=${e => setCustomFrom(e.target.value)} />
|
|
101
|
+
<span class="adm-date-separator">-</span>
|
|
102
|
+
<input type="date" value=${customTo} aria-label=${t('dashboard.periodTo')}
|
|
103
|
+
onInput=${e => setCustomTo(e.target.value)} />
|
|
104
|
+
<button class="adm-time-btn" onClick=${onApply}>${t('dashboard.periodApply')}</button>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function BreakdownTable({ byType, columns, typePrefix }) {
|
|
111
|
+
if (!byType || Object.keys(byType).length === 0) return null;
|
|
112
|
+
return html`
|
|
113
|
+
<table class="adm-breakdown-table">
|
|
114
|
+
<thead><tr>
|
|
115
|
+
<th>${t('dashboard.breakdownType')}</th>
|
|
116
|
+
${columns.map(c => html`<th class="num">${t(c.label)}</th>`)}
|
|
117
|
+
</tr></thead>
|
|
118
|
+
<tbody>
|
|
119
|
+
${Object.keys(byType).map(type => html`
|
|
120
|
+
<tr>
|
|
121
|
+
<td>${t(typePrefix + type) || type}</td>
|
|
122
|
+
${columns.map(c => html`<td class="num">${num(c.getData(type))}</td>`)}
|
|
123
|
+
</tr>
|
|
124
|
+
`)}
|
|
125
|
+
</tbody>
|
|
126
|
+
</table>
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* ── Main component ── */
|
|
131
|
+
|
|
132
|
+
export default function StatsTab({ data }) {
|
|
133
|
+
const [period, setPeriod] = useState('7d');
|
|
134
|
+
const [customFrom, setCustomFrom] = useState('');
|
|
135
|
+
const [customTo, setCustomTo] = useState('');
|
|
136
|
+
const [sd, setSd] = useState(data.stats);
|
|
137
|
+
const [loading, setLoading] = useState(false);
|
|
138
|
+
const chartRefs = useRef({});
|
|
139
|
+
|
|
140
|
+
const fetchStats = useCallback(async (p, cf, ct) => {
|
|
141
|
+
setLoading(true);
|
|
142
|
+
try {
|
|
143
|
+
const range = p === 'custom'
|
|
144
|
+
? (cf && ct ? { from: cf, to: ct } : null)
|
|
145
|
+
: getDateRange(p);
|
|
146
|
+
const resp = await api.getStats(range?.from, range?.to);
|
|
147
|
+
if (resp.data) setSd(resp.data);
|
|
148
|
+
} catch { /* keep existing data */ }
|
|
149
|
+
setLoading(false);
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (period !== 'custom') fetchStats(period);
|
|
154
|
+
}, [period]);
|
|
155
|
+
|
|
156
|
+
// Listen for live updates
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
const handler = () => { fetchStats(period, customFrom, customTo); };
|
|
159
|
+
window.addEventListener('aimeat-live-update', handler);
|
|
160
|
+
return () => window.removeEventListener('aimeat-live-update', handler);
|
|
161
|
+
}, [period, customFrom, customTo]);
|
|
162
|
+
|
|
163
|
+
// Re-render charts when data changes; destroy on unmount
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (sd) renderAllCharts(sd, chartRefs);
|
|
166
|
+
return () => {
|
|
167
|
+
for (const key of Object.keys(chartRefs.current)) {
|
|
168
|
+
if (chartRefs.current[key]) {
|
|
169
|
+
chartRefs.current[key].destroy();
|
|
170
|
+
delete chartRefs.current[key];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}, [sd]);
|
|
175
|
+
|
|
176
|
+
if (!sd) return html`<div class="empty">${t('dashboard.statsNotAvailable')}</div>`;
|
|
177
|
+
|
|
178
|
+
const ts = sd.tunnel || {};
|
|
179
|
+
const ms = sd.mailbox || {};
|
|
180
|
+
const cs = sd.consent_permissions || {};
|
|
181
|
+
const gs = sd.gauges || {};
|
|
182
|
+
|
|
183
|
+
// Email stats
|
|
184
|
+
const emailSent = sd.email_sent || 0;
|
|
185
|
+
const emailFailed = sd.email_failed || 0;
|
|
186
|
+
const emailRetried = sd.email_retried || 0;
|
|
187
|
+
const emailRate = successRate(emailSent, emailFailed);
|
|
188
|
+
|
|
189
|
+
// Push stats
|
|
190
|
+
const pushSent = sd.push_sent || 0;
|
|
191
|
+
const pushFailed = sd.push_failed || 0;
|
|
192
|
+
const pushExpired = sd.push_expired_subs || 0;
|
|
193
|
+
const pushRate = successRate(pushSent, pushFailed);
|
|
194
|
+
|
|
195
|
+
// Mailbox notification stats
|
|
196
|
+
const mboxSent = sd.mailbox_notif_sent || 0;
|
|
197
|
+
const mboxFailed = sd.mailbox_notif_failed || 0;
|
|
198
|
+
const mboxBlocked = sd.mailbox_notif_blocked || 0;
|
|
199
|
+
|
|
200
|
+
return html`
|
|
201
|
+
<!-- Time Range Selector -->
|
|
202
|
+
<${TimeRange} period=${period} setPeriod=${setPeriod}
|
|
203
|
+
customFrom=${customFrom} setCustomFrom=${setCustomFrom}
|
|
204
|
+
customTo=${customTo} setCustomTo=${setCustomTo}
|
|
205
|
+
onApply=${() => fetchStats('custom', customFrom, customTo)} />
|
|
206
|
+
|
|
207
|
+
${loading ? html`<div class="adm-text-dim adm-text-sm">${t('dashboard.loading')}</div>` : ''}
|
|
208
|
+
|
|
209
|
+
<p class="adm-text-dim adm-text-sm adm-mb-lg">${t('dashboard.statsExplain')}</p>
|
|
210
|
+
|
|
211
|
+
<!-- Top-level stat cards -->
|
|
212
|
+
<div class="adm-grid adm-grid-4">
|
|
213
|
+
<${StatCard} label=${t('dashboard.requestsTotal')} value=${sd.requests_total} />
|
|
214
|
+
<${StatCard} label=${t('dashboard.memoryOps')} value=${(sd.memory_reads || 0) + (sd.memory_writes || 0)} sub=${t('dashboard.reads') + ': ' + num(sd.memory_reads) + ' / ' + t('dashboard.writes') + ': ' + num(sd.memory_writes)} />
|
|
215
|
+
<${StatCard} label=${t('dashboard.consentOps')} value=${(sd.consent_grants || 0) + (sd.consent_revocations || 0)} sub=${t('dashboard.grants') + ': ' + num(sd.consent_grants) + ' / ' + t('dashboard.revocations') + ': ' + num(sd.consent_revocations)} />
|
|
216
|
+
<${StatCard} label=${t('dashboard.schemaOps')} value=${sd.schema_validations} sub=${t('dashboard.failures') + ': ' + num(sd.schema_validation_failures)} />
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div class="adm-grid adm-grid-4 adm-mb-lg">
|
|
220
|
+
<${StatCard} label=${t('dashboard.uptime')} value=${fmtUp(sd.uptime_seconds || 0)} />
|
|
221
|
+
<${StatCard} label=${t('dashboard.registeredOwners')} value=${sd.active_owners} />
|
|
222
|
+
<${StatCard} label=${t('dashboard.registeredAgents')} value=${sd.active_agents} />
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<!-- Charts (daily, weekly, monthly) -->
|
|
226
|
+
<div class="adm-grid adm-grid-2">
|
|
227
|
+
<div class="adm-card"><h2>${t('dashboard.dailyActivity')}</h2><canvas id="chartDaily" height="200"></canvas></div>
|
|
228
|
+
<div class="adm-card"><h2>${t('dashboard.weeklyComparison')}</h2><canvas id="chartWeekly" height="200"></canvas></div>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="adm-card adm-mt-lg"><h2>${t('dashboard.monthlyTrend')}</h2><canvas id="chartMonthly" height="160"></canvas></div>
|
|
231
|
+
|
|
232
|
+
<!-- Tunnel Stats -->
|
|
233
|
+
<h3 class="adm-mt-lg adm-text-sm adm-text-accent">${t('dashboard.tunnelStats')}</h3>
|
|
234
|
+
<div class="adm-grid adm-grid-4 adm-mt-md">
|
|
235
|
+
<${StatCard} label=${html`${t('dashboard.tunnelActive')} <span class="adm-badge-live">${t('dashboard.live')}</span>`} value=${gs.tunnel_connections_active ?? ts.connections_active ?? 0} color="#22c55e" />
|
|
236
|
+
<${StatCard} label=${t('dashboard.tunnelTotal')} value=${ts.connections_total || 0} />
|
|
237
|
+
<${StatCard} label=${t('dashboard.tunnelDisconnections')} value=${ts.disconnections_total || 0} color="#eab308" />
|
|
238
|
+
<${StatCard} label=${t('dashboard.tunnelReconnects')} value=${ts.reconnects_total || 0} color="#3b82f6" />
|
|
239
|
+
</div>
|
|
240
|
+
<div class="adm-grid adm-grid-4">
|
|
241
|
+
<${StatCard} label=${t('dashboard.tunnelMsgSent')} value=${ts.messages_sent_total || 0} />
|
|
242
|
+
<${StatCard} label=${t('dashboard.tunnelMsgReceived')} value=${ts.messages_received_total || 0} />
|
|
243
|
+
<${StatCard} label=${t('dashboard.tunnelDeliveryFails')} value=${ts.delivery_failures_total || 0} color=${ts.delivery_failures_total > 0 ? '#ef4444' : '#22c55e'} />
|
|
244
|
+
<${StatCard} label=${t('dashboard.tunnelHeartbeatMisses')} value=${ts.heartbeat_misses_total || 0} color=${ts.heartbeat_misses_total > 0 ? '#eab308' : '#22c55e'} />
|
|
245
|
+
</div>
|
|
246
|
+
<div class="adm-grid adm-grid-2">
|
|
247
|
+
<div class="adm-card">
|
|
248
|
+
<h2>${t('dashboard.latency')}</h2>
|
|
249
|
+
<${EconRow} label=${t('dashboard.tunnelLatencyAvg')} value=${(ts.delivery_latency_avg_ms || 0).toFixed(1) + ' ms'} />
|
|
250
|
+
<${EconRow} label=${t('dashboard.tunnelLatencyP95')} value=${(ts.delivery_latency_p95_ms || 0).toFixed(1) + ' ms'} />
|
|
251
|
+
<${EconRow} label=${t('dashboard.tunnelMailboxFallbacks')} value=${num(ts.mailbox_fallbacks_total || 0)} />
|
|
252
|
+
</div>
|
|
253
|
+
<div class="adm-card"><h2>${t('dashboard.tunnelActivity')}</h2><canvas id="chartTunnel" height="180"></canvas></div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<!-- Mailbox Stats -->
|
|
257
|
+
<h3 class="adm-mt-lg adm-text-sm adm-section-purple">${t('dashboard.mailboxStats')}</h3>
|
|
258
|
+
<div class="adm-grid adm-grid-4 adm-mt-md">
|
|
259
|
+
<${StatCard} label=${html`${t('dashboard.mailboxItems')} <span class="adm-badge-live">${t('dashboard.live')}</span>`} value=${gs.mailbox_items_total ?? ms.items_total ?? 0} />
|
|
260
|
+
<${StatCard} label=${html`${t('dashboard.mailboxBytes')} <span class="adm-badge-live">${t('dashboard.live')}</span>`} value=${fmtBytes(gs.mailbox_bytes_total ?? ms.bytes_total ?? 0)} />
|
|
261
|
+
<${StatCard} label=${t('dashboard.mailboxEnqueued')} value=${ms.enqueued_total || 0} color="#3b82f6" />
|
|
262
|
+
<${StatCard} label=${t('dashboard.mailboxDelivered')} value=${ms.delivered_total || 0} color="#22c55e" />
|
|
263
|
+
</div>
|
|
264
|
+
<div class="adm-grid adm-grid-4">
|
|
265
|
+
<${StatCard} label=${t('dashboard.mailboxExpired')} value=${ms.expired_total || 0} color=${ms.expired_total > 0 ? '#eab308' : '#22c55e'} />
|
|
266
|
+
<${StatCard} label=${t('dashboard.mailboxQuotaRejects')} value=${ms.quota_rejections_total || 0} color=${ms.quota_rejections_total > 0 ? '#ef4444' : '#22c55e'} />
|
|
267
|
+
<${StatCard} label=${html`${t('dashboard.mailboxOldestAge')} <span class="adm-badge-live">${t('dashboard.live')}</span>`} value=${fmtUp(gs.mailbox_oldest_item_age_seconds ?? ms.oldest_item_age_seconds ?? 0)} color="var(--text-dim)" />
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<!-- Email Delivery -->
|
|
271
|
+
<h3 class="adm-mt-lg adm-text-sm adm-section-cyan">${t('dashboard.emailDelivery')}</h3>
|
|
272
|
+
<div class="adm-grid adm-grid-4 adm-mt-md">
|
|
273
|
+
<${StatCard} label=${t('dashboard.emailsSent')} value=${emailSent} />
|
|
274
|
+
<${StatCard} label=${t('dashboard.emailsFailed')} value=${emailFailed} color=${emailFailed > 0 ? '#ef4444' : '#22c55e'} />
|
|
275
|
+
<${StatCard} label=${t('dashboard.emailsRetried')} value=${emailRetried} color=${emailRetried > 0 ? '#eab308' : '#22c55e'} />
|
|
276
|
+
<${StatCard} label=${t('dashboard.successRate')} value=${emailRate !== null ? emailRate + '%' : '—'} color=${rateColor(emailRate)} />
|
|
277
|
+
</div>
|
|
278
|
+
${sd.email_sent_by_type ? html`
|
|
279
|
+
<div class="adm-card adm-mt-md">
|
|
280
|
+
<${BreakdownTable} byType=${sd.email_sent_by_type}
|
|
281
|
+
typePrefix="dashboard.emailType."
|
|
282
|
+
columns=${[
|
|
283
|
+
{ label: 'dashboard.breakdownSent', getData: type => (sd.email_sent_by_type || {})[type] || 0 },
|
|
284
|
+
{ label: 'dashboard.breakdownFailed', getData: type => (sd.email_failed_by_type || {})[type] || 0 },
|
|
285
|
+
{ label: 'dashboard.breakdownRetried', getData: type => (sd.email_retried_by_type || {})[type] || 0 },
|
|
286
|
+
]} />
|
|
287
|
+
</div>
|
|
288
|
+
` : ''}
|
|
289
|
+
<div class="adm-card adm-mt-md"><canvas id="chartEmailDaily" height="180"></canvas></div>
|
|
290
|
+
|
|
291
|
+
<!-- Push Notifications -->
|
|
292
|
+
<h3 class="adm-mt-lg adm-text-sm adm-section-violet">${t('dashboard.pushDelivery')}</h3>
|
|
293
|
+
<div class="adm-grid adm-grid-4 adm-mt-md">
|
|
294
|
+
<${StatCard} label=${t('dashboard.pushSent')} value=${pushSent} />
|
|
295
|
+
<${StatCard} label=${t('dashboard.pushFailed')} value=${pushFailed} color=${pushFailed > 0 ? '#ef4444' : '#22c55e'} />
|
|
296
|
+
<${StatCard} label=${t('dashboard.expiredSubs')} value=${pushExpired} color=${pushExpired > 0 ? '#eab308' : '#22c55e'} />
|
|
297
|
+
<${StatCard} label=${t('dashboard.successRate')} value=${pushRate !== null ? pushRate + '%' : '—'} color=${rateColor(pushRate)} />
|
|
298
|
+
</div>
|
|
299
|
+
${sd.push_sent_by_type ? html`
|
|
300
|
+
<div class="adm-card adm-mt-md">
|
|
301
|
+
<${BreakdownTable} byType=${sd.push_sent_by_type}
|
|
302
|
+
typePrefix="dashboard.pushType."
|
|
303
|
+
columns=${[
|
|
304
|
+
{ label: 'dashboard.breakdownSent', getData: type => (sd.push_sent_by_type || {})[type] || 0 },
|
|
305
|
+
{ label: 'dashboard.breakdownFailed', getData: type => (sd.push_failed_by_type || {})[type] || 0 },
|
|
306
|
+
]} />
|
|
307
|
+
</div>
|
|
308
|
+
` : ''}
|
|
309
|
+
<div class="adm-card adm-mt-md"><canvas id="chartPushDaily" height="180"></canvas></div>
|
|
310
|
+
|
|
311
|
+
<!-- Mailbox Notifications -->
|
|
312
|
+
<h3 class="adm-mt-lg adm-text-sm adm-section-amber">${t('dashboard.mailboxNotifications')}</h3>
|
|
313
|
+
<div class="adm-grid adm-grid-3 adm-mt-md">
|
|
314
|
+
<${StatCard} label=${t('dashboard.mailboxNotifSent')} value=${mboxSent} />
|
|
315
|
+
<${StatCard} label=${t('dashboard.mailboxNotifFailed')} value=${mboxFailed} color=${mboxFailed > 0 ? '#ef4444' : '#22c55e'} />
|
|
316
|
+
<${StatCard} label=${t('dashboard.mailboxNotifBlocked')} value=${mboxBlocked} color=${mboxBlocked > 0 ? '#eab308' : '#22c55e'} />
|
|
317
|
+
</div>
|
|
318
|
+
${(sd.mailbox_notif_blocked_by_type || sd.mailbox_notif_sent_by_type) ? html`
|
|
319
|
+
<div class="adm-card adm-mt-md adm-text-sm adm-mailbox-inline">
|
|
320
|
+
${sd.mailbox_notif_blocked_by_type ? html`
|
|
321
|
+
<div class="adm-blocked-reasons">
|
|
322
|
+
<strong>${t('dashboard.blocked')}:</strong>
|
|
323
|
+
${' '} ${t('dashboard.blockedCooldown')}: ${num((sd.mailbox_notif_blocked_by_type || {}).cooldown || 0)}
|
|
324
|
+
${' | '} ${t('dashboard.blockedQuietHours')}: ${num((sd.mailbox_notif_blocked_by_type || {}).quiet_hours || 0)}
|
|
325
|
+
${' | '} ${t('dashboard.blockedDisabled')}: ${num((sd.mailbox_notif_blocked_by_type || {}).disabled || 0)}
|
|
326
|
+
</div>
|
|
327
|
+
` : ''}
|
|
328
|
+
${sd.mailbox_notif_sent_by_type ? html`
|
|
329
|
+
<div class="adm-channels-summary">
|
|
330
|
+
<strong>${t('dashboard.channels')}:</strong>
|
|
331
|
+
${' '} ${t('dashboard.channelPush')}: ${num((sd.mailbox_notif_sent_by_type || {}).push || 0)} ${t('dashboard.breakdownSent').toLowerCase()}, ${num((sd.mailbox_notif_failed_by_type || {}).push || 0)} ${t('dashboard.breakdownFailed').toLowerCase()}
|
|
332
|
+
${' | '} ${t('dashboard.channelEmail')}: ${num((sd.mailbox_notif_sent_by_type || {}).email || 0)} ${t('dashboard.breakdownSent').toLowerCase()}, ${num((sd.mailbox_notif_failed_by_type || {}).email || 0)} ${t('dashboard.breakdownFailed').toLowerCase()}
|
|
333
|
+
</div>
|
|
334
|
+
` : ''}
|
|
335
|
+
</div>
|
|
336
|
+
` : ''}
|
|
337
|
+
|
|
338
|
+
<!-- Consent Permission Stats -->
|
|
339
|
+
<h3 class="adm-mt-lg adm-text-sm adm-section-purple">${t('dashboard.consentPermStats')}</h3>
|
|
340
|
+
<div class="adm-grid adm-grid-4 adm-mt-md">
|
|
341
|
+
<${StatCard} label=${t('dashboard.consentActiveRules')} value=${cs.active_rules || 0} color="#a855f7" />
|
|
342
|
+
<${StatCard} label=${t('dashboard.consentByGaii')} value=${cs.by_gaii || 0} color="#3b82f6" />
|
|
343
|
+
<${StatCard} label=${t('dashboard.consentByGhii')} value=${cs.by_ghii || 0} color="#a855f7" />
|
|
344
|
+
<${StatCard} label=${t('dashboard.consentByOrganism')} value=${cs.by_organism || 0} color="#22c55e" />
|
|
345
|
+
</div>
|
|
346
|
+
<div class="adm-grid adm-grid-4">
|
|
347
|
+
<${StatCard} label=${t('dashboard.consentByDomain')} value=${cs.by_domain || 0} color="#eab308" />
|
|
348
|
+
<${StatCard} label=${t('dashboard.consentByNode')} value=${cs.by_node || 0} color="var(--text-dim)" />
|
|
349
|
+
<${StatCard} label=${t('dashboard.consentByWildcard')} value=${cs.by_wildcard || 0} color=${cs.by_wildcard > 0 ? '#ef4444' : '#22c55e'} />
|
|
350
|
+
<${StatCard} label=${t('dashboard.consentDataPatterns')} value=${cs.unique_patterns || 0} />
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
<!-- Security Stats -->
|
|
354
|
+
<h3 class="adm-mt-lg adm-text-sm adm-section-red">${t('dashboard.securityStats')}</h3>
|
|
355
|
+
<div class="adm-grid adm-grid-4 adm-mt-md">
|
|
356
|
+
<${StatCard} label=${t('dashboard.authFailures')} value=${sd.auth_failures_total || 0} color=${sd.auth_failures_total > 0 ? '#ef4444' : '#22c55e'} />
|
|
357
|
+
<${StatCard} label=${t('dashboard.rateLimitHits')} value=${sd.rate_limit_hits_total || 0} color=${sd.rate_limit_hits_total > 0 ? '#eab308' : '#22c55e'} />
|
|
358
|
+
<${StatCard} label=${t('dashboard.scopeDenials')} value=${sd.scope_denials_total || 0} color=${sd.scope_denials_total > 0 ? '#eab308' : '#22c55e'} />
|
|
359
|
+
</div>
|
|
360
|
+
`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* ── Chart rendering ── */
|
|
364
|
+
|
|
365
|
+
/** Load Chart.js from CDN and render all charts. */
|
|
366
|
+
async function renderAllCharts(sd, chartRefs) {
|
|
367
|
+
// Load Chart.js if not already available
|
|
368
|
+
if (!window.Chart) {
|
|
369
|
+
await new Promise(resolve => {
|
|
370
|
+
const s = document.createElement('script');
|
|
371
|
+
s.src = 'https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js';
|
|
372
|
+
s.onload = resolve;
|
|
373
|
+
document.head.appendChild(s);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
if (!window.Chart) return;
|
|
377
|
+
|
|
378
|
+
// Destroy previous chart instances
|
|
379
|
+
const refs = chartRefs.current;
|
|
380
|
+
for (const key of Object.keys(refs)) {
|
|
381
|
+
if (refs[key] && typeof refs[key].destroy === 'function') {
|
|
382
|
+
refs[key].destroy();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
chartRefs.current = {};
|
|
386
|
+
|
|
387
|
+
const daily = sd.daily || sd.daily_history || {};
|
|
388
|
+
const days = Object.keys(daily).sort().slice(-30);
|
|
389
|
+
|
|
390
|
+
const chartOpts = {
|
|
391
|
+
responsive: true,
|
|
392
|
+
plugins: { legend: { labels: { color: '#94a3b8', font: { size: 11 } } } },
|
|
393
|
+
scales: {
|
|
394
|
+
x: { ticks: { color: '#94a3b8', maxRotation: 45 }, grid: { color: '#334155' } },
|
|
395
|
+
y: { ticks: { color: '#94a3b8' }, grid: { color: '#334155' } },
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// Daily Activity bar chart
|
|
400
|
+
const dc = document.getElementById('chartDaily');
|
|
401
|
+
if (dc) {
|
|
402
|
+
refs.chartDaily = new Chart(dc, { type: 'bar', data: {
|
|
403
|
+
labels: days.map(d => d.slice(5)),
|
|
404
|
+
datasets: [
|
|
405
|
+
{ label: t('dashboard.requestsTotal') || 'Requests', data: days.map(d => (daily[d] || {}).requests_total || 0), backgroundColor: '#3b82f688' },
|
|
406
|
+
{ label: t('dashboard.writes') || 'Writes', data: days.map(d => (daily[d] || {}).memory_writes || 0), backgroundColor: '#22c55e88' },
|
|
407
|
+
{ label: t('dashboard.reads') || 'Reads', data: days.map(d => (daily[d] || {}).memory_reads || 0), backgroundColor: '#06b6d488' },
|
|
408
|
+
],
|
|
409
|
+
}, options: chartOpts });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Weekly Comparison line chart
|
|
413
|
+
const wc = document.getElementById('chartWeekly');
|
|
414
|
+
if (wc && days.length > 0) {
|
|
415
|
+
const weekLabels = [t('dashboard.weekMon'), t('dashboard.weekTue'), t('dashboard.weekWed'), t('dashboard.weekThu'), t('dashboard.weekFri'), t('dashboard.weekSat'), t('dashboard.weekSun')];
|
|
416
|
+
const getWeekData = (weeksAgo) => {
|
|
417
|
+
const data = new Array(7).fill(0);
|
|
418
|
+
const now = new Date();
|
|
419
|
+
for (let i = 0; i < 7; i++) {
|
|
420
|
+
const d = new Date(now);
|
|
421
|
+
d.setDate(d.getDate() - d.getDay() + i + 1 - (weeksAgo * 7));
|
|
422
|
+
const key = d.toISOString().slice(0, 10);
|
|
423
|
+
if (daily[key]) data[i] = daily[key].requests_total || 0;
|
|
424
|
+
}
|
|
425
|
+
return data;
|
|
426
|
+
};
|
|
427
|
+
refs.chartWeekly = new Chart(wc, { type: 'line', data: {
|
|
428
|
+
labels: weekLabels,
|
|
429
|
+
datasets: [
|
|
430
|
+
{ label: t('dashboard.thisWeek') || 'This week', data: getWeekData(0), borderColor: '#3b82f6', backgroundColor: '#3b82f622', fill: true },
|
|
431
|
+
{ label: t('dashboard.lastWeek') || 'Last week', data: getWeekData(1), borderColor: '#22c55e', backgroundColor: '#22c55e22', fill: true },
|
|
432
|
+
],
|
|
433
|
+
}, options: chartOpts });
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Monthly Trend line chart
|
|
437
|
+
const mc = document.getElementById('chartMonthly');
|
|
438
|
+
if (mc && days.length > 0) {
|
|
439
|
+
refs.chartMonthly = new Chart(mc, { type: 'line', data: {
|
|
440
|
+
labels: days.map(d => d.slice(5)),
|
|
441
|
+
datasets: [{ label: t('dashboard.requestsTotal') || 'Requests', data: days.map(d => (daily[d] || {}).requests_total || 0), borderColor: '#06b6d4', backgroundColor: '#06b6d422', fill: true }],
|
|
442
|
+
}, options: chartOpts });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Tunnel doughnut chart
|
|
446
|
+
const tc = document.getElementById('chartTunnel');
|
|
447
|
+
if (tc) {
|
|
448
|
+
const tunnel = sd.tunnel || {};
|
|
449
|
+
refs.chartTunnel = new Chart(tc, { type: 'doughnut', data: {
|
|
450
|
+
labels: [t('dashboard.tunnelMsgSent') || 'Sent', t('dashboard.tunnelMsgReceived') || 'Received', t('dashboard.tunnelDeliveryFails') || 'Rejected', t('dashboard.tunnelMailboxFallbacks') || 'Fallbacks'],
|
|
451
|
+
datasets: [{ data: [tunnel.messages_sent_total || 0, tunnel.messages_received_total || 0, tunnel.delivery_failures_total || 0, tunnel.mailbox_fallbacks_total || 0],
|
|
452
|
+
backgroundColor: ['#3b82f6', '#22c55e', '#ef4444', '#eab308'] }],
|
|
453
|
+
}, options: { responsive: true, plugins: { legend: { labels: { color: '#94a3b8' } } } } });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Email Daily bar chart
|
|
457
|
+
const ec = document.getElementById('chartEmailDaily');
|
|
458
|
+
if (ec && days.length > 0) {
|
|
459
|
+
refs.chartEmailDaily = new Chart(ec, { type: 'bar', data: {
|
|
460
|
+
labels: days.map(d => d.slice(5)),
|
|
461
|
+
datasets: [
|
|
462
|
+
{ label: t('dashboard.breakdownSent') || 'Sent', data: days.map(d => sumByPrefix(daily[d], 'email_sent')), backgroundColor: '#06b6d488' },
|
|
463
|
+
{ label: t('dashboard.breakdownFailed') || 'Failed', data: days.map(d => sumByPrefix(daily[d], 'email_failed')), backgroundColor: '#ef444488' },
|
|
464
|
+
{ label: t('dashboard.breakdownRetried') || 'Retried', data: days.map(d => sumByPrefix(daily[d], 'email_retried')), backgroundColor: '#eab30888' },
|
|
465
|
+
],
|
|
466
|
+
}, options: chartOpts });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Push Daily bar chart
|
|
470
|
+
const pc = document.getElementById('chartPushDaily');
|
|
471
|
+
if (pc && days.length > 0) {
|
|
472
|
+
refs.chartPushDaily = new Chart(pc, { type: 'bar', data: {
|
|
473
|
+
labels: days.map(d => d.slice(5)),
|
|
474
|
+
datasets: [
|
|
475
|
+
{ label: t('dashboard.breakdownSent') || 'Sent', data: days.map(d => sumByPrefix(daily[d], 'push_sent')), backgroundColor: '#8b5cf688' },
|
|
476
|
+
{ label: t('dashboard.breakdownFailed') || 'Failed', data: days.map(d => sumByPrefix(daily[d], 'push_failed')), backgroundColor: '#ef444488' },
|
|
477
|
+
],
|
|
478
|
+
}, options: chartOpts });
|
|
479
|
+
}
|
|
480
|
+
}
|