Commit Graph

2 Commits (8dcd262dc469d39fe4c3078a4890781df32406bc)

Author SHA1 Message Date
Talha Ahmed 921d3bf383 Inbox: label CVs the pipeline could not read, on the row itself
A CV goes through four steps before it is usable: the intake classifier
decides it is an application, the PDF text is extracted, an agent matches it
against job posts, and only then is an ATS score written. A PDF with no
readable text stops at step two. It is never matched and never scored.

The list said nothing about any of this. The parse badge existed only in the
detail pane, so a dead row looked exactly like a good one until you clicked
it. Across eleven hundred applications that is not discoverable, and the
candidate whose file happened not to open is simply lost.

The row now carries the badge, with two deliberate limits:

  - Exceptions only. Parsing, Failed and Pending show; Parsed shows nothing.
    A green chip on the overwhelming majority of rows is decoration that
    buries the two states worth spotting, and no badge already reads as fine.
    The detail pane still shows every state, Parsed included, because there
    the row is the whole subject.
  - Email only. Sheet Form applicants have no mailbox attachment to parse.
    The guard is redundant against today's mapFormRow, which sets no
    resumeStatus at all, and is kept as a tripwire rather than a load-bearing
    check — the comment says so rather than overclaiming.

"Failed" on its own tells a recruiter nothing to do next, so each state got a
plain-language tooltip saying what happened and that the attachment is still
there to open by hand. That needed Badge to forward `title`, which it did not.

The colour rule was duplicated inline in the detail pane; it is now one
helper both halves call, so the same state cannot paint two colours.

Four assertions added to inbox-loading.test.mjs. Three are proven sensitive
by reverting the change: removing the badge fails the label and tooltip
checks, and badging every state fails the exceptions-only check. The fourth,
that form rows stay unlabelled, passes even without the guard and is marked
in the test as a tripwire, not proof.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 17:48:28 +05:00
Talha Ahmed db0652ab24 Inbox: serve the queue from cache instead of re-skeletoning every visit
Opening the Recruitment Inbox showed skeleton placeholders every time, and
on the combined All channel it showed them stacked ABOVE rows that had
already arrived — the channel ORs its two sources' pending flags together,
so the email rows sat under placeholders while the sheet fetch finished.

The queue is a local table, not a live feed. Rows only appear when the Sync
worker writes them, and that path already invalidates qk.mailbox.all(). So
refetching on every visit bought nothing and cost a full-width skeleton each
time — worse on the All channel, which fetches BOTH sources unpaged and so
re-downloaded thousands of rows to render ten.

  - LIST_CACHE / COUNT_CACHE: 10 min staleTime, 1 h gcTime, keepPreviousData.
    A revisit inside the window paints rows immediately and issues no request.
  - Placeholders now mean "nothing to show yet": gated on the list being
    empty. A refetch over live rows is a 2px bar and the word "Updating…",
    with the rows readable and clickable throughout.
  - "Updated 4 min ago" + a Refresh button under the search box, because a
    cached list that never admits its age reads as a broken list. Refresh
    re-reads the DB; Sync pulls new mail from Outlook. Both tooltips now say
    which is which — two circular arrows were indistinguishable.
  - Search debounced to 300ms. Each keystroke used to mint a query key, a
    request and a skeleton, so the list strobed while you typed.
  - resume_text dropped from the list row mapping. serialize_application
    ships the whole extracted CV on every row and the queue renders none of
    it; caching a thousand of them for an hour is not a trade worth making.
    The detail query fetches it for the one row actually open.

Trade-off: a sync run started elsewhere is now invisible here for up to ten
minutes. The age label and the Refresh button are the deliberate mitigation.

inbox-loading.test.mjs pins all of it, driving hand-gated fetches so the
frames a fast API flashes past become assertable: both sources pending, one
source home, and a second visit served from cache. Reverting the placeholder
rule reproduces the original defect as "skeleton=true rows=2". Wired into
npm run verify; needed a mountRoute export on the smoke harness, since
renderRoute tears the tree down before those intermediate frames can be read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:39:55 +05:00