/* ============================================================
   NeuroRoute — Admin (SISL): Customers, Onboard,
   Provider Keys, Pricing, Model Preferences, Health, Audit Log
   All wired to live APIs.
   ============================================================ */

var eventColor=function(e){
  if(!e) return 'gray';
  if(e.startsWith('auth'))return 'blue';
  if(e.startsWith('key'))return e.includes('revok')?'red':'green';
  if(e.startsWith('config')||e.startsWith('pricing'))return 'amber';
  if(e.startsWith('provider'))return 'purple';
  if(e.startsWith('user'))return 'cyan';
  if(e.startsWith('data_retention')||e.startsWith('cmek'))return 'amber';
  return 'gray';
};

/* fmtDetailVal renders a single audit-detail value for display: {old,new}
   pairs (see gateway's auditChange type) get a dedicated diff renderer in
   AuditRow below; everything else falls through here. */
function fmtDetailVal(v){
  if(v===null||v===undefined||v==='') return '(empty)';
  if(typeof v==='object') return JSON.stringify(v);
  return String(v);
}

/* AuditRow — one audit_events row with an expandable detail/diff panel.
   `detail` was already being fetched from GET /v1/admin/audit-log but never
   rendered beyond `detail.name` — most fields (including before/after diffs
   for sensitive settings like data retention or CMEK) were silently
   discarded. Split into its own component (not an inline map callback) so
   each row can hold its own expand/collapse state. */
function AuditRow(props){
  var a=props.a;
  var _open=useState(false); var open=_open[0], setOpen=_open[1];
  var detail=a.detail||{};
  var keys=Object.keys(detail).filter(function(k){return k!=='actor_email';});
  var hasDetail=keys.length>0;
  return (
    <React.Fragment>
      <tr>
        <td className="num faint" style={{whiteSpace:'nowrap'}}>{timeAgo(a.created_at)}</td>
        <td className="faint" style={{maxWidth:160,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}} title={a.org_name||a.org_id||''}>{a.org_name||(a.org_id?a.org_id.slice(0,8)+'…':'—')}</td>
        <td style={{color:'var(--tx-0)'}}>
          {(a.detail && a.detail.name) ? a.detail.name : (a.actor_email||'system')}
          {(a.detail && a.detail.name && a.actor_email) && <div className="faint" style={{fontSize:11}}>{a.actor_email}</div>}
        </td>
        <td>
          <span className="row" style={{gap:6,alignItems:'center'}}>
            <Badge color={eventColor(a.event_type)}>{a.event_type}</Badge>
            {hasDetail && <button type="button" className="btn btn-ghost btn-sm" style={{padding:'1px 5px',height:'auto'}} onClick={function(){setOpen(!open);}} title="View details">
              <Icon name={open?'chevdown':'chevron'} size={11}/>
            </button>}
          </span>
        </td>
        <td className="num faint">{a.resource_type?(a.resource_type+(a.resource_id?': '+a.resource_id:'')):''}</td>
        <td className="num faint">{a.actor_ip||'—'}</td>
      </tr>
      {open && hasDetail && <tr>
        <td colSpan={6} style={{background:'var(--bg-inset)',padding:'8px 14px',fontSize:12}}>
          <div className="grid" style={{gap:4}}>
            {keys.map(function(k){
              var v=detail[k];
              var isDiff = v && typeof v==='object' && !Array.isArray(v) && ('old' in v || 'new' in v);
              return <div key={k} className="row" style={{gap:8,alignItems:'baseline'}}>
                <span className="faint mono" style={{minWidth:180}}>{k}</span>
                {isDiff ? (
                  <span>
                    <span style={{color:'var(--red)',textDecoration:'line-through',opacity:.75}}>{fmtDetailVal(v.old)}</span>
                    {' → '}
                    <span style={{color:'var(--green)',fontWeight:600}}>{fmtDetailVal(v.new)}</span>
                  </span>
                ) : <span className="mono">{fmtDetailVal(v)}</span>}
              </div>;
            })}
          </div>
        </td>
      </tr>}
    </React.Fragment>
  );
}

/* ----- Guardrails per-rule config editor (Customers -> Edit) -----
   GUARDRAIL_RULE_DEFS / parseGuardrailRows / buildGuardrailsConfig now live
   in ui.jsx (shared with portal-team.jsx's Settings guardrails section —
   see the ALERT_RULE_TYPES pattern there), since the two copies here and in
   portal-team.jsx had drifted into a byte-for-byte duplicate that would only
   drift again the next time a rule was added to one file and not the other. */

/* ----- Admin: manage an existing customer org's team (Customers -> Team) -----
   The admin-console equivalent of that org's own Team page. Before this, a
   SISL admin had no way to view or invite users into an org that already
   exists — "Onboard customer" only creates a BRAND NEW org. */
function AdminOrgTeamPanel(props){
  var orgId=props.orgId;
  var t=useToast();
  var _loading=useState(true); var loading=_loading[0], setLoading=_loading[1];
  var _team=useState([]); var team=_team[0], setTeam=_team[1];
  var _invites=useState([]); var invites=_invites[0], setInvites=_invites[1];
  var _email=useState(''); var email=_email[0], setEmail=_email[1];
  var _role=useState('org_member'); var role=_role[0], setRole=_role[1];
  var _sending=useState(false); var sending=_sending[0], setSending=_sending[1];

  var rc=function(r){return ({admin:'magenta',org_admin:'magenta',sisl_admin:'magenta',developer:'blue',org_member:'blue',viewer:'gray',org_viewer:'gray',org_billing:'amber'})[r]||'gray';};
  var roleName=function(r){return ({admin:'Admin',org_admin:'Admin',sisl_admin:'Sisl Admin',developer:'Developer',org_member:'Member',viewer:'Viewer',org_viewer:'Viewer',org_billing:'Billing'})[r]||r;};

  var load=function(){
    Promise.all([NR_API.adminOrgUsers(orgId), NR_API.adminOrgInvitations(orgId)]).then(function(results){
      setTeam((results[0]&&results[0].users)||[]);
      setInvites((results[1]&&results[1].invitations)||[]);
      setLoading(false);
    }).catch(function(){ setLoading(false); });
  };
  useEffect(function(){ load(); },[orgId]);

  var invite=function(){
    if(!email) return;
    setSending(true);
    NR_API.adminInvite(orgId,email,role).then(function(res){
      setSending(false);
      if(res && res.id){ setInvites(function(iv){return [res].concat(iv);}); setEmail(''); setRole('org_member'); t({title:'Invitation sent',msg:email}); }
      else t({kind:'error',title:'Failed to send invite'});
    }).catch(function(){ setSending(false); t({kind:'error',title:'Failed to send invite'}); });
  };
  var changeRole=function(uid,name,newRole){
    NR_API.updateRole(uid,newRole).then(function(res){
      if(res){ setTeam(function(tm){return tm.map(function(x){return (x.id||x.uid)===uid?Object.assign({},x,{role:newRole}):x;});}); t({title:'Role updated',msg:name+' is now '+roleName(newRole)}); }
      else t({kind:'error',title:'Failed to update role'});
    }).catch(function(){ t({kind:'error',title:'Failed to update role'}); });
  };
  var removeUser=function(uid,name){
    NR_API.removeUser(uid).then(function(ok){
      if(ok){
        setTeam(function(tm){return tm.filter(function(x){return (x.id||x.uid)!==uid;});});
        t({kind:'info',title:'Member removed',msg:name});
      } else {
        t({kind:'error',title:'Failed to remove member'});
      }
    }).catch(function(){ t({kind:'error',title:'Failed to remove member'}); });
  };
  var cancelInvite=function(id,em){
    NR_API.adminCancelInvite(orgId,id).then(function(ok){
      if(ok){ setInvites(function(iv){return iv.filter(function(y){return y.id!==id;});}); t({kind:'info',title:'Invite cancelled',msg:em}); }
      else t({kind:'error',title:'Failed to cancel invite'});
    }).catch(function(){ t({kind:'error',title:'Failed to cancel invite'}); });
  };
  var resendInvite=function(id,em){
    NR_API.adminResendInvite(orgId,id).then(function(res){
      if(res) t({title:'Invitation resent',msg:em});
      else t({kind:'error',title:'Failed to resend invite'});
    }).catch(function(){ t({kind:'error',title:'Failed to resend invite'}); });
  };

  if(loading) return <LoadingPage/>;

  return (
    <div className="grid" style={{gap:16}}>
      <SectionCard title="Invite a new user" sub="Use this to add or replace this organization's admin.">
        <div className="row" style={{gap:10,flexWrap:'wrap'}}>
          <input className="input" style={{flex:'1 1 220px'}} type="email" placeholder="person@customer.com" value={email} onChange={function(e){setEmail(e.target.value);}}/>
          <select className="select" style={{width:150}} value={role} onChange={function(e){setRole(e.target.value);}}>
            <option value="org_admin">Admin</option>
            <option value="org_member">Member</option>
            <option value="org_viewer">Viewer</option>
            <option value="org_billing">Billing</option>
          </select>
          <button className="btn btn-primary btn-sm" disabled={!email||sending} onClick={invite}><Icon name="send" size={14}/>{sending?'Sending…':'Send invitation'}</button>
        </div>
      </SectionCard>

      <SectionCard title="Members" sub={team.length+' members'} pad={false}>
        {team.length>0 ? <table className="table">
          <thead><tr><th>Member</th><th>Email</th><th>Role</th><th style={{textAlign:'right'}}>Actions</th></tr></thead>
          <tbody>{team.map(function(m){
            var name=m.name||m.email||'User'; var uid=m.id||m.uid;
            return (
              <tr key={uid||m.email}>
                <td><b style={{color:'var(--tx-0)'}}>{name}</b></td>
                <td className="faint">{m.email}</td>
                <td>
                  <select className="select" style={{minWidth:110,fontSize:12,padding:'4px 8px'}} value={m.role||'org_member'}
                    onChange={function(e){changeRole(uid,name,e.target.value);}}>
                    <option value="org_admin">Admin</option>
                    <option value="org_member">Member</option>
                    <option value="org_viewer">Viewer</option>
                    <option value="org_billing">Billing</option>
                  </select>
                </td>
                <td style={{textAlign:'right'}}><button className="iconbtn" onClick={function(){removeUser(uid,name);}} title="Remove member"><Icon name="trash" size={14}/></button></td>
              </tr>
            );
          })}</tbody>
        </table> : <div className="empty" style={{padding:24}}><p className="faint">No members yet — invite one above.</p></div>}
      </SectionCard>

      <SectionCard title="Pending invitations" sub={invites.length+' pending'} pad={false}>
        {invites.length? <table className="table">
          <thead><tr><th>Email</th><th>Role</th><th>Sent</th><th style={{textAlign:'right'}}></th></tr></thead>
          <tbody>{invites.map(function(iv){return (
            <tr key={iv.id||iv.email}><td style={{color:'var(--tx-0)'}}>{iv.email}</td><td><Badge color={rc(iv.role)}>{roleName(iv.role)}</Badge></td><td className="faint">{timeAgo(iv.created_at)||iv.created_at}</td>
              <td style={{textAlign:'right'}}>
                <div className="row" style={{gap:6,justifyContent:'flex-end'}}>
                  <button className="btn btn-soft btn-sm" onClick={function(){resendInvite(iv.id,iv.email);}}>Resend</button>
                  <button className="btn btn-soft btn-sm" onClick={function(){cancelInvite(iv.id,iv.email);}}>Cancel</button>
                </div>
              </td></tr>
          );})}</tbody>
        </table> : <div className="empty" style={{padding:24}}><p className="faint">No pending invitations.</p></div>}
      </SectionCard>
    </div>
  );
}

