third-audience-mdx 1.0.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/CLAUDE.md +41 -0
- package/INSTALLATION.md +367 -0
- package/README.md +303 -0
- package/WORKLOG.md +162 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +208 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/index.mjs +185 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/dashboard/auth.d.mts +16 -0
- package/dist/dashboard/auth.d.ts +16 -0
- package/dist/dashboard/auth.js +123 -0
- package/dist/dashboard/auth.js.map +1 -0
- package/dist/dashboard/auth.mjs +87 -0
- package/dist/dashboard/auth.mjs.map +1 -0
- package/dist/dashboard/routes/analytics-api-route.d.mts +6 -0
- package/dist/dashboard/routes/analytics-api-route.d.ts +6 -0
- package/dist/dashboard/routes/analytics-api-route.js +180 -0
- package/dist/dashboard/routes/analytics-api-route.js.map +1 -0
- package/dist/dashboard/routes/analytics-api-route.mjs +145 -0
- package/dist/dashboard/routes/analytics-api-route.mjs.map +1 -0
- package/dist/dashboard/routes/api-key-route.d.mts +8 -0
- package/dist/dashboard/routes/api-key-route.d.ts +8 -0
- package/dist/dashboard/routes/api-key-route.js +173 -0
- package/dist/dashboard/routes/api-key-route.js.map +1 -0
- package/dist/dashboard/routes/api-key-route.mjs +137 -0
- package/dist/dashboard/routes/api-key-route.mjs.map +1 -0
- package/dist/dashboard/routes/citation-route.d.mts +14 -0
- package/dist/dashboard/routes/citation-route.d.ts +14 -0
- package/dist/dashboard/routes/citation-route.js +202 -0
- package/dist/dashboard/routes/citation-route.js.map +1 -0
- package/dist/dashboard/routes/citation-route.mjs +166 -0
- package/dist/dashboard/routes/citation-route.mjs.map +1 -0
- package/dist/dashboard/routes/llms-txt-route.d.mts +6 -0
- package/dist/dashboard/routes/llms-txt-route.d.ts +6 -0
- package/dist/dashboard/routes/llms-txt-route.js +119 -0
- package/dist/dashboard/routes/llms-txt-route.js.map +1 -0
- package/dist/dashboard/routes/llms-txt-route.mjs +84 -0
- package/dist/dashboard/routes/llms-txt-route.mjs.map +1 -0
- package/dist/dashboard/routes/login-route.d.mts +6 -0
- package/dist/dashboard/routes/login-route.d.ts +6 -0
- package/dist/dashboard/routes/login-route.js +313 -0
- package/dist/dashboard/routes/login-route.js.map +1 -0
- package/dist/dashboard/routes/login-route.mjs +284 -0
- package/dist/dashboard/routes/login-route.mjs.map +1 -0
- package/dist/dashboard/routes/markdown-route.d.mts +15 -0
- package/dist/dashboard/routes/markdown-route.d.ts +15 -0
- package/dist/dashboard/routes/markdown-route.js +239 -0
- package/dist/dashboard/routes/markdown-route.js.map +1 -0
- package/dist/dashboard/routes/markdown-route.mjs +204 -0
- package/dist/dashboard/routes/markdown-route.mjs.map +1 -0
- package/dist/dashboard/routes/okf-route.d.mts +13 -0
- package/dist/dashboard/routes/okf-route.d.ts +13 -0
- package/dist/dashboard/routes/okf-route.js +184 -0
- package/dist/dashboard/routes/okf-route.js.map +1 -0
- package/dist/dashboard/routes/okf-route.mjs +149 -0
- package/dist/dashboard/routes/okf-route.mjs.map +1 -0
- package/dist/dashboard/routes/sitemap-ai-route.d.mts +6 -0
- package/dist/dashboard/routes/sitemap-ai-route.d.ts +6 -0
- package/dist/dashboard/routes/sitemap-ai-route.js +134 -0
- package/dist/dashboard/routes/sitemap-ai-route.js.map +1 -0
- package/dist/dashboard/routes/sitemap-ai-route.mjs +99 -0
- package/dist/dashboard/routes/sitemap-ai-route.mjs.map +1 -0
- package/dist/dashboard/ui/components/Sidebar.d.mts +5 -0
- package/dist/dashboard/ui/components/Sidebar.d.ts +5 -0
- package/dist/dashboard/ui/components/Sidebar.js +102 -0
- package/dist/dashboard/ui/components/Sidebar.js.map +1 -0
- package/dist/dashboard/ui/components/Sidebar.mjs +68 -0
- package/dist/dashboard/ui/components/Sidebar.mjs.map +1 -0
- package/dist/dashboard/ui/globals.css +175 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.d.mts +5 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.d.ts +5 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.js +269 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.js.map +1 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.mjs +232 -0
- package/dist/dashboard/ui/pages/BotAnalyticsPage.mjs.map +1 -0
- package/dist/dashboard/ui/pages/BotManagementPage.d.mts +13 -0
- package/dist/dashboard/ui/pages/BotManagementPage.d.ts +13 -0
- package/dist/dashboard/ui/pages/BotManagementPage.js +177 -0
- package/dist/dashboard/ui/pages/BotManagementPage.js.map +1 -0
- package/dist/dashboard/ui/pages/BotManagementPage.mjs +153 -0
- package/dist/dashboard/ui/pages/BotManagementPage.mjs.map +1 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.d.mts +5 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.d.ts +5 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.js +203 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.js.map +1 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.mjs +168 -0
- package/dist/dashboard/ui/pages/LlmTrafficPage.mjs.map +1 -0
- package/dist/dashboard/ui/pages/SettingsPage.d.mts +8 -0
- package/dist/dashboard/ui/pages/SettingsPage.d.ts +8 -0
- package/dist/dashboard/ui/pages/SettingsPage.js +181 -0
- package/dist/dashboard/ui/pages/SettingsPage.js.map +1 -0
- package/dist/dashboard/ui/pages/SettingsPage.mjs +157 -0
- package/dist/dashboard/ui/pages/SettingsPage.mjs.map +1 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.d.mts +5 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.d.ts +5 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.js +183 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.js.map +1 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.mjs +148 -0
- package/dist/dashboard/ui/pages/SystemHealthPage.mjs.map +1 -0
- package/dist/index.d.mts +84 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +372 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +346 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +125 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/* Third Audience Dashboard — Apple-inspired design system */
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--ta-blue: #007aff;
|
|
5
|
+
--ta-green: #34c759;
|
|
6
|
+
--ta-orange: #ff9500;
|
|
7
|
+
--ta-red: #ff3b30;
|
|
8
|
+
--ta-purple: #af52de;
|
|
9
|
+
--ta-teal: #5ac8fa;
|
|
10
|
+
|
|
11
|
+
--ta-gray-50: #fafafa;
|
|
12
|
+
--ta-gray-100: #f5f5f7;
|
|
13
|
+
--ta-gray-200: #e5e5ea;
|
|
14
|
+
--ta-gray-300: #d1d1d6;
|
|
15
|
+
--ta-gray-400: #c7c7cc;
|
|
16
|
+
--ta-gray-500: #aeaeb2;
|
|
17
|
+
--ta-gray-600: #8e8e93;
|
|
18
|
+
--ta-gray-700: #636366;
|
|
19
|
+
--ta-gray-800: #3a3a3c;
|
|
20
|
+
--ta-gray-900: #1c1c1e;
|
|
21
|
+
|
|
22
|
+
--ta-bg: #f5f5f7;
|
|
23
|
+
--ta-surface: #ffffff;
|
|
24
|
+
--ta-border: rgba(0,0,0,0.08);
|
|
25
|
+
|
|
26
|
+
--ta-radius-sm: 8px;
|
|
27
|
+
--ta-radius-md: 12px;
|
|
28
|
+
--ta-radius-lg: 16px;
|
|
29
|
+
--ta-radius-xl: 20px;
|
|
30
|
+
|
|
31
|
+
--ta-shadow-sm: 0 2px 4px rgba(0,0,0,0.06);
|
|
32
|
+
--ta-shadow-md: 0 4px 12px rgba(0,0,0,0.08);
|
|
33
|
+
--ta-shadow-lg: 0 8px 24px rgba(0,0,0,0.12);
|
|
34
|
+
|
|
35
|
+
--ta-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
|
36
|
+
--ta-font-mono: "SF Mono", Monaco, "Cascadia Code", Consolas, monospace;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
40
|
+
|
|
41
|
+
body {
|
|
42
|
+
font-family: var(--ta-font);
|
|
43
|
+
background: var(--ta-bg);
|
|
44
|
+
color: var(--ta-gray-900);
|
|
45
|
+
font-size: 14px;
|
|
46
|
+
line-height: 1.5;
|
|
47
|
+
-webkit-font-smoothing: antialiased;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
a { color: var(--ta-blue); text-decoration: none; }
|
|
51
|
+
a:hover { text-decoration: underline; }
|
|
52
|
+
|
|
53
|
+
/* Layout */
|
|
54
|
+
.ta-layout { display: flex; min-height: 100vh; }
|
|
55
|
+
.ta-sidebar {
|
|
56
|
+
width: 220px; flex-shrink: 0;
|
|
57
|
+
background: var(--ta-surface);
|
|
58
|
+
border-right: 1px solid var(--ta-border);
|
|
59
|
+
padding: 24px 0;
|
|
60
|
+
position: sticky; top: 0; height: 100vh; overflow-y: auto;
|
|
61
|
+
}
|
|
62
|
+
.ta-main { flex: 1; padding: 32px; max-width: 1200px; }
|
|
63
|
+
|
|
64
|
+
/* Sidebar */
|
|
65
|
+
.ta-sidebar-brand {
|
|
66
|
+
display: flex; align-items: center; gap: 10px;
|
|
67
|
+
padding: 0 20px 24px;
|
|
68
|
+
border-bottom: 1px solid var(--ta-border);
|
|
69
|
+
margin-bottom: 16px;
|
|
70
|
+
}
|
|
71
|
+
.ta-sidebar-brand svg { width: 28px; height: 28px; }
|
|
72
|
+
.ta-sidebar-brand span { font-weight: 700; font-size: 15px; letter-spacing: -0.3px; }
|
|
73
|
+
.ta-nav-item {
|
|
74
|
+
display: flex; align-items: center; gap: 10px;
|
|
75
|
+
padding: 10px 20px; color: var(--ta-gray-700);
|
|
76
|
+
font-size: 14px; font-weight: 500;
|
|
77
|
+
border-radius: 0; transition: all 0.15s;
|
|
78
|
+
cursor: pointer; text-decoration: none;
|
|
79
|
+
}
|
|
80
|
+
.ta-nav-item:hover { background: var(--ta-gray-100); color: var(--ta-gray-900); text-decoration: none; }
|
|
81
|
+
.ta-nav-item.active { color: var(--ta-blue); background: rgba(0,122,255,0.08); }
|
|
82
|
+
.ta-nav-item svg { width: 18px; height: 18px; flex-shrink: 0; }
|
|
83
|
+
|
|
84
|
+
/* Cards */
|
|
85
|
+
.ta-card {
|
|
86
|
+
background: var(--ta-surface);
|
|
87
|
+
border: 1px solid var(--ta-border);
|
|
88
|
+
border-radius: var(--ta-radius-md);
|
|
89
|
+
box-shadow: var(--ta-shadow-sm);
|
|
90
|
+
}
|
|
91
|
+
.ta-card-header {
|
|
92
|
+
padding: 16px 20px;
|
|
93
|
+
border-bottom: 1px solid var(--ta-border);
|
|
94
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
95
|
+
}
|
|
96
|
+
.ta-card-header h2 { font-size: 15px; font-weight: 600; letter-spacing: -0.2px; }
|
|
97
|
+
.ta-card-body { padding: 20px; }
|
|
98
|
+
|
|
99
|
+
/* Hero metrics grid */
|
|
100
|
+
.ta-hero-grid {
|
|
101
|
+
display: grid;
|
|
102
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
103
|
+
gap: 12px; margin-bottom: 24px;
|
|
104
|
+
}
|
|
105
|
+
.ta-hero-card {
|
|
106
|
+
background: var(--ta-surface);
|
|
107
|
+
border: 1px solid var(--ta-border);
|
|
108
|
+
border-radius: var(--ta-radius-md);
|
|
109
|
+
padding: 20px;
|
|
110
|
+
display: flex; align-items: flex-start; gap: 14px;
|
|
111
|
+
box-shadow: var(--ta-shadow-sm);
|
|
112
|
+
transition: box-shadow 0.2s;
|
|
113
|
+
}
|
|
114
|
+
.ta-hero-card:hover { box-shadow: var(--ta-shadow-md); }
|
|
115
|
+
.ta-hero-icon {
|
|
116
|
+
width: 40px; height: 40px; border-radius: var(--ta-radius-sm);
|
|
117
|
+
display: flex; align-items: center; justify-content: center;
|
|
118
|
+
flex-shrink: 0;
|
|
119
|
+
}
|
|
120
|
+
.ta-hero-icon svg { width: 20px; height: 20px; }
|
|
121
|
+
.ta-hero-card--blue .ta-hero-icon { background: rgba(0,122,255,0.12); color: var(--ta-blue); }
|
|
122
|
+
.ta-hero-card--green .ta-hero-icon { background: rgba(52,199,89,0.12); color: var(--ta-green); }
|
|
123
|
+
.ta-hero-card--orange .ta-hero-icon { background: rgba(255,149,0,0.12); color: var(--ta-orange); }
|
|
124
|
+
.ta-hero-card--teal .ta-hero-icon { background: rgba(90,200,250,0.12); color: var(--ta-teal); }
|
|
125
|
+
.ta-hero-label { font-size: 12px; font-weight: 500; color: var(--ta-gray-600); text-transform: uppercase; letter-spacing: 0.4px; }
|
|
126
|
+
.ta-hero-value { font-size: 28px; font-weight: 700; letter-spacing: -0.8px; line-height: 1.1; margin: 4px 0 2px; }
|
|
127
|
+
.ta-hero-meta { font-size: 12px; color: var(--ta-gray-600); }
|
|
128
|
+
|
|
129
|
+
/* Tables */
|
|
130
|
+
.ta-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
131
|
+
.ta-table th { padding: 10px 14px; text-align: left; font-weight: 600; font-size: 12px; color: var(--ta-gray-600); text-transform: uppercase; letter-spacing: 0.4px; border-bottom: 1px solid var(--ta-border); }
|
|
132
|
+
.ta-table td { padding: 11px 14px; border-bottom: 1px solid var(--ta-gray-100); }
|
|
133
|
+
.ta-table tr:last-child td { border-bottom: none; }
|
|
134
|
+
.ta-table tr:hover td { background: var(--ta-gray-50); }
|
|
135
|
+
|
|
136
|
+
/* Badges */
|
|
137
|
+
.ta-badge {
|
|
138
|
+
display: inline-flex; align-items: center;
|
|
139
|
+
padding: 3px 8px; border-radius: 99px;
|
|
140
|
+
font-size: 11px; font-weight: 600; letter-spacing: 0.2px;
|
|
141
|
+
}
|
|
142
|
+
.ta-badge--blue { background: rgba(0,122,255,0.1); color: var(--ta-blue); }
|
|
143
|
+
.ta-badge--green { background: rgba(52,199,89,0.1); color: var(--ta-green); }
|
|
144
|
+
.ta-badge--orange { background: rgba(255,149,0,0.1); color: var(--ta-orange); }
|
|
145
|
+
.ta-badge--red { background: rgba(255,59,48,0.1); color: var(--ta-red); }
|
|
146
|
+
.ta-badge--gray { background: var(--ta-gray-100); color: var(--ta-gray-700); }
|
|
147
|
+
|
|
148
|
+
/* Status dot */
|
|
149
|
+
.ta-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
|
150
|
+
.ta-dot--green { background: var(--ta-green); }
|
|
151
|
+
.ta-dot--orange { background: var(--ta-orange); }
|
|
152
|
+
.ta-dot--red { background: var(--ta-red); }
|
|
153
|
+
|
|
154
|
+
/* Buttons */
|
|
155
|
+
.ta-btn {
|
|
156
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
157
|
+
padding: 8px 16px; border-radius: var(--ta-radius-sm);
|
|
158
|
+
font-size: 13px; font-weight: 500; border: none; cursor: pointer;
|
|
159
|
+
transition: all 0.15s;
|
|
160
|
+
}
|
|
161
|
+
.ta-btn--primary { background: var(--ta-blue); color: #fff; }
|
|
162
|
+
.ta-btn--primary:hover { background: #0051d5; }
|
|
163
|
+
.ta-btn--ghost { background: var(--ta-gray-100); color: var(--ta-gray-800); }
|
|
164
|
+
.ta-btn--ghost:hover { background: var(--ta-gray-200); }
|
|
165
|
+
.ta-btn--danger { background: rgba(255,59,48,0.1); color: var(--ta-red); }
|
|
166
|
+
.ta-btn--danger:hover { background: rgba(255,59,48,0.2); }
|
|
167
|
+
|
|
168
|
+
/* Misc */
|
|
169
|
+
.ta-page-title { font-size: 22px; font-weight: 700; letter-spacing: -0.5px; margin-bottom: 4px; }
|
|
170
|
+
.ta-page-subtitle { color: var(--ta-gray-600); margin-bottom: 24px; }
|
|
171
|
+
.ta-section { margin-bottom: 24px; }
|
|
172
|
+
.ta-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
173
|
+
.ta-empty { text-align: center; padding: 48px; color: var(--ta-gray-500); }
|
|
174
|
+
.ta-empty p { margin-top: 8px; font-size: 13px; }
|
|
175
|
+
code { font-family: var(--ta-font-mono); background: var(--ta-gray-100); padding: 2px 6px; border-radius: 4px; font-size: 12px; }
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/dashboard/ui/pages/BotAnalyticsPage.tsx
|
|
31
|
+
var BotAnalyticsPage_exports = {};
|
|
32
|
+
__export(BotAnalyticsPage_exports, {
|
|
33
|
+
BotAnalyticsPage: () => BotAnalyticsPage
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(BotAnalyticsPage_exports);
|
|
36
|
+
|
|
37
|
+
// src/analytics/performance-stats.ts
|
|
38
|
+
var import_fs = __toESM(require("fs"));
|
|
39
|
+
var import_path = __toESM(require("path"));
|
|
40
|
+
var PerformanceStats = class {
|
|
41
|
+
constructor(dataDir = process.env.TA_DATA_DIR ?? "data") {
|
|
42
|
+
this.dataDir = dataDir;
|
|
43
|
+
}
|
|
44
|
+
compute(days = 30) {
|
|
45
|
+
const records = this.loadRecords(days);
|
|
46
|
+
const totalVisits = records.length;
|
|
47
|
+
const uniqueBots = [...new Set(records.map((r) => r.bot_name).filter(Boolean))];
|
|
48
|
+
const withResponseMs = records.filter((r) => r.response_ms !== null);
|
|
49
|
+
const avgResponseMs = withResponseMs.length > 0 ? withResponseMs.reduce((s, r) => s + r.response_ms, 0) / withResponseMs.length : null;
|
|
50
|
+
const cacheHitRate = records.length > 0 ? records.filter((r) => r.cache_hit).length / records.length : null;
|
|
51
|
+
const pageCounts = /* @__PURE__ */ new Map();
|
|
52
|
+
const botCounts = /* @__PURE__ */ new Map();
|
|
53
|
+
const dayCounts = /* @__PURE__ */ new Map();
|
|
54
|
+
for (const r of records) {
|
|
55
|
+
pageCounts.set(r.url, (pageCounts.get(r.url) ?? 0) + 1);
|
|
56
|
+
const name = r.bot_name ?? "unknown";
|
|
57
|
+
botCounts.set(name, (botCounts.get(name) ?? 0) + 1);
|
|
58
|
+
const day = r.timestamp.slice(0, 10);
|
|
59
|
+
dayCounts.set(day, (dayCounts.get(day) ?? 0) + 1);
|
|
60
|
+
}
|
|
61
|
+
const topPages = [...pageCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([url, visits]) => ({ url, visits }));
|
|
62
|
+
const topBots = [...botCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([name, visits]) => ({ name, visits }));
|
|
63
|
+
const visitsByDay = [...dayCounts.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([date, visits]) => ({ date, visits }));
|
|
64
|
+
return { totalVisits, uniqueBots, avgResponseMs, cacheHitRate, topPages, topBots, visitsByDay };
|
|
65
|
+
}
|
|
66
|
+
loadRecords(days) {
|
|
67
|
+
const filePath = import_path.default.join(this.dataDir, "ta-visits.jsonl");
|
|
68
|
+
if (!import_fs.default.existsSync(filePath)) return [];
|
|
69
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
70
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
71
|
+
const cutoffStr = cutoff.toISOString();
|
|
72
|
+
return import_fs.default.readFileSync(filePath, "utf-8").split("\n").filter(Boolean).map((line) => {
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(line);
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}).filter((r) => r !== null && r.timestamp >= cutoffStr);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/dashboard/ui/components/HeroCard.tsx
|
|
83
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
84
|
+
function HeroCard({ label, value, meta, color = "blue", icon }) {
|
|
85
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `ta-hero-card ta-hero-card--${color}`, children: [
|
|
86
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ta-hero-icon", children: icon }),
|
|
87
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
88
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ta-hero-label", children: label }),
|
|
89
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ta-hero-value", children: value }),
|
|
90
|
+
meta && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ta-hero-meta", children: meta })
|
|
91
|
+
] })
|
|
92
|
+
] });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/dashboard/ui/components/Card.tsx
|
|
96
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
97
|
+
function Card({ title, action, children }) {
|
|
98
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "ta-card ta-section", children: [
|
|
99
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "ta-card-header", children: [
|
|
100
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { children: title }),
|
|
101
|
+
action
|
|
102
|
+
] }),
|
|
103
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "ta-card-body", children })
|
|
104
|
+
] });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/dashboard/ui/components/VisitsChart.tsx
|
|
108
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
109
|
+
function VisitsChart({ data, height = 160 }) {
|
|
110
|
+
if (!data.length) {
|
|
111
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "ta-empty", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: "No visit data yet." }) });
|
|
112
|
+
}
|
|
113
|
+
const max = Math.max(...data.map((d) => d.visits), 1);
|
|
114
|
+
const barWidth = Math.max(4, Math.floor(560 / data.length) - 2);
|
|
115
|
+
const showLabel = data.length <= 14;
|
|
116
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { overflowX: "auto" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
117
|
+
"svg",
|
|
118
|
+
{
|
|
119
|
+
width: "100%",
|
|
120
|
+
viewBox: `0 0 ${Math.max(data.length * (barWidth + 2), 560)} ${height + 40}`,
|
|
121
|
+
style: { display: "block", minWidth: 320 },
|
|
122
|
+
children: data.map((d, i) => {
|
|
123
|
+
const barH = Math.max(2, Math.round(d.visits / max * height));
|
|
124
|
+
const x = i * (barWidth + 2);
|
|
125
|
+
const y = height - barH;
|
|
126
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("g", { children: [
|
|
127
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
128
|
+
"rect",
|
|
129
|
+
{
|
|
130
|
+
x,
|
|
131
|
+
y,
|
|
132
|
+
width: barWidth,
|
|
133
|
+
height: barH,
|
|
134
|
+
rx: 3,
|
|
135
|
+
fill: "var(--ta-blue)",
|
|
136
|
+
opacity: 0.85,
|
|
137
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("title", { children: `${d.date}: ${d.visits} visits` })
|
|
138
|
+
}
|
|
139
|
+
),
|
|
140
|
+
showLabel && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
141
|
+
"text",
|
|
142
|
+
{
|
|
143
|
+
x: x + barWidth / 2,
|
|
144
|
+
y: height + 16,
|
|
145
|
+
textAnchor: "middle",
|
|
146
|
+
fontSize: 9,
|
|
147
|
+
fill: "var(--ta-gray-500)",
|
|
148
|
+
children: [
|
|
149
|
+
d.date.slice(5),
|
|
150
|
+
" "
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
] }, d.date);
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
) });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/dashboard/ui/pages/BotAnalyticsPage.tsx
|
|
161
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
162
|
+
var BOT_COLORS = {
|
|
163
|
+
ClaudeBot: "orange",
|
|
164
|
+
GPTBot: "green",
|
|
165
|
+
PerplexityBot: "blue",
|
|
166
|
+
"Googlebot-AI": "teal",
|
|
167
|
+
default: "gray"
|
|
168
|
+
};
|
|
169
|
+
async function BotAnalyticsPage() {
|
|
170
|
+
const stats = new PerformanceStats();
|
|
171
|
+
const summary = stats.compute(30);
|
|
172
|
+
const cacheRate = summary.cacheHitRate !== null ? `${(summary.cacheHitRate * 100).toFixed(0)}%` : "\u2014";
|
|
173
|
+
const avgMs = summary.avgResponseMs !== null ? `${Math.round(summary.avgResponseMs)}ms` : "\u2014";
|
|
174
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
|
|
175
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h1", { className: "ta-page-title", children: "Bot Analytics" }),
|
|
176
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "ta-page-subtitle", children: "AI crawler visits over the last 30 days" }),
|
|
177
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "ta-hero-grid", children: [
|
|
178
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
179
|
+
HeroCard,
|
|
180
|
+
{
|
|
181
|
+
label: "Total Bot Visits",
|
|
182
|
+
value: summary.totalVisits.toLocaleString(),
|
|
183
|
+
meta: `${summary.uniqueBots.length} unique bots`,
|
|
184
|
+
color: "blue",
|
|
185
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("polyline", { points: "22 12 18 12 15 21 9 3 6 12 2 12" }) })
|
|
186
|
+
}
|
|
187
|
+
),
|
|
188
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
189
|
+
HeroCard,
|
|
190
|
+
{
|
|
191
|
+
label: "Unique Pages Crawled",
|
|
192
|
+
value: summary.topPages.length.toLocaleString(),
|
|
193
|
+
meta: "distinct URLs visited",
|
|
194
|
+
color: "teal",
|
|
195
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, children: [
|
|
196
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
|
|
197
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("polyline", { points: "14 2 14 8 20 8" })
|
|
198
|
+
] })
|
|
199
|
+
}
|
|
200
|
+
),
|
|
201
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
202
|
+
HeroCard,
|
|
203
|
+
{
|
|
204
|
+
label: "Cache Hit Rate",
|
|
205
|
+
value: cacheRate,
|
|
206
|
+
meta: "higher = faster bot response",
|
|
207
|
+
color: "green",
|
|
208
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, children: [
|
|
209
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M22 11.08V12a10 10 0 1 1-5.93-9.14" }),
|
|
210
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("polyline", { points: "22 4 12 14.01 9 11.01" })
|
|
211
|
+
] })
|
|
212
|
+
}
|
|
213
|
+
),
|
|
214
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
215
|
+
HeroCard,
|
|
216
|
+
{
|
|
217
|
+
label: "Avg Response",
|
|
218
|
+
value: avgMs,
|
|
219
|
+
meta: "for markdown requests",
|
|
220
|
+
color: "orange",
|
|
221
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, children: [
|
|
222
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
|
|
223
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("polyline", { points: "12 6 12 12 16 14" })
|
|
224
|
+
] })
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
] }),
|
|
228
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Card, { title: "Daily Visits (30 days)", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(VisitsChart, { data: summary.visitsByDay }) }),
|
|
229
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "ta-grid-2", children: [
|
|
230
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Card, { title: "Top Bots", children: summary.topBots.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "ta-empty", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { children: "No bot visits recorded yet." }) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("table", { className: "ta-table", children: [
|
|
231
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
|
|
232
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: "Bot" }),
|
|
233
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: "Visits" }),
|
|
234
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: "Share" })
|
|
235
|
+
] }) }),
|
|
236
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("tbody", { children: summary.topBots.map((b) => {
|
|
237
|
+
const pct = summary.totalVisits > 0 ? (b.visits / summary.totalVisits * 100).toFixed(1) : "0";
|
|
238
|
+
const color = BOT_COLORS[b.name] ?? BOT_COLORS.default;
|
|
239
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
|
|
240
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: `ta-badge ta-badge--${color}`, children: b.name }) }),
|
|
241
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { children: b.visits.toLocaleString() }) }),
|
|
242
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
|
|
243
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { flex: 1, height: 6, background: "var(--ta-gray-100)", borderRadius: 3 }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { width: `${pct}%`, height: "100%", background: "var(--ta-blue)", borderRadius: 3 } }) }),
|
|
244
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: { fontSize: 12, color: "var(--ta-gray-600)", width: 36 }, children: [
|
|
245
|
+
pct,
|
|
246
|
+
"%"
|
|
247
|
+
] })
|
|
248
|
+
] }) })
|
|
249
|
+
] }, b.name);
|
|
250
|
+
}) })
|
|
251
|
+
] }) }),
|
|
252
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Card, { title: "Top Pages Crawled", children: summary.topPages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "ta-empty", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { children: "No pages crawled yet." }) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("table", { className: "ta-table", children: [
|
|
253
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
|
|
254
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: "Page" }),
|
|
255
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: "Visits" })
|
|
256
|
+
] }) }),
|
|
257
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("tbody", { children: summary.topPages.map((p) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
|
|
258
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("a", { href: p.url, target: "_blank", rel: "noreferrer", style: { fontFamily: "var(--ta-font-mono)", fontSize: 12 }, children: p.url }) }),
|
|
259
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { children: p.visits.toLocaleString() }) })
|
|
260
|
+
] }, p.url)) })
|
|
261
|
+
] }) })
|
|
262
|
+
] })
|
|
263
|
+
] });
|
|
264
|
+
}
|
|
265
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
266
|
+
0 && (module.exports = {
|
|
267
|
+
BotAnalyticsPage
|
|
268
|
+
});
|
|
269
|
+
//# sourceMappingURL=BotAnalyticsPage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/dashboard/ui/pages/BotAnalyticsPage.tsx","../../../../src/analytics/performance-stats.ts","../../../../src/dashboard/ui/components/HeroCard.tsx","../../../../src/dashboard/ui/components/Card.tsx","../../../../src/dashboard/ui/components/VisitsChart.tsx"],"sourcesContent":["import { PerformanceStats } from '../../../analytics/performance-stats.js'\nimport { HeroCard } from '../components/HeroCard.js'\nimport { Card } from '../components/Card.js'\nimport { VisitsChart } from '../components/VisitsChart.js'\n\nconst BOT_COLORS: Record<string, string> = {\n ClaudeBot: 'orange', GPTBot: 'green', PerplexityBot: 'blue',\n 'Googlebot-AI': 'teal', default: 'gray',\n}\n\nexport async function BotAnalyticsPage() {\n const stats = new PerformanceStats()\n const summary = stats.compute(30)\n\n const cacheRate = summary.cacheHitRate !== null\n ? `${(summary.cacheHitRate * 100).toFixed(0)}%` : '—'\n const avgMs = summary.avgResponseMs !== null\n ? `${Math.round(summary.avgResponseMs)}ms` : '—'\n\n return (\n <div>\n <h1 className=\"ta-page-title\">Bot Analytics</h1>\n <p className=\"ta-page-subtitle\">AI crawler visits over the last 30 days</p>\n\n {/* Hero metrics */}\n <div className=\"ta-hero-grid\">\n <HeroCard\n label=\"Total Bot Visits\"\n value={summary.totalVisits.toLocaleString()}\n meta={`${summary.uniqueBots.length} unique bots`}\n color=\"blue\"\n icon={<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2}><polyline points=\"22 12 18 12 15 21 9 3 6 12 2 12\" /></svg>}\n />\n <HeroCard\n label=\"Unique Pages Crawled\"\n value={summary.topPages.length.toLocaleString()}\n meta=\"distinct URLs visited\"\n color=\"teal\"\n icon={<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2}><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"/><polyline points=\"14 2 14 8 20 8\"/></svg>}\n />\n <HeroCard\n label=\"Cache Hit Rate\"\n value={cacheRate}\n meta=\"higher = faster bot response\"\n color=\"green\"\n icon={<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2}><path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><polyline points=\"22 4 12 14.01 9 11.01\"/></svg>}\n />\n <HeroCard\n label=\"Avg Response\"\n value={avgMs}\n meta=\"for markdown requests\"\n color=\"orange\"\n icon={<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2}><circle cx=\"12\" cy=\"12\" r=\"10\"/><polyline points=\"12 6 12 12 16 14\"/></svg>}\n />\n </div>\n\n {/* Visits chart */}\n <Card title=\"Daily Visits (30 days)\">\n <VisitsChart data={summary.visitsByDay} />\n </Card>\n\n <div className=\"ta-grid-2\">\n {/* Top bots */}\n <Card title=\"Top Bots\">\n {summary.topBots.length === 0 ? (\n <div className=\"ta-empty\"><p>No bot visits recorded yet.</p></div>\n ) : (\n <table className=\"ta-table\">\n <thead>\n <tr><th>Bot</th><th>Visits</th><th>Share</th></tr>\n </thead>\n <tbody>\n {summary.topBots.map(b => {\n const pct = summary.totalVisits > 0\n ? ((b.visits / summary.totalVisits) * 100).toFixed(1)\n : '0'\n const color = BOT_COLORS[b.name] ?? BOT_COLORS.default\n return (\n <tr key={b.name}>\n <td>\n <span className={`ta-badge ta-badge--${color}`}>{b.name}</span>\n </td>\n <td><strong>{b.visits.toLocaleString()}</strong></td>\n <td>\n <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>\n <div style={{ flex: 1, height: 6, background: 'var(--ta-gray-100)', borderRadius: 3 }}>\n <div style={{ width: `${pct}%`, height: '100%', background: 'var(--ta-blue)', borderRadius: 3 }} />\n </div>\n <span style={{ fontSize: 12, color: 'var(--ta-gray-600)', width: 36 }}>{pct}%</span>\n </div>\n </td>\n </tr>\n )\n })}\n </tbody>\n </table>\n )}\n </Card>\n\n {/* Top pages */}\n <Card title=\"Top Pages Crawled\">\n {summary.topPages.length === 0 ? (\n <div className=\"ta-empty\"><p>No pages crawled yet.</p></div>\n ) : (\n <table className=\"ta-table\">\n <thead>\n <tr><th>Page</th><th>Visits</th></tr>\n </thead>\n <tbody>\n {summary.topPages.map(p => (\n <tr key={p.url}>\n <td>\n <a href={p.url} target=\"_blank\" rel=\"noreferrer\" style={{ fontFamily: 'var(--ta-font-mono)', fontSize: 12 }}>\n {p.url}\n </a>\n </td>\n <td><strong>{p.visits.toLocaleString()}</strong></td>\n </tr>\n ))}\n </tbody>\n </table>\n )}\n </Card>\n </div>\n </div>\n )\n}\n","import fs from 'fs'\nimport path from 'path'\nimport type { VisitRecord } from './visit-tracker.js'\n\nexport interface PerformanceSummary {\n totalVisits: number\n uniqueBots: string[]\n avgResponseMs: number | null\n cacheHitRate: number | null\n topPages: Array<{ url: string; visits: number }>\n topBots: Array<{ name: string; visits: number }>\n visitsByDay: Array<{ date: string; visits: number }>\n}\n\nexport class PerformanceStats {\n private dataDir: string\n\n constructor(dataDir = process.env.TA_DATA_DIR ?? 'data') {\n this.dataDir = dataDir\n }\n\n compute(days = 30): PerformanceSummary {\n const records = this.loadRecords(days)\n\n const totalVisits = records.length\n const uniqueBots = [...new Set(records.map(r => r.bot_name).filter(Boolean))] as string[]\n\n const withResponseMs = records.filter(r => r.response_ms !== null)\n const avgResponseMs = withResponseMs.length > 0\n ? withResponseMs.reduce((s, r) => s + r.response_ms!, 0) / withResponseMs.length\n : null\n\n const cacheHitRate = records.length > 0\n ? records.filter(r => r.cache_hit).length / records.length\n : null\n\n const pageCounts = new Map<string, number>()\n const botCounts = new Map<string, number>()\n const dayCounts = new Map<string, number>()\n\n for (const r of records) {\n pageCounts.set(r.url, (pageCounts.get(r.url) ?? 0) + 1)\n const name = r.bot_name ?? 'unknown'\n botCounts.set(name, (botCounts.get(name) ?? 0) + 1)\n const day = r.timestamp.slice(0, 10)\n dayCounts.set(day, (dayCounts.get(day) ?? 0) + 1)\n }\n\n const topPages = [...pageCounts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 10)\n .map(([url, visits]) => ({ url, visits }))\n\n const topBots = [...botCounts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 10)\n .map(([name, visits]) => ({ name, visits }))\n\n const visitsByDay = [...dayCounts.entries()]\n .sort((a, b) => a[0].localeCompare(b[0]))\n .map(([date, visits]) => ({ date, visits }))\n\n return { totalVisits, uniqueBots, avgResponseMs, cacheHitRate, topPages, topBots, visitsByDay }\n }\n\n private loadRecords(days: number): VisitRecord[] {\n const filePath = path.join(this.dataDir, 'ta-visits.jsonl')\n if (!fs.existsSync(filePath)) return []\n\n const cutoff = new Date()\n cutoff.setDate(cutoff.getDate() - days)\n const cutoffStr = cutoff.toISOString()\n\n return fs.readFileSync(filePath, 'utf-8')\n .split('\\n')\n .filter(Boolean)\n .map(line => { try { return JSON.parse(line) as VisitRecord } catch { return null } })\n .filter((r): r is VisitRecord => r !== null && r.timestamp >= cutoffStr)\n }\n}\n","import type { ReactNode } from 'react'\n\ninterface HeroCardProps {\n label: string\n value: string | number\n meta?: string\n color?: 'blue' | 'green' | 'orange' | 'teal'\n icon: ReactNode\n}\n\nexport function HeroCard({ label, value, meta, color = 'blue', icon }: HeroCardProps) {\n return (\n <div className={`ta-hero-card ta-hero-card--${color}`}>\n <div className=\"ta-hero-icon\">{icon}</div>\n <div>\n <div className=\"ta-hero-label\">{label}</div>\n <div className=\"ta-hero-value\">{value}</div>\n {meta && <div className=\"ta-hero-meta\">{meta}</div>}\n </div>\n </div>\n )\n}\n","import type { ReactNode } from 'react'\n\ninterface CardProps {\n title: string\n action?: ReactNode\n children: ReactNode\n}\n\nexport function Card({ title, action, children }: CardProps) {\n return (\n <div className=\"ta-card ta-section\">\n <div className=\"ta-card-header\">\n <h2>{title}</h2>\n {action}\n </div>\n <div className=\"ta-card-body\">{children}</div>\n </div>\n )\n}\n","'use client'\n\ninterface DayData { date: string; visits: number }\n\ninterface VisitsChartProps {\n data: DayData[]\n height?: number\n}\n\nexport function VisitsChart({ data, height = 160 }: VisitsChartProps) {\n if (!data.length) {\n return <div className=\"ta-empty\"><p>No visit data yet.</p></div>\n }\n\n const max = Math.max(...data.map(d => d.visits), 1)\n const barWidth = Math.max(4, Math.floor(560 / data.length) - 2)\n const showLabel = data.length <= 14\n\n return (\n <div style={{ overflowX: 'auto' }}>\n <svg\n width=\"100%\"\n viewBox={`0 0 ${Math.max(data.length * (barWidth + 2), 560)} ${height + 40}`}\n style={{ display: 'block', minWidth: 320 }}\n >\n {data.map((d, i) => {\n const barH = Math.max(2, Math.round((d.visits / max) * height))\n const x = i * (barWidth + 2)\n const y = height - barH\n return (\n <g key={d.date}>\n <rect\n x={x} y={y}\n width={barWidth} height={barH}\n rx={3}\n fill=\"var(--ta-blue)\"\n opacity={0.85}\n >\n <title>{`${d.date}: ${d.visits} visits`}</title>\n </rect>\n {showLabel && (\n <text\n x={x + barWidth / 2} y={height + 16}\n textAnchor=\"middle\"\n fontSize={9}\n fill=\"var(--ta-gray-500)\"\n >\n {d.date.slice(5)} {/* MM-DD */}\n </text>\n )}\n </g>\n )\n })}\n </svg>\n </div>\n )\n}\n"],"mappings":"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","names":["path","fs","import_jsx_runtime","import_jsx_runtime","import_jsx_runtime"]}
|