@byline/host-tanstack-start 6.0.1 → 6.0.2

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.
@@ -6,7 +6,7 @@ import { useTranslation } from "@byline/i18n/react";
6
6
  import { z } from "zod";
7
7
  import { BreadcrumbsClient } from "../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js";
8
8
  import { useNavigate } from "../admin-shell/chrome/loose-router.js";
9
- import { getAnalyticsCountries, getAnalyticsReferrers, getAnalyticsReportCoverage, getAnalyticsRuntime, getAnalyticsSummary, getAnalyticsTop } from "../server-fns/analytics/index.js";
9
+ import { getAnalyticsCountries, getAnalyticsDashboardPreference, getAnalyticsReferrers, getAnalyticsReportCoverage, getAnalyticsRuntime, getAnalyticsSummary, getAnalyticsTop, setAnalyticsDashboardPreference } from "../server-fns/analytics/index.js";
10
10
  import { getAdminRoutePath } from "./admin-path.js";
11
11
  import { buildAnalyticsDashboardRange } from "./analytics-range.js";
12
12
  const periodSchema = z.preprocess((value)=>{
@@ -21,13 +21,19 @@ function createAdminAnalyticsRoute(path) {
21
21
  const Route = createFileRoute(path)({
22
22
  validateSearch: searchSchema,
23
23
  loaderDeps: ({ search })=>({
24
- period: search.period ?? 30
24
+ period: search.period
25
25
  }),
26
26
  loader: async ({ deps })=>{
27
27
  const runtime = await getAnalyticsRuntime();
28
28
  if (!runtime.enabled) throw notFound();
29
- const coverage = await getAnalyticsReportCoverage();
30
- const range = buildAnalyticsDashboardRange(deps.period, new Date(), coverage.summaryFrom);
29
+ const [coverage, preference] = await Promise.all([
30
+ getAnalyticsReportCoverage(),
31
+ null == deps.period ? getAnalyticsDashboardPreference() : Promise.resolve({
32
+ period: null
33
+ })
34
+ ]);
35
+ const period = deps.period ?? preference.period ?? 30;
36
+ const range = buildAnalyticsDashboardRange(period, new Date(), coverage.summaryFrom);
31
37
  const [summary, pages, downloads, referrers, countries] = await Promise.all([
32
38
  getAnalyticsSummary({
33
39
  data: range
@@ -66,7 +72,7 @@ function createAdminAnalyticsRoute(path) {
66
72
  range,
67
73
  coverage
68
74
  },
69
- period: deps.period
75
+ period
70
76
  };
71
77
  },
72
78
  component: function() {
@@ -109,6 +115,13 @@ function createAdminAnalyticsRoute(path) {
109
115
  period: nextPeriod
110
116
  }
111
117
  });
118
+ setAnalyticsDashboardPreference({
119
+ data: {
120
+ period: nextPeriod
121
+ }
122
+ }).catch((err)=>{
123
+ console.warn('analytics dashboard preference save failed:', err);
124
+ });
112
125
  }
113
126
  })
114
127
  ]
@@ -6,4 +6,5 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  export { type DeleteAnalyticsEventsInput, deleteAnalyticsEvents, rebuildAnalyticsDay, } from './maintenance.js';
9
+ export { getAnalyticsDashboardPreference, setAnalyticsDashboardPreference, } from './preferences.js';
9
10
  export { type AnalyticsRuntimeState, type AnalyticsTopInput, getAnalyticsCountries, getAnalyticsReferrers, getAnalyticsReportCoverage, getAnalyticsRuntime, getAnalyticsSummary, getAnalyticsTop, } from './queries.js';
@@ -1,2 +1,3 @@
1
1
  export { deleteAnalyticsEvents, rebuildAnalyticsDay } from "./maintenance.js";
2
+ export { getAnalyticsDashboardPreference, setAnalyticsDashboardPreference } from "./preferences.js";
2
3
  export { getAnalyticsCountries, getAnalyticsReferrers, getAnalyticsReportCoverage, getAnalyticsRuntime, getAnalyticsSummary, getAnalyticsTop } from "./queries.js";
@@ -0,0 +1,36 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { AnalyticsDashboardPeriod } from '@byline/admin/analytics';
9
+ export interface SetAnalyticsDashboardPreferenceInput {
10
+ period: AnalyticsDashboardPeriod;
11
+ }
12
+ /**
13
+ * The stored period, or `null` when the user has never chosen one (or
14
+ * this host has no admin store).
15
+ *
16
+ * Never throws: a preference read must not be able to take the
17
+ * dashboard down. A headless context, an unauthenticated preview, or a
18
+ * database hiccup logs and yields `null`, and the caller falls back to
19
+ * the built-in default — the same posture the collection list's
20
+ * preference read takes.
21
+ */
22
+ export declare const getAnalyticsDashboardPreference: import("@tanstack/react-start").OptionalFetcher<readonly [import("@tanstack/react-start").FunctionMiddlewareAfterServer<{}, unknown, undefined, undefined, {
23
+ bylineExpectedSessionId: string | null;
24
+ }, undefined, undefined>], undefined, Promise<{
25
+ period: AnalyticsDashboardPeriod | null;
26
+ }>>;
27
+ /**
28
+ * Persist the chosen period. Fire-and-forget from the dashboard: the
29
+ * navigation has already happened, so a failed save must never toast,
30
+ * block, or roll anything back.
31
+ */
32
+ export declare const setAnalyticsDashboardPreference: import("@tanstack/react-start").RequiredFetcher<readonly [import("@tanstack/react-start").FunctionMiddlewareAfterServer<{}, unknown, undefined, undefined, {
33
+ bylineExpectedSessionId: string | null;
34
+ }, undefined, undefined>], (input: SetAnalyticsDashboardPreferenceInput) => SetAnalyticsDashboardPreferenceInput, Promise<{
35
+ ok: true;
36
+ }>>;
@@ -0,0 +1,60 @@
1
+ import { createServerFn } from "@tanstack/react-start";
2
+ import { getPreferenceCommand, setPreferenceCommand } from "@byline/admin/admin-preferences";
3
+ import { isAnalyticsDashboardPeriod } from "@byline/analytics/config";
4
+ import { getAdminRequestContext } from "@byline/client/server";
5
+ import { getLogger } from "@byline/core";
6
+ import { bylineCore } from "../../integrations/byline-core.js";
7
+ import { adminSessionMiddleware } from "../../integrations/session-middleware.js";
8
+ const ANALYTICS_DASHBOARD_SCOPE = 'analytics.dashboard';
9
+ const getAnalyticsDashboardPreference = createServerFn({
10
+ method: 'GET'
11
+ }).middleware([
12
+ adminSessionMiddleware
13
+ ]).handler(async ()=>{
14
+ const adminStore = bylineCore().adminStore;
15
+ if (null == adminStore) return {
16
+ period: null
17
+ };
18
+ try {
19
+ const context = await getAdminRequestContext();
20
+ const result = await getPreferenceCommand(context, {
21
+ scope: ANALYTICS_DASHBOARD_SCOPE
22
+ }, {
23
+ store: adminStore
24
+ });
25
+ const period = result.value?.period;
26
+ return {
27
+ period: isAnalyticsDashboardPeriod(period) ? period : null
28
+ };
29
+ } catch (err) {
30
+ getLogger().warn({
31
+ err
32
+ }, 'analytics dashboard preference read failed — using default');
33
+ return {
34
+ period: null
35
+ };
36
+ }
37
+ });
38
+ const setAnalyticsDashboardPreference = createServerFn({
39
+ method: 'POST'
40
+ }).middleware([
41
+ adminSessionMiddleware
42
+ ]).validator((input)=>input).handler(async ({ data })=>{
43
+ const adminStore = bylineCore().adminStore;
44
+ if (null == adminStore) return {
45
+ ok: true
46
+ };
47
+ const context = await getAdminRequestContext();
48
+ await setPreferenceCommand(context, {
49
+ scope: ANALYTICS_DASHBOARD_SCOPE,
50
+ value: {
51
+ period: data.period
52
+ }
53
+ }, {
54
+ store: adminStore
55
+ });
56
+ return {
57
+ ok: true
58
+ };
59
+ });
60
+ export { getAnalyticsDashboardPreference, setAnalyticsDashboardPreference };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "private": false,
4
4
  "type": "module",
5
5
  "license": "MPL-2.0",
6
- "version": "6.0.1",
6
+ "version": "6.0.2",
7
7
  "engines": {
8
8
  "node": ">=20.9.0"
9
9
  },
@@ -125,15 +125,15 @@
125
125
  "react-json-view-lite": "^2.5.0",
126
126
  "react-swipeable": "^7.0.2",
127
127
  "zod": "^4.6.2",
128
- "@byline/analytics-agent": "6.0.1",
129
- "@byline/admin": "6.0.1",
130
- "@byline/auth": "6.0.1",
131
- "@byline/analytics": "6.0.1",
132
- "@byline/ai": "6.0.1",
133
- "@byline/client": "6.0.1",
134
- "@byline/core": "6.0.1",
135
- "@byline/ui": "6.0.1",
136
- "@byline/i18n": "6.0.1"
128
+ "@byline/admin": "6.0.2",
129
+ "@byline/auth": "6.0.2",
130
+ "@byline/analytics": "6.0.2",
131
+ "@byline/analytics-agent": "6.0.2",
132
+ "@byline/client": "6.0.2",
133
+ "@byline/ai": "6.0.2",
134
+ "@byline/i18n": "6.0.2",
135
+ "@byline/core": "6.0.2",
136
+ "@byline/ui": "6.0.2"
137
137
  },
138
138
  "peerDependencies": {
139
139
  "@tanstack/react-router": "^1.167.0",
@@ -21,11 +21,13 @@ import { BreadcrumbsClient } from '../admin-shell/chrome/breadcrumbs/breadcrumbs
21
21
  import { useNavigate } from '../admin-shell/chrome/loose-router.js'
22
22
  import {
23
23
  getAnalyticsCountries,
24
+ getAnalyticsDashboardPreference,
24
25
  getAnalyticsReferrers,
25
26
  getAnalyticsReportCoverage,
26
27
  getAnalyticsRuntime,
27
28
  getAnalyticsSummary,
28
29
  getAnalyticsTop,
30
+ setAnalyticsDashboardPreference,
29
31
  } from '../server-fns/analytics/index.js'
30
32
  import { getAdminRoutePath } from './admin-path.js'
31
33
  import { buildAnalyticsDashboardRange } from './analytics-range.js'
@@ -47,15 +49,29 @@ interface AnalyticsSearch {
47
49
  export function createAdminAnalyticsRoute(path: string) {
48
50
  const Route: any = createFileRoute(path as never)({
49
51
  validateSearch: searchSchema,
52
+ // `period` stays possibly-undefined here rather than defaulting to 30:
53
+ // "no period in the URL" is a distinct state from "30 in the URL", and
54
+ // only the former defers to the user's stored preference. It is still a
55
+ // loader dep, so an explicit period change refetches.
50
56
  loaderDeps: ({ search }: { search: AnalyticsSearch }) => ({
51
- period: search.period ?? 30,
57
+ period: search.period,
52
58
  }),
53
- loader: async ({ deps }: { deps: { period: AnalyticsDashboardPeriod } }) => {
59
+ loader: async ({ deps }: { deps: { period?: AnalyticsDashboardPeriod } }) => {
54
60
  const runtime = await getAnalyticsRuntime()
55
61
  if (!runtime.enabled) throw notFound()
56
62
 
57
- const coverage = await getAnalyticsReportCoverage()
58
- const range = buildAnalyticsDashboardRange(deps.period, new Date(), coverage.summaryFrom)
63
+ // Both are roundtrips the dashboard has to make before it can build a
64
+ // range, so the preference read rides along in parallel and costs no
65
+ // added latency. It is skipped entirely when the URL already decides.
66
+ const [coverage, preference] = await Promise.all([
67
+ getAnalyticsReportCoverage(),
68
+ deps.period == null ? getAnalyticsDashboardPreference() : Promise.resolve({ period: null }),
69
+ ])
70
+
71
+ // Precedence, matching the collection list: explicit URL param →
72
+ // stored per-user preference → the built-in default.
73
+ const period: AnalyticsDashboardPeriod = deps.period ?? preference.period ?? 30
74
+ const range = buildAnalyticsDashboardRange(period, new Date(), coverage.summaryFrom)
59
75
  const [summary, pages, downloads, referrers, countries] = await Promise.all([
60
76
  getAnalyticsSummary({ data: range }),
61
77
  // Fetch exactly the preview depth each card shows; the full list is
@@ -76,7 +92,9 @@ export function createAdminAnalyticsRoute(path: string) {
76
92
  range,
77
93
  coverage,
78
94
  } satisfies AnalyticsDashboardData,
79
- period: deps.period,
95
+ // The *effective* period, so a params-less landing still highlights
96
+ // the period that actually ordered the data.
97
+ period,
80
98
  }
81
99
  },
82
100
  component: function AdminAnalyticsComponent() {
@@ -116,6 +134,14 @@ export function createAdminAnalyticsRoute(path: string) {
116
134
  to: getAdminRoutePath('analytics'),
117
135
  search: { period: nextPeriod },
118
136
  })
137
+ // Quietly make the choice sticky for next session. Fire-and-
138
+ // forget: the navigation already happened, so a failed save
139
+ // must never toast, block, or roll anything back.
140
+ setAnalyticsDashboardPreference({ data: { period: nextPeriod } }).catch(
141
+ (err: unknown) => {
142
+ console.warn('analytics dashboard preference save failed:', err)
143
+ }
144
+ )
119
145
  }}
120
146
  />
121
147
  </>
@@ -11,6 +11,10 @@ export {
11
11
  deleteAnalyticsEvents,
12
12
  rebuildAnalyticsDay,
13
13
  } from './maintenance.js'
14
+ export {
15
+ getAnalyticsDashboardPreference,
16
+ setAnalyticsDashboardPreference,
17
+ } from './preferences.js'
14
18
  export {
15
19
  type AnalyticsRuntimeState,
16
20
  type AnalyticsTopInput,
@@ -0,0 +1,99 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ /**
10
+ * Read and persist the signed-in admin user's sticky analytics
11
+ * dashboard period.
12
+ *
13
+ * The dashboard keeps its period in the URL (`?period=90`), so a shared
14
+ * link still opens exactly as sent; this preference only supplies the
15
+ * period when the URL carries none. Same precedence the collection list
16
+ * uses for page size and sort — explicit params win, the stored
17
+ * preference fills in, the built-in default is last.
18
+ *
19
+ * The scope is a singleton (`analytics.dashboard`), unlike the
20
+ * collection list's per-path scopes, so there is nothing to validate
21
+ * the way `set-list-view-preference` validates a collection path.
22
+ *
23
+ * Self-service: the target user is always the authenticated actor.
24
+ */
25
+
26
+ import { createServerFn } from '@tanstack/react-start'
27
+
28
+ import { getPreferenceCommand, setPreferenceCommand } from '@byline/admin/admin-preferences'
29
+ import type { AnalyticsDashboardPeriod } from '@byline/admin/analytics'
30
+ import { isAnalyticsDashboardPeriod } from '@byline/analytics/config'
31
+ import { getAdminRequestContext } from '@byline/client/server'
32
+ import { getLogger } from '@byline/core'
33
+
34
+ import { bylineCore } from '../../integrations/byline-core.js'
35
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
36
+
37
+ const ANALYTICS_DASHBOARD_SCOPE = 'analytics.dashboard'
38
+
39
+ export interface SetAnalyticsDashboardPreferenceInput {
40
+ period: AnalyticsDashboardPeriod
41
+ }
42
+
43
+ /**
44
+ * The stored period, or `null` when the user has never chosen one (or
45
+ * this host has no admin store).
46
+ *
47
+ * Never throws: a preference read must not be able to take the
48
+ * dashboard down. A headless context, an unauthenticated preview, or a
49
+ * database hiccup logs and yields `null`, and the caller falls back to
50
+ * the built-in default — the same posture the collection list's
51
+ * preference read takes.
52
+ */
53
+ export const getAnalyticsDashboardPreference = createServerFn({ method: 'GET' })
54
+ .middleware([adminSessionMiddleware])
55
+ .handler(async (): Promise<{ period: AnalyticsDashboardPeriod | null }> => {
56
+ const adminStore = bylineCore().adminStore
57
+ if (adminStore == null) return { period: null }
58
+
59
+ try {
60
+ const context = await getAdminRequestContext()
61
+ const result = await getPreferenceCommand(
62
+ context,
63
+ { scope: ANALYTICS_DASHBOARD_SCOPE },
64
+ { store: adminStore }
65
+ )
66
+ const period = (result.value as { period?: unknown } | null)?.period
67
+ // Re-validated on the way out, not just on the way in: a row
68
+ // written before a period was retired from the dashboard must
69
+ // degrade to the default rather than reach `buildAnalyticsDashboardRange`.
70
+ return { period: isAnalyticsDashboardPeriod(period) ? period : null }
71
+ } catch (err) {
72
+ getLogger().warn({ err }, 'analytics dashboard preference read failed — using default')
73
+ return { period: null }
74
+ }
75
+ })
76
+
77
+ /**
78
+ * Persist the chosen period. Fire-and-forget from the dashboard: the
79
+ * navigation has already happened, so a failed save must never toast,
80
+ * block, or roll anything back.
81
+ */
82
+ export const setAnalyticsDashboardPreference = createServerFn({ method: 'POST' })
83
+ .middleware([adminSessionMiddleware])
84
+ .validator((input: SetAnalyticsDashboardPreferenceInput) => input)
85
+ .handler(async ({ data }) => {
86
+ const adminStore = bylineCore().adminStore
87
+ if (adminStore == null) {
88
+ // Headless hosts without an admin store have no preference storage —
89
+ // the save is a silent no-op, mirroring set-locale's posture.
90
+ return { ok: true as const }
91
+ }
92
+ const context = await getAdminRequestContext()
93
+ await setPreferenceCommand(
94
+ context,
95
+ { scope: ANALYTICS_DASHBOARD_SCOPE, value: { period: data.period } },
96
+ { store: adminStore }
97
+ )
98
+ return { ok: true as const }
99
+ })