1import{c as j,h as w,t as b,j as e,i as o,L as k,s as _}from"./index-BKuXz_lx.js";import{u as v}from"./useQuery-Bz14us_N.js";import{u as I}from"./useMutation-BF1Hdgha.js";import{u as A}from"./useServerFn-DruDSVLX.js";import{B as S}from"./back-link-Cu1gDYAu.js";import{a as R}from"./plaid.functions-DFKWUb9g.js";import{S as N}from"./shield-DliqSqo8.js";import{L as y}from"./link-2-BrBmZL4v.js";import{B as m}from"./building-2-DDUOAwB-.js";import{T as x}from"./triangle-alert-BqWhhR-9.js";import{C as f}from"./clock-C8smJZOC.js";import{M as u}from"./mail-DBN6DUdR.js";import{R as E}from"./refresh-cw-DBVO72O8.js";import"./arrow-left-CjswZ4jf.js";const Q=j("/_authenticated/_admin/plaid-health")({component:P});function P(){const c=w(),n=A(R),{data:s,isLoading:a,error:r}=v({queryKey:["admin-plaid-health"],queryFn:async()=>{const{data:t,error:d}=await _.rpc("admin_plaid_health");if(d)throw d;return t},refetchInterval:6e4}),l=I({mutationFn:t=>n({data:{plaidItemId:t}}),onSuccess:t=>{b.success(`Re-auth email sent to ${t.sentTo}`),c.invalidateQueries({queryKey:["admin-plaid-health"]})},onError:t=>{b.error(t instanceof Error?t.message:"Failed to send reminder")}});return e.jsxs("div",{className:"max-w-6xl mx-auto px-4 md:px-6 py-6 md:py-10",children:[e.jsxs("div",{className:"mb-6",children:[e.jsxs("p",{className:"text-xs uppercase tracking-[0.2em] text-muted-foreground flex items-center gap-2",children:[e.jsx(N,{className:"size-3.5"})," Internal admin"]}),e.jsxs("h1",{className:"font-display text-3xl sm:text-4xl mt-1 leading-tight flex items-center gap-3",children:[e.jsx(y,{className:"size-7 text-gold"})," Plaid health"]}),e.jsx(S,{to:"/admin",className:"mt-4",children:"Back to admin"})]}),e.jsxs("section",{className:"rounded-2xl border border-gold/30 bg-gold/5 p-5 mb-8",children:[e.jsx("h2",{className:"font-display text-lg text-gold mb-2",children:"What this page is"}),e.jsxs("p",{className:"text-sm text-titanium leading-relaxed",children:["Plaid is the third-party service that securely connects users' bank accounts so Perqt can auto-match transactions to benefits. Every linked bank is a",e.jsx("code",{className:"text-xs mx-1 text-gold",children:"plaid_items"})," row."]}),e.jsxs("p",{className:"text-sm text-titanium leading-relaxed mt-2",children:[e.jsx("strong",{className:"text-platinum",children:"Why we have it:"})," a stale or broken Plaid link means a paying user stops getting auto-matches â they think Perqt is broken and churn. This page surfaces re-auth backlog and silent webhook gaps before users notice."]}),e.jsxs("p",{className:"text-sm text-titanium leading-relaxed mt-2",children:[e.jsx("strong",{className:"text-platinum",children:"Automated workflows:"})," the",e.jsx("code",{className:"text-xs mx-1 text-gold",children:"plaid-health-reminders"})," cron runs daily and emails users for re-auth, user-fixable sync errors, and newly-detected accounts. Admins get a digest only when something needs hands-on attention."]})]}),e.jsx(L,{}),a||!s?e.jsx("p",{className:"text-center text-muted-foreground py-20",children:"Loadingâ¦"}):r?e.jsx("p",{className:"text-center text-destructive py-20",children:"Failed to load Plaid stats."}):e.jsxs(e.Fragment,{children:[e.jsxs("section",{className:"grid sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8",children:[e.jsx(i,{label:"Linked bank items",value:s.total_items,sub:`${s.total_users} users · ${s.linked_30d} new (30d)`,accent:!0}),e.jsx(i,{label:"Needs re-auth",value:s.needs_reauth,sub:"User must reconnect bank",tone:s.needs_reauth>0?"warn":"ok"}),e.jsx(i,{label:"Last sync error",value:s.with_error,sub:"Plaid returned an error",tone:s.with_error>0?"warn":"ok"}),e.jsx(i,{label:"Has new accounts",value:s.has_new_accounts,sub:"User added card at bank"}),e.jsx(i,{label:"Stale ⥠3 days",value:s.stale_3d,sub:`${s.stale_7d} stale ⥠7d`,tone:s.stale_3d>0?"warn":"ok"}),e.jsx(i,{label:"Never synced",value:s.never_synced,sub:"Linked but no sync yet",tone:s.never_synced>0?"warn":"ok"}),e.jsx(i,{label:"No webhook ⥠3d",value:s.webhook_silent_3d,sub:"Possible webhook gap",tone:s.webhook_silent_3d>0?"warn":"ok"}),e.jsx(i,{label:"Oldest stale",value:`${s.oldest_stale_hours}h`,sub:"Time since oldest item last synced"})]}),s.top_institutions.length>0&&e.jsxs("section",{className:"rounded-2xl border border-metallic bg-metallic/40 p-5 mb-8",children:[e.jsxs("p",{className:"text-[11px] uppercase tracking-[0.2em] text-muted-foreground mb-3 flex items-center gap-2",children:[e.jsx(m,{className:"size-3.5"})," Top institutions"]}),e.jsx("div",{className:"rounded-xl border border-metallic bg-background/40 overflow-hidden",children:e.jsxs("table",{className:"w-full text-sm",children:[e.jsx("thead",{className:"bg-metallic/40 text-[11px] uppercase tracking-wide text-muted-foreground",children:e.jsxs("tr",{children:[e.jsx("th",{className:"text-left px-3 py-2",children:"Institution"}),e.jsx("th",{className:"text-right px-3 py-2",children:"Items"}),e.jsx("th",{className:"text-right px-3 py-2",children:"Re-auth"}),e.jsx("th",{className:"text-right px-3 py-2",children:"Errors"})]})}),e.jsx("tbody",{children:s.top_institutions.map(t=>e.jsxs("tr",{className:"border-t border-metallic/60",children:[e.jsx("td",{className:"px-3 py-2 text-platinum",children:t.institution}),e.jsx("td",{className:"px-3 py-2 text-right tabular-nums text-platinum",children:t.items}),e.jsx("td",{className:o("px-3 py-2 text-right tabular-nums",t.reauth>0?"text-amber-400":"text-muted-foreground"),children:t.reauth}),e.jsx("td",{className:o("px-3 py-2 text-right tabular-nums",t.errors>0?"text-red-400":"text-muted-foreground"),children:t.errors})]},t.institution))})]})})]}),s.reauth_items.length>0&&e.jsxs("section",{className:"rounded-2xl border border-amber-500/30 bg-amber-500/5 p-5",children:[e.jsxs("p",{className:"text-[11px] uppercase tracking-[0.2em] text-amber-300 mb-3 flex items-center gap-2",children:[e.jsx(x,{className:"size-3.5"})," Action queue â users with broken or new-account Plaid links"]}),e.jsx("div",{className:"rounded-xl border border-metallic bg-background/40 overflow-hidden",children:e.jsxs("table",{className:"w-full text-sm",children:[e.jsx("thead",{className:"bg-metallic/40 text-[11px] uppercase tracking-wide text-muted-foreground",children:e.jsxs("tr",{children:[e.jsx("th",{className:"text-left px-3 py-2",children:"User"}),e.jsx("th",{className:"text-left px-3 py-2",children:"Institution"}),e.jsx("th",{className:"text-left px-3 py-2",children:"Issue"}),e.jsx("th",{className:"text-left px-3 py-2",children:"Last sync"}),e.jsx("th",{className:"text-left px-3 py-2",children:"Reminders"}),e.jsx("th",{className:"text-right px-3 py-2",children:"Action"})]})}),e.jsx("tbody",{children:s.reauth_items.map(t=>{const d=[];t.needs_reauth&&d.push("Re-auth"),t.last_error&&d.push(t.last_error),t.has_new_accounts&&d.push("New accounts");const h=t.reauth_reminder_count??0,p=t.reauth_reminder_sent_at?new Date(t.reauth_reminder_sent_at).toLocaleDateString():null;return e.jsxs("tr",{className:"border-t border-metallic/60",children:[e.jsx("td",{className:"px-3 py-2 text-platinum",children:e.jsx(k,{to:"/users/$userId",params:{userId:t.user_id},className:"hover:text-gold underline-offset-2 hover:underline",children:t.email??t.user_id.slice(0,8)})}),e.jsx("td",{className:"px-3 py-2 text-titanium",children:t.institution}),e.jsx("td",{className:"px-3 py-2 text-xs",children:e.jsx("div",{className:"flex flex-wrap gap-1",children:d.map(g=>e.jsx("span",{className:"rounded bg-amber-500/15 text-amber-300 px-1.5 py-0.5 text-[10px] uppercase tracking-wide",children:g},g))})}),e.jsx("td",{className:"px-3 py-2 text-muted-foreground text-xs",children:e.jsxs("span",{className:"inline-flex items-center gap-1",children:[e.jsx(f,{className:"size-3"}
1),t.last_synced_at?new Date(t.last_synced_at).toLocaleString():"never"]})}),e.jsx("td",{className:"px-3 py-2 text-xs text-titanium",children:h>0?`${h}/3${p?` · ${p}`:""}`:e.jsx("span",{className:"text-muted-foreground",children:"none"})}),e.jsx("td",{className:"px-3 py-2 text-right",children:t.needs_reauth?e.jsxs("button",{onClick:()=>l.mutate(t.plaid_item_id),disabled:l.isPending,className:"inline-flex items-center gap-1 rounded border border-gold/40 bg-gold/10 hover:bg-gold/20 text-gold uppercase tracking-wider text-[10px] px-2 py-1 disabled:opacity-50",children:[e.jsx(u,{className:"size-3"}),l.isPending?"Sendingâ¦":"Send re-auth email"]}):e.jsx("span",{className:"text-[10px] text-muted-foreground",children:"â"})})]},t.plaid_item_id)})})]})})]}),e.jsxs("p",{className:"text-[11px] text-muted-foreground mt-6 flex items-center gap-1",children:[e.jsx(E,{className:"size-3"})," Auto-refreshes every 60s."]})]})]})}function L(){const c=[{issue:"Needs re-auth",detail:"ITEM_LOGIN_REQUIRED, PENDING_EXPIRATION, USER_PERMISSION_REVOKED",owner:"User",severity:"warn",trigger:"Plaid flags the item as needing reconnection",action:"Daily cron emails the user a 'Reconnect your bank' CTA. Re-nudges every 7 days, max 3 emails, then escalates to admin digest.",icon:y},{issue:"Sync error â user-fixable",detail:"INVALID_CREDENTIALS, INVALID_MFA, ITEM_LOCKED",owner:"User",severity:"warn",trigger:"Sync returns an error the user can resolve themselves",action:"Daily cron emails the user a tailored 'how to fix' note. Re-nudges every 7 days. Cleared when user reconnects.",icon:u},{issue:"Sync error â institution",detail:"INSTITUTION_DOWN, RATE_LIMIT, NOT_RESPONDING",owner:"Auto",severity:"info",trigger:"Transient bank-side outage or throttling",action:"Silent. Webhook + auto-sync keep retrying. Not surfaced to user to avoid false alarms.",icon:m},{issue:"Sync error â internal",detail:"INTERNAL_SERVER_ERROR, INVALID_ACCESS_TOKEN",owner:"Admin",severity:"critical",trigger:"Perqt-side or token-state failure",action:"Admin gets it in the daily digest. Usually requires re-running /item/remove and asking the user to relink.",icon:x},{issue:"New accounts available",detail:"NEW_ACCOUNTS_AVAILABLE",owner:"User",severity:"info",trigger:"User added a card at their bank that isn't linked in Perqt",action:"Cron emails the user once, prompting them to add the new card in Perqt (deep link to /cards).",icon:u},{issue:"Stale ⥠7 days, no error flag",detail:"No webhook activity, no error code",owner:"Admin",severity:"warn",trigger:"Item silently stops updating without Plaid raising a flag",action:"Surfaced as a 'silently stale' warning in admin digest â signals a webhook gap. Falls into the re-auth flow if Plaid eventually flags it.",icon:f},{issue:"Never synced (linked > 24h)",detail:"Initial /transactions/sync never completed",owner:"Admin",severity:"critical",trigger:"⥠5 items stuck in never-synced state",action:"Critical admin alert. Check plaid-webhook logs â initial sync isn't completing.",icon:x},{issue:"Webhook silent ⥠3 days",detail:"System-wide webhook gap",owner:"Admin",severity:"warn",trigger:"⥠10 items with no webhook activity in 3 days",action:"Warning admin alert. Verify Plaid Dashboard â Webhooks endpoint is healthy.",icon:N},{issue:"Institution > 50% items broken",detail:"Concentrated failure at one bank",owner:"Admin",severity:"warn",trigger:"More than half of a single institution's items are erroring",action:"Warning admin alert. Likely OAuth credential rotation needed for that institution, or an institution-wide outage.",icon:m}],n={info:{dot:"bg-sky-400",label:"text-sky-300",ring:"ring-sky-400/20"},warn:{dot:"bg-amber-400",label:"text-amber-300",ring:"ring-amber-400/20"},critical:{dot:"bg-rose-400",label:"text-rose-300",ring:"ring-rose-400/25"}},s={User:"bg-gold/10 text-gold border-gold/30",Admin:"bg-rose-400/10 text-rose-300 border-rose-400/30",Auto:"bg-metallic/60 text-titanium border-metallic"};return e.jsxs("section",{className:"rounded-2xl border border-metallic bg-metallic/30 p-4 sm:p-6 mb-8",children:[e.jsxs("div",{className:"flex items-baseline justify-between gap-3 mb-4",children:[e.jsx("p",{className:"text-[11px] uppercase tracking-[0.2em] text-muted-foreground",children:"Action plan â what happens for each issue"}),e.jsxs("p",{className:"hidden sm:block text-[11px] text-muted-foreground",children:[c.length," workflows"]})]}),e.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3",children:c.map(a=>{const r=n[a.severity],l=a.icon;return e.jsxs("article",{className:o("group rounded-xl border border-metallic bg-background/50 p-4 flex flex-col gap-3","ring-1 ring-inset",r.ring),children:[e.jsxs("header",{className:"grid grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3",children:[e.jsx("div",{className:"shrink-0 grid h-9 w-9 place-items-center rounded-lg bg-metallic/60 border border-metallic",children:e.jsx(l,{className:"size-4 text-titanium"})}),e.jsxs("div",{className:"min-w-0",children:[e.jsx("h3",{className:"text-sm font-semibold text-platinum leading-tight truncate",children:a.issue}),a.detail&&e.jsx("p",{className:"text-[11px] text-muted-foreground mt-0.5 font-mono truncate",children:a.detail})]}),e.jsxs("span",{className:o("shrink-0 inline-flex items-center gap-1.5 text-[10px] uppercase tracking-wider font-medium",r.label),children:[e.jsx("span",{className:o("size-1.5 rounded-full",r.dot)}),a.severity]})]}),e.jsx("div",{className:"flex flex-wrap items-center gap-2",children:e.jsx("span",{className:o("inline-flex items-center text-[10px] uppercase tracking-wider font-medium px-2 py-0.5 rounded-full border",s[a.owner]),children:a.owner==="Auto"?"Auto-retry":`${a.owner} fixes`})}),e.jsxs("div",{className:"space-y-2 pt-1 border-t border-metallic/50",children:[e.jsxs("div",{children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-muted-foreground mb-0.5",children:"Trigger"}),e.jsx("p",{className:"text-xs text-titanium leading-relaxed",children:a.trigger})]}),e.jsxs("div",{children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-muted-foreground mb-0.5",children:"What Perqt does"}),e.jsx("p",{className:"text-xs text-platinum leading-relaxed",children:a.action})]})]})]},a.issue)})})]})}function i({label:c,value:n,sub:s,accent:a,tone:r}){const l=a?"text-gold":r==="warn"?"text-amber-400":r==="ok"?"text-emerald-400":"text-platinum";return e.jsxs("div",{className:"rounded-2xl border border-metallic bg-metallic/40 p-5",children:[e.jsx("p",{className:"text-[11px] uppercase tracking-[0.2em] text-muted-foreground",children:c}),e.jsx("p",{className:o("font-display text-3xl mt-1",l),children:typeof n=="number"?n.toLocaleStr
1ing():n}),s&&e.jsx("p",{className:"text-[11px] text-muted-foreground mt-1",children:s})]})}export{Q as Route};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.