Phases 2 and 3 of the Hungarian tender-qualification feature are built on master (NOT yet pushed). Phase 2 adds an editable company_profile singleton + Settings form; Phase 3 adds on-demand LLM eligibility assessment (tender-qualify edge fn + queue/verdict UI), cloning the CRM /disclosure on-demand-LLM pattern. Continues the 2026-06-09 refocus; for source/schema context see implementation-guide and the TOPICS index.

State: on master, NOT pushed

Both phases are committed to local master and code-reviewed clean (web tests + build green), but not pushed. Two external/operator blockers (below) keep the feature from being live end-to-end.

For Agents

Phase 3 mirrors the /disclosure analyzer: <feature>_request queue table + one <feature> verdict per entity (upsert), an edge fn that claims-races the newest pending request, calls Claude with structured output, and writes the verdict back. Frontend = request hook + polling-status hook + verdict panel. Same ANTHROPIC_API_KEY secret as the disclosure analyzer (_shared/llm.ts).

Phase 2 — Company Profile (DONE, master, not pushed)

Editable singleton holding Levandor’s capabilities/references/financials — the baseline the qualification assessment compares each tender’s eligibility criteria against.

  • Table company_profile: id integer pk default 1 check (id = 1) — enforces exactly one row. Migration 20260615093853_company_profile applied live to project mkofmdtdldxgmmolxxhc.
  • Settings UI: CompanyProfileForm in the Settings page.
  • Hooks: useCompanyProfile (read) + useUpdateCompanyProfile (upsert { id: 1, ... }).
  • i18n: en/hu strings added.

Gotcha: focus-refetch clobbered unsaved edits

The form initialized its local state from query data on every render, so a TanStack Query focus-refetch wiped the user’s in-progress edits. Fixed with a hydrate-once guard:

const [hydrated, setHydrated] = useState(false);
if (data && !hydrated) { setHydrated(true); setForm(data); }

Hydrate the form from server data exactly once; subsequent refetches no longer overwrite local edits.

Phase 3 — On-demand Qualification (DONE, master, not pushed)

On-demand LLM verdict on whether Levandor qualifies to bid, per tender. Mirrors the /disclosure on-demand-LLM pattern (queue + claim-race + structured output + verdict panel).

Tables (migration 20260615163827_tender_qualification, applied live)

  • tender_qualification_request — the queue (one row per requested assessment; pending running done/error).
  • tender_qualificationone verdict per tender (upsert on tender_id).
  • RLS: authenticated SELECT + INSERT; service_role ALL.

tender-qualify edge function (DEPLOYED)

Pipeline:

  1. verifyAuth — accept a real user bearer token OR a shared secret (TENDER_QUALIFY_SECRET).
  2. Newest-pending lookup — pick the most recent pending request.
  3. Claim-race — conditional update to running so concurrent invocations don’t double-process.
  4. Load the tender + the company_profile singleton.
  5. Empty-criteria short-circuit — if the tender has no eligibility criteria, write verdict unknown without calling the LLM.
  6. Else call Claude claude-opus-4-8 with thinking: { type: 'adaptive' } and structured output via output_config.format.json_schema.
  7. Upsert the verdict, flip the request to done. On any throw request error.

Structured-output schema constraints (raw fetch 400s)

VERDICT_SCHEMA must avoid minimum / maximum / minLength / pattern and any recursion. Including those keywords makes the raw fetch to the Anthropic API return 400. Keep the JSON schema to plain types/enums/required.

(adaptive thinking)

With thinking: { type: 'adaptive' }, the API puts the thinking block first in content[]. Reading content[0] yields the thinking block, not the answer. Find the block where type === 'text' and parse that:

const text = resp.content.find((b) => b.type === 'text')?.text;

Frontend

  • lib/tender-qualification.ts — parsers + tone helper + i18n-key helpers (verdict label/tone/key).
  • Hooks:
    • useRequestTenderQualification — enqueue a request.
    • useTenderQualificationRequestpolls every 8s while the request is pending/running, with a 15-minute bound; invalidates the verdict query key on done.
    • useTenderQualification / useTenderQualifications / ...List — read verdict(s).
  • Components: TenderQualifyButton + QualificationVerdictPanel + an eligibility-prose section on TenderOverviewTab.
  • i18n: en/hu; labels rendered via t() so Hungarian resolves correctly.
  • Quality: code-reviewed clean; web tests + build green.

External blockers (operator action required)

Blocker 1 — ANTHROPIC_API_KEY not set in Supabase secrets

Without it, every real-tender LLM assessment lands status = 'error' with "ANTHROPIC_API_KEY not set". Same secret backs _shared/llm.ts (the disclosure analyzer).

supabase secrets set ANTHROPIC_API_KEY=sk-ant-... --project-ref mkofmdtdldxgmmolxxhc

Blocker 2 — web SPA deploy parked

Phase 1+2+3 frontend is not live — the Cloudflare Pages deploy is parked on wrangler login / CLOUDFLARE_API_TOKEN.

Self-verification gaps

  • Edge fn not self-verifiable end-to-end: verifyAuth needs a real user bearer or TENDER_QUALIFY_SECRET; live invoke is browser-driven.
  • Empty-criteria short-circuit untestable against real data: all 101 live tenders have eligibility_criteria (0 null-criteria), so the unknown short-circuit path can’t be exercised against production data right now.