astro-tractstack 2.2.4 → 2.2.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.2.4",
3
+ "version": "2.2.6",
4
4
  "description": "Astro integration for TractStack - the free web press by At Risk Media",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,6 +8,7 @@ import DashboardActivity from './Dashboard_Activity';
8
8
  import SankeyDiagram from '../codehooks/SankeyDiagram';
9
9
  import EpinetDurationSelector from '../codehooks/EpinetDurationSelector';
10
10
  import FetchAnalytics from './state/FetchAnalytics';
11
+ import type { PulseMetrics, SystemMetrics } from './state/FetchAnalytics';
11
12
  import type { FullContentMapItem } from '@/types/tractstack';
12
13
 
13
14
  interface StoryKeepDashboardAnalyticsProps {
@@ -15,12 +16,16 @@ interface StoryKeepDashboardAnalyticsProps {
15
16
  initializing?: boolean;
16
17
  }
17
18
 
18
- // Helper component for error boundary
19
+ interface ErrorBoundaryProps {
20
+ children: ReactNode;
21
+ fallback: ReactNode;
22
+ }
23
+
19
24
  class ErrorBoundary extends Component<
20
- { children: ReactNode; fallback: ReactNode },
25
+ ErrorBoundaryProps,
21
26
  { hasError: boolean }
22
27
  > {
23
- constructor(props: any) {
28
+ constructor(props: ErrorBoundaryProps) {
24
29
  super(props);
25
30
  this.state = { hasError: false };
26
31
  }
@@ -37,6 +42,70 @@ class ErrorBoundary extends Component<
37
42
  }
38
43
  }
39
44
 
45
+ // Types derived from Dashboard_Activity.tsx
46
+ interface ActivitySeries {
47
+ id: string;
48
+ data: Array<{ x: any; y: number }>;
49
+ }
50
+
51
+ interface DashboardData {
52
+ stats: {
53
+ daily: number;
54
+ weekly: number;
55
+ monthly: number;
56
+ };
57
+ line: ActivitySeries[];
58
+ dailyAnonymous: number;
59
+ dailyKnown: number;
60
+ weeklyAnonymous: number;
61
+ weeklyKnown: number;
62
+ monthlyAnonymous: number;
63
+ monthlyKnown: number;
64
+ status?: string;
65
+ }
66
+
67
+ interface LeadsData {
68
+ totalLeads: number;
69
+ status?: string;
70
+ }
71
+
72
+ // Types derived from SankeyDiagram.tsx
73
+ interface SankeyNode {
74
+ name: string;
75
+ id: string;
76
+ }
77
+
78
+ interface SankeyLink {
79
+ source: number;
80
+ target: number;
81
+ value: number;
82
+ }
83
+
84
+ interface EpinetData {
85
+ nodes: SankeyNode[];
86
+ links: SankeyLink[];
87
+ status?: string;
88
+ }
89
+
90
+ // Types derived from EpinetDurationSelector.tsx usage
91
+ interface UserCountItem {
92
+ id: string;
93
+ count: number;
94
+ }
95
+
96
+ interface AnalyticsState {
97
+ dashboard: DashboardData | null;
98
+ leads: LeadsData | null;
99
+ epinet: EpinetData | null;
100
+ userCounts: UserCountItem[];
101
+ hourlyNodeActivity: Record<string, number>;
102
+ pulse: PulseMetrics | null;
103
+ system: SystemMetrics | null;
104
+ isLoading: boolean;
105
+ status: string;
106
+ error: string | null;
107
+ }
108
+
40
109
  const DurationSelector = ({
41
110
  currentDurationHelper,
42
111
  setStandardDuration,
@@ -83,22 +152,14 @@ export default function StoryKeepDashboard_Analytics({
83
152
  const $epinetCustomFilters = useStore(epinetCustomFilters);
84
153
  const [isDownloading, setIsDownloading] = useState(false);
85
154
 
86
- // Analytics data state
87
- const [analytics, setAnalytics] = useState<{
88
- dashboard: any;
89
- leads: any;
90
- epinet: any;
91
- userCounts: any[];
92
- hourlyNodeActivity: any;
93
- isLoading: boolean;
94
- status: string;
95
- error: string | null;
96
- }>({
155
+ const [analytics, setAnalytics] = useState<AnalyticsState>({
97
156
  dashboard: null,
98
157
  leads: null,
99
158
  epinet: null,
100
159
  userCounts: [],
101
160
  hourlyNodeActivity: {},
161
+ pulse: null,
162
+ system: null,
102
163
  isLoading: false,
103
164
  status: 'idle',
104
165
  error: null,
@@ -128,7 +189,6 @@ export default function StoryKeepDashboard_Analytics({
128
189
  });
129
190
  };
130
191
 
131
- // Duration helper for UI
132
192
  const currentDurationHelper = useMemo(():
133
193
  | 'daily'
134
194
  | 'weekly'
@@ -151,7 +211,6 @@ export default function StoryKeepDashboard_Analytics({
151
211
  return 'monthly';
152
212
  }, [$epinetCustomFilters.startTimeUTC, $epinetCustomFilters.endTimeUTC]);
153
213
 
154
- // Standard duration setter helper
155
214
  const setStandardDuration = useCallback(
156
215
  (newValue: 'daily' | 'weekly' | 'monthly') => {
157
216
  const nowUTC = new Date();
@@ -171,7 +230,6 @@ export default function StoryKeepDashboard_Analytics({
171
230
  [$epinetCustomFilters]
172
231
  );
173
232
 
174
- // Download leads CSV
175
233
  const downloadLeadsCSV = async () => {
176
234
  if (isDownloading) return;
177
235
  try {
@@ -212,12 +270,10 @@ export default function StoryKeepDashboard_Analytics({
212
270
  }
213
271
  };
214
272
 
215
- // Helper function for number formatting
216
273
  const formatNumber = (num: number) => {
217
274
  return new Intl.NumberFormat().format(num);
218
275
  };
219
276
 
220
- // Prepare stats data for display
221
277
  const stats = [
222
278
  {
223
279
  name: 'Past 24 Hours',
@@ -263,6 +319,72 @@ export default function StoryKeepDashboard_Analytics({
263
319
  </div>
264
320
  )}
265
321
 
322
+ {/* Live Operations Row */}
323
+ <div className="mb-6 grid grid-cols-1 gap-4 md:grid-cols-3">
324
+ {/* Card 1: Active Visitors */}
325
+ <div className="rounded-lg border border-gray-100 bg-white px-4 py-3 shadow-sm transition-colors hover:border-cyan-100">
326
+ <div className="flex items-center gap-2">
327
+ <span className="relative flex h-2.5 w-2.5">
328
+ <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
329
+ <span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-green-500"></span>
330
+ </span>
331
+ <dt className="text-sm font-bold text-gray-800">Active Visitors</dt>
332
+ </div>
333
+ <dd className="mt-2">
334
+ <div className="text-2xl font-bold tracking-tight text-cyan-700">
335
+ {analytics.pulse
336
+ ? formatNumber(analytics.pulse.activeVisitors)
337
+ : '-'}
338
+ </div>
339
+ </dd>
340
+ </div>
341
+
342
+ {/* Card 2: Traffic Velocity */}
343
+ <div className="rounded-lg border border-gray-100 bg-white px-4 py-3 shadow-sm transition-colors hover:border-cyan-100">
344
+ <dt className="text-sm font-bold text-gray-800">Traffic Velocity</dt>
345
+ <dd className="mt-2">
346
+ <div className="text-2xl font-bold tracking-tight text-cyan-700">
347
+ {analytics.pulse ? formatNumber(analytics.pulse.velocity) : '-'}{' '}
348
+ <span className="text-sm font-normal text-gray-500">/ Hr</span>
349
+ </div>
350
+ <div className="mt-1 text-xs text-gray-600">
351
+ Events (Last Full Hour)
352
+ </div>
353
+ </dd>
354
+ </div>
355
+
356
+ {/* Card 3: System Health */}
357
+ <div className="rounded-lg border border-gray-100 bg-white px-4 py-3 shadow-sm transition-colors hover:border-cyan-100">
358
+ <dt className="text-sm font-bold text-gray-800">System Health</dt>
359
+ <dd className="mt-2">
360
+ {analytics.system ? (
361
+ <>
362
+ <div className="flex items-baseline gap-2">
363
+ <span
364
+ className={`text-2xl font-bold tracking-tight ${analytics.system.waitCount > 0 ? 'text-yellow-600' : 'text-green-600'}`}
365
+ >
366
+ {analytics.system.waitCount > 0 ? 'Busy' : 'Healthy'}
367
+ </span>
368
+ </div>
369
+ <div className="mt-1 text-xs text-gray-600">
370
+ DB Load:{' '}
371
+ {analytics.system.maxOpenConns > 0
372
+ ? Math.round(
373
+ (analytics.system.openConns /
374
+ analytics.system.maxOpenConns) *
375
+ 100
376
+ )
377
+ : 0}
378
+ %
379
+ </div>
380
+ </>
381
+ ) : (
382
+ <div className="text-2xl font-bold text-gray-300">-</div>
383
+ )}
384
+ </dd>
385
+ </div>
386
+ </div>
387
+
266
388
  {/* Stats Cards Grid */}
267
389
  <div className="mb-6 grid grid-cols-3 gap-4">
268
390
  {stats.map((item) => {
@@ -305,7 +427,6 @@ export default function StoryKeepDashboard_Analytics({
305
427
  <hr className="my-1.5 border-gray-100 md:my-3.5" />
306
428
 
307
429
  <dd>
308
- {/* Desktop: side-by-side layout */}
309
430
  <div className="hidden items-end justify-between md:flex">
310
431
  <div className="flex-1">
311
432
  <div className="text-sm text-gray-600">
@@ -327,7 +448,6 @@ export default function StoryKeepDashboard_Analytics({
327
448
  </div>
328
449
  </div>
329
450
 
330
- {/* Mobile: stacked layout */}
331
451
  <div className="md:hidden">
332
452
  <div className="mb-1.5">
333
453
  <div className="text-sm text-gray-600">
@@ -353,7 +473,6 @@ export default function StoryKeepDashboard_Analytics({
353
473
  );
354
474
  })}
355
475
 
356
- {/* Total Leads Card */}
357
476
  <div className="col-span-3 rounded-lg border border-gray-100 bg-white px-4 py-3 shadow-sm transition-colors hover:border-cyan-100">
358
477
  <div className="flex items-center justify-between">
359
478
  <dt className="text-sm font-bold text-gray-800">Total Leads</dt>
@@ -381,13 +500,11 @@ export default function StoryKeepDashboard_Analytics({
381
500
  </div>
382
501
  </div>
383
502
 
384
- {/* Duration Selector */}
385
503
  <DurationSelector
386
504
  currentDurationHelper={currentDurationHelper}
387
505
  setStandardDuration={setStandardDuration}
388
506
  />
389
507
 
390
- {/* Dashboard Activity Chart */}
391
508
  <div className="mb-6 overflow-hidden">
392
509
  <h3 className="mb-4 text-lg font-bold text-gray-900">
393
510
  Activity Over Time
@@ -412,12 +529,10 @@ export default function StoryKeepDashboard_Analytics({
412
529
  )}
413
530
  </div>
414
531
 
415
- {/* Separator */}
416
532
  <div className="my-8">
417
533
  <hr className="border-gray-200" />
418
534
  </div>
419
535
 
420
- {/* User Journey Section */}
421
536
  <div className="mb-6 overflow-visible">
422
537
  <h3 className="mb-4 text-lg font-bold text-gray-900">
423
538
  User Journey Analytics
@@ -466,7 +581,6 @@ export default function StoryKeepDashboard_Analytics({
466
581
  analytics.isLoading || analytics.status === 'loading'
467
582
  }
468
583
  hourlyNodeActivity={analytics.hourlyNodeActivity}
469
- // MODIFICATION: Read availableFilters from the store, not local state
470
584
  availableFilters={$epinetCustomFilters.availableFilters}
471
585
  appliedFilters={$epinetCustomFilters.appliedFilters}
472
586
  onBeliefFilterChange={handleBeliefFilterChange}
@@ -485,7 +599,6 @@ export default function StoryKeepDashboard_Analytics({
485
599
  analytics.isLoading || analytics.status === 'loading'
486
600
  }
487
601
  hourlyNodeActivity={analytics.hourlyNodeActivity}
488
- // MODIFICATION: Read availableFilters from the store, not local state
489
602
  availableFilters={$epinetCustomFilters.availableFilters}
490
603
  appliedFilters={$epinetCustomFilters.appliedFilters}
491
604
  onBeliefFilterChange={handleBeliefFilterChange}
@@ -502,7 +615,6 @@ export default function StoryKeepDashboard_Analytics({
502
615
  fullContentMap={fullContentMap}
503
616
  isLoading={analytics.isLoading || analytics.status === 'loading'}
504
617
  hourlyNodeActivity={analytics.hourlyNodeActivity}
505
- // MODIFICATION: Read availableFilters from the store, not local state
506
618
  availableFilters={$epinetCustomFilters.availableFilters}
507
619
  appliedFilters={$epinetCustomFilters.appliedFilters}
508
620
  onBeliefFilterChange={handleBeliefFilterChange}
@@ -5,12 +5,29 @@ import { TractStackAPI } from '@/utils/api';
5
5
 
6
6
  const VERBOSE = false;
7
7
 
8
+ export interface PulseMetrics {
9
+ activeVisitors: number;
10
+ velocity: number;
11
+ }
12
+
13
+ export interface SystemMetrics {
14
+ available: boolean;
15
+ openConns: number;
16
+ inUse: number;
17
+ idle: number;
18
+ waitCount: number;
19
+ waitDuration: string;
20
+ maxOpenConns: number;
21
+ }
22
+
8
23
  interface AnalyticsState {
9
24
  dashboard: any;
10
25
  leads: any;
11
26
  epinet: any;
12
27
  userCounts: any[];
13
28
  hourlyNodeActivity: any;
29
+ pulse: PulseMetrics | null;
30
+ system: SystemMetrics | null;
14
31
  isLoading: boolean;
15
32
  status: string;
16
33
  error: string | null;
@@ -127,7 +144,6 @@ class AnalyticsService {
127
144
  if (filters.selectedUserId)
128
145
  params.append('userId', filters.selectedUserId);
129
146
 
130
- // MODIFICATION: Properly format appliedFilters for the backend
131
147
  if (filters.appliedFilters && filters.appliedFilters.length > 0) {
132
148
  params.append('appliedFilters', JSON.stringify(filters.appliedFilters));
133
149
  }
@@ -145,6 +161,8 @@ class AnalyticsService {
145
161
  epinet: null,
146
162
  userCounts: [],
147
163
  hourlyNodeActivity: {},
164
+ pulse: null,
165
+ system: null,
148
166
  isLoading: true,
149
167
  status: 'loading',
150
168
  error: null,
@@ -153,13 +171,31 @@ class AnalyticsService {
153
171
  const api = new TractStackAPI(
154
172
  window.TRACTSTACK_CONFIG?.tenantId || 'default'
155
173
  );
174
+
156
175
  const endpoint = `/api/v1/analytics/all${params.toString() ? `?${params.toString()}` : ''}`;
157
- if (VERBOSE) console.log('🔥 Making API request', { endpoint });
158
176
 
159
- const response = await api.get(endpoint);
160
- if (!response.success)
161
- throw new Error(response.error || 'Failed to fetch analytics data');
162
- const data = response.data;
177
+ if (VERBOSE) {
178
+ console.log('🔥 Making API requests', {
179
+ analytics: endpoint,
180
+ pulse: '/api/v1/admin/pulse',
181
+ system: '/api/v1/admin/db/stats',
182
+ });
183
+ }
184
+
185
+ const [analyticsResponse, pulseResponse, systemResponse] =
186
+ await Promise.all([
187
+ api.get(endpoint),
188
+ api.get('/api/v1/admin/pulse'),
189
+ api.get('/api/v1/admin/db/stats'),
190
+ ]);
191
+
192
+ if (!analyticsResponse.success) {
193
+ throw new Error(
194
+ analyticsResponse.error || 'Failed to fetch analytics data'
195
+ );
196
+ }
197
+
198
+ const data = analyticsResponse.data;
163
199
 
164
200
  const isStillLoading =
165
201
  data?.status === 'loading' ||
@@ -173,12 +209,15 @@ class AnalyticsService {
173
209
 
174
210
  if (isStillLoading) {
175
211
  if (VERBOSE) console.log('⏳ Backend data still loading, will poll...');
212
+
176
213
  const partialAnalytics = {
177
214
  dashboard: data.dashboard,
178
215
  leads: data.leads,
179
216
  epinet: data.epinet,
180
217
  userCounts: data.userCounts || [],
181
218
  hourlyNodeActivity: data.hourlyNodeActivity || {},
219
+ pulse: pulseResponse.success ? pulseResponse.data?.pulse : null,
220
+ system: systemResponse.success ? systemResponse.data : null,
182
221
  status: 'loading',
183
222
  error: null,
184
223
  isLoading: true,
@@ -203,6 +242,8 @@ class AnalyticsService {
203
242
  epinet: data.epinet,
204
243
  userCounts: data.userCounts || [],
205
244
  hourlyNodeActivity: data.hourlyNodeActivity || {},
245
+ pulse: pulseResponse.success ? pulseResponse.data?.pulse : null,
246
+ system: systemResponse.success ? systemResponse.data : null,
206
247
  status: 'complete',
207
248
  error: null,
208
249
  isLoading: false,
@@ -211,13 +252,12 @@ class AnalyticsService {
211
252
  this.setCachedResponse(cacheKey, analyticsData);
212
253
  onUpdate(analyticsData);
213
254
 
214
- // MODIFICATION: Correctly extract top-level availableFilters and update the store
215
255
  epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
216
256
  ...filters,
217
257
  availableFilters: data.availableFilters || [],
218
258
  });
219
259
 
220
- if (VERBOSE) console.log('✅ Analytics request completed successfully');
260
+ if (VERBOSE) console.log('✅ Analytics requests completed successfully');
221
261
  this.activeRequest = null;
222
262
  } catch (error) {
223
263
  if (error instanceof Error && error.name === 'AbortError') {
@@ -231,6 +271,8 @@ class AnalyticsService {
231
271
  epinet: null,
232
272
  userCounts: [],
233
273
  hourlyNodeActivity: {},
274
+ pulse: null,
275
+ system: null,
234
276
  status: 'error',
235
277
  error:
236
278
  error instanceof Error ? error.message : 'Unknown error occurred',