astro-tractstack 2.2.3 → 2.2.5

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.3",
3
+ "version": "2.2.5",
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,77 @@ 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
+ <div className="mt-1 text-xs text-gray-600">
340
+ {analytics.pulse
341
+ ? `${formatNumber(analytics.pulse.activeLeads)} Leads, ${formatNumber(analytics.pulse.activeGuests)} Guests`
342
+ : 'Live sessions'}
343
+ </div>
344
+ </dd>
345
+ </div>
346
+
347
+ {/* Card 2: Traffic Velocity */}
348
+ <div className="rounded-lg border border-gray-100 bg-white px-4 py-3 shadow-sm transition-colors hover:border-cyan-100">
349
+ <dt className="text-sm font-bold text-gray-800">Traffic Velocity</dt>
350
+ <dd className="mt-2">
351
+ <div className="text-2xl font-bold tracking-tight text-cyan-700">
352
+ {analytics.pulse ? formatNumber(analytics.pulse.velocity) : '-'}{' '}
353
+ <span className="text-sm font-normal text-gray-500">/ Hr</span>
354
+ </div>
355
+ <div className="mt-1 text-xs text-gray-600">
356
+ Events (Last Full Hour)
357
+ </div>
358
+ </dd>
359
+ </div>
360
+
361
+ {/* Card 3: System Health */}
362
+ <div className="rounded-lg border border-gray-100 bg-white px-4 py-3 shadow-sm transition-colors hover:border-cyan-100">
363
+ <dt className="text-sm font-bold text-gray-800">System Health</dt>
364
+ <dd className="mt-2">
365
+ {analytics.system ? (
366
+ <>
367
+ <div className="flex items-baseline gap-2">
368
+ <span
369
+ className={`text-2xl font-bold tracking-tight ${analytics.system.waitCount > 0 ? 'text-yellow-600' : 'text-green-600'}`}
370
+ >
371
+ {analytics.system.waitCount > 0 ? 'Busy' : 'Healthy'}
372
+ </span>
373
+ </div>
374
+ <div className="mt-1 text-xs text-gray-600">
375
+ DB Load:{' '}
376
+ {analytics.system.maxOpenConns > 0
377
+ ? Math.round(
378
+ (analytics.system.openConns /
379
+ analytics.system.maxOpenConns) *
380
+ 100
381
+ )
382
+ : 0}
383
+ %
384
+ </div>
385
+ </>
386
+ ) : (
387
+ <div className="text-2xl font-bold text-gray-300">-</div>
388
+ )}
389
+ </dd>
390
+ </div>
391
+ </div>
392
+
266
393
  {/* Stats Cards Grid */}
267
394
  <div className="mb-6 grid grid-cols-3 gap-4">
