JobCache Ops — Concrete Views
JobCache is CareerVector's sister product, sharing the role → ad →
workspace job fact tree (CLAUDE.md sibling repo notes; jobcache repo
.agent/AGENT.md). Today it focuses solely on extracting job ads.
Its Ops surface therefore reads as a business cockpit for corpus
ingestion, not a user-friction cockpit.
Current shell contract: JobCache Ops uses the same primary tabs as CareerVector Ops:
Anomalies | Events | Infra | Inspector | Sysadmin
The source matrix, ingestion tape, device fleet, corpus panels, and recovery actions described below are JobCache-specific content inside those canonical tabs. They are not a separate one-page product shell.
The audience is different from CV's:
| Audience | Cares about | Will eventually pay for |
|---|---|---|
| Operator (today: Julian) | "Is the corpus growing? Is anything stuck? Is the cost line healthy?" | Operating the platform. |
| Business stakeholder (future) | "What sources are most productive? What is the cost per useful ad? Where is coverage weak?" | The corpus itself, as a B2B data product. |
| Ingestion partner (future) | "Is JobCache live? Is my source represented?" | Listing their source. |
Status (status.jobcache.corbet.ch) speaks to all three audiences in
aggregate. Ops (ops.jobcache.corbet.ch) is the operator's working surface and
the business stakeholder's gated read-only window.
What is different from CV
Some CV cockpit primitives apply directly: infra checks, queue mix, quota cost, event tape. Reuse them. Diverge where the product shape differs:
| Aspect | CareerVector Ops | JobCache Ops |
|---|---|---|
| Primary entity | Workspace | Source × Time period |
| Primary signal | Workspace operational state (green/yellow/red) | Corpus growth rate per source |
| Privacy model | Per-workspace tier consent | Mostly internal; source-specific volume is contractual concern |
| Cost concern | LLM tokens per workspace job | Scrape requests per posting, LLM tokens per posting |
| Failure pattern | A workspace gets stuck | A source goes silent or its schema changes |
| Drill-down | Workspace → request → op | Source → recent runs → individual ad |
JobCache does not have "workspaces" in the CV sense. It has sources, runs, postings, roles, and matches. The cockpit reflects that.
Canonical tab placement
Anomalies
source silence
schema drift
trust conflict loops
quota danger
device abuse
Events
ingestion tape
scrape/run events
observation events
lease/device events
operator journals
Infra
API health
CockroachDB
online S3/B2 snapshots
device fleet
provider quotas
cost per ad
freshness and corpus pulse
Inspector
source dossier
ad/role dossier
device dossier
recent runs and samples
linked QA/Ops/Wiki context
Sysadmin
pause/resume source
rerun failed scrape
reset drift
pin note
quarantine device/ad
The older one-page sketch with header panels, source matrix, ingestion tape, and audit log is retained below as a panel inventory. Implementation should mount those panels inside the canonical tabs and keep the shared Ops header/chrome.
Infra panels
Infra check rollup
Source: JobCache Ops API health and runtime/device observations. There is no canonical central heavy worker in the target architecture; scraping and embedding work run on leased devices or explicitly configured workers of last resort. The Ops surface should summarize that fleet rather than assume one server worker.
| Check | Notes |
|---|---|
| JobCache API | Worker liveness and route health. |
| CockroachDB | Test query (SELECT 1) round-trip. Latency exposed in Ops, not Status. |
| Online S3/B2 snapshots | HEAD on a sentinel key. |
| Device fleet | Active devices, idle capacity, leases, recent failures. |
| Browserbase quota | API call to check remaining session minutes when configured. |
| Firecrawl quota | API call to check remaining credits when configured. |
| Adapter/device classes | Periodic synthetic probe per adapter class where useful. |
Each row: name, state, latency, last-checked timestamp.
Status projection: API, CockroachDB, online S3/B2, and aggregate device fleet state only. No per-provider quota, per-source volume, or latency detail.
Corpus panel
Source: JobCache API/read-model counters and daily snapshots. Runtime counters from devices may back this, but the UI should read normalized API shapes rather than Prometheus text or a specific worker.
| Metric | Source | Notes |
|---|---|---|
| Postings (all-time) | jobcache_postings_inserted_total + jobcache_postings_duplicates_total |
Already computed. |
| Inserted today | Same minus the value at midnight UTC | Requires daily snapshot in D1 or AE. |
| Dedupe ratio | duplicates / (inserted + duplicates) over recent window | Computed from normalized API/runtime counter deltas. |
Status projection: all of these. "N postings, N today, dedupe X%" is the public headline ("How many companies are hiring through us?").
Freshness panel
Source: query CockroachDB for MAX(scraped_at) and
PERCENTILE_CONT(0.95)(now() - scraped_at) over visible ads.
| Metric | Notes |
|---|---|
| Median freshness | Half the corpus is fresher than this |
| p95 freshness | Tail: corpus that is staling |
| Stale share | % of corpus where scraped_at > N days (configurable, default 7) |
The freshness panel is the corpus's pulse. A stale corpus is a dying business.
Status projection: median + a one-word adjective ("fresh", "aging", "stale"). p95 and stale share are operator-internal.
Cost / ad panel
Source: cost ledger queries (we keep counters per provider call; need to add a per-ad cost rollup if not present).
| Metric | Notes |
|---|---|
| Scrape $/ad | Average over last 24h of inserted postings |
| LLM extract $/ad | Same |
| Total $/ad | Sum, for the headline |
Cost per ad inserted is the unit economic. If it crosses a threshold the operator should know immediately.
Status projection: none. Cost is operator-and-business-only.
Source matrix
The cockpit's centerpiece. One row per known source, sortable by freshness or activity.
Columns:
| Column | Source | Notes |
|---|---|---|
| State | Computed (see below) | green / yellow / red traffic light |
| Source | Source identifier | e.g. jobs.ch, linkedin (browserbase) |
| Last run | timestamp of most recent scrape run | Relative time |
| +Today | Inserted postings today | The growth signal |
| Error rate | Failed runs / total runs over 24h | Percentage |
| Schema status | OK / DRIFTED / UNKNOWN | Detected when extraction success rate falls off a cliff |
| Note | Operator-pinned note if any | Free text, optional |
Source operational state
| State | Trigger |
|---|---|
| red | Schema drift detected (extraction success < 30% over last hour), OR error rate > 50% over last hour, OR last successful run > 24h ago AND historically the source runs at least daily. |
| yellow | Error rate > 10%, OR no inserts in 24h despite runs succeeding (= source went quiet), OR Browserbase/Firecrawl quota for this source's scraper class > 80% used. |
| green | Otherwise. |
The state rules are aggressive on detection: a source going from producing 300 ads/day to 0 is more alarming than a stuck workspace. For JobCache, silence is failure.
Filters
- State — show only red/yellow sources during incidents.
- Scraper class — direct, browserbase, firecrawl, etc.
- Source category — direct-jobs-board, aggregator, ATS.
Source detail drill-down
Click a source → side panel.
- Recent runs: time, duration, postings inserted/deduped/skipped, errors, scraper class used.
- Schema-drift detail: which extractor field failed, on which postings, what the LLM returned (since this is internal scraping, no PII concerns), what the canonical schema expects.
- Cost breakdown for this source over the last 24h.
- Output examples: a sample of postings inserted in the last hour (deep-link to jobcache web view).
- Recovery actions: re-run last failed scrape, pause this source, reset schema-drift detection, pin operator note.
Every recovery action writes a row to a new operator_actions audit
log (operator id from session, action, target, timestamp, optional
note). The audit log is admin-only and rendered as the "Audit log"
cross-cut tab in the cockpit.
Audit log schema
Table: operator_actions (CockroachDB, append-only). See the migration
at jobcache/migrations/add-operator-actions.sql
for the canonical SQL — column list, types, and indexes are intentionally
not duplicated here so the migration file remains the single source of truth.
The schema mirrors CV's operator_actions (careervector-cockpit.md →
Audit log schema) with two
JobCache-specific differences:
- CockroachDB-flavored SQL:
STRINGinstead ofTEXT,INT8forts_ms,JSONBforresult_details(CRDB has native JSONB; D1 does not). target_typeenum reflects JobCache's primary entities:'source','scrape_run','posting','system'(vs CV's'workspace','process_request','system').
Canonical action enum. The JobCache cockpit emits exactly these
action values (extend by adding rows here and updating the writer in
the JobCache api.ops worker; the column itself is unconstrained STRING
so the set can grow without a schema bump):
action |
target_type |
Meaning |
|---|---|---|
rerun_scrape |
scrape_run |
Re-execute the last failed scrape run for a source. |
pause_source |
source |
Soft-disable a source so the scheduler skips it until cleared. |
resume_source |
source |
Clear the pause flag set by pause_source. |
reset_schema_drift |
source |
Clear the drift flag after the operator has investigated and decided the extractor is healthy. |
pin_source_note |
source |
Attach or replace the operator note shown in the source matrix. |
state_badge_override |
source |
Manual operator pin to yellow or red on a source row; auto-expires after 4h. |
Indexes cover the three common operator queries: recency
(idx_operator_actions_ts), per-target history
(idx_operator_actions_target), and per-action filter
(idx_operator_actions_action).
Apply the migration (against the JobCache CRDB cluster — see
secrets/cockroach.md for the connection URL):
cockroach sql --url "$JOBCACHE_DATABASE_URL" --file jobcache/migrations/add-operator-actions.sql
Or paste-and-execute via the CockroachDB Cloud console SQL shell.
Retention. Append-only by design. There is no UPDATE or DELETE
path from the cockpit, and no automatic purge job. If log volume becomes
a concern, add a separate retention job — out of scope for this table.
Ingestion tape — live
Source: same SSE Fanout pattern as CV (architecture B in
architectures.md), pointed at JobCache's queue tables. The schema
differs:
| Event | Renders as |
|---|---|
| Scrape run started | {source} scrape {scraper_class} started |
| Scrape run completed | {source} inserted {n} (deduped {m}) |
| Scrape run failed | {source} scrape {status_code}, retry {k}/{N} |
| Extraction failed | {source} extract failed: {category} |
| Role clustering completed | {source} extracted {n}, role-clustered {n}, conf {x.xx} avg |
| Schema drift detected | {source} schema DRIFT — extractor field {f} failing |
Same operator pulse as the CV tape, different vocabulary.
Status projection: an aggregate-only public version. "Postings ingested per hour" sparkline; no source attribution.
Cross-cuts (top-bar "more")
- Role-clustering quality — confidence distribution of recent ad → role assignments. Manual override rate (when corpus consumers reject a clustering).
- Embedding cost — token spend on embedding generation per day, by model.
- Workspace consumption — which CV workspaces pulled which ads in the last 24h. Anonymized by default (workspace-id hashed); unhashed only if the workspace is yellow/red tier. Pull-through rate: ads that became workspace jobs / ads pulled.
- Source coverage gap — known role families × known geographies, with a heat map of how many ads we have. The negative space is where the corpus is weak.
- Audit log — operator_actions for JobCache.
What status (public) gets
status.jobcache.corbet.ch continues at 60s ISR. The page is
already further along than CV's because readJobcacheHealth already
returns rich Health snapshots; status just renders them.
| Section | Source | Notes |
|---|---|---|
| Headline traffic light | rollupHealthState(snapshot) |
One color. |
| Corpus headline | Inserted total + today | "N postings indexed, N added today." |
| Freshness | Median + adjective | "Most postings are 4h fresh. Working well." |
| Infrastructure checks | snapshot.checks public projection |
Already correct in shape. |
| Source health summary | "M sources reporting, N degraded" | Aggregate. Never names a source publicly unless degraded. |
| Last snapshot freshness | Already shown | Same as CV status. |
What status drops or never adds:
- Per-source volume (contractual concern; some sources do not want their volume public).
- Cost data.
- Workspace consumption patterns.
Architecture notes specific to JobCache
- CockroachDB is the canonical store for shared ad/role state,
unlike CV where D1 is canonical. Most cockpit queries hit Cockroach,
not D1. The architecture options in
architectures.mdapply; substitute CockroachDB queries where D1 is mentioned. CRDB query latency is higher than D1, so caching the snapshot more aggressively at the edge (60s on status, 30s on ops) is fine. - Normalized Ops API shapes are the product boundary. Prometheus-style counters may still be a useful backing source for runtime/device metrics, but UI and MCP read normalized API payloads. The cockpit should not depend on Prometheus text, a specific worker, or direct vendor responses.
- Device/runtime fleet replaces the central heavy-worker assumption. Scraping, chunking, and embedding work run on leased devices or explicit workers of last resort. Ops probes the fleet, leases, and capacity rather than one named background worker.
- Browserbase / Firecrawl quotas are external rate ceilings that matter as much as CF quota. Surface them in the infra panel.
What this leaves to follow-up work
- Per-source freshness SLA — once we have business stakeholders, some sources will have contractual freshness commitments. The cockpit will need per-source SLA tracking with breach alerts.
- Source-level cost vs. value — when commercial pricing exists, cost per ad becomes value per ad (ads consumed × consumer pricing). The cost panel evolves into a margin panel.
- Embedding/match diagnostics — quality of matches in workspace pulls (precision/recall vs. operator-labeled ground truth). Out of scope for the first cut; will become important once matching is a tunable business surface.
- Public corpus browser on status — a paginated, anonymized, rate-limited view of recent insertions. Useful for partners. Not in v1.