/* =====================  CUSTOMERS  ===================== */
function CustomersPage(){
  var t=useToast();
  var _loading=useState(true); var loading=_loading[0], setLoading=_loading[1];
  var _customers=useState([]); var customers=_customers[0], setCustomers=_customers[1];
  // loadFailed distinguishes "the customers fetch itself failed" from "it
  // succeeded and there are genuinely zero customers" — before this, both
  // collapsed into the same "No customers yet" empty state.
  var _loadFailed=useState(false); var loadFailed=_loadFailed[0], setLoadFailed=_loadFailed[1];
  var _editing=useState(null); var editing=_editing[0], setEditing=_editing[1];
  var _editName=useState(''); var editName=_editName[0], setEditName=_editName[1];
  var _editBudget=useState(''); var editBudget=_editBudget[0], setEditBudget=_editBudget[1];
  var _editDaily=useState(''); var editDaily=_editDaily[0], setEditDaily=_editDaily[1];
  var _editRet=useState('retain'); var editRet=_editRet[0], setEditRet=_editRet[1];
  var _editRetDays=useState('30'); var editRetDays=_editRetDays[0], setEditRetDays=_editRetDays[1];
  var _editReqLogMode=useState('metadata'); var editReqLogMode=_editReqLogMode[0], setEditReqLogMode=_editReqLogMode[1];
  var _editSemCache=useState(false); var editSemCache=_editSemCache[0], setEditSemCache=_editSemCache[1];
  var _editComp=useState(false); var editComp=_editComp[0], setEditComp=_editComp[1];
  var _editCompAggr=useState(false); var editCompAggr=_editCompAggr[0], setEditCompAggr=_editCompAggr[1];
  var _editLegalHold=useState(false); var editLegalHold=_editLegalHold[0], setEditLegalHold=_editLegalHold[1];
  var _editML=useState(false); var editMLRouting=_editML[0], setEditMLRouting=_editML[1];
  var _editCO=useState(false); var editConciseOutput=_editCO[0], setEditConciseOutput=_editCO[1];
  var _editMPC=useState([]); var editMPC=_editMPC[0], setEditMPC=_editMPC[1];
  var _editMPCNew=useState(''); var editMPCNew=_editMPCNew[0], setEditMPCNew=_editMPCNew[1];
  var _fsMode=useState(''); var editFairShareMode=_fsMode[0], setEditFairShareMode=_fsMode[1];
  var _fsRPM=useState(''); var editFairRPM=_fsRPM[0], setEditFairRPM=_fsRPM[1];
  var _fsTPM=useState(''); var editFairTPM=_fsTPM[0], setEditFairTPM=_fsTPM[1];
  var _editTier=useState('enterprise'); var editTier=_editTier[0], setEditTier=_editTier[1];
  var _editOverrides=useState([]); var editOverrides=_editOverrides[0], setEditOverrides=_editOverrides[1];
  var _overridesLoading=useState(false); var overridesLoading=_overridesLoading[0], setOverridesLoading=_overridesLoading[1];
  // Feature catalog (key -> home tier) for the override editor, sourced live
  // from GET /v1/customer/entitlements' required_tiers — that field is
  // computed from entitlements.AllFeatures/RequiredTier and does not depend
  // on the calling org, so it doubles as a global feature+tier catalog. Not
  // org-specific data itself; loaded once, independent of which org is open.
  var _entReqTiers=useState(null); var entReqTiers=_entReqTiers[0], setEntReqTiers=_entReqTiers[1];
  var _editCMEK=useState(''); var editCMEK=_editCMEK[0], setEditCMEK=_editCMEK[1];
  var _showCmekHelp=useState(false); var showCmekHelp=_showCmekHelp[0], setShowCmekHelp=_showCmekHelp[1];
  var _viewLogsOrg=useState(null); var viewLogsOrg=_viewLogsOrg[0], setViewLogsOrg=_viewLogsOrg[1];
  var _teamOrg=useState(null); var teamOrg=_teamOrg[0], setTeamOrg=_teamOrg[1];
  var _editGuardrails=useState(false); var editGuardrails=_editGuardrails[0], setEditGuardrails=_editGuardrails[1];
  var _editGuardrailRows=useState(function(){return parseGuardrailRows('[]');}); var editGuardrailRows=_editGuardrailRows[0], setEditGuardrailRows=_editGuardrailRows[1];
  var _showGuardrailRules=useState(false); var showGuardrailRules=_showGuardrailRules[0], setShowGuardrailRules=_showGuardrailRules[1];
  var _erasure=useState(null); var erasure=_erasure[0], setErasure=_erasure[1];
  var _confirmErase=useState(null); var confirmErase=_confirmErase[0], setConfirmErase=_confirmErase[1]; // null | 'request' | 'immediate'
  var _confirmText=useState(''); var confirmText=_confirmText[0], setConfirmText=_confirmText[1];
  var _erasing=useState(false); var erasing=_erasing[0], setErasing=_erasing[1];
  var _saving=useState(false); var saving=_saving[0], setSaving=_saving[1];
  var _onboarding=useState(false); var onboarding=_onboarding[0], setOnboarding=_onboarding[1];
  var _newOrg=useState({org:'',email:'',name:''}); var newOrg=_newOrg[0], setNewOrg=_newOrg[1];
  var _creating=useState(false); var creating=_creating[0], setCreating=_creating[1];
  var _records=useState([]); var records=_records[0], setRecords=_records[1];
  var _finalizing=useState(false); var finalizing=_finalizing[0], setFinalizing=_finalizing[1];
  // Per-org commercial terms (P4). Hoisted to CustomersPage's top level — these
  // MUST be unconditional hooks (not declared inside the "editing &&" IIFE
  // below) or React throws "Rendered more hooks than during the previous
  // render" the moment the edit modal opens/closes, since the hook count would
  // then differ between renders.
  var _terms=useState(null); var terms=_terms[0], setTerms=_terms[1];
  // termsError distinguishes "the listPriceTerms fetch itself failed" from
  // "it succeeded and this org genuinely has zero terms rows" — before this,
  // both collapsed to the same terms===[] state (see PriceBookPage/A2APage's
  // identical issue, I-13).
  var _termsError=useState(false); var termsError=_termsError[0], setTermsError=_termsError[1];
  var _tForm=useState({discount_pct:'',base_fee_usd:'',in_rate_usd:'',out_rate_usd:'',effective_from:'',note:''});
  var tForm=_tForm[0], setTForm=_tForm[1];
  var _tSaving=useState(false); var tSaving=_tSaving[0], setTSaving=_tSaving[1];

  var thisMonth = function(){ var d=new Date(); return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0'); };

  var loadRecords=function(orgId){
    NR_API.billingRecords(orgId).then(function(res){
      if(res && Array.isArray(res.records)) setRecords(res.records); else setRecords([]);
    }).catch(function(){ setRecords([]); });
  };

  var finalizeBilling=function(){
    if(!editing) return;
    var orgId = editing.id||editing.org_id;
    setFinalizing(true);
    NR_API.finalizeBilling(orgId, thisMonth()).then(function(res){
      setFinalizing(false);
      if(res && res.status==='finalized'){
        t({title:'Billing finalized', msg:thisMonth()+' · '+(res.total_customer_charge!=null?('$'+Number(res.total_customer_charge).toFixed(2)):'')});
        loadRecords(orgId);
      } else {
        t({kind:'error', title:'Finalize failed', msg:(res&&res.error)||'Check server logs.'});
      }
    }).catch(function(){ setFinalizing(false); t({kind:'error', title:'Finalize failed'}); });
  };

  var loadCustomers=function(){
    setLoading(true); setLoadFailed(false);
    // NR_API.customers() is api()-backed: resolves null on any non-2xx
    // status or network failure, never rejects — detect failure via the
    // null result.
    NR_API.customers().then(function(res){
      if(res && Array.isArray(res.customers||res)) {
        setCustomers(res.customers||res);
      } else {
        setLoadFailed(true);
      }
      setLoading(false);
    }).catch(function(){ setLoadFailed(true); setLoading(false); });
  };

  useEffect(function(){ loadCustomers(); },[]);

  // Load the full feature -> home-tier catalog once. Reuses the customer
  // entitlements endpoint rather than a hardcoded per-tier list (see
  // internal/entitlements/plans.go, the single source of truth) or a new
  // backend endpoint — required_tiers already covers every declared Feature.
  useEffect(function(){
    NR_API.entitlements().then(function(res){ if(res && res.required_tiers) setEntReqTiers(res.required_tiers); }).catch(function(){});
  },[]);

  // Load (or reset) per-org commercial terms whenever the edit modal opens for
  // a different org, or closes. Refetching unconditionally on `editing`
  // change (rather than only when `terms` is empty) also fixes a latent bug
  // where switching from editing org A to org B would keep showing org A's
  // terms until some other state change happened to trigger a refetch.
  var loadTerms=function(orgId){
    setTermsError(false);
    // listPriceTerms is api()-backed: resolves null on any non-2xx status or
    // network failure, never rejects — detect failure via the null result.
    NR_API.listPriceTerms(orgId).then(function(r){
      if(r===null){ setTermsError(true); } else { setTerms(r.rows||[]); }
    });
  };
  useEffect(function(){
    var orgId = editing && (editing.id||editing.org_id);
    if(!orgId){ setTerms(null); setTermsError(false); return; }
    setTerms(null);
    setTForm({discount_pct:'',base_fee_usd:'',in_rate_usd:'',out_rate_usd:'',effective_from:'',note:''});
    loadTerms(orgId);
  },[editing]);

  var openEdit=function(c){
    setEditing(c);
    setEditName(c.name||c.org_name||c.org||'');
    setEditBudget(c.monthly_budget_usd!=null ? String(c.monthly_budget_usd) : '');
    setEditDaily(c.daily_budget_usd!=null ? String(c.daily_budget_usd) : '');
    setEditRet(c.retention_mode||'retain');
    setEditRetDays(c.content_retention_days!=null?String(c.content_retention_days):'30');
    setEditReqLogMode(c.request_logging_mode||'metadata');
    setEditSemCache(!!c.semantic_cache_enabled);
    setEditComp(!!c.compression_enabled);
    setEditCompAggr(!!c.compression_aggressive);
    setEditLegalHold(!!c.legal_hold);
    setEditMLRouting(!!c.ml_routing_enabled);
    setEditConciseOutput(!!c.concise_output);
    setEditMPC(Array.isArray(c.model_priority_chain)?c.model_priority_chain:[]);
    setEditMPCNew('');
    setEditFairShareMode(c.fair_share_mode||'');
    setEditFairRPM(c.fair_share_rpm_pool!=null && c.fair_share_rpm_pool>0 ? String(c.fair_share_rpm_pool) : '');
    setEditFairTPM(c.fair_share_tpm_pool!=null && c.fair_share_tpm_pool>0 ? String(c.fair_share_tpm_pool) : '');
    setEditGuardrails(!!c.guardrails_enabled);
    setEditGuardrailRows(parseGuardrailRows(c.guardrails_config||'[]'));
    setShowGuardrailRules(false);
    setEditTier(c.tier||'enterprise');
    setEditOverrides([]); setOverridesLoading(true);
    NR_API.orgOverrides(c.id||c.org_id).then(function(r){ setEditOverrides(r.overrides||[]); }).catch(function(){}).finally(function(){ setOverridesLoading(false); });
    setEditCMEK(c.cmek_key_name||'');
    setShowCmekHelp(false);
    setErasure(null); setConfirmErase(null); setConfirmText('');
    NR_API.getErasure(c.id||c.org_id).then(function(e){ setErasure(e); });
    setRecords([]);
    loadRecords(c.id||c.org_id);
  };

  var saveEdit=function(){
    if(!editing || !editName.trim()) return;
    var orgId = editing.id||editing.org_id;
    setSaving(true);
    var payload = {name:editName.trim()};
    // Empty budget field → clear cap (0 = unlimited). A number → set the cap.
    payload.monthly_budget_usd = editBudget.trim()==='' ? 0 : parseFloat(editBudget) || 0;
    payload.daily_budget_usd = editDaily.trim()==='' ? 0 : parseFloat(editDaily) || 0;
    payload.retention_mode = editRet;
    if(editRet==='retain'){ var rd=parseInt(editRetDays,10); payload.content_retention_days = (rd>=1&&rd<=365)?rd:30; }
    payload.request_logging_mode = editRet==='retain' ? editReqLogMode : 'metadata';
    payload.semantic_cache_enabled = editSemCache;
    payload.compression_enabled = editComp;
    payload.compression_aggressive = editComp && editCompAggr;
    payload.legal_hold = editLegalHold;
    payload.ml_routing_enabled = editMLRouting;
    payload.concise_output = editConciseOutput;
    payload.model_priority_chain = editMPC;
    payload.fair_share_mode = editFairShareMode;
    payload.fair_share_rpm_pool = editFairRPM===''?0:parseInt(editFairRPM,10);
    payload.fair_share_tpm_pool = editFairTPM===''?0:parseInt(editFairTPM,10);
    payload.guardrails_enabled = editGuardrails;
    payload.guardrails_config = buildGuardrailsConfig(editGuardrailRows);
    payload.cmek_key_name = editCMEK.trim();
    Promise.all([
      NR_API.updateOrg(orgId, payload),
      NR_API.setOrgTier(orgId, editTier),
    ]).then(function(results){
      setSaving(false);
      var res = results[0], tierRes = results[1];
      if(res && tierRes){
        setCustomers(function(list){
          return list.map(function(c){
            if((c.id||c.org_id)===orgId) return Object.assign({}, c, {name:editName.trim(), monthly_budget_usd:payload.monthly_budget_usd||null, daily_budget_usd:payload.daily_budget_usd||null, retention_mode:editRet, content_retention_days:payload.content_retention_days||null, semantic_cache_enabled:editSemCache, compression_enabled:editComp, compression_aggressive:payload.compression_aggressive, legal_hold:editLegalHold, ml_routing_enabled:editMLRouting, guardrails_enabled:editGuardrails, guardrails_config:JSON.stringify(payload.guardrails_config), request_logging_mode:payload.request_logging_mode, cmek_key_name:payload.cmek_key_name, fair_share_mode:payload.fair_share_mode, fair_share_rpm_pool:payload.fair_share_rpm_pool, fair_share_tpm_pool:payload.fair_share_tpm_pool, tier:editTier});
            if((c.id||c.org_id)===orgId) return Object.assign({}, c, {name:editName.trim(), monthly_budget_usd:payload.monthly_budget_usd||null, daily_budget_usd:payload.daily_budget_usd||null, retention_mode:editRet, content_retention_days:payload.content_retention_days||null, semantic_cache_enabled:editSemCache, compression_enabled:editComp, compression_aggressive:payload.compression_aggressive, legal_hold:editLegalHold, ml_routing_enabled:editMLRouting, concise_output:editConciseOutput, model_priority_chain:editMPC, guardrails_enabled:editGuardrails, guardrails_config:JSON.stringify(payload.guardrails_config), request_logging_mode:payload.request_logging_mode, cmek_key_name:payload.cmek_key_name, fair_share_mode:payload.fair_share_mode, fair_share_rpm_pool:payload.fair_share_rpm_pool, fair_share_tpm_pool:payload.fair_share_tpm_pool, tier:editTier});
            return c;
          });
        });
        setEditing(null);
        t({title:'Organization updated', msg:editName+' · '+editTier});
      } else {
        t({kind:'error', title:'Failed to update', msg:'Check server logs.'});
      }
    }).catch(function(){ setSaving(false); t({kind:'error', title:'Failed to update'}); });
  };

  var doErasure=function(){
    if(!editing || !confirmErase) return;
    var orgId = editing.id||editing.org_id;
    setErasing(true);
    NR_API.requestErasure(orgId, confirmErase==='immediate', '').then(function(res){
      setErasing(false);
      if(res && res.ok){
        t({title:'Erasure '+(confirmErase==='immediate'?'started':'scheduled'), msg:editName});
        setConfirmErase(null); setConfirmText('');
        NR_API.getErasure(orgId).then(setErasure);
      } else if(res && res.status===409){
        t({kind:'error', title:'Erasure already pending'});
      } else if(res && res.status===403){
        t({kind:'error', title:'Org is under legal hold'});
      } else {
        t({kind:'error', title:'Erasure failed', msg:(res&&res.body&&res.body.error&&res.body.error.message)||'Check server logs.'});
      }
    }).catch(function(){ setErasing(false); t({kind:'error', title:'Erasure failed'}); });
  };

  var cancelErasureNow=function(){
    if(!editing) return;
    var orgId = editing.id||editing.org_id;
    NR_API.cancelErasure(orgId).then(function(ok){
      if(ok){ t({title:'Erasure cancelled'}); NR_API.getErasure(orgId).then(setErasure); }
      else { t({kind:'error', title:'Cancel failed'}); }
    });
  };

  var createCustomer=function(){
    if(!newOrg.org||!newOrg.email) return;
    setCreating(true);
    NR_API.onboard(newOrg.org, newOrg.email, newOrg.name).then(function(res){
      setCreating(false);
      if(res && !res.error){
        setOnboarding(false);
        setNewOrg({org:'',email:'',name:''});
        t({title:'Customer onboarded', msg:newOrg.org});
        loadCustomers();
      } else {
        t({kind:'error', title:'Failed to onboard', msg:(res&&res.error&&res.error.message)||'Check server logs.'});
      }
    }).catch(function(){ setCreating(false); t({kind:'error', title:'Failed to onboard'}); });
  };

  if(loading) return <LoadingPage/>;

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc="All customer organizations on the NeuroRoute platform.">
        <button className="btn btn-primary btn-sm" onClick={function(){setOnboarding(true);}}><Icon name="onboard" size={15}/>Onboard customer</button>
      </PageHead>

      {loadFailed ? <EvalLoadError what="customers" onRetry={loadCustomers}/> :
       customers.length===0 ?
        <EmptyState icon="customers" title="No customers yet"
          desc="Onboard your first customer to start routing their AI requests through NeuroRoute."
          actions={[{label:'Onboard Customer', onClick:function(){setOnboarding(true);}, primary:true}]}/> :
        <>
          <div className="metric-grid">
            <Metric icon="customers" color="blue" value={customers.length} label="Total customers"/>
            <Metric icon="zap" color="cyan" value={fmtNum(customers.reduce(function(a,b){return a+(b.requests_this_month||b.total_requests||b.reqs||0);},0))} label="Requests (this month)"/>
            <Metric icon="dollar" color="green" value={fmtUSD(customers.reduce(function(a,b){return a+(b.total_revenue||b.total_cost||b.revenue||0);},0),2)} label="Revenue (this month)"/>
          </div>
          <SectionCard title="Organizations" pad={false}>
            <div className="scroll-x"><table className="table">
              <thead><tr><th>Organization</th><th>Tier</th><th style={{textAlign:'right'}}>Requests (mo)</th><th style={{textAlign:'right'}}>Revenue (mo)</th><th>Status</th><th></th></tr></thead>
              <tbody>{customers.map(function(c){
                var id = c.id||c.org_id||c.org;
                return (
                  <tr key={id}>
                    <td><div style={{fontWeight:600,color:'var(--tx-0)'}}>{c.name||c.org_name||c.org}</div><div className="faint" style={{fontSize:12}}>{c.email||c.billing_email||''}</div></td>
                    <td><Badge color={(c.tier||'basic')==='enterprise'?'purple':(c.tier||'basic')==='premium'?'blue':(c.tier||'basic')==='standard'?'teal':'gray'}>{c.tier||'basic'}</Badge></td>
                    <td className="num" style={{textAlign:'right'}}>{fmtNum(c.requests_this_month||c.total_requests||c.reqs||0)}</td>
                    <td className="num" style={{textAlign:'right'}}>{fmtUSD(c.total_revenue||c.total_cost||c.revenue||0,2)}</td>
                    <td><Badge color={(c.status||'active')==='active'?'green':'amber'} dot>{c.status||'Active'}</Badge></td>
                    <td style={{textAlign:'right'}}>
                      <button className="btn btn-ghost btn-sm" style={{marginRight:6}} onClick={function(){setViewLogsOrg(c);}}><Icon name="search" size={13}/>Logs</button>
                      <button className="btn btn-ghost btn-sm" style={{marginRight:6}} onClick={function(){setTeamOrg(c);}}><Icon name="team" size={13}/>Team</button>
                      <button className="btn btn-soft btn-sm" onClick={function(){openEdit(c);}}><Icon name="settings" size={13}/>Edit</button>
                    </td>
                  </tr>
                );
              })}</tbody>
            </table></div>
          </SectionCard>
        </>
      }

      {/* Onboard modal */}
      {onboarding && <Modal title="Onboard new customer" sub="Creates a new organization with an admin user." onClose={function(){setOnboarding(false);}}
        footer={<><button className="btn btn-ghost" onClick={function(){setOnboarding(false);}}>Cancel</button><button className="btn btn-primary" disabled={!newOrg.org||!newOrg.email||creating} onClick={createCustomer}>{creating?'Creating…':'Create customer'}</button></>}>
        <div className="grid" style={{gap:16}}>
          <div className="grid" style={{gridTemplateColumns:'1fr 1fr',gap:16}}>
            <Field label="Organization name"><input className="input" placeholder="Acme Corp" value={newOrg.org} onChange={function(e){setNewOrg(Object.assign({},newOrg,{org:e.target.value}));}}/></Field>
            <Field label="Admin name"><input className="input" placeholder="Jane Smith" value={newOrg.name} onChange={function(e){setNewOrg(Object.assign({},newOrg,{name:e.target.value}));}}/></Field>
          </div>
          <Field label="Admin email"><input className="input" type="email" placeholder="admin@acme.com" value={newOrg.email} onChange={function(e){setNewOrg(Object.assign({},newOrg,{email:e.target.value}));}}/></Field>
        </div>
      </Modal>}

      {/* Edit view — a full page (was a Modal; the org config has grown to
          cover tier/entitlements, commercial terms, safety controls, alerts,
          billing, erasure and export, which no longer fits a popup). The
          "Back to customers" button + Cancel both return to the same list
          view (setEditing(null)) without touching the URL. */}
      {editing && (
        <div className="grid" style={{gap:18}}>
          <div className="spread" style={{alignItems:'flex-start',flexWrap:'wrap',gap:12}}>
            <div>
              <button className="btn btn-ghost btn-sm" style={{marginBottom:12}} onClick={function(){setEditing(null);}}>
                <Icon name="chevron" size={13} style={{transform:'rotate(180deg)'}}/> Back to customers
              </button>
              <h2 style={{margin:'0 0 4px',fontSize:21}}>{editing.name||editing.org_name||editing.org}</h2>
              <div className="faint" style={{fontSize:13}}>{editing.email||editing.billing_email||''}</div>
            </div>
            <div className="row" style={{gap:10}}>
              <button className="btn btn-ghost" onClick={function(){setEditing(null);}}>Cancel</button>
              <button className="btn btn-primary" disabled={!editName.trim()||saving} onClick={saveEdit}>{saving?'Saving…':'Save changes'}</button>
            </div>
          </div>

        <SectionCard title="Organization">
        <div className="grid" style={{gap:16}}>
          <Field label="Organization name"><input className="input" value={editName} onChange={function(e){setEditName(e.target.value);}}/></Field>
          {(editing.id||editing.org_id) && <Field label="Organization ID"><input className="input input-mono" value={editing.id||editing.org_id} readOnly/></Field>}
          <Field label="Daily budget cap (USD)" hint="Hard limit on provider spend per day. Leave blank for unlimited.">
            <input className="input" type="number" min="0" step="1" placeholder="Unlimited" value={editDaily} onChange={function(e){setEditDaily(e.target.value);}}/>
          </Field>
          <Field label="Data retention" hint="Zero modes store no prompts/responses platform-side; strict also disables provider prompt caching. Proven per request via the X-Data-Retention header. Customers can also self-manage this in Settings → Data retention.">
            <select className="select" value={editRet} onChange={function(e){setEditRet(e.target.value);}}>
              <option value="zero-strict">Zero — strict (no provider caching)</option>
              <option value="zero">Zero — platform stores nothing</option>
              <option value="retain">Retain content N days</option>
            </select>
          </Field>
          {editRet==='retain' && <Field label="Retention window (days)" hint="Content older than this is deleted daily. 1-365.">
            <input className="input" type="number" min="1" max="365" step="1" value={editRetDays} onChange={function(e){setEditRetDays(e.target.value);}}/>
          </Field>}
          {editRet==='retain' && <Field label="Request content logging" hint="Metadata-only (default) never stores prompts/responses. Content mode encrypts and stores them under this org's DEK, viewable in Request Logs — requires retain mode.">
            <select className="select" value={editReqLogMode} onChange={function(e){setEditReqLogMode(e.target.value);}}>
              <option value="metadata">Metadata only (default)</option>
              <option value="content">Content — store encrypted prompt + response</option>
            </select>
          </Field>}
          <Field label="Monthly budget cap (USD)" hint="Hard limit on provider spend per month. Leave blank for unlimited. Requests are rejected once the cap is reached.">
            <input className="input" type="number" min="0" step="1" placeholder="Unlimited" value={editBudget} onChange={function(e){setEditBudget(e.target.value);}}/>
          </Field>
        </div>
        </SectionCard>

        <SectionCard title="Plan & entitlements" sub="Sets which NeuroRoute features this org's plan includes. Grants/revokes below can override individual features beyond the tier default.">
        <div className="grid" style={{gap:16}}>
          <Field label="Pricing tier">
            <select className="select" value={editTier} onChange={function(e){setEditTier(e.target.value);}}>
              {['basic','standard','premium','enterprise'].map(function(tr){return <option key={tr} value={tr}>{tr.charAt(0).toUpperCase()+tr.slice(1)}</option>;})}
            </select>
          </Field>

          {/* --- Feature override editor --- */}
          {(function(){
            // Hand-authored display copy ONLY — which features exist and
            // their home tier come live from entReqTiers (required_tiers,
            // fetched once from GET /v1/customer/entitlements), not from
            // this map, so plans.go stays the single source of truth for
            // tier assignment. An unlabeled key still renders via `||k`.
            var FEATURE_LABELS={
              routing:'Routing',compression:'Compression',workspaces:'Workspaces',cache_l1:'L1 cache',
              cache_l2:'Semantic cache',byok:'BYOK keys',mcp:'MCP endpoint',webhooks:'Webhooks',alerts:'Alerts',
              audit_log:'Audit log',scoped_keys:'Scoped keys',encryption_at_rest:'Encryption at rest',
              erasure:'Erasure / RTBF',fusion:'Fusion routing',sso_oidc:'SSO (OIDC)',sso_saml:'SSO (SAML)',
              ccr:'Conversation retrieval (CCR)',cmek:'CMEK',otel:'OpenTelemetry',custom_provider:'Custom provider',
              dek_rotation:'DEK rotation',concise_output:'Concise output',pipeline:'Pipeline chaining',
              agent_memory:'Agent memory',a2a:'A2A (agent-to-agent)',guardrails:'Guardrails',
              evals:'Evaluation framework',fair_share:'Fair-share scheduling',run_budgets:'Run budgets',
              model_priority_chain:'Model priority chain'
            };
            var reqTiers=entReqTiers||{};
            // Derived from the live catalog, not Object.keys(FEATURE_LABELS)
            // — a feature this editor has no label for yet still appears
            // (with its raw key as the label) instead of silently vanishing,
            // and a feature moved between tiers in plans.go needs no edit here.
            var FEATURES=Object.keys(reqTiers).map(function(k){
              return {key:k, label:FEATURE_LABELS[k]||k, tier:reqTiers[k]};
            });
            var tierRank={basic:0,standard:1,premium:2,enterprise:3};
            var curRank=editTier in tierRank ? tierRank[editTier] : 3;
            var overrideMap={};
            editOverrides.forEach(function(o){ overrideMap[o.feature]={granted:o.granted,reason:o.reason||''}; });
            var toggleOverride=function(feat,granted){
              var orgId=editing.id||editing.org_id;
              NR_API.setOrgOverride(orgId,feat,granted,'admin UI').then(function(){
                return NR_API.orgOverrides(orgId);
              }).then(function(r){ setEditOverrides(r.overrides||[]); t({title:'Override '+(granted?'granted':'revoked')+': '+feat}); })
              .catch(function(){ t({kind:'error',title:'Failed to set override'}); });
            };
            var clearOverride=function(feat){
              var orgId=editing.id||editing.org_id;
              NR_API.clearOrgOverride(orgId,feat).then(function(){
                return NR_API.orgOverrides(orgId);
              }).then(function(r){ setEditOverrides(r.overrides||[]); t({title:'Override cleared: '+feat}); })
              .catch(function(){ t({kind:'error',title:'Failed to clear override'}); });
            };
            return <div style={{marginBottom:12}}>
              <div style={{fontWeight:600,marginBottom:4}}>Feature overrides</div>
              <div className="faint" style={{fontSize:12,marginBottom:8}}>Grant or revoke individual features beyond what the tier includes. Overrides persist across tier changes.</div>
              {(overridesLoading || !entReqTiers) ? <div className="faint" style={{fontSize:12}}>Loading overrides…</div> :
              <div style={{display:'grid',gridTemplateColumns:'1fr auto',gap:'2px 8px',fontSize:13,alignItems:'center'}}>
                {FEATURES.map(function(f){
                  var inTier=tierRank[f.tier]<=curRank;
                  var ov=overrideMap[f.key];
                  var effective=ov ? ov.granted : inTier;
                  var badge=ov ? (ov.granted ? <Badge color="green" style={{fontSize:10,marginLeft:4}}>granted</Badge> : <Badge color="red" style={{fontSize:10,marginLeft:4}}>revoked</Badge>) : null;
                  return [
                    <div key={f.key+'l'} style={{padding:'3px 0',display:'flex',alignItems:'center'}}>
                      <span style={{opacity:effective?1:0.5}}>{f.label}</span>
                      <span className="faint" style={{fontSize:11,marginLeft:4}}>({f.tier})</span>
                      {badge}
                    </div>,
                    <div key={f.key+'a'} style={{display:'flex',gap:4,alignItems:'center'}}>
                      {!ov && !inTier && <button className="btn btn-soft btn-sm" style={{fontSize:11,padding:'1px 6px'}} onClick={function(){toggleOverride(f.key,true);}}>Grant</button>}
                      {!ov && inTier && <button className="btn btn-soft btn-sm" style={{fontSize:11,padding:'1px 6px'}} onClick={function(){toggleOverride(f.key,false);}}>Revoke</button>}
                      {ov && <button className="btn btn-soft btn-sm" style={{fontSize:11,padding:'1px 6px'}} onClick={function(){clearOverride(f.key);}}>Clear</button>}
                    </div>
                  ];
                })}
              </div>}
            </div>;
          })()}

          {(function(){
            /* Per-org commercial terms (P4) — append a new terms row inline.
               terms/tForm/tSaving are hoisted top-level hooks (see above) —
               this IIFE only reads/derives, it must never call a hook itself. */
            var orgId = editing && (editing.id||editing.org_id);
            if(!orgId) return null;
            var addTerms=function(){
              if(!tForm.discount_pct&&!tForm.base_fee_usd&&!tForm.in_rate_usd&&!tForm.out_rate_usd) return;
              setTSaving(true);
              var payload={};
              if(tForm.discount_pct) payload.discount_pct=parseFloat(tForm.discount_pct);
              if(tForm.base_fee_usd) payload.base_fee_usd=parseFloat(tForm.base_fee_usd);
              if(tForm.in_rate_usd) payload.in_rate_usd=parseFloat(tForm.in_rate_usd);
              if(tForm.out_rate_usd) payload.out_rate_usd=parseFloat(tForm.out_rate_usd);
              if(tForm.effective_from) payload.effective_from=tForm.effective_from;
              if(tForm.note) payload.note=tForm.note;
              NR_API.appendPriceTerms(orgId, payload).then(function(r){
                setTSaving(false);
                if(r&&r.id){ t({title:'Terms added', msg:'ID '+r.id}); loadTerms(orgId); }
                else { t({kind:'error',title:'Failed to add terms'}); }
              }).catch(function(){setTSaving(false);t({kind:'error',title:'Failed to add terms'});});
            };
            return <div style={{background:'var(--bg-inset)',borderRadius:10,padding:'14px 16px',marginTop:4}}>
              <div style={{fontWeight:600,marginBottom:8}}>Per-org commercial terms <Badge color="cyan" style={{fontSize:10}}>P4</Badge></div>
              {termsError ? <EvalLoadError what="commercial terms" onRetry={function(){loadTerms(orgId);}}/> :
               terms&&terms.length>0&&<div className="scroll-x" style={{marginBottom:10}}>
                <table className="table" style={{fontSize:12}}>
                  <thead><tr><th>Effective</th><th>Disc%</th><th>Base</th><th>In</th><th>Out</th><th>Note</th></tr></thead>
                  <tbody>{terms.map(function(r){return(<tr key={r.id}>
                    <td className="faint">{r.effective_from?r.effective_from.slice(0,10):''}</td>
                    <td>{r.discount_pct!=null?r.discount_pct+'%':'—'}</td>
                    <td>{r.base_fee_usd!=null?fmtUSD(r.base_fee_usd,2):'—'}</td>
                    <td>{r.in_rate_usd!=null?fmtUSD(r.in_rate_usd,4):'—'}</td>
                    <td>{r.out_rate_usd!=null?fmtUSD(r.out_rate_usd,4):'—'}</td>
                    <td className="faint">{r.note||''}</td>
                  </tr>);})}</tbody>
                </table>
              </div>}
              <div className="faint" style={{fontSize:12,marginBottom:8}}>Append a new effective-dated terms row. Discount% or explicit rate overrides apply from effective_from onward.</div>
              <div className="row" style={{gap:8,flexWrap:'wrap',marginBottom:8}}>
                <input className="input" style={{flex:'0 0 90px'}} type="number" min="0" max="100" step="1" placeholder="Disc%" value={tForm.discount_pct} onChange={function(e){setTForm(function(f){return Object.assign({},f,{discount_pct:e.target.value});});}}/>
                <input className="input" style={{flex:'0 0 90px'}} type="number" min="0" step="1" placeholder="Base$/mo" value={tForm.base_fee_usd} onChange={function(e){setTForm(function(f){return Object.assign({},f,{base_fee_usd:e.target.value});});}}/>
                <input className="input" style={{flex:'0 0 90px'}} type="number" min="0" step="0.01" placeholder="In$/1M" value={tForm.in_rate_usd} onChange={function(e){setTForm(function(f){return Object.assign({},f,{in_rate_usd:e.target.value});});}}/>
                <input className="input" style={{flex:'0 0 90px'}} type="number" min="0" step="0.01" placeholder="Out$/1M" value={tForm.out_rate_usd} onChange={function(e){setTForm(function(f){return Object.assign({},f,{out_rate_usd:e.target.value});});}}/>
                <input className="input" style={{flex:'0 0 120px'}} type="date" value={tForm.effective_from} onChange={function(e){setTForm(function(f){return Object.assign({},f,{effective_from:e.target.value});});}}/>
                <input className="input" style={{flex:'1 1 140px'}} placeholder="Note" value={tForm.note} onChange={function(e){setTForm(function(f){return Object.assign({},f,{note:e.target.value});});}}/>
              </div>
              <button className="btn btn-soft btn-sm" onClick={addTerms} disabled={tSaving}>{tSaving?'Saving…':'Add terms row'}</button>
            </div>;
          })()}
        </div>
        </SectionCard>

        <SectionCard title="Data & safety controls">
        <div className="grid" style={{gap:16}}>
          <div className="spread" style={{padding:'4px 0'}}>
            <div style={{maxWidth:'70%'}}>
              <div style={{fontWeight:600}}>Semantic cache <Badge color="amber" style={{fontSize:10}}>beta</Badge></div>
              <div className="faint" style={{fontSize:12.5}}>Reuse cached answers for near-duplicate prompts (skips the model call). Big savings on repetitive workloads; can occasionally return a close-but-not-identical answer.</div>
            </div>
            <Switch on={editSemCache} onChange={setEditSemCache}/>
          </div>

          <div className="spread" style={{padding:'4px 0'}}>
            <div style={{maxWidth:'70%'}}>
              <div style={{fontWeight:600}}>Context compression</div>
              <div className="faint" style={{fontSize:12.5}}>Shrink input tokens before the model (whitespace, JSON, log dedup). Deterministic & lossless. Savings shown on the customer's Usage page.</div>
            </div>
            <Switch on={editComp} onChange={setEditComp}/>
          </div>
          {editComp && <div className="spread" style={{padding:'4px 0 4px 16px'}}>
            <div style={{maxWidth:'70%'}}>
              <div style={{fontWeight:600,fontSize:13}}>Aggressive mode <Badge color="red" style={{fontSize:10}}>test only</Badge></div>
              <div className="faint" style={{fontSize:12}}>Lossy — also strips code comments & collapses repeats. Only enable for test orgs.</div>
            </div>
            <Switch on={editCompAggr} onChange={setEditCompAggr}/>
          </div>}

          <div className="spread" style={{padding:'4px 0'}}>
            <div style={{maxWidth:'70%'}}>
              <div style={{fontWeight:600}}>Legal hold</div>
              <div className="faint" style={{fontSize:12.5}}>Blocks data erasure and the retention sweeper. Set when litigation/audit requires preserving this org's data.</div>
            </div>
            <Switch on={editLegalHold} onChange={setEditLegalHold}/>
          </div>

          <Field label="Customer-managed key (CMEK)" hint="Full Cloud KMS key resource name in the CUSTOMER's project (projects/P/locations/L/keyRings/R/cryptoKeys/K), granted to our service account. Blank = platform key. Revoking our access is their instant kill switch — stored content becomes unreadable. Enterprise customers can also self-manage this in Settings.">
            <input className="input mono" style={{fontSize:12}} placeholder="Platform key (default)" value={editCMEK} onChange={function(e){setEditCMEK(e.target.value);}}/>
          </Field>

          <div style={{marginTop:-8}}>
            <button type="button" className="btn btn-ghost btn-sm" style={{padding:'2px 4px',height:'auto'}}
              onClick={function(){setShowCmekHelp(!showCmekHelp);}}>
              <Icon name={showCmekHelp?'chevdown':'chevron'} size={12}/>
              {showCmekHelp?'Hide setup guide':'How to set this up (and how to turn it off)'}
            </button>
            {showCmekHelp && <div className="codeblock" style={{fontSize:12,lineHeight:1.65,marginTop:8}}>
              <div className="faint" style={{fontSize:11,textTransform:'uppercase',letterSpacing:'.04em',marginBottom:8}}>
                Forward these steps to the customer's cloud team
              </div>

              <div style={{fontWeight:600,marginBottom:4}}>1. Create a key in your own GCP project</div>
              <div className="row" style={{gap:8,alignItems:'flex-start'}}>
                <pre style={{flex:1,margin:0,whiteSpace:'pre-wrap',wordBreak:'break-all'}}>{
`gcloud kms keyrings create neuroroute-cmek --location=<region>
gcloud kms keys create <key-name> \\
  --keyring=neuroroute-cmek --location=<region> \\
  --purpose=encryption`
                }</pre>
                <CopyBtn label="" text={`gcloud kms keyrings create neuroroute-cmek --location=<region>\ngcloud kms keys create <key-name> --keyring=neuroroute-cmek --location=<region> --purpose=encryption`}/>
              </div>

              <div style={{fontWeight:600,margin:'12px 0 4px'}}>2. Grant NeuroRoute's service account access to it</div>
              <div className="row" style={{gap:8,alignItems:'flex-start'}}>
                <pre style={{flex:1,margin:0,whiteSpace:'pre-wrap',wordBreak:'break-all'}}>{
`gcloud kms keys add-iam-policy-binding <key-name> \\
  --keyring=neuroroute-cmek --location=<region> \\
  --member="serviceAccount:neuroroute-run@neuroroute-prod.iam.gserviceaccount.com" \\
  --role="roles/cloudkms.cryptoKeyEncrypterDecrypter"`
                }</pre>
                <CopyBtn label="" text={`gcloud kms keys add-iam-policy-binding <key-name> --keyring=neuroroute-cmek --location=<region> --member="serviceAccount:neuroroute-run@neuroroute-prod.iam.gserviceaccount.com" --role="roles/cloudkms.cryptoKeyEncrypterDecrypter"`}/>
              </div>

              <div style={{fontWeight:600,margin:'12px 0 4px'}}>3. Paste the full key resource name above</div>
              <div className="row" style={{gap:8,alignItems:'flex-start'}}>
                <pre style={{flex:1,margin:0,whiteSpace:'pre-wrap',wordBreak:'break-all'}}>{'projects/<their-project>/locations/<region>/keyRings/neuroroute-cmek/cryptoKeys/<key-name>'}</pre>
                <CopyBtn label="" text={'projects/<their-project>/locations/<region>/keyRings/neuroroute-cmek/cryptoKeys/<key-name>'}/>
              </div>
              <div className="faint" style={{fontSize:11,marginTop:2}}>Save changes — the next encryption key we mint for this org is wrapped by their key instead of ours. Existing encrypted data stays under whichever key wrapped it until it's next rotated.</div>

              <div style={{fontWeight:600,margin:'12px 0 4px'}}>To turn it off</div>
              <div className="faint" style={{fontSize:11.5}}>Clear this field (leave it blank) and save. New data reverts to the platform key. Their own key and its data stay theirs — nothing is deleted.</div>
            </div>}
          </div>

          <div className="spread" style={{padding:'4px 0'}}>
            <div style={{maxWidth:'70%'}}>
              <div style={{fontWeight:600}}>Guardrails <Badge color="amber" style={{fontSize:10}}>beta</Badge></div>
              <div className="faint" style={{fontSize:12.5}}>Input/output content-safety checks (PII, secrets, prompt-injection, moderation, vision, length, custom rules). Default set: PII flagged, secrets blocked, oversized prompts blocked, prompt-injection flagged. Flags/blocks show up in Request Logs.</div>
            </div>
            <Switch on={editGuardrails} onChange={setEditGuardrails}/>
          </div>

          {editGuardrails && <div style={{marginTop:-4,marginBottom:4}}>
            <button type="button" className="btn btn-ghost btn-sm" style={{padding:'2px 4px',height:'auto'}}
              onClick={function(){setShowGuardrailRules(!showGuardrailRules);}}>
              <Icon name={showGuardrailRules?'chevdown':'chevron'} size={12}/>
              {showGuardrailRules?'Hide rule configuration':'Configure rules'}
            </button>
            {showGuardrailRules && <div className="codeblock" style={{fontSize:12,lineHeight:1.5,marginTop:8}}>
              <div className="faint" style={{fontSize:11,textTransform:'uppercase',letterSpacing:'.04em',marginBottom:8}}>
                Custom rule set for this org — replaces the platform default set (PII flagged, secrets blocked, oversized prompts blocked) while enabled above
              </div>
              {['input','output'].map(function(phase){
                return <div key={phase}>
                  <div className="faint" style={{fontSize:10.5,textTransform:'uppercase',letterSpacing:'.04em',margin:'10px 0 2px',opacity:.7}}>
                    {phase==='input'?'Input rules (checked before routing)':'Output rules (checked on the model response)'}
                  </div>
                  {editGuardrailRows.filter(function(row){return row.phase===phase;}).map(function(row){
                    var idx = editGuardrailRows.indexOf(row);
                    function patchRow(patch){
                      setEditGuardrailRows(editGuardrailRows.map(function(r,i){return i===idx?Object.assign({},r,patch):r;}));
                    }
                    return (
                    <div key={row.name} style={{padding:'8px 0',borderTop:'1px solid rgba(128,128,128,.2)'}}>
                      <div className="spread">
                        <div style={{display:'flex',alignItems:'flex-start',gap:8}}>
                          <input type="checkbox" style={{marginTop:3}} checked={row.enabled} onChange={function(e){patchRow({enabled:e.target.checked});}}/>
                          <div>
                            <div style={{fontWeight:600}}>{row.label}</div>
                            <div className="faint" style={{fontSize:11}}>{row.desc}</div>
                          </div>
                        </div>
                        <select className="select" style={{minWidth:100}} disabled={!row.enabled} value={row.action} onChange={function(e){patchRow({action:e.target.value});}}>
                          <option value="flag">Flag</option>
                          <option value="redact">Redact</option>
                          <option value="block">Block</option>
                        </select>
                      </div>
                      {row.enabled && (row.name==='length-cap'||row.name==='output-length-cap') && <div style={{marginTop:6,marginLeft:24,maxWidth:200}}>
                        <Field label="Max characters"><input className="input" type="number" min="1" step="1000" value={row.maxChars} onChange={function(e){patchRow({maxChars:e.target.value});}}/></Field>
                      </div>}
                      {row.enabled && (row.name==='denylist'||row.name==='output-denylist') && <div style={{marginTop:6,marginLeft:24}}>
                        <Field label="Patterns (one per line, regex)"><textarea className="input mono" rows={3} style={{fontSize:11.5,width:'100%'}} placeholder={'e.g.\nproject-codename-x\ninternal-only'} value={row.patterns} onChange={function(e){patchRow({patterns:e.target.value});}}/></Field>
                      </div>}
                      {row.enabled && row.name==='json-schema' && <div style={{marginTop:6,marginLeft:24}}>
                        <Field label="Required top-level JSON keys (comma-separated)" hint="Leave blank to only check the response is valid JSON.">
                          <input className="input" placeholder="e.g. name, amount" value={row.required} onChange={function(e){patchRow({required:e.target.value});}}/>
                        </Field>
                      </div>}
                      {row.enabled && row.name==='webhook' && <div style={{marginTop:6,marginLeft:24,display:'grid',gap:8}}>
                        <Field label="Endpoint URL"><input className="input mono" placeholder="https://yourapp.example.com/guardrail" value={row.webhookUrl} onChange={function(e){patchRow({webhookUrl:e.target.value});}}/></Field>
                        <Field label="Signing secret" hint="Used to HMAC-SHA256 sign each request (header X-NeuroRoute-Signature)."><input className="input mono" type="password" placeholder="shared secret" value={row.webhookSecret} onChange={function(e){patchRow({webhookSecret:e.target.value});}}/></Field>
                      </div>}
                      {row.enabled && row.name==='moderation' && <div style={{marginTop:6,marginLeft:24}}>
                        <Field label="System prompt override (optional)" hint="Leave blank to use the default binary safety classifier prompt.">
                          <textarea className="input mono" rows={2} style={{fontSize:11.5,width:'100%'}} value={row.systemPrompt} onChange={function(e){patchRow({systemPrompt:e.target.value});}}/>
                        </Field>
                      </div>}
                      {row.enabled && row.name==='vision' && <div style={{marginTop:6,marginLeft:24,display:'grid',gap:8}}>
                        <Field label="Max image size (bytes)" hint="Blank = default 10 MB. -1 = unlimited.">
                          <input className="input" type="number" step="1" placeholder="10485760" value={row.maxImageSizeBytes} onChange={function(e){patchRow({maxImageSizeBytes:e.target.value});}}/>
                        </Field>
                        <Field label="Allowed MIME types (comma-separated)" hint="Blank = allow all standard image types (jpeg/png/gif/webp/bmp/tiff/svg+xml).">
                          <input className="input mono" placeholder="e.g. image/png, image/jpeg" value={row.allowedMimeTypes} onChange={function(e){patchRow({allowedMimeTypes:e.target.value});}}/>
                        </Field>
                      </div>}
                    </div>
                    );
                  })}
                </div>;
              })}
              {editGuardrailRows.filter(function(r){return r.__unknownRule;}).length>0 && <div className="faint" style={{fontSize:11,marginTop:10,paddingTop:8,borderTop:'1px solid rgba(128,128,128,.2)'}}>
                {editGuardrailRows.filter(function(r){return r.__unknownRule;}).length} rule(s) configured outside this UI ({editGuardrailRows.filter(function(r){return r.__unknownRule;}).map(function(r){return r.name;}).join(', ')}) are already active for this org and will be preserved when you save.
              </div>}
            </div>}
          </div>}

          <div className="spread" style={{padding:'4px 0'}}>
            <div style={{maxWidth:'70%'}}>
              <div style={{fontWeight:600}}>Learned routing (KNN)</div>
              <div className="faint" style={{fontSize:12.5}}>Route by what worked for similar past prompts (learns from feedback). Retain-mode orgs only; off until enough data accrues.</div>
            </div>
            <Switch on={editMLRouting} onChange={setEditMLRouting}/>
          </div>

          <div className="spread" style={{padding:'4px 0'}}>
            <div style={{maxWidth:'70%'}}>
              <div style={{fontWeight:600}}>Concise output</div>
              <div className="faint" style={{fontSize:12.5}}>Append a brevity directive to every system message — skipped when the client already specifies a style.</div>
            </div>
            <Switch on={editConciseOutput} onChange={setEditConciseOutput}/>
          </div>

          <div style={{marginTop:8}}>
            <div style={{fontWeight:600,marginBottom:4}}>Model priority chain</div>
            <div className="faint" style={{fontSize:12.5,marginBottom:8}}>Restrict routing to this ordered list — cascade tries in order, direct routing scores within it. Empty = normal AI routing.</div>
            {editMPC.length===0 && <div className="faint" style={{fontSize:12,marginBottom:4}}>No chain — AI routing selects the best model.</div>}
            {editMPC.map(function(id,idx){
              return <div key={id} className="spread" style={{padding:'3px 0',borderBottom:'1px solid var(--line)'}}>
                <span className="row" style={{gap:6}}>
                  <span style={{color:'var(--fg-3)',fontSize:11,minWidth:16}}>{idx+1}.</span>
                  <code style={{fontSize:12}}>{id}</code>
                </span>
                <span className="row" style={{gap:2}}>
                  <button className="btn btn-ghost btn-sm" style={{padding:'1px 4px'}} disabled={idx===0} onClick={function(){var a=editMPC.slice();var tmp=a[idx];a[idx]=a[idx-1];a[idx-1]=tmp;setEditMPC(a);}}>↑</button>
                  <button className="btn btn-ghost btn-sm" style={{padding:'1px 4px'}} disabled={idx===editMPC.length-1} onClick={function(){var a=editMPC.slice();var tmp=a[idx];a[idx]=a[idx+1];a[idx+1]=tmp;setEditMPC(a);}}>↓</button>
                  <button className="btn btn-ghost btn-sm" style={{padding:'1px 4px',color:'var(--red)'}} onClick={function(){setEditMPC(editMPC.filter(function(_,i){return i!==idx;}));}} >×</button>
                </span>
              </div>;
            })}
            {editMPC.length<10 && <div className="row" style={{gap:6,marginTop:6}}>
              <input className="input mono" style={{flex:1,fontSize:12}} placeholder="model ID e.g. gpt-4o-mini"
                value={editMPCNew} onChange={function(e){setEditMPCNew(e.target.value);}}
                onKeyDown={function(e){if(e.key==='Enter'){var id=editMPCNew.trim();if(id&&editMPC.indexOf(id)<0&&editMPC.length<10){setEditMPC(editMPC.concat([id]));setEditMPCNew('');}}}}>
              </input>
              <button className="btn btn-soft btn-sm" onClick={function(){var id=editMPCNew.trim();if(id&&editMPC.indexOf(id)<0&&editMPC.length<10){setEditMPC(editMPC.concat([id]));setEditMPCNew('');}}}>+ Add</button>
            </div>}
          </div>

          <Field label="Fair-share scheduling" hint="Under contention (org pool ≥85% used), each API key is held to its fair share; idle capacity is lent to active keys. Budget basis pools the org's daily budget cap (or the pro-rated monthly cap if no daily cap is set).">
            <select className="select" value={editFairShareMode} onChange={function(e){setEditFairShareMode(e.target.value);}}>
              <option value="">Off</option>
              <option value="budget">Budget ($ pool = daily budget cap)</option>
              <option value="throughput">Throughput (RPM/TPM)</option>
            </select>
          </Field>
          {editFairShareMode==='throughput' && <div className="grid" style={{gridTemplateColumns:'1fr 1fr',gap:16}}>
            <Field label="Requests/min pool" hint="Leave blank for unlimited.">
              <input className="input" type="number" min="0" step="1" placeholder="Unlimited" value={editFairRPM} onChange={function(e){setEditFairRPM(e.target.value);}}/>
            </Field>
            <Field label="Tokens/min pool" hint="Leave blank for unlimited.">
              <input className="input" type="number" min="0" step="1" placeholder="Unlimited" value={editFairTPM} onChange={function(e){setEditFairTPM(e.target.value);}}/>
            </Field>
          </div>}
        </div>
        </SectionCard>

        <SectionCard title="Alerts">
          <AlertsPanel orgId={editing.id||editing.org_id}/>
        </SectionCard>

        <SectionCard title="Monthly billing" sub={'Finalize '+thisMonth()+' into an invoiceable record.'}
          action={<button className="btn btn-soft btn-sm" disabled={finalizing} onClick={finalizeBilling}><Icon name="billing" size={14}/>{finalizing?'Finalizing…':'Finalize '+thisMonth()}</button>}
          pad={records.length>0}>
          {records.length>0 && <div>
            <div className="faint" style={{fontSize:12,marginBottom:8,letterSpacing:'.04em',textTransform:'uppercase'}}>Billing records</div>
            <div className="scroll-x"><table className="table">
              <thead><tr><th>Period</th><th>Status</th><th style={{textAlign:'right'}}>Platform fee</th><th style={{textAlign:'right'}}>Total charge</th><th></th></tr></thead>
              <tbody>{records.map(function(rec){
                var m = (rec.period_start||'').slice(0,7); // YYYY-MM
                var orgId = editing.id||editing.org_id;
                return (
                <tr key={rec.id}>
                  <td className="mono faint" style={{fontSize:12}}>{rec.period_start} → {rec.period_end}</td>
                  <td><Badge color={rec.status==='finalized'?'green':rec.status==='exported'?'cyan':'gray'} dot>{rec.status}</Badge></td>
                  <td className="num" style={{textAlign:'right'}}>{fmtUSD(rec.sisl_markup_amount||0,2)}</td>
                  <td className="num" style={{textAlign:'right',fontWeight:700}}>{fmtUSD(rec.total_customer_charge||0,2)}</td>
                  <td style={{textAlign:'right'}}><button className="iconbtn" title="Download CSV" onClick={function(){window.open('/v1/admin/organizations/'+orgId+'/billing/export?month='+m,'_blank');}}><Icon name="download" size={14}/></button></td>
                </tr>
              );})}</tbody>
            </table></div>
          </div>}
        </SectionCard>

        <SectionCard title="Data erasure (right to be forgotten)">
          <div>
            <div className="faint" style={{fontSize:12.5,marginBottom:12}}>
              {erasure===null ? 'Loading…' :
               erasure.status==='pending' ? ('Scheduled for '+(erasure.scheduled_for?new Date(erasure.scheduled_for).toLocaleString():'—')+'.') :
               erasure.status==='completed' ? ('Erased on '+(erasure.executed_at?new Date(erasure.executed_at).toLocaleString():'—')+'.') :
               erasure.status==='cancelled' ? 'Last request cancelled.' :
               'No erasure scheduled.'}
            </div>
            {erasure && erasure.status==='completed' && erasure.certificate && erasure.certificate.counts && <div className="faint mono" style={{fontSize:11,marginBottom:12}}>
              {Object.keys(erasure.certificate.counts).map(function(k){return k+': '+erasure.certificate.counts[k];}).join(' · ')}
            </div>}

            {erasure && erasure.status==='pending' ?
              <button className="btn btn-soft btn-sm" onClick={cancelErasureNow}>Cancel erasure</button> :
              <div className="row" style={{gap:10}}>
                <button className="btn btn-soft btn-sm" disabled={editLegalHold} onClick={function(){setConfirmErase('request');setConfirmText('');}}>Request erasure (7-day grace)</button>
                <button className="btn btn-danger btn-sm" disabled={editLegalHold} onClick={function(){setConfirmErase('immediate');setConfirmText('');}}>Erase now (immediate)</button>
              </div>}
            {editLegalHold && (!erasure || erasure.status!=='pending') && <div className="faint" style={{fontSize:12,marginTop:8}}>Clear legal hold to enable erasure.</div>}

            {confirmErase && <div className="card card-pad" style={{marginTop:12,background:'var(--red-t)',borderColor:'rgba(244,63,94,.35)'}}>
              <div style={{fontSize:12.5,marginBottom:10,color:'var(--tx-0)'}}>
                This permanently deletes all conversations, keys, and identity for this org and crypto-shreds its encryption key. Billing records are retained. Type the organization name to confirm.
              </div>
              <input className="input" placeholder={editName} value={confirmText} onChange={function(e){setConfirmText(e.target.value);}} style={{marginBottom:10}}/>
              <div className="row" style={{gap:10}}>
                <button className="btn btn-danger btn-sm" disabled={erasing || confirmText.trim()!==editName.trim()} onClick={doErasure}>{erasing?'Working…':'Confirm'}</button>
                <button className="btn btn-ghost btn-sm" onClick={function(){setConfirmErase(null);setConfirmText('');}}>Cancel</button>
              </div>
            </div>}
          </div>
        </SectionCard>

        <SectionCard title="Data export" sub="GDPR Art. 20 — download everything we hold for this org as machine-readable JSON (conversations, usage, account). Read-only."
          action={<button className="btn btn-soft btn-sm" onClick={function(){ window.open('/v1/admin/organizations/'+(editing.id||editing.org_id)+'/export','_blank'); }}><Icon name="download" size={14}/>Download JSON</button>}
          pad={false}>
        </SectionCard>
        </div>
      )}

      {viewLogsOrg && <Modal lg title={'Request logs — '+(viewLogsOrg.name||viewLogsOrg.org_name||viewLogsOrg.org)} sub={viewLogsOrg.email||viewLogsOrg.billing_email||''}
        onClose={function(){setViewLogsOrg(null);}}
        footer={<button className="btn btn-ghost" onClick={function(){setViewLogsOrg(null);}}>Close</button>}>
        <LogsPage orgId={viewLogsOrg.id||viewLogsOrg.org_id}/>
      </Modal>}

      {teamOrg && <Modal lg title={'Team — '+(teamOrg.name||teamOrg.org_name||teamOrg.org)} sub="Invite users, change roles, or replace this organization's admin."
        onClose={function(){setTeamOrg(null);}}
        footer={<button className="btn btn-ghost" onClick={function(){setTeamOrg(null);}}>Close</button>}>
        <AdminOrgTeamPanel orgId={teamOrg.id||teamOrg.org_id}/>
      </Modal>}
    </div>
  );
}

