wiki-formant 0.99.0 → 0.100.0
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/README.md +2 -0
- package/dist/analytics.d.ts +11 -3
- package/dist/analytics.js +37 -13
- package/dist/chart.js +7 -4
- package/dist/react-server.d.ts +2 -0
- package/dist/react-server.js +1 -1
- package/dist/stats-figures.js +4 -3
- package/dist/stats.d.ts +4 -3
- package/dist/stats.js +6 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ The editor, history, sanitiser, chart and passkey modules bring their libraries
|
|
|
8
8
|
npm install wiki-formant
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
An agent can start a wiki for you, hosted on miow.me or in your own app, with the skill in `skills/wiki-formant`: `npx skills add tutmoses/wiki-formant`.
|
|
12
|
+
|
|
11
13
|
What stays the app's: its design system, its block types beyond the core, its connection, cache and auth. Everything else here is shared.
|
|
12
14
|
|
|
13
15
|
## API
|
package/dist/analytics.d.ts
CHANGED
|
@@ -78,10 +78,18 @@ export interface DigestOptions {
|
|
|
78
78
|
ownAgents?: string[];
|
|
79
79
|
/**
|
|
80
80
|
* Narrows every figure to the visits that pass, and the events to their
|
|
81
|
-
* visitors
|
|
82
|
-
* and starts at `counted_from`,
|
|
81
|
+
* visitors; an agent passes as `AGENT_TEST` has it. `ViewDay` keeps no
|
|
82
|
+
* visits, so a narrowed digest leaves it out and starts at `counted_from`,
|
|
83
|
+
* and has no `previous_visitors`.
|
|
83
84
|
*/
|
|
84
85
|
filter?: StatsFilter;
|
|
86
|
+
/**
|
|
87
|
+
* People and agents counted apart, as they are by default: the ranked lists
|
|
88
|
+
* count people. False counts agents in the pages, entry pages, sources,
|
|
89
|
+
* countries and devices too, as `visitors_incl_agents` counts them, so a
|
|
90
|
+
* stats page's lists add up to the visitors its chart draws.
|
|
91
|
+
*/
|
|
92
|
+
apart?: boolean;
|
|
85
93
|
}
|
|
86
94
|
type Ranked<K extends string, M extends string> = Array<Record<K, string> & Record<M, number>>;
|
|
87
95
|
/** One UTC day of the figures a digest totals; a visit falls on the day it began. */
|
|
@@ -160,7 +168,7 @@ export interface Digest {
|
|
|
160
168
|
* The window before is the same chain of views, visits and events a second
|
|
161
169
|
* time, `_p` on every name, so its figures are the headline's to the letter.
|
|
162
170
|
*/
|
|
163
|
-
export declare function digest(sql: Sql, { site, days, handle, siblings, gaps, ownAgents, filter }: DigestOptions): Promise<Digest>;
|
|
171
|
+
export declare function digest(sql: Sql, { site, days, handle, siblings, gaps, ownAgents, filter, apart }: DigestOptions): Promise<Digest>;
|
|
164
172
|
/**
|
|
165
173
|
* Props for an "MCP Call" event, read out of the JSON-RPC envelope.
|
|
166
174
|
*
|
package/dist/analytics.js
CHANGED
|
@@ -109,7 +109,9 @@ export function recordEvent(sql, { name, props = {}, url, headers, site = '' })
|
|
|
109
109
|
let path = null;
|
|
110
110
|
if (url)
|
|
111
111
|
path = normalisePath(new URL(url, 'http://x').pathname);
|
|
112
|
-
|
|
112
|
+
// An agent's country is where its requests came from, so the stats page's countries can count it.
|
|
113
|
+
const country = headers?.get('x-vercel-ip-country')?.slice(0, 2);
|
|
114
|
+
return sql('INSERT INTO "Event" ("siteId", name, path, props, visitor) VALUES ($1, $2, $3, $4::jsonb, $5)', site, name.slice(0, 64), path, JSON.stringify(country ? { ...props, country } : props), headers?.get('user-agent') ? await visitorOf(sql, site, headers) : null);
|
|
113
115
|
});
|
|
114
116
|
}
|
|
115
117
|
/** A browser-sent event's props: eight at most, each value a short string or a number. */
|
|
@@ -166,6 +168,21 @@ const VISIT_TEST = {
|
|
|
166
168
|
country: v => `bool_or(country = ${v})`,
|
|
167
169
|
device: v => `bool_or(device = ${v})`,
|
|
168
170
|
};
|
|
171
|
+
/** An agent's source and device in the lists, which no request of its carries. */
|
|
172
|
+
const AGENT_SOURCE = '(agent)';
|
|
173
|
+
const AGENT_DEVICE = 'agent';
|
|
174
|
+
/**
|
|
175
|
+
* Each filter as a test on one agent: its reads in the window are its visit,
|
|
176
|
+
* the first its entry page. Its source and device are the two above, and its
|
|
177
|
+
* country the one its requests came from, where they were counted with one.
|
|
178
|
+
*/
|
|
179
|
+
const AGENT_TEST = {
|
|
180
|
+
page: v => `bool_or(path = ${v})`,
|
|
181
|
+
entry: v => `(array_agg(path ORDER BY at))[1] = ${v}`,
|
|
182
|
+
source: v => `${v} = '${AGENT_SOURCE}'`,
|
|
183
|
+
country: v => `bool_or(country = ${v})`,
|
|
184
|
+
device: v => `${v} = '${AGENT_DEVICE}'`,
|
|
185
|
+
};
|
|
169
186
|
/**
|
|
170
187
|
* An agent's name from its event: the roster's label, else what its user agent
|
|
171
188
|
* calls itself, `compatible; X` or the first product token.
|
|
@@ -180,7 +197,7 @@ const AGENT_NAME = `coalesce(props->>'bot', substring(props->>'ua' from 'compati
|
|
|
180
197
|
* The window before is the same chain of views, visits and events a second
|
|
181
198
|
* time, `_p` on every name, so its figures are the headline's to the letter.
|
|
182
199
|
*/
|
|
183
|
-
export async function digest(sql, { site = '', days, handle, siblings = [], gaps = [SEARCH_GAPS], ownAgents = ['-mcp-test', 'radix-studio'], filter = {} }) {
|
|
200
|
+
export async function digest(sql, { site = '', days, handle, siblings = [], gaps = [SEARCH_GAPS], ownAgents = ['-mcp-test', 'radix-studio'], filter = {}, apart = true }) {
|
|
184
201
|
// The window is the database's clock, which stamped the rows; the app's can run behind it.
|
|
185
202
|
// All time is a century: further back than any row, and still an int to Postgres.
|
|
186
203
|
const values = [site, Number.isFinite(days) ? Math.max(1, Math.round(days)) : 36_500];
|
|
@@ -191,6 +208,9 @@ export async function digest(sql, { site = '', days, handle, siblings = [], gaps
|
|
|
191
208
|
/** Clicks on links to the site's own X account, among `e`. */
|
|
192
209
|
const follows = handle && `name = 'Outbound Link: Click' AND props->>'url' ILIKE ANY(${$([`%x.com/${handle}%`, `%twitter.com/${handle}%`])}::text[])`;
|
|
193
210
|
const tests = STATS_FILTERS.flatMap(k => (filter[k] ? [VISIT_TEST[k]($(filter[k]))] : []));
|
|
211
|
+
const agentTests = STATS_FILTERS.flatMap(k => (filter[k] ? [AGENT_TEST[k]($(filter[k]))] : []));
|
|
212
|
+
/** Agents among the rows of a ranked list, unless they are counted apart. */
|
|
213
|
+
const agentsToo = (select) => (apart ? '' : ` UNION ALL ${select}`);
|
|
194
214
|
const day = (at) => `to_char(${at} AT TIME ZONE 'UTC', 'YYYY-MM-DD')`;
|
|
195
215
|
/** The site's own agents, as a test on an event's user agent. */
|
|
196
216
|
const ours = `coalesce(props->>'ua', '') ILIKE ANY(${$(ownAgents.map(a => `%${a}%`))}::text[])`;
|
|
@@ -220,16 +240,20 @@ export async function digest(sql, { site = '', days, handle, siblings = [], gaps
|
|
|
220
240
|
FROM numbered${s} GROUP BY visitor, visit${tests.length ? ` HAVING ${tests.join(' AND ')}` : ''}
|
|
221
241
|
), v${s} AS (
|
|
222
242
|
${tests.length ? `SELECT n.id, n.path, n.source, n.country, n.device, n.visitor, n.at FROM numbered${s} n JOIN visits${s} USING (visitor, visit)` : `SELECT * FROM viewed${s}`}
|
|
243
|
+
), read${s} AS (
|
|
244
|
+
SELECT visitor, "createdAt" AS at, path, props->>'country' AS country, ${AGENT_NAME} AS agent FROM "Event"
|
|
245
|
+
WHERE "siteId" = $1 AND ${at} AND name IN ('AI Bot Visit', 'MCP Call') AND visitor IS NOT NULL AND NOT ${ours}
|
|
246
|
+
), ag${s} AS (
|
|
247
|
+
SELECT * FROM read${s}${agentTests.length ? ` WHERE visitor IN (SELECT visitor FROM read${s} GROUP BY 1 HAVING ${agentTests.join(' AND ')})` : ''}
|
|
223
248
|
), e${s} AS (
|
|
224
249
|
SELECT name, props, visitor, "createdAt" AS at FROM "Event" WHERE "siteId" = $1 AND ${at}
|
|
225
|
-
${tests.length ? `AND visitor IN (SELECT visitor FROM v${s})` : ''}
|
|
250
|
+
${tests.length ? `AND (visitor IN (SELECT visitor FROM v${s}) OR visitor IN (SELECT visitor FROM ag${s}))` : ''}
|
|
251
|
+
), acted${s} AS MATERIALIZED (
|
|
252
|
+
SELECT visitor, at FROM e${s} WHERE name NOT IN ('AI Bot Visit', 'MCP Call') AND visitor IS NOT NULL
|
|
226
253
|
), bounced${s} AS (
|
|
254
|
+
-- Against the few reader events alone: probing every agent event once per visit took seconds.
|
|
227
255
|
SELECT * FROM visits${s} b WHERE views = 1 AND NOT EXISTS (
|
|
228
|
-
SELECT 1 FROM
|
|
229
|
-
AND x.at >= b.at AND x.at < b.at + interval '30 minutes')
|
|
230
|
-
), ag${s} AS (
|
|
231
|
-
SELECT visitor, at, ${AGENT_NAME} AS agent FROM e${s}
|
|
232
|
-
WHERE name IN ('AI Bot Visit', 'MCP Call') AND visitor IS NOT NULL AND NOT ${ours}
|
|
256
|
+
SELECT 1 FROM acted${s} x WHERE x.visitor = b.visitor AND x.at >= b.at AND x.at < b.at + interval '30 minutes')
|
|
233
257
|
), a${s} AS (
|
|
234
258
|
SELECT * FROM "ViewDay" WHERE "siteId" = $1 AND day >= ${dayAgo(back + 1)}${back ? ` AND day < ${dayAgo(back)}` : ''}${tests.length ? ' AND false' : ''}
|
|
235
259
|
), sessions${s} AS (
|
|
@@ -250,19 +274,19 @@ export async function digest(sql, { site = '', days, handle, siblings = [], gaps
|
|
|
250
274
|
'visitors_incl_agents', (SELECT count(DISTINCT visitor) FROM (SELECT visitor FROM v${s} UNION SELECT visitor FROM ag${s}) u) + (SELECT visitors FROM archived${s})`;
|
|
251
275
|
const [row] = (await sql(`WITH ${chain('', 0)}, ${chain('_p', 1)}, src AS (
|
|
252
276
|
SELECT coalesce(source, '(none)') AS source, count(DISTINCT visitor) AS visitors FROM visits GROUP BY 1
|
|
253
|
-
UNION ALL SELECT key, value::int FROM a, jsonb_each_text(a.sources)
|
|
277
|
+
UNION ALL SELECT key, value::int FROM a, jsonb_each_text(a.sources)${agentsToo(`SELECT '${AGENT_SOURCE}', count(DISTINCT visitor) FROM ag HAVING count(*) > 0`)}
|
|
254
278
|
)
|
|
255
279
|
SELECT json_build_object(
|
|
256
280
|
${headline('')},
|
|
257
281
|
'previous', json_build_object(${headline('_p')}, 'follow_clicks', ${follows ? `(SELECT count(*) FROM e_p WHERE ${follows})` : 'NULL'}),
|
|
258
282
|
'counted_from', (SELECT ${day('min("createdAt")')} FROM "View" WHERE "siteId" = $1),
|
|
259
283
|
'top_sources', ${list('SELECT source, sum(visitors) AS visitors FROM src GROUP BY 1', 6)},
|
|
260
|
-
'top_pages', ${list(merged('page', 'pages',
|
|
261
|
-
'entry_pages', ${list(merged('page', 'entryPages',
|
|
284
|
+
'top_pages', ${list(merged('page', 'pages', `SELECT path, count(DISTINCT visitor) FROM v GROUP BY 1${agentsToo('SELECT path, count(DISTINCT visitor) FROM ag WHERE path IS NOT NULL GROUP BY 1')}`), 6)},
|
|
285
|
+
'entry_pages', ${list(merged('page', 'entryPages', `SELECT entry, count(DISTINCT visitor) FROM visits GROUP BY 1${agentsToo('SELECT entry, count(*) FROM (SELECT (array_agg(path ORDER BY at))[1] AS entry FROM ag GROUP BY visitor) f WHERE entry IS NOT NULL GROUP BY 1')}`), 6)},
|
|
262
286
|
'from_posts', ${list(`SELECT entry AS page, count(DISTINCT visitor) AS visitors FROM visits WHERE source = 'x' GROUP BY 1`, 15)},
|
|
263
287
|
'sibling_referrals', ${list(`SELECT source, sum(visitors) AS visitors FROM src WHERE source = ANY(${$(siblings)}::text[]) GROUP BY 1`, 6)},
|
|
264
|
-
'countries', ${list(merged('country', 'countries',
|
|
265
|
-
'devices', ${list(merged('device', 'devices',
|
|
288
|
+
'countries', ${list(merged('country', 'countries', `SELECT country, count(DISTINCT visitor) FROM v WHERE country IS NOT NULL GROUP BY 1${agentsToo('SELECT country, count(DISTINCT visitor) FROM ag WHERE country IS NOT NULL GROUP BY 1')}`), 10)},
|
|
289
|
+
'devices', ${list(merged('device', 'devices', `SELECT device, count(DISTINCT visitor) FROM v GROUP BY 1${agentsToo(`SELECT '${AGENT_DEVICE}', count(DISTINCT visitor) FROM ag HAVING count(*) > 0`)}`))},
|
|
266
290
|
'by_day', ${list(`SELECT date, sum(visitors) AS visitors, sum(pageviews) AS pageviews, sum(visits) AS visits, sum(bounces) AS bounces,
|
|
267
291
|
sum(seconds) AS seconds, sum(agents) AS agents, sum(everyone) AS visitors_incl_agents, sum(follows) AS follow_clicks FROM (
|
|
268
292
|
SELECT ${day('at')}, count(DISTINCT visitor), count(*), 0, 0, 0, 0, 0, 0 FROM v GROUP BY 1
|
package/dist/chart.js
CHANGED
|
@@ -60,21 +60,24 @@ function bucketStart(time, step) {
|
|
|
60
60
|
* Points, oldest first, one per step. A price keeps the last value in each
|
|
61
61
|
* step, as a candle does its close: the first open, the highest high, the
|
|
62
62
|
* lowest low. A count is the total of its days; a rate, their mean. Volume is
|
|
63
|
-
* summed.
|
|
63
|
+
* summed. The first step sits on its first point when the points start inside
|
|
64
|
+
* it, so the axis never reaches back past the days that were counted.
|
|
64
65
|
*
|
|
65
66
|
* @internal
|
|
66
67
|
*/
|
|
67
68
|
export function bucketChart(points, step, aggregate) {
|
|
68
69
|
const out = [];
|
|
69
70
|
let n = 0;
|
|
71
|
+
let bucket;
|
|
70
72
|
for (const p of points) {
|
|
71
73
|
const start = bucketStart(p.time, step);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
if (bucket !== start) {
|
|
75
|
+
out.push({ ...p, time: out.length ? start : p.time });
|
|
76
|
+
bucket = start;
|
|
75
77
|
n = 1;
|
|
76
78
|
continue;
|
|
77
79
|
}
|
|
80
|
+
const last = out.at(-1);
|
|
78
81
|
if (aggregate === 'last')
|
|
79
82
|
last.value = p.value;
|
|
80
83
|
else if (aggregate === 'sum')
|
package/dist/react-server.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface WikiLinkProps {
|
|
|
22
22
|
'aria-checked'?: boolean;
|
|
23
23
|
/** The name of a link drawn as an icon. */
|
|
24
24
|
'aria-label'?: string;
|
|
25
|
+
/** False where following the link costs the server a query, as a stats page's links do; a plain anchor ignores it. */
|
|
26
|
+
prefetch?: boolean;
|
|
25
27
|
children?: ReactNode;
|
|
26
28
|
}
|
|
27
29
|
export type WikiLinkComponent = ComponentType<WikiLinkProps>;
|
package/dist/react-server.js
CHANGED
|
@@ -29,7 +29,7 @@ import { formatDay } from './freshness.js';
|
|
|
29
29
|
* Exported because `block-views` renders links too and had declared the same
|
|
30
30
|
* three lines; a default that exists twice is a default that can diverge.
|
|
31
31
|
*/
|
|
32
|
-
export const Anchor = (props) => _jsx("a", { ...props });
|
|
32
|
+
export const Anchor = ({ prefetch: _, ...props }) => _jsx("a", { ...props });
|
|
33
33
|
const pagesPhrase = (n) => `${n} page${n === 1 ? '' : 's'}`;
|
|
34
34
|
/**
|
|
35
35
|
* The section's second axis, as pressable chips.
|
package/dist/stats-figures.js
CHANGED
|
@@ -79,8 +79,9 @@ const EMPTY = { date: '', visitors: 0, pageviews: 0, visits: 0, bounces: 0, seco
|
|
|
79
79
|
*/
|
|
80
80
|
export function StatsFigures({ digest, first, apart, controls }) {
|
|
81
81
|
const figures = useMemo(() => figuresOf(digest, apart), [digest, apart]);
|
|
82
|
-
|
|
83
|
-
const
|
|
82
|
+
// By name, since a window or the agents switch can arrive without a reload and the figures shift.
|
|
83
|
+
const [chosen, choose] = useState(figures[0].label);
|
|
84
|
+
const figure = figures.find(f => f.label === chosen) ?? figures[0];
|
|
84
85
|
const byDay = useMemo(() => new Map(digest.by_day.map(r => [r.date, r])), [digest.by_day]);
|
|
85
86
|
const series = useMemo(() => {
|
|
86
87
|
const today = Math.floor(Date.now() / 1000 / DAY);
|
|
@@ -90,7 +91,7 @@ export function StatsFigures({ digest, first, apart, controls }) {
|
|
|
90
91
|
return value == null ? null : { time, value };
|
|
91
92
|
}).filter(p => p !== null);
|
|
92
93
|
}, [byDay, first, figure]);
|
|
93
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "stats-figures", role: "group", "aria-label": "Chart", children: figures.map(
|
|
94
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "stats-figures", role: "group", "aria-label": "Chart", children: figures.map(f => (_jsxs("button", { type: "button", "aria-pressed": f === figure, onClick: () => choose(f.label), children: [_jsx("span", { children: f.label }), _jsxs("strong", { children: [f.value, f.delta && (_jsx("small", { className: "stats-delta", "data-better": f.delta.better, children: f.delta.text }))] })] }, f.label))) }), _jsx(TimeChart, { series: series, label: figure.label, ranges: ['all'], aggregate: figure.aggregate ?? 'sum', fromZero: true, format: figure.format, controls: controls, className: "stats-chart" })] }));
|
|
94
95
|
}
|
|
95
96
|
async function post(endpoint, body) {
|
|
96
97
|
const res = await fetch(endpoint, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
package/dist/stats.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface StatsProps {
|
|
|
13
13
|
apart?: boolean;
|
|
14
14
|
/** The windows offered as `?days=` links on the same page. */
|
|
15
15
|
ranges?: readonly number[];
|
|
16
|
-
/** Every link on the page: `next/link`, say, so a window or a filter does not reload it. */
|
|
16
|
+
/** Every link on the page: `next/link`, say, so a window or a filter does not reload it. Each is given `prefetch={false}`. */
|
|
17
17
|
link?: WikiLinkComponent;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
@@ -23,14 +23,15 @@ export interface StatsProps {
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function statsQuery(params: Record<string, string | string[] | undefined>, ranges?: readonly number[]): {
|
|
25
25
|
days: number;
|
|
26
|
-
filter: Partial<Record<"page" | "
|
|
26
|
+
filter: Partial<Record<"page" | "country" | "entry" | "source" | "device", string>>;
|
|
27
27
|
apart: boolean;
|
|
28
28
|
};
|
|
29
29
|
/**
|
|
30
30
|
* A site's own analytics on one page: the headline figures against the window
|
|
31
31
|
* before, visitors counting agents unless `apart`, whichever of them is chosen per day, and every ranked list the
|
|
32
32
|
* digest carries, each row a link that narrows the page to it. Lists with
|
|
33
|
-
* nothing in them are left out. The
|
|
33
|
+
* nothing in them are left out. The digest is made with the same `apart`, so
|
|
34
|
+
* the lists count the visitors the chart draws. The chart and the bars are `currentColor`
|
|
34
35
|
* until the importing stylesheet gives them a colour.
|
|
35
36
|
*/
|
|
36
37
|
export declare function Stats({ digest: d, days, filter, apart, ranges, link: Link }: StatsProps): import("react").JSX.Element;
|
package/dist/stats.js
CHANGED
|
@@ -22,6 +22,7 @@ export function statsQuery(params, ranges = STATS_RANGES) {
|
|
|
22
22
|
if (typeof v === 'string' && v)
|
|
23
23
|
filter[k] = v.slice(0, 500);
|
|
24
24
|
}
|
|
25
|
+
// `apart` is `digest`'s option too, so its lists count the visitors the chart draws.
|
|
25
26
|
return { days: statsDays(params.days, ranges), filter, apart: params.agents === 'apart' };
|
|
26
27
|
}
|
|
27
28
|
/** The page at a window, a filter and a count of agents, as a query string. */
|
|
@@ -49,13 +50,14 @@ function StatsList({ title, rows, label = v => v, to, link: Link, }) {
|
|
|
49
50
|
if (!rows.length)
|
|
50
51
|
return null;
|
|
51
52
|
const max = Math.max(...rows.map(([, n]) => n));
|
|
52
|
-
return (_jsxs("section", { className: "stats-list", children: [_jsx("h2", { children: title }), _jsx("ol", { children: rows.map(([v, n]) => (_jsxs("li", { style: { '--share': n / max }, children: [_jsx("span", { children: to ? _jsx(Link, { href: to(v), children: label(v) }) : label(v) }), _jsx("span", { className: "stats-count", children: count(n) })] }, v))) })] }));
|
|
53
|
+
return (_jsxs("section", { className: "stats-list", children: [_jsx("h2", { children: title }), _jsx("ol", { children: rows.map(([v, n]) => (_jsxs("li", { style: { '--share': n / max }, children: [_jsx("span", { children: to ? (_jsx(Link, { href: to(v), prefetch: false, children: label(v) })) : (label(v)) }), _jsx("span", { className: "stats-count", children: count(n) })] }, v))) })] }));
|
|
53
54
|
}
|
|
54
55
|
/**
|
|
55
56
|
* A site's own analytics on one page: the headline figures against the window
|
|
56
57
|
* before, visitors counting agents unless `apart`, whichever of them is chosen per day, and every ranked list the
|
|
57
58
|
* digest carries, each row a link that narrows the page to it. Lists with
|
|
58
|
-
* nothing in them are left out. The
|
|
59
|
+
* nothing in them are left out. The digest is made with the same `apart`, so
|
|
60
|
+
* the lists count the visitors the chart draws. The chart and the bars are `currentColor`
|
|
59
61
|
* until the importing stylesheet gives them a colour.
|
|
60
62
|
*/
|
|
61
63
|
export function Stats({ digest: d, days, filter = {}, apart = false, ranges = STATS_RANGES, link: Link = Anchor }) {
|
|
@@ -74,8 +76,8 @@ export function Stats({ digest: d, days, filter = {}, apart = false, ranges = ST
|
|
|
74
76
|
const to = (k) => (v) => at({ ...filter, [k]: v });
|
|
75
77
|
// Each window is another digest, so a link rather than a button, pinned
|
|
76
78
|
// with the chart's own controls, and so is the switch that counts agents apart.
|
|
77
|
-
const controls = (_jsxs(_Fragment, { children: [_jsx("div", { role: "group", "aria-label": "Window", children: ranges.map(n => (_jsx(Link, { href: at(filter, n), "aria-current": n === days ? 'page' : undefined, children: windowLabel(n) }, n))) }), _jsx("div", { children: _jsx(Link, { href: at(filter, days, !apart), role: "switch", "aria-checked": !apart, "aria-label": "Agents counted among visitors", title: "Agents counted among visitors", children: _jsxs("svg", { className: "chart-icon", viewBox: "0 0 12 12", "aria-hidden": true, children: [_jsx("path", { d: "M6 1.5v2M1 6.5v2M11 6.5v2", stroke: "currentColor" }), _jsx("rect", { x: "2.5", y: "3.5", width: "7", height: "7", rx: "1.5", fill: "none", stroke: "currentColor" }), _jsx("circle", { cx: "4.75", cy: "7", r: "0.9", fill: "currentColor" }), _jsx("circle", { cx: "7.25", cy: "7", r: "0.9", fill: "currentColor" })] }) }) })] }));
|
|
78
|
-
return (_jsxs("div", { className: "stats", children: [narrowed.length > 0 && (_jsxs("div", { className: "stats-filters", children: [_jsx("ul", { "aria-label": "Narrowed to", children: narrowed.map(k => (_jsx("li", { children: _jsxs(Link, { href: at({ ...filter, [k]: undefined }), title: "Remove", children: [NAMES[k], ": ", k === 'country' ? country(filter[k]) : filter[k], " \u00D7"] }) }, k))) }), start < counted && d.counted_from && (_jsxs("p", { children: ["Narrowed figures start on ", d.counted_from, ", the first day this site counted its own visits."] }))] })), _jsx(StatsFigures, { digest: d, first: first, apart: apart, controls: controls }), _jsxs("div", { className: "stats-lists", children: [_jsx(StatsList, { link: Link, title: "Pages", rows: rows(d.top_pages, 'page', 'visitors'), to: to('page') }), _jsx(StatsList, { link: Link, title: "Entry pages", rows: rows(d.entry_pages, 'page', 'visitors'), to: to('entry') }), _jsx(StatsList, { link: Link, title: "Sources", rows: rows(d.top_sources, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { link: Link, title: "From X posts", rows: rows(d.from_posts, 'page', 'visitors'), to: v => at({ ...filter, source: 'x', entry: v }) }), _jsx(StatsList, { link: Link, title: "Sibling sites", rows: rows(d.sibling_referrals, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { link: Link, title: "Countries", rows: rows(d.countries, 'country', 'visitors'), label: country, to: to('country') }), _jsx(StatsList, { link: Link, title: "Devices", rows: rows(d.devices, 'device', 'visitors'), to: to('device') }), _jsx(StatsList, { link: Link, title: "Agents", rows: rows(d.top_agents, 'agent', 'visitors') }), _jsx(StatsList, { link: Link, title: "Agent tool calls", rows: rows(d.agent_tool_calls, 'tool', 'events') }), _jsx(StatsList, { link: Link, title: "Searches that found nothing", rows: rows(d.gaps, 'query', 'events') })] })] }));
|
|
79
|
+
const controls = (_jsxs(_Fragment, { children: [_jsx("div", { role: "group", "aria-label": "Window", children: ranges.map(n => (_jsx(Link, { href: at(filter, n), "aria-current": n === days ? 'page' : undefined, prefetch: false, children: windowLabel(n) }, n))) }), _jsx("div", { children: _jsx(Link, { href: at(filter, days, !apart), role: "switch", "aria-checked": !apart, "aria-label": "Agents counted among visitors", title: "Agents counted among visitors", prefetch: false, children: _jsxs("svg", { className: "chart-icon", viewBox: "0 0 12 12", "aria-hidden": true, children: [_jsx("path", { d: "M6 1.5v2M1 6.5v2M11 6.5v2", stroke: "currentColor" }), _jsx("rect", { x: "2.5", y: "3.5", width: "7", height: "7", rx: "1.5", fill: "none", stroke: "currentColor" }), _jsx("circle", { cx: "4.75", cy: "7", r: "0.9", fill: "currentColor" }), _jsx("circle", { cx: "7.25", cy: "7", r: "0.9", fill: "currentColor" })] }) }) })] }));
|
|
80
|
+
return (_jsxs("div", { className: "stats", children: [narrowed.length > 0 && (_jsxs("div", { className: "stats-filters", children: [_jsx("ul", { "aria-label": "Narrowed to", children: narrowed.map(k => (_jsx("li", { children: _jsxs(Link, { href: at({ ...filter, [k]: undefined }), title: "Remove", prefetch: false, children: [NAMES[k], ": ", k === 'country' ? country(filter[k]) : filter[k], " \u00D7"] }) }, k))) }), start < counted && d.counted_from && (_jsxs("p", { children: ["Narrowed figures start on ", d.counted_from, ", the first day this site counted its own visits."] }))] })), _jsx(StatsFigures, { digest: d, first: first, apart: apart, controls: controls }), _jsxs("div", { className: "stats-lists", children: [_jsx(StatsList, { link: Link, title: "Pages", rows: rows(d.top_pages, 'page', 'visitors'), to: to('page') }), _jsx(StatsList, { link: Link, title: "Entry pages", rows: rows(d.entry_pages, 'page', 'visitors'), to: to('entry') }), _jsx(StatsList, { link: Link, title: "Sources", rows: rows(d.top_sources, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { link: Link, title: "From X posts", rows: rows(d.from_posts, 'page', 'visitors'), to: v => at({ ...filter, source: 'x', entry: v }) }), _jsx(StatsList, { link: Link, title: "Sibling sites", rows: rows(d.sibling_referrals, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { link: Link, title: "Countries", rows: rows(d.countries, 'country', 'visitors'), label: country, to: to('country') }), _jsx(StatsList, { link: Link, title: "Devices", rows: rows(d.devices, 'device', 'visitors'), to: to('device') }), _jsx(StatsList, { link: Link, title: "Agents", rows: rows(d.top_agents, 'agent', 'visitors') }), _jsx(StatsList, { link: Link, title: "Agent tool calls", rows: rows(d.agent_tool_calls, 'tool', 'events') }), _jsx(StatsList, { link: Link, title: "Searches that found nothing", rows: rows(d.gaps, 'query', 'events') })] })] }));
|
|
79
81
|
}
|
|
80
82
|
/**
|
|
81
83
|
* A stats page's door: `children` and a sign-out button for a signed-in
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-formant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.100.0",
|
|
4
4
|
"description": "The portable half of a wiki: derived taxonomy and facet controls, a version-negotiating MCP transport, markdown twins, block rendering, a rich-text editor engine, conditional-GET plumbing, and the headers and CSP every origin sends.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|