@extends('admin.layout', ['menuKey' => 'crawler']) @section('title', 'Crawler') @section('content') @php $hbAt = isset($heartbeat['at']) ? \Illuminate\Support\Carbon::parse($heartbeat['at']) : null; $hbOk = $hbAt && $hbAt->gt(now()->subMinutes(15)); $pill = fn ($s) => ['active' => 'on', 'paused' => 'wait', 'failing' => 'bad', 'blocked' => 'bad', 'robots' => 'off', 'opted_out' => 'off', 'existing_org' => 'off'][$s] ?? 'off'; @endphp
Board

Crawler

Healthcare organizations whose own careers sites the crawler visits. Their open jobs go on the board as Unclaimed posts, rewritten in new words with the same facts, with an Apply button to the employer's site. Each site is visited in full every {{ config('sync.revisit_days') }} days and each live job is checked daily; a job comes down after two misses in a row.

The /bot page
{{ $hbOk ? 'Running' : ($hbAt ? 'Silent' : 'Not started') }}
Crawler service @if ($hbAt)· heard {{ $hbAt->diffForHumans() }}@if (! empty($heartbeat['version'])) · v{{ $heartbeat['version'] }}@endif @endif
{{ number_format($states['live'] ?? 0) }}
Live crawled jobs · {{ number_format($states['pending'] ?? 0) }} being prepared · {{ number_format($states['held'] ?? 0) }} in review · {{ number_format($states['attn'] ?? 0) }} need a fix
${{ number_format($ai['spent'], 2) }}
Claude this month, of ${{ $ai['budget'] }} · {{ number_format($ai['calls']) }} rewrites
{{ number_format(array_sum($counts)) }}
Sites · {{ $counts['active'] ?? 0 }} active · {{ ($counts['failing'] ?? 0) + ($counts['blocked'] ?? 0) + ($counts['paused'] ?? 0) }} need a look · {{ $counts['opted_out'] ?? 0 }} opted out
@if (! $tokenSet)@endif @include('partials.errors')
@csrf

Add an organization

@csrf

Import a list (CSV)

Columns: name, website, careers_url, country, region. Only name and website are needed. Organizations already on the board with their own team are listed but not crawled.

@csrf

Settings

About 1 cent a job. When it runs out, new jobs wait for next month.
Google allows about 200 a day for the whole site; posts made on the site always get theirs.

Government lists

@foreach (config('sync.directory.datasets') as $key => $set) @endforeach
About 3 to 6 cents a facility, once. Later months only new facilities.
3 a minute finishes about 5,400 hospitals in 1 to 2 days.

Government lists (CMS)

Each facility's website is looked up with Claude and web search (${{ number_format($finder['spent'], 2) }} of ${{ $finder['budget'] }} this month, {{ number_format($finder['calls']) }} lookups). The crawler then checks the site shows the facility's phone or street address before crawling it. Lists refresh on the 3rd of each month.

@if ($finderPaused) @endif @if (! $directory)

No list downloaded yet. Tick a list above and save; it downloads within a minute.

@else
@foreach (\App\Models\DirectoryEntry::STATUSES as $k => $label)@endforeach @foreach ($directory as $set => $counts) @foreach (array_keys(\App\Models\DirectoryEntry::STATUSES) as $k)@endforeach @endforeach
List{{ $label }}
{{ config('sync.directory.datasets.'.$set.'.label', $set) }}{{ number_format($counts[$k] ?? 0) }}
@if ($directoryLatest->isNotEmpty())

Latest

@foreach ($directoryLatest as $d) @endforeach
FacilityWebsiteResult
{{ $d->name }}
{{ $d->city }}, {{ $d->state }}
@if ($d->website){{ $d->domain }} ↗@else – @endif {{ \App\Models\DirectoryEntry::STATUSES[$d->status] ?? $d->status }}@if ($d->reason)
{{ $d->reason }}@endif
@endif @endif
@if ($status !== '')@endif
@if ($sites->isEmpty())
No sites yet. Add an organization or import a list above.
@else
@foreach ($sites as $s) @endforeach
OrganizationStatusFound withLiveFixLast visitNext
{{ $s->name }}
{{ $s->domain }}@if ($s->organization_id) · org #{{ $s->organization_id }}@endif @if ($s->careers_url)
careers page ↗@endif
{{ $s->statusLabel() }}@if ($s->status_reason)
{{ $s->status_reason }}@endif
{{ $s->detected ?? '–' }}@if ($s->board)
{{ \Illuminate\Support\Str::limit($s->board, 30) }}@endif
{{ $s->live_count }} {{ $s->attn_count ?: '' }} {{ $s->last_visit_at?->diffForHumans() ?? 'never' }} {{ $s->status === 'active' ? ($s->next_visit_at?->isPast() ? 'now' : $s->next_visit_at?->diffForHumans()) : '–' }} @if ($s->status !== 'opted_out')
@csrf @if ($s->status === 'active') @elseif ($s->status !== 'existing_org')@endif
@endif
{{ $sites->links() }} @endif

Jobs that need a fix

Real jobs that can't go up as they are: no pay where the law requires it, a title that matches none of the standard titles, no location. Most fix themselves when the employer updates their page. Add a missing job title in Job titles (with the employer's wording as an alias), then retry.

@if ($attn->isEmpty())

Nothing waiting.

@else
@foreach ($attn as $i) @endforeach
JobWhy
{{ \Illuminate\Support\Str::limit((string) ($i->raw['title'] ?? $i->external_id), 70) }} ↗
{{ $i->source?->name }}
{{ $i->reason }}
@csrf
@endif

Latest visits

@if ($reports->isEmpty())

No visits yet.

@else @foreach ($reports as $r) @endforeach
WhenSiteCrawler foundResult
{{ $r->created_at->format('M j, g:i A') }}{{ $sourceNames[$r->sync_source_id] ?? '#'.$r->sync_source_id }}{{ $r->status }}{{ $r->result ?? 'Processing…' }}
@endif
@endsection