/* =====================  ONBOARD CUSTOMER  ===================== */
function OnboardPage(){
  var t=useToast();
  var _form=useState({org:'',email:'',name:''}); var form=_form[0], setForm=_form[1];
  var _done=useState(null); var done=_done[0], setDone=_done[1];
  var _submitting=useState(false); var submitting=_submitting[0], setSubmitting=_submitting[1];
  var set=function(k,v){setForm(function(f){var n=Object.assign({},f); n[k]=v; return n;});};

  var create=function(){
    setSubmitting(true);
    NR_API.onboard(form.org, form.email, form.name).then(function(res){
      setSubmitting(false);
      if(res && !res.error) {
        setDone(Object.assign({}, form, res));
        t({title:'Customer created'});
      } else {
        t({kind:'error', title:'Failed to onboard', msg:(res&&res.error&&res.error.message)||'Check server logs.'});
      }
    }).catch(function(){ setSubmitting(false); t({kind:'error', title:'Failed to onboard'}); });
  };

  return (
    <div className="grid" style={{gap:18,maxWidth:760}}>
      <PageHead desc="Provision a new customer organization with an admin account."/>
      {!done ? <SectionCard title="New customer">
        <div className="grid" style={{gap:16}}>
          <div className="grid" style={{gridTemplateColumns:'1fr 1fr',gap:16}}>
            <Field label="Organization name"><input className="input" placeholder="Acme Corp" value={form.org} onChange={function(e){set('org',e.target.value);}}/></Field>
            <Field label="Admin name"><input className="input" placeholder="Jane Smith" value={form.name} onChange={function(e){set('name',e.target.value);}}/></Field>
          </div>
          <Field label="Admin email"><input className="input" type="email" placeholder="admin@acme.com" value={form.email} onChange={function(e){set('email',e.target.value);}}/></Field>
          <button className="btn btn-primary btn-block btn-lg" disabled={!form.org||!form.email||submitting} onClick={create}>
            <Icon name="onboard" size={16}/>{submitting?'Creating…':'Create customer'}
          </button>
        </div>
      </SectionCard> : (
        <SectionCard title="Customer created">
          <div className="card card-pad" style={{background:'var(--green-t)',borderColor:'rgba(16,185,129,.3)',marginBottom:16}}>
            <div className="row" style={{gap:10}}><Icon name="checkcircle" size={20} style={{color:'var(--green)'}}/><b style={{fontSize:15}}>{done.org||done.org_name} is ready</b></div>
          </div>
          <div className="grid" style={{gap:12}}>
            {[
              ['Organization', done.org||done.org_name],
              ['Admin email', done.email],
              done.org_id ? ['Organization ID', done.org_id] : null,
              done.api_key ? ['API Key', done.api_key] : null,
            ].filter(Boolean).map(function(pair){return (
              <div key={pair[0]} className="spread" style={{padding:'12px 14px',background:'var(--bg-inset)',borderRadius:10}}>
                <span className="faint">{pair[0]}</span><span className="mono" style={{color:'var(--tx-0)'}}>{pair[1]}</span>
              </div>
            );})}
          </div>
          <div className="row" style={{gap:10,marginTop:18}}>
            <button className="btn btn-primary" onClick={function(){setDone(null);setForm({org:'',email:'',name:''});}}>Onboard another</button>
            <button className="btn btn-ghost" onClick={function(){go('customers');}}>View customers</button>
          </div>
        </SectionCard>
      )}
    </div>
  );
}