268
395
  {stats.map((item) => {
@@ -305,7 +432,6 @@ export default function StoryKeepDashboard_Analytics({
305
432
  <hr className="my-1.5 border-gray-100 md:my-3.5" />
306
433
 
307
434
  <dd>
308
- {/* Desktop: side-by-side layout */}
309
435
  <div className="hidden items-end justify-between md:flex">
310
436
  <div className="flex-1">
311
437
  <div className="text-sm text-gray-600">
@@ -327,7 +453,6 @@ export default function StoryKeepDashboard_Analytics({
327
453
  </div>
328
454
  </div>
329
455
 
330
- {/* Mobile: stacked layout */}
331
456
  <div className="md:hidden">
332
457
  <div className="mb-1.5">
333
458
  <div className="text-sm text-gray-600">
@@ -353,7 +478,6 @@ export default function StoryKeepDashboard_Analytics({
353
478
  );
354
479
  })}
355
480
 
356
- {/* Total Leads Card */}
357
481
  <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
482
  <div className="flex items-center justify-between">
359
483
  <dt className="text-sm font-bold text-gray-800">Total Leads</dt>
@@ -381,13 +505,11 @@ export default function StoryKeepDashboard_Analytics({
381
505
  </div>
382
506
  </div>
383
507
 
384
- {/* Duration Selector */}
385
508
  <DurationSelector
386
509
  currentDurationHelper={currentDurationHelper}
387
510
  setStandardDuration={setStandardDuration}
388
511
  />
389
512
 
390
- {/* Dashboard Activity Chart */}
391
513
  <div className="mb-6 overflow-hidden">
392
514
  <h3 className="mb-4 text-lg font-bold text-gray-900">
393
515
  Activity Over Time
@@ -412,12 +534,10 @@ export default function StoryKeepDashboard_Analytics({
412
534
  )}
413
535
  </div>
414
536
 
415
- {/* Separator */}
416
537
  <div className="my-8">
417
538
  <hr className="border-gray-200" />
418
539
  </div>
419
540
 
420
- {/* User Journey Section */}
421
541
  <div className="mb-6 overflow-visible">
422
542
  <h3 className="mb-4 text-lg font-bold text-gray-900">
423
543
  User Journey Analytics
@@ -466,7 +586,6 @@ export default function StoryKeepDashboard_Analytics({
466
586
  analytics.isLoading || analytics.status === 'loading'
467
587
  }
468
588
  hourlyNodeActivity={analytics.hourlyNodeActivity}
469
- // MODIFICATION: Read availableFilters from the store, not local state
470
589
  availableFilters={$epinetCustomFilters.availableFilters}
471
590
  appliedFilters={$epinetCustomFilters.appliedFilters}
472
591
  onBeliefFilterChange={handleBeliefFilterChange}
@@ -485,7 +604,6 @@ export default function StoryKeepDashboard_Analytics({
485
604
  analytics.isLoading || analytics.status === 'loading'
486
605
  }
487
606
  hourlyNodeActivity={analytics.hourlyNodeActivity}
488
- // MODIFICATION: Read availableFilters from the store, not local state
489
607
  availableFilters={$epinetCustomFilters.availableFilters}
490
608
  appliedFilters={$epinetCustomFilters.appliedFilters}
491
609
  onBeliefFilterChange={handleBeliefFilterChange}
@@ -502,7 +620,6 @@ export default function StoryKeepDashboard_Analytics({
502
620
  fullContentMap={fullContentMap}
503
621
  isLoading={analytics.isLoading || analytics.status === 'loading'}
504
622
  hourlyNodeActivity={analytics.hourlyNodeActivity}
505
- // MODIFICATION: Read availableFilters from the store, not local state
506
623
  availableFilters={$epinetCustomFilters.availableFilters}
507
624
  appliedFilters={$epinetCustomFilters.appliedFilters}
508
625
  onBeliefFilterChange={handleBeliefFilterChange}
@@ -5,12 +5,31 @@ import { TractStackAPI } from '@/utils/api';
5
5
 
6
6
  const VERBOSE = false;
7
7
 
8
+ export interface PulseMetrics {
9
+ activeVisitors: number;
10
+ activeLeads: number;
11
+ activeGuests: number;
12
+ velocity: number;
13
+ }
14
+
15
+ export interface SystemMetrics {
16
+ available: boolean;
17
+ openConns: number;
18
+ inUse: number;
19
+ idle: number;
20
+ waitCount: number;
21
+ waitDuration: string;
22
+ maxOpenConns: number;
23
+ }
24
+
8
25
  interface AnalyticsState {
9
26
  dashboard: any;
10
27
  leads: any;
11
28
  epinet: any;
12
29
  userCounts: any[];
13
30
  hourlyNodeActivity: any;
31
+ pulse: PulseMetrics | null;
32
+ system: SystemMetrics | null;
14
33
  isLoading: boolean;
15
34
  status: string;
16
35
  error: string | null;
@@ -127,7 +146,6 @@ class AnalyticsService {
127
146
  if (filters.selectedUserId)
128
147
  params.append('userId', filters.selectedUserId);
129
148
 
130
- // MODIFICATION: Properly format appliedFilters for the backend
131
149
  if (filters.appliedFilters && filters.appliedFilters.length > 0) {
132
150
  params.append('appliedFilters', JSON.stringify(filters.appliedFilters));
133
151
  }
@@ -145,6 +163,8 @@ class AnalyticsService {
145
163
  epinet: null,
146
164
  userCounts: [],
147
165
  hourlyNodeActivity: {},
166
+ pulse: null,
167
+ system: null,
148
168
  isLoading: true,
149
169
  status: 'loading',
150
170
  error: null,
@@ -153,13 +173,31 @@ class AnalyticsService {
153
173
  const api = new TractStackAPI(
154
174
  window.TRACTSTACK_CONFIG?.tenantId || 'default'
155
175
  );
176
+
156
177
  const endpoint = `/api/v1/analytics/all${params.toString() ? `?${params.toString()}` : ''}`;
157
- if (VERBOSE) console.log('🔥 Making API request', { endpoint });
158
178
 
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;
179
+ if (VERBOSE) {
180
+ console.log('🔥 Making API requests', {
181
+ analytics: endpoint,
182
+ pulse: '/api/v1/admin/pulse',
183
+ system: '/api/v1/admin/db/stats',
184
+ });
185
+ }
186
+
187
+ const [analyticsResponse, pulseResponse, systemResponse] =
188
+ await Promise.all([
189
+ api.get(endpoint),
190
+ api.get('/api/v1/admin/pulse'),
191
+ api.get('/api/v1/admin/db/stats'),
192
+ ]);
193
+
194
+ if (!analyticsResponse.success) {
195
+ throw new Error(
196
+ analyticsResponse.error || 'Failed to fetch analytics data'
197
+ );
198
+ }
199
+
200
+ const data = analyticsResponse.data;
163
201
 
164
202
  const isStillLoading =
165
203
  data?.status === 'loading' ||
@@ -173,12 +211,15 @@ class AnalyticsService {
173
211
 
174
212
  if (isStillLoading) {
175
213
  if (VERBOSE) console.log('⏳ Backend data still loading, will poll...');
214
+
176
215
  const partialAnalytics = {
177
216
  dashboard: data.dashboard,
178
217
  leads: data.leads,
179
218
  epinet: data.epinet,
180
219
  userCounts: data.userCounts || [],
181
220
  hourlyNodeActivity: data.hourlyNodeActivity || {},
221
+ pulse: pulseResponse.success ? pulseResponse.data?.pulse : null,
222
+ system: systemResponse.success ? systemResponse.data : null,
182
223
  status: 'loading',
183
224
  error: null,
184
225
  isLoading: true,
@@ -203,6 +244,8 @@ class AnalyticsService {
203
244
  epinet: data.epinet,
204
245
  userCounts: data.userCounts || [],
205
246
  hourlyNodeActivity: data.hourlyNodeActivity || {},
247
+ pulse: pulseResponse.success ? pulseResponse.data?.pulse : null,
248
+ system: systemResponse.success ? systemResponse.data : null,
206
249
  status: 'complete',
207
250
  error: null,
208
251
  isLoading: false,
@@ -211,13 +254,12 @@ class AnalyticsService {
211
254
  this.setCachedResponse(cacheKey, analyticsData);
212
255
  onUpdate(analyticsData);
213
256
 
214
- // MODIFICATION: Correctly extract top-level availableFilters and update the store
215
257
  epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
216
258
  ...filters,
217
259
  availableFilters: data.availableFilters || [],
218
260
  });
219
261
 
220
- if (VERBOSE) console.log('✅ Analytics request completed successfully');
262
+ if (VERBOSE) console.log('✅ Analytics requests completed successfully');
221
263
  this.activeRequest = null;
222
264
  } catch (error) {
223
265
  if (error instanceof Error && error.name === 'AbortError') {
@@ -231,6 +273,8 @@ class AnalyticsService {
231
273
  epinet: null,
232
274
  userCounts: [],
233
275
  hourlyNodeActivity: {},
276
+ pulse: null,
277
+ system: null,
234
278
  status: 'error',
235
279
  error:
236
280
  error instanceof Error ? error.message : 'Unknown error occurred',
@@ -1,28 +1,17 @@
1
1
  import type { APIRoute } from '@/types/astro';
2
2
 
3
- export const POST: APIRoute = async ({ cookies }) => {
3
+ export const POST: APIRoute = async ({ cookies, url }) => {
4
4
  try {
5
- const goBackend =
6
- import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
7
- let rootDomain: string | undefined;
5
+ const isLocalhost =
6
+ url.hostname === 'localhost' || url.hostname === '127.0.0.1';
8
7
 
9
- try {
10
- const url = new URL(goBackend);
11
- // Only set domain for non-localhost to preserve local dev behavior
12
- if (url.hostname !== 'localhost' && url.hostname !== '127.0.0.1') {
13
- rootDomain = url.hostname;
14
- }
15
- } catch (e) {
16
- console.warn('Logout: Failed to parse backend URL for cookie domain', e);
17
- }
18
-
19
- // Determine the options ONCE to prevent overwriting
20
- const cookieOptions: any = { path: '/' };
21
- if (rootDomain) {
22
- cookieOptions.domain = rootDomain;
23
- }
8
+ const cookieOptions: any = {
9
+ path: '/',
10
+ secure: !isLocalhost,
11
+ httpOnly: true,
12
+ sameSite: 'lax',
13
+ };
24
14
 
25
- // Execute deletion with the single, correct configuration
26
15
  cookies.delete('admin_auth', cookieOptions);
27
16
  cookies.delete('editor_auth', cookieOptions);
28
17