@yourbright/emdash-analytics-plugin 0.1.2 → 0.1.4
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/admin.js +369 -95
- package/dist/index.js +192 -37
- package/package.json +1 -1
package/dist/admin.js
CHANGED
|
@@ -19,7 +19,7 @@ var ADMIN_ROUTES = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
// src/admin.tsx
|
|
22
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
22
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
23
23
|
var API_BASE = `/_emdash/api/plugins/${PLUGIN_ID}`;
|
|
24
24
|
var EMPTY_CONFIG = {
|
|
25
25
|
siteOrigin: "",
|
|
@@ -138,6 +138,19 @@ function Section({
|
|
|
138
138
|
children
|
|
139
139
|
] });
|
|
140
140
|
}
|
|
141
|
+
function InlineHeader({
|
|
142
|
+
title,
|
|
143
|
+
description,
|
|
144
|
+
actions
|
|
145
|
+
}) {
|
|
146
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 border-b border-border pb-4 md:flex-row md:items-end md:justify-between", children: [
|
|
147
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
148
|
+
/* @__PURE__ */ jsx("h2", { className: "text-xl font-semibold tracking-tight", children: title }),
|
|
149
|
+
description ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: description }) : null
|
|
150
|
+
] }),
|
|
151
|
+
actions ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: actions }) : null
|
|
152
|
+
] });
|
|
153
|
+
}
|
|
141
154
|
function StatCard({
|
|
142
155
|
label,
|
|
143
156
|
value,
|
|
@@ -149,6 +162,45 @@ function StatCard({
|
|
|
149
162
|
note ? /* @__PURE__ */ jsx("div", { className: "mt-1 text-xs text-muted-foreground", children: note }) : null
|
|
150
163
|
] });
|
|
151
164
|
}
|
|
165
|
+
function sectionFromLocation() {
|
|
166
|
+
if (typeof window === "undefined") return "dashboard";
|
|
167
|
+
const url = new URL(window.location.href);
|
|
168
|
+
const section = url.searchParams.get("section");
|
|
169
|
+
return section === "pages" || section === "settings" ? section : "dashboard";
|
|
170
|
+
}
|
|
171
|
+
function updateSectionInUrl(section) {
|
|
172
|
+
if (typeof window === "undefined") return;
|
|
173
|
+
const url = new URL(window.location.href);
|
|
174
|
+
if (section === "dashboard") {
|
|
175
|
+
url.searchParams.delete("section");
|
|
176
|
+
} else {
|
|
177
|
+
url.searchParams.set("section", section);
|
|
178
|
+
}
|
|
179
|
+
window.history.replaceState({}, "", `${url.pathname}${url.search}${url.hash}`);
|
|
180
|
+
}
|
|
181
|
+
function AnalyticsTabs({
|
|
182
|
+
section,
|
|
183
|
+
onChange
|
|
184
|
+
}) {
|
|
185
|
+
const tabs = [
|
|
186
|
+
{ key: "dashboard", label: "Dashboard" },
|
|
187
|
+
{ key: "pages", label: "Pages" },
|
|
188
|
+
{ key: "settings", label: "Settings" }
|
|
189
|
+
];
|
|
190
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: tabs.map((tab) => {
|
|
191
|
+
const active = tab.key === section;
|
|
192
|
+
return /* @__PURE__ */ jsx(
|
|
193
|
+
"button",
|
|
194
|
+
{
|
|
195
|
+
type: "button",
|
|
196
|
+
onClick: () => onChange(tab.key),
|
|
197
|
+
className: `rounded-full border px-3 py-2 text-sm font-medium transition ${active ? "border-foreground bg-foreground text-background" : "border-border bg-background text-foreground hover:bg-accent"}`,
|
|
198
|
+
children: tab.label
|
|
199
|
+
},
|
|
200
|
+
tab.key
|
|
201
|
+
);
|
|
202
|
+
}) });
|
|
203
|
+
}
|
|
152
204
|
function ErrorBanner({ message }) {
|
|
153
205
|
if (!message) return null;
|
|
154
206
|
return /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700", children: message });
|
|
@@ -273,7 +325,138 @@ function MetricTable({
|
|
|
273
325
|
] }, item.urlPath)) })
|
|
274
326
|
] }) });
|
|
275
327
|
}
|
|
276
|
-
function
|
|
328
|
+
function KpiDeltaCard({ metric }) {
|
|
329
|
+
return /* @__PURE__ */ jsx(
|
|
330
|
+
StatCard,
|
|
331
|
+
{
|
|
332
|
+
label: metric.label,
|
|
333
|
+
value: formatInteger(metric.current),
|
|
334
|
+
note: `vs prev ${formatSignedInteger(metric.delta)} (${formatInteger(metric.previous)})`
|
|
335
|
+
}
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
function TrendPanel({
|
|
339
|
+
title,
|
|
340
|
+
subtitle,
|
|
341
|
+
metrics,
|
|
342
|
+
trend
|
|
343
|
+
}) {
|
|
344
|
+
return /* @__PURE__ */ jsx(Section, { title, subtitle, children: /* @__PURE__ */ jsx("div", { className: "grid gap-4 lg:grid-cols-2", children: metrics.map((metric) => /* @__PURE__ */ jsx(TrendMetricCard, { metric, trend }, metric.key)) }) });
|
|
345
|
+
}
|
|
346
|
+
function TrendMetricCard({
|
|
347
|
+
metric,
|
|
348
|
+
trend
|
|
349
|
+
}) {
|
|
350
|
+
const data = trend.map((row) => ({
|
|
351
|
+
date: row.date,
|
|
352
|
+
value: metric.key === "gscClicks" ? row.gscClicks : metric.key === "gscImpressions" ? row.gscImpressions : metric.key === "gaViews" ? row.gaViews : metric.key === "gaUsers" ? row.gaUsers : row.gaSessions
|
|
353
|
+
}));
|
|
354
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-border bg-background p-4", children: [
|
|
355
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
|
|
356
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
357
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: metric.label }),
|
|
358
|
+
/* @__PURE__ */ jsx("div", { className: "mt-1 text-2xl font-semibold", children: formatInteger(metric.current) })
|
|
359
|
+
] }),
|
|
360
|
+
/* @__PURE__ */ jsx("div", { className: `text-sm font-medium ${metric.delta >= 0 ? "text-emerald-700" : "text-red-700"}`, children: formatSignedInteger(metric.delta) })
|
|
361
|
+
] }),
|
|
362
|
+
/* @__PURE__ */ jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsx(Sparkline, { data }) }),
|
|
363
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-2 text-xs text-muted-foreground", children: [
|
|
364
|
+
"Previous 28d: ",
|
|
365
|
+
formatInteger(metric.previous)
|
|
366
|
+
] })
|
|
367
|
+
] });
|
|
368
|
+
}
|
|
369
|
+
function Sparkline({
|
|
370
|
+
data
|
|
371
|
+
}) {
|
|
372
|
+
if (data.length === 0) {
|
|
373
|
+
return /* @__PURE__ */ jsx("div", { className: "h-28 rounded-lg border border-dashed border-border bg-card" });
|
|
374
|
+
}
|
|
375
|
+
const width = 320;
|
|
376
|
+
const height = 112;
|
|
377
|
+
const padding = 10;
|
|
378
|
+
const values = data.map((item) => item.value);
|
|
379
|
+
const max = Math.max(...values, 1);
|
|
380
|
+
const min = Math.min(...values, 0);
|
|
381
|
+
const range = Math.max(max - min, 1);
|
|
382
|
+
const points = data.map((item, index) => {
|
|
383
|
+
const x = padding + index / Math.max(data.length - 1, 1) * (width - padding * 2);
|
|
384
|
+
const y = height - padding - (item.value - min) / range * (height - padding * 2);
|
|
385
|
+
return `${x},${y}`;
|
|
386
|
+
});
|
|
387
|
+
return /* @__PURE__ */ jsx("svg", { viewBox: `0 0 ${width} ${height}`, className: "h-28 w-full overflow-visible", children: /* @__PURE__ */ jsx(
|
|
388
|
+
"path",
|
|
389
|
+
{
|
|
390
|
+
d: `M ${points.join(" L ")}`,
|
|
391
|
+
fill: "none",
|
|
392
|
+
stroke: "var(--color-kumo-brand)",
|
|
393
|
+
strokeWidth: "2.5",
|
|
394
|
+
strokeLinecap: "round",
|
|
395
|
+
strokeLinejoin: "round"
|
|
396
|
+
}
|
|
397
|
+
) });
|
|
398
|
+
}
|
|
399
|
+
function BreakdownTable({
|
|
400
|
+
rows,
|
|
401
|
+
emptyMessage
|
|
402
|
+
}) {
|
|
403
|
+
if (rows.length === 0) {
|
|
404
|
+
return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: emptyMessage });
|
|
405
|
+
}
|
|
406
|
+
return /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full text-sm", children: [
|
|
407
|
+
/* @__PURE__ */ jsx("thead", { className: "text-left text-xs uppercase tracking-[0.16em] text-muted-foreground", children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
408
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Group" }),
|
|
409
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Pages" }),
|
|
410
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "GSC Clicks" }),
|
|
411
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "GA Views" }),
|
|
412
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "GA Sessions" })
|
|
413
|
+
] }) }),
|
|
414
|
+
/* @__PURE__ */ jsx("tbody", { children: rows.map((row) => /* @__PURE__ */ jsxs("tr", { className: "border-t border-border/80", children: [
|
|
415
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4 font-medium", children: row.label }),
|
|
416
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: formatInteger(row.trackedPages) }),
|
|
417
|
+
/* @__PURE__ */ jsxs("td", { className: "py-3 pr-4", children: [
|
|
418
|
+
formatInteger(row.current.gscClicks),
|
|
419
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: formatSignedInteger(row.delta.gscClicks) })
|
|
420
|
+
] }),
|
|
421
|
+
/* @__PURE__ */ jsxs("td", { className: "py-3 pr-4", children: [
|
|
422
|
+
formatInteger(row.current.gaViews),
|
|
423
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: formatSignedInteger(row.delta.gaViews) })
|
|
424
|
+
] }),
|
|
425
|
+
/* @__PURE__ */ jsxs("td", { className: "py-3 pr-4", children: [
|
|
426
|
+
formatInteger(row.current.gaSessions),
|
|
427
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: formatSignedInteger(row.delta.gaSessions) })
|
|
428
|
+
] })
|
|
429
|
+
] }, row.key)) })
|
|
430
|
+
] }) });
|
|
431
|
+
}
|
|
432
|
+
function MoversTable({
|
|
433
|
+
items,
|
|
434
|
+
emptyMessage
|
|
435
|
+
}) {
|
|
436
|
+
if (items.length === 0) {
|
|
437
|
+
return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: emptyMessage });
|
|
438
|
+
}
|
|
439
|
+
return /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full text-sm", children: [
|
|
440
|
+
/* @__PURE__ */ jsx("thead", { className: "text-left text-xs uppercase tracking-[0.16em] text-muted-foreground", children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
441
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Page" }),
|
|
442
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Type" }),
|
|
443
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "GA Views \u0394" }),
|
|
444
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "GSC Clicks \u0394" }),
|
|
445
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Current Views" })
|
|
446
|
+
] }) }),
|
|
447
|
+
/* @__PURE__ */ jsx("tbody", { children: items.map((item) => /* @__PURE__ */ jsxs("tr", { className: "border-t border-border/80", children: [
|
|
448
|
+
/* @__PURE__ */ jsxs("td", { className: "py-3 pr-4", children: [
|
|
449
|
+
/* @__PURE__ */ jsx("div", { className: "font-medium", children: item.title }),
|
|
450
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: item.urlPath })
|
|
451
|
+
] }),
|
|
452
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: pageKindLabel(item.pageKind) }),
|
|
453
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: formatSignedInteger(item.gaViewsDelta) }),
|
|
454
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: formatSignedInteger(item.gscClicksDelta) }),
|
|
455
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: formatInteger(item.gaViews28d) })
|
|
456
|
+
] }, item.urlPath)) })
|
|
457
|
+
] }) });
|
|
458
|
+
}
|
|
459
|
+
function OverviewPage({ embedded = false }) {
|
|
277
460
|
const [status, setStatus] = React.useState(null);
|
|
278
461
|
const [overview, setOverview] = React.useState(null);
|
|
279
462
|
const [loading, setLoading] = React.useState(true);
|
|
@@ -303,44 +486,79 @@ function OverviewPage() {
|
|
|
303
486
|
}, [load]);
|
|
304
487
|
const summary = overview?.summary ?? status?.summary ?? null;
|
|
305
488
|
const freshness = overview?.freshness ?? status?.freshness ?? idleFreshness();
|
|
306
|
-
|
|
489
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
490
|
+
/* @__PURE__ */ jsx(ErrorBanner, { message: error }),
|
|
491
|
+
!status?.config ? /* @__PURE__ */ jsx(Section, { title: "Not Configured", subtitle: "Save your Google connection settings first.", children: /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "After saving the configuration, run a manual sync to populate this dashboard." }) }) : null,
|
|
492
|
+
/* @__PURE__ */ jsx(Section, { title: "Freshness", subtitle: "Track the latest sync and the effective source dates.", children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-4", children: [
|
|
493
|
+
/* @__PURE__ */ jsx(StatCard, { label: "Last Sync", value: formatDateTime(freshness.lastSyncedAt), note: statusLabel(freshness.lastStatus) }),
|
|
494
|
+
/* @__PURE__ */ jsx(StatCard, { label: "GSC Final Date", value: freshness.lastGscDate || "-" }),
|
|
495
|
+
/* @__PURE__ */ jsx(StatCard, { label: "GA Final Date", value: freshness.lastGaDate || "-" }),
|
|
496
|
+
/* @__PURE__ */ jsx(StatCard, { label: "Service Account", value: status?.config?.serviceAccountEmail || "-" })
|
|
497
|
+
] }) }),
|
|
498
|
+
/* @__PURE__ */ jsx(Section, { title: "KPI Snapshot", subtitle: "Current 28 days versus the previous 28 days across all tracked public pages.", children: /* @__PURE__ */ jsx("div", { className: "grid gap-4 md:grid-cols-2 xl:grid-cols-5", children: (overview?.kpiDeltas ?? []).map((metric) => /* @__PURE__ */ jsx(KpiDeltaCard, { metric }, metric.key)) }) }),
|
|
499
|
+
summary ? /* @__PURE__ */ jsxs("div", { className: "grid gap-6 xl:grid-cols-2", children: [
|
|
500
|
+
/* @__PURE__ */ jsx(
|
|
501
|
+
TrendPanel,
|
|
502
|
+
{
|
|
503
|
+
title: "Search Trend",
|
|
504
|
+
subtitle: "Daily search demand and click capture for the current 28-day window.",
|
|
505
|
+
metrics: (overview?.kpiDeltas ?? []).filter(
|
|
506
|
+
(metric) => metric.key === "gscClicks" || metric.key === "gscImpressions"
|
|
507
|
+
),
|
|
508
|
+
trend: summary.trend
|
|
509
|
+
}
|
|
510
|
+
),
|
|
511
|
+
/* @__PURE__ */ jsx(
|
|
512
|
+
TrendPanel,
|
|
513
|
+
{
|
|
514
|
+
title: "Traffic Trend",
|
|
515
|
+
subtitle: "Daily traffic movement from GA4 for the current 28-day window.",
|
|
516
|
+
metrics: (overview?.kpiDeltas ?? []).filter(
|
|
517
|
+
(metric) => metric.key === "gaViews" || metric.key === "gaSessions"
|
|
518
|
+
),
|
|
519
|
+
trend: summary.trend
|
|
520
|
+
}
|
|
521
|
+
)
|
|
522
|
+
] }) : null,
|
|
523
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-6 xl:grid-cols-2", children: [
|
|
524
|
+
/* @__PURE__ */ jsx(Section, { title: "Page Mix", subtitle: "Compare page groups by current volume and change from the previous window.", children: /* @__PURE__ */ jsx(BreakdownTable, { rows: overview?.pageKindBreakdown ?? [], emptyMessage: "No tracked pages yet." }) }),
|
|
525
|
+
/* @__PURE__ */ jsx(Section, { title: "Managed Coverage", subtitle: "See whether growth is coming from EmDash-managed content or unmanaged pages.", children: /* @__PURE__ */ jsx(BreakdownTable, { rows: overview?.managedBreakdown ?? [], emptyMessage: "No tracked pages yet." }) })
|
|
526
|
+
] }),
|
|
527
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-6 xl:grid-cols-2", children: [
|
|
528
|
+
/* @__PURE__ */ jsx(Section, { title: "Top Gainers", subtitle: "Pages with the strongest positive movement in the last 28 days.", children: /* @__PURE__ */ jsx(MoversTable, { items: overview?.topGainers ?? [], emptyMessage: "No gaining pages yet." }) }),
|
|
529
|
+
/* @__PURE__ */ jsx(Section, { title: "Top Decliners", subtitle: "Pages with the sharpest drop and the clearest candidates for investigation.", children: /* @__PURE__ */ jsx(MoversTable, { items: overview?.topDecliners ?? [], emptyMessage: "No declining pages yet." }) })
|
|
530
|
+
] }),
|
|
531
|
+
/* @__PURE__ */ jsx(Section, { title: "Reporting Windows", subtitle: "The agent API returns the same windows.", children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [
|
|
532
|
+
/* @__PURE__ */ jsx(WindowCard, { label: "GSC Current", value: summary?.window.gscCurrent }),
|
|
533
|
+
/* @__PURE__ */ jsx(WindowCard, { label: "GSC Previous", value: summary?.window.gscPrevious }),
|
|
534
|
+
/* @__PURE__ */ jsx(WindowCard, { label: "GA Current", value: summary?.window.gaCurrent }),
|
|
535
|
+
/* @__PURE__ */ jsx(WindowCard, { label: "GA Previous", value: summary?.window.gaPrevious })
|
|
536
|
+
] }) })
|
|
537
|
+
] });
|
|
538
|
+
if (embedded) {
|
|
539
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
540
|
+
/* @__PURE__ */ jsx(
|
|
541
|
+
InlineHeader,
|
|
542
|
+
{
|
|
543
|
+
title: "Dashboard",
|
|
544
|
+
description: "Monitor site health, compare the last 28 days to the previous window, and spot pages that changed fastest.",
|
|
545
|
+
actions: /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => void load(), disabled: loading, children: "Reload" })
|
|
546
|
+
}
|
|
547
|
+
),
|
|
548
|
+
content
|
|
549
|
+
] });
|
|
550
|
+
}
|
|
551
|
+
return /* @__PURE__ */ jsx(
|
|
307
552
|
Shell,
|
|
308
553
|
{
|
|
309
554
|
title: "Content Insights",
|
|
310
|
-
description: "
|
|
555
|
+
description: "Monitor site health, compare the last 28 days to the previous window, and spot pages that changed fastest.",
|
|
311
556
|
actions: /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => void load(), disabled: loading, children: "Reload" }),
|
|
312
|
-
children:
|
|
313
|
-
/* @__PURE__ */ jsx(ErrorBanner, { message: error }),
|
|
314
|
-
!status?.config ? /* @__PURE__ */ jsx(Section, { title: "Not Configured", subtitle: "Save your Google connection settings first.", children: /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "After saving the configuration, run a manual sync to populate this dashboard." }) }) : null,
|
|
315
|
-
/* @__PURE__ */ jsx(Section, { title: "Freshness", subtitle: "Track the latest sync and the effective source dates.", children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-4", children: [
|
|
316
|
-
/* @__PURE__ */ jsx(StatCard, { label: "Last Sync", value: formatDateTime(freshness.lastSyncedAt), note: statusLabel(freshness.lastStatus) }),
|
|
317
|
-
/* @__PURE__ */ jsx(StatCard, { label: "GSC Final Date", value: freshness.lastGscDate || "-" }),
|
|
318
|
-
/* @__PURE__ */ jsx(StatCard, { label: "GA Final Date", value: freshness.lastGaDate || "-" }),
|
|
319
|
-
/* @__PURE__ */ jsx(StatCard, { label: "Service Account", value: status?.config?.serviceAccountEmail || "-" })
|
|
320
|
-
] }) }),
|
|
321
|
-
/* @__PURE__ */ jsx(Section, { title: "KPI Snapshot", subtitle: "Aggregated totals for the last 28 days across public pages.", children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-3 xl:grid-cols-6", children: [
|
|
322
|
-
/* @__PURE__ */ jsx(StatCard, { label: "GSC Clicks", value: formatInteger(summary?.totals.gscClicks28d ?? 0) }),
|
|
323
|
-
/* @__PURE__ */ jsx(StatCard, { label: "GSC Impressions", value: formatInteger(summary?.totals.gscImpressions28d ?? 0) }),
|
|
324
|
-
/* @__PURE__ */ jsx(StatCard, { label: "GA Views", value: formatInteger(summary?.totals.gaViews28d ?? 0) }),
|
|
325
|
-
/* @__PURE__ */ jsx(StatCard, { label: "GA Users", value: formatInteger(summary?.totals.gaUsers28d ?? 0) }),
|
|
326
|
-
/* @__PURE__ */ jsx(StatCard, { label: "GA Sessions", value: formatInteger(summary?.totals.gaSessions28d ?? 0) }),
|
|
327
|
-
/* @__PURE__ */ jsx(StatCard, { label: "Managed Opportunities", value: formatInteger(summary?.totals.managedOpportunities ?? 0) })
|
|
328
|
-
] }) }),
|
|
329
|
-
/* @__PURE__ */ jsxs("div", { className: "grid gap-6 xl:grid-cols-2", children: [
|
|
330
|
-
/* @__PURE__ */ jsx(Section, { title: "Top Opportunities", subtitle: "Managed content only.", children: /* @__PURE__ */ jsx(MetricTable, { items: overview?.topOpportunities ?? [], emptyMessage: "No opportunities yet." }) }),
|
|
331
|
-
/* @__PURE__ */ jsx(Section, { title: "Top Unmanaged Pages", subtitle: "Public pages outside EmDash-managed content.", children: /* @__PURE__ */ jsx(MetricTable, { items: overview?.topUnmanaged ?? [], emptyMessage: "No unmanaged page data yet." }) })
|
|
332
|
-
] }),
|
|
333
|
-
/* @__PURE__ */ jsx(Section, { title: "Reporting Windows", subtitle: "The agent API returns the same windows.", children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [
|
|
334
|
-
/* @__PURE__ */ jsx(WindowCard, { label: "GSC Current", value: summary?.window.gscCurrent }),
|
|
335
|
-
/* @__PURE__ */ jsx(WindowCard, { label: "GSC Previous", value: summary?.window.gscPrevious }),
|
|
336
|
-
/* @__PURE__ */ jsx(WindowCard, { label: "GA Current", value: summary?.window.gaCurrent }),
|
|
337
|
-
/* @__PURE__ */ jsx(WindowCard, { label: "GA Previous", value: summary?.window.gaPrevious })
|
|
338
|
-
] }) })
|
|
339
|
-
]
|
|
557
|
+
children: content
|
|
340
558
|
}
|
|
341
559
|
);
|
|
342
560
|
}
|
|
343
|
-
function PagesPage() {
|
|
561
|
+
function PagesPage({ embedded = false }) {
|
|
344
562
|
const [managed, setManaged] = React.useState("all");
|
|
345
563
|
const [pageKind, setPageKind] = React.useState("all");
|
|
346
564
|
const [hasOpportunity, setHasOpportunity] = React.useState(false);
|
|
@@ -396,7 +614,7 @@ function PagesPage() {
|
|
|
396
614
|
cancelled = true;
|
|
397
615
|
};
|
|
398
616
|
}, [selected]);
|
|
399
|
-
|
|
617
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
400
618
|
/* @__PURE__ */ jsx(ErrorBanner, { message: error }),
|
|
401
619
|
/* @__PURE__ */ jsx(Section, { title: "Filters", children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-4", children: [
|
|
402
620
|
/* @__PURE__ */ jsx(Field, { label: "Scope", children: /* @__PURE__ */ jsx(
|
|
@@ -485,8 +703,21 @@ function PagesPage() {
|
|
|
485
703
|
] }) })
|
|
486
704
|
] })
|
|
487
705
|
] });
|
|
706
|
+
if (embedded) {
|
|
707
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
708
|
+
/* @__PURE__ */ jsx(
|
|
709
|
+
InlineHeader,
|
|
710
|
+
{
|
|
711
|
+
title: "Pages",
|
|
712
|
+
description: "Explore all public pages and filter down to the content that needs attention."
|
|
713
|
+
}
|
|
714
|
+
),
|
|
715
|
+
content
|
|
716
|
+
] });
|
|
717
|
+
}
|
|
718
|
+
return /* @__PURE__ */ jsx(Shell, { title: "Pages", description: "Explore all public pages and filter down to the content that needs attention.", children: content });
|
|
488
719
|
}
|
|
489
|
-
function SettingsPage() {
|
|
720
|
+
function SettingsPage({ embedded = false }) {
|
|
490
721
|
const [draft, setDraft] = React.useState(EMPTY_CONFIG);
|
|
491
722
|
const [storedConfig, setStoredConfig] = React.useState({
|
|
492
723
|
siteOrigin: "",
|
|
@@ -640,70 +871,108 @@ function SettingsPage() {
|
|
|
640
871
|
setBusy(null);
|
|
641
872
|
}
|
|
642
873
|
};
|
|
643
|
-
|
|
874
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
875
|
+
/* @__PURE__ */ jsx(ErrorBanner, { message: error }),
|
|
876
|
+
/* @__PURE__ */ jsx(SuccessBanner, { message: success }),
|
|
877
|
+
/* @__PURE__ */ jsxs(Section, { title: "Google Connection", children: [
|
|
878
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [
|
|
879
|
+
/* @__PURE__ */ jsx(Field, { label: "Canonical Site Origin", hint: "Example: https://www.yourbright.co.jp", children: /* @__PURE__ */ jsx(Input, { value: draft.siteOrigin, onChange: (value) => setDraft((current) => ({ ...current, siteOrigin: value })) }) }),
|
|
880
|
+
/* @__PURE__ */ jsx(Field, { label: "GA4 Property ID", hint: "Enter the numeric property ID.", children: /* @__PURE__ */ jsx(Input, { value: draft.ga4PropertyId, onChange: (value) => setDraft((current) => ({ ...current, ga4PropertyId: value })) }) }),
|
|
881
|
+
/* @__PURE__ */ jsx("div", { className: "md:col-span-2", children: /* @__PURE__ */ jsx(Field, { label: "Search Console Property", hint: "Example: https://www.yourbright.co.jp/ or sc-domain:yourbright.co.jp", children: /* @__PURE__ */ jsx(Input, { value: draft.gscSiteUrl, onChange: (value) => setDraft((current) => ({ ...current, gscSiteUrl: value })) }) }) }),
|
|
882
|
+
/* @__PURE__ */ jsx("div", { className: "md:col-span-2", children: /* @__PURE__ */ jsx(
|
|
883
|
+
Field,
|
|
884
|
+
{
|
|
885
|
+
label: "Service Account JSON",
|
|
886
|
+
hint: hasStoredServiceAccount ? `Current: ${storedServiceAccountEmail || "configured"}. Leave blank to keep the current secret.` : "Required on the first save.",
|
|
887
|
+
children: /* @__PURE__ */ jsx(
|
|
888
|
+
TextArea,
|
|
889
|
+
{
|
|
890
|
+
value: draft.serviceAccountJson,
|
|
891
|
+
onChange: (value) => setDraft((current) => ({ ...current, serviceAccountJson: value })),
|
|
892
|
+
placeholder: '{"client_email":"...","private_key":"..."}',
|
|
893
|
+
rows: 12
|
|
894
|
+
}
|
|
895
|
+
)
|
|
896
|
+
}
|
|
897
|
+
) })
|
|
898
|
+
] }),
|
|
899
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-6 flex flex-wrap gap-3 border-t border-border pt-4", children: [
|
|
900
|
+
/* @__PURE__ */ jsx(Button, { onClick: () => void save(), disabled: !!busy, children: busy === "save" ? "Saving..." : "Save Settings" }),
|
|
901
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => void testConnection(), disabled: !!busy, children: busy === "test" ? "Testing..." : "Test Connection" }),
|
|
902
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => void syncNow(), disabled: !!busy, children: busy === "sync" ? "Syncing..." : "Run Manual Sync" })
|
|
903
|
+
] })
|
|
904
|
+
] }),
|
|
905
|
+
/* @__PURE__ */ jsxs(Section, { title: "Agent API Keys", subtitle: "Use these with Authorization: AgentKey yb_ins_... or X-Emdash-Agent-Key. Raw keys are shown only once.", children: [
|
|
906
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-[minmax(0,1fr)_auto]", children: [
|
|
907
|
+
/* @__PURE__ */ jsx(Field, { label: "New key label", children: /* @__PURE__ */ jsx(Input, { value: newKeyLabel, onChange: setNewKeyLabel, placeholder: "content-feedback-agent" }) }),
|
|
908
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-end", children: /* @__PURE__ */ jsx(Button, { onClick: () => void createKey(), disabled: !!busy || !newKeyLabel.trim(), children: busy === "create-key" ? "Creating..." : "Create Key" }) })
|
|
909
|
+
] }),
|
|
910
|
+
generatedKey ? /* @__PURE__ */ jsxs("div", { className: "mt-4 rounded-lg border border-amber-200 bg-amber-50 p-4", children: [
|
|
911
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium text-amber-900", children: "Generated Key" }),
|
|
912
|
+
/* @__PURE__ */ jsx("div", { className: "mt-2 break-all font-mono text-sm text-amber-900", children: generatedKey })
|
|
913
|
+
] }) : null,
|
|
914
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4 overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full text-sm", children: [
|
|
915
|
+
/* @__PURE__ */ jsx("thead", { className: "text-left text-xs uppercase tracking-[0.16em] text-muted-foreground", children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
916
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Prefix" }),
|
|
917
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Label" }),
|
|
918
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Created" }),
|
|
919
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Last Used" }),
|
|
920
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Status" }),
|
|
921
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4" })
|
|
922
|
+
] }) }),
|
|
923
|
+
/* @__PURE__ */ jsx("tbody", { children: keys.map((key) => /* @__PURE__ */ jsxs("tr", { className: "border-t border-border/80", children: [
|
|
924
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4 font-mono text-xs", children: key.prefix }),
|
|
925
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: key.label }),
|
|
926
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: formatDateTime(key.createdAt) }),
|
|
927
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: formatDateTime(key.lastUsedAt) }),
|
|
928
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: key.revokedAt ? "Revoked" : "Active" }),
|
|
929
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: key.revokedAt ? null : /* @__PURE__ */ jsx(Button, { variant: "danger", onClick: () => void revokeKey(key.prefix), disabled: busy === key.prefix, children: "Revoke" }) })
|
|
930
|
+
] }, key.prefix)) })
|
|
931
|
+
] }) })
|
|
932
|
+
] })
|
|
933
|
+
] });
|
|
934
|
+
if (embedded) {
|
|
935
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
936
|
+
/* @__PURE__ */ jsx(
|
|
937
|
+
InlineHeader,
|
|
938
|
+
{
|
|
939
|
+
title: "Settings",
|
|
940
|
+
description: "Manage Google connection settings, manual sync, and agent API keys."
|
|
941
|
+
}
|
|
942
|
+
),
|
|
943
|
+
content
|
|
944
|
+
] });
|
|
945
|
+
}
|
|
946
|
+
return /* @__PURE__ */ jsx(
|
|
644
947
|
Shell,
|
|
645
948
|
{
|
|
646
949
|
title: "Analytics",
|
|
647
950
|
description: "Manage Google connection settings, manual sync, and agent API keys.",
|
|
951
|
+
children: content
|
|
952
|
+
}
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
function AnalyticsPage() {
|
|
956
|
+
const [section, setSection] = React.useState(sectionFromLocation);
|
|
957
|
+
React.useEffect(() => {
|
|
958
|
+
const handlePopstate = () => setSection(sectionFromLocation());
|
|
959
|
+
window.addEventListener("popstate", handlePopstate);
|
|
960
|
+
return () => window.removeEventListener("popstate", handlePopstate);
|
|
961
|
+
}, []);
|
|
962
|
+
const selectSection = (nextSection) => {
|
|
963
|
+
setSection(nextSection);
|
|
964
|
+
updateSectionInUrl(nextSection);
|
|
965
|
+
};
|
|
966
|
+
return /* @__PURE__ */ jsxs(
|
|
967
|
+
Shell,
|
|
968
|
+
{
|
|
969
|
+
title: "Analytics",
|
|
970
|
+
description: "Use one workspace for dashboard review, page drilldown, and connection management.",
|
|
971
|
+
actions: /* @__PURE__ */ jsx(AnalyticsTabs, { section, onChange: selectSection }),
|
|
648
972
|
children: [
|
|
649
|
-
/* @__PURE__ */ jsx(
|
|
650
|
-
/* @__PURE__ */ jsx(
|
|
651
|
-
/* @__PURE__ */
|
|
652
|
-
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [
|
|
653
|
-
/* @__PURE__ */ jsx(Field, { label: "Canonical Site Origin", hint: "Example: https://www.yourbright.co.jp", children: /* @__PURE__ */ jsx(Input, { value: draft.siteOrigin, onChange: (value) => setDraft((current) => ({ ...current, siteOrigin: value })) }) }),
|
|
654
|
-
/* @__PURE__ */ jsx(Field, { label: "GA4 Property ID", hint: "Enter the numeric property ID.", children: /* @__PURE__ */ jsx(Input, { value: draft.ga4PropertyId, onChange: (value) => setDraft((current) => ({ ...current, ga4PropertyId: value })) }) }),
|
|
655
|
-
/* @__PURE__ */ jsx("div", { className: "md:col-span-2", children: /* @__PURE__ */ jsx(Field, { label: "Search Console Property", hint: "Example: https://www.yourbright.co.jp/ or sc-domain:yourbright.co.jp", children: /* @__PURE__ */ jsx(Input, { value: draft.gscSiteUrl, onChange: (value) => setDraft((current) => ({ ...current, gscSiteUrl: value })) }) }) }),
|
|
656
|
-
/* @__PURE__ */ jsx("div", { className: "md:col-span-2", children: /* @__PURE__ */ jsx(
|
|
657
|
-
Field,
|
|
658
|
-
{
|
|
659
|
-
label: "Service Account JSON",
|
|
660
|
-
hint: hasStoredServiceAccount ? `Current: ${storedServiceAccountEmail || "configured"}. Leave blank to keep the current secret.` : "Required on the first save.",
|
|
661
|
-
children: /* @__PURE__ */ jsx(
|
|
662
|
-
TextArea,
|
|
663
|
-
{
|
|
664
|
-
value: draft.serviceAccountJson,
|
|
665
|
-
onChange: (value) => setDraft((current) => ({ ...current, serviceAccountJson: value })),
|
|
666
|
-
placeholder: '{"client_email":"...","private_key":"..."}',
|
|
667
|
-
rows: 12
|
|
668
|
-
}
|
|
669
|
-
)
|
|
670
|
-
}
|
|
671
|
-
) })
|
|
672
|
-
] }),
|
|
673
|
-
/* @__PURE__ */ jsxs("div", { className: "mt-6 flex flex-wrap gap-3 border-t border-border pt-4", children: [
|
|
674
|
-
/* @__PURE__ */ jsx(Button, { onClick: () => void save(), disabled: !!busy, children: busy === "save" ? "Saving..." : "Save Settings" }),
|
|
675
|
-
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => void testConnection(), disabled: !!busy, children: busy === "test" ? "Testing..." : "Test Connection" }),
|
|
676
|
-
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => void syncNow(), disabled: !!busy, children: busy === "sync" ? "Syncing..." : "Run Manual Sync" })
|
|
677
|
-
] })
|
|
678
|
-
] }),
|
|
679
|
-
/* @__PURE__ */ jsxs(Section, { title: "Agent API Keys", subtitle: "Use these with Authorization: AgentKey yb_ins_... or X-Emdash-Agent-Key. Raw keys are shown only once.", children: [
|
|
680
|
-
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-[minmax(0,1fr)_auto]", children: [
|
|
681
|
-
/* @__PURE__ */ jsx(Field, { label: "New key label", children: /* @__PURE__ */ jsx(Input, { value: newKeyLabel, onChange: setNewKeyLabel, placeholder: "content-feedback-agent" }) }),
|
|
682
|
-
/* @__PURE__ */ jsx("div", { className: "flex items-end", children: /* @__PURE__ */ jsx(Button, { onClick: () => void createKey(), disabled: !!busy || !newKeyLabel.trim(), children: busy === "create-key" ? "Creating..." : "Create Key" }) })
|
|
683
|
-
] }),
|
|
684
|
-
generatedKey ? /* @__PURE__ */ jsxs("div", { className: "mt-4 rounded-lg border border-amber-200 bg-amber-50 p-4", children: [
|
|
685
|
-
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium text-amber-900", children: "Generated Key" }),
|
|
686
|
-
/* @__PURE__ */ jsx("div", { className: "mt-2 break-all font-mono text-sm text-amber-900", children: generatedKey })
|
|
687
|
-
] }) : null,
|
|
688
|
-
/* @__PURE__ */ jsx("div", { className: "mt-4 overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full text-sm", children: [
|
|
689
|
-
/* @__PURE__ */ jsx("thead", { className: "text-left text-xs uppercase tracking-[0.16em] text-muted-foreground", children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
690
|
-
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Prefix" }),
|
|
691
|
-
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Label" }),
|
|
692
|
-
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Created" }),
|
|
693
|
-
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Last Used" }),
|
|
694
|
-
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Status" }),
|
|
695
|
-
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4" })
|
|
696
|
-
] }) }),
|
|
697
|
-
/* @__PURE__ */ jsx("tbody", { children: keys.map((key) => /* @__PURE__ */ jsxs("tr", { className: "border-t border-border/80", children: [
|
|
698
|
-
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4 font-mono text-xs", children: key.prefix }),
|
|
699
|
-
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: key.label }),
|
|
700
|
-
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: formatDateTime(key.createdAt) }),
|
|
701
|
-
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: formatDateTime(key.lastUsedAt) }),
|
|
702
|
-
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: key.revokedAt ? "Revoked" : "Active" }),
|
|
703
|
-
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: key.revokedAt ? null : /* @__PURE__ */ jsx(Button, { variant: "danger", onClick: () => void revokeKey(key.prefix), disabled: busy === key.prefix, children: "Revoke" }) })
|
|
704
|
-
] }, key.prefix)) })
|
|
705
|
-
] }) })
|
|
706
|
-
] })
|
|
973
|
+
section === "dashboard" ? /* @__PURE__ */ jsx(OverviewPage, { embedded: true }) : null,
|
|
974
|
+
section === "pages" ? /* @__PURE__ */ jsx(PagesPage, { embedded: true }) : null,
|
|
975
|
+
section === "settings" ? /* @__PURE__ */ jsx(SettingsPage, { embedded: true }) : null
|
|
707
976
|
]
|
|
708
977
|
}
|
|
709
978
|
);
|
|
@@ -789,6 +1058,11 @@ function WindowCard({
|
|
|
789
1058
|
function formatInteger(value) {
|
|
790
1059
|
return new Intl.NumberFormat("ja-JP").format(value ?? 0);
|
|
791
1060
|
}
|
|
1061
|
+
function formatSignedInteger(value) {
|
|
1062
|
+
const numeric = value ?? 0;
|
|
1063
|
+
if (numeric === 0) return "0";
|
|
1064
|
+
return `${numeric > 0 ? "+" : ""}${formatInteger(numeric)}`;
|
|
1065
|
+
}
|
|
792
1066
|
function formatPercent(value) {
|
|
793
1067
|
return `${((value ?? 0) * 100).toFixed(1)}%`;
|
|
794
1068
|
}
|
|
@@ -847,7 +1121,7 @@ function numberValue(value) {
|
|
|
847
1121
|
return typeof value === "number" ? value : 0;
|
|
848
1122
|
}
|
|
849
1123
|
var pages = {
|
|
850
|
-
"/":
|
|
1124
|
+
"/": AnalyticsPage,
|
|
851
1125
|
"/pages": PagesPage,
|
|
852
1126
|
"/settings": SettingsPage
|
|
853
1127
|
};
|
package/dist/index.js
CHANGED
|
@@ -264,31 +264,36 @@ function buildContentUrl(siteOrigin, urlPath) {
|
|
|
264
264
|
return new URL(urlPath, `${siteOrigin}/`).toString();
|
|
265
265
|
}
|
|
266
266
|
async function getManagedContentMap(siteOrigin) {
|
|
267
|
-
const result = await getEmDashCollection("posts", {
|
|
268
|
-
status: "published",
|
|
269
|
-
limit: 1e3,
|
|
270
|
-
orderBy: { updatedAt: "desc" }
|
|
271
|
-
});
|
|
272
267
|
const managed = /* @__PURE__ */ new Map();
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const seoDescription = typeof data.seo_description === "string" ? data.seo_description : void 0;
|
|
281
|
-
const urlPath = `/blog/${slug || id}/`;
|
|
282
|
-
managed.set(urlPath, {
|
|
283
|
-
collection: "posts",
|
|
284
|
-
id,
|
|
285
|
-
slug,
|
|
286
|
-
urlPath,
|
|
287
|
-
title,
|
|
288
|
-
excerpt,
|
|
289
|
-
seoDescription
|
|
268
|
+
let cursor;
|
|
269
|
+
do {
|
|
270
|
+
const result = await getEmDashCollection("posts", {
|
|
271
|
+
status: "published",
|
|
272
|
+
limit: 50,
|
|
273
|
+
cursor,
|
|
274
|
+
orderBy: { updatedAt: "desc" }
|
|
290
275
|
});
|
|
291
|
-
|
|
276
|
+
for (const entry of result.entries) {
|
|
277
|
+
const id = typeof entry.id === "string" ? entry.id : "";
|
|
278
|
+
if (!id) continue;
|
|
279
|
+
const slug = typeof entry.slug === "string" ? entry.slug : null;
|
|
280
|
+
const data = entry.data ?? {};
|
|
281
|
+
const title = typeof data.title === "string" ? data.title : slug || id;
|
|
282
|
+
const excerpt = typeof data.excerpt === "string" ? data.excerpt : void 0;
|
|
283
|
+
const seoDescription = typeof data.seo_description === "string" ? data.seo_description : void 0;
|
|
284
|
+
const urlPath = `/blog/${slug || id}/`;
|
|
285
|
+
managed.set(urlPath, {
|
|
286
|
+
collection: "posts",
|
|
287
|
+
id,
|
|
288
|
+
slug,
|
|
289
|
+
urlPath,
|
|
290
|
+
title,
|
|
291
|
+
excerpt,
|
|
292
|
+
seoDescription
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
cursor = result.nextCursor;
|
|
296
|
+
} while (cursor);
|
|
292
297
|
void siteOrigin;
|
|
293
298
|
return managed;
|
|
294
299
|
}
|
|
@@ -997,7 +1002,7 @@ async function listPages(ctx, filters) {
|
|
|
997
1002
|
};
|
|
998
1003
|
}
|
|
999
1004
|
async function getOverview(ctx) {
|
|
1000
|
-
const [summary, freshness, topOpportunities, topUnmanaged] = await Promise.all([
|
|
1005
|
+
const [summary, freshness, topOpportunities, topUnmanaged, allPages] = await Promise.all([
|
|
1001
1006
|
ctx.kv.get(SITE_SUMMARY_KEY),
|
|
1002
1007
|
getFreshness(ctx),
|
|
1003
1008
|
ctx.storage.pages.query({
|
|
@@ -1009,13 +1014,36 @@ async function getOverview(ctx) {
|
|
|
1009
1014
|
where: { managed: false },
|
|
1010
1015
|
orderBy: { gaViews28d: "desc" },
|
|
1011
1016
|
limit: 5
|
|
1012
|
-
})
|
|
1017
|
+
}),
|
|
1018
|
+
listAllPages(ctx)
|
|
1013
1019
|
]);
|
|
1020
|
+
return buildOverviewData(
|
|
1021
|
+
summary,
|
|
1022
|
+
freshness,
|
|
1023
|
+
allPages,
|
|
1024
|
+
topOpportunities.items.map((item) => item.data),
|
|
1025
|
+
topUnmanaged.items.map((item) => item.data)
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
function buildOverviewData(summary, freshness, allPages, topOpportunities, topUnmanaged) {
|
|
1014
1029
|
return {
|
|
1015
1030
|
summary,
|
|
1016
1031
|
freshness,
|
|
1017
|
-
|
|
1018
|
-
|
|
1032
|
+
kpiDeltas: buildKpiDeltas(allPages),
|
|
1033
|
+
pageKindBreakdown: buildBreakdown(
|
|
1034
|
+
allPages,
|
|
1035
|
+
(page) => page.pageKind,
|
|
1036
|
+
(key) => pageKindLabel(key)
|
|
1037
|
+
),
|
|
1038
|
+
managedBreakdown: buildBreakdown(
|
|
1039
|
+
allPages,
|
|
1040
|
+
(page) => page.managed ? "managed" : "unmanaged",
|
|
1041
|
+
(key) => key === "managed" ? "Managed" : "Unmanaged"
|
|
1042
|
+
),
|
|
1043
|
+
topGainers: buildMovers(allPages, "gainers"),
|
|
1044
|
+
topDecliners: buildMovers(allPages, "decliners"),
|
|
1045
|
+
topOpportunities,
|
|
1046
|
+
topUnmanaged
|
|
1019
1047
|
};
|
|
1020
1048
|
}
|
|
1021
1049
|
async function getContentContext(ctx, collection, id, slug) {
|
|
@@ -1235,6 +1263,19 @@ async function getFreshness(ctx) {
|
|
|
1235
1263
|
lastStatus: "idle"
|
|
1236
1264
|
};
|
|
1237
1265
|
}
|
|
1266
|
+
async function listAllPages(ctx) {
|
|
1267
|
+
const pages = [];
|
|
1268
|
+
let cursor;
|
|
1269
|
+
do {
|
|
1270
|
+
const batch = await ctx.storage.pages.query({
|
|
1271
|
+
limit: 500,
|
|
1272
|
+
cursor
|
|
1273
|
+
});
|
|
1274
|
+
cursor = batch.cursor;
|
|
1275
|
+
pages.push(...batch.items.map((item) => item.data));
|
|
1276
|
+
} while (cursor);
|
|
1277
|
+
return pages;
|
|
1278
|
+
}
|
|
1238
1279
|
async function refreshSummaryFromStorage(ctx) {
|
|
1239
1280
|
const windows = buildWindows();
|
|
1240
1281
|
const allPages = [];
|
|
@@ -1327,6 +1368,112 @@ function mergeTrend(gscTrend, gaTrend) {
|
|
|
1327
1368
|
}
|
|
1328
1369
|
return Array.from(map.values()).sort((left, right) => left.date.localeCompare(right.date));
|
|
1329
1370
|
}
|
|
1371
|
+
function buildKpiDeltas(pages) {
|
|
1372
|
+
const metrics = [
|
|
1373
|
+
{
|
|
1374
|
+
key: "gscClicks",
|
|
1375
|
+
label: "GSC Clicks",
|
|
1376
|
+
current: sumPages(pages, (page) => page.gscClicks28d),
|
|
1377
|
+
previous: sumPages(pages, (page) => page.gscClicksPrev28d)
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
key: "gscImpressions",
|
|
1381
|
+
label: "GSC Impressions",
|
|
1382
|
+
current: sumPages(pages, (page) => page.gscImpressions28d),
|
|
1383
|
+
previous: sumPages(pages, (page) => page.gscImpressionsPrev28d)
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
key: "gaViews",
|
|
1387
|
+
label: "GA Views",
|
|
1388
|
+
current: sumPages(pages, (page) => page.gaViews28d),
|
|
1389
|
+
previous: sumPages(pages, (page) => page.gaViewsPrev28d)
|
|
1390
|
+
},
|
|
1391
|
+
{
|
|
1392
|
+
key: "gaUsers",
|
|
1393
|
+
label: "GA Users",
|
|
1394
|
+
current: sumPages(pages, (page) => page.gaUsers28d),
|
|
1395
|
+
previous: sumPages(pages, (page) => page.gaUsersPrev28d)
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
key: "gaSessions",
|
|
1399
|
+
label: "GA Sessions",
|
|
1400
|
+
current: sumPages(pages, (page) => page.gaSessions28d),
|
|
1401
|
+
previous: sumPages(pages, (page) => page.gaSessionsPrev28d)
|
|
1402
|
+
}
|
|
1403
|
+
];
|
|
1404
|
+
return metrics.map((metric) => ({
|
|
1405
|
+
...metric,
|
|
1406
|
+
delta: metric.current - metric.previous
|
|
1407
|
+
}));
|
|
1408
|
+
}
|
|
1409
|
+
function buildBreakdown(pages, getKey, getLabel) {
|
|
1410
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1411
|
+
for (const page of pages) {
|
|
1412
|
+
const key = getKey(page);
|
|
1413
|
+
const existing = buckets.get(key) ?? {
|
|
1414
|
+
key,
|
|
1415
|
+
label: getLabel(key),
|
|
1416
|
+
trackedPages: 0,
|
|
1417
|
+
current: { gscClicks: 0, gaViews: 0, gaSessions: 0 },
|
|
1418
|
+
previous: { gscClicks: 0, gaViews: 0, gaSessions: 0 },
|
|
1419
|
+
delta: { gscClicks: 0, gaViews: 0, gaSessions: 0 }
|
|
1420
|
+
};
|
|
1421
|
+
existing.trackedPages += 1;
|
|
1422
|
+
existing.current.gscClicks += page.gscClicks28d;
|
|
1423
|
+
existing.current.gaViews += page.gaViews28d;
|
|
1424
|
+
existing.current.gaSessions += page.gaSessions28d;
|
|
1425
|
+
existing.previous.gscClicks += page.gscClicksPrev28d;
|
|
1426
|
+
existing.previous.gaViews += page.gaViewsPrev28d;
|
|
1427
|
+
existing.previous.gaSessions += page.gaSessionsPrev28d;
|
|
1428
|
+
existing.delta.gscClicks = existing.current.gscClicks - existing.previous.gscClicks;
|
|
1429
|
+
existing.delta.gaViews = existing.current.gaViews - existing.previous.gaViews;
|
|
1430
|
+
existing.delta.gaSessions = existing.current.gaSessions - existing.previous.gaSessions;
|
|
1431
|
+
buckets.set(key, existing);
|
|
1432
|
+
}
|
|
1433
|
+
return Array.from(buckets.values()).sort((left, right) => right.current.gaViews - left.current.gaViews);
|
|
1434
|
+
}
|
|
1435
|
+
function buildMovers(pages, direction) {
|
|
1436
|
+
const rows = pages.map((page) => ({
|
|
1437
|
+
urlPath: page.urlPath,
|
|
1438
|
+
title: page.title,
|
|
1439
|
+
pageKind: page.pageKind,
|
|
1440
|
+
managed: page.managed,
|
|
1441
|
+
gscClicks28d: page.gscClicks28d,
|
|
1442
|
+
gaViews28d: page.gaViews28d,
|
|
1443
|
+
gscClicksDelta: page.gscClicks28d - page.gscClicksPrev28d,
|
|
1444
|
+
gaViewsDelta: page.gaViews28d - page.gaViewsPrev28d,
|
|
1445
|
+
opportunityScore: page.opportunityScore
|
|
1446
|
+
}));
|
|
1447
|
+
const filtered = rows.filter(
|
|
1448
|
+
(row) => direction === "gainers" ? row.gaViewsDelta > 0 || row.gscClicksDelta > 0 : row.gaViewsDelta < 0 || row.gscClicksDelta < 0
|
|
1449
|
+
);
|
|
1450
|
+
filtered.sort((left, right) => {
|
|
1451
|
+
if (direction === "gainers") {
|
|
1452
|
+
return right.gaViewsDelta - left.gaViewsDelta || right.gscClicksDelta - left.gscClicksDelta || right.gaViews28d - left.gaViews28d;
|
|
1453
|
+
}
|
|
1454
|
+
return left.gaViewsDelta - right.gaViewsDelta || left.gscClicksDelta - right.gscClicksDelta || right.gaViews28d - left.gaViews28d;
|
|
1455
|
+
});
|
|
1456
|
+
return filtered.slice(0, 5);
|
|
1457
|
+
}
|
|
1458
|
+
function sumPages(pages, getValue) {
|
|
1459
|
+
return pages.reduce((total, page) => total + getValue(page), 0);
|
|
1460
|
+
}
|
|
1461
|
+
function pageKindLabel(pageKind) {
|
|
1462
|
+
switch (pageKind) {
|
|
1463
|
+
case "blog_post":
|
|
1464
|
+
return "Blog Post";
|
|
1465
|
+
case "blog_archive":
|
|
1466
|
+
return "Blog Archive";
|
|
1467
|
+
case "tag":
|
|
1468
|
+
return "Tag";
|
|
1469
|
+
case "author":
|
|
1470
|
+
return "Author";
|
|
1471
|
+
case "landing":
|
|
1472
|
+
return "Landing";
|
|
1473
|
+
default:
|
|
1474
|
+
return "Other";
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1330
1477
|
|
|
1331
1478
|
// src/index.ts
|
|
1332
1479
|
var configSaveSchema = z2.object({
|
|
@@ -1366,9 +1513,7 @@ function contentInsightsPlugin() {
|
|
|
1366
1513
|
"www.googleapis.com"
|
|
1367
1514
|
],
|
|
1368
1515
|
adminPages: [
|
|
1369
|
-
{ path: "/", label: "
|
|
1370
|
-
{ path: "/pages", label: "Pages", icon: "list" },
|
|
1371
|
-
{ path: "/settings", label: "Analytics", icon: "gear" }
|
|
1516
|
+
{ path: "/", label: "Analytics", icon: "chart-bar" }
|
|
1372
1517
|
],
|
|
1373
1518
|
adminWidgets: [
|
|
1374
1519
|
{ id: "content-opportunities", title: "Content Opportunities", size: "full" }
|
|
@@ -1473,14 +1618,26 @@ function createPlugin() {
|
|
|
1473
1618
|
if (!resolved.success) {
|
|
1474
1619
|
throw new PluginRouteError2("BAD_REQUEST", resolved.message, 400);
|
|
1475
1620
|
}
|
|
1476
|
-
|
|
1621
|
+
try {
|
|
1622
|
+
return testConnection(ctx, resolved.data);
|
|
1623
|
+
} catch (error) {
|
|
1624
|
+
const message = error instanceof Error ? error.message : "Connection test failed";
|
|
1625
|
+
console.error("[analytics-plugin] connection test failed", error);
|
|
1626
|
+
throw new PluginRouteError2("INTERNAL_ERROR", message, 500);
|
|
1627
|
+
}
|
|
1477
1628
|
}
|
|
1478
1629
|
},
|
|
1479
1630
|
[ADMIN_ROUTES.SYNC_NOW]: {
|
|
1480
1631
|
handler: async (ctx) => {
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1632
|
+
try {
|
|
1633
|
+
const base = await syncBase(ctx, "manual");
|
|
1634
|
+
const enriched = await enrichManagedQueries(ctx);
|
|
1635
|
+
return { ...base, ...enriched };
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
const message = error instanceof Error ? error.message : "Manual sync failed";
|
|
1638
|
+
console.error("[analytics-plugin] manual sync failed", error);
|
|
1639
|
+
throw new PluginRouteError2("INTERNAL_ERROR", message, 500);
|
|
1640
|
+
}
|
|
1484
1641
|
}
|
|
1485
1642
|
},
|
|
1486
1643
|
[ADMIN_ROUTES.AGENT_KEYS_LIST]: {
|
|
@@ -1537,9 +1694,7 @@ function createPlugin() {
|
|
|
1537
1694
|
},
|
|
1538
1695
|
admin: {
|
|
1539
1696
|
pages: [
|
|
1540
|
-
{ path: "/", label: "
|
|
1541
|
-
{ path: "/pages", label: "Pages", icon: "list" },
|
|
1542
|
-
{ path: "/settings", label: "Analytics", icon: "gear" }
|
|
1697
|
+
{ path: "/", label: "Analytics", icon: "chart-bar" }
|
|
1543
1698
|
],
|
|
1544
1699
|
widgets: [
|
|
1545
1700
|
{ id: "content-opportunities", title: "Content Opportunities", size: "full" }
|