/* =====================  SISL PROVIDER KEYS  ===================== */
function SislKeysPage(){
  var t=useToast();
  var _loading=useState(true); var loading=_loading[0], setLoading=_loading[1];
  var _keys=useState([]); var keys=_keys[0], setKeys=_keys[1];
  // loadFailed distinguishes "the provider-keys fetch itself failed" from
  // "it succeeded and there are genuinely zero provider keys" — before this,
  // both collapsed into the same "No provider keys registered" empty state.
  var _loadFailed=useState(false); var loadFailed=_loadFailed[0], setLoadFailed=_loadFailed[1];
  var _modal=useState(false); var modal=_modal[0], setModal=_modal[1];
  // editingProvider is null for the "Add provider key" flow (a brand-new
  // provider) and set to the provider id when editing an existing row — same
  // modal, same POST /v1/admin/provider-keys endpoint either way, since the
  // backend already overwrites the keystore secret + hot-registers on every
  // call for a given provider (see handleProviderKeyCreate). Editing only
  // changes what the modal shows and how the client reconciles state after
  // a successful submit.
  var _editingProvider=useState(null); var editingProvider=_editingProvider[0], setEditingProvider=_editingProvider[1];
  var _provider=useState('openai'); var provider=_provider[0], setProvider=_provider[1];
  var _key=useState(''); var key=_key[0], setKey=_key[1];
  var _label=useState(''); var label=_label[0], setLabel=_label[1];
  var _adding=useState(false); var adding=_adding[0], setAdding=_adding[1];
  /* provider -> enabled map (default true when no status row). */
  var _status=useState({}); var status=_status[0], setStatus=_status[1];

  var _customProviders=useState([]); var customProviders=_customProviders[0], setCustomProviders=_customProviders[1];
  var _cpModal=useState(false); var cpModal=_cpModal[0], setCpModal=_cpModal[1];
  var _cpId=useState(''); var cpId=_cpId[0], setCpId=_cpId[1];
  var _cpName=useState(''); var cpName=_cpName[0], setCpName=_cpName[1];
  var _cpBaseUrl=useState(''); var cpBaseUrl=_cpBaseUrl[0], setCpBaseUrl=_cpBaseUrl[1];
  var _cpModelsJSON=useState('[\n  {\n    "id": "mistral-large-latest",\n    "display_name": "Mistral Large",\n    "input_per_1k_tokens": 0.002,\n    "output_per_1k_tokens": 0.006,\n    "max_context_tokens": 128000,\n    "supports_tools": true\n  }\n]');
  var cpModelsJSON=_cpModelsJSON[0], setCpModelsJSON=_cpModelsJSON[1];
  var _cpSaving=useState(false); var cpSaving=_cpSaving[0], setCpSaving=_cpSaving[1];

  var loadCustomProviders=function(){
    NR_API.customProviders().then(function(res){
      setCustomProviders((res&&res.providers)||[]);
    }).catch(function(){});
  };

  var loadKeys=function(){
    setLoading(true); setLoadFailed(false);
    // NR_API.providerKeys() is api()-backed: resolves null on any non-2xx
    // status or network failure, never rejects — detect failure via the
    // null result.
    NR_API.providerKeys().then(function(res){
      if(res===null){ setLoadFailed(true); setLoading(false); return; }
      /* Backend returns {provider_keys:[...]}. Accept legacy shapes too. */
      var list = (res && (res.provider_keys || res.keys)) || (Array.isArray(res) ? res : []);
      if(Array.isArray(list)) { setKeys(list); }
      setLoading(false);
    }).catch(function(){ setLoadFailed(true); setLoading(false); });
  };

  useEffect(function(){
    loadKeys();
    NR_API.providerStatus().then(function(res){
      var provs = (res && res.providers) || [];
      var m = {};
      provs.forEach(function(p){ m[p.provider] = p.enabled; });
      setStatus(m);
    }).catch(function(){});
    loadCustomProviders();
  },[]);

  var addCustomProvider=function(){
    var models;
    try { models = JSON.parse(cpModelsJSON); }
    catch(e){ t({kind:'error',title:'Models must be valid JSON',msg:e.message}); return; }
    if(!/^custom-[a-z0-9-]{1,40}$/.test(cpId)){
      t({kind:'error',title:'Invalid provider ID',msg:'Must match custom-[a-z0-9-]+, e.g. custom-mistral'});
      return;
    }
    setCpSaving(true);
    NR_API.createCustomProvider(cpId, cpName||cpId, cpBaseUrl, models).then(function(res){
      setCpSaving(false);
      if(res && res.provider_id){
        t({title:'Custom provider created', msg:'Now add its API key below to activate it.'});
        setCpModal(false); setCpId(''); setCpName(''); setCpBaseUrl('');
        loadCustomProviders();
      } else {
        t({kind:'error',title:'Failed to create custom provider'});
      }
    }).catch(function(){ setCpSaving(false); t({kind:'error',title:'Failed to create custom provider'}); });
  };

  var removeCustomProvider=function(id){
    NR_API.deleteCustomProvider(id).then(function(ok){
      if(ok){ setCustomProviders(function(cs){return cs.filter(function(c){return c.provider_id!==id;});}); t({kind:'info',title:'Custom provider removed'}); }
      else t({kind:'error',title:'Failed to remove custom provider'});
    });
  };

  /* enabled defaults to true when the provider has no status row (fail-open). */
  var isEnabled=function(prov){ return status[prov]!==false; };
  var toggleProvider=function(prov,next){
    setStatus(function(s){ var n=Object.assign({},s); n[prov]=next; return n; }); /* optimistic */
    NR_API.setProviderStatus(prov,next).then(function(ok){
      if(ok){ t({title:next?'Provider enabled':'Provider disabled'}); }
      else {
        setStatus(function(s){ var n=Object.assign({},s); n[prov]=!next; return n; }); /* revert */
        t({kind:'error',title:'Failed to update provider'});
      }
    }).catch(function(){
      setStatus(function(s){ var n=Object.assign({},s); n[prov]=!next; return n; });
      t({kind:'error',title:'Failed to update provider'});
    });
  };

  /* Opens the same modal used for "Add provider key", pre-scoped to an
     existing row so the admin can rotate its credential (e.g. add
     inference_profile to a Bedrock key) without re-typing everything.
     Submitting still calls the exact same POST /v1/admin/provider-keys the
     add flow uses — the backend already overwrites the keystore secret for
     that provider and hot-registers the new adapter on every call, so no new
     endpoint was needed, only a client-side entry point. The credential
     field always starts empty: it's a write-only secret, never echoed back,
     so there is nothing to pre-fill and no partial-update semantics — this
     is a full replace, matching what the user asked for ("update new keys"). */
  var openEditModal=function(k){
    setEditingProvider(k.provider);
    setProvider(k.provider);
    setLabel(k.label && k.label!==k.provider ? k.label : '');
    setKey('');
    setModal(true);
  };

  var addKey=function(){
    if(!key) return;
    setAdding(true);
    NR_API.addProviderKey(provider, key, label||provider).then(function(res){
      setAdding(false);
      if(res && !res.error) {
        if(editingProvider){
          // Reconcile in place rather than prepending — the row already
          // exists, so an optimistic insert would duplicate it until the
          // next full reload.
          setKeys(function(ks){return ks.map(function(row){
            return row.provider===editingProvider
              ? Object.assign({}, row, {label:label||row.label, masked:'••••'+key.slice(-4), status:'active'})
              : row;
          });});
          t({title:'Provider key updated'});
        } else {
          setKeys(function(ks){return [{provider:provider, label:label||provider, masked:'••••'+key.slice(-4), created_at:new Date().toISOString(), status:'active'}].concat(ks);});
          t({title:'Provider key added'});
        }
        setModal(false); setKey(''); setLabel(''); setEditingProvider(null);
      } else {
        t({kind:'error',title:editingProvider?'Failed to update key':'Failed to add key',msg:(res&&res.error&&res.error.message)||''});
      }
    }).catch(function(){ setAdding(false); t({kind:'error',title:editingProvider?'Failed to update key':'Failed to add key'}); });
  };

  if(loading) return <LoadingPage/>;

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc="Active provider credentials NeuroRoute routes through. Providers configured via environment (Anthropic key, Vertex service account) show as 'Environment'; keys added here are stored encrypted in the keystore.">
        <div className="row" style={{gap:8}}>
          <button className="btn btn-soft btn-sm" onClick={function(){setCpModal(true);}}><Icon name="plus" size={15}/>Add custom provider</button>
          <button className="btn btn-primary btn-sm" onClick={function(){setEditingProvider(null);setKey('');setLabel('');setModal(true);}}><Icon name="plus" size={15}/>Add provider key</button>
        </div>
      </PageHead>

      <SectionCard title="Custom OpenAI-compatible providers" sub="Config-driven providers (Mistral, Together, Fireworks, self-hosted, ...) — define once, then add its API key above to activate." pad={false}>
        {customProviders.length===0 ? <div className="empty" style={{padding:24}}><p className="faint">No custom providers yet. Click "Add custom provider" to register one (e.g. Mistral, Together AI, a self-hosted OpenAI-compatible server).</p></div> :
        <div className="scroll-x"><table className="table">
          <thead><tr><th>Provider ID</th><th>Base URL</th><th>Models</th><th>Status</th><th></th></tr></thead>
          <tbody>{customProviders.map(function(cp){
            return (
              <tr key={cp.provider_id}>
                <td><b style={{color:'var(--tx-0)'}}>{cp.display_name}</b><div className="faint mono" style={{fontSize:11}}>{cp.provider_id}</div></td>
                <td className="mono faint" style={{fontSize:12}}>{cp.base_url}</td>
                <td className="faint">{(cp.models||[]).length} model{(cp.models||[]).length===1?'':'s'}</td>
                <td><Badge color={cp.enabled?'green':'gray'} dot>{cp.enabled?'enabled':'disabled'}</Badge></td>
                <td style={{textAlign:'right'}}><button className="iconbtn" title="Delete" onClick={function(){removeCustomProvider(cp.provider_id);}}><Icon name="trash" size={14}/></button></td>
              </tr>
            );
          })}</tbody>
        </table></div>}
      </SectionCard>

      {loadFailed ? <EvalLoadError what="provider keys" onRetry={loadKeys}/> :
       keys.length===0 ?
        <EmptyState icon="key" title="No provider keys registered"
          desc="Add your provider API keys (OpenAI, Anthropic, Google, etc.) so NeuroRoute can route requests through them."
          actions={[{label:'Add Provider Key', onClick:function(){setEditingProvider(null);setKey('');setLabel('');setModal(true);}, primary:true}]}/> :
        <SectionCard title="Provider keys" sub={keys.length+' active'} pad={false}>
          <div className="scroll-x"><table className="table">
            <thead><tr><th>Provider</th><th>Models</th><th>Credential</th><th>Source</th><th>Status</th><th>Enabled</th><th></th></tr></thead>
            <tbody>{keys.map(function(k,i){
              var prov = window.DATA.providerById(k.provider||'');
              // Trust the API's explicit source when present (keystore-backed
              // adapter rows have no key preview, so the metadata heuristic alone
              // would wrongly label them "environment"); fall back to the heuristic
              // only for rows without a source (e.g. optimistic add).
              var isEnv = k.source ? (k.source==='environment') : (!k.masked && !k.key_preview && !k.created_at);
              var models = (k.model_count!=null) ? (k.model_count+' model'+(k.model_count===1?'':'s')) : (k.label||'—');
              var enabled = isEnabled(k.provider);
              return (
                <tr key={i} style={enabled?null:{opacity:.55}}>
                  <td><span className="row" style={{gap:8}}><ProviderLogo id={k.provider} size={24}/>{prov.name||k.provider}</span></td>
                  <td className="faint">{models}</td>
                  <td className="mono faint" style={{fontSize:12}}>{isEnv ? 'Managed via environment' : (k.masked||k.key_preview||'••••••')}</td>
                  <td><Badge color={isEnv?'blue':'purple'}>{isEnv?'Environment':'Keystore'}</Badge></td>
                  <td>{enabled
                    ? <Badge color={(k.status||'active')==='active'?'green':'amber'} dot>{k.status||'active'}</Badge>
                    : <Badge color="gray" dot>disabled</Badge>}</td>
                  <td><Switch on={enabled} onChange={function(next){toggleProvider(k.provider,next);}}/></td>
                  <td style={{textAlign:'right'}}>
                    {/* Environment-configured credentials (env vars / service
                        account) have nothing in the keystore to overwrite —
                        editing here would only create a keystore row that
                        confusingly coexists with the env one. Keystore rows
                        rotate freely: submitting always overwrites this
                        provider's secret and hot-registers immediately. */}
                    {!isEnv && <button className="iconbtn" title="Edit / rotate key" onClick={function(){openEditModal(k);}}><Icon name="edit" size={14}/></button>}
                  </td>
                </tr>
              );
            })}</tbody>
          </table></div>
        </SectionCard>
      }

      {modal && <Modal title={editingProvider?'Edit provider key':'Add provider key'}
        sub={editingProvider?'This replaces the stored credential for this provider — enter the new key below.':undefined}
        onClose={function(){setModal(false);setEditingProvider(null);}}
        footer={<><button className="btn btn-ghost" onClick={function(){setModal(false);setEditingProvider(null);}}>Cancel</button><button className="btn btn-primary" disabled={!key||adding} onClick={addKey}>{adding?(editingProvider?'Updating…':'Adding…'):(editingProvider?'Update key':'Add key')}</button></>}>
        <div className="grid" style={{gap:16}}>
          <Field label="Provider"><select className="select" value={provider} disabled={!!editingProvider} onChange={function(e){setProvider(e.target.value);}}>
            {window.DATA.PROVIDERS.map(function(p){return <option key={p.id} value={p.id}>{p.name}</option>;})}
            {customProviders.map(function(cp){return <option key={cp.provider_id} value={cp.provider_id}>{cp.display_name} (custom)</option>;})}
          </select></Field>
          <Field label="Label" hint="Optional — to distinguish multiple keys per provider."><input className="input" placeholder="e.g. Production" value={label} onChange={function(e){setLabel(e.target.value);}}/></Field>
          {/* Bedrock is the one provider whose "API key" isn't a plain secret —
              it's either a Bedrock API key or a JSON SigV4 blob — so the
              generic sk-... placeholder actively misleads there. */}
          {provider==='bedrock' ? (
            <Field label="Bedrock credentials"
              hint="Paste the Bedrock API key straight from the AWS console, or a JSON blob: {&quot;api_key&quot;:&quot;ABSK…&quot;,&quot;region&quot;:&quot;…&quot;} or {&quot;access_key_id&quot;:&quot;AKIA…&quot;,&quot;secret_access_key&quot;:&quot;…&quot;,&quot;region&quot;:&quot;…&quot;}. Region defaults to us-east-1. If a request fails with 'on-demand throughput isn't supported... retry with an inference profile', add &quot;inference_profile&quot;:&quot;global&quot; to the JSON blob (also accepts us/eu/au/jp) — check the model's AWS Bedrock docs page for which regions need this. Use straight quotes — smart quotes will be rejected.">
              <textarea className="textarea input-mono" style={{minHeight:70}} placeholder="ABSK…  or  {&quot;api_key&quot;:&quot;ABSK…&quot;,&quot;region&quot;:&quot;ap-south-1&quot;,&quot;inference_profile&quot;:&quot;global&quot;}"
                value={key} onChange={function(e){setKey(e.target.value);}}/>
            </Field>
          ) : (
            <Field label="API key"><input className="input input-mono" placeholder="sk-..." value={key} onChange={function(e){setKey(e.target.value);}}/></Field>
          )}
        </div>
      </Modal>}

      {cpModal && <Modal title="Add custom provider" sub="Any OpenAI-wire-format API — define the base URL and model list, then add its API key above to activate."
        onClose={function(){setCpModal(false);}}
        footer={<><button className="btn btn-ghost" onClick={function(){setCpModal(false);}}>Cancel</button><button className="btn btn-primary" disabled={!cpId||!cpBaseUrl||cpSaving} onClick={addCustomProvider}>{cpSaving?'Creating…':'Create provider'}</button></>}>
        <div className="grid" style={{gap:16}}>
          <Field label="Provider ID" hint="Lowercase, must start with custom- , e.g. custom-mistral">
            <input className="input mono" placeholder="custom-mistral" value={cpId} onChange={function(e){setCpId(e.target.value.toLowerCase());}}/>
          </Field>
          <Field label="Display name"><input className="input" placeholder="Mistral AI" value={cpName} onChange={function(e){setCpName(e.target.value);}}/></Field>
          <Field label="Base URL" hint="No trailing slash, no /chat/completions suffix.">
            <input className="input mono" placeholder="https://api.mistral.ai/v1" value={cpBaseUrl} onChange={function(e){setCpBaseUrl(e.target.value);}}/>
          </Field>
          <Field label="Models (JSON array)" hint="One object per model: id, display_name, input_per_1k_tokens, output_per_1k_tokens, max_context_tokens, supports_vision, supports_tools.">
            <textarea className="input mono" rows={10} style={{fontSize:11.5}} value={cpModelsJSON} onChange={function(e){setCpModelsJSON(e.target.value);}}/>
          </Field>
        </div>
      </Modal>}
    </div>
  );
}

