diff --git a/docs/ui-roadmap.md b/docs/ui-roadmap.md new file mode 100644 index 0000000..790b239 --- /dev/null +++ b/docs/ui-roadmap.md @@ -0,0 +1,148 @@ +# FlightDeck Serve UI roadmap + +This document turns the strict UI review into sequenced work. Scope is the checked-in React app under `web/` (served from `flightdeck serve`). Goal: move from **ledger viewer** to a **release-centric control plane** without changing core product boundaries (CLI-first, local ledger). + +## Principles + +1. **One spine**: a release under judgment → diff verdict → promotion action (evidence on demand). +2. **URL is state**: deep links prefill forms so operators can share “this comparison” and “this promotion.” +3. **Verdict before detail**: policy outcome and blockers must dominate; tables and JSON stay secondary. +4. **Boring over flashy**: prefer clear hierarchy and high-contrast failure states over decorative chrome. + +## Phase 0 — Done in repo (this slice) + +| Item | Outcome | +|------|---------| +| Release-centric hero | Overview highlights a focused release when `?release=` is set; row shortcuts jump to Diff / Runs / Promote with params. | +| Wire navigation to state | `Diff`, `Runs`, and `Promote` read `baseline`, `candidate`, `release_id`, `window`, `environment` from the URL search string. | +| Blocked / pass unavoidable | Diff page shows a full-width **verdict banner** (alert on FAIL) above the result card stack. | +| Bridge Diff → Promote | After a computed diff, a primary **Continue to promote** action links to Promote with release + environment + window prefilled (read-only builds omit). | + +## Phase 1 — Hierarchy and differentiation + +| Priority | Work | Status | +|----------|------|--------| +| P1 | Collapse or relocate **Ledger metrics** on Overview so the releases + promoted story leads. | Done — metrics in collapsible panel below tables (collapsed by default). | +| P1 | **Reorder Diff result**: top fold = verdict + key deltas; pricing/catalog in collapsed sections or tabs. | Done — verdict banner; samples + rollups; pricing summary inline with expandable detail. | +| P1 | **Promoted vs candidate** narrative per `agent + environment` (e.g. inline summary above tables). | Done — promoted table first with version column; releases show Live vs Registered. | +| P1 | Reduce reliance on **manual checksum scanning** — surface version + agent + env as the human keys. | Done — Primary column on releases table; hero leads with agent/version/env. | + +## Phase 2 — Polish and operator UX + +| Priority | Work | Status | +|----------|------|--------| +| P2 | Typography scale for page vs card titles; consistent vertical rhythm. | Done — `fd-page-sub--tight` / `--meta`, wider page header measure. | +| P2 | Table ergonomics: row hover, optional filters, copy-to-clipboard for release IDs. | Done — filter row on releases; copy buttons; hover accent on `fd-table--hover`. | +| P2 | Tone down gradient accents for a more **infra / audit** aesthetic (keep accessible contrast). | Done — solid primary buttons; flat logo tile; nav indicator unchanged. | +| P2 | Copy pass: each primary page answers *What changed?* *Is it safe?* *Can I ship?* in one short block. | Done — Overview, Diff, Runs, Actions, Settings intros. | + +## Non-goals (near term) + +- Embedded orchestration or graph execution. +- Chart-heavy analytics dashboards (prefer summary metrics tied to gates). +- Replacing the CLI registration / ingest workflow. + +## Verification + +After `web/` changes: from `web/`, `npm ci && npm run build`; commit `src/flightdeck/server/static/` updates; run `npm run test:e2e` when navigation or forms behavior changes. + +On Unix hosts where `python` is not on `PATH`, set `FLIGHTDECK_E2E_PYTHON` to a Python that has FlightDeck installed (for example the repo venv: `FLIGHTDECK_E2E_PYTHON=/path/to/.venv/bin/python npm run test:e2e`). The default is `python3`. + +## Blueprint alignment (external product IA review) + +This section maps a fuller “control plane” blueprint to FlightDeck’s **current** CLI-first ledger and HTTP surface. Use it to avoid building UI that implies APIs or workflows we do not ship yet. + +### Adopted from the blueprint + +- **Page litmus**: each primary screen should answer at least one of — *What changed?* · *What happened because of it?* · *Can I ship?* +- **Cross-page consistency**: shared status semantics (pass / fail / warn / neutral), fixed vocabulary (**Release**, **Diff**, **Policy**, **Evidence**), repeated rhythm (**header → summary → detail → actions**). +- **Sparse chrome**: summary metrics and tables over chart-heavy dashboards (matches roadmap non-goals). +- **Diff as differentiator**: structured comparison and policy outcome stay central; layout can evolve toward “baseline vs candidate” twin + verdict-first fold (Phase 1). +- **Evidence as ground truth**: runs + rollups remain the forensic surface; avoid Langfuse-style analytics_scope creep. +- **Component direction**: prefer one reusable set (`ReleaseHeader`, `StatusBadge`, `MetricCard`, etc.) over one-off page styling. + +### Merged information architecture (near term) + +Avoid exploding to eight top-level nav items before contracts exist. Practical sequencing: + +1. **Overview** — situational awareness; add promoted / last-action strip before burying operators in ledger counters (Phase 1). +2. **Releases** — table-first browsing (today: Overview table; later: dedicated route if needed). +3. **Release detail** — evolve `?release=` hero into `/release/:id` when we want a stable bookmark per artifact. +4. **Diff** — deep dive; expand “change → impact → policy” **only** when diff payloads expose comparable structure (prompt/tools/model deltas as data, not copy). +5. **Evidence** — Runs page (rename in nav only if it helps operators). +6. **Promote** — Actions; surface approval flow when `promotion_requires_approval` is on (today: request / confirm API). + +Defer standalone **Policies** (rule catalog with thresholds), **multi-role approval chains**, and **rich audit timeline filters** until read APIs and persistence match those stories. + +### Deferred / backend-gated (do not imply in UI yet) + +- **Per-release row status** (“Blocked”, “Live”, “Rolled back”) with sortable **cost Δ / latency Δ**: “Live” can align with promoted pointers; “blocked” is **evaluation-scoped** (depends on baseline, window, environment)—not a global attribute unless we store or cache last evaluation per release. +- **Policies page** listing rules with “expected vs actual”: needs a stable **rule listing** or workspace-backed contract; today policy output is **evaluated reasons**, not necessarily a browsable catalog. +- **Approvals** as org chart (Platform → ML → Security): requires identity, roles, and workflow beyond optional promotion request/confirm. +- **Risk score** / composite **HIGH** labels: needs a defined server-side aggregate or explicit mapping from existing fields (e.g. sample confidence alone is not a full risk model). +- **Release twin** lines such as “system prompt +N tokens” unless those deltas exist on the wire from release/diff payloads. + +### Terminology note + +Treat **policy FAIL** as **do not promote this candidate under this evaluation context** (baseline + window + environment), not “this release ID is permanently blocked everywhere.” + +## Production wireframe direction (external — change → impact → policy → decision) + +This section folds **final wireframe** feedback into the same constraints as **Blueprint alignment**: useful as **layout and component targets**, not as a promise that every block exists on the wire today. + +### Thesis (keep) + +The UI should reinforce **change → impact → policy → decision**, not generic dashboards. Prefer **deepening diff causality and decision clarity** over charts and vanity metrics (already in **Non-goals**). + +### Target section stack (conceptual) + +| Section | Role | FlightDeck today (serve UI) | Next evolution | +|--------|------|----------------------------|----------------| +| Sidebar | Stable nav | `AppShell` | Optional rename **Runs → Evidence** if it helps operators without splitting routes. | +| Release header | Human anchor for the release under review | Overview `?release=` hero; Diff form IDs | Dedicated **`/release/:id`** or shared **`ReleaseHeader`** component fed by timeline + focused release. | +| Block reason banner | Unmissable “why stop” | Diff verdict banner (policy FAIL + reasons) | Optional **single-line primary reason** when server ranks or summarizes reasons. | +| Release twin (OLD vs NEW) | At-a-glance identity change | Pricing model line + rollups (Diff) | Explicit **baseline vs candidate** strip (version/agent/env + model/provider) once data is stable in **`POST /v1/diff`**. | +| Change impact analysis (expandable) | Causal / drill-down | Collapsible pricing/catalog + metric grid | **Structured change list** only when diff payload exposes comparable artifacts (prompt/tools deltas)—no invented causality. | +| Policy evaluation | Gate outcome | Verdict banner + policy reasons | Optional **`PolicyPanel`** extracting banner + evaluated_at for reuse on Actions outcomes. | +| Approvals | Human layer | **Actions** when `promotion_requires_approval` | Not multi-role org charts until backend supports it; keep **request / confirm** truthy UI. | +| Decision | Readable outcome | PASS/FAIL copy + promote CTA | **`DecisionCard`** summarizing verdict + next step (promote / fix / widen evidence). | +| Actions | Mutations | Promote / rollback / request / confirm | Same page; ensure cross-links from Diff retain window/env. | + +### Suggested components (map to repo gradually) + +Names from feedback are **targets** for extraction/refactor—not required file renames in one PR: + +- **`ReleaseHeader`** — consolidate Overview hero + future release route header. +- **`ReleaseTwin`** — thin summary row for baseline vs candidate (model/pricing/version IDs). +- **`DiffList` / change rows** — defer until **`changes[]`** (or equivalent) exists on the API. +- **`PolicyPanel`** — wrapper around policy PASS/FAIL + reasons + timestamp. +- **`ApprovalPanel`** — pending requests + confirm flow (today on Actions). +- **`DecisionCard`** — verdict + recommended action line. + +### Illustrative data shape (not current wire contract) + +A unified front-end model such as: + +```ts +// Illustrative only — do not treat as implemented HTTP schema. +type Release = { + id: string; + status: "blocked" | "ready"; + changes: Change[]; + policies: PolicyResult[]; + approvals: Approval[]; +}; +``` + +…only makes sense after the server can compute **`blocked` vs `ready`** for a **specific evaluation context** (baseline, window, environment) and optionally expose **`changes[]`**. Until then, compose views from **`TimelinePayload`**, **`POST /v1/diff`**, **`GET /v1/runs`**, and promotion APIs **without** implying a single merged **`Release`** document. + +### Hard “don’t” (reasserted) + +- Do **not** add chart-heavy dashboards or random metric walls. +- Do **not** fake approval chains or policy catalogs without API backing. + +### Relation to open UI work (e.g. PR #53 trajectory) + +Recent UI slices move toward this wireframe: **verdict-first Diff**, **collapsed deep pricing**, **promoted-first Overview**, **copy/filters**, **decision-litmus copy**. On **Diff**, the **Release twin** (baseline vs candidate + resolved model line), **blocked strip** (first policy reason), **policy evaluation card**, **decision card** (promote when PASS), and **Change impact** section align layout with **change → impact → policy → decision** without inventing API fields. + +Remaining gap is mostly **component extraction** (`ReleaseHeader`, shared panels) and **release route**, gated on contracts above. diff --git a/src/flightdeck/server/static/assets/index-BPDMrxvX.js b/src/flightdeck/server/static/assets/index-BPDMrxvX.js deleted file mode 100644 index e52c282..0000000 --- a/src/flightdeck/server/static/assets/index-BPDMrxvX.js +++ /dev/null @@ -1,11 +0,0 @@ -(function(){const f=document.createElement("link").relList;if(f&&f.supports&&f.supports("modulepreload"))return;for(const m of document.querySelectorAll('link[rel="modulepreload"]'))r(m);new MutationObserver(m=>{for(const h of m)if(h.type==="childList")for(const b of h.addedNodes)b.tagName==="LINK"&&b.rel==="modulepreload"&&r(b)}).observe(document,{childList:!0,subtree:!0});function o(m){const h={};return m.integrity&&(h.integrity=m.integrity),m.referrerPolicy&&(h.referrerPolicy=m.referrerPolicy),m.crossOrigin==="use-credentials"?h.credentials="include":m.crossOrigin==="anonymous"?h.credentials="omit":h.credentials="same-origin",h}function r(m){if(m.ep)return;m.ep=!0;const h=o(m);fetch(m.href,h)}})();var Qs={exports:{}},Vn={};var ym;function Hy(){if(ym)return Vn;ym=1;var c=Symbol.for("react.transitional.element"),f=Symbol.for("react.fragment");function o(r,m,h){var b=null;if(h!==void 0&&(b=""+h),m.key!==void 0&&(b=""+m.key),"key"in m){h={};for(var T in m)T!=="key"&&(h[T]=m[T])}else h=m;return m=h.ref,{$$typeof:c,type:r,key:b,ref:m!==void 0?m:null,props:h}}return Vn.Fragment=f,Vn.jsx=o,Vn.jsxs=o,Vn}var pm;function wy(){return pm||(pm=1,Qs.exports=Hy()),Qs.exports}var u=wy(),Xs={exports:{}},ae={};var gm;function qy(){if(gm)return ae;gm=1;var c=Symbol.for("react.transitional.element"),f=Symbol.for("react.portal"),o=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),m=Symbol.for("react.profiler"),h=Symbol.for("react.consumer"),b=Symbol.for("react.context"),T=Symbol.for("react.forward_ref"),x=Symbol.for("react.suspense"),p=Symbol.for("react.memo"),O=Symbol.for("react.lazy"),N=Symbol.for("react.activity"),B=Symbol.iterator;function q(_){return _===null||typeof _!="object"?null:(_=B&&_[B]||_["@@iterator"],typeof _=="function"?_:null)}var Z={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Q=Object.assign,L={};function V(_,M,X){this.props=_,this.context=M,this.refs=L,this.updater=X||Z}V.prototype.isReactComponent={},V.prototype.setState=function(_,M){if(typeof _!="object"&&typeof _!="function"&&_!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,_,M,"setState")},V.prototype.forceUpdate=function(_){this.updater.enqueueForceUpdate(this,_,"forceUpdate")};function F(){}F.prototype=V.prototype;function $(_,M,X){this.props=_,this.context=M,this.refs=L,this.updater=X||Z}var G=$.prototype=new F;G.constructor=$,Q(G,V.prototype),G.isPureReactComponent=!0;var ne=Array.isArray;function pe(){}var H={H:null,A:null,T:null,S:null},se=Object.prototype.hasOwnProperty;function He(_,M,X){var J=X.ref;return{$$typeof:c,type:_,key:M,ref:J!==void 0?J:null,props:X}}function de(_,M){return He(_.type,M,_.props)}function Qe(_){return typeof _=="object"&&_!==null&&_.$$typeof===c}function xe(_){var M={"=":"=0",":":"=2"};return"$"+_.replace(/[=:]/g,function(X){return M[X]})}var Ee=/\/+/g;function ze(_,M){return typeof _=="object"&&_!==null&&_.key!=null?xe(""+_.key):M.toString(36)}function De(_){switch(_.status){case"fulfilled":return _.value;case"rejected":throw _.reason;default:switch(typeof _.status=="string"?_.then(pe,pe):(_.status="pending",_.then(function(M){_.status==="pending"&&(_.status="fulfilled",_.value=M)},function(M){_.status==="pending"&&(_.status="rejected",_.reason=M)})),_.status){case"fulfilled":return _.value;case"rejected":throw _.reason}}throw _}function C(_,M,X,J,le){var ie=typeof _;(ie==="undefined"||ie==="boolean")&&(_=null);var ge=!1;if(_===null)ge=!0;else switch(ie){case"bigint":case"string":case"number":ge=!0;break;case"object":switch(_.$$typeof){case c:case f:ge=!0;break;case O:return ge=_._init,C(ge(_._payload),M,X,J,le)}}if(ge)return le=le(_),ge=J===""?"."+ze(_,0):J,ne(le)?(X="",ge!=null&&(X=ge.replace(Ee,"$&/")+"/"),C(le,M,X,"",function(oe){return oe})):le!=null&&(Qe(le)&&(le=de(le,X+(le.key==null||_&&_.key===le.key?"":(""+le.key).replace(Ee,"$&/")+"/")+ge)),M.push(le)),1;ge=0;var Ze=J===""?".":J+":";if(ne(_))for(var w=0;w<_.length;w++)J=_[w],ie=Ze+ze(J,w),ge+=C(J,M,X,ie,le);else if(w=q(_),typeof w=="function")for(_=w.call(_),w=0;!(J=_.next()).done;)J=J.value,ie=Ze+ze(J,w++),ge+=C(J,M,X,ie,le);else if(ie==="object"){if(typeof _.then=="function")return C(De(_),M,X,J,le);throw M=String(_),Error("Objects are not valid as a React child (found: "+(M==="[object Object]"?"object with keys {"+Object.keys(_).join(", ")+"}":M)+"). If you meant to render a collection of children, use an array instead.")}return ge}function Y(_,M,X){if(_==null)return _;var J=[],le=0;return C(_,J,"","",function(ie){return M.call(X,ie,le++)}),J}function P(_){if(_._status===-1){var M=_._result;M=M(),M.then(function(X){(_._status===0||_._status===-1)&&(_._status=1,_._result=X)},function(X){(_._status===0||_._status===-1)&&(_._status=2,_._result=X)}),_._status===-1&&(_._status=0,_._result=M)}if(_._status===1)return _._result.default;throw _._result}var _e=typeof reportError=="function"?reportError:function(_){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var M=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof _=="object"&&_!==null&&typeof _.message=="string"?String(_.message):String(_),error:_});if(!window.dispatchEvent(M))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",_);return}console.error(_)},ee={map:Y,forEach:function(_,M,X){Y(_,function(){M.apply(this,arguments)},X)},count:function(_){var M=0;return Y(_,function(){M++}),M},toArray:function(_){return Y(_,function(M){return M})||[]},only:function(_){if(!Qe(_))throw Error("React.Children.only expected to receive a single React element child.");return _}};return ae.Activity=N,ae.Children=ee,ae.Component=V,ae.Fragment=o,ae.Profiler=m,ae.PureComponent=$,ae.StrictMode=r,ae.Suspense=x,ae.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=H,ae.__COMPILER_RUNTIME={__proto__:null,c:function(_){return H.H.useMemoCache(_)}},ae.cache=function(_){return function(){return _.apply(null,arguments)}},ae.cacheSignal=function(){return null},ae.cloneElement=function(_,M,X){if(_==null)throw Error("The argument must be a React element, but you passed "+_+".");var J=Q({},_.props),le=_.key;if(M!=null)for(ie in M.key!==void 0&&(le=""+M.key),M)!se.call(M,ie)||ie==="key"||ie==="__self"||ie==="__source"||ie==="ref"&&M.ref===void 0||(J[ie]=M[ie]);var ie=arguments.length-2;if(ie===1)J.children=X;else if(1>>1,ee=C[_e];if(0>>1;_e<_;){var M=2*(_e+1)-1,X=C[M],J=M+1,le=C[J];if(0>m(X,P))Jm(le,X)?(C[_e]=le,C[J]=P,_e=J):(C[_e]=X,C[M]=P,_e=M);else if(Jm(le,P))C[_e]=le,C[J]=P,_e=J;else break e}}return Y}function m(C,Y){var P=C.sortIndex-Y.sortIndex;return P!==0?P:C.id-Y.id}if(c.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var h=performance;c.unstable_now=function(){return h.now()}}else{var b=Date,T=b.now();c.unstable_now=function(){return b.now()-T}}var x=[],p=[],O=1,N=null,B=3,q=!1,Z=!1,Q=!1,L=!1,V=typeof setTimeout=="function"?setTimeout:null,F=typeof clearTimeout=="function"?clearTimeout:null,$=typeof setImmediate<"u"?setImmediate:null;function G(C){for(var Y=o(p);Y!==null;){if(Y.callback===null)r(p);else if(Y.startTime<=C)r(p),Y.sortIndex=Y.expirationTime,f(x,Y);else break;Y=o(p)}}function ne(C){if(Q=!1,G(C),!Z)if(o(x)!==null)Z=!0,pe||(pe=!0,xe());else{var Y=o(p);Y!==null&&De(ne,Y.startTime-C)}}var pe=!1,H=-1,se=5,He=-1;function de(){return L?!0:!(c.unstable_now()-HeC&&de());){var _e=N.callback;if(typeof _e=="function"){N.callback=null,B=N.priorityLevel;var ee=_e(N.expirationTime<=C);if(C=c.unstable_now(),typeof ee=="function"){N.callback=ee,G(C),Y=!0;break t}N===o(x)&&r(x),G(C)}else r(x);N=o(x)}if(N!==null)Y=!0;else{var _=o(p);_!==null&&De(ne,_.startTime-C),Y=!1}}break e}finally{N=null,B=P,q=!1}Y=void 0}}finally{Y?xe():pe=!1}}}var xe;if(typeof $=="function")xe=function(){$(Qe)};else if(typeof MessageChannel<"u"){var Ee=new MessageChannel,ze=Ee.port2;Ee.port1.onmessage=Qe,xe=function(){ze.postMessage(null)}}else xe=function(){V(Qe,0)};function De(C,Y){H=V(function(){C(c.unstable_now())},Y)}c.unstable_IdlePriority=5,c.unstable_ImmediatePriority=1,c.unstable_LowPriority=4,c.unstable_NormalPriority=3,c.unstable_Profiling=null,c.unstable_UserBlockingPriority=2,c.unstable_cancelCallback=function(C){C.callback=null},c.unstable_forceFrameRate=function(C){0>C||125_e?(C.sortIndex=P,f(p,C),o(x)===null&&C===o(p)&&(Q?(F(H),H=-1):Q=!0,De(ne,P-_e))):(C.sortIndex=ee,f(x,C),Z||q||(Z=!0,pe||(pe=!0,xe()))),C},c.unstable_shouldYield=de,c.unstable_wrapCallback=function(C){var Y=B;return function(){var P=B;B=Y;try{return C.apply(this,arguments)}finally{B=P}}}})(Ks)),Ks}var xm;function By(){return xm||(xm=1,Zs.exports=Ly()),Zs.exports}var Js={exports:{}},ut={};var Sm;function Yy(){if(Sm)return ut;Sm=1;var c=nf();function f(x){var p="https://react.dev/errors/"+x;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(c)}catch(f){console.error(f)}}return c(),Js.exports=Yy(),Js.exports}var Nm;function Qy(){if(Nm)return Zn;Nm=1;var c=By(),f=nf(),o=Gy();function r(e){var t="https://react.dev/errors/"+e;if(1ee||(e.current=_e[ee],_e[ee]=null,ee--)}function X(e,t){ee++,_e[ee]=e.current,e.current=t}var J=_(null),le=_(null),ie=_(null),ge=_(null);function Ze(e,t){switch(X(ie,t),X(le,e),X(J,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Bd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Bd(t),e=Yd(t,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}M(J),X(J,e)}function w(){M(J),M(le),M(ie)}function oe(e){e.memoizedState!==null&&X(ge,e);var t=J.current,l=Yd(t,e.type);t!==l&&(X(le,e),X(J,l))}function Ue(e){le.current===e&&(M(J),M(le)),ge.current===e&&(M(ge),Yn._currentValue=P)}var K,fe;function re(e){if(K===void 0)try{throw Error()}catch(l){var t=l.stack.trim().match(/\n( *(at )?)/);K=t&&t[1]||"",fe=-1)":-1n||v[a]!==E[n]){var z=` -`+v[a].replace(" at new "," at ");return e.displayName&&z.includes("")&&(z=z.replace("",e.displayName)),z}while(1<=a&&0<=n);break}}}finally{it=!1,Error.prepareStackTrace=l}return(l=e?e.displayName||e.name:"")?re(l):""}function Pe(e,t){switch(e.tag){case 26:case 27:case 5:return re(e.type);case 16:return re("Lazy");case 13:return e.child!==t&&t!==null?re("Suspense Fallback"):re("Suspense");case 19:return re("SuspenseList");case 0:case 15:return st(e.type,!1);case 11:return st(e.type.render,!1);case 1:return st(e.type,!0);case 31:return re("Activity");default:return""}}function In(e){try{var t="",l=null;do t+=Pe(e,l),l=e,e=e.return;while(e);return t}catch(a){return` -Error generating stack: `+a.message+` -`+a.stack}}var Ru=Object.prototype.hasOwnProperty,Au=c.unstable_scheduleCallback,Ou=c.unstable_cancelCallback,mh=c.unstable_shouldYield,hh=c.unstable_requestPaint,yt=c.unstable_now,vh=c.unstable_getCurrentPriorityLevel,yf=c.unstable_ImmediatePriority,pf=c.unstable_UserBlockingPriority,Pn=c.unstable_NormalPriority,yh=c.unstable_LowPriority,gf=c.unstable_IdlePriority,ph=c.log,gh=c.unstable_setDisableYieldValue,Fa=null,pt=null;function hl(e){if(typeof ph=="function"&&gh(e),pt&&typeof pt.setStrictMode=="function")try{pt.setStrictMode(Fa,e)}catch{}}var gt=Math.clz32?Math.clz32:xh,bh=Math.log,_h=Math.LN2;function xh(e){return e>>>=0,e===0?32:31-(bh(e)/_h|0)|0}var ei=256,ti=262144,li=4194304;function Gl(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function ai(e,t,l){var a=e.pendingLanes;if(a===0)return 0;var n=0,i=e.suspendedLanes,s=e.pingedLanes;e=e.warmLanes;var d=a&134217727;return d!==0?(a=d&~i,a!==0?n=Gl(a):(s&=d,s!==0?n=Gl(s):l||(l=d&~e,l!==0&&(n=Gl(l))))):(d=a&~i,d!==0?n=Gl(d):s!==0?n=Gl(s):l||(l=a&~e,l!==0&&(n=Gl(l)))),n===0?0:t!==0&&t!==n&&(t&i)===0&&(i=n&-n,l=t&-t,i>=l||i===32&&(l&4194048)!==0)?t:n}function Ia(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Sh(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function bf(){var e=li;return li<<=1,(li&62914560)===0&&(li=4194304),e}function Cu(e){for(var t=[],l=0;31>l;l++)t.push(e);return t}function Pa(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function jh(e,t,l,a,n,i){var s=e.pendingLanes;e.pendingLanes=l,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=l,e.entangledLanes&=l,e.errorRecoveryDisabledLanes&=l,e.shellSuspendCounter=0;var d=e.entanglements,v=e.expirationTimes,E=e.hiddenUpdates;for(l=s&~l;0"u")return null;try{return e.activeElement||e.body}catch{return e.body}}var Oh=/[\n"\\]/g;function At(e){return e.replace(Oh,function(t){return"\\"+t.charCodeAt(0).toString(16)+" "})}function wu(e,t,l,a,n,i,s,d){e.name="",s!=null&&typeof s!="function"&&typeof s!="symbol"&&typeof s!="boolean"?e.type=s:e.removeAttribute("type"),t!=null?s==="number"?(t===0&&e.value===""||e.value!=t)&&(e.value=""+Rt(t)):e.value!==""+Rt(t)&&(e.value=""+Rt(t)):s!=="submit"&&s!=="reset"||e.removeAttribute("value"),t!=null?qu(e,s,Rt(t)):l!=null?qu(e,s,Rt(l)):a!=null&&e.removeAttribute("value"),n==null&&i!=null&&(e.defaultChecked=!!i),n!=null&&(e.checked=n&&typeof n!="function"&&typeof n!="symbol"),d!=null&&typeof d!="function"&&typeof d!="symbol"&&typeof d!="boolean"?e.name=""+Rt(d):e.removeAttribute("name")}function Df(e,t,l,a,n,i,s,d){if(i!=null&&typeof i!="function"&&typeof i!="symbol"&&typeof i!="boolean"&&(e.type=i),t!=null||l!=null){if(!(i!=="submit"&&i!=="reset"||t!=null)){Hu(e);return}l=l!=null?""+Rt(l):"",t=t!=null?""+Rt(t):l,d||t===e.value||(e.value=t),e.defaultValue=t}a=a??n,a=typeof a!="function"&&typeof a!="symbol"&&!!a,e.checked=d?e.checked:!!a,e.defaultChecked=!!a,s!=null&&typeof s!="function"&&typeof s!="symbol"&&typeof s!="boolean"&&(e.name=s),Hu(e)}function qu(e,t,l){t==="number"&&ui(e.ownerDocument)===e||e.defaultValue===""+l||(e.defaultValue=""+l)}function ma(e,t,l,a){if(e=e.options,t){t={};for(var n=0;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Qu=!1;if(Ft)try{var an={};Object.defineProperty(an,"passive",{get:function(){Qu=!0}}),window.addEventListener("test",an,an),window.removeEventListener("test",an,an)}catch{Qu=!1}var yl=null,Xu=null,si=null;function Bf(){if(si)return si;var e,t=Xu,l=t.length,a,n="value"in yl?yl.value:yl.textContent,i=n.length;for(e=0;e=cn),Zf=" ",Kf=!1;function Jf(e,t){switch(e){case"keyup":return av.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function kf(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var pa=!1;function iv(e,t){switch(e){case"compositionend":return kf(t);case"keypress":return t.which!==32?null:(Kf=!0,Zf);case"textInput":return e=t.data,e===Zf&&Kf?null:e;default:return null}}function uv(e,t){if(pa)return e==="compositionend"||!ku&&Jf(e,t)?(e=Bf(),si=Xu=yl=null,pa=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:l,offset:t-e};e=a}e:{for(;l;){if(l.nextSibling){l=l.nextSibling;break e}l=l.parentNode}l=void 0}l=lr(l)}}function nr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?nr(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function ir(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=ui(e.document);t instanceof e.HTMLIFrameElement;){try{var l=typeof t.contentWindow.location.href=="string"}catch{l=!1}if(l)e=t.contentWindow;else break;t=ui(e.document)}return t}function Fu(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}var hv=Ft&&"documentMode"in document&&11>=document.documentMode,ga=null,Iu=null,on=null,Pu=!1;function ur(e,t,l){var a=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;Pu||ga==null||ga!==ui(a)||(a=ga,"selectionStart"in a&&Fu(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),on&&rn(on,a)||(on=a,a=tu(Iu,"onSelect"),0>=s,n-=s,Vt=1<<32-gt(t)+n|l<ce?(ye=W,W=null):ye=W.sibling;var je=R(S,W,j[ce],D);if(je===null){W===null&&(W=ye);break}e&&W&&je.alternate===null&&t(S,W),g=i(je,g,ce),Se===null?I=je:Se.sibling=je,Se=je,W=ye}if(ce===j.length)return l(S,W),be&&Pt(S,ce),I;if(W===null){for(;cece?(ye=W,W=null):ye=W.sibling;var Ll=R(S,W,je.value,D);if(Ll===null){W===null&&(W=ye);break}e&&W&&Ll.alternate===null&&t(S,W),g=i(Ll,g,ce),Se===null?I=Ll:Se.sibling=Ll,Se=Ll,W=ye}if(je.done)return l(S,W),be&&Pt(S,ce),I;if(W===null){for(;!je.done;ce++,je=j.next())je=U(S,je.value,D),je!==null&&(g=i(je,g,ce),Se===null?I=je:Se.sibling=je,Se=je);return be&&Pt(S,ce),I}for(W=a(W);!je.done;ce++,je=j.next())je=A(W,S,ce,je.value,D),je!==null&&(e&&je.alternate!==null&&W.delete(je.key===null?ce:je.key),g=i(je,g,ce),Se===null?I=je:Se.sibling=je,Se=je);return e&&W.forEach(function(Uy){return t(S,Uy)}),be&&Pt(S,ce),I}function Ce(S,g,j,D){if(typeof j=="object"&&j!==null&&j.type===Q&&j.key===null&&(j=j.props.children),typeof j=="object"&&j!==null){switch(j.$$typeof){case q:e:{for(var I=j.key;g!==null;){if(g.key===I){if(I=j.type,I===Q){if(g.tag===7){l(S,g.sibling),D=n(g,j.props.children),D.return=S,S=D;break e}}else if(g.elementType===I||typeof I=="object"&&I!==null&&I.$$typeof===se&&Il(I)===g.type){l(S,g.sibling),D=n(g,j.props),pn(D,j),D.return=S,S=D;break e}l(S,g);break}else t(S,g);g=g.sibling}j.type===Q?(D=Jl(j.props.children,S.mode,D,j.key),D.return=S,S=D):(D=gi(j.type,j.key,j.props,null,S.mode,D),pn(D,j),D.return=S,S=D)}return s(S);case Z:e:{for(I=j.key;g!==null;){if(g.key===I)if(g.tag===4&&g.stateNode.containerInfo===j.containerInfo&&g.stateNode.implementation===j.implementation){l(S,g.sibling),D=n(g,j.children||[]),D.return=S,S=D;break e}else{l(S,g);break}else t(S,g);g=g.sibling}D=uc(j,S.mode,D),D.return=S,S=D}return s(S);case se:return j=Il(j),Ce(S,g,j,D)}if(De(j))return k(S,g,j,D);if(xe(j)){if(I=xe(j),typeof I!="function")throw Error(r(150));return j=I.call(j),te(S,g,j,D)}if(typeof j.then=="function")return Ce(S,g,Ei(j),D);if(j.$$typeof===$)return Ce(S,g,xi(S,j),D);Ti(S,j)}return typeof j=="string"&&j!==""||typeof j=="number"||typeof j=="bigint"?(j=""+j,g!==null&&g.tag===6?(l(S,g.sibling),D=n(g,j),D.return=S,S=D):(l(S,g),D=ic(j,S.mode,D),D.return=S,S=D),s(S)):l(S,g)}return function(S,g,j,D){try{yn=0;var I=Ce(S,g,j,D);return Oa=null,I}catch(W){if(W===Aa||W===ji)throw W;var Se=_t(29,W,null,S.mode);return Se.lanes=D,Se.return=S,Se}}}var ea=Or(!0),Cr=Or(!1),xl=!1;function gc(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function bc(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Sl(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function jl(e,t,l){var a=e.updateQueue;if(a===null)return null;if(a=a.shared,(Ne&2)!==0){var n=a.pending;return n===null?t.next=t:(t.next=n.next,n.next=t),a.pending=t,t=pi(e),mr(e,null,l),t}return yi(e,a,t,l),pi(e)}function gn(e,t,l){if(t=t.updateQueue,t!==null&&(t=t.shared,(l&4194048)!==0)){var a=t.lanes;a&=e.pendingLanes,l|=a,t.lanes=l,xf(e,l)}}function _c(e,t){var l=e.updateQueue,a=e.alternate;if(a!==null&&(a=a.updateQueue,l===a)){var n=null,i=null;if(l=l.firstBaseUpdate,l!==null){do{var s={lane:l.lane,tag:l.tag,payload:l.payload,callback:null,next:null};i===null?n=i=s:i=i.next=s,l=l.next}while(l!==null);i===null?n=i=t:i=i.next=t}else n=i=t;l={baseState:a.baseState,firstBaseUpdate:n,lastBaseUpdate:i,shared:a.shared,callbacks:a.callbacks},e.updateQueue=l;return}e=l.lastBaseUpdate,e===null?l.firstBaseUpdate=t:e.next=t,l.lastBaseUpdate=t}var xc=!1;function bn(){if(xc){var e=Ra;if(e!==null)throw e}}function _n(e,t,l,a){xc=!1;var n=e.updateQueue;xl=!1;var i=n.firstBaseUpdate,s=n.lastBaseUpdate,d=n.shared.pending;if(d!==null){n.shared.pending=null;var v=d,E=v.next;v.next=null,s===null?i=E:s.next=E,s=v;var z=e.alternate;z!==null&&(z=z.updateQueue,d=z.lastBaseUpdate,d!==s&&(d===null?z.firstBaseUpdate=E:d.next=E,z.lastBaseUpdate=v))}if(i!==null){var U=n.baseState;s=0,z=E=v=null,d=i;do{var R=d.lane&-536870913,A=R!==d.lane;if(A?(ve&R)===R:(a&R)===R){R!==0&&R===Ta&&(xc=!0),z!==null&&(z=z.next={lane:0,tag:d.tag,payload:d.payload,callback:null,next:null});e:{var k=e,te=d;R=t;var Ce=l;switch(te.tag){case 1:if(k=te.payload,typeof k=="function"){U=k.call(Ce,U,R);break e}U=k;break e;case 3:k.flags=k.flags&-65537|128;case 0:if(k=te.payload,R=typeof k=="function"?k.call(Ce,U,R):k,R==null)break e;U=N({},U,R);break e;case 2:xl=!0}}R=d.callback,R!==null&&(e.flags|=64,A&&(e.flags|=8192),A=n.callbacks,A===null?n.callbacks=[R]:A.push(R))}else A={lane:R,tag:d.tag,payload:d.payload,callback:d.callback,next:null},z===null?(E=z=A,v=U):z=z.next=A,s|=R;if(d=d.next,d===null){if(d=n.shared.pending,d===null)break;A=d,d=A.next,A.next=null,n.lastBaseUpdate=A,n.shared.pending=null}}while(!0);z===null&&(v=U),n.baseState=v,n.firstBaseUpdate=E,n.lastBaseUpdate=z,i===null&&(n.shared.lanes=0),Al|=s,e.lanes=s,e.memoizedState=U}}function zr(e,t){if(typeof e!="function")throw Error(r(191,e));e.call(t)}function Dr(e,t){var l=e.callbacks;if(l!==null)for(e.callbacks=null,e=0;ei?i:8;var s=C.T,d={};C.T=d,Yc(e,!1,t,l);try{var v=n(),E=C.S;if(E!==null&&E(d,v),v!==null&&typeof v=="object"&&typeof v.then=="function"){var z=jv(v,a);jn(e,t,z,Et(e))}else jn(e,t,a,Et(e))}catch(U){jn(e,t,{then:function(){},status:"rejected",reason:U},Et())}finally{Y.p=i,s!==null&&d.types!==null&&(s.types=d.types),C.T=s}}function Ov(){}function Lc(e,t,l,a){if(e.tag!==5)throw Error(r(476));var n=ro(e).queue;fo(e,n,t,P,l===null?Ov:function(){return oo(e),l(a)})}function ro(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:P,baseState:P,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:al,lastRenderedState:P},next:null};var l={};return t.next={memoizedState:l,baseState:l,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:al,lastRenderedState:l},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function oo(e){var t=ro(e);t.next===null&&(t=e.alternate.memoizedState),jn(e,t.next.queue,{},Et())}function Bc(){return lt(Yn)}function mo(){return Ve().memoizedState}function ho(){return Ve().memoizedState}function Cv(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var l=Et();e=Sl(l);var a=jl(t,e,l);a!==null&&(vt(a,t,l),gn(a,t,l)),t={cache:hc()},e.payload=t;return}t=t.return}}function zv(e,t,l){var a=Et();l={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null},wi(e)?yo(t,l):(l=ac(e,t,l,a),l!==null&&(vt(l,e,a),po(l,t,a)))}function vo(e,t,l){var a=Et();jn(e,t,l,a)}function jn(e,t,l,a){var n={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null};if(wi(e))yo(t,n);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var s=t.lastRenderedState,d=i(s,l);if(n.hasEagerState=!0,n.eagerState=d,bt(d,s))return yi(e,t,n,0),Me===null&&vi(),!1}catch{}if(l=ac(e,t,n,a),l!==null)return vt(l,e,a),po(l,t,a),!0}return!1}function Yc(e,t,l,a){if(a={lane:2,revertLane:gs(),gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null},wi(e)){if(t)throw Error(r(479))}else t=ac(e,l,a,2),t!==null&&vt(t,e,2)}function wi(e){var t=e.alternate;return e===ue||t!==null&&t===ue}function yo(e,t){za=Oi=!0;var l=e.pending;l===null?t.next=t:(t.next=l.next,l.next=t),e.pending=t}function po(e,t,l){if((l&4194048)!==0){var a=t.lanes;a&=e.pendingLanes,l|=a,t.lanes=l,xf(e,l)}}var Nn={readContext:lt,use:Di,useCallback:Ye,useContext:Ye,useEffect:Ye,useImperativeHandle:Ye,useLayoutEffect:Ye,useInsertionEffect:Ye,useMemo:Ye,useReducer:Ye,useRef:Ye,useState:Ye,useDebugValue:Ye,useDeferredValue:Ye,useTransition:Ye,useSyncExternalStore:Ye,useId:Ye,useHostTransitionStatus:Ye,useFormState:Ye,useActionState:Ye,useOptimistic:Ye,useMemoCache:Ye,useCacheRefresh:Ye};Nn.useEffectEvent=Ye;var go={readContext:lt,use:Di,useCallback:function(e,t){return ct().memoizedState=[e,t===void 0?null:t],e},useContext:lt,useEffect:eo,useImperativeHandle:function(e,t,l){l=l!=null?l.concat([e]):null,Ui(4194308,4,no.bind(null,t,e),l)},useLayoutEffect:function(e,t){return Ui(4194308,4,e,t)},useInsertionEffect:function(e,t){Ui(4,2,e,t)},useMemo:function(e,t){var l=ct();t=t===void 0?null:t;var a=e();if(ta){hl(!0);try{e()}finally{hl(!1)}}return l.memoizedState=[a,t],a},useReducer:function(e,t,l){var a=ct();if(l!==void 0){var n=l(t);if(ta){hl(!0);try{l(t)}finally{hl(!1)}}}else n=t;return a.memoizedState=a.baseState=n,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:n},a.queue=e,e=e.dispatch=zv.bind(null,ue,e),[a.memoizedState,e]},useRef:function(e){var t=ct();return e={current:e},t.memoizedState=e},useState:function(e){e=Mc(e);var t=e.queue,l=vo.bind(null,ue,t);return t.dispatch=l,[e.memoizedState,l]},useDebugValue:wc,useDeferredValue:function(e,t){var l=ct();return qc(l,e,t)},useTransition:function(){var e=Mc(!1);return e=fo.bind(null,ue,e.queue,!0,!1),ct().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,l){var a=ue,n=ct();if(be){if(l===void 0)throw Error(r(407));l=l()}else{if(l=t(),Me===null)throw Error(r(349));(ve&127)!==0||Lr(a,t,l)}n.memoizedState=l;var i={value:l,getSnapshot:t};return n.queue=i,eo(Yr.bind(null,a,i,e),[e]),a.flags|=2048,Ma(9,{destroy:void 0},Br.bind(null,a,i,l,t),null),l},useId:function(){var e=ct(),t=Me.identifierPrefix;if(be){var l=Zt,a=Vt;l=(a&~(1<<32-gt(a)-1)).toString(32)+l,t="_"+t+"R_"+l,l=Ci++,0<\/script>",i=i.removeChild(i.firstChild);break;case"select":i=typeof a.is=="string"?s.createElement("select",{is:a.is}):s.createElement("select"),a.multiple?i.multiple=!0:a.size&&(i.size=a.size);break;default:i=typeof a.is=="string"?s.createElement(n,{is:a.is}):s.createElement(n)}}i[et]=t,i[ft]=a;e:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)i.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break e;for(;s.sibling===null;){if(s.return===null||s.return===t)break e;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=i;e:switch(nt(i,n,a),n){case"button":case"input":case"select":case"textarea":a=!!a.autoFocus;break e;case"img":a=!0;break e;default:a=!1}a&&il(t)}}return qe(t),es(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,l),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==a&&il(t);else{if(typeof a!="string"&&t.stateNode===null)throw Error(r(166));if(e=ie.current,Na(t)){if(e=t.stateNode,l=t.memoizedProps,a=null,n=tt,n!==null)switch(n.tag){case 27:case 5:a=n.memoizedProps}e[et]=t,e=!!(e.nodeValue===l||a!==null&&a.suppressHydrationWarning===!0||qd(e.nodeValue,l)),e||bl(t,!0)}else e=lu(e).createTextNode(a),e[et]=t,t.stateNode=e}return qe(t),null;case 31:if(l=t.memoizedState,e===null||e.memoizedState!==null){if(a=Na(t),l!==null){if(e===null){if(!a)throw Error(r(318));if(e=t.memoizedState,e=e!==null?e.dehydrated:null,!e)throw Error(r(557));e[et]=t}else kl(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;qe(t),e=!1}else l=rc(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=l),e=!0;if(!e)return t.flags&256?(St(t),t):(St(t),null);if((t.flags&128)!==0)throw Error(r(558))}return qe(t),null;case 13:if(a=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(n=Na(t),a!==null&&a.dehydrated!==null){if(e===null){if(!n)throw Error(r(318));if(n=t.memoizedState,n=n!==null?n.dehydrated:null,!n)throw Error(r(317));n[et]=t}else kl(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;qe(t),n=!1}else n=rc(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),n=!0;if(!n)return t.flags&256?(St(t),t):(St(t),null)}return St(t),(t.flags&128)!==0?(t.lanes=l,t):(l=a!==null,e=e!==null&&e.memoizedState!==null,l&&(a=t.child,n=null,a.alternate!==null&&a.alternate.memoizedState!==null&&a.alternate.memoizedState.cachePool!==null&&(n=a.alternate.memoizedState.cachePool.pool),i=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(i=a.memoizedState.cachePool.pool),i!==n&&(a.flags|=2048)),l!==e&&l&&(t.child.flags|=8192),Gi(t,t.updateQueue),qe(t),null);case 4:return w(),e===null&&Ss(t.stateNode.containerInfo),qe(t),null;case 10:return tl(t.type),qe(t),null;case 19:if(M(Xe),a=t.memoizedState,a===null)return qe(t),null;if(n=(t.flags&128)!==0,i=a.rendering,i===null)if(n)Tn(a,!1);else{if(Ge!==0||e!==null&&(e.flags&128)!==0)for(e=t.child;e!==null;){if(i=Ai(e),i!==null){for(t.flags|=128,Tn(a,!1),e=i.updateQueue,t.updateQueue=e,Gi(t,e),t.subtreeFlags=0,e=l,l=t.child;l!==null;)hr(l,e),l=l.sibling;return X(Xe,Xe.current&1|2),be&&Pt(t,a.treeForkCount),t.child}e=e.sibling}a.tail!==null&&yt()>Ki&&(t.flags|=128,n=!0,Tn(a,!1),t.lanes=4194304)}else{if(!n)if(e=Ai(i),e!==null){if(t.flags|=128,n=!0,e=e.updateQueue,t.updateQueue=e,Gi(t,e),Tn(a,!0),a.tail===null&&a.tailMode==="hidden"&&!i.alternate&&!be)return qe(t),null}else 2*yt()-a.renderingStartTime>Ki&&l!==536870912&&(t.flags|=128,n=!0,Tn(a,!1),t.lanes=4194304);a.isBackwards?(i.sibling=t.child,t.child=i):(e=a.last,e!==null?e.sibling=i:t.child=i,a.last=i)}return a.tail!==null?(e=a.tail,a.rendering=e,a.tail=e.sibling,a.renderingStartTime=yt(),e.sibling=null,l=Xe.current,X(Xe,n?l&1|2:l&1),be&&Pt(t,a.treeForkCount),e):(qe(t),null);case 22:case 23:return St(t),jc(),a=t.memoizedState!==null,e!==null?e.memoizedState!==null!==a&&(t.flags|=8192):a&&(t.flags|=8192),a?(l&536870912)!==0&&(t.flags&128)===0&&(qe(t),t.subtreeFlags&6&&(t.flags|=8192)):qe(t),l=t.updateQueue,l!==null&&Gi(t,l.retryQueue),l=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(l=e.memoizedState.cachePool.pool),a=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(a=t.memoizedState.cachePool.pool),a!==l&&(t.flags|=2048),e!==null&&M(Fl),null;case 24:return l=null,e!==null&&(l=e.memoizedState.cache),t.memoizedState.cache!==l&&(t.flags|=2048),tl(Ke),qe(t),null;case 25:return null;case 30:return null}throw Error(r(156,t.tag))}function wv(e,t){switch(sc(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return tl(Ke),w(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return Ue(t),null;case 31:if(t.memoizedState!==null){if(St(t),t.alternate===null)throw Error(r(340));kl()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(St(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(r(340));kl()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return M(Xe),null;case 4:return w(),null;case 10:return tl(t.type),null;case 22:case 23:return St(t),jc(),e!==null&&M(Fl),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return tl(Ke),null;case 25:return null;default:return null}}function Qo(e,t){switch(sc(t),t.tag){case 3:tl(Ke),w();break;case 26:case 27:case 5:Ue(t);break;case 4:w();break;case 31:t.memoizedState!==null&&St(t);break;case 13:St(t);break;case 19:M(Xe);break;case 10:tl(t.type);break;case 22:case 23:St(t),jc(),e!==null&&M(Fl);break;case 24:tl(Ke)}}function Rn(e,t){try{var l=t.updateQueue,a=l!==null?l.lastEffect:null;if(a!==null){var n=a.next;l=n;do{if((l.tag&e)===e){a=void 0;var i=l.create,s=l.inst;a=i(),s.destroy=a}l=l.next}while(l!==n)}}catch(d){Re(t,t.return,d)}}function Tl(e,t,l){try{var a=t.updateQueue,n=a!==null?a.lastEffect:null;if(n!==null){var i=n.next;a=i;do{if((a.tag&e)===e){var s=a.inst,d=s.destroy;if(d!==void 0){s.destroy=void 0,n=t;var v=l,E=d;try{E()}catch(z){Re(n,v,z)}}}a=a.next}while(a!==i)}}catch(z){Re(t,t.return,z)}}function Xo(e){var t=e.updateQueue;if(t!==null){var l=e.stateNode;try{Dr(t,l)}catch(a){Re(e,e.return,a)}}}function Vo(e,t,l){l.props=la(e.type,e.memoizedProps),l.state=e.memoizedState;try{l.componentWillUnmount()}catch(a){Re(e,t,a)}}function An(e,t){try{var l=e.ref;if(l!==null){switch(e.tag){case 26:case 27:case 5:var a=e.stateNode;break;case 30:a=e.stateNode;break;default:a=e.stateNode}typeof l=="function"?e.refCleanup=l(a):l.current=a}}catch(n){Re(e,t,n)}}function Kt(e,t){var l=e.ref,a=e.refCleanup;if(l!==null)if(typeof a=="function")try{a()}catch(n){Re(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof l=="function")try{l(null)}catch(n){Re(e,t,n)}else l.current=null}function Zo(e){var t=e.type,l=e.memoizedProps,a=e.stateNode;try{e:switch(t){case"button":case"input":case"select":case"textarea":l.autoFocus&&a.focus();break e;case"img":l.src?a.src=l.src:l.srcSet&&(a.srcset=l.srcSet)}}catch(n){Re(e,e.return,n)}}function ts(e,t,l){try{var a=e.stateNode;ny(a,e.type,l,t),a[ft]=t}catch(n){Re(e,e.return,n)}}function Ko(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Ml(e.type)||e.tag===4}function ls(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Ko(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Ml(e.type)||e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function as(e,t,l){var a=e.tag;if(a===5||a===6)e=e.stateNode,t?(l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l).insertBefore(e,t):(t=l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l,t.appendChild(e),l=l._reactRootContainer,l!=null||t.onclick!==null||(t.onclick=Wt));else if(a!==4&&(a===27&&Ml(e.type)&&(l=e.stateNode,t=null),e=e.child,e!==null))for(as(e,t,l),e=e.sibling;e!==null;)as(e,t,l),e=e.sibling}function Qi(e,t,l){var a=e.tag;if(a===5||a===6)e=e.stateNode,t?l.insertBefore(e,t):l.appendChild(e);else if(a!==4&&(a===27&&Ml(e.type)&&(l=e.stateNode),e=e.child,e!==null))for(Qi(e,t,l),e=e.sibling;e!==null;)Qi(e,t,l),e=e.sibling}function Jo(e){var t=e.stateNode,l=e.memoizedProps;try{for(var a=e.type,n=t.attributes;n.length;)t.removeAttributeNode(n[0]);nt(t,a,l),t[et]=e,t[ft]=l}catch(i){Re(e,e.return,i)}}var ul=!1,$e=!1,ns=!1,ko=typeof WeakSet=="function"?WeakSet:Set,Ie=null;function qv(e,t){if(e=e.containerInfo,Es=fu,e=ir(e),Fu(e)){if("selectionStart"in e)var l={start:e.selectionStart,end:e.selectionEnd};else e:{l=(l=e.ownerDocument)&&l.defaultView||window;var a=l.getSelection&&l.getSelection();if(a&&a.rangeCount!==0){l=a.anchorNode;var n=a.anchorOffset,i=a.focusNode;a=a.focusOffset;try{l.nodeType,i.nodeType}catch{l=null;break e}var s=0,d=-1,v=-1,E=0,z=0,U=e,R=null;t:for(;;){for(var A;U!==l||n!==0&&U.nodeType!==3||(d=s+n),U!==i||a!==0&&U.nodeType!==3||(v=s+a),U.nodeType===3&&(s+=U.nodeValue.length),(A=U.firstChild)!==null;)R=U,U=A;for(;;){if(U===e)break t;if(R===l&&++E===n&&(d=s),R===i&&++z===a&&(v=s),(A=U.nextSibling)!==null)break;U=R,R=U.parentNode}U=A}l=d===-1||v===-1?null:{start:d,end:v}}else l=null}l=l||{start:0,end:0}}else l=null;for(Ts={focusedElem:e,selectionRange:l},fu=!1,Ie=t;Ie!==null;)if(t=Ie,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,Ie=e;else for(;Ie!==null;){switch(t=Ie,i=t.alternate,e=t.flags,t.tag){case 0:if((e&4)!==0&&(e=t.updateQueue,e=e!==null?e.events:null,e!==null))for(l=0;l title"))),nt(i,a,l),i[et]=e,Fe(i),a=i;break e;case"link":var s=em("link","href",n).get(a+(l.href||""));if(s){for(var d=0;dCe&&(s=Ce,Ce=te,te=s);var S=ar(d,te),g=ar(d,Ce);if(S&&g&&(A.rangeCount!==1||A.anchorNode!==S.node||A.anchorOffset!==S.offset||A.focusNode!==g.node||A.focusOffset!==g.offset)){var j=U.createRange();j.setStart(S.node,S.offset),A.removeAllRanges(),te>Ce?(A.addRange(j),A.extend(g.node,g.offset)):(j.setEnd(g.node,g.offset),A.addRange(j))}}}}for(U=[],A=d;A=A.parentNode;)A.nodeType===1&&U.push({element:A,left:A.scrollLeft,top:A.scrollTop});for(typeof d.focus=="function"&&d.focus(),d=0;dl?32:l,C.T=null,l=os,os=null;var i=Cl,s=ol;if(We=0,La=Cl=null,ol=0,(Ne&6)!==0)throw Error(r(331));var d=Ne;if(Ne|=4,id(i.current),ld(i,i.current,s,l),Ne=d,Un(0,!1),pt&&typeof pt.onPostCommitFiberRoot=="function")try{pt.onPostCommitFiberRoot(Fa,i)}catch{}return!0}finally{Y.p=n,C.T=a,jd(e,t)}}function Ed(e,t,l){t=Ct(l,t),t=Vc(e.stateNode,t,2),e=jl(e,t,2),e!==null&&(Pa(e,2),Jt(e))}function Re(e,t,l){if(e.tag===3)Ed(e,e,l);else for(;t!==null;){if(t.tag===3){Ed(t,e,l);break}else if(t.tag===1){var a=t.stateNode;if(typeof t.type.getDerivedStateFromError=="function"||typeof a.componentDidCatch=="function"&&(Ol===null||!Ol.has(a))){e=Ct(l,e),l=To(2),a=jl(t,l,2),a!==null&&(Ro(l,a,t,e),Pa(a,2),Jt(a));break}}t=t.return}}function vs(e,t,l){var a=e.pingCache;if(a===null){a=e.pingCache=new Yv;var n=new Set;a.set(t,n)}else n=a.get(t),n===void 0&&(n=new Set,a.set(t,n));n.has(l)||(cs=!0,n.add(l),e=Zv.bind(null,e,t,l),t.then(e,e))}function Zv(e,t,l){var a=e.pingCache;a!==null&&a.delete(t),e.pingedLanes|=e.suspendedLanes&l,e.warmLanes&=~l,Me===e&&(ve&l)===l&&(Ge===4||Ge===3&&(ve&62914560)===ve&&300>yt()-Zi?(Ne&2)===0&&Ba(e,0):ss|=l,qa===ve&&(qa=0)),Jt(e)}function Td(e,t){t===0&&(t=bf()),e=Kl(e,t),e!==null&&(Pa(e,t),Jt(e))}function Kv(e){var t=e.memoizedState,l=0;t!==null&&(l=t.retryLane),Td(e,l)}function Jv(e,t){var l=0;switch(e.tag){case 31:case 13:var a=e.stateNode,n=e.memoizedState;n!==null&&(l=n.retryLane);break;case 19:a=e.stateNode;break;case 22:a=e.stateNode._retryCache;break;default:throw Error(r(314))}a!==null&&a.delete(t),Td(e,l)}function kv(e,t){return Au(e,t)}var Ii=null,Ga=null,ys=!1,Pi=!1,ps=!1,Dl=0;function Jt(e){e!==Ga&&e.next===null&&(Ga===null?Ii=Ga=e:Ga=Ga.next=e),Pi=!0,ys||(ys=!0,Wv())}function Un(e,t){if(!ps&&Pi){ps=!0;do for(var l=!1,a=Ii;a!==null;){if(e!==0){var n=a.pendingLanes;if(n===0)var i=0;else{var s=a.suspendedLanes,d=a.pingedLanes;i=(1<<31-gt(42|e)+1)-1,i&=n&~(s&~d),i=i&201326741?i&201326741|1:i?i|2:0}i!==0&&(l=!0,Cd(a,i))}else i=ve,i=ai(a,a===Me?i:0,a.cancelPendingCommit!==null||a.timeoutHandle!==-1),(i&3)===0||Ia(a,i)||(l=!0,Cd(a,i));a=a.next}while(l);ps=!1}}function $v(){Rd()}function Rd(){Pi=ys=!1;var e=0;Dl!==0&&uy()&&(e=Dl);for(var t=yt(),l=null,a=Ii;a!==null;){var n=a.next,i=Ad(a,t);i===0?(a.next=null,l===null?Ii=n:l.next=n,n===null&&(Ga=l)):(l=a,(e!==0||(i&3)!==0)&&(Pi=!0)),a=n}We!==0&&We!==5||Un(e),Dl!==0&&(Dl=0)}function Ad(e,t){for(var l=e.suspendedLanes,a=e.pingedLanes,n=e.expirationTimes,i=e.pendingLanes&-62914561;0d)break;var z=v.transferSize,U=v.initiatorType;z&&Ld(U)&&(v=v.responseEnd,s+=z*(v"u"?null:document;function Wd(e,t,l){var a=Qa;if(a&&typeof t=="string"&&t){var n=At(t);n='link[rel="'+e+'"][href="'+n+'"]',typeof l=="string"&&(n+='[crossorigin="'+l+'"]'),$d.has(n)||($d.add(n),e={rel:e,crossOrigin:l,href:t},a.querySelector(n)===null&&(t=a.createElement("link"),nt(t,"link",e),Fe(t),a.head.appendChild(t)))}}function vy(e){dl.D(e),Wd("dns-prefetch",e,null)}function yy(e,t){dl.C(e,t),Wd("preconnect",e,t)}function py(e,t,l){dl.L(e,t,l);var a=Qa;if(a&&e&&t){var n='link[rel="preload"][as="'+At(t)+'"]';t==="image"&&l&&l.imageSrcSet?(n+='[imagesrcset="'+At(l.imageSrcSet)+'"]',typeof l.imageSizes=="string"&&(n+='[imagesizes="'+At(l.imageSizes)+'"]')):n+='[href="'+At(e)+'"]';var i=n;switch(t){case"style":i=Xa(e);break;case"script":i=Va(e)}wt.has(i)||(e=N({rel:"preload",href:t==="image"&&l&&l.imageSrcSet?void 0:e,as:t},l),wt.set(i,e),a.querySelector(n)!==null||t==="style"&&a.querySelector(Ln(i))||t==="script"&&a.querySelector(Bn(i))||(t=a.createElement("link"),nt(t,"link",e),Fe(t),a.head.appendChild(t)))}}function gy(e,t){dl.m(e,t);var l=Qa;if(l&&e){var a=t&&typeof t.as=="string"?t.as:"script",n='link[rel="modulepreload"][as="'+At(a)+'"][href="'+At(e)+'"]',i=n;switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":i=Va(e)}if(!wt.has(i)&&(e=N({rel:"modulepreload",href:e},t),wt.set(i,e),l.querySelector(n)===null)){switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(l.querySelector(Bn(i)))return}a=l.createElement("link"),nt(a,"link",e),Fe(a),l.head.appendChild(a)}}}function by(e,t,l){dl.S(e,t,l);var a=Qa;if(a&&e){var n=oa(a).hoistableStyles,i=Xa(e);t=t||"default";var s=n.get(i);if(!s){var d={loading:0,preload:null};if(s=a.querySelector(Ln(i)))d.loading=5;else{e=N({rel:"stylesheet",href:e,"data-precedence":t},l),(l=wt.get(i))&&Ms(e,l);var v=s=a.createElement("link");Fe(v),nt(v,"link",e),v._p=new Promise(function(E,z){v.onload=E,v.onerror=z}),v.addEventListener("load",function(){d.loading|=1}),v.addEventListener("error",function(){d.loading|=2}),d.loading|=4,nu(s,t,a)}s={type:"stylesheet",instance:s,count:1,state:d},n.set(i,s)}}}function _y(e,t){dl.X(e,t);var l=Qa;if(l&&e){var a=oa(l).hoistableScripts,n=Va(e),i=a.get(n);i||(i=l.querySelector(Bn(n)),i||(e=N({src:e,async:!0},t),(t=wt.get(n))&&Us(e,t),i=l.createElement("script"),Fe(i),nt(i,"link",e),l.head.appendChild(i)),i={type:"script",instance:i,count:1,state:null},a.set(n,i))}}function xy(e,t){dl.M(e,t);var l=Qa;if(l&&e){var a=oa(l).hoistableScripts,n=Va(e),i=a.get(n);i||(i=l.querySelector(Bn(n)),i||(e=N({src:e,async:!0,type:"module"},t),(t=wt.get(n))&&Us(e,t),i=l.createElement("script"),Fe(i),nt(i,"link",e),l.head.appendChild(i)),i={type:"script",instance:i,count:1,state:null},a.set(n,i))}}function Fd(e,t,l,a){var n=(n=ie.current)?au(n):null;if(!n)throw Error(r(446));switch(e){case"meta":case"title":return null;case"style":return typeof l.precedence=="string"&&typeof l.href=="string"?(t=Xa(l.href),l=oa(n).hoistableStyles,a=l.get(t),a||(a={type:"style",instance:null,count:0,state:null},l.set(t,a)),a):{type:"void",instance:null,count:0,state:null};case"link":if(l.rel==="stylesheet"&&typeof l.href=="string"&&typeof l.precedence=="string"){e=Xa(l.href);var i=oa(n).hoistableStyles,s=i.get(e);if(s||(n=n.ownerDocument||n,s={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},i.set(e,s),(i=n.querySelector(Ln(e)))&&!i._p&&(s.instance=i,s.state.loading=5),wt.has(e)||(l={rel:"preload",as:"style",href:l.href,crossOrigin:l.crossOrigin,integrity:l.integrity,media:l.media,hrefLang:l.hrefLang,referrerPolicy:l.referrerPolicy},wt.set(e,l),i||Sy(n,e,l,s.state))),t&&a===null)throw Error(r(528,""));return s}if(t&&a!==null)throw Error(r(529,""));return null;case"script":return t=l.async,l=l.src,typeof l=="string"&&t&&typeof t!="function"&&typeof t!="symbol"?(t=Va(l),l=oa(n).hoistableScripts,a=l.get(t),a||(a={type:"script",instance:null,count:0,state:null},l.set(t,a)),a):{type:"void",instance:null,count:0,state:null};default:throw Error(r(444,e))}}function Xa(e){return'href="'+At(e)+'"'}function Ln(e){return'link[rel="stylesheet"]['+e+"]"}function Id(e){return N({},e,{"data-precedence":e.precedence,precedence:null})}function Sy(e,t,l,a){e.querySelector('link[rel="preload"][as="style"]['+t+"]")?a.loading=1:(t=e.createElement("link"),a.preload=t,t.addEventListener("load",function(){return a.loading|=1}),t.addEventListener("error",function(){return a.loading|=2}),nt(t,"link",l),Fe(t),e.head.appendChild(t))}function Va(e){return'[src="'+At(e)+'"]'}function Bn(e){return"script[async]"+e}function Pd(e,t,l){if(t.count++,t.instance===null)switch(t.type){case"style":var a=e.querySelector('style[data-href~="'+At(l.href)+'"]');if(a)return t.instance=a,Fe(a),a;var n=N({},l,{"data-href":l.href,"data-precedence":l.precedence,href:null,precedence:null});return a=(e.ownerDocument||e).createElement("style"),Fe(a),nt(a,"style",n),nu(a,l.precedence,e),t.instance=a;case"stylesheet":n=Xa(l.href);var i=e.querySelector(Ln(n));if(i)return t.state.loading|=4,t.instance=i,Fe(i),i;a=Id(l),(n=wt.get(n))&&Ms(a,n),i=(e.ownerDocument||e).createElement("link"),Fe(i);var s=i;return s._p=new Promise(function(d,v){s.onload=d,s.onerror=v}),nt(i,"link",a),t.state.loading|=4,nu(i,l.precedence,e),t.instance=i;case"script":return i=Va(l.src),(n=e.querySelector(Bn(i)))?(t.instance=n,Fe(n),n):(a=l,(n=wt.get(i))&&(a=N({},l),Us(a,n)),e=e.ownerDocument||e,n=e.createElement("script"),Fe(n),nt(n,"link",a),e.head.appendChild(n),t.instance=n);case"void":return null;default:throw Error(r(443,t.type))}else t.type==="stylesheet"&&(t.state.loading&4)===0&&(a=t.instance,t.state.loading|=4,nu(a,l.precedence,e));return t.instance}function nu(e,t,l){for(var a=l.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),n=a.length?a[a.length-1]:null,i=n,s=0;s title"):null)}function jy(e,t,l){if(l===1||t.itemProp!=null)return!1;switch(e){case"meta":case"title":return!0;case"style":if(typeof t.precedence!="string"||typeof t.href!="string"||t.href==="")break;return!0;case"link":if(typeof t.rel!="string"||typeof t.href!="string"||t.href===""||t.onLoad||t.onError)break;return t.rel==="stylesheet"?(e=t.disabled,typeof t.precedence=="string"&&e==null):!0;case"script":if(t.async&&typeof t.async!="function"&&typeof t.async!="symbol"&&!t.onLoad&&!t.onError&&t.src&&typeof t.src=="string")return!0}return!1}function lm(e){return!(e.type==="stylesheet"&&(e.state.loading&3)===0)}function Ny(e,t,l,a){if(l.type==="stylesheet"&&(typeof a.media!="string"||matchMedia(a.media).matches!==!1)&&(l.state.loading&4)===0){if(l.instance===null){var n=Xa(a.href),i=t.querySelector(Ln(n));if(i){t=i._p,t!==null&&typeof t=="object"&&typeof t.then=="function"&&(e.count++,e=uu.bind(e),t.then(e,e)),l.state.loading|=4,l.instance=i,Fe(i);return}i=t.ownerDocument||t,a=Id(a),(n=wt.get(n))&&Ms(a,n),i=i.createElement("link"),Fe(i);var s=i;s._p=new Promise(function(d,v){s.onload=d,s.onerror=v}),nt(i,"link",a),l.instance=i}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(l,t),(t=l.state.preload)&&(l.state.loading&3)===0&&(e.count++,l=uu.bind(e),t.addEventListener("load",l),t.addEventListener("error",l))}}var Hs=0;function Ey(e,t){return e.stylesheets&&e.count===0&&su(e,e.stylesheets),0Hs?50:800)+t);return e.unsuspend=l,function(){e.unsuspend=null,clearTimeout(a),clearTimeout(n)}}:null}function uu(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)su(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var cu=null;function su(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,cu=new Map,t.forEach(Ty,e),cu=null,uu.call(e))}function Ty(e,t){if(!(t.state.loading&4)){var l=cu.get(e);if(l)var a=l.get(null);else{l=new Map,cu.set(e,l);for(var n=e.querySelectorAll("link[data-precedence],style[data-precedence]"),i=0;i"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(c)}catch(f){console.error(f)}}return c(),Vs.exports=Qy(),Vs.exports}var Vy=Xy();var Tm="popstate";function Rm(c){return typeof c=="object"&&c!=null&&"pathname"in c&&"search"in c&&"hash"in c&&"state"in c&&"key"in c}function Zy(c={}){function f(m,h){let{pathname:b="/",search:T="",hash:x=""}=ua(m.location.hash.substring(1));return!b.startsWith("/")&&!b.startsWith(".")&&(b="/"+b),Ps("",{pathname:b,search:T,hash:x},h.state&&h.state.usr||null,h.state&&h.state.key||"default")}function o(m,h){let b=m.document.querySelector("base"),T="";if(b&&b.getAttribute("href")){let x=m.location.href,p=x.indexOf("#");T=p===-1?x:x.slice(0,p)}return T+"#"+(typeof h=="string"?h:$n(h))}function r(m,h){Lt(m.pathname.charAt(0)==="/",`relative pathnames are not supported in hash history.push(${JSON.stringify(h)})`)}return Jy(f,o,r,c)}function Be(c,f){if(c===!1||c===null||typeof c>"u")throw new Error(f)}function Lt(c,f){if(!c){typeof console<"u"&&console.warn(f);try{throw new Error(f)}catch{}}}function Ky(){return Math.random().toString(36).substring(2,10)}function Am(c,f){return{usr:c.state,key:c.key,idx:f,masked:c.unstable_mask?{pathname:c.pathname,search:c.search,hash:c.hash}:void 0}}function Ps(c,f,o=null,r,m){return{pathname:typeof c=="string"?c:c.pathname,search:"",hash:"",...typeof f=="string"?ua(f):f,state:o,key:f&&f.key||r||Ky(),unstable_mask:m}}function $n({pathname:c="/",search:f="",hash:o=""}){return f&&f!=="?"&&(c+=f.charAt(0)==="?"?f:"?"+f),o&&o!=="#"&&(c+=o.charAt(0)==="#"?o:"#"+o),c}function ua(c){let f={};if(c){let o=c.indexOf("#");o>=0&&(f.hash=c.substring(o),c=c.substring(0,o));let r=c.indexOf("?");r>=0&&(f.search=c.substring(r),c=c.substring(0,r)),c&&(f.pathname=c)}return f}function Jy(c,f,o,r={}){let{window:m=document.defaultView,v5Compat:h=!1}=r,b=m.history,T="POP",x=null,p=O();p==null&&(p=0,b.replaceState({...b.state,idx:p},""));function O(){return(b.state||{idx:null}).idx}function N(){T="POP";let L=O(),V=L==null?null:L-p;p=L,x&&x({action:T,location:Q.location,delta:V})}function B(L,V){T="PUSH";let F=Rm(L)?L:Ps(Q.location,L,V);o&&o(F,L),p=O()+1;let $=Am(F,p),G=Q.createHref(F.unstable_mask||F);try{b.pushState($,"",G)}catch(ne){if(ne instanceof DOMException&&ne.name==="DataCloneError")throw ne;m.location.assign(G)}h&&x&&x({action:T,location:Q.location,delta:1})}function q(L,V){T="REPLACE";let F=Rm(L)?L:Ps(Q.location,L,V);o&&o(F,L),p=O();let $=Am(F,p),G=Q.createHref(F.unstable_mask||F);b.replaceState($,"",G),h&&x&&x({action:T,location:Q.location,delta:0})}function Z(L){return ky(L)}let Q={get action(){return T},get location(){return c(m,b)},listen(L){if(x)throw new Error("A history only accepts one active listener");return m.addEventListener(Tm,N),x=L,()=>{m.removeEventListener(Tm,N),x=null}},createHref(L){return f(m,L)},createURL:Z,encodeLocation(L){let V=Z(L);return{pathname:V.pathname,search:V.search,hash:V.hash}},push:B,replace:q,go(L){return b.go(L)}};return Q}function ky(c,f=!1){let o="http://localhost";typeof window<"u"&&(o=window.location.origin!=="null"?window.location.origin:window.location.href),Be(o,"No window.location.(origin|href) available to create URL");let r=typeof c=="string"?c:$n(c);return r=r.replace(/ $/,"%20"),!f&&r.startsWith("//")&&(r=o+r),new URL(r,o)}function qm(c,f,o="/"){return $y(c,f,o,!1)}function $y(c,f,o,r){let m=typeof f=="string"?ua(f):f,h=ml(m.pathname||"/",o);if(h==null)return null;let b=Lm(c);Wy(b);let T=null;for(let x=0;T==null&&x{let O={relativePath:p===void 0?b.path||"":p,caseSensitive:b.caseSensitive===!0,childrenIndex:T,route:b};if(O.relativePath.startsWith("/")){if(!O.relativePath.startsWith(r)&&x)return;Be(O.relativePath.startsWith(r),`Absolute route path "${O.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),O.relativePath=O.relativePath.slice(r.length)}let N=Qt([r,O.relativePath]),B=o.concat(O);b.children&&b.children.length>0&&(Be(b.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${N}".`),Lm(b.children,f,B,N,x)),!(b.path==null&&!b.index)&&f.push({path:N,score:ap(N,b.index),routesMeta:B})};return c.forEach((b,T)=>{if(b.path===""||!b.path?.includes("?"))h(b,T);else for(let x of Bm(b.path))h(b,T,!0,x)}),f}function Bm(c){let f=c.split("/");if(f.length===0)return[];let[o,...r]=f,m=o.endsWith("?"),h=o.replace(/\?$/,"");if(r.length===0)return m?[h,""]:[h];let b=Bm(r.join("/")),T=[];return T.push(...b.map(x=>x===""?h:[h,x].join("/"))),m&&T.push(...b),T.map(x=>c.startsWith("/")&&x===""?"/":x)}function Wy(c){c.sort((f,o)=>f.score!==o.score?o.score-f.score:np(f.routesMeta.map(r=>r.childrenIndex),o.routesMeta.map(r=>r.childrenIndex)))}var Fy=/^:[\w-]+$/,Iy=3,Py=2,ep=1,tp=10,lp=-2,Om=c=>c==="*";function ap(c,f){let o=c.split("/"),r=o.length;return o.some(Om)&&(r+=lp),f&&(r+=Py),o.filter(m=>!Om(m)).reduce((m,h)=>m+(Fy.test(h)?Iy:h===""?ep:tp),r)}function np(c,f){return c.length===f.length&&c.slice(0,-1).every((r,m)=>r===f[m])?c[c.length-1]-f[f.length-1]:0}function ip(c,f,o=!1){let{routesMeta:r}=c,m={},h="/",b=[];for(let T=0;T{if(O==="*"){let Z=T[B]||"";b=h.slice(0,h.length-Z.length).replace(/(.)\/+$/,"$1")}const q=T[B];return N&&!q?p[O]=void 0:p[O]=(q||"").replace(/%2F/g,"/"),p},{}),pathname:h,pathnameBase:b,pattern:c}}function up(c,f=!1,o=!0){Lt(c==="*"||!c.endsWith("*")||c.endsWith("/*"),`Route path "${c}" will be treated as if it were "${c.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${c.replace(/\*$/,"/*")}".`);let r=[],m="^"+c.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(b,T,x,p,O)=>{if(r.push({paramName:T,isOptional:x!=null}),x){let N=O.charAt(p+b.length);return N&&N!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return c.endsWith("*")?(r.push({paramName:"*"}),m+=c==="*"||c==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):o?m+="\\/*$":c!==""&&c!=="/"&&(m+="(?:(?=\\/|$))"),[new RegExp(m,f?void 0:"i"),r]}function cp(c){try{return c.split("/").map(f=>decodeURIComponent(f).replace(/\//g,"%2F")).join("/")}catch(f){return Lt(!1,`The URL path "${c}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${f}).`),c}}function ml(c,f){if(f==="/")return c;if(!c.toLowerCase().startsWith(f.toLowerCase()))return null;let o=f.endsWith("/")?f.length-1:f.length,r=c.charAt(o);return r&&r!=="/"?null:c.slice(o)||"/"}var sp=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function fp(c,f="/"){let{pathname:o,search:r="",hash:m=""}=typeof c=="string"?ua(c):c,h;return o?(o=Ym(o),o.startsWith("/")?h=Cm(o.substring(1),"/"):h=Cm(o,f)):h=f,{pathname:h,search:dp(r),hash:mp(m)}}function Cm(c,f){let o=ju(f).split("/");return c.split("/").forEach(m=>{m===".."?o.length>1&&o.pop():m!=="."&&o.push(m)}),o.length>1?o.join("/"):"/"}function ks(c,f,o,r){return`Cannot include a '${c}' character in a manually specified \`to.${f}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${o}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function rp(c){return c.filter((f,o)=>o===0||f.route.path&&f.route.path.length>0)}function uf(c){let f=rp(c);return f.map((o,r)=>r===f.length-1?o.pathname:o.pathnameBase)}function Nu(c,f,o,r=!1){let m;typeof c=="string"?m=ua(c):(m={...c},Be(!m.pathname||!m.pathname.includes("?"),ks("?","pathname","search",m)),Be(!m.pathname||!m.pathname.includes("#"),ks("#","pathname","hash",m)),Be(!m.search||!m.search.includes("#"),ks("#","search","hash",m)));let h=c===""||m.pathname==="",b=h?"/":m.pathname,T;if(b==null)T=o;else{let N=f.length-1;if(!r&&b.startsWith("..")){let B=b.split("/");for(;B[0]==="..";)B.shift(),N-=1;m.pathname=B.join("/")}T=N>=0?f[N]:"/"}let x=fp(m,T),p=b&&b!=="/"&&b.endsWith("/"),O=(h||b===".")&&o.endsWith("/");return!x.pathname.endsWith("/")&&(p||O)&&(x.pathname+="/"),x}var Ym=c=>c.replace(/\/\/+/g,"/"),Qt=c=>Ym(c.join("/")),ju=c=>c.replace(/\/+$/,""),op=c=>ju(c).replace(/^\/*/,"/"),dp=c=>!c||c==="?"?"":c.startsWith("?")?c:"?"+c,mp=c=>!c||c==="#"?"":c.startsWith("#")?c:"#"+c,hp=class{constructor(c,f,o,r=!1){this.status=c,this.statusText=f||"",this.internal=r,o instanceof Error?(this.data=o.toString(),this.error=o):this.data=o}};function vp(c){return c!=null&&typeof c.status=="number"&&typeof c.statusText=="string"&&typeof c.internal=="boolean"&&"data"in c}function yp(c){let f=c.map(o=>o.route.path).filter(Boolean);return Qt(f)||"/"}var Gm=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Qm(c,f){let o=c;if(typeof o!="string"||!sp.test(o))return{absoluteURL:void 0,isExternal:!1,to:o};let r=o,m=!1;if(Gm)try{let h=new URL(window.location.href),b=o.startsWith("//")?new URL(h.protocol+o):new URL(o),T=ml(b.pathname,f);b.origin===h.origin&&T!=null?o=T+b.search+b.hash:m=!0}catch{Lt(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:m,to:o}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var Xm=["POST","PUT","PATCH","DELETE"];new Set(Xm);var pp=["GET",...Xm];new Set(pp);var $a=y.createContext(null);$a.displayName="DataRouter";var Eu=y.createContext(null);Eu.displayName="DataRouterState";var Vm=y.createContext(!1);function gp(){return y.useContext(Vm)}var Zm=y.createContext({isTransitioning:!1});Zm.displayName="ViewTransition";var bp=y.createContext(new Map);bp.displayName="Fetchers";var _p=y.createContext(null);_p.displayName="Await";var Tt=y.createContext(null);Tt.displayName="Navigation";var Wn=y.createContext(null);Wn.displayName="Location";var Xt=y.createContext({outlet:null,matches:[],isDataRoute:!1});Xt.displayName="Route";var cf=y.createContext(null);cf.displayName="RouteError";var Km="REACT_ROUTER_ERROR",xp="REDIRECT",Sp="ROUTE_ERROR_RESPONSE";function jp(c){if(c.startsWith(`${Km}:${xp}:{`))try{let f=JSON.parse(c.slice(28));if(typeof f=="object"&&f&&typeof f.status=="number"&&typeof f.statusText=="string"&&typeof f.location=="string"&&typeof f.reloadDocument=="boolean"&&typeof f.replace=="boolean")return f}catch{}}function Np(c){if(c.startsWith(`${Km}:${Sp}:{`))try{let f=JSON.parse(c.slice(40));if(typeof f=="object"&&f&&typeof f.status=="number"&&typeof f.statusText=="string")return new hp(f.status,f.statusText,f.data)}catch{}}function Ep(c,{relative:f}={}){Be(Wa(),"useHref() may be used only in the context of a component.");let{basename:o,navigator:r}=y.useContext(Tt),{hash:m,pathname:h,search:b}=Fn(c,{relative:f}),T=h;return o!=="/"&&(T=h==="/"?o:Qt([o,h])),r.createHref({pathname:T,search:b,hash:m})}function Wa(){return y.useContext(Wn)!=null}function kt(){return Be(Wa(),"useLocation() may be used only in the context of a component."),y.useContext(Wn).location}var Jm="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function km(c){y.useContext(Tt).static||y.useLayoutEffect(c)}function $m(){let{isDataRoute:c}=y.useContext(Xt);return c?Yp():Tp()}function Tp(){Be(Wa(),"useNavigate() may be used only in the context of a component.");let c=y.useContext($a),{basename:f,navigator:o}=y.useContext(Tt),{matches:r}=y.useContext(Xt),{pathname:m}=kt(),h=JSON.stringify(uf(r)),b=y.useRef(!1);return km(()=>{b.current=!0}),y.useCallback((x,p={})=>{if(Lt(b.current,Jm),!b.current)return;if(typeof x=="number"){o.go(x);return}let O=Nu(x,JSON.parse(h),m,p.relative==="path");c==null&&f!=="/"&&(O.pathname=O.pathname==="/"?f:Qt([f,O.pathname])),(p.replace?o.replace:o.push)(O,p.state,p)},[f,o,h,m,c])}var Rp=y.createContext(null);function Ap(c){let f=y.useContext(Xt).outlet;return y.useMemo(()=>f&&y.createElement(Rp.Provider,{value:c},f),[f,c])}function Fn(c,{relative:f}={}){let{matches:o}=y.useContext(Xt),{pathname:r}=kt(),m=JSON.stringify(uf(o));return y.useMemo(()=>Nu(c,JSON.parse(m),r,f==="path"),[c,m,r,f])}function Op(c,f){return Wm(c,f)}function Wm(c,f,o){Be(Wa(),"useRoutes() may be used only in the context of a component.");let{navigator:r}=y.useContext(Tt),{matches:m}=y.useContext(Xt),h=m[m.length-1],b=h?h.params:{},T=h?h.pathname:"/",x=h?h.pathnameBase:"/",p=h&&h.route;{let L=p&&p.path||"";Im(T,!p||L.endsWith("*")||L.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${T}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. - -Please change the parent to .`)}let O=kt(),N;if(f){let L=typeof f=="string"?ua(f):f;Be(x==="/"||L.pathname?.startsWith(x),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${x}" but pathname "${L.pathname}" was given in the \`location\` prop.`),N=L}else N=O;let B=N.pathname||"/",q=B;if(x!=="/"){let L=x.replace(/^\//,"").split("/");q="/"+B.replace(/^\//,"").split("/").slice(L.length).join("/")}let Z=qm(c,{pathname:q});Lt(p||Z!=null,`No routes matched location "${N.pathname}${N.search}${N.hash}" `),Lt(Z==null||Z[Z.length-1].route.element!==void 0||Z[Z.length-1].route.Component!==void 0||Z[Z.length-1].route.lazy!==void 0,`Matched leaf route at location "${N.pathname}${N.search}${N.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let Q=Up(Z&&Z.map(L=>Object.assign({},L,{params:Object.assign({},b,L.params),pathname:Qt([x,r.encodeLocation?r.encodeLocation(L.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:L.pathname]),pathnameBase:L.pathnameBase==="/"?x:Qt([x,r.encodeLocation?r.encodeLocation(L.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:L.pathnameBase])})),m,o);return f&&Q?y.createElement(Wn.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",unstable_mask:void 0,...N},navigationType:"POP"}},Q):Q}function Cp(){let c=Bp(),f=vp(c)?`${c.status} ${c.statusText}`:c instanceof Error?c.message:JSON.stringify(c),o=c instanceof Error?c.stack:null,r="rgba(200,200,200, 0.5)",m={padding:"0.5rem",backgroundColor:r},h={padding:"2px 4px",backgroundColor:r},b=null;return console.error("Error handled by React Router default ErrorBoundary:",c),b=y.createElement(y.Fragment,null,y.createElement("p",null,"💿 Hey developer 👋"),y.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",y.createElement("code",{style:h},"ErrorBoundary")," or"," ",y.createElement("code",{style:h},"errorElement")," prop on your route.")),y.createElement(y.Fragment,null,y.createElement("h2",null,"Unexpected Application Error!"),y.createElement("h3",{style:{fontStyle:"italic"}},f),o?y.createElement("pre",{style:m},o):null,b)}var zp=y.createElement(Cp,null),Fm=class extends y.Component{constructor(c){super(c),this.state={location:c.location,revalidation:c.revalidation,error:c.error}}static getDerivedStateFromError(c){return{error:c}}static getDerivedStateFromProps(c,f){return f.location!==c.location||f.revalidation!=="idle"&&c.revalidation==="idle"?{error:c.error,location:c.location,revalidation:c.revalidation}:{error:c.error!==void 0?c.error:f.error,location:f.location,revalidation:c.revalidation||f.revalidation}}componentDidCatch(c,f){this.props.onError?this.props.onError(c,f):console.error("React Router caught the following error during render",c)}render(){let c=this.state.error;if(this.context&&typeof c=="object"&&c&&"digest"in c&&typeof c.digest=="string"){const o=Np(c.digest);o&&(c=o)}let f=c!==void 0?y.createElement(Xt.Provider,{value:this.props.routeContext},y.createElement(cf.Provider,{value:c,children:this.props.component})):this.props.children;return this.context?y.createElement(Dp,{error:c},f):f}};Fm.contextType=Vm;var $s=new WeakMap;function Dp({children:c,error:f}){let{basename:o}=y.useContext(Tt);if(typeof f=="object"&&f&&"digest"in f&&typeof f.digest=="string"){let r=jp(f.digest);if(r){let m=$s.get(f);if(m)throw m;let h=Qm(r.location,o);if(Gm&&!$s.get(f))if(h.isExternal||r.reloadDocument)window.location.href=h.absoluteURL||h.to;else{const b=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(h.to,{replace:r.replace}));throw $s.set(f,b),b}return y.createElement("meta",{httpEquiv:"refresh",content:`0;url=${h.absoluteURL||h.to}`})}}return c}function Mp({routeContext:c,match:f,children:o}){let r=y.useContext($a);return r&&r.static&&r.staticContext&&(f.route.errorElement||f.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=f.route.id),y.createElement(Xt.Provider,{value:c},o)}function Up(c,f=[],o){let r=o?.state;if(c==null){if(!r)return null;if(r.errors)c=r.matches;else if(f.length===0&&!r.initialized&&r.matches.length>0)c=r.matches;else return null}let m=c,h=r?.errors;if(h!=null){let O=m.findIndex(N=>N.route.id&&h?.[N.route.id]!==void 0);Be(O>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(h).join(",")}`),m=m.slice(0,Math.min(m.length,O+1))}let b=!1,T=-1;if(o&&r){b=r.renderFallback;for(let O=0;O=0?m=m.slice(0,T+1):m=[m[0]];break}}}}let x=o?.onError,p=r&&x?(O,N)=>{x(O,{location:r.location,params:r.matches?.[0]?.params??{},unstable_pattern:yp(r.matches),errorInfo:N})}:void 0;return m.reduceRight((O,N,B)=>{let q,Z=!1,Q=null,L=null;r&&(q=h&&N.route.id?h[N.route.id]:void 0,Q=N.route.errorElement||zp,b&&(T<0&&B===0?(Im("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),Z=!0,L=null):T===B&&(Z=!0,L=N.route.hydrateFallbackElement||null)));let V=f.concat(m.slice(0,B+1)),F=()=>{let $;return q?$=Q:Z?$=L:N.route.Component?$=y.createElement(N.route.Component,null):N.route.element?$=N.route.element:$=O,y.createElement(Mp,{match:N,routeContext:{outlet:O,matches:V,isDataRoute:r!=null},children:$})};return r&&(N.route.ErrorBoundary||N.route.errorElement||B===0)?y.createElement(Fm,{location:r.location,revalidation:r.revalidation,component:Q,error:q,children:F(),routeContext:{outlet:null,matches:V,isDataRoute:!0},onError:p}):F()},null)}function sf(c){return`${c} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Hp(c){let f=y.useContext($a);return Be(f,sf(c)),f}function wp(c){let f=y.useContext(Eu);return Be(f,sf(c)),f}function qp(c){let f=y.useContext(Xt);return Be(f,sf(c)),f}function ff(c){let f=qp(c),o=f.matches[f.matches.length-1];return Be(o.route.id,`${c} can only be used on routes that contain a unique "id"`),o.route.id}function Lp(){return ff("useRouteId")}function Bp(){let c=y.useContext(cf),f=wp("useRouteError"),o=ff("useRouteError");return c!==void 0?c:f.errors?.[o]}function Yp(){let{router:c}=Hp("useNavigate"),f=ff("useNavigate"),o=y.useRef(!1);return km(()=>{o.current=!0}),y.useCallback(async(m,h={})=>{Lt(o.current,Jm),o.current&&(typeof m=="number"?await c.navigate(m):await c.navigate(m,{fromRouteId:f,...h}))},[c,f])}var zm={};function Im(c,f,o){!f&&!zm[c]&&(zm[c]=!0,Lt(!1,o))}y.memo(Gp);function Gp({routes:c,future:f,state:o,isStatic:r,onError:m}){return Wm(c,void 0,{state:o,isStatic:r,onError:m})}function Qp({to:c,replace:f,state:o,relative:r}){Be(Wa()," may be used only in the context of a component.");let{static:m}=y.useContext(Tt);Lt(!m," must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.");let{matches:h}=y.useContext(Xt),{pathname:b}=kt(),T=$m(),x=Nu(c,uf(h),b,r==="path"),p=JSON.stringify(x);return y.useEffect(()=>{T(JSON.parse(p),{replace:f,state:o,relative:r})},[T,p,r,f,o]),null}function Xp(c){return Ap(c.context)}function Bl(c){Be(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function Vp({basename:c="/",children:f=null,location:o,navigationType:r="POP",navigator:m,static:h=!1,unstable_useTransitions:b}){Be(!Wa(),"You cannot render a inside another . You should never have more than one in your app.");let T=c.replace(/^\/*/,"/"),x=y.useMemo(()=>({basename:T,navigator:m,static:h,unstable_useTransitions:b,future:{}}),[T,m,h,b]);typeof o=="string"&&(o=ua(o));let{pathname:p="/",search:O="",hash:N="",state:B=null,key:q="default",unstable_mask:Z}=o,Q=y.useMemo(()=>{let L=ml(p,T);return L==null?null:{location:{pathname:L,search:O,hash:N,state:B,key:q,unstable_mask:Z},navigationType:r}},[T,p,O,N,B,q,r,Z]);return Lt(Q!=null,` is not able to match the URL "${p}${O}${N}" because it does not start with the basename, so the won't render anything.`),Q==null?null:y.createElement(Tt.Provider,{value:x},y.createElement(Wn.Provider,{children:f,value:Q}))}function Zp({children:c,location:f}){return Op(ef(c),f)}function ef(c,f=[]){let o=[];return y.Children.forEach(c,(r,m)=>{if(!y.isValidElement(r))return;let h=[...f,m];if(r.type===y.Fragment){o.push.apply(o,ef(r.props.children,h));return}Be(r.type===Bl,`[${typeof r.type=="string"?r.type:r.type.name}] is not a component. All component children of must be a or `),Be(!r.props.index||!r.props.children,"An index route cannot have child routes.");let b={id:r.props.id||h.join("-"),caseSensitive:r.props.caseSensitive,element:r.props.element,Component:r.props.Component,index:r.props.index,path:r.props.path,middleware:r.props.middleware,loader:r.props.loader,action:r.props.action,hydrateFallbackElement:r.props.hydrateFallbackElement,HydrateFallback:r.props.HydrateFallback,errorElement:r.props.errorElement,ErrorBoundary:r.props.ErrorBoundary,hasErrorBoundary:r.props.hasErrorBoundary===!0||r.props.ErrorBoundary!=null||r.props.errorElement!=null,shouldRevalidate:r.props.shouldRevalidate,handle:r.props.handle,lazy:r.props.lazy};r.props.children&&(b.children=ef(r.props.children,h)),o.push(b)}),o}var gu="get",bu="application/x-www-form-urlencoded";function Tu(c){return typeof HTMLElement<"u"&&c instanceof HTMLElement}function Kp(c){return Tu(c)&&c.tagName.toLowerCase()==="button"}function Jp(c){return Tu(c)&&c.tagName.toLowerCase()==="form"}function kp(c){return Tu(c)&&c.tagName.toLowerCase()==="input"}function $p(c){return!!(c.metaKey||c.altKey||c.ctrlKey||c.shiftKey)}function Wp(c,f){return c.button===0&&(!f||f==="_self")&&!$p(c)}var yu=null;function Fp(){if(yu===null)try{new FormData(document.createElement("form"),0),yu=!1}catch{yu=!0}return yu}var Ip=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function Ws(c){return c!=null&&!Ip.has(c)?(Lt(!1,`"${c}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${bu}"`),null):c}function Pp(c,f){let o,r,m,h,b;if(Jp(c)){let T=c.getAttribute("action");r=T?ml(T,f):null,o=c.getAttribute("method")||gu,m=Ws(c.getAttribute("enctype"))||bu,h=new FormData(c)}else if(Kp(c)||kp(c)&&(c.type==="submit"||c.type==="image")){let T=c.form;if(T==null)throw new Error('Cannot submit a + ); +} diff --git a/web/src/components/ReleaseLifecycleStrip.tsx b/web/src/components/ReleaseLifecycleStrip.tsx index 5202b33..cf68169 100644 --- a/web/src/components/ReleaseLifecycleStrip.tsx +++ b/web/src/components/ReleaseLifecycleStrip.tsx @@ -78,8 +78,7 @@ export function ReleaseLifecycleStrip() { ))}

