PageSourceSearch

https://stuartandrews.me/assets/BudgetsPanel-Bn2bCcM_.js

js stuartandrews.me collected 2026-09-24 22:18:44 UTC 6,754 bytes, 1 lines download raw bytes

1import{r as a,j as t}from"./react-vendor-DHsc3z8R.js";import{S as k}from"./StatusPill-BSgGM2e7.js";import"./dropdown-menu-0sz3yqpS.js";import{j as y}from"./index-D70tPUFV.js";import{S as f}from"./shield-alert-BYYT5J2P.js";import{T as C}from"./triangle-alert-BUbPnYVy.js";import{S as T}from"./shield-check-C7Ix4IyA.js";import{L as N}from"./loader-circle-Co7lRgMX.js";import{T as A}from"./target-Dfb3jDIp.js";import{S as U}from"./save-D_irsuPn.js";import"./ui-vendor-CSbCjZRx.js";import"./check-DNgNGAzZ.js";import"./chevron-right-15wZkIJf.js";import"./circle-vqIdLyen.js";import"./query-vendor-BpSM1h0G.js";import"./motion-vendor-BtpmimIC.js";function g({ok:i,warn:e,label:d,detail:n}){const c=e?C:i?T:f,l=e?"text-[hsl(var(--warning))]":i?"text-[hsl(var(--success))]":"text-[hsl(var(--destructive))]";return t.jsxs("div",{className:"flex items-start gap-2 py-2 border-b border-border/20 last:border-0",children:[t.jsx(c,{className:`w-4 h-4 mt-0.5 shrink-0 ${l}`}),t.jsxs("div",{children:[t.jsx("div",{className:"text-sm text-foreground",children:d}),t.jsx("div",{className:"text-xs text-muted-foreground",children:n})]})]})}function E({token:i}){const[e,d]=a.useState(null),[n,c]=a.useState(!1),[l,m]=a.useState(null);return a.useEffect(()=>{let r=!1;return(async()=>{c(!0),m(null);try{const o=await fetch("/api/admin/ai-costs/guards",{headers:{...y()}});if(!o.ok)throw new Error(`HTTP ${o.status}`);const x=await o.json();r||d(x)}catch(o){r||m(o.message)}finally{r||c(!1)}})(),()=>{r=!0}},[i]),t.jsxs("div",{className:"bg-secondary/20 border border-border/40 rounded-xl p-5","data-testid":"card-ai-cost-alerts",children:[t.jsxs("h4",{className:"text-sm font-medium text-foreground flex items-center gap-2 mb-3",children:[t.jsx(f,{className:"w-4 h-4 text-[hsl(var(--accent-gold))]"})," Cost Alerts",e&&e.breachKinds.length>0&&t.jsxs("span",{className:"text-[10px] bg-[hsl(var(--destructive))]/15 text-[hsl(var(--destructive))] rounded px-1.5 py-0.5",children:[e.breachKinds.length," active"]})]}),n&&t.jsx("p",{className:"text-sm text-muted-foreground py-4",children:"Checking guards…"}),l&&t.jsxs("p",{className:"text-sm text-[hsl(var(--destructive))] py-4",children:["Couldn't load guards: ",l]}),e&&!n&&t.jsxs("div",{children:[t.jsx(g,{ok:e.reconciliation.available&&!e.reconciliation.breached,warn:e.reconciliation.breached,label:"Billed vs app-logged (Anthropic)",detail:e.reconciliation.available?`billed $${e.reconciliation.billedUsd.toFixed(2)} vs logged $${e.reconciliation.appLoggedUsd.toFixed(2)} — $${e.reconciliation.unexplainedUsd.toFixed(2)} (${e.reconciliation.unexplainedPct}%) unexplained`:e.reconciliation.note??"set ANTHROPIC_ADMIN_KEY to enable"}),t.jsx(g,{ok:!e.spike.breached,warn:e.spike.breached,label:"Daily spike",detail:`today $${e.spike.todayUsd.toFixed(2)} vs median $${e.spike.trailingMedianUsd.toFixed(2)}${e.spike.ratio?` (${e.spike.ratio}×)`:""}`}),t.jsx(g,{ok:!e.ceiling.configured||!e.ceiling.breached,warn:e.ceiling.breached,label:"Monthly ceiling",detail:e.ceiling.configured?`$${e.ceiling.monthUsd.toFixed(2)} of $${e.ceiling.ceilingUsd.toFixed(2)} (${e.ceiling.pctUsed}%)`:"not configured (set AI_MONTHLY_CEILING_USD)"}),t.jsx(g,{ok:e.modelRecon.available&&e.modelRecon.discrepancies.length===0,warn:e.modelRecon.discrepancies.length>0,label:"Per-model reconciliation",detail:e.modelRecon.available?e.modelRecon.discrepancies.length===0?"all models match app logs":e.modelRecon.discrepancies.map(r=>`${r.model}: ~$${r.unexplainedUsd.toFixed(2)} out-of-app`).join("; "):e.modelRecon.note??"set ANTHROPIC_ADMIN_KEY to enable"})]})]})}function Q({token:i}){const e={...y(),"Content-Type":"application/json"},[d,n]=a.useState([]),[c,l]=a.useState(!0),[m,r]=a.useState(""),[o,x]=a.useState(!1),[w,b]=a.useState(!1),[v,h]=a.useState(null),p=a.useCallback(async(s=!1)=>{s||l(!0);try{const u=await fetch("/api/admin/budgets",{headers:e});if(!u.ok)throw new Error(`HTTP ${u.status}`);const j=(await u.json()).budgets??[];n(j),r(String(j.find(S=>S.provider.toLowerCase()==="overall")?.amountUsd??"")),h(null)}catch(u){h(u?.message??"Failed to load budgets")}finally{s||l(!1)}},[i]);a.useEffect(()=>{p()},[p]);const $=a.useCallback(async()=>{x(!0);try{const s=await fetch("/api/admin/budgets",{method:"POST",headers:e,body:JSON.stringify({provider:"overall",amountUsd:Number(m)||0,alertThresholdPct:80})});if(!s.ok)throw new Error(`HTTP ${s.status}`);b(!0),setTimeout(()=>b(!1),1500),await p(!0)}catch(s){h(s?.message??"Save failed")}finally{x(!1)}},[m,i,p]);return c?t.jsxs("div",{className:"flex items-center gap-2 p-6 text-sm text-muted-foreground",children:[t.jsx(N,{className:"h-4 w-4 animate-spin"})," Loading budgets…"]}):t.jsxs("div",{className:"space-y-5",children:[t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx(f,{className:"h-5 w-5 text-accent-gold"}),t.jsx("h2",{className:"text-lg font-display font-semibold",children:"Budgets & Alerts"})]}),v&&t.jsx("div",{className:"rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive",children:v}),t.jsxs("div",{className:"rounded-xl border border-border/30 bg-secondary/30 p-4",children:[t.jsxs("div",{className:"flex items-center gap-2 text-sm font-semibold",children:[t.jsx(A,{className:"h-4 w-4 text-accent-gold"})," Monthly budget"]}),t.jsx("p",{className:"mt-0.5 text-xs text-muted-foreground",children:"Overall monthly spend target. Drives the forecast banner in Insights and the budget alert."}),t.jsxs("div",{className:"mt-3 flex items-center gap-2",children:[t.jsx("span",{className:"text-sm text-muted-foreground",children:"$"}),t.jsx("input",{type:"number",min:0,value:m,placeholder:"0","data-testid":"budget-overall",onChange:s=>r(s.target.value),className:"w-32 rounded-md border border-border bg-background px-2 py-1.5 text-sm"}),t.jsx("span",{className:"text-xs text-muted-foreground",children:"/mo"}),t.jsxs("button",{type:"button",onClick:$,disabled:o,"data-testid":"budget-save",className:"flex items-center gap-1 rounded-md bg-accent-gold px-3 py-1.5 text-xs font-semibold text-background disabled:opacity-40",children:[o?t.jsx(N,{className:"h-3 w-3 animate-spin"}):t.jsx(U,{className:"h-3 w-3"})," Save"]}),w&&t.jsx(k,{variant:"success",dot:!0,size:"sm",children:"Saved"})]}),d.filter(s=>s.provider.toLowerCase()!=="overall").length>0&&t.jsxs("div",{className:"mt-3 text-xs text-muted-foreground",children:["Per-provider budgets: ",d.filter(s=>s.provider.toLowerCase()!=="overall").map(s=>`${s.provider} $${s.amountUsd}`).join(" · ")]})]}),t.jsx(E,{token:i}),t.jsxs("p",{className:"text-[11px] text-muted-foreground",children:["Alert email cadence, recipient and the spend threshold are configured in the ",t.jsx("strong",{children:"Notifications"})," tab."]})]})}export{Q as default};

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.