/* =====================  PRICING  ===================== */
/* =====================  PLATFORM BURN LIMITS (SISL global guardrail)  ===================== */
function PlatformBudgetCard(){
  var t=useToast();
  var _st=useState(null); var st=_st[0], setSt=_st[1];
  var _d=useState(''); var d=_d[0], setD=_d[1];
  var _m=useState(''); var m=_m[0], setM=_m[1];
  var _saving=useState(false); var saving=_saving[0], setSaving=_saving[1];
  useEffect(function(){
    NR_API.platformBudget().then(function(res){
      if(!res) return;
      setSt({spendDay:res.spend_today_usd||0, spendMonth:res.spend_month_usd||0});
      setD(res.daily_budget_usd!=null?String(res.daily_budget_usd):'');
      setM(res.monthly_budget_usd!=null?String(res.monthly_budget_usd):'');
    });
  },[]);
  var save=function(){
    setSaving(true);
    NR_API.setPlatformBudget(parseFloat(d)||0, parseFloat(m)||0).then(function(ok){
      setSaving(false);
      if(ok) t({title:'Platform burn limits saved'}); else t({kind:'error', title:'Failed to save limits'});
    }).catch(function(){ setSaving(false); t({kind:'error', title:'Failed to save limits'}); });
  };
  return (
    <SectionCard title="Platform burn limits" sub="Global hard caps on SISL's total provider spend across ALL customers. When hit, every request is rejected with a generic capacity message (no $ figures leak). Blank = unlimited.">
      <div className="grid" style={{gap:12}}>
        {st && <div className="faint" style={{fontSize:12.5}}>Current burn: ${st.spendDay.toFixed(2)} today · ${st.spendMonth.toFixed(2)} this month</div>}
        <div className="row" style={{gap:10}}>
          <Field label="Daily cap (USD)"><input className="input" type="number" min="0" step="10" placeholder="Unlimited" value={d} onChange={function(e){setD(e.target.value);}}/></Field>
          <Field label="Monthly cap (USD)"><input className="input" type="number" min="0" step="50" placeholder="Unlimited" value={m} onChange={function(e){setM(e.target.value);}}/></Field>
        </div>
        <div><button className="btn btn-primary" disabled={saving} onClick={save}>{saving?'Saving…':'Save platform limits'}</button></div>
      </div>
    </SectionCard>
  );
}

/* =====================  EMAIL GATEWAY (Microsoft Graph / M365)  ===================== */
/* Platform-admin config for outbound email (invitations, onboarding welcome).
   Secret is write-only (stored server-side in the keystore, never returned). */
function GraphEmailGatewayCard(){
  var t=useToast();
  var _tenant=useState(''); var tenant=_tenant[0], setTenant=_tenant[1];
  var _client=useState(''); var client=_client[0], setClient=_client[1];
  var _secret=useState(''); var secret=_secret[0], setSecret=_secret[1];
  var _showSecret=useState(false); var showSecret=_showSecret[0], setShowSecret=_showSecret[1];
  var _mailbox=useState(''); var mailbox=_mailbox[0], setMailbox=_mailbox[1];
  var _enabled=useState(false); var enabled=_enabled[0], setEnabled=_enabled[1];
  var _secretSet=useState(false); var secretSet=_secretSet[0], setSecretSet=_secretSet[1];
  var _configured=useState(false); var configured=_configured[0], setConfigured=_configured[1];
  var _updatedAt=useState(''); var updatedAt=_updatedAt[0], setUpdatedAt=_updatedAt[1];
  var _saving=useState(false); var saving=_saving[0], setSaving=_saving[1];
  var _testing=useState(false); var testing=_testing[0], setTesting=_testing[1];
  var _test=useState(null); var test=_test[0], setTest=_test[1];

  useEffect(function(){
    NR_API.getEmailGateway().then(function(res){
      if(!res) return;
      setConfigured(!!res.configured); setSecretSet(!!res.secret_set);
      setTenant(res.tenant_id||''); setClient(res.client_id||'');
      setMailbox(res.mailbox||''); setEnabled(!!res.enabled);
      setUpdatedAt(res.updated_at||'');
    }).catch(function(){});
  },[]);

  var save=function(){
    setSaving(true);
    var body={tenant_id:tenant.trim(), client_id:client.trim(), mailbox:mailbox.trim(), enabled:enabled};
    if(secret) body.client_secret=secret;
    NR_API.setEmailGateway(body).then(function(res){
      setSaving(false);
      if(res.ok){ t({title:'Email gateway saved'}); if(secret){setSecretSet(true); setSecret('');} setConfigured(true); }
      else t({kind:'error', title:'Failed to save', msg:(res.data&&res.data.error)||''});
    }).catch(function(){ setSaving(false); t({kind:'error', title:'Failed to save email gateway'}); });
  };

  var runTest=function(){
    setTesting(true); setTest(null);
    NR_API.testEmailGateway().then(function(res){
      setTesting(false);
      setTest(res||{ok:false, detail:'no response'});
      if(res&&res.ok) t({title:'Test email sent', msg:'Check '+(res.sent_to||'the mailbox')});
      else t({kind:'error', title:'Test failed', msg:(res&&res.detail)||''});
    }).catch(function(){ setTesting(false); setTest({ok:false, detail:'request failed'}); t({kind:'error', title:'Test failed'}); });
  };

  return (
    <SectionCard title={<span className="row" style={{gap:9}}><Icon name="onboard" size={17}/>Graph API (M365)</span>}
      sub="Outbound email via Microsoft Graph (app-only). Used for team invitations and customer-onboarding welcome emails."
      action={<Badge color={enabled?'green':'gray'} dot>{enabled?'Enabled':'Disabled'}</Badge>}>
      <div className="grid" style={{gap:14}}>
        <div className="grid" style={{gridTemplateColumns:'1fr 1fr',gap:14}}>
          <Field label="Azure AD Tenant ID"><input className="input mono" placeholder="00000000-0000-0000-0000-000000000000" value={tenant} onChange={function(e){setTenant(e.target.value);}}/></Field>
          <Field label="Client ID"><input className="input mono" placeholder="Application (client) ID" value={client} onChange={function(e){setClient(e.target.value);}}/></Field>
        </div>
        <div className="grid" style={{gridTemplateColumns:'1fr 1fr',gap:14}}>
          <Field label="Client Secret" hint={secretSet?'Stored — leave blank to keep it.':'App registration client secret.'}>
            <div className="row" style={{gap:8}}>
              <input className="input mono" style={{flex:1}} type={showSecret?'text':'password'} placeholder={secretSet?'••••••••••':'client secret'} value={secret} onChange={function(e){setSecret(e.target.value);}}/>
              <button type="button" className="iconbtn" title={showSecret?'Hide':'Show'} onClick={function(){setShowSecret(!showSecret);}}><Icon name="eye" size={15}/></button>
            </div>
          </Field>
          <Field label="Mailbox" hint="The M365 user/shared mailbox to send as (UPN).">
            <input className="input mono" placeholder="tech.support@yourdomain.com" value={mailbox} onChange={function(e){setMailbox(e.target.value);}}/>
          </Field>
        </div>
        <div className="spread" style={{padding:'2px 0'}}>
          <div><div style={{fontWeight:600}}>Enabled</div><div className="faint" style={{fontSize:12.5}}>When on, all NeuroRoute email sends via Graph (falls back to env config when off).</div></div>
          <Switch on={enabled} onChange={setEnabled}/>
        </div>
        <div className="row" style={{gap:10}}>
          <button className="btn btn-primary" disabled={saving} onClick={save}>{saving?'Saving…':'Save'}</button>
          <button className="btn btn-soft" disabled={testing||!configured} onClick={runTest}><Icon name="onboard" size={15}/>{testing?'Testing…':'Test Connection'}</button>
        </div>
        {test && <div className="card card-pad" style={{fontSize:12.5, background:test.ok?'var(--green-t)':'var(--red-t)', borderColor:test.ok?'rgba(34,197,94,.3)':'rgba(239,68,68,.3)'}}>
          <span className="row" style={{gap:8}}><Icon name={test.ok?'check':'alert'} size={15} style={{color:test.ok?'var(--green)':'var(--red)'}}/>
          {test.ok? ('Test email sent to '+(test.sent_to||mailbox)+' — check that mailbox.') : ('Test failed: '+(test.detail||'unknown error'))}</span>
        </div>}
        {updatedAt && <div className="faint" style={{fontSize:11.5}}>Last updated {timeAgo(updatedAt)||updatedAt}</div>}
      </div>
    </SectionCard>
  );
}

/* ----- Provider-cost reconciliation (Pricing page) -----
   Recorded provider_cost per provider/model for a month vs the ACTUAL amount
   the provider invoiced (admin-entered). Drift = mispriced rates or unbilled
   token classes = platform loss, caught here instead of in the P&L. */
function ReconciliationCard(){
  var t=useToast();
  var _month=useState(new Date().toISOString().slice(0,7)); var month=_month[0], setMonth=_month[1];
  var _report=useState(null); var report=_report[0], setReport=_report[1];
  var _expanded=useState({}); var expanded=_expanded[0], setExpanded=_expanded[1];
  var _invEdits=useState({}); var invEdits=_invEdits[0], setInvEdits=_invEdits[1]; // provider -> input value
  var _saving=useState(''); var saving=_saving[0], setSaving=_saving[1];

  var load=function(m){
    NR_API.reconciliation(m).then(function(res){ setReport(res||null); }).catch(function(){ setReport(null); });
  };
  useEffect(function(){ load(month); },[month]);

  var saveInvoice=function(provider){
    var v = parseFloat(invEdits[provider]);
    if(isNaN(v) || v<0){ t({kind:'error',title:'Enter a valid invoice amount'}); return; }
    setSaving(provider);
    NR_API.setProviderInvoice(month, provider, v).then(function(res){
      setSaving('');
      if(res){ t({title:'Invoice saved',msg:provider+' · '+month}); load(month); }
      else t({kind:'error',title:'Failed to save invoice'});
    }).catch(function(){ setSaving(''); t({kind:'error',title:'Failed to save invoice'}); });
  };

  var providers = (report&&report.providers)||[];
  return (
    <SectionCard title="Provider-cost reconciliation"
      sub="What we recorded (and bill customers) vs what each provider actually invoiced us. A positive delta means we under-recorded — a platform loss to investigate."
      action={<input className="input" type="month" style={{width:150}} value={month} onChange={function(e){ if(e.target.value) setMonth(e.target.value); }}/>}
      pad={false}>
      {providers.length===0 ? <div className="empty" style={{padding:28}}><p className="faint">No provider usage recorded for {month}.</p></div> :
        <div className="scroll-x"><table className="table">
          <thead><tr><th></th><th>Provider</th><th style={{textAlign:'right'}}>Requests</th><th style={{textAlign:'right'}}>Input tok</th><th style={{textAlign:'right'}}>Output tok</th><th style={{textAlign:'right'}}>Recorded</th><th style={{textAlign:'right'}}>Invoiced</th><th style={{textAlign:'right'}}>Δ (inv−rec)</th><th></th></tr></thead>
          <tbody>{providers.map(function(p){
            var deltaColor = p.delta_usd==null ? undefined : (p.delta_usd>0.01 ? 'var(--red)' : (p.delta_usd < -0.01 ? 'var(--amber)' : 'var(--green)'));
            var rows=[(
              <tr key={p.provider} style={{fontWeight:600}}>
                <td style={{width:26,cursor:'pointer'}} onClick={function(){setExpanded(function(x){var y=Object.assign({},x); y[p.provider]=!y[p.provider]; return y;});}}>
                  <span className="faint">{expanded[p.provider]?'▾':'▸'}</span>
                </td>
                <td style={{color:'var(--tx-0)'}}>{p.provider}</td>
                <td className="num" style={{textAlign:'right'}}>{fmtNum(p.requests)}</td>
                <td className="num" style={{textAlign:'right'}}>{fmtNum(p.input_tokens)}</td>
                <td className="num" style={{textAlign:'right'}}>{fmtNum(p.output_tokens)}</td>
                <td className="num" style={{textAlign:'right'}}>{fmtUSD(p.recorded_cost_usd,4)}</td>
                <td style={{textAlign:'right'}}>
                  <input className="input" style={{width:100,textAlign:'right'}} type="number" min="0" step="0.01"
                    placeholder={p.invoice_usd!=null?p.invoice_usd.toFixed(2):'—'}
                    value={invEdits[p.provider]!=null?invEdits[p.provider]:''}
                    onChange={function(e){var v=e.target.value; setInvEdits(function(x){var y=Object.assign({},x); y[p.provider]=v; return y;});}}/>
                </td>
                <td className="num" style={{textAlign:'right',color:deltaColor}}>{p.delta_usd!=null?fmtUSD(p.delta_usd,4):'—'}</td>
                <td style={{textAlign:'right'}}><button className="btn btn-soft btn-sm" disabled={saving===p.provider||invEdits[p.provider]==null||invEdits[p.provider]===''} onClick={function(){saveInvoice(p.provider);}}>{saving===p.provider?'Saving…':'Save'}</button></td>
              </tr>
            )];
            if(expanded[p.provider]) (p.models||[]).forEach(function(m){
              rows.push(
                <tr key={p.provider+'/'+m.model}>
                  <td></td>
                  <td className="mono faint" style={{fontSize:12,paddingLeft:18}}>{m.model}</td>
                  <td className="num faint" style={{textAlign:'right'}}>{fmtNum(m.requests)}</td>
                  <td className="num faint" style={{textAlign:'right'}}>{fmtNum(m.input_tokens)}</td>
                  <td className="num faint" style={{textAlign:'right'}}>{fmtNum(m.output_tokens)}</td>
                  <td className="num faint" style={{textAlign:'right'}}>{fmtUSD(m.recorded_cost_usd,4)}</td>
                  <td></td><td></td><td></td>
                </tr>
              );
            });
            return rows;
          })}</tbody>
        </table></div>}
    </SectionCard>
  );
}

// Derived from window.DATA.TASKS (data.js) — the single canonical task-type
// list mirroring internal/router/classifier.go's TaskType constants. This
// used to be its own hand-maintained 11-entry array+label-map here, plus a
// second near-identical ROUTING_TASK_TYPES ~400 lines below with the SAME
// ids but different display labels — two copies of the same taxonomy
// drifting against each other. Both now read off DATA.TASKS.
var CATALOG_TASK_TYPES = window.DATA.TASKS.map(function(t){return t.id;});
var CATALOG_TASK_LABELS = (function(){
  var m={}; window.DATA.TASKS.forEach(function(t){ m[t.id]=t.label; }); return m;
})();

