@telepat/snoopy 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/LICENSE +21 -0
- package/README.md +255 -0
- package/dist/src/cli/commands/analytics.d.ts +3 -0
- package/dist/src/cli/commands/analytics.js +147 -0
- package/dist/src/cli/commands/analytics.js.map +1 -0
- package/dist/src/cli/commands/daemon.d.ts +5 -0
- package/dist/src/cli/commands/daemon.js +85 -0
- package/dist/src/cli/commands/daemon.js.map +1 -0
- package/dist/src/cli/commands/doctor.d.ts +1 -0
- package/dist/src/cli/commands/doctor.js +106 -0
- package/dist/src/cli/commands/doctor.js.map +1 -0
- package/dist/src/cli/commands/errors.d.ts +3 -0
- package/dist/src/cli/commands/errors.js +51 -0
- package/dist/src/cli/commands/errors.js.map +1 -0
- package/dist/src/cli/commands/export.d.ts +1 -0
- package/dist/src/cli/commands/export.js +48 -0
- package/dist/src/cli/commands/export.js.map +1 -0
- package/dist/src/cli/commands/job.d.ts +16 -0
- package/dist/src/cli/commands/job.js +350 -0
- package/dist/src/cli/commands/job.js.map +1 -0
- package/dist/src/cli/commands/logs.d.ts +3 -0
- package/dist/src/cli/commands/logs.js +44 -0
- package/dist/src/cli/commands/logs.js.map +1 -0
- package/dist/src/cli/commands/selection.d.ts +19 -0
- package/dist/src/cli/commands/selection.js +182 -0
- package/dist/src/cli/commands/selection.js.map +1 -0
- package/dist/src/cli/commands/settings.d.ts +1 -0
- package/dist/src/cli/commands/settings.js +31 -0
- package/dist/src/cli/commands/settings.js.map +1 -0
- package/dist/src/cli/commands/startup.d.ts +5 -0
- package/dist/src/cli/commands/startup.js +26 -0
- package/dist/src/cli/commands/startup.js.map +1 -0
- package/dist/src/cli/flows/jobAddFlow.d.ts +26 -0
- package/dist/src/cli/flows/jobAddFlow.js +209 -0
- package/dist/src/cli/flows/jobAddFlow.js.map +1 -0
- package/dist/src/cli/flows/settingsFlow.d.ts +15 -0
- package/dist/src/cli/flows/settingsFlow.js +180 -0
- package/dist/src/cli/flows/settingsFlow.js.map +1 -0
- package/dist/src/cli/flows/settingsFlowModel.d.ts +47 -0
- package/dist/src/cli/flows/settingsFlowModel.js +143 -0
- package/dist/src/cli/flows/settingsFlowModel.js.map +1 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +138 -0
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/cli/ui/consoleUi.d.ts +13 -0
- package/dist/src/cli/ui/consoleUi.js +165 -0
- package/dist/src/cli/ui/consoleUi.js.map +1 -0
- package/dist/src/cli/ui/time.d.ts +9 -0
- package/dist/src/cli/ui/time.js +35 -0
- package/dist/src/cli/ui/time.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/scripts/e2eSmoke.d.ts +1 -0
- package/dist/src/scripts/e2eSmoke.js +102 -0
- package/dist/src/scripts/e2eSmoke.js.map +1 -0
- package/dist/src/services/analytics/analyticsService.d.ts +50 -0
- package/dist/src/services/analytics/analyticsService.js +88 -0
- package/dist/src/services/analytics/analyticsService.js.map +1 -0
- package/dist/src/services/daemonControl.d.ts +12 -0
- package/dist/src/services/daemonControl.js +58 -0
- package/dist/src/services/daemonControl.js.map +1 -0
- package/dist/src/services/db/repositories/jobsRepo.d.ts +19 -0
- package/dist/src/services/db/repositories/jobsRepo.js +164 -0
- package/dist/src/services/db/repositories/jobsRepo.js.map +1 -0
- package/dist/src/services/db/repositories/runsRepo.d.ts +58 -0
- package/dist/src/services/db/repositories/runsRepo.js +190 -0
- package/dist/src/services/db/repositories/runsRepo.js.map +1 -0
- package/dist/src/services/db/repositories/scanItemsRepo.d.ts +69 -0
- package/dist/src/services/db/repositories/scanItemsRepo.js +176 -0
- package/dist/src/services/db/repositories/scanItemsRepo.js.map +1 -0
- package/dist/src/services/db/repositories/settingsRepo.d.ts +14 -0
- package/dist/src/services/db/repositories/settingsRepo.js +132 -0
- package/dist/src/services/db/repositories/settingsRepo.js.map +1 -0
- package/dist/src/services/db/sqlite.d.ts +2 -0
- package/dist/src/services/db/sqlite.js +192 -0
- package/dist/src/services/db/sqlite.js.map +1 -0
- package/dist/src/services/export/csvResults.d.ts +10 -0
- package/dist/src/services/export/csvResults.js +42 -0
- package/dist/src/services/export/csvResults.js.map +1 -0
- package/dist/src/services/logging/logReader.d.ts +4 -0
- package/dist/src/services/logging/logReader.js +230 -0
- package/dist/src/services/logging/logReader.js.map +1 -0
- package/dist/src/services/logging/logRotation.d.ts +1 -0
- package/dist/src/services/logging/logRotation.js +30 -0
- package/dist/src/services/logging/logRotation.js.map +1 -0
- package/dist/src/services/logging/runLogger.d.ts +9 -0
- package/dist/src/services/logging/runLogger.js +42 -0
- package/dist/src/services/logging/runLogger.js.map +1 -0
- package/dist/src/services/openrouter/client.d.ts +60 -0
- package/dist/src/services/openrouter/client.js +437 -0
- package/dist/src/services/openrouter/client.js.map +1 -0
- package/dist/src/services/openrouter/prompts.d.ts +5 -0
- package/dist/src/services/openrouter/prompts.js +48 -0
- package/dist/src/services/openrouter/prompts.js.map +1 -0
- package/dist/src/services/reddit/client.d.ts +25 -0
- package/dist/src/services/reddit/client.js +186 -0
- package/dist/src/services/reddit/client.js.map +1 -0
- package/dist/src/services/scheduler/cronScheduler.d.ts +11 -0
- package/dist/src/services/scheduler/cronScheduler.js +76 -0
- package/dist/src/services/scheduler/cronScheduler.js.map +1 -0
- package/dist/src/services/scheduler/jobRunner.d.ts +76 -0
- package/dist/src/services/scheduler/jobRunner.js +414 -0
- package/dist/src/services/scheduler/jobRunner.js.map +1 -0
- package/dist/src/services/scheduler/jobRunnerStub.d.ts +5 -0
- package/dist/src/services/scheduler/jobRunnerStub.js +11 -0
- package/dist/src/services/scheduler/jobRunnerStub.js.map +1 -0
- package/dist/src/services/security/secretStore.d.ts +6 -0
- package/dist/src/services/security/secretStore.js +193 -0
- package/dist/src/services/security/secretStore.js.map +1 -0
- package/dist/src/services/startup/index.d.ts +13 -0
- package/dist/src/services/startup/index.js +120 -0
- package/dist/src/services/startup/index.js.map +1 -0
- package/dist/src/services/startup/linuxCronFallback.d.ts +2 -0
- package/dist/src/services/startup/linuxCronFallback.js +29 -0
- package/dist/src/services/startup/linuxCronFallback.js.map +1 -0
- package/dist/src/services/startup/linuxSystemd.d.ts +3 -0
- package/dist/src/services/startup/linuxSystemd.js +47 -0
- package/dist/src/services/startup/linuxSystemd.js.map +1 -0
- package/dist/src/services/startup/macosLaunchd.d.ts +2 -0
- package/dist/src/services/startup/macosLaunchd.js +40 -0
- package/dist/src/services/startup/macosLaunchd.js.map +1 -0
- package/dist/src/services/startup/windowsRunFallback.d.ts +2 -0
- package/dist/src/services/startup/windowsRunFallback.js +17 -0
- package/dist/src/services/startup/windowsRunFallback.js.map +1 -0
- package/dist/src/services/startup/windowsTaskScheduler.d.ts +2 -0
- package/dist/src/services/startup/windowsTaskScheduler.js +16 -0
- package/dist/src/services/startup/windowsTaskScheduler.js.map +1 -0
- package/dist/src/types/job.d.ts +34 -0
- package/dist/src/types/job.js +2 -0
- package/dist/src/types/job.js.map +1 -0
- package/dist/src/types/settings.d.ts +35 -0
- package/dist/src/types/settings.js +8 -0
- package/dist/src/types/settings.js.map +1 -0
- package/dist/src/ui/components/AppFrame.d.ts +17 -0
- package/dist/src/ui/components/AppFrame.js +26 -0
- package/dist/src/ui/components/AppFrame.js.map +1 -0
- package/dist/src/ui/components/CliHeader.d.ts +8 -0
- package/dist/src/ui/components/CliHeader.js +8 -0
- package/dist/src/ui/components/CliHeader.js.map +1 -0
- package/dist/src/ui/components/SubredditMultiSelect.d.ts +7 -0
- package/dist/src/ui/components/SubredditMultiSelect.js +91 -0
- package/dist/src/ui/components/SubredditMultiSelect.js.map +1 -0
- package/dist/src/ui/components/TextPrompt.d.ts +10 -0
- package/dist/src/ui/components/TextPrompt.js +13 -0
- package/dist/src/ui/components/TextPrompt.js.map +1 -0
- package/dist/src/ui/components/YesNoSelector.d.ts +10 -0
- package/dist/src/ui/components/YesNoSelector.js +25 -0
- package/dist/src/ui/components/YesNoSelector.js.map +1 -0
- package/dist/src/ui/components/subredditOptions.d.ts +5 -0
- package/dist/src/ui/components/subredditOptions.js +14 -0
- package/dist/src/ui/components/subredditOptions.js.map +1 -0
- package/dist/src/ui/components/yesNoSelectorModel.d.ts +9 -0
- package/dist/src/ui/components/yesNoSelectorModel.js +23 -0
- package/dist/src/ui/components/yesNoSelectorModel.js.map +1 -0
- package/dist/src/ui/theme.d.ts +26 -0
- package/dist/src/ui/theme.js +37 -0
- package/dist/src/ui/theme.js.map +1 -0
- package/dist/src/utils/logger.d.ts +5 -0
- package/dist/src/utils/logger.js +15 -0
- package/dist/src/utils/logger.js.map +1 -0
- package/dist/src/utils/notify.d.ts +6 -0
- package/dist/src/utils/notify.js +14 -0
- package/dist/src/utils/notify.js.map +1 -0
- package/dist/src/utils/paths.d.ts +10 -0
- package/dist/src/utils/paths.js +24 -0
- package/dist/src/utils/paths.js.map +1 -0
- package/dist/src/utils/scanLogFormatting.d.ts +26 -0
- package/dist/src/utils/scanLogFormatting.js +60 -0
- package/dist/src/utils/scanLogFormatting.js.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getDb } from '../sqlite.js';
|
|
5
|
+
import { getAppPaths } from '../../../utils/paths.js';
|
|
6
|
+
function toSlug(value) {
|
|
7
|
+
const cleaned = value
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.trim()
|
|
10
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
11
|
+
.replace(/\s+/g, '-')
|
|
12
|
+
.replace(/-+/g, '-')
|
|
13
|
+
.replace(/^-|-$/g, '');
|
|
14
|
+
return cleaned.slice(0, 40) || 'job';
|
|
15
|
+
}
|
|
16
|
+
function mapRow(row) {
|
|
17
|
+
return {
|
|
18
|
+
id: String(row.id),
|
|
19
|
+
slug: String(row.slug),
|
|
20
|
+
name: String(row.name),
|
|
21
|
+
description: String(row.description),
|
|
22
|
+
qualificationPrompt: String(row.qualification_prompt),
|
|
23
|
+
subreddits: JSON.parse(String(row.subreddits_json)),
|
|
24
|
+
scheduleCron: String(row.schedule_cron),
|
|
25
|
+
enabled: Number(row.enabled) === 1,
|
|
26
|
+
monitorComments: row.monitor_comments === undefined ? true : Number(row.monitor_comments) === 1,
|
|
27
|
+
createdAt: String(row.created_at),
|
|
28
|
+
updatedAt: String(row.updated_at)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export class JobsRepository {
|
|
32
|
+
db = getDb();
|
|
33
|
+
removeCascadeStmt = this.db.transaction((jobId) => {
|
|
34
|
+
const jobRow = this.db.prepare('SELECT slug FROM jobs WHERE id = ?').get(jobId);
|
|
35
|
+
const runLogs = this.db
|
|
36
|
+
.prepare(`SELECT log_file_path as logFilePath
|
|
37
|
+
FROM job_runs
|
|
38
|
+
WHERE job_id = ?
|
|
39
|
+
AND log_file_path IS NOT NULL`)
|
|
40
|
+
.all(jobId);
|
|
41
|
+
for (const row of runLogs) {
|
|
42
|
+
if (!row.logFilePath) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
if (fs.existsSync(row.logFilePath)) {
|
|
47
|
+
fs.unlinkSync(row.logFilePath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Ignore filesystem cleanup failures and continue DB cleanup.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (jobRow?.slug) {
|
|
55
|
+
const csvPath = path.join(getAppPaths().resultsDir, `${jobRow.slug}.csv`);
|
|
56
|
+
try {
|
|
57
|
+
if (fs.existsSync(csvPath)) {
|
|
58
|
+
fs.unlinkSync(csvPath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Ignore filesystem cleanup failures and continue DB cleanup.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
this.db.prepare('DELETE FROM scan_items WHERE job_id = ?').run(jobId);
|
|
66
|
+
this.db.prepare('DELETE FROM job_runs WHERE job_id = ?').run(jobId);
|
|
67
|
+
this.db.prepare('DELETE FROM jobs WHERE id = ?').run(jobId);
|
|
68
|
+
});
|
|
69
|
+
constructor() {
|
|
70
|
+
this.backfillMissingSlugs();
|
|
71
|
+
}
|
|
72
|
+
slugExists(slug) {
|
|
73
|
+
const row = this.db
|
|
74
|
+
.prepare('SELECT 1 FROM jobs WHERE slug = ? LIMIT 1')
|
|
75
|
+
.get(slug);
|
|
76
|
+
return Boolean(row);
|
|
77
|
+
}
|
|
78
|
+
ensureUniqueSlug(base) {
|
|
79
|
+
const normalized = toSlug(base);
|
|
80
|
+
if (!this.slugExists(normalized)) {
|
|
81
|
+
return normalized;
|
|
82
|
+
}
|
|
83
|
+
const basePrefix = normalized.slice(0, 30);
|
|
84
|
+
let counter = 2;
|
|
85
|
+
while (true) {
|
|
86
|
+
const candidate = `${basePrefix}-${counter}`;
|
|
87
|
+
if (!this.slugExists(candidate)) {
|
|
88
|
+
return candidate;
|
|
89
|
+
}
|
|
90
|
+
counter += 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
backfillMissingSlugs() {
|
|
94
|
+
const rows = this.db
|
|
95
|
+
.prepare("SELECT id, name FROM jobs WHERE slug IS NULL OR slug = ''")
|
|
96
|
+
.all();
|
|
97
|
+
rows.forEach((row) => {
|
|
98
|
+
const slug = this.ensureUniqueSlug(row.name);
|
|
99
|
+
this.db.prepare('UPDATE jobs SET slug = ? WHERE id = ?').run(slug, row.id);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
create(input) {
|
|
103
|
+
const id = crypto.randomUUID();
|
|
104
|
+
const schedule = input.scheduleCron ?? '*/30 * * * *';
|
|
105
|
+
const enabled = input.enabled ?? true;
|
|
106
|
+
const slug = this.ensureUniqueSlug(input.slug ?? input.name);
|
|
107
|
+
const monitorComments = input.monitorComments ?? true;
|
|
108
|
+
this.db
|
|
109
|
+
.prepare(`INSERT INTO jobs (
|
|
110
|
+
id, slug, name, description, qualification_prompt, subreddits_json,
|
|
111
|
+
schedule_cron, enabled, monitor_comments, created_at, updated_at
|
|
112
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`)
|
|
113
|
+
.run(id, slug, input.name, input.description, input.qualificationPrompt, JSON.stringify(input.subreddits), schedule, enabled ? 1 : 0, monitorComments ? 1 : 0);
|
|
114
|
+
return this.getById(id);
|
|
115
|
+
}
|
|
116
|
+
getById(id) {
|
|
117
|
+
const row = this.db.prepare('SELECT * FROM jobs WHERE id = ?').get(id);
|
|
118
|
+
return row ? mapRow(row) : null;
|
|
119
|
+
}
|
|
120
|
+
getBySlug(slug) {
|
|
121
|
+
const row = this.db.prepare('SELECT * FROM jobs WHERE slug = ?').get(slug);
|
|
122
|
+
return row ? mapRow(row) : null;
|
|
123
|
+
}
|
|
124
|
+
getByRef(ref) {
|
|
125
|
+
return this.getById(ref) ?? this.getBySlug(ref);
|
|
126
|
+
}
|
|
127
|
+
list() {
|
|
128
|
+
const rows = this.db.prepare('SELECT * FROM jobs ORDER BY created_at DESC').all();
|
|
129
|
+
return rows.map(mapRow);
|
|
130
|
+
}
|
|
131
|
+
listEnabled() {
|
|
132
|
+
const rows = this.db
|
|
133
|
+
.prepare('SELECT * FROM jobs WHERE enabled = 1 ORDER BY created_at DESC')
|
|
134
|
+
.all();
|
|
135
|
+
return rows.map(mapRow);
|
|
136
|
+
}
|
|
137
|
+
setEnabled(id, enabled) {
|
|
138
|
+
this.db
|
|
139
|
+
.prepare(`UPDATE jobs
|
|
140
|
+
SET enabled = ?, updated_at = datetime('now')
|
|
141
|
+
WHERE id = ?`)
|
|
142
|
+
.run(enabled ? 1 : 0, id);
|
|
143
|
+
}
|
|
144
|
+
setEnabledByRef(ref, enabled) {
|
|
145
|
+
const job = this.getByRef(ref);
|
|
146
|
+
if (!job) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
this.setEnabled(job.id, enabled);
|
|
150
|
+
return this.getById(job.id);
|
|
151
|
+
}
|
|
152
|
+
remove(id) {
|
|
153
|
+
this.removeCascadeStmt(id);
|
|
154
|
+
}
|
|
155
|
+
removeByRef(ref) {
|
|
156
|
+
const job = this.getByRef(ref);
|
|
157
|
+
if (!job) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
this.remove(job.id);
|
|
161
|
+
return job;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=jobsRepo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobsRepo.js","sourceRoot":"","sources":["../../../../../src/services/db/repositories/jobsRepo.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAErC,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEtD,SAAS,MAAM,CAAC,KAAa;IAC3B,MAAM,OAAO,GAAG,KAAK;SAClB,WAAW,EAAE;SACb,IAAI,EAAE;SACN,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC;SAC5B,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAEzB,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,KAAK,CAAC;AACvC,CAAC;AAED,SAAS,MAAM,CAAC,GAA4B;IAC1C,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;QAClB,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;QACtB,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC;QACpC,mBAAmB,EAAE,MAAM,CAAC,GAAG,CAAC,oBAAoB,CAAC;QACrD,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,CAAa;QAC/D,YAAY,EAAE,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC;QACvC,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC;QAClC,eAAe,EAAE,GAAG,CAAC,gBAAgB,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,KAAK,CAAC;QAC/F,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC;QACjC,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC;KAClC,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,cAAc;IACR,EAAE,GAAG,KAAK,EAAE,CAAC;IACb,iBAAiB,GAAG,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,KAAa,EAAE,EAAE;QACzE,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,oCAAoC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAiC,CAAC;QAEhH,MAAM,OAAO,GAAG,IAAI,CAAC,EAAE;aACpB,OAAO,CACN;;;yCAGiC,CAClC;aACA,GAAG,CAAC,KAAK,CAA0C,CAAC;QAEvD,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;YAC1B,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;gBACrB,SAAS;YACX,CAAC;YAED,IAAI,CAAC;gBACH,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;oBACnC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;gBACjC,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,8DAA8D;YAChE,CAAC;QACH,CAAC;QAED,IAAI,MAAM,EAAE,IAAI,EAAE,CAAC;YACjB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,IAAI,MAAM,CAAC,CAAC;YAC1E,IAAI,CAAC;gBACH,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;oBAC3B,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;gBACzB,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,8DAA8D;YAChE,CAAC;QACH,CAAC;QAED,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,yCAAyC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACtE,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,uCAAuC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACpE,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,+BAA+B,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH;QACE,IAAI,CAAC,oBAAoB,EAAE,CAAC;IAC9B,CAAC;IAEO,UAAU,CAAC,IAAY;QAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE;aAChB,OAAO,CAAC,2CAA2C,CAAC;aACpD,GAAG,CAAC,IAAI,CAAwC,CAAC;QACpD,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC;IAEO,gBAAgB,CAAC,IAAY;QACnC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YACjC,OAAO,UAAU,CAAC;QACpB,CAAC;QAED,MAAM,UAAU,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3C,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,OAAO,IAAI,EAAE,CAAC;YACZ,MAAM,SAAS,GAAG,GAAG,UAAU,IAAI,OAAO,EAAE,CAAC;YAC7C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;gBAChC,OAAO,SAAS,CAAC;YACnB,CAAC;YACD,OAAO,IAAI,CAAC,CAAC;QACf,CAAC;IACH,CAAC;IAEO,oBAAoB;QAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE;aACjB,OAAO,CAAC,2DAA2D,CAAC;aACpE,GAAG,EAAyC,CAAC;QAEhD,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;YACnB,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC7C,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,uCAAuC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAC7E,CAAC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAC,KAAa;QAClB,MAAM,EAAE,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAG,KAAK,CAAC,YAAY,IAAI,cAAc,CAAC;QACtD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,IAAI,CAAC;QACtC,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC;QAE7D,MAAM,eAAe,GAAG,KAAK,CAAC,eAAe,IAAI,IAAI,CAAC;QAEtD,IAAI,CAAC,EAAE;aACJ,OAAO,CACN;;;+EAGuE,CACxE;aACA,GAAG,CACF,EAAE,EACF,IAAI,EACJ,KAAK,CAAC,IAAI,EACV,KAAK,CAAC,WAAW,EACjB,KAAK,CAAC,mBAAmB,EACzB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,EAChC,QAAQ,EACR,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EACf,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CACxB,CAAC;QAEJ,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,CAAE,CAAC;IAC3B,CAAC;IAED,OAAO,CAAC,EAAU;QAChB,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,iCAAiC,CAAC,CAAC,GAAG,CAAC,EAAE,CAExD,CAAC;QACd,OAAO,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAClC,CAAC;IAED,SAAS,CAAC,IAAY;QACpB,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,mCAAmC,CAAC,CAAC,GAAG,CAAC,IAAI,CAE5D,CAAC;QACd,OAAO,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAClC,CAAC;IAED,QAAQ,CAAC,GAAW;QAClB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAClD,CAAC;IAED,IAAI;QACF,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,6CAA6C,CAAC,CAAC,GAAG,EAA+B,CAAC;QAC/G,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B,CAAC;IAED,WAAW;QACT,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE;aACjB,OAAO,CAAC,+DAA+D,CAAC;aACxE,GAAG,EAA+B,CAAC;QACtC,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B,CAAC;IAED,UAAU,CAAC,EAAU,EAAE,OAAgB;QACrC,IAAI,CAAC,EAAE;aACJ,OAAO,CACN;;sBAEc,CACf;aACA,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC9B,CAAC;IAED,eAAe,CAAC,GAAW,EAAE,OAAgB;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;QACjC,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC9B,CAAC;IAED,MAAM,CAAC,EAAU;QACf,IAAI,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC;IAC7B,CAAC;IAED,WAAW,CAAC,GAAW;QACrB,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACpB,OAAO,GAAG,CAAC;IACb,CAAC;CACF"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface RunStats {
|
|
2
|
+
itemsDiscovered: number;
|
|
3
|
+
itemsNew: number;
|
|
4
|
+
itemsQualified: number;
|
|
5
|
+
promptTokens: number;
|
|
6
|
+
completionTokens: number;
|
|
7
|
+
estimatedCostUsd: number | null;
|
|
8
|
+
}
|
|
9
|
+
export interface RunRow {
|
|
10
|
+
id: string;
|
|
11
|
+
jobId: string;
|
|
12
|
+
jobName: string | null;
|
|
13
|
+
status: string;
|
|
14
|
+
message: string | null;
|
|
15
|
+
startedAt: string | null;
|
|
16
|
+
finishedAt: string | null;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
itemsDiscovered: number;
|
|
19
|
+
itemsNew: number;
|
|
20
|
+
itemsQualified: number;
|
|
21
|
+
promptTokens: number;
|
|
22
|
+
completionTokens: number;
|
|
23
|
+
estimatedCostUsd: number | null;
|
|
24
|
+
logFilePath: string | null;
|
|
25
|
+
}
|
|
26
|
+
export interface RunAnalyticsRow extends RunRow {
|
|
27
|
+
newPosts: number;
|
|
28
|
+
newComments: number;
|
|
29
|
+
}
|
|
30
|
+
interface RunAnalyticsFilter {
|
|
31
|
+
jobId?: string;
|
|
32
|
+
days: number;
|
|
33
|
+
limit?: number;
|
|
34
|
+
}
|
|
35
|
+
export declare class RunsRepository {
|
|
36
|
+
private readonly db;
|
|
37
|
+
private buildRunFilter;
|
|
38
|
+
addRun(jobId: string, status: string, message: string): void;
|
|
39
|
+
startRun(jobId: string, logFilePath?: string): string;
|
|
40
|
+
completeRun(runId: string, stats: RunStats): void;
|
|
41
|
+
failRun(runId: string, message: string): void;
|
|
42
|
+
setLogFilePath(runId: string, logFilePath: string): void;
|
|
43
|
+
getById(runId: string): RunRow | null;
|
|
44
|
+
latest(limit?: number): Array<{
|
|
45
|
+
jobId: string;
|
|
46
|
+
status: string;
|
|
47
|
+
message: string;
|
|
48
|
+
createdAt: string;
|
|
49
|
+
}>;
|
|
50
|
+
listByJob(jobId: string, limit?: number): RunRow[];
|
|
51
|
+
latestWithJobNames(limit?: number): RunRow[];
|
|
52
|
+
listAnalyticsRuns(filter: RunAnalyticsFilter): RunAnalyticsRow[];
|
|
53
|
+
countRuns(filter: {
|
|
54
|
+
jobId?: string;
|
|
55
|
+
days: number;
|
|
56
|
+
}): number;
|
|
57
|
+
}
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { getDb } from '../sqlite.js';
|
|
3
|
+
export class RunsRepository {
|
|
4
|
+
db = getDb();
|
|
5
|
+
buildRunFilter(filter) {
|
|
6
|
+
const params = [`-${filter.days} days`];
|
|
7
|
+
const conditions = ["datetime(jr.created_at) >= datetime('now', ?)"];
|
|
8
|
+
if (filter.jobId) {
|
|
9
|
+
conditions.push('jr.job_id = ?');
|
|
10
|
+
params.push(filter.jobId);
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
clause: `WHERE ${conditions.join(' AND ')}`,
|
|
14
|
+
params
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
addRun(jobId, status, message) {
|
|
18
|
+
this.db
|
|
19
|
+
.prepare(`INSERT INTO job_runs (id, job_id, status, message, created_at)
|
|
20
|
+
VALUES (?, ?, ?, ?, datetime('now'))`)
|
|
21
|
+
.run(crypto.randomUUID(), jobId, status, message);
|
|
22
|
+
}
|
|
23
|
+
startRun(jobId, logFilePath) {
|
|
24
|
+
const id = crypto.randomUUID();
|
|
25
|
+
this.db
|
|
26
|
+
.prepare(`INSERT INTO job_runs (
|
|
27
|
+
id,
|
|
28
|
+
job_id,
|
|
29
|
+
status,
|
|
30
|
+
started_at,
|
|
31
|
+
created_at,
|
|
32
|
+
log_file_path
|
|
33
|
+
) VALUES (?, ?, 'running', datetime('now'), datetime('now'), ?)`)
|
|
34
|
+
.run(id, jobId, logFilePath ?? null);
|
|
35
|
+
return id;
|
|
36
|
+
}
|
|
37
|
+
completeRun(runId, stats) {
|
|
38
|
+
this.db
|
|
39
|
+
.prepare(`UPDATE job_runs
|
|
40
|
+
SET status = 'completed',
|
|
41
|
+
finished_at = datetime('now'),
|
|
42
|
+
items_discovered = ?,
|
|
43
|
+
items_new = ?,
|
|
44
|
+
items_qualified = ?,
|
|
45
|
+
prompt_tokens = ?,
|
|
46
|
+
completion_tokens = ?,
|
|
47
|
+
estimated_cost_usd = ?
|
|
48
|
+
WHERE id = ?`)
|
|
49
|
+
.run(stats.itemsDiscovered, stats.itemsNew, stats.itemsQualified, stats.promptTokens, stats.completionTokens, stats.estimatedCostUsd, runId);
|
|
50
|
+
}
|
|
51
|
+
failRun(runId, message) {
|
|
52
|
+
this.db
|
|
53
|
+
.prepare(`UPDATE job_runs
|
|
54
|
+
SET status = 'failed',
|
|
55
|
+
message = ?,
|
|
56
|
+
finished_at = datetime('now')
|
|
57
|
+
WHERE id = ?`)
|
|
58
|
+
.run(message, runId);
|
|
59
|
+
}
|
|
60
|
+
setLogFilePath(runId, logFilePath) {
|
|
61
|
+
this.db
|
|
62
|
+
.prepare(`UPDATE job_runs
|
|
63
|
+
SET log_file_path = ?
|
|
64
|
+
WHERE id = ?`)
|
|
65
|
+
.run(logFilePath, runId);
|
|
66
|
+
}
|
|
67
|
+
getById(runId) {
|
|
68
|
+
const row = this.db
|
|
69
|
+
.prepare(`SELECT
|
|
70
|
+
jr.id as id,
|
|
71
|
+
jr.job_id as jobId,
|
|
72
|
+
j.name as jobName,
|
|
73
|
+
jr.status as status,
|
|
74
|
+
jr.message as message,
|
|
75
|
+
jr.started_at as startedAt,
|
|
76
|
+
jr.finished_at as finishedAt,
|
|
77
|
+
jr.created_at as createdAt,
|
|
78
|
+
jr.items_discovered as itemsDiscovered,
|
|
79
|
+
jr.items_new as itemsNew,
|
|
80
|
+
jr.items_qualified as itemsQualified,
|
|
81
|
+
jr.prompt_tokens as promptTokens,
|
|
82
|
+
jr.completion_tokens as completionTokens,
|
|
83
|
+
jr.estimated_cost_usd as estimatedCostUsd,
|
|
84
|
+
jr.log_file_path as logFilePath
|
|
85
|
+
FROM job_runs jr
|
|
86
|
+
LEFT JOIN jobs j ON j.id = jr.job_id
|
|
87
|
+
WHERE jr.id = ?
|
|
88
|
+
LIMIT 1`)
|
|
89
|
+
.get(runId);
|
|
90
|
+
return row ?? null;
|
|
91
|
+
}
|
|
92
|
+
latest(limit = 20) {
|
|
93
|
+
return this.db
|
|
94
|
+
.prepare(`SELECT job_id as jobId, status, message, created_at as createdAt
|
|
95
|
+
FROM job_runs
|
|
96
|
+
ORDER BY created_at DESC
|
|
97
|
+
LIMIT ?`)
|
|
98
|
+
.all(limit);
|
|
99
|
+
}
|
|
100
|
+
listByJob(jobId, limit = 20) {
|
|
101
|
+
return this.db
|
|
102
|
+
.prepare(`SELECT
|
|
103
|
+
jr.id as id,
|
|
104
|
+
jr.job_id as jobId,
|
|
105
|
+
j.name as jobName,
|
|
106
|
+
jr.status as status,
|
|
107
|
+
jr.message as message,
|
|
108
|
+
jr.started_at as startedAt,
|
|
109
|
+
jr.finished_at as finishedAt,
|
|
110
|
+
jr.created_at as createdAt,
|
|
111
|
+
jr.items_discovered as itemsDiscovered,
|
|
112
|
+
jr.items_new as itemsNew,
|
|
113
|
+
jr.items_qualified as itemsQualified,
|
|
114
|
+
jr.prompt_tokens as promptTokens,
|
|
115
|
+
jr.completion_tokens as completionTokens,
|
|
116
|
+
jr.estimated_cost_usd as estimatedCostUsd,
|
|
117
|
+
jr.log_file_path as logFilePath
|
|
118
|
+
FROM job_runs jr
|
|
119
|
+
LEFT JOIN jobs j ON j.id = jr.job_id
|
|
120
|
+
WHERE jr.job_id = ?
|
|
121
|
+
ORDER BY jr.created_at DESC
|
|
122
|
+
LIMIT ?`)
|
|
123
|
+
.all(jobId, limit);
|
|
124
|
+
}
|
|
125
|
+
latestWithJobNames(limit = 20) {
|
|
126
|
+
return this.db
|
|
127
|
+
.prepare(`SELECT
|
|
128
|
+
jr.id as id,
|
|
129
|
+
jr.job_id as jobId,
|
|
130
|
+
j.name as jobName,
|
|
131
|
+
jr.status as status,
|
|
132
|
+
jr.message as message,
|
|
133
|
+
jr.started_at as startedAt,
|
|
134
|
+
jr.finished_at as finishedAt,
|
|
135
|
+
jr.created_at as createdAt,
|
|
136
|
+
jr.items_discovered as itemsDiscovered,
|
|
137
|
+
jr.items_new as itemsNew,
|
|
138
|
+
jr.items_qualified as itemsQualified,
|
|
139
|
+
jr.prompt_tokens as promptTokens,
|
|
140
|
+
jr.completion_tokens as completionTokens,
|
|
141
|
+
jr.estimated_cost_usd as estimatedCostUsd,
|
|
142
|
+
jr.log_file_path as logFilePath
|
|
143
|
+
FROM job_runs jr
|
|
144
|
+
LEFT JOIN jobs j ON j.id = jr.job_id
|
|
145
|
+
ORDER BY jr.created_at DESC
|
|
146
|
+
LIMIT ?`)
|
|
147
|
+
.all(limit);
|
|
148
|
+
}
|
|
149
|
+
listAnalyticsRuns(filter) {
|
|
150
|
+
const { clause, params } = this.buildRunFilter(filter);
|
|
151
|
+
const limit = filter.limit ?? 20;
|
|
152
|
+
return this.db
|
|
153
|
+
.prepare(`SELECT
|
|
154
|
+
jr.id as id,
|
|
155
|
+
jr.job_id as jobId,
|
|
156
|
+
j.name as jobName,
|
|
157
|
+
jr.status as status,
|
|
158
|
+
jr.message as message,
|
|
159
|
+
jr.started_at as startedAt,
|
|
160
|
+
jr.finished_at as finishedAt,
|
|
161
|
+
jr.created_at as createdAt,
|
|
162
|
+
jr.items_discovered as itemsDiscovered,
|
|
163
|
+
jr.items_new as itemsNew,
|
|
164
|
+
jr.items_qualified as itemsQualified,
|
|
165
|
+
jr.prompt_tokens as promptTokens,
|
|
166
|
+
jr.completion_tokens as completionTokens,
|
|
167
|
+
jr.estimated_cost_usd as estimatedCostUsd,
|
|
168
|
+
jr.log_file_path as logFilePath,
|
|
169
|
+
COALESCE(SUM(CASE WHEN si.type = 'post' THEN 1 ELSE 0 END), 0) as newPosts,
|
|
170
|
+
COALESCE(SUM(CASE WHEN si.type = 'comment' THEN 1 ELSE 0 END), 0) as newComments
|
|
171
|
+
FROM job_runs jr
|
|
172
|
+
LEFT JOIN jobs j ON j.id = jr.job_id
|
|
173
|
+
LEFT JOIN scan_items si ON si.run_id = jr.id
|
|
174
|
+
${clause}
|
|
175
|
+
GROUP BY jr.id
|
|
176
|
+
ORDER BY datetime(jr.created_at) DESC
|
|
177
|
+
LIMIT ?`)
|
|
178
|
+
.all(...params, limit);
|
|
179
|
+
}
|
|
180
|
+
countRuns(filter) {
|
|
181
|
+
const { clause, params } = this.buildRunFilter(filter);
|
|
182
|
+
const row = this.db
|
|
183
|
+
.prepare(`SELECT COUNT(*) as runCount
|
|
184
|
+
FROM job_runs jr
|
|
185
|
+
${clause}`)
|
|
186
|
+
.get(...params);
|
|
187
|
+
return Number(row?.runCount ?? 0);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=runsRepo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runsRepo.js","sourceRoot":"","sources":["../../../../../src/services/db/repositories/runsRepo.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type ScanItemType = 'post' | 'comment';
|
|
2
|
+
export interface NewScanItem {
|
|
3
|
+
jobId: string;
|
|
4
|
+
runId: string;
|
|
5
|
+
type: ScanItemType;
|
|
6
|
+
redditPostId: string;
|
|
7
|
+
redditCommentId: string | null;
|
|
8
|
+
subreddit: string;
|
|
9
|
+
author: string;
|
|
10
|
+
title: string | null;
|
|
11
|
+
body: string;
|
|
12
|
+
url: string;
|
|
13
|
+
redditPostedAt: string;
|
|
14
|
+
qualified: boolean;
|
|
15
|
+
viewed?: boolean;
|
|
16
|
+
validated?: boolean;
|
|
17
|
+
processed?: boolean;
|
|
18
|
+
promptTokens?: number;
|
|
19
|
+
completionTokens?: number;
|
|
20
|
+
estimatedCostUsd?: number | null;
|
|
21
|
+
qualificationReason: string | null;
|
|
22
|
+
}
|
|
23
|
+
export interface QualifiedScanItemRow {
|
|
24
|
+
id: string;
|
|
25
|
+
jobId: string;
|
|
26
|
+
runId: string;
|
|
27
|
+
author: string;
|
|
28
|
+
title: string | null;
|
|
29
|
+
body: string;
|
|
30
|
+
url: string;
|
|
31
|
+
redditPostedAt: string;
|
|
32
|
+
viewed: boolean;
|
|
33
|
+
validated: boolean;
|
|
34
|
+
processed: boolean;
|
|
35
|
+
qualificationReason: string | null;
|
|
36
|
+
createdAt: string;
|
|
37
|
+
}
|
|
38
|
+
export interface AnalyticsTotalsRow {
|
|
39
|
+
newPosts: number;
|
|
40
|
+
newComments: number;
|
|
41
|
+
promptTokens: number;
|
|
42
|
+
completionTokens: number;
|
|
43
|
+
estimatedCostUsd: number;
|
|
44
|
+
}
|
|
45
|
+
export interface AnalyticsBySubredditRow extends AnalyticsTotalsRow {
|
|
46
|
+
subreddit: string;
|
|
47
|
+
}
|
|
48
|
+
export interface AnalyticsByJobRow extends AnalyticsTotalsRow {
|
|
49
|
+
jobId: string;
|
|
50
|
+
jobName: string;
|
|
51
|
+
jobSlug: string;
|
|
52
|
+
}
|
|
53
|
+
interface AnalyticsFilter {
|
|
54
|
+
jobId?: string;
|
|
55
|
+
days: number;
|
|
56
|
+
}
|
|
57
|
+
export declare class ScanItemsRepository {
|
|
58
|
+
private readonly db;
|
|
59
|
+
private buildFilterClause;
|
|
60
|
+
private toAnalyticsTotalsRow;
|
|
61
|
+
listQualifiedByJob(jobId: string): QualifiedScanItemRow[];
|
|
62
|
+
existsPost(jobId: string, postId: string): boolean;
|
|
63
|
+
getAnalyticsTotals(filter: AnalyticsFilter): AnalyticsTotalsRow;
|
|
64
|
+
listAnalyticsBySubreddit(filter: AnalyticsFilter): AnalyticsBySubredditRow[];
|
|
65
|
+
listAnalyticsByJob(days: number): AnalyticsByJobRow[];
|
|
66
|
+
existsComment(jobId: string, postId: string, commentId: string): boolean;
|
|
67
|
+
create(item: NewScanItem): string;
|
|
68
|
+
}
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { getDb } from '../sqlite.js';
|
|
3
|
+
export class ScanItemsRepository {
|
|
4
|
+
db = getDb();
|
|
5
|
+
buildFilterClause(alias, filter) {
|
|
6
|
+
const params = [`-${filter.days} days`];
|
|
7
|
+
const conditions = [`datetime(${alias}.created_at) >= datetime('now', ?)`];
|
|
8
|
+
if (filter.jobId) {
|
|
9
|
+
conditions.push(`${alias}.job_id = ?`);
|
|
10
|
+
params.push(filter.jobId);
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
clause: `WHERE ${conditions.join(' AND ')}`,
|
|
14
|
+
params
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
toAnalyticsTotalsRow(row) {
|
|
18
|
+
return {
|
|
19
|
+
newPosts: Number(row.newPosts ?? 0),
|
|
20
|
+
newComments: Number(row.newComments ?? 0),
|
|
21
|
+
promptTokens: Number(row.promptTokens ?? 0),
|
|
22
|
+
completionTokens: Number(row.completionTokens ?? 0),
|
|
23
|
+
estimatedCostUsd: Number(row.estimatedCostUsd ?? 0)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
listQualifiedByJob(jobId) {
|
|
27
|
+
const rows = this.db
|
|
28
|
+
.prepare(`SELECT
|
|
29
|
+
id,
|
|
30
|
+
job_id as jobId,
|
|
31
|
+
run_id as runId,
|
|
32
|
+
author,
|
|
33
|
+
title,
|
|
34
|
+
body,
|
|
35
|
+
url,
|
|
36
|
+
reddit_posted_at as redditPostedAt,
|
|
37
|
+
viewed,
|
|
38
|
+
validated,
|
|
39
|
+
processed,
|
|
40
|
+
qualification_reason as qualificationReason,
|
|
41
|
+
created_at as createdAt
|
|
42
|
+
FROM scan_items
|
|
43
|
+
WHERE job_id = ?
|
|
44
|
+
AND qualified = 1
|
|
45
|
+
ORDER BY datetime(reddit_posted_at) DESC, datetime(created_at) DESC, id DESC`)
|
|
46
|
+
.all(jobId);
|
|
47
|
+
return rows.map((row) => ({
|
|
48
|
+
...row,
|
|
49
|
+
viewed: row.viewed === 1,
|
|
50
|
+
validated: row.validated === 1,
|
|
51
|
+
processed: row.processed === 1
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
existsPost(jobId, postId) {
|
|
55
|
+
const row = this.db
|
|
56
|
+
.prepare(`SELECT 1
|
|
57
|
+
FROM scan_items
|
|
58
|
+
WHERE job_id = ?
|
|
59
|
+
AND reddit_post_id = ?
|
|
60
|
+
AND reddit_comment_id IS NULL
|
|
61
|
+
LIMIT 1`)
|
|
62
|
+
.get(jobId, postId);
|
|
63
|
+
return Boolean(row);
|
|
64
|
+
}
|
|
65
|
+
getAnalyticsTotals(filter) {
|
|
66
|
+
const { clause, params } = this.buildFilterClause('si', filter);
|
|
67
|
+
const row = this.db
|
|
68
|
+
.prepare(`SELECT
|
|
69
|
+
COALESCE(SUM(CASE WHEN si.type = 'post' THEN 1 ELSE 0 END), 0) as newPosts,
|
|
70
|
+
COALESCE(SUM(CASE WHEN si.type = 'comment' THEN 1 ELSE 0 END), 0) as newComments,
|
|
71
|
+
COALESCE(SUM(si.prompt_tokens), 0) as promptTokens,
|
|
72
|
+
COALESCE(SUM(si.completion_tokens), 0) as completionTokens,
|
|
73
|
+
COALESCE(SUM(COALESCE(si.estimated_cost_usd, 0)), 0) as estimatedCostUsd
|
|
74
|
+
FROM scan_items si
|
|
75
|
+
${clause}`)
|
|
76
|
+
.get(...params);
|
|
77
|
+
if (!row) {
|
|
78
|
+
return {
|
|
79
|
+
newPosts: 0,
|
|
80
|
+
newComments: 0,
|
|
81
|
+
promptTokens: 0,
|
|
82
|
+
completionTokens: 0,
|
|
83
|
+
estimatedCostUsd: 0
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return this.toAnalyticsTotalsRow(row);
|
|
87
|
+
}
|
|
88
|
+
listAnalyticsBySubreddit(filter) {
|
|
89
|
+
const { clause, params } = this.buildFilterClause('si', filter);
|
|
90
|
+
const rows = this.db
|
|
91
|
+
.prepare(`SELECT
|
|
92
|
+
si.subreddit as subreddit,
|
|
93
|
+
COALESCE(SUM(CASE WHEN si.type = 'post' THEN 1 ELSE 0 END), 0) as newPosts,
|
|
94
|
+
COALESCE(SUM(CASE WHEN si.type = 'comment' THEN 1 ELSE 0 END), 0) as newComments,
|
|
95
|
+
COALESCE(SUM(si.prompt_tokens), 0) as promptTokens,
|
|
96
|
+
COALESCE(SUM(si.completion_tokens), 0) as completionTokens,
|
|
97
|
+
COALESCE(SUM(COALESCE(si.estimated_cost_usd, 0)), 0) as estimatedCostUsd
|
|
98
|
+
FROM scan_items si
|
|
99
|
+
${clause}
|
|
100
|
+
GROUP BY si.subreddit
|
|
101
|
+
ORDER BY (SUM(CASE WHEN si.type = 'post' THEN 1 ELSE 0 END) + SUM(CASE WHEN si.type = 'comment' THEN 1 ELSE 0 END)) DESC,
|
|
102
|
+
si.subreddit ASC`)
|
|
103
|
+
.all(...params);
|
|
104
|
+
return rows.map((row) => ({
|
|
105
|
+
subreddit: row.subreddit,
|
|
106
|
+
...this.toAnalyticsTotalsRow(row)
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
listAnalyticsByJob(days) {
|
|
110
|
+
const params = [`-${days} days`];
|
|
111
|
+
const rows = this.db
|
|
112
|
+
.prepare(`SELECT
|
|
113
|
+
si.job_id as jobId,
|
|
114
|
+
j.name as jobName,
|
|
115
|
+
j.slug as jobSlug,
|
|
116
|
+
COALESCE(SUM(CASE WHEN si.type = 'post' THEN 1 ELSE 0 END), 0) as newPosts,
|
|
117
|
+
COALESCE(SUM(CASE WHEN si.type = 'comment' THEN 1 ELSE 0 END), 0) as newComments,
|
|
118
|
+
COALESCE(SUM(si.prompt_tokens), 0) as promptTokens,
|
|
119
|
+
COALESCE(SUM(si.completion_tokens), 0) as completionTokens,
|
|
120
|
+
COALESCE(SUM(COALESCE(si.estimated_cost_usd, 0)), 0) as estimatedCostUsd
|
|
121
|
+
FROM scan_items si
|
|
122
|
+
INNER JOIN jobs j ON j.id = si.job_id
|
|
123
|
+
WHERE datetime(si.created_at) >= datetime('now', ?)
|
|
124
|
+
GROUP BY si.job_id, j.name, j.slug
|
|
125
|
+
ORDER BY (SUM(CASE WHEN si.type = 'post' THEN 1 ELSE 0 END) + SUM(CASE WHEN si.type = 'comment' THEN 1 ELSE 0 END)) DESC,
|
|
126
|
+
j.name ASC`)
|
|
127
|
+
.all(...params);
|
|
128
|
+
return rows.map((row) => ({
|
|
129
|
+
jobId: row.jobId,
|
|
130
|
+
jobName: row.jobName,
|
|
131
|
+
jobSlug: row.jobSlug,
|
|
132
|
+
...this.toAnalyticsTotalsRow(row)
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
existsComment(jobId, postId, commentId) {
|
|
136
|
+
const row = this.db
|
|
137
|
+
.prepare(`SELECT 1
|
|
138
|
+
FROM scan_items
|
|
139
|
+
WHERE job_id = ?
|
|
140
|
+
AND reddit_post_id = ?
|
|
141
|
+
AND reddit_comment_id = ?
|
|
142
|
+
LIMIT 1`)
|
|
143
|
+
.get(jobId, postId, commentId);
|
|
144
|
+
return Boolean(row);
|
|
145
|
+
}
|
|
146
|
+
create(item) {
|
|
147
|
+
const id = crypto.randomUUID();
|
|
148
|
+
this.db
|
|
149
|
+
.prepare(`INSERT INTO scan_items (
|
|
150
|
+
id,
|
|
151
|
+
job_id,
|
|
152
|
+
run_id,
|
|
153
|
+
type,
|
|
154
|
+
reddit_post_id,
|
|
155
|
+
reddit_comment_id,
|
|
156
|
+
subreddit,
|
|
157
|
+
author,
|
|
158
|
+
title,
|
|
159
|
+
body,
|
|
160
|
+
url,
|
|
161
|
+
reddit_posted_at,
|
|
162
|
+
qualified,
|
|
163
|
+
viewed,
|
|
164
|
+
validated,
|
|
165
|
+
processed,
|
|
166
|
+
prompt_tokens,
|
|
167
|
+
completion_tokens,
|
|
168
|
+
estimated_cost_usd,
|
|
169
|
+
qualification_reason,
|
|
170
|
+
created_at
|
|
171
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`)
|
|
172
|
+
.run(id, item.jobId, item.runId, item.type, item.redditPostId, item.redditCommentId, item.subreddit, item.author, item.title, item.body, item.url, item.redditPostedAt, item.qualified ? 1 : 0, item.viewed ? 1 : 0, item.validated ? 1 : 0, item.processed ? 1 : 0, item.promptTokens ?? 0, item.completionTokens ?? 0, item.estimatedCostUsd ?? null, item.qualificationReason);
|
|
173
|
+
return id;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=scanItemsRepo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanItemsRepo.js","sourceRoot":"","sources":["../../../../../src/services/db/repositories/scanItemsRepo.ts"],"names":[],"mappings":"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"}
|