@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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +255 -0
  3. package/dist/src/cli/commands/analytics.d.ts +3 -0
  4. package/dist/src/cli/commands/analytics.js +147 -0
  5. package/dist/src/cli/commands/analytics.js.map +1 -0
  6. package/dist/src/cli/commands/daemon.d.ts +5 -0
  7. package/dist/src/cli/commands/daemon.js +85 -0
  8. package/dist/src/cli/commands/daemon.js.map +1 -0
  9. package/dist/src/cli/commands/doctor.d.ts +1 -0
  10. package/dist/src/cli/commands/doctor.js +106 -0
  11. package/dist/src/cli/commands/doctor.js.map +1 -0
  12. package/dist/src/cli/commands/errors.d.ts +3 -0
  13. package/dist/src/cli/commands/errors.js +51 -0
  14. package/dist/src/cli/commands/errors.js.map +1 -0
  15. package/dist/src/cli/commands/export.d.ts +1 -0
  16. package/dist/src/cli/commands/export.js +48 -0
  17. package/dist/src/cli/commands/export.js.map +1 -0
  18. package/dist/src/cli/commands/job.d.ts +16 -0
  19. package/dist/src/cli/commands/job.js +350 -0
  20. package/dist/src/cli/commands/job.js.map +1 -0
  21. package/dist/src/cli/commands/logs.d.ts +3 -0
  22. package/dist/src/cli/commands/logs.js +44 -0
  23. package/dist/src/cli/commands/logs.js.map +1 -0
  24. package/dist/src/cli/commands/selection.d.ts +19 -0
  25. package/dist/src/cli/commands/selection.js +182 -0
  26. package/dist/src/cli/commands/selection.js.map +1 -0
  27. package/dist/src/cli/commands/settings.d.ts +1 -0
  28. package/dist/src/cli/commands/settings.js +31 -0
  29. package/dist/src/cli/commands/settings.js.map +1 -0
  30. package/dist/src/cli/commands/startup.d.ts +5 -0
  31. package/dist/src/cli/commands/startup.js +26 -0
  32. package/dist/src/cli/commands/startup.js.map +1 -0
  33. package/dist/src/cli/flows/jobAddFlow.d.ts +26 -0
  34. package/dist/src/cli/flows/jobAddFlow.js +209 -0
  35. package/dist/src/cli/flows/jobAddFlow.js.map +1 -0
  36. package/dist/src/cli/flows/settingsFlow.d.ts +15 -0
  37. package/dist/src/cli/flows/settingsFlow.js +180 -0
  38. package/dist/src/cli/flows/settingsFlow.js.map +1 -0
  39. package/dist/src/cli/flows/settingsFlowModel.d.ts +47 -0
  40. package/dist/src/cli/flows/settingsFlowModel.js +143 -0
  41. package/dist/src/cli/flows/settingsFlowModel.js.map +1 -0
  42. package/dist/src/cli/index.d.ts +2 -0
  43. package/dist/src/cli/index.js +138 -0
  44. package/dist/src/cli/index.js.map +1 -0
  45. package/dist/src/cli/ui/consoleUi.d.ts +13 -0
  46. package/dist/src/cli/ui/consoleUi.js +165 -0
  47. package/dist/src/cli/ui/consoleUi.js.map +1 -0
  48. package/dist/src/cli/ui/time.d.ts +9 -0
  49. package/dist/src/cli/ui/time.js +35 -0
  50. package/dist/src/cli/ui/time.js.map +1 -0
  51. package/dist/src/index.d.ts +1 -0
  52. package/dist/src/index.js +2 -0
  53. package/dist/src/index.js.map +1 -0
  54. package/dist/src/scripts/e2eSmoke.d.ts +1 -0
  55. package/dist/src/scripts/e2eSmoke.js +102 -0
  56. package/dist/src/scripts/e2eSmoke.js.map +1 -0
  57. package/dist/src/services/analytics/analyticsService.d.ts +50 -0
  58. package/dist/src/services/analytics/analyticsService.js +88 -0
  59. package/dist/src/services/analytics/analyticsService.js.map +1 -0
  60. package/dist/src/services/daemonControl.d.ts +12 -0
  61. package/dist/src/services/daemonControl.js +58 -0
  62. package/dist/src/services/daemonControl.js.map +1 -0
  63. package/dist/src/services/db/repositories/jobsRepo.d.ts +19 -0
  64. package/dist/src/services/db/repositories/jobsRepo.js +164 -0
  65. package/dist/src/services/db/repositories/jobsRepo.js.map +1 -0
  66. package/dist/src/services/db/repositories/runsRepo.d.ts +58 -0
  67. package/dist/src/services/db/repositories/runsRepo.js +190 -0
  68. package/dist/src/services/db/repositories/runsRepo.js.map +1 -0
  69. package/dist/src/services/db/repositories/scanItemsRepo.d.ts +69 -0
  70. package/dist/src/services/db/repositories/scanItemsRepo.js +176 -0
  71. package/dist/src/services/db/repositories/scanItemsRepo.js.map +1 -0
  72. package/dist/src/services/db/repositories/settingsRepo.d.ts +14 -0
  73. package/dist/src/services/db/repositories/settingsRepo.js +132 -0
  74. package/dist/src/services/db/repositories/settingsRepo.js.map +1 -0
  75. package/dist/src/services/db/sqlite.d.ts +2 -0
  76. package/dist/src/services/db/sqlite.js +192 -0
  77. package/dist/src/services/db/sqlite.js.map +1 -0
  78. package/dist/src/services/export/csvResults.d.ts +10 -0
  79. package/dist/src/services/export/csvResults.js +42 -0
  80. package/dist/src/services/export/csvResults.js.map +1 -0
  81. package/dist/src/services/logging/logReader.d.ts +4 -0
  82. package/dist/src/services/logging/logReader.js +230 -0
  83. package/dist/src/services/logging/logReader.js.map +1 -0
  84. package/dist/src/services/logging/logRotation.d.ts +1 -0
  85. package/dist/src/services/logging/logRotation.js +30 -0
  86. package/dist/src/services/logging/logRotation.js.map +1 -0
  87. package/dist/src/services/logging/runLogger.d.ts +9 -0
  88. package/dist/src/services/logging/runLogger.js +42 -0
  89. package/dist/src/services/logging/runLogger.js.map +1 -0
  90. package/dist/src/services/openrouter/client.d.ts +60 -0
  91. package/dist/src/services/openrouter/client.js +437 -0
  92. package/dist/src/services/openrouter/client.js.map +1 -0
  93. package/dist/src/services/openrouter/prompts.d.ts +5 -0
  94. package/dist/src/services/openrouter/prompts.js +48 -0
  95. package/dist/src/services/openrouter/prompts.js.map +1 -0
  96. package/dist/src/services/reddit/client.d.ts +25 -0
  97. package/dist/src/services/reddit/client.js +186 -0
  98. package/dist/src/services/reddit/client.js.map +1 -0
  99. package/dist/src/services/scheduler/cronScheduler.d.ts +11 -0
  100. package/dist/src/services/scheduler/cronScheduler.js +76 -0
  101. package/dist/src/services/scheduler/cronScheduler.js.map +1 -0
  102. package/dist/src/services/scheduler/jobRunner.d.ts +76 -0
  103. package/dist/src/services/scheduler/jobRunner.js +414 -0
  104. package/dist/src/services/scheduler/jobRunner.js.map +1 -0
  105. package/dist/src/services/scheduler/jobRunnerStub.d.ts +5 -0
  106. package/dist/src/services/scheduler/jobRunnerStub.js +11 -0
  107. package/dist/src/services/scheduler/jobRunnerStub.js.map +1 -0
  108. package/dist/src/services/security/secretStore.d.ts +6 -0
  109. package/dist/src/services/security/secretStore.js +193 -0
  110. package/dist/src/services/security/secretStore.js.map +1 -0
  111. package/dist/src/services/startup/index.d.ts +13 -0
  112. package/dist/src/services/startup/index.js +120 -0
  113. package/dist/src/services/startup/index.js.map +1 -0
  114. package/dist/src/services/startup/linuxCronFallback.d.ts +2 -0
  115. package/dist/src/services/startup/linuxCronFallback.js +29 -0
  116. package/dist/src/services/startup/linuxCronFallback.js.map +1 -0
  117. package/dist/src/services/startup/linuxSystemd.d.ts +3 -0
  118. package/dist/src/services/startup/linuxSystemd.js +47 -0
  119. package/dist/src/services/startup/linuxSystemd.js.map +1 -0
  120. package/dist/src/services/startup/macosLaunchd.d.ts +2 -0
  121. package/dist/src/services/startup/macosLaunchd.js +40 -0
  122. package/dist/src/services/startup/macosLaunchd.js.map +1 -0
  123. package/dist/src/services/startup/windowsRunFallback.d.ts +2 -0
  124. package/dist/src/services/startup/windowsRunFallback.js +17 -0
  125. package/dist/src/services/startup/windowsRunFallback.js.map +1 -0
  126. package/dist/src/services/startup/windowsTaskScheduler.d.ts +2 -0
  127. package/dist/src/services/startup/windowsTaskScheduler.js +16 -0
  128. package/dist/src/services/startup/windowsTaskScheduler.js.map +1 -0
  129. package/dist/src/types/job.d.ts +34 -0
  130. package/dist/src/types/job.js +2 -0
  131. package/dist/src/types/job.js.map +1 -0
  132. package/dist/src/types/settings.d.ts +35 -0
  133. package/dist/src/types/settings.js +8 -0
  134. package/dist/src/types/settings.js.map +1 -0
  135. package/dist/src/ui/components/AppFrame.d.ts +17 -0
  136. package/dist/src/ui/components/AppFrame.js +26 -0
  137. package/dist/src/ui/components/AppFrame.js.map +1 -0
  138. package/dist/src/ui/components/CliHeader.d.ts +8 -0
  139. package/dist/src/ui/components/CliHeader.js +8 -0
  140. package/dist/src/ui/components/CliHeader.js.map +1 -0
  141. package/dist/src/ui/components/SubredditMultiSelect.d.ts +7 -0
  142. package/dist/src/ui/components/SubredditMultiSelect.js +91 -0
  143. package/dist/src/ui/components/SubredditMultiSelect.js.map +1 -0
  144. package/dist/src/ui/components/TextPrompt.d.ts +10 -0
  145. package/dist/src/ui/components/TextPrompt.js +13 -0
  146. package/dist/src/ui/components/TextPrompt.js.map +1 -0
  147. package/dist/src/ui/components/YesNoSelector.d.ts +10 -0
  148. package/dist/src/ui/components/YesNoSelector.js +25 -0
  149. package/dist/src/ui/components/YesNoSelector.js.map +1 -0
  150. package/dist/src/ui/components/subredditOptions.d.ts +5 -0
  151. package/dist/src/ui/components/subredditOptions.js +14 -0
  152. package/dist/src/ui/components/subredditOptions.js.map +1 -0
  153. package/dist/src/ui/components/yesNoSelectorModel.d.ts +9 -0
  154. package/dist/src/ui/components/yesNoSelectorModel.js +23 -0
  155. package/dist/src/ui/components/yesNoSelectorModel.js.map +1 -0
  156. package/dist/src/ui/theme.d.ts +26 -0
  157. package/dist/src/ui/theme.js +37 -0
  158. package/dist/src/ui/theme.js.map +1 -0
  159. package/dist/src/utils/logger.d.ts +5 -0
  160. package/dist/src/utils/logger.js +15 -0
  161. package/dist/src/utils/logger.js.map +1 -0
  162. package/dist/src/utils/notify.d.ts +6 -0
  163. package/dist/src/utils/notify.js +14 -0
  164. package/dist/src/utils/notify.js.map +1 -0
  165. package/dist/src/utils/paths.d.ts +10 -0
  166. package/dist/src/utils/paths.js +24 -0
  167. package/dist/src/utils/paths.js.map +1 -0
  168. package/dist/src/utils/scanLogFormatting.d.ts +26 -0
  169. package/dist/src/utils/scanLogFormatting.js +60 -0
  170. package/dist/src/utils/scanLogFormatting.js.map +1 -0
  171. 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":"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"}
@@ -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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"}