function PricingPage(){
  var t=useToast();
  var _loading=useState(true); var loading=_loading[0], setLoading=_loading[1];
  var _models=useState([]); var models=_models[0], setModels=_models[1];
  var _catalog=useState([]); var catalog=_catalog[0], setCatalog=_catalog[1];
  /* addQuality: model_id → {taskType, quality, saving} — tracks the "add" form per model row */
  var _addQuality=useState({}); var addQuality=_addQuality[0], setAddQuality=_addQuality[1];
  /* deletingKey: model_id+':'+task_type — tracks in-flight deletes */
  var _deletingKey=useState(''); var deletingKey=_deletingKey[0], setDeletingKey=_deletingKey[1];
  /* editDN: model_id → {value, saving} — tracks inline display-name editing */
  var _editDN=useState({}); var editDN=_editDN[0], setEditDN=_editDN[1];
  /* openAdd: model_id → true while that row's add-override form is expanded.
     The form is per-row and collapsed by default; rendering it unconditionally
     put a dropdown + number field + button on all 33 catalog rows. */
  var _openAdd=useState({}); var openAdd=_openAdd[0], setOpenAdd=_openAdd[1];
  /* catFilter: substring match over model id + display name, so a 33-model
     catalog is navigable without scrolling past everything. */
  var _catFilter=useState(''); var catFilter=_catFilter[0], setCatFilter=_catFilter[1];
  /* editOv: 'model_id:task_type' → in-progress score string, for click-to-edit
     on an existing override. Without this the only way to change a score was
     delete-then-re-add, and on a model with every task type already scored the
     add control is (correctly) absent — leaving existing scores uneditable. */
  var _editOv=useState({}); var editOv=_editOv[0], setEditOv=_editOv[1];

  var loadCatalog=function(){
    NR_API.modelCatalog().then(function(res){
      setCatalog((res&&res.models)||[]);
    }).catch(function(){});
  };

  useEffect(function(){
    NR_API.models().then(function(res){
      var list = (res && (res.data||res.models||res)) || [];
      list = list.filter(function(m){ return (m.id||m) !== 'auto'; });
      setModels(list.map(function(m){
        var info = window.DATA.modelById(m.id);
        // Carry the live provider through when the catalog has no row for this
        // model (hydration unavailable): Object.assign alone kept info's
        // synthetic provider:'unknown' and threw away the owned_by we just
        // fetched, so the Provider column read "unknown" for a model whose
        // provider was right there in the response.
        var out = Object.assign({}, info, {id:m.id});
        if(info.unknown && m.owned_by) out.provider = m.owned_by;
        return out;
      }));
      setLoading(false);
    }).catch(function(){
      setModels(window.DATA.MODELS);
      setLoading(false);
    });
    loadCatalog();
  },[]);

  var startEditDN=function(modelId,cur){
    setEditDN(function(p){return Object.assign({},p,{[modelId]:{value:cur,saving:false}});});
  };
  var cancelEditDN=function(modelId){
    setEditDN(function(p){var n=Object.assign({},p);delete n[modelId];return n;});
  };
  var saveDisplayName=function(modelId){
    var ed=editDN[modelId];
    if(!ed||!ed.value.trim()){t({kind:'error',title:'Display name cannot be empty'});return;}
    setEditDN(function(p){return Object.assign({},p,{[modelId]:{value:ed.value,saving:true}});});
    NR_API.updateModelCatalog(modelId,ed.value.trim()).then(function(ok){
      cancelEditDN(modelId);
      if(ok){t({title:'Display name updated'});loadCatalog();}
      else t({kind:'error',title:'Failed to update display name'});
    }).catch(function(){cancelEditDN(modelId);t({kind:'error',title:'Failed to update display name'});});
  };

  var patchAdd=function(modelId, patch){
    setAddQuality(function(prev){
      var cur = prev[modelId] || {taskType:CATALOG_TASK_TYPES[0], quality:''};
      return Object.assign({},prev,{[modelId]:Object.assign({},cur,patch)});
    });
  };

  /* taskType is passed from the render closure so it always matches what the
     dropdown DISPLAYS (first unused task when untouched) — deriving it here
     from state alone would default to CATALOG_TASK_TYPES[0] and silently
     overwrite an existing code_generation override. */
  var addOverride=function(modelId, taskType){
    var s = addQuality[modelId] || {};
    var q = parseFloat(s.quality);
    if(isNaN(q)||q<0||q>1){ t({kind:'error',title:'Quality must be 0–1'}); return; }
    patchAdd(modelId,{saving:true});
    NR_API.setModelQuality(modelId, taskType, q).then(function(ok){
      patchAdd(modelId,{saving:false, quality:''});
      if(ok){ t({title:'Quality override saved'}); loadCatalog(); }
      else t({kind:'error',title:'Failed to save quality override'});
    }).catch(function(){ patchAdd(modelId,{saving:false}); t({kind:'error',title:'Failed to save quality override'}); });
  };

  var cancelOverrideEdit=function(key){
    setEditOv(function(p){ var n=Object.assign({},p); delete n[key]; return n; });
  };

  /* Saves a click-to-edit score. PUT /v1/admin/model-quality is an upsert, so
     re-sending an existing (model, task_type) with a new value is the edit —
     no separate endpoint needed. Closes the editor on invalid input rather
     than trapping the user in a field they can't escape by clicking away. */
  var saveOverrideEdit=function(modelId, taskType){
    var key = modelId+':'+taskType;
    var raw = editOv[key];
    if(raw===undefined) return;
    var q = parseFloat(raw);
    if(isNaN(q)||q<0||q>1){
      t({kind:'error',title:'Quality must be 0–1'});
      cancelOverrideEdit(key);
      return;
    }
    cancelOverrideEdit(key);
    NR_API.setModelQuality(modelId, taskType, q).then(function(ok){
      if(ok){ t({title:'Quality updated'}); loadCatalog(); }
      else t({kind:'error',title:'Failed to update quality'});
    }).catch(function(){ t({kind:'error',title:'Failed to update quality'}); });
  };

  var deleteOverride=function(modelId, taskType){
    var key = modelId+':'+taskType;
    setDeletingKey(key);
    NR_API.deleteModelQuality(modelId, taskType).then(function(ok){
      setDeletingKey('');
      if(ok){ t({title:'Override removed'}); loadCatalog(); }
      else t({kind:'error',title:'Failed to remove override'});
    }).catch(function(){ setDeletingKey(''); t({kind:'error',title:'Failed to remove override'}); });
  };

  if(loading) return <LoadingPage/>;

  var catQ = catFilter.trim().toLowerCase();
  var visibleCatalog = catQ ? catalog.filter(function(c){
    return (c.model_id||'').toLowerCase().indexOf(catQ) >= 0 ||
           (c.display_name||'').toLowerCase().indexOf(catQ) >= 0;
  }) : catalog;

  return (
    <div className="grid" style={{gap:18}}>
      <PlatformBudgetCard/>
      <PageHead desc={'Platform pricing — '+models.length+' models registered. Cost per 1M tokens used for billing & savings calculations.'}/>
      <SectionCard title="Per-model pricing" pad={false}>
        <div className="scroll-x"><table className="table">
          <thead><tr><th>Model</th><th>Provider</th><th style={{textAlign:'right'}}>Input / 1M</th><th style={{textAlign:'right'}}>Output / 1M</th><th style={{textAlign:'right'}}>Quality</th><th style={{textAlign:'right'}}>Avg latency</th><th style={{textAlign:'right'}}>Context</th></tr></thead>
          <tbody>{models.map(function(m){return (
            <tr key={m.id}>
              <td><ModelCell id={m.id}/></td>
              <td className="faint">{window.DATA.providerById(m.provider).name}</td>
              {/* '—' when there is no rate for this model, not '$0.00' — on the
                  page that calls itself the billing basis, a zero reads as
                  "this model is free to run". */}
              <td className="num" style={{textAlign:'right'}}>{m.hasPricing===false?'—':'$'+(m.in||0).toFixed(2)}</td>
              <td className="num" style={{textAlign:'right'}}>{m.hasPricing===false?'—':'$'+(m.out||0).toFixed(2)}</td>
              <td className="num" style={{textAlign:'right'}}><Badge color={m.q>=95?'green':m.q>=85?'cyan':'gray'}>{m.q||'—'}</Badge></td>
              <td className="num" style={{textAlign:'right'}}>{m.lat?m.lat+'ms':'—'}</td>
              <td className="num faint" style={{textAlign:'right'}}>{m.ctx||'—'}</td>
            </tr>
          );})}</tbody>
        </table></div>
      </SectionCard>

      <SectionCard title="Model catalog — quality overrides"
        sub="Set routing quality scores (0–1) per task type without a deploy. Scores feed the router within 5 minutes. Each model can have a separate score for each task type."
        action={catalog.length>8 ? <input className="input input-sm" style={{width:190}} placeholder="Filter models…"
          value={catFilter} onChange={function(e){setCatFilter(e.target.value);}}/> : null}>
        {catalog.length===0 && <p className="faint" style={{padding:'12px 0'}}>No models in catalog yet — register a provider key to populate.</p>}
        {catalog.length>0 && visibleCatalog.length===0 &&
          <p className="faint" style={{padding:'12px 0'}}>No models match “{catFilter}”.</p>}
        {visibleCatalog.map(function(c){
          var ovs = c.quality_overrides || [];
          var usedTasks = ovs.map(function(q){return q.task_type;});
          var freeTasks = CATALOG_TASK_TYPES.filter(function(tt){return !usedTasks.includes(tt);});
          var add = addQuality[c.model_id] || {};
          /* Honor the user's dropdown pick only while that task is still free —
             after a successful add it becomes used and must not stick as the value. */
          var addTaskType = (add.taskType && freeTasks.includes(add.taskType)) ? add.taskType : (freeTasks[0] || CATALOG_TASK_TYPES[0]);
          var open = !!openAdd[c.model_id];
          return (
            <div key={c.model_id} className="drow" style={{flexWrap:'wrap'}}>
              {editDN[c.model_id] ? (
                <div style={{display:'flex',gap:6,alignItems:'center',flex:1,minWidth:0}}>
                  <input className="input input-sm" style={{flex:1,maxWidth:280}} autoFocus
                    value={editDN[c.model_id].value}
                    onChange={function(ev){var v=ev.target.value;setEditDN(function(p){var cur=p[c.model_id]||{};return Object.assign({},p,{[c.model_id]:{value:v,saving:!!cur.saving}});});}}
                    onKeyDown={function(ev){if(ev.key==='Enter')saveDisplayName(c.model_id);if(ev.key==='Escape')cancelEditDN(c.model_id);}}/>
                  <button className="btn btn-soft btn-xs" disabled={!!editDN[c.model_id].saving}
                    onClick={function(){saveDisplayName(c.model_id);}}>
                    {editDN[c.model_id].saving?'Saving…':'Save'}
                  </button>
                  <button className="btn btn-ghost btn-xs" onClick={function(){cancelEditDN(c.model_id);}}>Cancel</button>
                </div>
              ) : (
                <React.Fragment>
                  {/* Identity gets its own fixed grid track so the model name
                      stays legible no matter how many chips the row carries. */}
                  <div className="drow-id">
                    <ProviderLogo id={c.provider} size={20}/>
                    <span className="mono" style={{fontSize:12.5,fontWeight:600}}
                      title={c.model_id}>{c.display_name||c.model_id}</span>
                    <button className="iconbtn" style={{width:20,height:20,flex:'none'}} title="Rename"
                      onClick={function(){startEditDN(c.model_id,c.display_name||c.model_id);}}><Icon name="code" size={11}/></button>
                  </div>
                  {/* Overrides inline as wrapping chips instead of one
                      full-width row each — a fully-covered model was 11 rows. */}
                  <div className="drow-chips">
                    {ovs.map(function(ov){
                      var key = c.model_id+':'+ov.task_type;
                      var ed = editOv[key];
                      return (
                        <span key={ov.task_type} className="qchip"
                          title={(CATALOG_TASK_LABELS[ov.task_type]||ov.task_type)+' — click the score to edit'}>
                          {CATALOG_TASK_LABELS[ov.task_type]||ov.task_type}
                          {ed !== undefined ? (
                            /* Click-to-edit the score. Previously the only way
                               to change a value was to delete the override and
                               re-add it — and on a fully-covered model there was
                               no add control at all (no free task types left),
                               so an existing score was effectively read-only. */
                            <input type="number" min="0" max="1" step="0.01" autoFocus value={ed}
                              onChange={function(ev){var v=ev.target.value;setEditOv(function(p){return Object.assign({},p,{[key]:v});});}}
                              onKeyDown={function(ev){
                                if(ev.key==='Enter') saveOverrideEdit(c.model_id, ov.task_type);
                                if(ev.key==='Escape') cancelOverrideEdit(key);
                              }}
                              onBlur={function(){saveOverrideEdit(c.model_id, ov.task_type);}}/>
                          ) : (
                            <b title="Click to edit"
                              onClick={function(){setEditOv(function(p){return Object.assign({},p,{[key]:(ov.quality||0).toFixed(2)});});}}>
                              {(ov.quality||0).toFixed(2)}
                            </b>
                          )}
                          <button title="Remove override" disabled={deletingKey===key}
                            onClick={function(){deleteOverride(c.model_id,ov.task_type);}}>
                            {deletingKey===key?'…':<Icon name="x" size={10}/>}
                          </button>
                        </span>
                      );
                    })}
                    {ovs.length===0 && <span className="faint" style={{fontSize:11.5}}>code defaults</span>}
                  </div>
                </React.Fragment>
              )}
              {/* Trailing action track. The add control used to render for
                  EVERY model whether or not you were editing it — a dropdown +
                  number field + button per row, ~1600px of untouched form
                  across the catalog — so it's behind a per-model toggle. Its
                  own grid column keeps it reachable on a chip-heavy row. */}
              {!editDN[c.model_id] && (
                <div className="drow-act">
                  {freeTasks.length>0 && !open &&
                    <button className="btn btn-ghost btn-xs" title="Add a quality override"
                      onClick={function(){setOpenAdd(function(p){return Object.assign({},p,{[c.model_id]:true});});}}>
                      <Icon name="plus" size={11}/>
                    </button>}
                  {/* Every task type already has a score — nothing left to add.
                      Say so, rather than rendering nothing and leaving the row
                      looking like its controls failed to load. */}
                  {freeTasks.length===0 &&
                    <span className="faint" style={{fontSize:10.5,whiteSpace:'nowrap'}}>all set</span>}
                  {freeTasks.length>0 && open && (
                    <React.Fragment>
                      <select className="input input-sm select-sm" style={{width:150}} value={addTaskType}
                        onChange={function(ev){patchAdd(c.model_id,{taskType:ev.target.value});}}>
                        {freeTasks.map(function(tt){return <option key={tt} value={tt}>{CATALOG_TASK_LABELS[tt]||tt}</option>;})}
                      </select>
                      <input className="input input-sm" style={{width:70}} type="number" min="0" max="1" step="0.01"
                        placeholder="0.00" value={add.quality||''} autoFocus
                        onChange={function(ev){patchAdd(c.model_id,{quality:ev.target.value});}}
                        onKeyDown={function(ev){if(ev.key==='Enter')addOverride(c.model_id, addTaskType);}}/>
                      <button className="btn btn-soft btn-xs" disabled={!!add.saving}
                        onClick={function(){addOverride(c.model_id, addTaskType);}}>
                        {add.saving?'…':'Add'}
                      </button>
                      <button className="btn btn-ghost btn-xs"
                        onClick={function(){setOpenAdd(function(p){var n=Object.assign({},p);delete n[c.model_id];return n;});}}>Cancel</button>
                    </React.Fragment>
                  )}
                </div>
              )}
            </div>
          );
        })}
      </SectionCard>

      <ReconciliationCard/>
    </div>
  );
}

/* =====================  MODEL PERFORMANCE  ===================== */
function ModelPrefPage(){
  var _loading=useState(true); var loading=_loading[0], setLoading=_loading[1];
  var _stats=useState([]); var stats=_stats[0], setStats=_stats[1];
  var _filter=useState('All'); var filter=_filter[0], setFilter=_filter[1];
  // loadFailed distinguishes "both model-data fetches failed" from "they
  // succeeded and there are genuinely zero models" — before this, a real
  // outage rendered the same "No model data" empty state as a fresh install.
  var _loadFailed=useState(false); var loadFailed=_loadFailed[0], setLoadFailed=_loadFailed[1];

  var load=function(){
    setLoading(true); setLoadFailed(false);
    /* Pull live stats from /v1/dashboard/models and reference data from window.DATA */
    Promise.all([NR_API.dashboardModels(), NR_API.models()]).then(function(results){
      // Both are api()-backed: resolve null on any non-2xx status or network
      // failure, never reject. If BOTH failed, treat it as a real load
      // failure rather than falling through to the static-reference fallback
      // below (which would otherwise mask the outage as a normal, if stat-less, view).
      if(results[0]===null && results[1]===null){ setLoadFailed(true); setLoading(false); return; }
      var dashData = results[0];
      var modelsData = results[1];
      /* Build a model -> stats map */
      var statsByModel = {};
      var liveList = (dashData && dashData.models) || [];
      liveList.forEach(function(s){ statsByModel[s.model] = s; });
      /* Build the registered models list — drop the "auto" pseudo-model, it's a
         routing directive, not a real model with performance stats. */
      var registered = [];
      if(modelsData && (modelsData.data || modelsData.models)) {
        registered = modelsData.data || modelsData.models;
      } else {
        /* Fallback to reference data */
        registered = window.DATA.MODELS.map(function(m){return {id:m.id};});
      }
      registered = registered.filter(function(m){return m.id!=='auto';});
      /* Merge live stats with reference attributes */
      var merged = registered.map(function(m){
        var info = window.DATA.modelById(m.id);
        var live = statsByModel[m.id] || {};
        return {
          id: m.id,
          name: info.name || m.id,
          // info.unknown, not `info.provider ||` — modelById's synthetic row
          // sets provider to the STRING 'unknown', which is truthy, so the
          // `|| m.owned_by` fallback could never fire and the live provider
          // from /v1/models was fetched and then discarded.
          provider: (info.unknown ? (m.owned_by || 'unknown') : info.provider),
          quality: info.q || 0,
          qualityKnown: info.qKnown !== false && !!info.q,
          inputPrice: info.in || 0,
          outputPrice: info.out || 0,
          latency: live.avg_latency_ms || info.lat || 0,
          requests: live.request_count || 0,
          totalCost: live.total_cost || 0,
        };
      });
      setStats(merged);
      setLoading(false);
    }).catch(function(){ setLoadFailed(true); setLoading(false); });
  };
  useEffect(function(){ load(); },[]);

  if(loading) return <LoadingPage/>;

  var providerCounts = {};
  stats.forEach(function(s){
    var pid = s.provider;
    providerCounts[pid] = (providerCounts[pid]||0) + 1;
  });

  /* Build the provider chips from the providers actually PRESENT in the data,
     resolving each through providerById for its label/colour — not by filtering
     the known-provider list, which silently dropped any provider outside it.
     A model whose provider had no chip was reachable only via "All", and its
     count was missing from every chip while still being in the All total. */
  var visibleProviders = Object.keys(providerCounts).sort().map(function(pid){
    return window.DATA.providerById(pid);
  });
  /* Apply filter */
  var filtered = filter==='All' ? stats : stats.filter(function(s){return s.provider===filter;});

  var totalRequests = stats.reduce(function(a,b){return a+b.requests;},0);
  var totalSpend = stats.reduce(function(a,b){return a+b.totalCost;},0);

  /* Color helpers */
  var latColor = function(ms){
    if(!ms) return 'var(--tx-3)';
    if(ms < 200) return 'var(--green)';
    if(ms < 400) return 'var(--amber)';
    return 'var(--red)';
  };
  var fmtCost = function(v){
    if(v >= 1000) return '$'+(v/1000).toFixed(1)+'K';
    if(v >= 1) return '$'+v.toFixed(2);
    return '$'+v.toFixed(4);
  };
  var fmtReq = function(n){
    if(n >= 1000) return (n/1000).toFixed(1)+'K';
    return ''+n;
  };

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc={stats.length+' models · '+fmtNum(totalRequests)+' requests · '+fmtUSD(totalSpend,2)+' total spend (30d)'}/>

      {/* Provider filter tabs */}
      <div className="row" style={{gap:8,flexWrap:'wrap'}}>
        <button className={'chip'+(filter==='All'?' chip-active':'')} onClick={function(){setFilter('All');}}
          style={filter==='All'?{background:'var(--blue-t)',color:'var(--blue)',borderColor:'rgba(59,130,246,.4)'}:null}>
          <span className="statusdot" style={{background:filter==='All'?'var(--blue)':'var(--tx-3)'}}/>
          All <span className="faint">{stats.length}</span>
        </button>
        {visibleProviders.map(function(p){
          var active = filter===p.id;
          return (
            <button key={p.id} className="chip" onClick={function(){setFilter(p.id);}}
              style={active?{background:'rgba(59,130,246,.12)',color:'var(--tx-0)',borderColor:p.color}:null}>
              <span className="statusdot" style={{background:active?p.color:'var(--tx-3)'}}/>
              {p.name} <span className="faint">{providerCounts[p.id]||0}</span>
            </button>
          );
        })}
      </div>

      {loadFailed ? <EvalLoadError what="model performance data" onRetry={load}/> :
       filtered.length===0 ?
        <EmptyState icon="modelpref" title="No model data"
          desc="No requests have been routed through these models yet. Send a request via the Playground to see performance stats."/> :
        <div className="tilegrid">
          {filtered.map(function(m){
            var prov = window.DATA.providerById(m.provider);
            return (
              <div key={m.id} className="tile" style={{borderLeft:'2px solid '+prov.color}}
                title={m.id+' · '+prov.name}>
                <div className="tile-hd">
                  <ProviderLogo id={m.provider} size={22}/>
                  <span className="tile-name">{m.id}</span>
                </div>
                {/* The four numbers that were four stacked label+meter rows.
                    Latency keeps its colour coding; quality keeps a hairline
                    bar because a 0-100 score reads better with a rail. */}
                <div className="tile-stats">
                  <div className="tile-stat"><b style={{color:latColor(m.latency)}}>{m.latency?Math.round(m.latency)+'ms':'—'}</b><span>latency</span></div>
                  <div className="tile-stat"><b>{m.quality?m.quality.toFixed(0)+'%':'—'}</b><span>quality</span></div>
                  <div className="tile-stat"><b>{fmtReq(m.requests)}</b><span>reqs</span></div>
                  <div className="tile-stat"><b>{fmtCost(m.totalCost)}</b><span>cost</span></div>
                </div>
                {m.quality>0 && <div className="meter-xs"><span style={{width:Math.min(100,m.quality)+'%',background:'var(--blue)'}}/></div>}
              </div>
            );
          })}
        </div>
      }
    </div>
  );
}