- Links always open the page. They do not auto-fill forms: if tables are empty, register and ingest with the CLI - first, then copy release IDs from Overview into Runs or Diff. + Links always open the page. Deep links can prefill Diff, Runs, and Promote via URL query params; still run diff or load runs explicitly on those pages.

); diff --git a/web/src/components/diff/DiffChangeImpact.tsx b/web/src/components/diff/DiffChangeImpact.tsx new file mode 100644 index 0000000..a266bbf --- /dev/null +++ b/web/src/components/diff/DiffChangeImpact.tsx @@ -0,0 +1,107 @@ +import { DiffMetric } from "./diffPayload"; +import type { PricingInfo } from "./diffPayload"; +import { DiffPricingExpand } from "./DiffPricingExpand"; + +function num(v: unknown) { + return typeof v === "number" && Number.isFinite(v) ? String(v) : "—"; +} + +function pct(v: unknown) { + return typeof v === "number" && Number.isFinite(v) ? `${(v * 100).toFixed(2)}%` : "—"; +} + +export function DiffChangeImpact({ + samples, + metrics, + pricing, + pricingResetKey, +}: { + samples: Record | null; + metrics: Record | null; + pricing: PricingInfo | null; + pricingResetKey: number; +}) { + return ( +
+
+

+ Change impact +

+

+ Evidence coverage, runtime rollups, and expandable pricing detail — causal drill-down stays here (no invented + change rows until the API exposes them). +

+
+
+
+

+ Sample coverage +

+ {samples ? ( +

+ Baseline runs: {num(samples.baseline_runs)} · Candidate runs:{" "} + {num(samples.candidate_runs)} · Confidence:{" "} + {String(samples.confidence ?? "—")} + {typeof samples.confidence_reason === "string" ? ` — ${samples.confidence_reason}` : null} +

+ ) : ( +

No sample counts in this response.

+ )} +
+ +
+

+ Cost and quality rollups +

+ {metrics ? ( +
+ = 0 ? "+" : ""}${(metrics.delta_cost_per_run_pct * 100).toFixed(2)}% vs baseline)` + : "" + }` + : undefined + } + /> + + +
+ ) : ( +

No metrics block in this response.

+ )} +
+ + {pricing ? ( + + ) : ( +
+

Pricing & model

+

No pricing block in this response.

+
+ )} +
+
+ ); +} diff --git a/web/src/components/diff/DiffDecisionCard.tsx b/web/src/components/diff/DiffDecisionCard.tsx new file mode 100644 index 0000000..9678958 --- /dev/null +++ b/web/src/components/diff/DiffDecisionCard.tsx @@ -0,0 +1,37 @@ +import { Link } from "react-router-dom"; +import type { PolicyView } from "./diffPayload"; + +export function DiffDecisionCard({ + policy, + promoteSearch, +}: { + policy: PolicyView | null; + promoteSearch: string; +}) { + return ( +
+
+

+ Decision +

+

+ {policy === null + ? "Run diff again after fixing the payload or server configuration." + : policy.passed + ? "Gate passed for this baseline, candidate, window, and environment. Next: promote from Actions if operational checks agree." + : "Gate failed — resolve policy findings or choose a different candidate/baseline before promoting."} +

+
+ {policy?.passed === true && promoteSearch !== "" ? ( +
+ + Continue to promote + + + Candidate release and window/environment are prefilled; reason is still required on Actions. + +
+ ) : null} +
+ ); +} diff --git a/web/src/components/diff/DiffPolicyPanel.tsx b/web/src/components/diff/DiffPolicyPanel.tsx new file mode 100644 index 0000000..631ac82 --- /dev/null +++ b/web/src/components/diff/DiffPolicyPanel.tsx @@ -0,0 +1,33 @@ +import { Badge } from "../Badge"; +import type { PolicyView } from "./diffPayload"; + +export function DiffPolicyPanel({ policy }: { policy: PolicyView }) { + return ( +
+
+

+ Policy evaluation +

+ {policy.passed ? "PASS" : "FAIL"} +
+ {policy.evaluatedAt ? ( +

+ evaluated_at {policy.evaluatedAt} +

+ ) : null} + {policy.reasons.length > 0 ? ( +
    + {policy.reasons.map((r) => ( +
  • {r}
  • + ))} +
+ ) : ( +

+ {policy.passed + ? "No constraint messages returned (pass with empty reasons)." + : "No reasons listed — inspect raw JSON and server policy."} +

+ )} +
+ ); +} diff --git a/web/src/components/diff/DiffPricingExpand.tsx b/web/src/components/diff/DiffPricingExpand.tsx new file mode 100644 index 0000000..dea657b --- /dev/null +++ b/web/src/components/diff/DiffPricingExpand.tsx @@ -0,0 +1,172 @@ +import { useEffect, useId, useState } from "react"; +import type { PricingInfo } from "./diffPayload"; + +export function DiffPricingExpand({ + pricing, + resetDetailKey, +}: { + pricing: PricingInfo; + /** Bump when a new diff payload arrives so the fold resets closed. */ + resetDetailKey: number; +}) { + const pricingPanelId = useId(); + const [detailOpen, setDetailOpen] = useState(false); + + useEffect(() => { + setDetailOpen(false); + }, [resetDetailKey]); + + return ( +
+
+

+ Pricing & model +

+

+ + {pricing.baselineProvider}/{pricing.baselineVersion} {pricing.baselineModel} + + + → + + + {pricing.candidateProvider}/{pricing.candidateVersion} {pricing.candidateModel} + + {pricing.changed ? ( + pricing/model changed + ) : ( + unchanged + )} +

+ +
+ {detailOpen ? ( +
+ {(() => { + const bp = pricing.baselineProvider.trim(); + const cp = pricing.candidateProvider.trim(); + const bv = pricing.baselineVersion.trim(); + const cv = pricing.candidateVersion.trim(); + const providerSkew = bp.length > 0 && cp.length > 0 && bp !== cp; + const versionSkew = bv.length > 0 && cv.length > 0 && bv !== cv; + if (!providerSkew && !versionSkew) return null; + return ( +

+ {versionSkew ? ( + <> + Imported pricing table versions differ ( + {bv} vs{" "} + {cv}).{" "} + + ) : null} + {providerSkew ? ( + <> + Providers differ ( + {bp} vs{" "} + {cp}).{" "} + + ) : null} + Treat per-1k and catalog lines below as resolved per release; skew can change comparability. +

+ ); + })()} + {pricing.warnings.length > 0 ? ( + <> +

Pricing warnings

+
    + {pricing.warnings.map((w) => ( +
  • {w}
  • + ))} +
+ + ) : null} + {pricing.hints.length > 0 ? ( + <> +

Hints

+
    + {pricing.hints.map((h) => ( +
  • {h}
  • + ))} +
+ + ) : null} + {pricing.catalog && (pricing.catalog.enabled || pricing.catalog.warnings.length > 0) ? ( + <> +

Pricing catalog

+
+ {pricing.catalog.enabled ? ( +

+ Catalog v{pricing.catalog.version ?? "—"} · slots{" "} + {pricing.catalog.baselineSlot ?? "—"} →{" "} + {pricing.catalog.candidateSlot ?? "—"} + {pricing.catalog.baselineCost !== null && + pricing.catalog.candidateCost !== null && + pricing.catalog.deltaCost !== null ? ( + <> +
+ Comparable cost/run: {pricing.catalog.baselineCost.toFixed(6)} →{" "} + {pricing.catalog.candidateCost.toFixed(6)} (Δ{" "} + {pricing.catalog.deltaCost >= 0 ? "+" : ""} + {pricing.catalog.deltaCost.toFixed(6)}) + + ) : null} +

+ ) : ( +

Catalog disabled or incomplete for this diff.

+ )} + {pricing.catalog.warnings.length > 0 ? ( +
    + {pricing.catalog.warnings.map((w) => ( +
  • {w}
  • + ))} +
+ ) : null} +
+ + ) : null} + {pricing.changed && + pricing.prices && + pricing.prices.baselineInput !== null && + pricing.prices.candidateInput !== null && + pricing.prices.baselineOutput !== null && + pricing.prices.candidateOutput !== null ? ( + <> +

+ Per-1k token prices (USD) +

+
+
+
Input / 1k
+
+ {pricing.prices.baselineInput.toFixed(6)} → {pricing.prices.candidateInput.toFixed(6)} +
+
+
+
Output / 1k
+
+ {pricing.prices.baselineOutput.toFixed(6)} → {pricing.prices.candidateOutput.toFixed(6)} +
+
+
+

+ Cost rollups reflect pricing table and model identity; compare with catalog lines above when configured. +

+ + ) : null} +
+ ) : null} +
+ ); +} diff --git a/web/src/components/diff/DiffReleaseTwin.tsx b/web/src/components/diff/DiffReleaseTwin.tsx new file mode 100644 index 0000000..9a3f624 --- /dev/null +++ b/web/src/components/diff/DiffReleaseTwin.tsx @@ -0,0 +1,48 @@ +import type { PricingInfo } from "./diffPayload"; +import { pricingLine } from "./diffPayload"; + +export function DiffReleaseTwin({ + diffBaseline, + diffCandidate, + diffEnv, + diffWindow, + pricing, +}: { + diffBaseline: string; + diffCandidate: string; + diffEnv: string; + diffWindow: string; + pricing: PricingInfo | null; +}) { + const b = diffBaseline.trim(); + const c = diffCandidate.trim(); + return ( +
+

+ Release comparison +

+
+ Environment {diffEnv.trim() || "—"} · Window {diffWindow.trim() || "—"} +
+
+
+ Baseline (OLD) + + {b !== "" ? b : "—"} + +

{pricingLine(pricing, "baseline")}

+
+
+ → +
+
+ Candidate (NEW) + + {c !== "" ? c : "—"} + +

{pricingLine(pricing, "candidate")}

+
+
+
+ ); +} diff --git a/web/src/components/diff/DiffVerdictStack.tsx b/web/src/components/diff/DiffVerdictStack.tsx new file mode 100644 index 0000000..087a7a3 --- /dev/null +++ b/web/src/components/diff/DiffVerdictStack.tsx @@ -0,0 +1,35 @@ +import type { PolicyView } from "./diffPayload"; + +export function DiffVerdictStack({ policy }: { policy: PolicyView | null }) { + return ( + <> + {policy && !policy.passed && policy.reasons.length > 0 ? ( +
+ Blocked: {policy.reasons[0]} + {policy.reasons.length > 1 ? ( + + (+{policy.reasons.length - 1} more in policy evaluation) + + ) : null} +
+ ) : null} + + {policy ? ( +
+ {policy.passed + ? "Policy PASS — candidate may proceed if you accept the impact below." + : "Policy FAIL — do not promote this candidate for this evaluation."} +
+ ) : ( +
+ No policy block in this diff response — confirm server version and request payload before + treating the outcome as gated. +
+ )} + + ); +} diff --git a/web/src/components/diff/diffPayload.tsx b/web/src/components/diff/diffPayload.tsx new file mode 100644 index 0000000..9ad7f8e --- /dev/null +++ b/web/src/components/diff/diffPayload.tsx @@ -0,0 +1,158 @@ +export type DiffJson = Record; + +export function isRecord(v: unknown): v is Record { + return typeof v === "object" && v !== null; +} + +export type PolicyView = { + passed: boolean; + reasons: string[]; + evaluatedAt: string | null; +}; + +export function pickPolicy(data: DiffJson): PolicyView | null { + const p = data.policy; + if (!isRecord(p)) return null; + const passed = p.passed; + const reasons = p.reasons; + const ev = p.evaluated_at; + return { + passed: passed === true, + reasons: Array.isArray(reasons) ? reasons.filter((x): x is string => typeof x === "string") : [], + evaluatedAt: typeof ev === "string" ? ev : null, + }; +} + +export type CatalogInfo = { + enabled: boolean; + version: string | null; + baselineSlot: string | null; + candidateSlot: string | null; + baselineCost: number | null; + candidateCost: number | null; + deltaCost: number | null; + warnings: string[]; +}; + +export type PricingInfo = { + baselineProvider: string; + baselineVersion: string; + baselineModel: string; + candidateProvider: string; + candidateVersion: string; + candidateModel: string; + changed: boolean; + prices: PricingPrices | null; + warnings: string[]; + hints: string[]; + catalog: CatalogInfo | null; +}; + +export type PricingPrices = { + baselineInput: number | null; + baselineOutput: number | null; + candidateInput: number | null; + candidateOutput: number | null; +}; + +function pickPrices(p: Record): PricingPrices | null { + const block = p.prices; + if (!isRecord(block)) return null; + const numOrNull = (k: string): number | null => + typeof block[k] === "number" && Number.isFinite(block[k]) ? (block[k] as number) : null; + return { + baselineInput: numOrNull("baseline_input_usd_per_1k_tokens"), + baselineOutput: numOrNull("baseline_output_usd_per_1k_tokens"), + candidateInput: numOrNull("candidate_input_usd_per_1k_tokens"), + candidateOutput: numOrNull("candidate_output_usd_per_1k_tokens"), + }; +} + +function pickCatalog(block: Record): CatalogInfo { + const rawW = block.warnings; + const warnings = Array.isArray(rawW) ? rawW.filter((x): x is string => typeof x === "string") : []; + const numOrNull = (k: string): number | null => + typeof block[k] === "number" && Number.isFinite(block[k]) ? (block[k] as number) : null; + const strOrNull = (k: string): string | null => + typeof block[k] === "string" ? (block[k] as string) : null; + return { + enabled: block.enabled === true, + version: strOrNull("catalog_version"), + baselineSlot: strOrNull("baseline_slot_id"), + candidateSlot: strOrNull("candidate_slot_id"), + baselineCost: numOrNull("baseline_cost_per_run_usd"), + candidateCost: numOrNull("candidate_cost_per_run_usd"), + deltaCost: numOrNull("delta_cost_per_run_usd"), + warnings, + }; +} + +export function pickPricing(data: DiffJson): PricingInfo | null { + const p = data.pricing; + if (!isRecord(p)) return null; + const get = (k: string): string => (typeof p[k] === "string" ? (p[k] as string) : ""); + const rawWarnings = p.warnings; + const warnings = Array.isArray(rawWarnings) + ? rawWarnings.filter((x): x is string => typeof x === "string") + : []; + const rawHints = p.hints; + const hints = Array.isArray(rawHints) ? rawHints.filter((x): x is string => typeof x === "string") : []; + const catRaw = p.catalog; + const catalog = isRecord(catRaw) ? pickCatalog(catRaw) : null; + return { + baselineProvider: get("baseline_provider"), + baselineVersion: get("baseline_version"), + baselineModel: get("baseline_model"), + candidateProvider: get("candidate_provider"), + candidateVersion: get("candidate_version"), + candidateModel: get("candidate_model"), + changed: p.pricing_or_model_changed === true, + prices: pickPrices(p), + warnings, + hints, + catalog, + }; +} + +export function pricingLine(pricing: PricingInfo | null, side: "baseline" | "candidate"): string { + if (!pricing) return "—"; + const prov = side === "baseline" ? pricing.baselineProvider : pricing.candidateProvider; + const ver = side === "baseline" ? pricing.baselineVersion : pricing.candidateVersion; + const mod = side === "baseline" ? pricing.baselineModel : pricing.candidateModel; + const parts = [prov.trim(), ver.trim(), mod.trim()].filter(Boolean); + return parts.length > 0 ? parts.join(" · ") : "—"; +} + +export function DiffMetric({ + label, + baseline, + candidate, + delta, + suffix = "", +}: { + label: string; + baseline: string; + candidate: string; + delta?: string; + suffix?: string; +}) { + return ( +
+
{label}
+
+ + B {baseline} + {suffix} + + + → + + + C {candidate} + {suffix} + +
+ {delta ?
{delta}
: null} +
+ ); +} diff --git a/web/src/index.css b/web/src/index.css index af9598b..eeb2129 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -257,9 +257,9 @@ body { flex-shrink: 0; overflow: hidden; border-radius: 12px; - background: linear-gradient(165deg, var(--fd-surface-2) 0%, var(--fd-surface) 100%); + background: var(--fd-surface); box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.08), + inset 0 1px 0 rgba(255, 255, 255, 0.06), 0 0 0 1px var(--fd-border); } @@ -559,12 +559,27 @@ html[data-theme="dark"] .fd-theme-toggle--settings .fd-theme-toggle__label:has(. .fd-page-sub { margin: 0.4rem 0 0; - max-width: 52ch; + max-width: min(52ch, 100%); font-size: var(--fd-type-page-sub); line-height: var(--fd-type-page-sub--lh); color: var(--fd-muted); } +.fd-page-sub--tight { + margin-top: 0.35rem; +} + +.fd-page-sub--meta { + margin-top: 0.45rem; + font-size: 0.875rem; + line-height: 1.42; + max-width: min(56ch, 100%); +} + +.fd-page-head > div:first-child { + max-width: min(56rem, 100%); +} + .fd-page-sub a { color: var(--fd-accent); font-weight: 600; @@ -765,16 +780,162 @@ html[data-theme="dark"] .fd-theme-toggle--settings .fd-theme-toggle__label:has(. border-bottom: none; } -.fd-table tbody tr:hover td { - background: var(--fd-surface-2); +.fd-table--hover tbody tr:hover td { + background: color-mix(in srgb, var(--fd-surface-2) 88%, var(--fd-accent) 12%); } @media (prefers-reduced-motion: no-preference) { - .fd-table tbody tr:hover td { + .fd-table--hover tbody tr:hover td { transition: background 0.12s ease; } } +.fd-th-narrow { + width: 4.5rem; +} + +.fd-table-toolbar { + padding: 0 0 0.65rem; + margin: 0 0 0.25rem; + border-bottom: 1px solid var(--fd-border); +} + +.fd-filter-row { + display: flex; + flex-wrap: wrap; + gap: 0.65rem 1rem; + align-items: flex-end; +} + +.fd-field--compact { + min-width: 10rem; +} + +.fd-field--compact .fd-field__label { + font-size: 0.72rem; +} + +.fd-cell-stack { + display: flex; + flex-direction: column; + gap: 0.25rem; + align-items: flex-start; +} + +.fd-cell-stack__main { + font-size: 0.9rem; + color: var(--fd-text); +} + +.fd-cell-stack__sub { + font-size: 0.78rem; + color: var(--fd-muted); +} + +.fd-cell-inline { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.35rem 0.5rem; +} + +.fd-copy-btn { + padding: 0.2rem 0.45rem; + font-size: 0.75rem; + min-height: 0; +} + +.fd-card--collapse { + padding-bottom: 0.35rem; +} + +.fd-collapse-head { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.35rem 0.75rem; + width: 100%; + padding: 0.65rem 0.85rem; + margin: 0; + border: none; + border-radius: var(--fd-radius-sm); + background: var(--fd-surface-2); + font: inherit; + text-align: left; + cursor: pointer; + color: var(--fd-text); +} + +.fd-collapse-head:hover { + background: color-mix(in srgb, var(--fd-surface-2) 92%, var(--fd-border) 8%); +} + +.fd-collapse-head:focus-visible { + outline: none; + box-shadow: var(--fd-focus-ring); +} + +.fd-collapse-head__chevron { + flex-shrink: 0; + width: 1rem; + color: var(--fd-muted); +} + +.fd-collapse-head__title { + font-weight: 650; + font-size: var(--fd-type-card-title); +} + +.fd-collapse-head__hint { + flex: 1 1 12rem; + font-size: 0.78rem; + color: var(--fd-muted); +} + +.fd-collapse-body { + padding: 0 0.85rem 0.85rem; +} + +.fd-metric-meta { + margin: 0 0 0.75rem; +} + +.fd-release-hero__primary { + color: var(--fd-text); +} + +.fd-release-hero__env { + font-weight: 500; + color: var(--fd-muted); +} + +.fd-diff-pricing-inline { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 0.35rem 0.75rem; +} + +.fd-diff-pricing-inline__summary { + flex: 1 1 14rem; +} + +.fd-diff-detail-toggle { + flex-shrink: 0; + margin-left: auto; +} + +.fd-mx-xs { + margin-left: 0.25rem; + margin-right: 0.25rem; +} + +@media (max-width: 36rem) { + .fd-diff-detail-toggle { + margin-left: 0; + } +} + .fd-mono { font-family: var(--fd-mono); font-size: 0.82rem; @@ -890,19 +1051,18 @@ code.fd-mono--sm { } .fd-btn--primary { - background-image: var(--fd-accent-gradient); - background-origin: border-box; background-color: var(--fd-accent); - border-color: transparent; + border-color: color-mix(in srgb, var(--fd-accent) 65%, var(--fd-border-strong)); color: var(--fd-on-accent); } .fd-btn--primary:hover:not(:disabled) { - filter: brightness(1.08); + background-color: var(--fd-accent-hover); + filter: none; } .fd-btn--primary:active:not(:disabled) { - filter: brightness(0.96); + filter: brightness(0.94); } @media (prefers-reduced-motion: reduce) { @@ -1025,6 +1185,190 @@ code.fd-mono--sm { border: 1px solid var(--fd-warn-border); } +.fd-verdict-banner { + margin: 0 0 1rem; + padding: 0.85rem 1rem; + border-radius: var(--fd-radius); + border: 1px solid var(--fd-border-strong); + font-size: 0.9375rem; + line-height: 1.45; +} + +.fd-verdict-banner__title { + display: block; + margin: 0 0 0.35rem; + font-size: 1rem; + font-weight: 650; + letter-spacing: 0.01em; +} + +.fd-verdict-banner__reasons { + margin: 0.5rem 0 0; + padding-left: 1.15rem; +} + +.fd-verdict-banner__reasons li { + margin: 0.2em 0; +} + +.fd-verdict-banner--pass { + background: var(--fd-pass-bg); + color: var(--fd-pass-fg); + border-color: var(--fd-pass-fg); +} + +.fd-verdict-banner--fail { + background: var(--fd-fail-bg); + color: var(--fd-fail-fg); + border-color: var(--fd-fail-border); +} + +.fd-diff-twin { + margin-bottom: 1rem; + padding: 1rem 1.15rem; + border-radius: var(--fd-radius); + border: 1px solid var(--fd-border-strong); + background: var(--fd-surface-2); +} + +.fd-diff-twin__meta { + margin: 0 0 0.65rem; +} + +.fd-diff-twin__grid { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: 0.65rem 0.85rem; + align-items: start; +} + +@media (max-width: 40rem) { + .fd-diff-twin__grid { + grid-template-columns: 1fr; + } + + .fd-diff-twin__arrow { + justify-self: center; + transform: rotate(90deg); + padding: 0.15rem 0; + } +} + +.fd-diff-twin__col { + min-width: 0; +} + +.fd-diff-twin__label { + display: block; + font-size: 0.68rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--fd-muted); + margin-bottom: 0.35rem; +} + +.fd-diff-twin__id { + display: block; + font-size: 0.82rem; + word-break: break-all; + line-height: 1.35; +} + +.fd-diff-twin__detail { + font-size: 0.8rem; + line-height: 1.4; +} + +.fd-diff-twin__arrow { + padding-top: 1.85rem; + font-size: 1.25rem; + color: var(--fd-muted); + text-align: center; +} + +.fd-diff-block-strip { + margin: 0 0 0.65rem; + padding: 0.55rem 0.85rem; + border-radius: var(--fd-radius-sm); + border: 1px solid var(--fd-fail-border); + background: var(--fd-fail-bg); + color: var(--fd-fail-fg); + font-size: 0.9rem; + line-height: 1.45; +} + +.fd-diff-verdict-strip { + margin: 0 0 0.85rem; + padding: 0.5rem 0.85rem; + border-radius: var(--fd-radius-sm); + font-size: 0.88rem; + font-weight: 600; + letter-spacing: 0.02em; +} + +.fd-diff-verdict-strip--pass { + border: 1px solid color-mix(in srgb, var(--fd-pass-fg) 35%, var(--fd-border)); + background: var(--fd-pass-bg); + color: var(--fd-pass-fg); +} + +.fd-diff-verdict-strip--fail { + border: 1px solid var(--fd-fail-border); + background: color-mix(in srgb, var(--fd-fail-bg) 92%, var(--fd-fail-fg)); + color: var(--fd-fail-fg); +} + +.fd-policy-panel .fd-card__head { + margin-bottom: 0.5rem; +} + +.fd-decision-card .fd-card__head { + margin-bottom: 0; +} + +.fd-release-hero { + margin-bottom: 1rem; + padding: 1rem 1.1rem; + border-radius: var(--fd-radius); + border: 1px solid var(--fd-border-strong); + background: var(--fd-surface-2); +} + +.fd-release-hero__title { + margin: 0 0 0.35rem; + font-size: 1.0625rem; + font-weight: 650; +} + +.fd-release-hero__meta { + margin: 0; + font-size: 0.875rem; + color: var(--fd-muted); +} + +.fd-release-hero__actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.75rem; +} + +.fd-release-hero__actions .fd-btn { + text-decoration: none; +} + +.fd-table-actions { + display: flex; + flex-wrap: wrap; + gap: 0.35rem 0.65rem; + font-size: 0.8125rem; +} + +.fd-table-actions a { + white-space: nowrap; +} + .fd-security-strip { flex-shrink: 0; padding: 0.5rem 1.25rem 0; diff --git a/web/src/pages/ActionsPage.tsx b/web/src/pages/ActionsPage.tsx index a76a7d7..cc152c4 100644 --- a/web/src/pages/ActionsPage.tsx +++ b/web/src/pages/ActionsPage.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useState } from "react"; +import { useSearchParams } from "react-router-dom"; import type { ActionOutcomePayload, PromotionRequestListItem, WorkspacePublicPayload } from "../api"; import { fetchHealth, fetchJson, fetchPromotionRequests, fetchWorkspace } from "../api"; import { clientMutationTokenConfigured } from "../uiConfig"; @@ -57,6 +58,7 @@ function pickOutcome(data: unknown): ActionOutcomePayload | null { type Busy = null | "promote" | "rollback" | "request" | "confirm"; export function ActionsPage() { + const [searchParams] = useSearchParams(); const { notifyTimelineMutated } = useTimelineRefresh(); const [workspace, setWorkspace] = useState(null); const [workspaceLoading, setWorkspaceLoading] = useState(true); @@ -142,6 +144,15 @@ export function ActionsPage() { void refreshPending(); }, [refreshPending, listNonce]); + useEffect(() => { + const rid = searchParams.get("release_id"); + const env = searchParams.get("environment"); + const win = searchParams.get("window"); + if (rid !== null && rid.trim() !== "") setActRelease(rid.trim()); + if (env !== null && env.trim() !== "") setActEnv(env.trim()); + if (win !== null && win.trim() !== "") setActWindow(win.trim()); + }, [searchParams]); + const runAction = async (path: "/v1/promote" | "/v1/rollback") => { setActErr(null); setActOutcome(null); @@ -278,11 +289,16 @@ export function ActionsPage() {

Promote & rollback

-

- Writes use the same HTTP contract as the CLI. When{" "} +

+ What changed? You choose the candidate release and window. Is it safe? The + server evaluates policy before mutating the ledger. Can I ship? Promotion succeeds only when + policy passes (or follow request/confirm when approval is required). +

+

+ Same HTTP contract as the CLI. When{" "} FLIGHTDECK_LOCAL_API_TOKEN is set, include it via{" "} - VITE_FLIGHTDECK_LOCAL_API_TOKEN so reads and mutations - send Authorization: Bearer. + VITE_FLIGHTDECK_LOCAL_API_TOKEN so reads and mutations send{" "} + Authorization: Bearer.

@@ -457,7 +473,7 @@ export function ActionsPage() {

No pending requests. After you request a promotion, it appears here.

) : (
- +
diff --git a/web/src/pages/DiffPage.tsx b/web/src/pages/DiffPage.tsx index 9d9c444..3c2ddae 100644 --- a/web/src/pages/DiffPage.tsx +++ b/web/src/pages/DiffPage.tsx @@ -1,162 +1,24 @@ -import { useState } from "react"; -import { Link } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { Link, useSearchParams } from "react-router-dom"; import { fetchJson } from "../api"; -import { Badge } from "../components/Badge"; import { JsonPanel } from "../components/JsonPanel"; - -type DiffJson = Record; - -function isRecord(v: unknown): v is Record { - return typeof v === "object" && v !== null; -} - -function pickPolicy(data: DiffJson): { - passed: boolean; - reasons: string[]; - evaluatedAt: string | null; -} | null { - const p = data.policy; - if (!isRecord(p)) return null; - const passed = p.passed; - const reasons = p.reasons; - const ev = p.evaluated_at; - return { - passed: passed === true, - reasons: Array.isArray(reasons) ? reasons.filter((x): x is string => typeof x === "string") : [], - evaluatedAt: typeof ev === "string" ? ev : null, - }; -} - -type CatalogInfo = { - enabled: boolean; - version: string | null; - baselineSlot: string | null; - candidateSlot: string | null; - baselineCost: number | null; - candidateCost: number | null; - deltaCost: number | null; - warnings: string[]; -}; - -type PricingInfo = { - baselineProvider: string; - baselineVersion: string; - baselineModel: string; - candidateProvider: string; - candidateVersion: string; - candidateModel: string; - changed: boolean; - prices: PricingPrices | null; - warnings: string[]; - hints: string[]; - catalog: CatalogInfo | null; -}; - -type PricingPrices = { - baselineInput: number | null; - baselineOutput: number | null; - candidateInput: number | null; - candidateOutput: number | null; -}; - -function pickPrices(p: Record): PricingPrices | null { - const block = p.prices; - if (!isRecord(block)) return null; - const numOrNull = (k: string): number | null => - typeof block[k] === "number" && Number.isFinite(block[k]) ? (block[k] as number) : null; - return { - baselineInput: numOrNull("baseline_input_usd_per_1k_tokens"), - baselineOutput: numOrNull("baseline_output_usd_per_1k_tokens"), - candidateInput: numOrNull("candidate_input_usd_per_1k_tokens"), - candidateOutput: numOrNull("candidate_output_usd_per_1k_tokens"), - }; -} - -/** - * Coerces the `pricing` block from `/v1/diff` into a typed view. The contract - * is set by the route in `src/flightdeck/server/routes/actions.py`. - */ -function pickCatalog(block: Record): CatalogInfo { - const rawW = block.warnings; - const warnings = Array.isArray(rawW) ? rawW.filter((x): x is string => typeof x === "string") : []; - const numOrNull = (k: string): number | null => - typeof block[k] === "number" && Number.isFinite(block[k]) ? (block[k] as number) : null; - const strOrNull = (k: string): string | null => - typeof block[k] === "string" ? (block[k] as string) : null; - return { - enabled: block.enabled === true, - version: strOrNull("catalog_version"), - baselineSlot: strOrNull("baseline_slot_id"), - candidateSlot: strOrNull("candidate_slot_id"), - baselineCost: numOrNull("baseline_cost_per_run_usd"), - candidateCost: numOrNull("candidate_cost_per_run_usd"), - deltaCost: numOrNull("delta_cost_per_run_usd"), - warnings, - }; -} - -function pickPricing(data: DiffJson): PricingInfo | null { - const p = data.pricing; - if (!isRecord(p)) return null; - const get = (k: string): string => (typeof p[k] === "string" ? (p[k] as string) : ""); - const rawWarnings = p.warnings; - const warnings = Array.isArray(rawWarnings) - ? rawWarnings.filter((x): x is string => typeof x === "string") - : []; - const rawHints = p.hints; - const hints = Array.isArray(rawHints) ? rawHints.filter((x): x is string => typeof x === "string") : []; - const catRaw = p.catalog; - const catalog = isRecord(catRaw) ? pickCatalog(catRaw) : null; - return { - baselineProvider: get("baseline_provider"), - baselineVersion: get("baseline_version"), - baselineModel: get("baseline_model"), - candidateProvider: get("candidate_provider"), - candidateVersion: get("candidate_version"), - candidateModel: get("candidate_model"), - changed: p.pricing_or_model_changed === true, - prices: pickPrices(p), - warnings, - hints, - catalog, - }; -} - -function Metric({ - label, - baseline, - candidate, - delta, - suffix = "", -}: { - label: string; - baseline: string; - candidate: string; - delta?: string; - suffix?: string; -}) { - return ( -
-
{label}
-
- - B {baseline} - {suffix} - - - → - - - C {candidate} - {suffix} - -
- {delta ?
{delta}
: null} -
- ); -} +import { DiffChangeImpact } from "../components/diff/DiffChangeImpact"; +import { DiffDecisionCard } from "../components/diff/DiffDecisionCard"; +import { DiffPolicyPanel } from "../components/diff/DiffPolicyPanel"; +import { DiffReleaseTwin } from "../components/diff/DiffReleaseTwin"; +import { DiffVerdictStack } from "../components/diff/DiffVerdictStack"; +import { + type DiffJson, + isRecord, + pickPolicy, + pickPricing, +} from "../components/diff/diffPayload"; +import { UI_READ_ONLY } from "../uiConfig"; +import { pickTrimmedSearch, searchParamsFromRecord } from "../urlSearch"; export function DiffPage() { + const [searchParams, setSearchParams] = useSearchParams(); + const [diffResultSeq, setDiffResultSeq] = useState(0); const [diffBaseline, setDiffBaseline] = useState(""); const [diffCandidate, setDiffCandidate] = useState(""); const [diffWindow, setDiffWindow] = useState("7d"); @@ -165,16 +27,35 @@ export function DiffPage() { const [diffErr, setDiffErr] = useState(null); const [busy, setBusy] = useState(false); + useEffect(() => { + setDiffBaseline(pickTrimmedSearch(searchParams, "baseline")); + setDiffCandidate(pickTrimmedSearch(searchParams, "candidate")); + const w = pickTrimmedSearch(searchParams, "window"); + setDiffWindow(w !== "" ? w : "7d"); + const e = pickTrimmedSearch(searchParams, "environment"); + setDiffEnv(e !== "" ? e : "local"); + }, [searchParams]); + const runDiff = async () => { setDiffErr(null); setDiffOut(null); setBusy(true); + const baseline = diffBaseline.trim(); + const candidate = diffCandidate.trim(); + const windowVal = diffWindow.trim(); + const envVal = diffEnv.trim(); + const nextParams = new URLSearchParams(); + if (baseline) nextParams.set("baseline", baseline); + if (candidate) nextParams.set("candidate", candidate); + nextParams.set("window", windowVal || "7d"); + if (envVal) nextParams.set("environment", envVal); + setSearchParams(nextParams); try { const body = { - baseline_release_id: diffBaseline.trim(), - candidate_release_id: diffCandidate.trim(), - window: diffWindow.trim(), - environment: diffEnv.trim() || null, + baseline_release_id: baseline, + candidate_release_id: candidate, + window: windowVal, + environment: envVal || null, }; const data = await fetchJson("/v1/diff", { method: "POST", @@ -182,6 +63,7 @@ export function DiffPage() { body: JSON.stringify(body), }); setDiffOut(data); + setDiffResultSeq((n) => n + 1); } catch (e) { setDiffErr(String(e)); } finally { @@ -196,19 +78,32 @@ export function DiffPage() { const policy = diffOut ? pickPolicy(diffOut) : null; const pricing = diffOut ? pickPricing(diffOut) : null; - const num = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? String(v) : "—"); - const pct = (v: unknown) => - typeof v === "number" && Number.isFinite(v) ? `${(v * 100).toFixed(2)}%` : "—"; + const promoteSearch = + !UI_READ_ONLY && diffCandidate.trim() !== "" + ? searchParamsFromRecord({ + release_id: diffCandidate.trim(), + environment: diffEnv.trim(), + window: diffWindow.trim(), + }) + : ""; return ( <>

Run diff

-

- Compare baseline vs candidate over a window. Same contract as{" "} - flightdeck release diff. Release IDs usually come from{" "} - Overview or the CLI—nothing is prefilled from navigation. +

+ What changed? Baseline vs candidate releases over a window. Is it safe? Policy + verdict below. Can I ship? Use promote when policy passes —{" "} + Actions. +

+

+ Overview shortcuts and URLs can prefill{" "} + baseline,{" "} + candidate,{" "} + window,{" "} + environment; click Compute diff to run (same + contract as flightdeck release diff).

@@ -269,240 +164,22 @@ export function DiffPage() { {diffOut ? ( <> -
-
-

Diff result

-
-
-
-

- Policy gate -

- {policy ? ( -
- {policy.passed ? "PASS" : "FAIL"} - {policy.evaluatedAt ? ( - evaluated_at {policy.evaluatedAt} - ) : null} - {policy.reasons.length > 0 ? ( -
    - {policy.reasons.map((r) => ( -
  • {r}
  • - ))} -
- ) : ( -

- No policy constraint messages (pass with empty reasons, or policy omitted). -

- )} -
- ) : ( -

No policy block in this response.

- )} -
+ + + + + {policy ? : null} -
-

- Evidence window -

- {samples ? ( -

- Baseline runs: {num(samples.baseline_runs)} · Candidate runs:{" "} - {num(samples.candidate_runs)} · Confidence:{" "} - {String(samples.confidence ?? "—")} - {typeof samples.confidence_reason === "string" ? ` — ${samples.confidence_reason}` : null} -

- ) : ( -

No sample counts in this response.

- )} -
+ -
-

- Pricing, model, and catalog -

- {pricing ? ( -
-

- Resolved models:{" "} - - {pricing.baselineProvider}/{pricing.baselineVersion} {pricing.baselineModel} - {" "} - →{" "} - - {pricing.candidateProvider}/{pricing.candidateVersion} {pricing.candidateModel} - - {pricing.changed ? ( - pricing/model changed - ) : ( - unchanged - )} -

- {(() => { - const bp = pricing.baselineProvider.trim(); - const cp = pricing.candidateProvider.trim(); - const bv = pricing.baselineVersion.trim(); - const cv = pricing.candidateVersion.trim(); - const providerSkew = bp.length > 0 && cp.length > 0 && bp !== cp; - const versionSkew = bv.length > 0 && cv.length > 0 && bv !== cv; - if (!providerSkew && !versionSkew) return null; - return ( -

- {versionSkew ? ( - <> - Imported pricing table versions differ ( - {bv} vs{" "} - {cv}).{" "} - - ) : null} - {providerSkew ? ( - <> - Providers differ ( - {bp} vs{" "} - {cp}).{" "} - - ) : null} - Treat per-1k and catalog lines below as resolved per release; skew can change comparability. -

- ); - })()} - {pricing.warnings.length > 0 ? ( - <> -

Pricing warnings

-
    - {pricing.warnings.map((w) => ( -
  • {w}
  • - ))} -
- - ) : null} - {pricing.hints.length > 0 ? ( - <> -

Hints

-
    - {pricing.hints.map((h) => ( -
  • {h}
  • - ))} -
- - ) : null} - {pricing.catalog && (pricing.catalog.enabled || pricing.catalog.warnings.length > 0) ? ( - <> -

Pricing catalog

-
- {pricing.catalog.enabled ? ( -

- Catalog v{pricing.catalog.version ?? "—"} · slots{" "} - {pricing.catalog.baselineSlot ?? "—"} →{" "} - {pricing.catalog.candidateSlot ?? "—"} - {pricing.catalog.baselineCost !== null && - pricing.catalog.candidateCost !== null && - pricing.catalog.deltaCost !== null ? ( - <> -
- Comparable cost/run: {pricing.catalog.baselineCost.toFixed(6)} →{" "} - {pricing.catalog.candidateCost.toFixed(6)} (Δ{" "} - {pricing.catalog.deltaCost >= 0 ? "+" : ""} - {pricing.catalog.deltaCost.toFixed(6)}) - - ) : null} -

- ) : ( -

- Catalog disabled or incomplete for this diff. -

- )} - {pricing.catalog.warnings.length > 0 ? ( -
    - {pricing.catalog.warnings.map((w) => ( -
  • {w}
  • - ))} -
- ) : null} -
- - ) : null} - {pricing.changed && - pricing.prices && - pricing.prices.baselineInput !== null && - pricing.prices.candidateInput !== null && - pricing.prices.baselineOutput !== null && - pricing.prices.candidateOutput !== null ? ( - <> -

Per-1k token prices (USD)

-
-
-
Input / 1k
-
- {pricing.prices.baselineInput.toFixed(6)} → {pricing.prices.candidateInput.toFixed(6)} -
-
-
-
Output / 1k
-
- {pricing.prices.baselineOutput.toFixed(6)} → {pricing.prices.candidateOutput.toFixed(6)} -
-
-
-

- Cost rollups reflect pricing table and model identity; compare with catalog lines above when - configured. -

- - ) : null} -
- ) : ( -

No pricing block in this response.

- )} -
+ -
-

- Cost and quality rollups -

- {metrics ? ( -
- = 0 ? "+" : ""}${(metrics.delta_cost_per_run_pct * 100).toFixed(2)}% vs baseline)` - : "" - }` - : undefined - } - /> - - -
- ) : ( -

No metrics block in this response.

- )} -
-
-
) : null} diff --git a/web/src/pages/OverviewPage.tsx b/web/src/pages/OverviewPage.tsx index 5e95316..0a1573d 100644 --- a/web/src/pages/OverviewPage.tsx +++ b/web/src/pages/OverviewPage.tsx @@ -1,11 +1,14 @@ -import { useCallback, useEffect, useId, useState, type ReactNode } from "react"; -import { Link } from "react-router-dom"; +import { useCallback, useEffect, useId, useMemo, useState, type ReactNode } from "react"; +import { Link, useSearchParams } from "react-router-dom"; import type { ActionRow, MetricsPayload, PromotedRow, ReleaseRow, TimelinePayload } from "../api"; import { fetchMetrics, loadTimeline } from "../api"; import { useTimelineRefresh } from "../context/TimelineRefreshContext"; import { Badge } from "../components/Badge"; +import { CopyTextButton } from "../components/CopyTextButton"; import { JsonPanel } from "../components/JsonPanel"; import { ReleaseLifecycleStrip } from "../components/ReleaseLifecycleStrip"; +import { UI_READ_ONLY } from "../uiConfig"; +import { searchParamsFromRecord } from "../urlSearch"; const OVERVIEW_POLL_MS = 30_000; @@ -17,10 +20,12 @@ function shortId(id: string, keepStart = 10, keepEnd = 6) { function TableShell({ title, description, + toolbar, children, }: { title: string; description?: string; + toolbar?: ReactNode; children: ReactNode; }) { const hid = useId(); @@ -32,12 +37,16 @@ function TableShell({ {description ?

{description}

: null} + {toolbar ?
{toolbar}
: null}
{children}
); } export function OverviewPage() { + const [searchParams, setSearchParams] = useSearchParams(); + const focusReleaseId = (searchParams.get("release") ?? "").trim(); + const { generation } = useTimelineRefresh(); const [data, setData] = useState(null); const [metrics, setMetrics] = useState(null); @@ -94,21 +103,158 @@ export function OverviewPage() { 2, ); + const focusRelease = useMemo(() => { + if (!data || !focusReleaseId) return null; + return data.releases.find((r) => r.release_id === focusReleaseId) ?? null; + }, [data, focusReleaseId]); + + const promotedBaselineForFocus = useMemo(() => { + if (!data || !focusRelease) return null; + return ( + data.promoted.find( + (p) => p.agent_id === focusRelease.agent_id && p.environment === focusRelease.environment, + ) ?? null + ); + }, [data, focusRelease]); + + const clearReleaseFocus = () => { + const next = new URLSearchParams(searchParams); + next.delete("release"); + setSearchParams(next); + }; + + const baselineReleaseForRow = (r: ReleaseRow) => + data?.promoted.find((p) => p.agent_id === r.agent_id && p.environment === r.environment)?.release_id ?? ""; + + const releaseLookup = useMemo(() => { + const m = new Map(); + if (!data) return m; + for (const r of data.releases) m.set(r.release_id, r); + return m; + }, [data]); + + const [filterAgent, setFilterAgent] = useState(""); + const [filterEnv, setFilterEnv] = useState(""); + const [filterPromoted, setFilterPromoted] = useState<"" | "live" | "not-live">(""); + + const filteredReleases = useMemo(() => { + if (!data) return []; + const a = filterAgent.trim().toLowerCase(); + const e = filterEnv.trim().toLowerCase(); + return data.releases.filter((r) => { + if (a && !r.agent_id.toLowerCase().includes(a)) return false; + if (e && !r.environment.toLowerCase().includes(e)) return false; + const baseline = + data.promoted.find((p) => p.agent_id === r.agent_id && p.environment === r.environment)?.release_id ?? ""; + const isLive = baseline !== "" && baseline === r.release_id; + if (filterPromoted === "live" && !isLive) return false; + if (filterPromoted === "not-live" && isLive) return false; + return true; + }); + }, [data, filterAgent, filterEnv, filterPromoted]); + + const metricsPanelId = useId(); + const [metricsOpen, setMetricsOpen] = useState(false); + return ( <>

Overview

-

- Registered releases, promotion pointers, and recent ledger actions. Refreshes automatically every{" "} - {OVERVIEW_POLL_MS / 1000}s while this tab is visible; also updates after promote or rollback from{" "} - Actions. +

+ What changed? Scan releases and promotion pointers. Is it safe? Use{" "} + Diff for policy on baseline vs candidate. Can I ship? Promote from{" "} + Actions when policy passes. +

+

+ Tables refresh every {OVERVIEW_POLL_MS / 1000}s while this tab is visible; mutations update after promote or + rollback.

+ {focusReleaseId && !loading && data ? ( + focusRelease ? ( +
+

+ + {focusRelease.agent_id} v{focusRelease.version} + {" "} + ({focusRelease.environment}) +

+

+ Release ID{" "} + + {shortId(focusRelease.release_id, 14, 8)} + + · checksum{" "} + + {shortId(focusRelease.checksum, 8, 6)} + + {promotedBaselineForFocus ? ( + <> + {" "} + · promoted baseline for this pair:{" "} + + {shortId(promotedBaselineForFocus.release_id, 14, 8)} + + + ) : ( + <> · no promoted pointer for this agent/environment yet + )} +

+
+ + Open diff + + + Open runs + + {UI_READ_ONLY ? null : ( + + Promote + + )} + +
+
+ ) : ( +
+ Unknown release in URL. No registered release matches{" "} + {shortId(focusReleaseId, 20, 10)}.{" "} + +
+ ) + ) : null} + {error && !loading ?

{error}

: null} {loading ? (
@@ -119,151 +265,210 @@ export function OverviewPage() {
) : null} - {metrics ? ( -
-
-

Ledger metrics

-

- Read-only counters from{" "} - GET /v1/metrics (schema v - {metrics.schema_version}, generated{" "} - {new Date(metrics.generated_at).toLocaleString()}). -

-
-
-
-
Releases
-
- {metrics.counters.releases_total} -
-

Registered release.yaml bundles in this workspace.

-
-
-
Pricing tables
-
- {metrics.counters.pricing_tables_total} -
-

Imported pricing CSV snapshots used for diff economics.

-
-
-
Run events
-
- {metrics.counters.run_events_total} -
-

Ingested runtime evidence rows (CLI ingest or POST /v1/events).

-
-
-
Promoted pointers
-
- {metrics.counters.promoted_pointers_total} -
-

Active promoted release pointers per agent/environment pair.

-
-
-
Actions
-
- {metrics.counters.actions_total} -
- {Object.keys(metrics.counters.actions_by_action).length > 0 ? ( -
- {Object.entries(metrics.counters.actions_by_action) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([k, v]) => `${k}=${v}`) - .join(" · ")} -
- ) : null} -

Ledger audit rows for promote/rollback attempts (policy outcome recorded).

-
-
-

- Next: open Diff to compare releases, or Runs for evidence - forensics. -

-
- ) : metricsError && !loading ? ( -

Ledger metrics unavailable: {metricsError}

- ) : null} - {data ? ( <> - -
Request ID
+ +
- - - - + + + - {data.releases.length === 0 ? ( + {data.promoted.length === 0 ? ( - ) : ( - data.releases.map((r: ReleaseRow) => ( - - - - - - - - - )) + data.promoted.map((p: PromotedRow) => { + const row = releaseLookup.get(p.release_id); + return ( + + + + + + + + ); + }) )}
Release ID AgentVersion EnvironmentChecksumCreatedPromoted releaseVersion + Copy +
- No releases yet. + + No promotion pointers yet.
- - {shortId(r.release_id)} - - {r.agent_id}{r.version}{r.environment} - - {shortId(r.checksum, 8, 6)} - - {new Date(r.created_at).toLocaleString()}
{p.agent_id}{p.environment} + + + {shortId(p.release_id)} + + + {row?.version ?? "—"} + +
- - + + + + + + } + > +
- - - + + + + + - {data.promoted.length === 0 ? ( + {data.releases.length === 0 ? ( - + + ) : filteredReleases.length === 0 ? ( + + ) : ( - data.promoted.map((p: PromotedRow) => ( - - - - - - )) + filteredReleases.map((r: ReleaseRow) => { + const baseline = baselineReleaseForRow(r); + const isLive = baseline !== "" && baseline === r.release_id; + return ( + + + + + + + + ); + }) )}
AgentEnvironmentActive releasePrimaryRelease IDChecksumCreatedShortcuts
- No promotion pointers yet. + + No releases yet. +
+ No releases match filters.
{p.agent_id}{p.environment} - - {shortId(p.release_id)} - -
+
+ + {r.agent_id} v{r.version} + + {r.environment} + {isLive ? ( + Live + ) : ( + Registered + )} +
+
+
+ + {shortId(r.release_id)} + + +
+
+ + {shortId(r.checksum, 8, 6)} + + {new Date(r.created_at).toLocaleString()} +
+ + Diff + + + Runs + + {UI_READ_ONLY ? null : ( + + Promote + + )} +
+
- +
@@ -305,6 +510,91 @@ export function OverviewPage() {
When
+ {metrics ? ( +
+ + +
+ ) : metricsError && !loading ? ( +

Ledger metrics unavailable: {metricsError}

+ ) : null} + ) : null} diff --git a/web/src/pages/RunsPage.tsx b/web/src/pages/RunsPage.tsx index 6d450f9..a929662 100644 --- a/web/src/pages/RunsPage.tsx +++ b/web/src/pages/RunsPage.tsx @@ -1,8 +1,9 @@ import { useCallback, useEffect, useId, useRef, useState, type ReactNode } from "react"; -import { Link } from "react-router-dom"; +import { Link, useSearchParams } from "react-router-dom"; import type { ReleaseRow, RunsListPayload } from "../api"; import { fetchRuns, fetchRunsExportBlob, loadTimeline } from "../api"; import { JsonPanel } from "../components/JsonPanel"; +import { pickTrimmedSearch } from "../urlSearch"; function shortId(id: string, keepStart = 12, keepEnd = 6) { if (id.length <= keepStart + keepEnd + 1) return id; @@ -102,6 +103,7 @@ function buildTraceGroups(events: unknown[]): { key: string; rows: Record(null); const drawerPanelRef = useRef(null); const drawerReturnFocusRef = useRef(null); @@ -138,6 +140,15 @@ export function RunsPage() { }); }, []); + useEffect(() => { + const rid = pickTrimmedSearch(searchParams, "release_id"); + const win = pickTrimmedSearch(searchParams, "window"); + const env = pickTrimmedSearch(searchParams, "environment"); + if (rid) setReleaseId(rid); + if (win) setWindowVal(win); + setEnvironment(env); + }, [searchParams]); + const closeDrawer = useCallback(() => { setDetailEvent(null); window.setTimeout(() => { @@ -363,10 +374,14 @@ export function RunsPage() {

Run events

-

- Read-only slice of ingested runs (GET /v1/runs). Newest - first; offset pages through the match set. Paste a release ID from Overview (or the CLI), then load. Use a row's View action for structured detail (same payload - as export lines). +

+ What changed? Inspect ingested runs for a release. Is it safe? Correlate with{" "} + Diff policy. Can I ship? Evidence supports the promotion decision on{" "} + Actions. +

+

+ Read-only GET /v1/runs, newest first; paste a release ID from{" "} + Overview or the CLI. Row View opens structured detail (same shape as export lines).

@@ -553,7 +568,7 @@ export function RunsPage() { )}
- +
{tableHead} {g.rows.map((rec, idx) => renderEventRow(rec, idx, g.key))}
@@ -564,7 +579,7 @@ export function RunsPage() {
) : (
- +
{tableHead} {result.events.length === 0 ? ( diff --git a/web/src/pages/SettingsPage.tsx b/web/src/pages/SettingsPage.tsx index 431eed9..5c45367 100644 --- a/web/src/pages/SettingsPage.tsx +++ b/web/src/pages/SettingsPage.tsx @@ -6,7 +6,11 @@ export function SettingsPage() {

Settings

-

Appearance and workspace preferences (more options later).

+

+ What changed? Theme preference only for now. Is it safe? Stored locally in this + browser. Can I ship? Unrelated to promotion — use Actions for ledger writes. +

+

Appearance and workspace preferences (more options later).

diff --git a/web/src/urlSearch.ts b/web/src/urlSearch.ts new file mode 100644 index 0000000..9445077 --- /dev/null +++ b/web/src/urlSearch.ts @@ -0,0 +1,15 @@ +/** Hash-router search helpers for deep-linking forms. */ + +export function pickTrimmedSearch(searchParams: URLSearchParams, key: string): string { + const raw = searchParams.get(key); + return raw !== null ? raw.trim() : ""; +} + +export function searchParamsFromRecord(rec: Record): string { + const sp = new URLSearchParams(); + for (const [k, v] of Object.entries(rec)) { + if (v !== "") sp.set(k, v); + } + const s = sp.toString(); + return s ? `?${s}` : ""; +}