Score: {{ app.score }}
diff --git a/apps/web/src/views/Research.vue b/apps/web/src/views/Research.vue
index 5ddc1a3..cf40d6a 100644
--- a/apps/web/src/views/Research.vue
+++ b/apps/web/src/views/Research.vue
@@ -2,6 +2,7 @@
import { onMounted, ref } from 'vue'
import { useToastStore } from '@/stores/toast'
import * as api from '@/api'
+import { HttpError } from '@/api'
import type { JobPosting } from '@/types'
const toast = useToastStore()
@@ -11,10 +12,32 @@ const loading = ref(true)
const newUrl = ref('')
const scoringId = ref
(null)
const scoreMap = ref }>>({})
+const redFlagsMap = ref>({})
+
+// Fetch form
+const fetchQuery = ref('')
+const fetchRegion = ref('')
+const fetching = ref(false)
+const fetchResult = ref<{ new: number; dupes: number } | null>(null)
async function loadPostings() {
try {
postings.value = await api.getPostings()
+ // Load red flags for existing postings via batch scoring
+ if (postings.value.length > 0) {
+ try {
+ const batch = await api.batchScore(postings.value.map((p) => p.id))
+ const map: Record = {}
+ for (const r of batch.results) {
+ if (r.red_flags && r.red_flags.length > 0) {
+ map[r.application_id] = r.red_flags
+ }
+ }
+ redFlagsMap.value = map
+ } catch {
+ // batch scoring is optional; ignore errors
+ }
+ }
} catch {
toast.push('Failed to load postings', 'error')
} finally {
@@ -34,6 +57,27 @@ async function addPosting() {
}
}
+async function doFetch() {
+ if (!fetchQuery.value.trim()) return
+ fetching.value = true
+ fetchResult.value = null
+ try {
+ fetchResult.value = await api.fetchPostings(fetchQuery.value.trim(), fetchRegion.value.trim() || undefined)
+ toast.push(`Fetched ${fetchResult.value.new} new postings`, 'success')
+ // Reload postings to show new ones
+ await loadPostings()
+ } catch (err) {
+ let msg = 'Fetch failed'
+ if (err instanceof HttpError) {
+ const body = err.body as { error?: { message?: string } } | null
+ msg = body?.error?.message ?? msg
+ }
+ toast.push(msg, 'error')
+ } finally {
+ fetching.value = false
+ }
+}
+
async function scorePosting(p: JobPosting) {
scoringId.value = p.id
try {
@@ -47,6 +91,15 @@ async function scorePosting(p: JobPosting) {
}
}
+function hasScamFlag(p: JobPosting): boolean {
+ const flags = redFlagsMap.value[p.id]
+ return flags != null && flags.length > 0
+}
+
+function scamFlagsFor(p: JobPosting): string[] {
+ return redFlagsMap.value[p.id] ?? []
+}
+
onMounted(loadPostings)
@@ -54,6 +107,37 @@ onMounted(loadPostings)