/* =====================  HEALTH MONITOR  ===================== */
function HealthPage(){
  var _loading=useState(true); var loading=_loading[0], setLoading=_loading[1];
  var _healthData=useState(null); var healthData=_healthData[0], setHealthData=_healthData[1];
  var _modelStats=useState({}); var modelStats=_modelStats[0], setModelStats=_modelStats[1];
  var _routingDecisions=useState([]); var routingDecisions=_routingDecisions[0], setRoutingDecisions=_routingDecisions[1];
  var _lastRefresh=useState(null); var lastRefresh=_lastRefresh[0], setLastRefresh=_lastRefresh[1];
  var _expanded=useState({}); var expanded=_expanded[0], setExpanded=_expanded[1];
  // loadFailed distinguishes "the health fetches themselves failed" from
  // "they succeeded and there are genuinely zero providers" — before this,
  // a real outage rendered the same "No health data" empty state as a fresh
  // install with no providers registered.
  var _loadFailed=useState(false); var loadFailed=_loadFailed[0], setLoadFailed=_loadFailed[1];

  var refresh=function(){
    setLoading(true); setLoadFailed(false);
    Promise.all([NR_API.health(), NR_API.dashboardModels(), NR_API.dashboardRouting(100)]).then(function(results){
      // NR_API.health() never resolves null (it defaults to
      // {status:'unknown',providers:{}} on failure), so detect a real outage
      // via the other two calls, which are api()-backed and resolve null on
      // any non-2xx status or network failure.
      if(results[1]===null && results[2]===null){ setLoadFailed(true); setLoading(false); return; }
      setHealthData(results[0]);
      /* Build model stats map keyed by model id */
      var statMap = {};
      var liveList = (results[1] && results[1].models) || [];
      liveList.forEach(function(s){ statMap[s.model] = s; });
      setModelStats(statMap);
      setRoutingDecisions((results[2] && results[2].decisions) || []);
      setLastRefresh(new Date());
      setLoading(false);
    }).catch(function(){ setLoadFailed(true); setLoading(false); });
  };

  useEffect(function(){
    refresh();
    /* Auto-refresh every 60s */
    var iv = setInterval(refresh, 60000);
    return function(){ clearInterval(iv); };
  },[]);

  if(loading && !healthData) return <LoadingPage/>;

  /* Group health data by provider with real metrics */
  var groups = groupHealthByProvider(healthData).map(function(g){
    /* Aggregate per-provider metrics from model_stats */
    var totalLat = 0, latCount = 0, totalReqs = 0, totalErrors = 0;
    g.models.forEach(function(m){
      var stat = modelStats[m.id];
      if(stat) {
        if(stat.avg_latency_ms > 0) { totalLat += stat.avg_latency_ms; latCount++; }
        totalReqs += stat.request_count || 0;
        totalErrors += (stat.error_rate || 0) * (stat.request_count || 0);
      }
    });
    var avgLat = latCount>0 ? Math.round(totalLat/latCount) : 0;
    var successRate = totalReqs>0 ? Math.max(0, Math.min(100, 100 - (totalErrors/totalReqs)*100)) : 100;
    /* Score = blend of health (70%) + success (30%) */
    var healthScore = g.total>0 ? (g.healthy/g.total)*100 : 0;
    var score = Math.round(healthScore*0.7 + successRate*0.3);
    /* Build a synthetic 24-bucket latency sparkline from recent routing decisions for this provider */
    var providerLatencies = routingDecisions
      .filter(function(d){
        var mInfo = window.DATA.modelById(d.model||d.Model||'');
        return mInfo.provider === g.id;
      })
      .map(function(d){ return d.routing_latency_ms || d.RoutingLatencyMs || avgLat || 0; });
    /* If no real history, fall back to a flat line at avgLat (still real — not random) */
    var spark = providerLatencies.length >= 3 ? providerLatencies.slice(0,24).reverse() :
                (avgLat > 0 ? [avgLat,avgLat,avgLat,avgLat,avgLat,avgLat,avgLat,avgLat] : []);
    return Object.assign({}, g, {avgLat:avgLat, successRate:successRate, score:score, totalReqs:totalReqs, spark:spark});
  });

  var overallOk = groups.length>0 && groups.every(function(g){return g.status==='optimal';});
  var hasDegraded = groups.some(function(g){return g.status!=='optimal';});

  /* Build "Recent incidents" list from groups currently degraded */
  var incidents = groups.filter(function(g){return g.status!=='optimal';}).map(function(g){
    var downModels = g.models.filter(function(m){return m.status!=='ok';});
    return {
      provider: g,
      severity: g.status==='down' ? 'red' : 'amber',
      title: g.status==='down' ? 'Provider unavailable' : (downModels.length+' of '+g.total+' models failing health probe'),
      detail: downModels.map(function(m){return m.name;}).join(', ') || 'unknown',
    };
  });

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc={'Real-time provider health from background probes. Auto-refreshes every 60s.'}>
        <div className="row" style={{gap:10}}>
          {lastRefresh && <span className="faint" style={{fontSize:12}}>Updated {timeAgo(lastRefresh.toISOString())}</span>}
          <button className="btn btn-soft btn-sm" onClick={refresh} disabled={loading}><Icon name="refresh" size={15}/>{loading?'Refreshing…':'Refresh'}</button>
        </div>
      </PageHead>

      <div className="card card-pad" style={{background:overallOk?'var(--green-t)':'var(--amber-t)',borderColor:overallOk?'rgba(16,185,129,.3)':'rgba(245,158,11,.3)'}}>
        <div className="row" style={{gap:10}}>
          <Icon name={overallOk?'checkcircle':'alert'} size={20} style={{color:overallOk?'var(--green)':'var(--amber)'}}/>
          <b style={{fontSize:15}}>{overallOk ? 'All providers operational' : 'Some providers degraded'}</b>
          {healthData && healthData.status && <Badge color={healthData.status==='ok'?'green':'amber'}>{healthData.status}</Badge>}
          <span className="faint" style={{marginLeft:'auto',fontSize:12}}>
            {groups.length} provider{groups.length===1?'':'s'} · {groups.reduce(function(a,b){return a+b.healthy;},0)}/{groups.reduce(function(a,b){return a+b.total;},0)} models healthy
          </span>
        </div>
      </div>

      {loadFailed ? <EvalLoadError what="provider health data" onRetry={refresh}/> :
       groups.length===0 ?
        <EmptyState icon="health" title="No health data"
          desc="The health endpoint returned no provider information. Make sure models are registered and the health monitor is running."/> :
        <div className="cards-grid">
          {groups.map(function(g){
            var statusColor = g.status==='optimal'?'green':g.status==='degraded'?'amber':'red';
            var latColor = g.avgLat===0 ? 'var(--tx-3)' : g.avgLat<350 ? COL.green : g.avgLat<700 ? COL.amber : COL.red;
            var isExpanded = expanded[g.id];
            return (
              <div key={g.id} className="card" style={{borderLeft:'3px solid '+g.color,padding:'12px 14px'}}>
                {/* Header — provider, status and score on one line. The gauge
                    shrinks from 64px to 44px and the two bordered stat tiles
                    become inline label/value pairs: same four facts, roughly
                    half the card. */}
                <div className="spread" style={{gap:10}}>
                  <span className="row" style={{gap:9,minWidth:0}}>
                    <ProviderLogo id={g.id} size={28}/>
                    <div style={{minWidth:0}}>
                      <div style={{fontWeight:700,fontSize:13.5,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{g.name}</div>
                      <Badge color={statusColor} dot style={{fontSize:10.5}}>{g.status}</Badge>
                    </div>
                  </span>
                  {/* 52, not 44: Gauge auto-fits its label to the ring, and 44
                      squeezed "100%" down to ~11.7px — smaller than the provider
                      name beside it. 52 lands it at ~13.8px, costing +8px. */}
                  <Gauge value={g.score} size={52}/>
                </div>

                <div className="tile-stats" style={{marginTop:10}}>
                  <div className="tile-stat"><b style={{color:latColor}}>{g.avgLat>0 ? g.avgLat+'ms' : '—'}</b><span>avg latency</span></div>
                  <div className="tile-stat"><b style={{color: g.successRate>=99 ? COL.green : g.successRate>=95 ? COL.amber : COL.red}}>{g.successRate.toFixed(1)}%</b><span>success</span></div>
                  <div className="tile-stat"><b>{g.totalReqs>0 ? fmtNum(g.totalReqs) : '—'}</b><span>requests</span></div>
                  <div className="tile-stat"><b>{g.healthy}/{g.total}</b><span>healthy</span></div>
                </div>

                {/* Latency sparkline — its caption is now redundant with the
                    stats above, so only the trace remains. */}
                {g.spark.length>0 &&
                  <div style={{marginTop:8}}><Spark data={g.spark} color={latColor} w={280} h={26} fill={false}/></div>}

                {/* Model breakdown — a text toggle rather than a full-width
                    button bar, since it's secondary to the numbers above. */}
                <button className="btn btn-ghost btn-xs" style={{marginTop:8}} onClick={function(){
                  setExpanded(function(e){var n=Object.assign({},e); n[g.id]=!n[g.id]; return n;});
                }}>
                  <Icon name={isExpanded?'arrowup':'arrowdown'} size={11}/>
                  {isExpanded?'Hide':'Show'} {g.total} model{g.total===1?'':'s'}
                </button>

                {isExpanded && <div className="grid" style={{gap:3,marginTop:7}}>
                  {g.models.map(function(m){
                    var stat = modelStats[m.id];
                    return (
                      <div key={m.id} className="row" style={{gap:7,padding:'4px 8px',background:'var(--bg-inset)',borderRadius:6}}>
                        <span className="statusdot" style={{background:m.status==='ok'?'var(--green)':'var(--red)'}}/>
                        <span style={{fontSize:11.5,fontWeight:600,flex:1,minWidth:0,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}
                          title={m.name}>{m.name}</span>
                        {stat && stat.avg_latency_ms > 0 && <span className="mono faint" style={{fontSize:10.5}}>{Math.round(stat.avg_latency_ms)}ms</span>}
                        <span className="mono faint" style={{fontSize:10,width:26,textAlign:'right'}}>{m.status}</span>
                      </div>
                    );
                  })}
                </div>}
              </div>
            );
          })}
        </div>
      }

      {/* Recent incidents */}
      {incidents.length>0 && <SectionCard title="Active incidents" sub="Providers currently in a degraded state" pad={false}>
        <table className="table">
          <tbody>
            {incidents.map(function(inc,i){return (
              <tr key={i}>
                <td>
                  <span className="row" style={{gap:8}}>
                    <span className="statusdot" style={{background:'var(--'+inc.severity+')'}}/>
                    <b style={{color:'var(--tx-0)'}}>{inc.provider.name}</b>
                  </span>
                </td>
                <td><Badge color={inc.severity}>{inc.provider.status}</Badge></td>
                <td className="faint" style={{fontSize:13}}>{inc.title}</td>
                <td className="faint mono" style={{fontSize:12,textAlign:'right'}}>{inc.detail}</td>
              </tr>
            );})}
          </tbody>
        </table>
      </SectionCard>}

      {!hasDegraded && groups.length>0 && <div className="card card-pad" style={{background:'var(--bg-inset)',textAlign:'center'}}>
        <Icon name="checkcircle" size={20} style={{color:'var(--green)',marginRight:8}}/>
        <span className="faint" style={{fontSize:13}}>No active incidents · All probes returning <span className="mono">ok</span></span>
      </div>}
    </div>
  );
}

/* =====================  AUDIT LOG  ===================== */
function AuditPage(){
  var _loading=useState(true); var loading=_loading[0], setLoading=_loading[1];
  var _events=useState([]); var events=_events[0], setEvents=_events[1];
  // loadFailed distinguishes "the audit-log fetch itself failed" from "it
  // succeeded and there are genuinely zero audit events" — before this, both
  // collapsed into the same "No audit events yet" empty state.
  var _loadFailed=useState(false); var loadFailed=_loadFailed[0], setLoadFailed=_loadFailed[1];

  var load=function(){
    setLoading(true); setLoadFailed(false);
    // NR_API.auditLog() is api()-backed: resolves null on any non-2xx status
    // or network failure, never rejects — detect failure via the null result.
    NR_API.auditLog().then(function(res){
      if(res && Array.isArray(res.events||res)) {
        setEvents(res.events||res);
      } else {
        setLoadFailed(true);
      }
      setLoading(false);
    }).catch(function(){ setLoadFailed(true); setLoading(false); });
  };
  useEffect(function(){ load(); },[]);

  if(loading) return <LoadingPage/>;

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc="Immutable record of security-sensitive actions across the platform."/>

      {loadFailed ? <EvalLoadError what="audit events" onRetry={load}/> :
       events.length===0 ?
        <EmptyState icon="audit" title="No audit events yet"
          desc="Security-sensitive actions (key creation, role changes, config updates) will appear here automatically."/> :
        <SectionCard title="Audit log" sub={events.length+' events'} pad={false}>
          <div className="scroll-x"><table className="table">
            <thead><tr><th>Timestamp</th><th>Customer</th><th>Actor</th><th>Event</th><th>Resource</th><th>IP address</th></tr></thead>
            <tbody>{events.map(function(a){return <AuditRow key={a.id} a={a}/>;})}</tbody>
          </table></div>
        </SectionCard>
      }
    </div>
  );
}

/* =====================  LOGIN ACCESS (domain allowlist)  ===================== */
function AccessPage(){
  var t=useToast();
  var _loading=useState(true); var loading=_loading[0], setLoading=_loading[1];
  var _domains=useState([]); var domains=_domains[0], setDomains=_domains[1];
  var _input=useState(''); var input=_input[0], setInput=_input[1];
  var _adding=useState(false); var adding=_adding[0], setAdding=_adding[1];

  var load=function(){
    NR_API.allowedDomains().then(function(res){
      if(res && Array.isArray(res.domains)) setDomains(res.domains); else setDomains([]);
      setLoading(false);
    }).catch(function(){ setLoading(false); });
  };
  useEffect(load, []);

  var add=function(){
    var d=input.trim();
    if(!d) return;
    setAdding(true);
    NR_API.addAllowedDomain(d).then(function(res){
      setAdding(false);
      if(res && res.status==='allowed'){
        setInput('');
        t({title:'Domain whitelisted', msg:res.domain});
        load();
      } else {
        t({kind:'error', title:'Failed to add domain', msg:(res&&res.error)||'Enter a valid domain like acme.com'});
      }
    }).catch(function(){ setAdding(false); t({kind:'error', title:'Failed to add domain'}); });
  };

  var remove=function(domain){
    NR_API.removeAllowedDomain(domain).then(function(ok){
      if(ok){ setDomains(function(ds){return ds.filter(function(x){return x.domain!==domain;});}); t({kind:'info', title:'Domain removed', msg:domain}); }
      else t({kind:'error', title:'Failed to remove domain'});
    }).catch(function(){ t({kind:'error', title:'Failed to remove domain'}); });
  };

  if(loading) return <LoadingPage/>;

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc="Only users whose email domain is whitelisted here can sign in. Everyone else sees a &quot;Contact SISL CloudWorx Sales&quot; page. SISL admins always have access."/>

      <GraphEmailGatewayCard/>

      <SectionCard title="Add a domain" sub="e.g. acme.com — onboarding a customer auto-adds their domain.">
        <div className="row" style={{gap:10}}>
          <input className="input" style={{flex:1}} placeholder="acme.com or admin@acme.com" value={input}
            onChange={function(e){setInput(e.target.value);}} onKeyDown={function(e){if(e.key==='Enter') add();}}/>
          <button className="btn btn-primary" disabled={!input.trim()||adding} onClick={add}><Icon name="plus" size={15}/>{adding?'Adding…':'Whitelist domain'}</button>
        </div>
      </SectionCard>

      {domains.length===0 ?
        <div className="card card-pad" style={{background:'var(--amber-t)',borderColor:'rgba(245,158,11,.3)'}}>
          <div className="row" style={{gap:10}}><Icon name="alert" size={17} style={{color:'var(--amber)'}}/>
          <span><b>No domains whitelisted.</b> Only SISL admins can sign in right now. Add a customer's domain (or onboard them) to let them log in.</span></div>
        </div> :
        <SectionCard title="Whitelisted domains" sub={domains.length+' domain'+(domains.length===1?'':'s')+' may sign in'} pad={false}>
          <table className="table">
            <thead><tr><th>Domain</th><th>Added by</th><th>Added</th><th></th></tr></thead>
            <tbody>{domains.map(function(d){return (
              <tr key={d.domain}>
                <td style={{fontWeight:600,color:'var(--tx-0)'}}>@{d.domain}</td>
                <td className="mono faint" style={{fontSize:12}}>{d.added_by||'—'}</td>
                <td className="faint">{timeAgo(d.created_at)||'—'}</td>
                <td style={{textAlign:'right'}}><button className="btn btn-danger btn-sm" onClick={function(){remove(d.domain);}}>Remove</button></td>
              </tr>
            );})}</tbody>
          </table>
        </SectionCard>
      }
    </div>
  );
}

/* =====================  ROUTING RULES (hard task→model pins)  ===================== */
// Derived from window.DATA.TASKS — see CATALOG_TASK_TYPES above (same
// consolidation; this used to be a second, independently drifting copy of
// the same 11-id taxonomy with its own labels).
var ROUTING_TASK_TYPES = window.DATA.TASKS.map(function(t){return {v:t.id, label:t.label};});

/* Tier helper for cascade strategy suggestions — uses model ID patterns. */
var _modelTier=function(id){
  var lo=(id||'').toLowerCase();
  if(/opus|gpt-4o[^-]|gpt-4\.1[^-m]|gemini-2\.5-pro/.test(lo)) return 'premium';
  if(/sonnet|gpt-4o-mini|gpt-4\.1-mini|gemini-2\.5-flash|gemini-2\.0-flash|haiku|claude-haiku/.test(lo)) return 'standard';
  return 'budget';
};
var _suggestChain=function(strategy, models){
  var byTier={premium:[],standard:[],budget:[]};
  models.forEach(function(m){byTier[_modelTier(m.id)].push(m.id);});
  if(strategy==='quality') return [...byTier.premium.slice(0,2),...byTier.standard.slice(0,1),...byTier.budget.slice(0,1)].slice(0,4);
  if(strategy==='cost')    return [...byTier.budget.slice(0,2),...byTier.standard.slice(0,1),...byTier.premium.slice(0,1)].slice(0,4);
  /* failover: one model per "provider family" for provider diversity */
  var seen=new Set(); var chain=[];
  var provKey=function(id){return id.split('-')[0];};
  [...byTier.standard,...byTier.premium,...byTier.budget].forEach(function(id){
    var p=provKey(id);
    if(!seen.has(p)&&chain.length<4){seen.add(p);chain.push(id);}
  });
  return chain;
};

/* Single cascade-chain builder used per task row */
function CascadeBuilder(props){
  /* props: chain [string], models [{id,name}], onChange fn(newChain), onSuggest fn(strategy) */
  var chain=props.chain||[];
  var models=props.models||[];
  var LABELS=['Primary','Fallback 1','Fallback 2','Fallback 3'];
  var slotsToShow=Math.min(4, chain.length<4 ? chain.length+1 : 4);

  var setSlot=function(idx,val){
    var next=chain.slice();
    if(val) next[idx]=val; else next.splice(idx,1);
    // trim trailing empties
    while(next.length>0&&!next[next.length-1]) next.pop();
    props.onChange(next);
  };

  return (
    /* No top margin: the caller now owns the gap, and stacking both left a
       double gutter above every expanded cascade row. */
    <div>
      {/* Strategy suggestions */}
      <div className="row" style={{gap:6,marginBottom:8,flexWrap:'wrap'}}>
        <span style={{fontSize:11.5,color:'var(--tx-2)',marginRight:2}}>Quick setup:</span>
        {[
          {k:'quality',label:'Quality-first',title:'Best model first, cheaper fallbacks'},
          {k:'cost',   label:'Cost-optimized',title:'Cheapest first, escalate on failure'},
          {k:'failover',label:'HA Failover',title:'Same quality tier across different providers'},
        ].map(function(s){
          return (
            <button key={s.k} className="btn btn-xs btn-outline" title={s.title}
              onClick={function(){props.onSuggest(s.k);}}>
              {s.label}
            </button>
          );
        })}
      </div>
      {/* Chain slots */}
      <div className="grid" style={{gap:4}}>
        {Array.from({length:slotsToShow},function(_,i){
          var val=chain[i]||'';
          return (
            <div key={i} className="row" style={{gap:6,alignItems:'center'}}>
              {i>0 && <span style={{fontSize:11,color:'var(--tx-3)',marginLeft:2}}>↳</span>}
              {i===0 && <span style={{fontSize:11,color:'var(--tx-3)',minWidth:8}}/>}
              <span style={{fontSize:11.5,fontWeight:600,color:'var(--tx-2)',minWidth:66}}>{LABELS[i]}</span>
              <select className="select select-sm" style={{minWidth:220,flex:1,maxWidth:420}} value={val}
                onChange={function(e){setSlot(i,e.target.value);}}>
                <option value="">{i===0?'— choose primary model —':'— none (stop here) —'}</option>
                {models.map(function(m){
                  var used=chain.indexOf(m.id)>=0&&chain.indexOf(m.id)!==i;
                  return <option key={m.id} value={m.id} disabled={used}>{m.name}{used?' (already in chain)':''}</option>;
                })}
              </select>
              {val&&<button className="btn btn-xs btn-ghost" title="Remove this slot"
                onClick={function(){setSlot(i,'');}} style={{color:'var(--red)',padding:'2px 6px'}}>✕</button>}
            </div>
          );
        })}
      </div>
      {chain.length===0&&<div className="faint" style={{fontSize:12,marginTop:4}}>Pick a primary model above or use a Quick setup strategy.</div>}
      {chain.length>0&&(
        <div className="faint" style={{fontSize:11,marginTop:8}}>
          Cascade order: {chain.map(function(id,i){return <span key={id}>{i>0&&<span style={{margin:'0 3px'}}>→</span>}<code style={{fontSize:11}}>{id}</code></span>;})}
        </div>
      )}
    </div>
  );
}

function RoutingRulesPage(){
  var t=useToast();
  var _loading=useState(true); var loading=_loading[0], setLoading=_loading[1];
  /* rules: task_type -> {mode:'auto'|'cascade', chain:[modelId,...], dirty:bool} */
  var _rules=useState({}); var rules=_rules[0], setRules=_rules[1];
  var _models=useState([]); var models=_models[0], setModels=_models[1];
  var _pii=useState('auto'); var pii=_pii[0], setPii=_pii[1];
  var _saving=useState({}); var saving=_saving[0], setSaving=_saving[1];
  /* orgOverrides: customer-authored org-scoped rules (read-only here — they're
     managed by each org's own admins on their Routing Rules page). */
  var _orgOverrides=useState([]); var orgOverrides=_orgOverrides[0], setOrgOverrides=_orgOverrides[1];

  var load=function(){
    Promise.all([NR_API.routingRules(), NR_API.models()]).then(function(res){
      var apiRules=(res[0]&&res[0].rules)||[];
      var map={}; var ov=[];
      apiRules.forEach(function(r){
        // Org-scoped rows are customer-authored overrides: collect them for
        // the read-only section below rather than dropping them silently —
        // the platform operator should see where their defaults are being
        // superseded.
        if(r.org_id){ if(r.task_type!=='pii') ov.push(r); return; }
        if(r.task_type==='pii'){setPii(r.model_id||'auto'); return;}
        var chain=r.cascade_models&&r.cascade_models.length>0 ? r.cascade_models
                  : (r.model_id ? [r.model_id] : []);
        map[r.task_type]={mode:chain.length>0?'cascade':'auto', chain:chain, dirty:false};
      });
      setRules(map);
      setOrgOverrides(ov);
      var m=res[1]; var list=(m&&(m.data||m.models||m))||[];
      if(!Array.isArray(list)) list=[];
      setModels(list.map(function(x){var id=x.id||x; return {id:id, name:(D2.modelById(id).name)||id};}));
      setLoading(false);
    }).catch(function(){setLoading(false);});
  };
  useEffect(load,[]);

  var patchTask=function(task,patch){
    setRules(function(r){
      var prev=r[task]||{mode:'auto',chain:[],dirty:false};
      return Object.assign({},r,{[task]:Object.assign({},prev,patch,{dirty:true})});
    });
  };

  var saveTask=function(task){
    var rule=rules[task]||{mode:'auto',chain:[]};
    var chain=rule.mode==='cascade'?rule.chain:[];
    setSaving(function(s){return Object.assign({},s,{[task]:true});});
    NR_API.setRoutingRule(task,'','',chain).then(function(ok){
      setSaving(function(s){var n=Object.assign({},s); delete n[task]; return n;});
      if(ok){
        t({title:chain.length>0?'Rule saved':'Rule cleared',
           msg:chain.length>0?(task+' → '+chain.join(' → ')):(task+' → Auto (score-based)')});
        setRules(function(r){return Object.assign({},r,{[task]:Object.assign({},r[task],{dirty:false})});});
      } else t({kind:'error',title:'Failed to save rule'});
    }).catch(function(){
      setSaving(function(s){var n=Object.assign({},s); delete n[task]; return n;});
      t({kind:'error',title:'Failed to save rule'});
    });
  };

  var setPiiMode=function(mode){
    var prev=pii; setPii(mode);
    NR_API.setRoutingRule('pii',mode,'').then(function(ok){
      if(ok) t({title:'PII policy saved',msg:'PII → '+mode});
      else {t({kind:'error',title:'Failed to save PII policy'}); setPii(prev);}
    }).catch(function(){t({kind:'error',title:'Failed to save PII policy'}); setPii(prev);});
  };

  if(loading) return <LoadingPage/>;
  var cascadeCount=Object.values(rules).filter(function(r){return r.mode==='cascade'&&r.chain.length>0;}).length;

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc="Task routing rules. Each task type defaults to Auto (score-based routing). Switch to Cascade to specify an ordered chain of up to 4 models — NeuroRoute tries them in order and escalates on failure."/>

      <div className="card card-pad" style={{background:'var(--blue-t)',borderColor:'rgba(59,130,246,.3)'}}>
        <div className="row" style={{gap:10}}>
          <Icon name="route" size={17} style={{color:'var(--blue)'}}/>
          <span><b>{cascadeCount} of {ROUTING_TASK_TYPES.length} tasks configured.</b> Cascade rules override all routing strategies globally. If all models in a chain fail, the request errors — ensure at least one fallback per chain.</span>
        </div>
      </div>

      <SectionCard title="Task routing rules" sub="Auto uses score-based routing. Cascade tries models in order, escalating on hard errors." pad={false}>
        <div>
          {ROUTING_TASK_TYPES.map(function(tt){
            var rule=rules[tt.v]||{mode:'auto',chain:[],dirty:false};
            var isAuto=rule.mode==='auto';
            var isSaving=saving[tt.v];
            return (
              <div key={tt.v} style={{borderBottom:'1px solid var(--line)',padding:'8px 14px'}}>
                <div className="row" style={{gap:10,alignItems:'center'}}>
                  {/* Task label — the wire value moves to a tooltip; it's
                      debugging detail, not something to spend a line on for
                      all 11 rows. */}
                  <div style={{minWidth:170,fontSize:12.5,fontWeight:600,color:'var(--tx-0)'}} title={tt.v}>{tt.label}</div>
                  {/* Auto|Cascade as a 2-up segmented control rather than two
                      90px buttons plus a sentence of helper text. */}
                  <div className="row" style={{gap:4}}>
                    <button className={'btn btn-xs '+(isAuto?'btn-primary':'btn-outline')} style={{minWidth:56}}
                      onClick={function(){patchTask(tt.v,{mode:'auto',chain:[]});}}>Auto</button>
                    <button className={'btn btn-xs '+(isAuto?'btn-outline':'btn-primary')} style={{minWidth:66}}
                      onClick={function(){if(isAuto) patchTask(tt.v,{mode:'cascade',chain:rule.chain.length>0?rule.chain:[]});}}>Cascade</button>
                  </div>
                  {!isAuto&&rule.chain.length>0&&<Badge color="blue">{rule.chain.length} model{rule.chain.length>1?'s':''}</Badge>}
                  <div style={{flex:1}}/>
                  {rule.dirty&&(
                    <button className="btn btn-xs btn-primary" disabled={isSaving||(!isAuto&&rule.chain.length===0)}
                      onClick={function(){saveTask(tt.v);}}>
                      {isSaving?'Saving…':'Apply'}
                    </button>
                  )}
                  {!rule.dirty&&rule.mode==='cascade'&&rule.chain.length>0&&(
                    <span style={{fontSize:11,color:'var(--green)',whiteSpace:'nowrap'}}>✓ saved</span>
                  )}
                </div>
                {/* The chain builder only exists for a cascade row, so it drops
                    to its own line there and costs nothing on the 11 auto rows. */}
                {!isAuto&&(
                  <div style={{marginTop:8}}>
                    <CascadeBuilder
                      chain={rule.chain}
                      models={models}
                      onChange={function(chain){patchTask(tt.v,{chain:chain});}}
                      onSuggest={function(strategy){
                        var chain=_suggestChain(strategy,models);
                        if(chain.length>0) patchTask(tt.v,{chain:chain});
                        else t({kind:'error',title:'No models available for this strategy'});
                      }}
                    />
                  </div>
                )}
              </div>
            );
          })}
        </div>
      </SectionCard>

      {orgOverrides.length>0 && <SectionCard title="Customer overrides" pad={false}
        sub="Org-defined rules that supersede the platform defaults above, for that org only. Managed by each org's admins on their own Routing Rules page — read-only here.">
        <div className="scroll-x"><table className="table">
          <thead><tr><th>Organization</th><th>Task</th><th>Rule</th></tr></thead>
          <tbody>{orgOverrides.map(function(r){
            var chain=r.cascade_models&&r.cascade_models.length>0?r.cascade_models:(r.model_id?[r.model_id]:[]);
            return (
              <tr key={r.org_id+':'+r.task_type}>
                <td className="mono faint" style={{fontSize:12}} title={r.org_id}>{r.org_id.slice(0,8)}…</td>
                <td style={{fontSize:12.5}}>{r.task_type}</td>
                <td className="mono faint" style={{fontSize:11.5}}>{chain.join(' → ')||'—'}</td>
              </tr>
            );
          })}</tbody>
        </table></div>
      </SectionCard>}

      <SectionCard title="PII handling" sub="What NeuroRoute does when a request is flagged as containing PII (email, phone, PAN, Aadhaar, card, SSN).">
        <div className="card-pad grid" style={{gap:10}}>
          <div className="row" style={{gap:12,alignItems:'center'}}>
            <span style={{fontWeight:600,color:'var(--tx-0)',minWidth:120}}>PII policy</span>
            <select className="select" style={{minWidth:300}} value={pii} onChange={function(e){setPiiMode(e.target.value);}}>
              <option value="auto">Auto — route normally (no restriction)</option>
              <option value="self-hosted">Self-hosted models only (block if none)</option>
              <option value="block">Block the request</option>
            </select>
            {pii==='auto'?<span className="faint" style={{fontSize:12}}>default</span>:<Badge color="blue">{pii}</Badge>}
          </div>
          <div className="faint" style={{fontSize:12}}>Auto is recommended unless you run self-hosted models. Self-hosted forces on-prem models and blocks the request if none are registered; Block rejects PII requests outright.</div>
        </div>
      </SectionCard>
    </div>
  );
}

