diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d479d481..25fa9725b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to Bambuddy will be documented in this file. - **SpoolBuddy Kiosk Auth Bypass via API Key** — When Bambuddy auth is enabled, the SpoolBuddy kiosk (Chromium on RPi) was redirected to the login page because the `ProtectedRoute` requires a user object from `GET /auth/me`, which only accepted JWT tokens. The `/auth/me` endpoint now also accepts API keys (via `Authorization: Bearer bb_xxx` or `X-API-Key` header) and returns a synthetic admin user with all permissions. The frontend's `AuthContext` reads an optional `?token=` URL parameter on first load, stores it in localStorage, and strips it from the URL to prevent leakage via browser history or referrer. The install script now includes the API key in the kiosk URL (`/spoolbuddy?token=${API_KEY}`), so the device authenticates automatically on boot without manual login. ### Fixed +- **Sidebar Navigation Ignores User Permissions** — All sidebar navigation items (Archives, Queue, Stats, Profiles, Maintenance, Projects, Inventory, Files) were visible to every user regardless of their role's permissions. Only the Settings item was permission-gated. Now each nav item is hidden when the user lacks the corresponding read permission (e.g., `archives:read`, `queue:read`, `library:read`). The Printers item remains always visible as the home page. Also added the missing `inventory:read|create|update|delete` permissions to the frontend Permission type (they existed in the backend but were absent from the frontend type definition). - **Camera Button Clickable Without Permission & ffmpeg Process Leak** ([#550](https://github.com/maziggy/bambuddy/issues/550)) — Two camera issues in multi-user environments (e.g., classrooms with multiple printers). First, the camera button on the printer card was clickable even when the user's role lacked `camera:view` permission. Now disabled with a permission tooltip, matching the existing pattern for `printers:control` on the chamber light button. Second, ffmpeg processes (~240MB each) were never cleaned up after closing a camera stream. The `stop_camera_stream` endpoint called `terminate()` but never `wait()`ed or `kill()`ed, and HTTP disconnect detection in the streaming response only checked between frames — if the generator was blocked reading from ffmpeg stdout, disconnect was never detected (due to TCP send buffer masking the closed connection). Three fixes: (1) the stop endpoint now uses `terminate()` → `wait(2s)` → `kill()` → `wait()`; (2) each stream gets a background disconnect monitor task that polls `request.is_disconnected()` every 2 seconds independently of the frame loop, directly killing the ffmpeg process on disconnect; (3) a periodic cleanup (every 60s) scans `/proc` for any ffmpeg process with a Bambu RTSP URL (`rtsps://bblp:`) that isn't in an active stream and `SIGKILL`s it — catching orphans that survive app restarts or generator abandonment. - **Windows Install Fails With "Syntax of the Command Is Incorrect"** ([#544](https://github.com/maziggy/bambuddy/issues/544)) — The `start_bambuddy.bat` launcher had Unix (LF) line endings instead of Windows (CRLF). When a user's git config has `core.autocrlf=false` or `input`, the file is checked out with LF endings and `cmd.exe` cannot parse it. Added a `.gitattributes` file that forces CRLF for all `.bat` files regardless of git config. - **Queue Badge Shows on Incompatible Printers** ([#486](https://github.com/maziggy/bambuddy/issues/486)) — The purple queue counter badge in the printer card header showed on all printers of the same model when a job was scheduled for "any [model]", even if the printer didn't have the matching filament color loaded. The `PrinterQueueWidget` (which shows "Clear Plate & Start") already filtered by filament type and color, but the badge count used the raw unfiltered queue length. Now applies the same filament compatibility filter to the badge count. diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 5559650b8..2180c65a5 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1998,6 +1998,7 @@ export type Permission = | 'library:update_own' | 'library:update_all' | 'library:delete_own' | 'library:delete_all' | 'projects:read' | 'projects:create' | 'projects:update' | 'projects:delete' | 'filaments:read' | 'filaments:create' | 'filaments:update' | 'filaments:delete' + | 'inventory:read' | 'inventory:create' | 'inventory:update' | 'inventory:delete' | 'smart_plugs:read' | 'smart_plugs:create' | 'smart_plugs:update' | 'smart_plugs:delete' | 'smart_plugs:control' | 'camera:view' | 'maintenance:read' | 'maintenance:create' | 'maintenance:update' | 'maintenance:delete' diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 82c80bc81..a1792c8e7 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -6,7 +6,7 @@ import { useTheme } from '../contexts/ThemeContext'; import { KeyboardShortcutsModal } from './KeyboardShortcutsModal'; import { SwitchbarPopover } from './SwitchbarPopover'; import { useQuery, useQueries } from '@tanstack/react-query'; -import { api, supportApi, pendingUploadsApi } from '../api/client'; +import { api, supportApi, pendingUploadsApi, type Permission } from '../api/client'; import { getIconByName } from './IconPicker'; import { useIsSidebarCompact } from '../hooks/useIsSidebarCompact'; import { useAuth } from '../contexts/AuthContext'; @@ -216,16 +216,30 @@ export function Layout() { const extLinksMap = useMemo(() => new Map((externalLinks || []).map(link => [`ext-${link.id}`, link])), [externalLinks]); // Compute the ordered sidebar: include stored order + any new items - // Filter out 'settings' for users with 'user' role + // Hide nav items the user doesn't have read permission for const orderedSidebarIds = (() => { const result: string[] = []; const seen = new Set(); - // Determine if settings should be hidden (no settings:read permission) - const hideSettings = authEnabled && !hasPermission('settings:read'); + // Map nav item IDs to the permission required to see them + const navPermissions: Record = { + archives: 'archives:read', + queue: 'queue:read', + stats: 'stats:read', + profiles: 'kprofiles:read', + maintenance: 'maintenance:read', + projects: 'projects:read', + inventory: 'inventory:read', + files: 'library:read', + settings: 'settings:read', + }; + + const isHidden = (id: string) => + authEnabled && id in navPermissions && !hasPermission(navPermissions[id]); + // Add items in stored order for (const id of sidebarOrder) { - if (hideSettings && id === 'settings') continue; + if (isHidden(id)) continue; if (navItemsMap.has(id) || extLinksMap.has(id)) { result.push(id); seen.add(id); @@ -234,7 +248,7 @@ export function Layout() { // Add any new internal nav items not in stored order for (const item of defaultNavItems) { - if (hideSettings && item.id === 'settings') continue; + if (isHidden(item.id)) continue; if (!seen.has(item.id)) { result.push(item.id); seen.add(item.id); diff --git a/static/assets/index-BfXsQTz2.js b/static/assets/index-rCstaUXR.js similarity index 66% rename from static/assets/index-BfXsQTz2.js rename to static/assets/index-rCstaUXR.js index 6faae566e..43285e070 100644 --- a/static/assets/index-BfXsQTz2.js +++ b/static/assets/index-rCstaUXR.js @@ -1,5 +1,6 @@ function dle(t,e){for(var n=0;nr[i]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).observe(document,{childList:!0,subtree:!0});function n(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function r(i){if(i.ep)return;i.ep=!0;const s=n(i);fetch(i.href,s)}})();var Qw=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function rc(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var AM={exports:{}},Yx={};var BU;function ule(){if(BU)return Yx;BU=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function n(r,i,s){var o=null;if(s!==void 0&&(o=""+s),i.key!==void 0&&(o=""+i.key),"key"in i){s={};for(var l in i)l!=="key"&&(s[l]=i[l])}else s=i;return i=s.ref,{$$typeof:t,type:r,key:o,ref:i!==void 0?i:null,props:s}}return Yx.Fragment=e,Yx.jsx=n,Yx.jsxs=n,Yx}var HU;function hle(){return HU||(HU=1,AM.exports=ule()),AM.exports}var a=hle(),TM={exports:{}},kn={};var $U;function mle(){if($U)return kn;$U=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),s=Symbol.for("react.consumer"),o=Symbol.for("react.context"),l=Symbol.for("react.forward_ref"),c=Symbol.for("react.suspense"),d=Symbol.for("react.memo"),u=Symbol.for("react.lazy"),h=Symbol.for("react.activity"),f=Symbol.iterator;function p(H){return H===null||typeof H!="object"?null:(H=f&&H[f]||H["@@iterator"],typeof H=="function"?H:null)}var x={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},v=Object.assign,g={};function y(H,X,Z){this.props=H,this.context=X,this.refs=g,this.updater=Z||x}y.prototype.isReactComponent={},y.prototype.setState=function(H,X){if(typeof H!="object"&&typeof H!="function"&&H!=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,H,X,"setState")},y.prototype.forceUpdate=function(H){this.updater.enqueueForceUpdate(this,H,"forceUpdate")};function _(){}_.prototype=y.prototype;function N(H,X,Z){this.props=H,this.context=X,this.refs=g,this.updater=Z||x}var A=N.prototype=new _;A.constructor=N,v(A,y.prototype),A.isPureReactComponent=!0;var k=Array.isArray;function P(){}var C={H:null,A:null,T:null,S:null},j=Object.prototype.hasOwnProperty;function M(H,X,Z){var ee=Z.ref;return{$$typeof:t,type:H,key:X,ref:ee!==void 0?ee:null,props:Z}}function E(H,X){return M(H.type,X,H.props)}function B(H){return typeof H=="object"&&H!==null&&H.$$typeof===t}function L(H){var X={"=":"=0",":":"=2"};return"$"+H.replace(/[=:]/g,function(Z){return X[Z]})}var U=/\/+/g;function D(H,X){return typeof H=="object"&&H!==null&&H.key!=null?L(""+H.key):X.toString(36)}function $(H){switch(H.status){case"fulfilled":return H.value;case"rejected":throw H.reason;default:switch(typeof H.status=="string"?H.then(P,P):(H.status="pending",H.then(function(X){H.status==="pending"&&(H.status="fulfilled",H.value=X)},function(X){H.status==="pending"&&(H.status="rejected",H.reason=X)})),H.status){case"fulfilled":return H.value;case"rejected":throw H.reason}}throw H}function q(H,X,Z,ee,Se){var oe=typeof H;(oe==="undefined"||oe==="boolean")&&(H=null);var ie=!1;if(H===null)ie=!0;else switch(oe){case"bigint":case"string":case"number":ie=!0;break;case"object":switch(H.$$typeof){case t:case e:ie=!0;break;case u:return ie=H._init,q(ie(H._payload),X,Z,ee,Se)}}if(ie)return Se=Se(H),ie=ee===""?"."+D(H,0):ee,k(Se)?(Z="",ie!=null&&(Z=ie.replace(U,"$&/")+"/"),q(Se,X,Z,"",function(ce){return ce})):Se!=null&&(B(Se)&&(Se=E(Se,Z+(Se.key==null||H&&H.key===Se.key?"":(""+Se.key).replace(U,"$&/")+"/")+ie)),X.push(Se)),1;ie=0;var me=ee===""?".":ee+":";if(k(H))for(var se=0;se>>1,te=q[Q];if(0>>1;Qi(Z,W))eei(Se,Z)?(q[Q]=Se,q[ee]=W,Q=ee):(q[Q]=Z,q[X]=W,Q=X);else if(eei(Se,W))q[Q]=Se,q[ee]=W,Q=ee;else break e}}return z}function i(q,z){var W=q.sortIndex-z.sortIndex;return W!==0?W:q.id-z.id}if(t.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var s=performance;t.unstable_now=function(){return s.now()}}else{var o=Date,l=o.now();t.unstable_now=function(){return o.now()-l}}var c=[],d=[],u=1,h=null,f=3,p=!1,x=!1,v=!1,g=!1,y=typeof setTimeout=="function"?setTimeout:null,_=typeof clearTimeout=="function"?clearTimeout:null,N=typeof setImmediate<"u"?setImmediate:null;function A(q){for(var z=n(d);z!==null;){if(z.callback===null)r(d);else if(z.startTime<=q)r(d),z.sortIndex=z.expirationTime,e(c,z);else break;z=n(d)}}function k(q){if(v=!1,A(q),!x)if(n(c)!==null)x=!0,P||(P=!0,L());else{var z=n(d);z!==null&&$(k,z.startTime-q)}}var P=!1,C=-1,j=5,M=-1;function E(){return g?!0:!(t.unstable_now()-Mq&&E());){var Q=h.callback;if(typeof Q=="function"){h.callback=null,f=h.priorityLevel;var te=Q(h.expirationTime<=q);if(q=t.unstable_now(),typeof te=="function"){h.callback=te,A(q),z=!0;break t}h===n(c)&&r(c),A(q)}else r(c);h=n(c)}if(h!==null)z=!0;else{var H=n(d);H!==null&&$(k,H.startTime-q),z=!1}}break e}finally{h=null,f=W,p=!1}z=void 0}}finally{z?L():P=!1}}}var L;if(typeof N=="function")L=function(){N(B)};else if(typeof MessageChannel<"u"){var U=new MessageChannel,D=U.port2;U.port1.onmessage=B,L=function(){D.postMessage(null)}}else L=function(){y(B,0)};function $(q,z){C=y(function(){q(t.unstable_now())},z)}t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(q){q.callback=null},t.unstable_forceFrameRate=function(q){0>q||125Q?(q.sortIndex=W,e(d,q),n(c)===null&&q===n(d)&&(v?(_(C),C=-1):v=!0,$(k,W-Q))):(q.sortIndex=te,e(c,q),x||p||(x=!0,P||(P=!0,L()))),q},t.unstable_shouldYield=E,t.unstable_wrapCallback=function(q){var z=f;return function(){var W=f;f=z;try{return q.apply(this,arguments)}finally{f=W}}}})(EM)),EM}var GU;function gle(){return GU||(GU=1,PM.exports=ple()),PM.exports}var jM={exports:{}},ss={};var WU;function ble(){if(WU)return ss;WU=1;var t=zb();function e(c){var d="https://react.dev/errors/"+c;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(e){console.error(e)}}return t(),jM.exports=ble(),jM.exports}var XU;function xle(){if(XU)return Qx;XU=1;var t=gle(),e=zb(),n=_K();function r(m){var b="https://react.dev/errors/"+m;if(1te||(m.current=Q[te],Q[te]=null,te--)}function Z(m,b){te++,Q[te]=m.current,m.current=b}var ee=H(null),Se=H(null),oe=H(null),ie=H(null);function me(m,b){switch(Z(oe,b),Z(Se,m),Z(ee,null),b.nodeType){case 9:case 11:m=(m=b.documentElement)&&(m=m.namespaceURI)?dU(m):0;break;default:if(m=b.tagName,b=b.namespaceURI)b=dU(b),m=uU(b,m);else switch(m){case"svg":m=1;break;case"math":m=2;break;default:m=0}}X(ee),Z(ee,m)}function se(){X(ee),X(Se),X(oe)}function ce(m){m.memoizedState!==null&&Z(ie,m);var b=ee.current,S=uU(b,m.type);b!==S&&(Z(Se,m),Z(ee,S))}function Ce(m){Se.current===m&&(X(ee),X(Se)),ie.current===m&&(X(ie),Gx._currentValue=W)}var le,Le;function V(m){if(le===void 0)try{throw Error()}catch(S){var b=S.stack.trim().match(/\n( *(at )?)/);le=b&&b[1]||"",Le=-1)":-1F||Ge[T]!==ht[F]){var wt=` @@ -14,6 +15,17 @@ Error generating stack: `+T.message+` >>>>>>> 1eeeb37 ( Updated commit message (now includes the test fix):):static/assets/index-BfXsQTz2.js Please change the parent to .`)}let f=qc(),p;if(e){let _=typeof e=="string"?Ub(e):e;pa(u==="/"||_.pathname?.startsWith(u),`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 "${u}" but pathname "${_.pathname}" was given in the \`location\` prop.`),p=_}else p=f;let x=p.pathname||"/",v=x;if(u!=="/"){let _=u.replace(/^\//,"").split("/");v="/"+x.replace(/^\//,"").split("/").slice(_.length).join("/")}let g=RK(t,{pathname:v});vl(h||g!=null,`No routes matched location "${p.pathname}${p.search}${p.hash}" `),vl(g==null||g[g.length-1].route.element!==void 0||g[g.length-1].route.Component!==void 0||g[g.length-1].route.lazy!==void 0,`Matched leaf route at location "${p.pathname}${p.search}${p.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 y=pde(g&&g.map(_=>Object.assign({},_,{params:Object.assign({},c,_.params),pathname:Vd([u,s.encodeLocation?s.encodeLocation(_.pathname.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:_.pathname]),pathnameBase:_.pathnameBase==="/"?u:Vd([u,s.encodeLocation?s.encodeLocation(_.pathnameBase.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:_.pathnameBase])})),o,n,r,i);return e&&y?w.createElement(iv.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",...p},navigationType:"POP"}},y):y}function ude(){let t=vde(),e=Qce(t)?`${t.status} ${t.statusText}`:t instanceof Error?t.message:JSON.stringify(t),n=t instanceof Error?t.stack:null,r="rgba(200,200,200, 0.5)",i={padding:"0.5rem",backgroundColor:r},s={padding:"2px 4px",backgroundColor:r},o=null;return console.error("Error handled by React Router default ErrorBoundary:",t),o=w.createElement(w.Fragment,null,w.createElement("p",null,"💿 Hey developer 👋"),w.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",w.createElement("code",{style:s},"ErrorBoundary")," or"," ",w.createElement("code",{style:s},"errorElement")," prop on your route.")),w.createElement(w.Fragment,null,w.createElement("h2",null,"Unexpected Application Error!"),w.createElement("h3",{style:{fontStyle:"italic"}},e),n?w.createElement("pre",{style:i},n):null,o)}var hde=w.createElement(ude,null),GK=class extends w.Component{constructor(t){super(t),this.state={location:t.location,revalidation:t.revalidation,error:t.error}}static getDerivedStateFromError(t){return{error:t}}static getDerivedStateFromProps(t,e){return e.location!==t.location||e.revalidation!=="idle"&&t.revalidation==="idle"?{error:t.error,location:t.location,revalidation:t.revalidation}:{error:t.error!==void 0?t.error:e.error,location:e.location,revalidation:t.revalidation||e.revalidation}}componentDidCatch(t,e){this.props.onError?this.props.onError(t,e):console.error("React Router caught the following error during render",t)}render(){let t=this.state.error;if(this.context&&typeof t=="object"&&t&&"digest"in t&&typeof t.digest=="string"){const n=sde(t.digest);n&&(t=n)}let e=t!==void 0?w.createElement(Tl.Provider,{value:this.props.routeContext},w.createElement(hF.Provider,{value:t,children:this.props.component})):this.props.children;return this.context?w.createElement(mde,{error:t},e):e}};GK.contextType=ede;var IM=new WeakMap;function mde({children:t,error:e}){let{basename:n}=w.useContext(Io);if(typeof e=="object"&&e&&"digest"in e&&typeof e.digest=="string"){let r=ide(e.digest);if(r){let i=IM.get(e);if(i)throw i;let s=IK(r.location,n);if(OK&&!IM.get(e))if(s.isExternal||r.reloadDocument)window.location.href=s.absoluteURL||s.to;else{const o=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(s.to,{replace:r.replace}));throw IM.set(e,o),o}return w.createElement("meta",{httpEquiv:"refresh",content:`0;url=${s.absoluteURL||s.to}`})}}return t}function fde({routeContext:t,match:e,children:n}){let r=w.useContext(Bb);return r&&r.static&&r.staticContext&&(e.route.errorElement||e.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=e.route.id),w.createElement(Tl.Provider,{value:t},n)}function pde(t,e=[],n=null,r=null,i=null){if(t==null){if(!n)return null;if(n.errors)t=n.matches;else if(e.length===0&&!n.initialized&&n.matches.length>0)t=n.matches;else return null}let s=t,o=n?.errors;if(o!=null){let u=s.findIndex(h=>h.route.id&&o?.[h.route.id]!==void 0);pa(u>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(o).join(",")}`),s=s.slice(0,Math.min(s.length,u+1))}let l=!1,c=-1;if(n)for(let u=0;u=0?s=s.slice(0,c+1):s=[s[0]];break}}}let d=n&&r?(u,h)=>{r(u,{location:n.location,params:n.matches?.[0]?.params??{},unstable_pattern:Zce(n.matches),errorInfo:h})}:void 0;return s.reduceRight((u,h,f)=>{let p,x=!1,v=null,g=null;n&&(p=o&&h.route.id?o[h.route.id]:void 0,v=h.route.errorElement||hde,l&&(c<0&&f===0?(WK("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),x=!0,g=null):c===f&&(x=!0,g=h.route.hydrateFallbackElement||null)));let y=e.concat(s.slice(0,f+1)),_=()=>{let N;return p?N=v:x?N=g:h.route.Component?N=w.createElement(h.route.Component,null):h.route.element?N=h.route.element:N=u,w.createElement(fde,{match:h,routeContext:{outlet:u,matches:y,isDataRoute:n!=null},children:N})};return n&&(h.route.ErrorBoundary||h.route.errorElement||f===0)?w.createElement(GK,{location:n.location,revalidation:n.revalidation,component:v,error:p,children:_(),routeContext:{outlet:null,matches:y,isDataRoute:!0},onError:d}):_()},null)}function fF(t){return`${t} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function gde(t){let e=w.useContext(Bb);return pa(e,fF(t)),e}function bde(t){let e=w.useContext(zC);return pa(e,fF(t)),e}function xde(t){let e=w.useContext(Tl);return pa(e,fF(t)),e}function pF(t){let e=xde(t),n=e.matches[e.matches.length-1];return pa(n.route.id,`${t} can only be used on routes that contain a unique "id"`),n.route.id}function yde(){return pF("useRouteId")}function vde(){let t=w.useContext(hF),e=bde("useRouteError"),n=pF("useRouteError");return t!==void 0?t:e.errors?.[n]}function wde(){let{router:t}=gde("useNavigate"),e=pF("useNavigate"),n=w.useRef(!1);return $K(()=>{n.current=!0}),w.useCallback(async(i,s={})=>{vl(n.current,HK),n.current&&(typeof i=="number"?await t.navigate(i):await t.navigate(i,{fromRouteId:e,...s}))},[t,e])}var _B={};function WK(t,e,n){!e&&!_B[t]&&(_B[t]=!0,vl(!1,n))}w.memo(Sde);function Sde({routes:t,future:e,state:n,onError:r}){return qK(t,void 0,n,r,e)}function lb({to:t,replace:e,state:n,relative:r}){pa(Hb()," may be used only in the context of a component.");let{static:i}=w.useContext(Io);vl(!i," 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:s}=w.useContext(Tl),{pathname:o}=qc(),l=ac(),c=uF(t,dF(s),o,r==="path"),d=JSON.stringify(c);return w.useEffect(()=>{l(JSON.parse(d),{replace:e,state:n,relative:r})},[l,d,r,e,n]),null}function KK(t){return cde(t.context)}function Ir(t){pa(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function _de({basename:t="/",children:e=null,location:n,navigationType:r="POP",navigator:i,static:s=!1,unstable_useTransitions:o}){pa(!Hb(),"You cannot render a inside another . You should never have more than one in your app.");let l=t.replace(/^\/*/,"/"),c=w.useMemo(()=>({basename:l,navigator:i,static:s,unstable_useTransitions:o,future:{}}),[l,i,s,o]);typeof n=="string"&&(n=Ub(n));let{pathname:d="/",search:u="",hash:h="",state:f=null,key:p="default"}=n,x=w.useMemo(()=>{let v=Jd(d,l);return v==null?null:{location:{pathname:v,search:u,hash:h,state:f,key:p},navigationType:r}},[l,d,u,h,f,p,r]);return vl(x!=null,` is not able to match the URL "${d}${u}${h}" because it does not start with the basename, so the won't render anything.`),x==null?null:w.createElement(Io.Provider,{value:c},w.createElement(iv.Provider,{children:e,value:x}))}function Nde({children:t,location:e}){return dde(Wj(t),e)}function Wj(t,e=[]){let n=[];return w.Children.forEach(t,(r,i)=>{if(!w.isValidElement(r))return;let s=[...e,i];if(r.type===w.Fragment){n.push.apply(n,Wj(r.props.children,s));return}pa(r.type===Ir,`[${typeof r.type=="string"?r.type:r.type.name}] is not a component. All component children of must be a or `),pa(!r.props.index||!r.props.children,"An index route cannot have child routes.");let o={id:r.props.id||s.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&&(o.children=Wj(r.props.children,s)),n.push(o)}),n}var I_="get",z_="application/x-www-form-urlencoded";function UC(t){return typeof HTMLElement<"u"&&t instanceof HTMLElement}function Cde(t){return UC(t)&&t.tagName.toLowerCase()==="button"}function kde(t){return UC(t)&&t.tagName.toLowerCase()==="form"}function Ade(t){return UC(t)&&t.tagName.toLowerCase()==="input"}function Tde(t){return!!(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey)}function Mde(t,e){return t.button===0&&(!e||e==="_self")&&!Tde(t)}function Kj(t=""){return new URLSearchParams(typeof t=="string"||Array.isArray(t)||t instanceof URLSearchParams?t:Object.keys(t).reduce((e,n)=>{let r=t[n];return e.concat(Array.isArray(r)?r.map(i=>[n,i]):[[n,r]])},[]))}function Pde(t,e){let n=Kj(t);return e&&e.forEach((r,i)=>{n.has(i)||e.getAll(i).forEach(s=>{n.append(i,s)})}),n}var Jw=null;function Ede(){if(Jw===null)try{new FormData(document.createElement("form"),0),Jw=!1}catch{Jw=!0}return Jw}var jde=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function zM(t){return t!=null&&!jde.has(t)?(vl(!1,`"${t}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${z_}"`),null):t}function Dde(t,e){let n,r,i,s,o;if(kde(t)){let l=t.getAttribute("action");r=l?Jd(l,e):null,n=t.getAttribute("method")||I_,i=zM(t.getAttribute("enctype"))||z_,s=new FormData(t)}else if(Cde(t)||Ade(t)&&(t.type==="submit"||t.type==="image")){let l=t.form;if(l==null)throw new Error('Cannot submit a