prospector_engine 0.1.1

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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +333 -0
  4. data/Rakefile +9 -0
  5. data/app/CLAUDE.md +43 -0
  6. data/app/assets/stylesheets/prospector/application.css +476 -0
  7. data/app/controllers/prospector/application_controller.rb +16 -0
  8. data/app/controllers/prospector/candidates_controller.rb +31 -0
  9. data/app/controllers/prospector/keyword_generations_controller.rb +10 -0
  10. data/app/controllers/prospector/keywords_controller.rb +38 -0
  11. data/app/controllers/prospector/run_bulk_approvals_controller.rb +13 -0
  12. data/app/controllers/prospector/run_cancellations_controller.rb +9 -0
  13. data/app/controllers/prospector/run_reclassifications_controller.rb +21 -0
  14. data/app/controllers/prospector/run_restarts_controller.rb +14 -0
  15. data/app/controllers/prospector/run_retries_controller.rb +14 -0
  16. data/app/controllers/prospector/runs_controller.rb +47 -0
  17. data/app/jobs/prospector/application_job.rb +5 -0
  18. data/app/jobs/prospector/bulk_approve_job.rb +14 -0
  19. data/app/jobs/prospector/classify_job.rb +17 -0
  20. data/app/jobs/prospector/fetch_job.rb +8 -0
  21. data/app/models/prospector/application_record.rb +6 -0
  22. data/app/models/prospector/candidate.rb +93 -0
  23. data/app/models/prospector/classification_run.rb +15 -0
  24. data/app/models/prospector/keyword.rb +16 -0
  25. data/app/models/prospector/run.rb +94 -0
  26. data/app/views/prospector/candidates/show.html.erb +63 -0
  27. data/app/views/prospector/keywords/index.html.erb +72 -0
  28. data/app/views/prospector/layouts/prospector.html.erb +38 -0
  29. data/app/views/prospector/runs/index.html.erb +33 -0
  30. data/app/views/prospector/runs/new.html.erb +109 -0
  31. data/app/views/prospector/runs/show.html.erb +111 -0
  32. data/config/routes.rb +15 -0
  33. data/db/prospector_schema.rb +81 -0
  34. data/lib/generators/prospector/install/install_generator.rb +31 -0
  35. data/lib/generators/prospector/install/templates/create_prospector_tables.rb +83 -0
  36. data/lib/generators/prospector/install/templates/prospector.rb +37 -0
  37. data/lib/prospector/CLAUDE.md +52 -0
  38. data/lib/prospector/classification/runner.rb +105 -0
  39. data/lib/prospector/configuration.rb +56 -0
  40. data/lib/prospector/engine.rb +18 -0
  41. data/lib/prospector/enrichment/contact_scraper.rb +188 -0
  42. data/lib/prospector/error.rb +8 -0
  43. data/lib/prospector/geography/base.rb +40 -0
  44. data/lib/prospector/geography/bounding_box.rb +58 -0
  45. data/lib/prospector/geography/city.rb +29 -0
  46. data/lib/prospector/geography/coordinates.rb +43 -0
  47. data/lib/prospector/geography/metro_area.rb +74 -0
  48. data/lib/prospector/geography/zip_code.rb +25 -0
  49. data/lib/prospector/keywords/generator.rb +74 -0
  50. data/lib/prospector/pipeline/normalizer.rb +57 -0
  51. data/lib/prospector/pipeline/orchestrator.rb +151 -0
  52. data/lib/prospector/sources/base.rb +13 -0
  53. data/lib/prospector/sources/google_places/adapter.rb +92 -0
  54. data/lib/prospector/sources/google_places/client.rb +58 -0
  55. data/lib/prospector/sources/google_places/us_address_validator.rb +24 -0
  56. data/lib/prospector/sources/result.rb +21 -0
  57. data/lib/prospector/version.rb +3 -0
  58. data/lib/prospector.rb +20 -0
  59. metadata +185 -0