/* =====================  ANALYTICS (AO-2 / AO-3)  ===================== */
function AnalyticsPage(){
  var _days=useState(30); var days=_days[0], setDays=_days[1];
  var _loading=useState(true); var loading=_loading[0], setLoading=_loading[1];
  var _mix=useState([]); var mix=_mix[0], setMix=_mix[1];
  var _series=useState([]); var series=_series[0], setSeries=_series[1];
  var _tasks=useState([]); var tasks=_tasks[0], setTasks=_tasks[1];
  var _routing=useState(null); var routing=_routing[0], setRouting=_routing[1];
  var _slo=useState([]); var slo=_slo[0], setSlo=_slo[1];
  var _health=useState({}); var health=_health[0], setHealth=_health[1];
  // Each *Failed flag distinguishes "this section's fetch itself failed"
  // from "it succeeded and there is genuinely no data for this window" —
  // before this, both collapsed into the same empty-state copy per section.
  var _mixFailed=useState(false); var mixFailed=_mixFailed[0], setMixFailed=_mixFailed[1];
  var _seriesFailed=useState(false); var seriesFailed=_seriesFailed[0], setSeriesFailed=_seriesFailed[1];
  var _tasksFailed=useState(false); var tasksFailed=_tasksFailed[0], setTasksFailed=_tasksFailed[1];
  var _sloFailed=useState(false); var sloFailed=_sloFailed[0], setSloFailed=_sloFailed[1];

  var load=function(){
    setLoading(true);
    setMixFailed(false); setSeriesFailed(false); setTasksFailed(false); setSloFailed(false);
    // Each analytics* call is api()-backed: resolves null on any non-2xx
    // status or network failure, never rejects — detect per-section failure
    // via the null result.
    Promise.all([
      NR_API.analyticsModelMix(days), NR_API.analyticsTimeseries(days),
      NR_API.analyticsTaskDist(days), NR_API.analyticsRouting(days),
      NR_API.analyticsSLO(Math.min(days,30)), NR_API.adminMetrics(),
    ]).then(function(r){
      setMix((r[0]&&r[0].models)||[]); setMixFailed(r[0]===null);
      setSeries((r[1]&&r[1].series)||[]); setSeriesFailed(r[1]===null);
      setTasks((r[2]&&r[2].tasks)||[]); setTasksFailed(r[2]===null);
      setRouting(r[3]||null);
      setSlo((r[4]&&r[4].providers)||[]); setSloFailed(r[4]===null);
      var hmap={};
      if(r[5]&&r[5].provider_health){ r[5].provider_health.forEach(function(h){ hmap[h.provider]=h; }); }
      setHealth(hmap);
      setLoading(false);
    }).catch(function(){
      setLoading(false);
      setMixFailed(true); setSeriesFailed(true); setTasksFailed(true); setSloFailed(true);
    });
  };
  useEffect(function(){ load(); },[days]);

  if(loading) return <LoadingPage/>;

  var totalReq = mix.reduce(function(a,b){return a+(b.requests||0);},0);
  var totalCost = mix.reduce(function(a,b){return a+(b.provider_cost||0);},0);
  var totalSavings = mix.reduce(function(a,b){return a+(b.savings||0);},0);
  var maxDaySeries = series.reduce(function(a,b){return Math.max(a,b.requests||0);},0)||1;
  var maxTask = tasks.reduce(function(a,b){return Math.max(a,b.requests||0);},0)||1;
  var pct = function(x){ return (x*100).toFixed(1)+'%'; };
  var latColor = function(ms){ if(!ms) return 'var(--tx-3)'; if(ms<400) return 'var(--green)'; if(ms<1000) return 'var(--amber)'; return 'var(--red)'; };
  var errColor = function(r){ if(r>=0.05) return 'var(--red)'; if(r>=0.01) return 'var(--amber)'; return 'var(--green)'; };
  var healthColor = function(s){ if(s>=0.9) return 'var(--green)'; if(s>=0.5) return 'var(--amber)'; return 'var(--red)'; };

  var DaysTabs = (
    <div className="row" style={{gap:8}}>
      {[7,30,90].map(function(d){
        return <button key={d} className={'chip'+(days===d?' chip-active':'')} onClick={function(){setDays(d);}}>{d}d</button>;
      })}
    </div>
  );

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc={'Routing-decision analytics + SLO observability across all customers. Reads the hourly usage rollup. Window: last '+days+' days.'}>
        {DaysTabs}
      </PageHead>

      {/* Headline metrics */}
      <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit, minmax(180px, 1fr))', gap:14}}>
        <Metric icon="zap" color="blue" value={fmtNum(totalReq)} label="Requests"/>
        <Metric icon="dollar" color="green" value={fmtUSD(totalCost,2)} label="Provider cost"/>
        <Metric icon="sparkles" color="cyan" value={fmtUSD(totalSavings,2)} label="Savings"/>
        <Metric icon="layers" color="purple" value={routing?pct(routing.cache_hit_rate||0):'—'} label="Cache-hit rate"/>
        <Metric icon="refresh" color="amber" value={routing?pct(routing.failover_rate||0):'—'} label="Failover rate"/>
      </div>

      {/* SLO card row — per-provider p95 latency + error rate + health */}
      <SectionCard title="SLO · per-provider latency & error rate" sub={'p50/p95 latency, failover-based error rate, live health · last '+Math.min(days,30)+'d'}>
        {sloFailed ? <EvalLoadError what="latency data" onRetry={load}/> :
         slo.length===0 ?
          <EmptyState icon="clock" title="No latency data" desc="No requests recorded in this window yet."/> :
          <div className="grid" style={{gridTemplateColumns:'repeat(auto-fill, minmax(240px, 1fr))', gap:12}}>
            {slo.map(function(s){
              var prov = window.DATA.providerById(s.provider)||{name:s.provider,color:'var(--tx-2)'};
              var h = health[s.provider];
              return (
                <div key={s.provider} className="card" style={{padding:14, borderLeft:'3px solid '+(prov.color||'var(--tx-2)')}}>
                  <div className="spread" style={{marginBottom:10}}>
                    <div style={{fontWeight:700,fontSize:13.5}}>{prov.name||s.provider}</div>
                    {h ? <Badge color={h.health_score>=0.9?'green':h.health_score>=0.5?'amber':'red'} dot>
                      {Math.round((h.health_score||0)*100)}% health</Badge>
                      : <Badge color="gray">no probe</Badge>}
                  </div>
                  <div className="spread" style={{fontSize:12,marginBottom:6}}>
                    <span className="faint">p95 latency</span>
                    <span className="mono" style={{fontWeight:700,color:latColor(s.p95_latency_ms)}}>{s.p95_latency_ms?s.p95_latency_ms+'ms':'—'}</span>
                  </div>
                  <div className="meter" style={{marginBottom:10}}><span style={{width:Math.min(100,(s.p95_latency_ms/2000)*100)+'%',background:latColor(s.p95_latency_ms)}}/></div>
                  <div className="grid" style={{gridTemplateColumns:'repeat(3,1fr)',gap:6,paddingTop:8,borderTop:'1px solid var(--line)'}}>
                    <div><div className="faint" style={{fontSize:10.5}}>p50</div><div className="mono" style={{fontSize:12,fontWeight:700}}>{s.p50_latency_ms?s.p50_latency_ms+'ms':'—'}</div></div>
                    <div><div className="faint" style={{fontSize:10.5}}>Error rate</div><div className="mono" style={{fontSize:12,fontWeight:700,color:errColor(s.error_rate||0)}}>{pct(s.error_rate||0)}</div></div>
                    <div><div className="faint" style={{fontSize:10.5}}>Requests</div><div className="mono" style={{fontSize:12,fontWeight:700}}>{fmtNum(s.requests||0)}</div></div>
                  </div>
                </div>
              );
            })}
          </div>}
      </SectionCard>

      {/* Requests / cost / savings daily timeseries — CSS bar chart */}
      <SectionCard title="Daily requests & savings" sub={'Requests per day (bar height) with cost + savings · last '+days+'d'}>
        {seriesFailed ? <EvalLoadError what="timeseries data" onRetry={load}/> :
         series.length===0 ?
          <EmptyState icon="analytics" title="No timeseries data" desc="No usage recorded in this window yet."/> :
          <div>
            <div className="row" style={{alignItems:'flex-end',gap:3,height:140,overflowX:'auto',paddingBottom:4}}>
              {series.map(function(p){
                var hgt = Math.max(2,(p.requests/maxDaySeries)*130);
                return (
                  <div key={p.day} title={p.day+': '+fmtNum(p.requests)+' req · '+fmtUSD(p.provider_cost,2)+' cost · '+fmtUSD(p.savings,2)+' saved'}
                    style={{flex:'1 0 8px',minWidth:8,display:'flex',flexDirection:'column',justifyContent:'flex-end',alignItems:'center'}}>
                    <div style={{width:'70%',height:hgt,background:'var(--blue)',borderRadius:'3px 3px 0 0'}}/>
                  </div>
                );
              })}
            </div>
            <div className="spread faint" style={{fontSize:11,marginTop:6}}>
              <span>{series[0]&&series[0].day}</span>
              <span>{series[series.length-1]&&series[series.length-1].day}</span>
            </div>
          </div>}
      </SectionCard>

      {/* Model mix table */}
      <SectionCard title="Model mix" sub="Per-model request share, cost, and savings" pad={false}>
        {mixFailed ? <div style={{padding:20}}><EvalLoadError what="model mix data" onRetry={load}/></div> :
         mix.length===0 ?
          <div style={{padding:20}}><EmptyState icon="modelpref" title="No routing data" desc="No requests have been routed in this window."/></div> :
          <table className="table">
            <thead><tr>
              <th>Model</th><th>Provider</th><th style={{textAlign:'right'}}>Requests</th>
              <th style={{width:'25%'}}>Share</th><th style={{textAlign:'right'}}>Cost</th><th style={{textAlign:'right'}}>Savings</th>
            </tr></thead>
            <tbody>
              {mix.map(function(m){
                var prov = window.DATA.providerById(m.provider)||{name:m.provider,color:'var(--blue)'};
                return (
                  <tr key={m.model+'|'+m.provider}>
                    <td style={{fontWeight:600}}>{m.model}</td>
                    <td className="faint">{prov.name||m.provider}</td>
                    <td className="num" style={{textAlign:'right'}}>{fmtNum(m.requests||0)}</td>
                    <td>
                      <div className="row" style={{gap:8,alignItems:'center'}}>
                        <div className="meter" style={{flex:1}}><span style={{width:Math.min(100,(m.share||0)*100)+'%',background:prov.color||'var(--blue)'}}/></div>
                        <span className="mono faint" style={{fontSize:11,minWidth:44,textAlign:'right'}}>{pct(m.share||0)}</span>
                      </div>
                    </td>
                    <td className="num" style={{textAlign:'right'}}>{fmtUSD(m.provider_cost||0,2)}</td>
                    <td className="num" style={{textAlign:'right',color:'var(--green)'}}>{fmtUSD(m.savings||0,2)}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>}
      </SectionCard>

      {/* Task distribution */}
      <SectionCard title="Task distribution" sub="Requests by classified task type">
        {tasksFailed ? <EvalLoadError what="task distribution data" onRetry={load}/> :
         tasks.length===0 ?
          <EmptyState icon="filter" title="No task data" desc="No classified requests in this window."/> :
          <div className="grid" style={{gap:10}}>
            {tasks.map(function(t){
              return (
                <div key={t.task_type}>
                  <div className="spread" style={{fontSize:12.5,marginBottom:4}}>
                    <span style={{fontWeight:600,textTransform:'capitalize'}}>{t.task_type}</span>
                    <span className="mono faint">{fmtNum(t.requests||0)} · {pct(t.share||0)}</span>
                  </div>
                  <div className="meter"><span style={{width:Math.max(2,(t.requests/maxTask)*100)+'%',background:'var(--cyan)'}}/></div>
                </div>
              );
            })}
          </div>}
      </SectionCard>
    </div>
  );
}

/* ── Price Book admin page (P4.3) ─────────────────────────────────────────── */
function PriceBookPage(){
  var _rows=React.useState([]); var rows=_rows[0], setRows=_rows[1];
  var _loading=React.useState(true); var loading=_loading[0], setLoading=_loading[1];
  // loadFailed distinguishes "the listPriceBook fetch itself failed" from
  // "it succeeded and there are genuinely zero rows" — before this, a 500
  // rendered the exact same "No entries — seed via tiermigrate..." empty
  // state as a real empty price book, which is actively misleading on a
  // real server error (I-13).
  var _loadFailed=React.useState(false); var loadFailed=_loadFailed[0], setLoadFailed=_loadFailed[1];
  var _saving=React.useState(false); var saving=_saving[0], setSaving=_saving[1];
  var _form=React.useState({tier:'enterprise',base_fee_usd:'',in_rate_usd:'',out_rate_usd:'',effective_from:'',note:''});
  var form=_form[0], setForm=_form[1];
  var t=useToast();

  var load=function(){
    setLoading(true); setLoadFailed(false);
    // listPriceBook is api()-backed: resolves null on any non-2xx status or
    // network failure, never rejects — detect failure via the null result.
    NR_API.listPriceBook().then(function(r){
      if(r===null){ setLoadFailed(true); } else { setRows(r.rows||[]); }
      setLoading(false);
    });
  };
  React.useEffect(function(){ load(); },[]);

  var save=function(){
    if(!form.tier||!form.in_rate_usd||!form.out_rate_usd) return;
    setSaving(true);
    NR_API.appendPriceBook({
      tier:form.tier,
      base_fee_usd:parseFloat(form.base_fee_usd)||0,
      in_rate_usd:parseFloat(form.in_rate_usd),
      out_rate_usd:parseFloat(form.out_rate_usd),
      effective_from:form.effective_from||undefined,
      note:form.note
    }).then(function(r){
      setSaving(false);
      if(r&&r.id){ t({title:'Price book entry added', msg:'ID '+r.id}); load(); }
      else { t({kind:'error',title:'Failed to add entry'}); }
    }).catch(function(){ setSaving(false); t({kind:'error',title:'Failed to add entry'}); });
  };

  if(loading) return <LoadingPage/>;

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc="Effective-dated list prices per tier. Rows are append-only — each entry activates on its effective_from date."/>
      <SectionCard title="Current price book" pad={false}>
        {loadFailed ? <div style={{padding:20}}><EvalLoadError what="the price book" onRetry={load}/></div> :
        <div className="scroll-x"><table className="table">
          <thead><tr><th>Tier</th><th style={{textAlign:'right'}}>Base fee/mo</th><th style={{textAlign:'right'}}>In rate ($/1M)</th><th style={{textAlign:'right'}}>Out rate ($/1M)</th><th>Effective from</th><th>Note</th></tr></thead>
          <tbody>{rows.length===0?<tr><td colSpan={6} style={{textAlign:'center',color:'var(--tx-2)',padding:32}}>No entries — seed via tiermigrate or POST /v1/admin/price-book</td></tr>:rows.map(function(r){return(
            <tr key={r.id}>
              <td><Badge color={r.tier==='enterprise'?'purple':r.tier==='premium'?'blue':r.tier==='standard'?'cyan':'gray'}>{r.tier}</Badge></td>
              <td className="num mono" style={{textAlign:'right'}}>{fmtUSD(r.base_fee_usd||0,2)}</td>
              <td className="num mono" style={{textAlign:'right'}}>{fmtUSD(r.in_rate_usd||0,4)}</td>
              <td className="num mono" style={{textAlign:'right'}}>{fmtUSD(r.out_rate_usd||0,4)}</td>
              <td className="faint">{r.effective_from?r.effective_from.slice(0,10):''}</td>
              <td className="faint" style={{fontSize:12}}>{r.note||''}</td>
            </tr>);
          })}</tbody>
        </table></div>}
      </SectionCard>
      <SectionCard title="Add price book entry" sub="Append a new effective-dated row. Won't affect existing billed events.">
        <div className="grid" style={{gap:12}}>
          <div className="row" style={{gap:10,flexWrap:'wrap'}}>
            <div style={{flex:'0 0 140px'}}>
              <Field label="Tier">
                <select className="select" value={form.tier} onChange={function(e){setForm(function(f){return Object.assign({},f,{tier:e.target.value});});}}>
                  <option value="basic">basic</option><option value="standard">standard</option><option value="premium">premium</option><option value="enterprise">enterprise</option>
                </select>
              </Field>
            </div>
            <div style={{flex:'0 0 110px'}}>
              <Field label="Base fee/mo ($)">
                <input className="input" type="number" min="0" step="1" placeholder="99" value={form.base_fee_usd} onChange={function(e){setForm(function(f){return Object.assign({},f,{base_fee_usd:e.target.value});});}}/>
              </Field>
            </div>
            <div style={{flex:'0 0 130px'}}>
              <Field label="In rate ($/1M tok)">
                <input className="input" type="number" min="0" step="0.01" placeholder="0.60" value={form.in_rate_usd} onChange={function(e){setForm(function(f){return Object.assign({},f,{in_rate_usd:e.target.value});});}}/>
              </Field>
            </div>
            <div style={{flex:'0 0 130px'}}>
              <Field label="Out rate ($/1M tok)">
                <input className="input" type="number" min="0" step="0.01" placeholder="1.00" value={form.out_rate_usd} onChange={function(e){setForm(function(f){return Object.assign({},f,{out_rate_usd:e.target.value});});}}/>
              </Field>
            </div>
            <div style={{flex:'0 0 140px'}}>
              <Field label="Effective from">
                <input className="input" type="date" value={form.effective_from} onChange={function(e){setForm(function(f){return Object.assign({},f,{effective_from:e.target.value});});}}/>
              </Field>
            </div>
            <div style={{flex:'1 1 200px'}}>
              <Field label="Note">
                <input className="input" placeholder="e.g. Q4 2026 price increase" value={form.note} onChange={function(e){setForm(function(f){return Object.assign({},f,{note:e.target.value});});}}/>
              </Field>
            </div>
          </div>
          <div><button className="btn btn-primary btn-sm" onClick={save} disabled={saving||!form.in_rate_usd||!form.out_rate_usd}>{saving?'Saving…':'Add entry'}</button></div>
        </div>
      </SectionCard>
    </div>
  );
}

/* A2APage moved to portal-team.jsx (I-5a) — it's a customer-facing,
   org-scoped feature (internal/gateway/a2a_endpoints.go reads AuthContext.
   OrgID, with no cross-org {org_id} path at all), so it belongs in the
   customer nav, not admin-only. See portal-team.jsx's A2APage for the full
   reasoning. SISL admin roles still reach it — allowedRoutesFor(app.jsx)
   returns null (full access) for admin/sisl_admin, so nothing was lost by
   removing this admin-only copy. */

window.PAGES = Object.assign(window.PAGES||{}, {customers:CustomersPage, onboard:OnboardPage,
  sislkeys:SislKeysPage, pricing:PricingPage, modelpref:ModelPrefPage, health:HealthPage, access:AccessPage, audit:AuditPage,
  routing:RoutingRulesPage, analytics:AnalyticsPage, pricebook:PriceBookPage});
