nuxt-ai-ready 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/runtime/server/db/queries.d.ts +46 -0
- package/dist/runtime/server/db/queries.js +73 -0
- package/dist/runtime/server/db/schema-sql.d.ts +1 -1
- package/dist/runtime/server/db/schema-sql.js +19 -2
- package/dist/runtime/server/middleware/markdown.js +13 -1
- package/dist/runtime/server/routes/__ai-ready-debug.get.d.ts +25 -0
- package/dist/runtime/server/routes/__ai-ready-debug.get.js +45 -1
- package/dist/runtime/server/utils/runCron.d.ts +1 -0
- package/dist/runtime/server/utils/runCron.js +20 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -162,3 +162,49 @@ export interface IndexNowStats {
|
|
|
162
162
|
* Get IndexNow stats
|
|
163
163
|
*/
|
|
164
164
|
export declare function getIndexNowStats(event: H3Event | undefined): Promise<IndexNowStats>;
|
|
165
|
+
export interface CronRunRow {
|
|
166
|
+
id: number;
|
|
167
|
+
started_at: number;
|
|
168
|
+
finished_at: number | null;
|
|
169
|
+
duration_ms: number | null;
|
|
170
|
+
pages_indexed: number;
|
|
171
|
+
pages_remaining: number;
|
|
172
|
+
indexnow_submitted: number;
|
|
173
|
+
indexnow_remaining: number;
|
|
174
|
+
errors: string;
|
|
175
|
+
status: 'running' | 'success' | 'partial' | 'error';
|
|
176
|
+
}
|
|
177
|
+
export interface CronRun {
|
|
178
|
+
id: number;
|
|
179
|
+
startedAt: number;
|
|
180
|
+
finishedAt: number | null;
|
|
181
|
+
durationMs: number | null;
|
|
182
|
+
pagesIndexed: number;
|
|
183
|
+
pagesRemaining: number;
|
|
184
|
+
indexNowSubmitted: number;
|
|
185
|
+
indexNowRemaining: number;
|
|
186
|
+
errors: string[];
|
|
187
|
+
status: 'running' | 'success' | 'partial' | 'error';
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Start a cron run and return its ID
|
|
191
|
+
*/
|
|
192
|
+
export declare function startCronRun(event: H3Event | undefined): Promise<number | null>;
|
|
193
|
+
/**
|
|
194
|
+
* Complete a cron run with results
|
|
195
|
+
*/
|
|
196
|
+
export declare function completeCronRun(event: H3Event | undefined, runId: number, result: {
|
|
197
|
+
pagesIndexed: number;
|
|
198
|
+
pagesRemaining: number;
|
|
199
|
+
indexNowSubmitted: number;
|
|
200
|
+
indexNowRemaining: number;
|
|
201
|
+
errors: string[];
|
|
202
|
+
}): Promise<void>;
|
|
203
|
+
/**
|
|
204
|
+
* Get recent cron runs
|
|
205
|
+
*/
|
|
206
|
+
export declare function getRecentCronRuns(event: H3Event | undefined, limit?: number): Promise<CronRun[]>;
|
|
207
|
+
/**
|
|
208
|
+
* Clean up old cron runs (keep last N)
|
|
209
|
+
*/
|
|
210
|
+
export declare function cleanupOldCronRuns(event: H3Event | undefined, keepCount?: number): Promise<number>;
|
|
@@ -374,3 +374,76 @@ export async function getIndexNowStats(event) {
|
|
|
374
374
|
lastError: stats.indexnow_last_error || null
|
|
375
375
|
};
|
|
376
376
|
}
|
|
377
|
+
function rowToCronRun(row) {
|
|
378
|
+
return {
|
|
379
|
+
id: row.id,
|
|
380
|
+
startedAt: row.started_at,
|
|
381
|
+
finishedAt: row.finished_at,
|
|
382
|
+
durationMs: row.duration_ms,
|
|
383
|
+
pagesIndexed: row.pages_indexed,
|
|
384
|
+
pagesRemaining: row.pages_remaining,
|
|
385
|
+
indexNowSubmitted: row.indexnow_submitted,
|
|
386
|
+
indexNowRemaining: row.indexnow_remaining,
|
|
387
|
+
errors: JSON.parse(row.errors || "[]"),
|
|
388
|
+
status: row.status
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
export async function startCronRun(event) {
|
|
392
|
+
const db = await getDb(event);
|
|
393
|
+
if (!db)
|
|
394
|
+
return null;
|
|
395
|
+
const now = Date.now();
|
|
396
|
+
await db.exec(
|
|
397
|
+
"INSERT INTO ai_ready_cron_runs (started_at, status) VALUES (?, ?)",
|
|
398
|
+
[now, "running"]
|
|
399
|
+
);
|
|
400
|
+
const row = await db.first("SELECT last_insert_rowid() as id");
|
|
401
|
+
return row?.id || null;
|
|
402
|
+
}
|
|
403
|
+
export async function completeCronRun(event, runId, result) {
|
|
404
|
+
const db = await getDb(event);
|
|
405
|
+
if (!db)
|
|
406
|
+
return;
|
|
407
|
+
const now = Date.now();
|
|
408
|
+
const row = await db.first("SELECT started_at FROM ai_ready_cron_runs WHERE id = ?", [runId]);
|
|
409
|
+
const durationMs = row ? now - row.started_at : null;
|
|
410
|
+
const status = result.errors.length > 0 ? result.pagesIndexed > 0 ? "partial" : "error" : "success";
|
|
411
|
+
await db.exec(`
|
|
412
|
+
UPDATE ai_ready_cron_runs SET
|
|
413
|
+
finished_at = ?,
|
|
414
|
+
duration_ms = ?,
|
|
415
|
+
pages_indexed = ?,
|
|
416
|
+
pages_remaining = ?,
|
|
417
|
+
indexnow_submitted = ?,
|
|
418
|
+
indexnow_remaining = ?,
|
|
419
|
+
errors = ?,
|
|
420
|
+
status = ?
|
|
421
|
+
WHERE id = ?
|
|
422
|
+
`, [now, durationMs, result.pagesIndexed, result.pagesRemaining, result.indexNowSubmitted, result.indexNowRemaining, JSON.stringify(result.errors), status, runId]);
|
|
423
|
+
}
|
|
424
|
+
export async function getRecentCronRuns(event, limit = 10) {
|
|
425
|
+
const db = await getDb(event);
|
|
426
|
+
if (!db)
|
|
427
|
+
return [];
|
|
428
|
+
const rows = await db.all(
|
|
429
|
+
"SELECT * FROM ai_ready_cron_runs ORDER BY started_at DESC LIMIT ?",
|
|
430
|
+
[limit]
|
|
431
|
+
);
|
|
432
|
+
return rows.map(rowToCronRun);
|
|
433
|
+
}
|
|
434
|
+
export async function cleanupOldCronRuns(event, keepCount = 50) {
|
|
435
|
+
const db = await getDb(event);
|
|
436
|
+
if (!db)
|
|
437
|
+
return 0;
|
|
438
|
+
const countRow = await db.first("SELECT COUNT(*) as count FROM ai_ready_cron_runs");
|
|
439
|
+
const total = countRow?.count || 0;
|
|
440
|
+
if (total <= keepCount)
|
|
441
|
+
return 0;
|
|
442
|
+
const deleteCount = total - keepCount;
|
|
443
|
+
await db.exec(`
|
|
444
|
+
DELETE FROM ai_ready_cron_runs WHERE id IN (
|
|
445
|
+
SELECT id FROM ai_ready_cron_runs ORDER BY started_at ASC LIMIT ?
|
|
446
|
+
)
|
|
447
|
+
`, [deleteCount]);
|
|
448
|
+
return deleteCount;
|
|
449
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const SCHEMA_VERSION = "v1.
|
|
1
|
+
export const SCHEMA_VERSION = "v1.7.0";
|
|
2
2
|
const PAGES_TABLE_SQL = `
|
|
3
3
|
CREATE TABLE IF NOT EXISTS ai_ready_pages (
|
|
4
4
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -54,9 +54,24 @@ CREATE TABLE IF NOT EXISTS _ai_ready_info (
|
|
|
54
54
|
checksum TEXT,
|
|
55
55
|
ready INTEGER DEFAULT 0
|
|
56
56
|
)`;
|
|
57
|
+
const CRON_RUNS_TABLE_SQL = `
|
|
58
|
+
CREATE TABLE IF NOT EXISTS ai_ready_cron_runs (
|
|
59
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
60
|
+
started_at INTEGER NOT NULL,
|
|
61
|
+
finished_at INTEGER,
|
|
62
|
+
duration_ms INTEGER,
|
|
63
|
+
pages_indexed INTEGER DEFAULT 0,
|
|
64
|
+
pages_remaining INTEGER DEFAULT 0,
|
|
65
|
+
indexnow_submitted INTEGER DEFAULT 0,
|
|
66
|
+
indexnow_remaining INTEGER DEFAULT 0,
|
|
67
|
+
errors TEXT DEFAULT '[]',
|
|
68
|
+
status TEXT DEFAULT 'running'
|
|
69
|
+
)`;
|
|
70
|
+
const CRON_RUNS_INDEX_SQL = "CREATE INDEX IF NOT EXISTS idx_ai_ready_cron_runs_started ON ai_ready_cron_runs(started_at DESC)";
|
|
57
71
|
export const DROP_TABLES_SQL = [
|
|
58
72
|
"DROP TABLE IF EXISTS ai_ready_pages_fts",
|
|
59
73
|
"DROP TABLE IF EXISTS ai_ready_pages",
|
|
74
|
+
"DROP TABLE IF EXISTS ai_ready_cron_runs",
|
|
60
75
|
"DROP TABLE IF EXISTS _ai_ready_info",
|
|
61
76
|
// Legacy unprefixed tables (migration from v1.0.0)
|
|
62
77
|
"DROP TABLE IF EXISTS pages_fts",
|
|
@@ -67,5 +82,7 @@ export const ALL_SCHEMA_SQL = [
|
|
|
67
82
|
...PAGES_INDEXES_SQL,
|
|
68
83
|
FTS_TABLE_SQL,
|
|
69
84
|
...FTS_TRIGGERS_SQL,
|
|
70
|
-
INFO_TABLE_SQL
|
|
85
|
+
INFO_TABLE_SQL,
|
|
86
|
+
CRON_RUNS_TABLE_SQL,
|
|
87
|
+
CRON_RUNS_INDEX_SQL
|
|
71
88
|
];
|
|
@@ -13,7 +13,8 @@ export default defineEventHandler(async (event) => {
|
|
|
13
13
|
const { path, isExplicit } = renderInfo;
|
|
14
14
|
const config = useRuntimeConfig(event)["nuxt-ai-ready"];
|
|
15
15
|
const response = await event.fetch(path, {
|
|
16
|
-
headers: { [INTERNAL_HEADER]: "1" }
|
|
16
|
+
headers: { [INTERNAL_HEADER]: "1" },
|
|
17
|
+
redirect: "manual"
|
|
17
18
|
}).catch((e) => {
|
|
18
19
|
logger.error(`Failed to fetch HTML for ${path}`, e);
|
|
19
20
|
return null;
|
|
@@ -28,6 +29,17 @@ export default defineEventHandler(async (event) => {
|
|
|
28
29
|
}
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
32
|
+
if (response.status >= 300 && response.status < 400) {
|
|
33
|
+
const location = response.headers.get("location");
|
|
34
|
+
if (location) {
|
|
35
|
+
const redirectTarget = location.endsWith("/") ? `${location.slice(0, -1)}.md` : `${location}.md`;
|
|
36
|
+
setHeader(event, "location", redirectTarget);
|
|
37
|
+
return createError({
|
|
38
|
+
statusCode: response.status,
|
|
39
|
+
statusMessage: response.statusText
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
31
43
|
if (!response.ok) {
|
|
32
44
|
if (isExplicit) {
|
|
33
45
|
return createError({
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
interface CronRunInfo {
|
|
2
|
+
id: number;
|
|
3
|
+
startedAt: string;
|
|
4
|
+
finishedAt: string | null;
|
|
5
|
+
durationMs: number | null;
|
|
6
|
+
status: string;
|
|
7
|
+
pagesIndexed: number;
|
|
8
|
+
pagesRemaining: number;
|
|
9
|
+
indexNowSubmitted: number;
|
|
10
|
+
indexNowRemaining: number;
|
|
11
|
+
errors: string[];
|
|
12
|
+
}
|
|
1
13
|
interface DebugInfo {
|
|
2
14
|
version: string;
|
|
3
15
|
environment: {
|
|
@@ -10,6 +22,19 @@ interface DebugInfo {
|
|
|
10
22
|
cacheMaxAgeSeconds: number;
|
|
11
23
|
mdreamOptions: unknown;
|
|
12
24
|
};
|
|
25
|
+
runtimeSync?: {
|
|
26
|
+
total: number;
|
|
27
|
+
indexed: number;
|
|
28
|
+
pending: number;
|
|
29
|
+
sitemapSeededAt: string | null;
|
|
30
|
+
};
|
|
31
|
+
indexNow?: {
|
|
32
|
+
pending: number;
|
|
33
|
+
totalSubmitted: number;
|
|
34
|
+
lastSubmittedAt: string | null;
|
|
35
|
+
lastError: string | null;
|
|
36
|
+
};
|
|
37
|
+
cronRuns?: CronRunInfo[];
|
|
13
38
|
pageData: {
|
|
14
39
|
source: string;
|
|
15
40
|
pageCount: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createError, eventHandler, setHeader } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
|
-
import { queryPages } from "../db/queries.js";
|
|
3
|
+
import { countPages, countPagesNeedingIndexNowSync, getIndexNowStats, getRecentCronRuns, getSitemapSeededAt, queryPages } from "../db/queries.js";
|
|
4
4
|
export default eventHandler(async (event) => {
|
|
5
5
|
const runtimeConfig = useRuntimeConfig(event)["nuxt-ai-ready"];
|
|
6
6
|
if (!runtimeConfig.debug) {
|
|
@@ -90,6 +90,47 @@ export default eventHandler(async (event) => {
|
|
|
90
90
|
issues.push("Prerender mode but no pages found");
|
|
91
91
|
suggestions.push("Check if pages.db exists in .data/ai-ready/");
|
|
92
92
|
}
|
|
93
|
+
let runtimeSyncInfo;
|
|
94
|
+
let indexNowInfo;
|
|
95
|
+
let cronRunsInfo;
|
|
96
|
+
if (!isDev && !isPrerender) {
|
|
97
|
+
const [total, pending, sitemapSeededAt, cronRuns] = await Promise.all([
|
|
98
|
+
countPages(event),
|
|
99
|
+
countPages(event, { where: { pending: true } }),
|
|
100
|
+
getSitemapSeededAt(event),
|
|
101
|
+
getRecentCronRuns(event, 20)
|
|
102
|
+
]);
|
|
103
|
+
runtimeSyncInfo = {
|
|
104
|
+
total,
|
|
105
|
+
indexed: total - pending,
|
|
106
|
+
pending,
|
|
107
|
+
sitemapSeededAt: sitemapSeededAt ? new Date(sitemapSeededAt).toISOString() : null
|
|
108
|
+
};
|
|
109
|
+
cronRunsInfo = cronRuns.map((run) => ({
|
|
110
|
+
id: run.id,
|
|
111
|
+
startedAt: new Date(run.startedAt).toISOString(),
|
|
112
|
+
finishedAt: run.finishedAt ? new Date(run.finishedAt).toISOString() : null,
|
|
113
|
+
durationMs: run.durationMs,
|
|
114
|
+
status: run.status,
|
|
115
|
+
pagesIndexed: run.pagesIndexed,
|
|
116
|
+
pagesRemaining: run.pagesRemaining,
|
|
117
|
+
indexNowSubmitted: run.indexNowSubmitted,
|
|
118
|
+
indexNowRemaining: run.indexNowRemaining,
|
|
119
|
+
errors: run.errors
|
|
120
|
+
}));
|
|
121
|
+
if (runtimeConfig.indexNowKey) {
|
|
122
|
+
const [indexNowPending, indexNowStats] = await Promise.all([
|
|
123
|
+
countPagesNeedingIndexNowSync(event),
|
|
124
|
+
getIndexNowStats(event)
|
|
125
|
+
]);
|
|
126
|
+
indexNowInfo = {
|
|
127
|
+
pending: indexNowPending,
|
|
128
|
+
totalSubmitted: indexNowStats.totalSubmitted,
|
|
129
|
+
lastSubmittedAt: indexNowStats.lastSubmittedAt ? new Date(indexNowStats.lastSubmittedAt).toISOString() : null,
|
|
130
|
+
lastError: indexNowStats.lastError
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
93
134
|
const debugInfo = {
|
|
94
135
|
version: runtimeConfig.version || "unknown",
|
|
95
136
|
environment: {
|
|
@@ -102,6 +143,9 @@ export default eventHandler(async (event) => {
|
|
|
102
143
|
cacheMaxAgeSeconds: runtimeConfig.cacheMaxAgeSeconds,
|
|
103
144
|
mdreamOptions: runtimeConfig.mdreamOptions
|
|
104
145
|
},
|
|
146
|
+
runtimeSync: runtimeSyncInfo,
|
|
147
|
+
indexNow: indexNowInfo,
|
|
148
|
+
cronRuns: cronRunsInfo,
|
|
105
149
|
pageData: {
|
|
106
150
|
source,
|
|
107
151
|
pageCount: pages.length,
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
2
|
+
import { cleanupOldCronRuns, completeCronRun, startCronRun } from "../db/queries.js";
|
|
2
3
|
import { batchIndexPages } from "./batchIndex.js";
|
|
3
4
|
import { syncToIndexNow } from "./indexnow.js";
|
|
4
5
|
export async function runCron(event, options) {
|
|
5
6
|
const config = useRuntimeConfig()["nuxt-ai-ready"];
|
|
6
7
|
const results = {};
|
|
8
|
+
const allErrors = [];
|
|
9
|
+
const runId = await startCronRun(event);
|
|
10
|
+
results.runId = runId;
|
|
7
11
|
if (config.runtimeSync.enabled) {
|
|
8
12
|
const limit = options?.batchSize ?? config.runtimeSync.batchSize;
|
|
9
13
|
const indexResult = await batchIndexPages(event, {
|
|
@@ -16,6 +20,9 @@ export async function runCron(event, options) {
|
|
|
16
20
|
errors: indexResult.errors.length > 0 ? indexResult.errors : void 0,
|
|
17
21
|
complete: indexResult.complete
|
|
18
22
|
};
|
|
23
|
+
if (indexResult.errors.length > 0) {
|
|
24
|
+
allErrors.push(...indexResult.errors);
|
|
25
|
+
}
|
|
19
26
|
}
|
|
20
27
|
if (config.indexNowKey) {
|
|
21
28
|
const indexNowResult = await syncToIndexNow(event, 100).catch((err) => {
|
|
@@ -27,6 +34,19 @@ export async function runCron(event, options) {
|
|
|
27
34
|
remaining: indexNowResult.remaining,
|
|
28
35
|
error: indexNowResult.error
|
|
29
36
|
};
|
|
37
|
+
if (indexNowResult.error) {
|
|
38
|
+
allErrors.push(`IndexNow: ${indexNowResult.error}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (runId) {
|
|
42
|
+
await completeCronRun(event, runId, {
|
|
43
|
+
pagesIndexed: results.index?.indexed || 0,
|
|
44
|
+
pagesRemaining: results.index?.remaining || 0,
|
|
45
|
+
indexNowSubmitted: results.indexNow?.submitted || 0,
|
|
46
|
+
indexNowRemaining: results.indexNow?.remaining || 0,
|
|
47
|
+
errors: allErrors
|
|
48
|
+
});
|
|
49
|
+
await cleanupOldCronRuns(event, 50);
|
|
30
50
|
}
|
|
31
51
|
return results;
|
|
32
52
|
}
|