@@ -0,0 +1,476 @@
1
+ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=JetBrains+Mono:wght@400;500;600&display=swap');
2
+
3
+ :root {
4
+ --p-bg: #111113;
5
+ --p-bg-elevated: #18181b;
6
+ --p-surface: #1c1c20;
7
+ --p-surface-hover: #222226;
8
+ --p-border: #2a2a2f;
9
+ --p-border-subtle: #232328;
10
+ --p-text: #ececef;
11
+ --p-text-secondary: #a1a1a8;
12
+ --p-text-muted: #6b6b73;
13
+ --p-accent: #e2a336;
14
+ --p-accent-hover: #c98c1f;
15
+ --p-accent-subtle: rgba(226, 163, 54, 0.12);
16
+ --p-accent-border: rgba(226, 163, 54, 0.25);
17
+ --p-success: #3ec97a;
18
+ --p-success-bg: rgba(62, 201, 122, 0.1);
19
+ --p-warning: #e2a336;
20
+ --p-warning-bg: rgba(226, 163, 54, 0.1);
21
+ --p-danger: #e55b5b;
22
+ --p-danger-bg: rgba(229, 91, 91, 0.1);
23
+ --p-info: #5ba4e5;
24
+ --p-info-bg: rgba(91, 164, 229, 0.1);
25
+ --p-purple: #a78bfa;
26
+ --p-purple-bg: rgba(167, 139, 250, 0.1);
27
+ --p-radius: 4px;
28
+ --p-font: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
29
+ --p-mono: 'JetBrains Mono', 'SF Mono', monospace;
30
+ --p-shadow: 0 1px 3px rgba(0,0,0,0.4), 0 1px 2px rgba(0,0,0,0.3);
31
+ --p-shadow-lg: 0 4px 12px rgba(0,0,0,0.5);
32
+ }
33
+
34
+ /* ---- Base ---- */
35
+
36
+ .prospector-ui {
37
+ font-family: var(--p-font);
38
+ background: var(--p-bg);
39
+ color: var(--p-text);
40
+ min-height: 100vh;
41
+ display: flex;
42
+ flex-direction: column;
43
+ font-size: 14px;
44
+ line-height: 1.5;
45
+ -webkit-font-smoothing: antialiased;
46
+ -moz-osx-font-smoothing: grayscale;
47
+ }
48
+
49
+ .prospector-ui *, .prospector-ui *::before, .prospector-ui *::after {
50
+ box-sizing: border-box;
51
+ }
52
+
53
+ .prospector-ui a:not(.prospector-nav-title):not(.prospector-nav-link):not(.prospector-btn):not(.prospector-tab) { color: var(--p-accent); transition: color 0.15s; }
54
+ .prospector-ui a:not(.prospector-nav-title):not(.prospector-nav-link):not(.prospector-btn):not(.prospector-tab):hover { color: var(--p-text); }
55
+
56
+ /* ---- Navigation ---- */
57
+
58
+ .prospector-nav {
59
+ display: flex;
60
+ align-items: center;
61
+ padding: 0 1.5rem;
62
+ height: 48px;
63
+ background: var(--p-surface);
64
+ border-bottom: 1px solid var(--p-border);
65
+ position: sticky;
66
+ top: 0;
67
+ z-index: 100;
68
+ }
69
+
70
+ .prospector-nav-brand {
71
+ font-weight: 600;
72
+ font-size: 0.8rem;
73
+ letter-spacing: 0.08em;
74
+ text-transform: uppercase;
75
+ }
76
+
77
+ .prospector-nav-title {
78
+ color: var(--p-accent);
79
+ text-decoration: none;
80
+ font-family: var(--p-mono);
81
+ font-weight: 600;
82
+ }
83
+ .prospector-nav-title:hover { color: var(--p-text); }
84
+
85
+ .prospector-nav-links {
86
+ margin-left: 2rem;
87
+ display: flex;
88
+ gap: 0;
89
+ height: 48px;
90
+ align-items: stretch;
91
+ }
92
+
93
+ .prospector-nav-link {
94
+ color: var(--p-text-secondary);
95
+ text-decoration: none;
96
+ font-size: 0.8rem;
97
+ font-weight: 500;
98
+ letter-spacing: 0.02em;
99
+ display: flex;
100
+ align-items: center;
101
+ padding: 0 1rem;
102
+ border-bottom: 2px solid transparent;
103
+ transition: color 0.15s, border-color 0.15s;
104
+ }
105
+ .prospector-nav-link:hover {
106
+ color: var(--p-text);
107
+ border-bottom-color: var(--p-border);
108
+ }
109
+
110
+ /* ---- Main Content ---- */
111
+
112
+ .prospector-main {
113
+ max-width: 1140px;
114
+ margin: 0 auto;
115
+ padding: 2rem 1.5rem;
116
+ flex: 1;
117
+ width: 100%;
118
+ box-sizing: border-box;
119
+ }
120
+
121
+ /* ---- Flash Messages ---- */
122
+
123
+ .prospector-flash {
124
+ padding: 0.625rem 0.875rem;
125
+ border-radius: var(--p-radius);
126
+ margin-bottom: 1.25rem;
127
+ font-size: 0.8rem;
128
+ font-weight: 500;
129
+ border-left: 3px solid;
130
+ }
131
+ .prospector-flash-notice {
132
+ background: var(--p-success-bg);
133
+ color: var(--p-success);
134
+ border-left-color: var(--p-success);
135
+ }
136
+ .prospector-flash-alert {
137
+ background: var(--p-danger-bg);
138
+ color: var(--p-danger);
139
+ border-left-color: var(--p-danger);
140
+ }
141
+
142
+ /* ---- Cards ---- */
143
+
144
+ .prospector-card {
145
+ background: var(--p-surface);
146
+ border: 1px solid var(--p-border);
147
+ border-radius: var(--p-radius);
148
+ padding: 1.25rem;
149
+ margin-bottom: 1rem;
150
+ box-shadow: var(--p-shadow);
151
+ }
152
+
153
+ /* ---- Typography ---- */
154
+
155
+ .prospector-heading {
156
+ font-size: 1.25rem;
157
+ font-weight: 700;
158
+ margin-bottom: 1.25rem;
159
+ letter-spacing: -0.01em;
160
+ color: var(--p-text);
161
+ }
162
+
163
+ .prospector-subheading {
164
+ font-size: 0.7rem;
165
+ font-weight: 600;
166
+ margin-bottom: 0.75rem;
167
+ text-transform: uppercase;
168
+ letter-spacing: 0.06em;
169
+ color: var(--p-text-muted);
170
+ }
171
+
172
+ /* ---- Tables ---- */
173
+
174
+ table.prospector-table {
175
+ width: 100%;
176
+ border-collapse: collapse;
177
+ }
178
+
179
+ .prospector-table th {
180
+ text-align: left;
181
+ padding: 0.5rem 0.875rem;
182
+ font-family: var(--p-mono);
183
+ font-size: 0.65rem;
184
+ font-weight: 500;
185
+ text-transform: uppercase;
186
+ letter-spacing: 0.08em;
187
+ color: var(--p-text-muted);
188
+ border-bottom: 1px solid var(--p-border);
189
+ background: var(--p-bg-elevated);
190
+ }
191
+
192
+ .prospector-table td {
193
+ padding: 0.625rem 0.875rem;
194
+ font-size: 0.8rem;
195
+ border-bottom: 1px solid var(--p-border-subtle);
196
+ color: var(--p-text-secondary);
197
+ }
198
+
199
+ .prospector-table td strong,
200
+ .prospector-table td a:not(.prospector-btn) {
201
+ color: var(--p-text);
202
+ }
203
+
204
+ .prospector-table tr { transition: background 0.1s; }
205
+ .prospector-table tbody tr:hover td { background: var(--p-surface-hover); }
206
+
207
+ .prospector-table tr:last-child td { border-bottom: none; }
208
+
209
+ /* ---- Badges ---- */
210
+
211
+ .prospector-badge {
212
+ display: inline-flex;
213
+ align-items: center;
214
+ padding: 0.15rem 0.5rem;
215
+ border-radius: 3px;
216
+ font-family: var(--p-mono);
217
+ font-size: 0.65rem;
218
+ font-weight: 500;
219
+ letter-spacing: 0.03em;
220
+ text-transform: uppercase;
221
+ border: 1px solid transparent;
222
+ }
223
+
224
+ .prospector-badge-pending {
225
+ background: var(--p-warning-bg);
226
+ color: var(--p-warning);
227
+ border-color: rgba(226, 163, 54, 0.2);
228
+ }
229
+ .prospector-badge-running {
230
+ background: var(--p-info-bg);
231
+ color: var(--p-info);
232
+ border-color: rgba(91, 164, 229, 0.2);
233
+ animation: p-pulse 2s ease-in-out infinite;
234
+ }
235
+ .prospector-badge-classifying {
236
+ background: var(--p-purple-bg);
237
+ color: var(--p-purple);
238
+ border-color: rgba(167, 139, 250, 0.2);
239
+ animation: p-pulse 2s ease-in-out infinite;
240
+ }
241
+ .prospector-badge-completed {
242
+ background: var(--p-success-bg);
243
+ color: var(--p-success);
244
+ border-color: rgba(62, 201, 122, 0.2);
245
+ }
246
+ .prospector-badge-failed {
247
+ background: var(--p-danger-bg);
248
+ color: var(--p-danger);
249
+ border-color: rgba(229, 91, 91, 0.2);
250
+ }
251
+ .prospector-badge-cancelled {
252
+ background: rgba(107, 107, 115, 0.1);
253
+ color: var(--p-text-muted);
254
+ border-color: rgba(107, 107, 115, 0.2);
255
+ }
256
+ .prospector-badge-approved {
257
+ background: var(--p-success-bg);
258
+ color: var(--p-success);
259
+ border-color: rgba(62, 201, 122, 0.2);
260
+ }
261
+ .prospector-badge-rejected {
262
+ background: var(--p-danger-bg);
263
+ color: var(--p-danger);
264
+ border-color: rgba(229, 91, 91, 0.2);
265
+ }
266
+
267
+ @keyframes p-pulse {
268
+ 0%, 100% { opacity: 1; }
269
+ 50% { opacity: 0.6; }
270
+ }
271
+
272
+ /* ---- Buttons ---- */
273
+
274
+ .prospector-btn {
275
+ display: inline-flex;
276
+ align-items: center;
277
+ justify-content: center;
278
+ gap: 0.375rem;
279
+ padding: 0.5rem 0.875rem;
280
+ border-radius: var(--p-radius);
281
+ font-family: var(--p-font);
282
+ font-size: 0.8rem;
283
+ font-weight: 500;
284
+ text-decoration: none;
285
+ border: 1px solid transparent;
286
+ cursor: pointer;
287
+ transition: all 0.15s;
288
+ white-space: nowrap;
289
+ }
290
+
291
+ .prospector-btn-primary {
292
+ background: var(--p-accent);
293
+ color: #111113;
294
+ border-color: var(--p-accent);
295
+ font-weight: 600;
296
+ }
297
+ .prospector-btn-primary:hover {
298
+ background: var(--p-accent-hover);
299
+ border-color: var(--p-accent-hover);
300
+ box-shadow: 0 0 12px rgba(226, 163, 54, 0.2);
301
+ }
302
+
303
+ .prospector-btn-danger {
304
+ background: transparent;
305
+ color: var(--p-danger);
306
+ border-color: rgba(229, 91, 91, 0.3);
307
+ }
308
+ .prospector-btn-danger:hover {
309
+ background: var(--p-danger-bg);
310
+ border-color: var(--p-danger);
311
+ }
312
+
313
+ .prospector-btn-outline {
314
+ background: transparent;
315
+ color: var(--p-text-secondary);
316
+ border-color: var(--p-border);
317
+ }
318
+ .prospector-btn-outline:hover {
319
+ color: var(--p-text);
320
+ border-color: var(--p-text-muted);
321
+ background: var(--p-surface-hover);
322
+ }
323
+
324
+ .prospector-btn-sm {
325
+ padding: 0.25rem 0.5rem;
326
+ font-size: 0.7rem;
327
+ }
328
+
329
+ /* ---- Forms ---- */
330
+
331
+ .prospector-form-group { margin-bottom: 1rem; }
332
+
333
+ .prospector-label {
334
+ display: block;
335
+ font-size: 0.7rem;
336
+ font-weight: 600;
337
+ margin-bottom: 0.375rem;
338
+ color: var(--p-text-muted);
339
+ text-transform: uppercase;
340
+ letter-spacing: 0.04em;
341
+ }
342
+
343
+ .prospector-input, .prospector-select {
344
+ width: 100%;
345
+ padding: 0.5rem 0.75rem;
346
+ background: var(--p-bg);
347
+ border: 1px solid var(--p-border);
348
+ border-radius: var(--p-radius);
349
+ color: var(--p-text);
350
+ font-family: var(--p-font);
351
+ font-size: 0.8rem;
352
+ transition: border-color 0.15s, box-shadow 0.15s;
353
+ }
354
+
355
+ .prospector-input::placeholder { color: var(--p-text-muted); }
356
+
357
+ .prospector-input:focus, .prospector-select:focus {
358
+ outline: none;
359
+ border-color: var(--p-accent);
360
+ box-shadow: 0 0 0 2px var(--p-accent-subtle);
361
+ }
362
+
363
+ fieldset.prospector-form-group {
364
+ border: 1px solid var(--p-border-subtle);
365
+ border-radius: var(--p-radius);
366
+ padding: 1rem;
367
+ }
368
+
369
+ fieldset.prospector-form-group legend {
370
+ padding: 0 0.375rem;
371
+ }
372
+
373
+ /* ---- Stats ---- */
374
+
375
+ .prospector-stats {
376
+ display: grid;
377
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
378
+ gap: 0.75rem;
379
+ margin-bottom: 1.5rem;
380
+ }
381
+
382
+ .prospector-stat {
383
+ background: var(--p-surface);
384
+ border: 1px solid var(--p-border);
385
+ border-radius: var(--p-radius);
386
+ padding: 1rem;
387
+ box-shadow: var(--p-shadow);
388
+ border-top: 2px solid var(--p-accent-border);
389
+ }
390
+
391
+ .prospector-stat-value {
392
+ font-family: var(--p-mono);
393
+ font-size: 1.5rem;
394
+ font-weight: 600;
395
+ color: var(--p-text);
396
+ letter-spacing: -0.02em;
397
+ }
398
+
399
+ .prospector-stat-label {
400
+ font-size: 0.65rem;
401
+ font-weight: 500;
402
+ color: var(--p-text-muted);
403
+ text-transform: uppercase;
404
+ letter-spacing: 0.06em;
405
+ margin-top: 0.25rem;
406
+ }
407
+
408
+ /* ---- Tabs ---- */
409
+
410
+ .prospector-tabs {
411
+ display: flex;
412
+ gap: 0;
413
+ margin-bottom: 1rem;
414
+ border-bottom: 1px solid var(--p-border);
415
+ }
416
+
417
+ .prospector-tab {
418
+ padding: 0.625rem 1rem;
419
+ font-size: 0.8rem;
420
+ font-weight: 500;
421
+ text-decoration: none;
422
+ color: var(--p-text-muted);
423
+ border-bottom: 2px solid transparent;
424
+ margin-bottom: -1px;
425
+ transition: color 0.15s, border-color 0.15s;
426
+ }
427
+ .prospector-tab:hover {
428
+ color: var(--p-text-secondary);
429
+ }
430
+ .prospector-tab-active {
431
+ color: var(--p-accent);
432
+ border-bottom-color: var(--p-accent);
433
+ }
434
+
435
+ /* ---- Utilities ---- */
436
+
437
+ .prospector-flex { display: flex; }
438
+ .prospector-flex-between { justify-content: space-between; align-items: center; }
439
+ .prospector-gap-2 { gap: 0.5rem; }
440
+ .prospector-gap-4 { gap: 1rem; }
441
+ .prospector-mt-4 { margin-top: 1rem; }
442
+ .prospector-text-muted { color: var(--p-text-muted); }
443
+ .prospector-text-sm { font-size: 0.8rem; }
444
+
445
+ /* ---- Footer ---- */
446
+
447
+ .prospector-footer {
448
+ border-top: 1px solid var(--p-border-subtle);
449
+ padding: 0.75rem 1.5rem;
450
+ margin-top: auto;
451
+ }
452
+
453
+ .prospector-footer-inner {
454
+ max-width: 1140px;
455
+ margin: 0 auto;
456
+ display: flex;
457
+ justify-content: space-between;
458
+ align-items: center;
459
+ font-family: var(--p-mono);
460
+ font-size: 0.55rem;
461
+ text-transform: uppercase;
462
+ letter-spacing: 0.08em;
463
+ color: var(--p-text-muted);
464
+ opacity: 0.5;
465
+ }
466
+
467
+ .prospector-footer a {
468
+ color: var(--p-text-muted);
469
+ text-decoration: none;
470
+ border-bottom: 1px solid transparent;
471
+ transition: color 0.15s, border-color 0.15s;
472
+ }
473
+ .prospector-footer a:hover {
474
+ color: var(--p-text-secondary);
475
+ border-bottom-color: var(--p-text-muted);
476
+ }
@@ -0,0 +1,16 @@
1
+ module Prospector
2
+ class ApplicationController < ::ApplicationController
3
+ layout "prospector/layouts/prospector"
4
+
5
+ before_action :authenticate_prospector_admin!
6
+
7
+ private
8
+
9
+ def authenticate_prospector_admin!
10
+ auth = Prospector.config.authenticate_admin_with
11
+ return if auth&.call(self)
12
+
13
+ redirect_to main_app.root_path, alert: "Not authorized"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ module Prospector
2
+ class CandidatesController < ApplicationController
3
+ def show
4
+ @candidate = Candidate.find(params[:id])
5
+ @run = @candidate.run
6
+ end
7
+
8
+ def update
9
+ @candidate = Candidate.find(params[:id])
10
+ apply_status_change
11
+ redirect_to run_path(@candidate.run, filter: params[:return_filter] || @candidate.status)
12
+ end
13
+
14
+ private
15
+
16
+ def apply_status_change
17
+ case params[:status]
18
+ when "approved"
19
+ @candidate.approvable? ? @candidate.approve! : flash_guard_failure("approve")
20
+ when "rejected"
21
+ @candidate.pending? ? @candidate.reject! : flash_guard_failure("reject")
22
+ when "pending"
23
+ @candidate.rejected? ? @candidate.restore_to_pending! : flash_guard_failure("restore")
24
+ end
25
+ end
26
+
27
+ def flash_guard_failure(action)
28
+ flash[:alert] = "Cannot #{action} candidate in its current state"
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,10 @@
1
+ module Prospector
2
+ class KeywordGenerationsController < ApplicationController
3
+ def create
4
+ domain = params[:domain] || Prospector.config.domain
5
+ category = params[:category]
6
+ Keywords::Generator.for(domain: domain, categories: [category])
7
+ redirect_to keywords_path(domain: domain), notice: "Keywords generated for #{category}"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,38 @@
1
+ module Prospector
2
+ class KeywordsController < ApplicationController
3
+ def index
4
+ @domain = params[:domain] || Prospector.config.domain
5
+ @keywords = Keyword.for_domain(@domain).order(:category, :keyword)
6
+ @categories = Keyword.for_domain(@domain).distinct.pluck(:category).sort
7
+ end
8
+
9
+ def create
10
+ @keyword = Keyword.new(keyword_params)
11
+ @keyword.source = "manual"
12
+ if @keyword.save
13
+ redirect_to keywords_path(domain: @keyword.domain), notice: "Keyword added"
14
+ else
15
+ redirect_to keywords_path(domain: @keyword.domain), alert: @keyword.errors.full_messages.join(", ")
16
+ end
17
+ end
18
+
19
+ def update
20
+ keyword = Keyword.find(params[:id])
21
+ keyword.update!(active: !keyword.active)
22
+ redirect_to keywords_path(domain: keyword.domain)
23
+ end
24
+
25
+ def destroy
26
+ keyword = Keyword.find(params[:id])
27
+ domain = keyword.domain
28
+ keyword.destroy!
29
+ redirect_to keywords_path(domain: domain), notice: "Keyword removed"
30
+ end
31
+
32
+ private
33
+
34
+ def keyword_params
35
+ params.require(:keyword).permit(:domain, :category, :keyword)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,13 @@
1
+ module Prospector
2
+ class RunBulkApprovalsController < ApplicationController
3
+ def create
4
+ run = Run.find(params[:run_id])
5
+ unless run.completed? || run.classifying?
6
+ redirect_to run_path(run), alert: "Run is not ready for approval"
7
+ return
8
+ end
9
+ BulkApproveJob.perform_later(run.id)
10
+ redirect_to run_path(run), notice: "Bulk approval started"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ module Prospector
2
+ class RunCancellationsController < ApplicationController
3
+ def create
4
+ run = Run.find(params[:run_id])
5
+ run.cancel! if run.cancellable?
6
+ redirect_to run_path(run), notice: "Import cancelled"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ module Prospector
2
+ class RunReclassificationsController < ApplicationController
3
+ def create
4
+ run = Run.find(params[:run_id])
5
+ cr = build_classification_run(run)
6
+ cr.save!
7
+ ClassifyJob.perform_later(run.id, force: cr.include_approved, model: cr.ai_model, classification_run_id: cr.id)
8
+ redirect_to run_path(run), notice: "Reclassification started"
9
+ end
10
+
11
+ private
12
+
13
+ def build_classification_run(run)
14
+ run.classification_runs.new(
15
+ ai_model: params[:ai_model] || Prospector.config.default_classifier_model,
16
+ include_approved: params[:include_approved] == "1",
17
+ actor_id: respond_to?(:current_user) ? current_user&.id : nil
18
+ )
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ module Prospector
2
+ class RunRestartsController < ApplicationController
3
+ def create
4
+ run = Run.lock.find(params[:run_id])
5
+ unless run.restartable?
6
+ redirect_to run_path(run), alert: "Run cannot be restarted"
7
+ return
8
+ end
9
+ run.restart!
10
+ FetchJob.perform_later(run.id)
11
+ redirect_to run_path(run), notice: "Import restarted"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module Prospector
2
+ class RunRetriesController < ApplicationController
3
+ def create
4
+ run = Run.lock.find(params[:run_id])
5
+ unless run.retryable?
6
+ redirect_to run_path(run), alert: "Run cannot be retried"
7
+ return
8
+ end
9
+ run.reset_for_retry!
10
+ FetchJob.perform_later(run.id)
11
+ redirect_to run_path(run), notice: "Retrying import"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,47 @@
1
+ module Prospector
2
+ class RunsController < ApplicationController
3
+ def index
4
+ @runs = Run.recent.limit(50)
5
+ end
6
+
7
+ def show
8
+ @run = Run.find(params[:id])
9
+ @filter = params[:filter] || "pending"
10
+ @candidates = @run.candidates.by_status(@filter)
11
+ @status_counts = @run.candidates.group(:status).count
12
+ end
13
+
14
+ def new
15
+ @run = Run.new(source_adapter: Prospector.config.default_source)
16
+ end
17
+
18
+ def create
19
+ @run = Run.new(run_params)
20
+ if @run.save
21
+ FetchJob.perform_later(@run.id)
22
+ redirect_to run_path(@run), notice: "Import started"
23
+ else
24
+ render :new, status: :unprocessable_entity
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ GEOGRAPHY_KEYS = %i[
31
+ name primary_state city state
32
+ lat lng radius_meters zip
33
+ ne_lat ne_lng sw_lat sw_lng
34
+ ].freeze
35
+
36
+ def run_params
37
+ params.require(:run).permit(:source_adapter, :geography_type, :label, categories: []).tap do |p|
38
+ p[:geography_data] = params.require(:run).permit(geography_data: GEOGRAPHY_KEYS)[:geography_data]&.to_h || {}
39
+ p[:actor_id] = current_user_id
40
+ end
41
+ end
42
+
43
+ def current_user_id
44
+ respond_to?(:current_user) ? current_user&.id : nil
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,5 @@
1
+ module Prospector
2
+ class ApplicationJob < ActiveJob::Base
3
+ queue_as { Prospector.config.queue_name }
4
+ end
5
+ end