@adcops/autocore-react 3.5.3 → 3.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/components/ams/AmsProvider.d.ts +14 -0
  2. package/dist/components/ams/AmsProvider.d.ts.map +1 -1
  3. package/dist/components/ams/AmsProvider.js +1 -1
  4. package/dist/components/ams/AssetDetailView.d.ts.map +1 -1
  5. package/dist/components/ams/AssetDetailView.js +1 -1
  6. package/dist/components/ams/AssetNameplateGrid.d.ts +13 -0
  7. package/dist/components/ams/AssetNameplateGrid.d.ts.map +1 -0
  8. package/dist/components/ams/AssetNameplateGrid.js +1 -0
  9. package/dist/components/ams/InstalledAssetPicker.d.ts +35 -0
  10. package/dist/components/ams/InstalledAssetPicker.d.ts.map +1 -0
  11. package/dist/components/ams/InstalledAssetPicker.js +1 -0
  12. package/dist/components/ams/index.d.ts +5 -0
  13. package/dist/components/ams/index.d.ts.map +1 -1
  14. package/dist/components/ams/index.js +1 -1
  15. package/dist/components/ams/useInstalledAsset.d.ts +26 -0
  16. package/dist/components/ams/useInstalledAsset.d.ts.map +1 -0
  17. package/dist/components/ams/useInstalledAsset.js +1 -0
  18. package/dist/components/tis/TestFieldRow.d.ts +19 -0
  19. package/dist/components/tis/TestFieldRow.d.ts.map +1 -1
  20. package/dist/components/tis/TestFieldRow.js +1 -1
  21. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -1
  22. package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -1
  23. package/dist/components/tis-editor/types.d.ts +6 -2
  24. package/dist/components/tis-editor/types.d.ts.map +1 -1
  25. package/package.json +1 -1
  26. package/src/components/ams/AmsProvider.tsx +36 -1
  27. package/src/components/ams/AssetDetailView.tsx +2 -20
  28. package/src/components/ams/AssetNameplateGrid.tsx +62 -0
  29. package/src/components/ams/InstalledAssetPicker.tsx +209 -0
  30. package/src/components/ams/index.ts +7 -0
  31. package/src/components/ams/useInstalledAsset.ts +108 -0
  32. package/src/components/tis/TestFieldRow.tsx +31 -0
  33. package/src/components/tis-editor/editor/TestFieldDialog.tsx +29 -18
  34. package/src/components/tis-editor/types.ts +6 -2
@@ -73,6 +73,20 @@ export interface AmsContextValue {
73
73
  listCalibrations: (assetId: string) => Promise<string[]>;
74
74
  readCalibration: (assetId: string, calId: string) => Promise<any | null>;
75
75
  readUsage: (assetId: string) => Promise<any | null>;
76
+ /**
77
+ * Declare which asset is INSTALLED at a role (`ams.assign_role`).
78
+ *
79
+ * Pass `null` for `assetId` to record that nothing is fitted. One RPC,
80
+ * not two `updateAsset` calls: the server vacates the role and installs
81
+ * the new asset atomically, so the role is never momentarily empty or
82
+ * double-claimed — `resolve_method_refs` takes the FIRST active asset at
83
+ * a location, so a double-claimed role resolves to an arbitrary one.
84
+ *
85
+ * Resolves to `null` on success, or the server's message on refusal
86
+ * (wrong asset type for the role, asset not active, unknown id) so the
87
+ * caller can show the operator WHY the swap was refused.
88
+ */
89
+ assignRole: (location: string, assetId: string | null) => Promise<string | null>;
76
90
  selection: AmsSelection;
77
91
  setSelection: (patch: Partial<AmsSelection>) => void;
78
92
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AmsProvider.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AmsProvider.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AmsProvider.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AmsProvider.tsx"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- import{jsx as _jsx}from"react/jsx-runtime";import React,{createContext,useCallback,useContext,useEffect,useMemo,useState}from"react";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";const EMPTY_ALERTS={assetCount:0,calibrationOverdue:0,laneUnavailable:0},AmsContext=createContext({schemas:{},schemasLoaded:!1,roles:{},rolesLoaded:!1,alerts:EMPTY_ALERTS,assets:[],refreshAssets:async()=>{},readAsset:async()=>null,listCalibrations:async()=>[],readCalibration:async()=>null,readUsage:async()=>null,selection:{assetType:null,assetId:null},setSelection:()=>{}});export const AmsProvider=({children:e})=>{const{invoke:s,subscribe:t,unsubscribe:a}=useContext(EventEmitterContext),[n,c]=useState({}),[r,o]=useState(!1),[l,u]=useState({}),[i,m]=useState(!1),[d,C]=useState(EMPTY_ALERTS),[x,b]=useState([]),[y,_]=useState({assetType:null,assetId:null}),A=useCallback(e=>{_(s=>({...s,...e}))},[]),h=useCallback(async()=>{try{const e=await s("ams.list_schemas",MessageType.Request,{});e?.success&&e.data&&(c(e.data.asset_types??{}),o(!0))}catch(e){}},[s]);useEffect(()=>{let e=!1;return h(),(async()=>{try{const t=await s("ams.list_roles",MessageType.Request,{});if(e)return;t?.success&&t.data?(u(t.data.roles??{}),m(!0)):m(!0)}catch(e){m(!0)}})(),()=>{e=!0}},[]);const p=useCallback(async()=>{try{const e=await s("ams.list_assets",MessageType.Request,{include_retired:!0});e?.success&&b(e.data.assets??[])}catch(e){}},[s]);useEffect(()=>{p()},[p]),useEffect(()=>{const e=[t("ams.asset_changed",()=>{p()}),t("ams.calibration_added",()=>{p()}),t("ams.calibration_updated",()=>{p()}),t("ams.schema_changed",()=>{h()}),t("ams.asset_count",e=>C(s=>({...s,assetCount:Number(e)||0}))),t("ams.alert_calibration_overdue",e=>C(s=>({...s,calibrationOverdue:Number(e)||0}))),t("ams.alert_lane_unavailable",e=>C(s=>({...s,laneUnavailable:Number(e)||0})))];return()=>{e.forEach(a)}},[t,a,p,h]);const E=useCallback(async e=>{try{const t=await s("ams.read_asset",MessageType.Request,{asset_id:e});return t?.success?t.data:null}catch{return null}},[s]),f=useCallback(async e=>{try{const t=await s("ams.list_calibrations",MessageType.Request,{asset_id:e});return t?.success?t.data.cal_ids??[]:[]}catch{return[]}},[s]),S=useCallback(async(e,t)=>{try{const a=await s("ams.read_calibration",MessageType.Request,{asset_id:e,cal_id:t});return a?.success?a.data:null}catch{return null}},[s]),T=useCallback(async e=>{try{const t=await s("ams.read_usage",MessageType.Request,{asset_id:e});return t?.success?t.data:null}catch{return null}},[s]),g=useMemo(()=>({schemas:n,schemasLoaded:r,roles:l,rolesLoaded:i,alerts:d,assets:x,refreshAssets:p,readAsset:E,listCalibrations:f,readCalibration:S,readUsage:T,selection:y,setSelection:A}),[n,r,l,i,d,x,p,E,f,S,T,y,A]);return _jsx(AmsContext.Provider,{value:g,children:e})};export const useAms=()=>useContext(AmsContext);export const useAmsSchemas=()=>useContext(AmsContext).schemas;export const useAmsRoles=()=>useContext(AmsContext).roles;export const useAmsAlerts=()=>useContext(AmsContext).alerts;export const useAmsAssets=()=>useContext(AmsContext).assets;export const useAmsSelection=()=>{const e=useContext(AmsContext);return[e.selection,e.setSelection]};
1
+ import{jsx as _jsx}from"react/jsx-runtime";import React,{createContext,useCallback,useContext,useEffect,useMemo,useState}from"react";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";const EMPTY_ALERTS={assetCount:0,calibrationOverdue:0,laneUnavailable:0},AmsContext=createContext({schemas:{},schemasLoaded:!1,roles:{},rolesLoaded:!1,alerts:EMPTY_ALERTS,assets:[],refreshAssets:async()=>{},readAsset:async()=>null,listCalibrations:async()=>[],readCalibration:async()=>null,readUsage:async()=>null,assignRole:async()=>"AmsProvider is not mounted",selection:{assetType:null,assetId:null},setSelection:()=>{}});export const AmsProvider=({children:e})=>{const{invoke:s,subscribe:t,unsubscribe:a}=useContext(EventEmitterContext),[n,r]=useState({}),[c,o]=useState(!1),[l,u]=useState({}),[i,m]=useState(!1),[d,C]=useState(EMPTY_ALERTS),[y,_]=useState([]),[x,b]=useState({assetType:null,assetId:null}),A=useCallback(e=>{b(s=>({...s,...e}))},[]),h=useCallback(async()=>{try{const e=await s("ams.list_schemas",MessageType.Request,{});e?.success&&e.data&&(r(e.data.asset_types??{}),o(!0))}catch(e){}},[s]);useEffect(()=>{let e=!1;return h(),(async()=>{try{const t=await s("ams.list_roles",MessageType.Request,{});if(e)return;t?.success&&t.data?(u(t.data.roles??{}),m(!0)):m(!0)}catch(e){m(!0)}})(),()=>{e=!0}},[]);const p=useCallback(async()=>{try{const e=await s("ams.list_assets",MessageType.Request,{include_retired:!0});e?.success&&_(e.data.assets??[])}catch(e){}},[s]);useEffect(()=>{p()},[p]),useEffect(()=>{const e=[t("ams.asset_changed",()=>{p()}),t("ams.calibration_added",()=>{p()}),t("ams.calibration_updated",()=>{p()}),t("ams.schema_changed",()=>{h()}),t("ams.asset_count",e=>C(s=>({...s,assetCount:Number(e)||0}))),t("ams.alert_calibration_overdue",e=>C(s=>({...s,calibrationOverdue:Number(e)||0}))),t("ams.alert_lane_unavailable",e=>C(s=>({...s,laneUnavailable:Number(e)||0})))];return()=>{e.forEach(a)}},[t,a,p,h]);const g=useCallback(async e=>{try{const t=await s("ams.read_asset",MessageType.Request,{asset_id:e});return t?.success?t.data:null}catch{return null}},[s]),f=useCallback(async e=>{try{const t=await s("ams.list_calibrations",MessageType.Request,{asset_id:e});return t?.success?t.data.cal_ids??[]:[]}catch{return[]}},[s]),E=useCallback(async(e,t)=>{try{const a=await s("ams.read_calibration",MessageType.Request,{asset_id:e,cal_id:t});return a?.success?a.data:null}catch{return null}},[s]),S=useCallback(async e=>{try{const t=await s("ams.read_usage",MessageType.Request,{asset_id:e});return t?.success?t.data:null}catch{return null}},[s]),T=useCallback(async(e,t)=>{try{const a=await s("ams.assign_role",MessageType.Request,{location:e,asset_id:t});return a?.success?(await p(),null):a?.error_message||"ams.assign_role failed"}catch(e){return String(e?.message??e)}},[s,p]),M=useMemo(()=>({schemas:n,schemasLoaded:c,roles:l,rolesLoaded:i,alerts:d,assets:y,refreshAssets:p,readAsset:g,listCalibrations:f,readCalibration:E,readUsage:S,assignRole:T,selection:x,setSelection:A}),[n,c,l,i,d,y,p,g,f,E,S,T,x,A]);return _jsx(AmsContext.Provider,{value:M,children:e})};export const useAms=()=>useContext(AmsContext);export const useAmsSchemas=()=>useContext(AmsContext).schemas;export const useAmsRoles=()=>useContext(AmsContext).roles;export const useAmsAlerts=()=>useContext(AmsContext).alerts;export const useAmsAssets=()=>useContext(AmsContext).assets;export const useAmsSelection=()=>{const e=useContext(AmsContext);return[e.selection,e.setSelection]};
@@ -1 +1 @@
1
- {"version":3,"file":"AssetDetailView.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AssetDetailView.tsx"],"names":[],"mappings":"AAQA,OAAO,KAA0C,MAAM,OAAO,CAAC;AAW/D,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EA4SnC,CAAC"}
1
+ {"version":3,"file":"AssetDetailView.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AssetDetailView.tsx"],"names":[],"mappings":"AAQA,OAAO,KAA0C,MAAM,OAAO,CAAC;AAY/D,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAyRnC,CAAC"}
@@ -1 +1 @@
1
- import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React,{useContext,useEffect,useState}from"react";import{Button}from"primereact/button";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{ConfirmDialog,confirmDialog}from"primereact/confirmdialog";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useAms}from"./AmsProvider";import{CalibrationEntryDialog}from"./CalibrationEntryDialog";import{AssetEditDialog}from"./AssetEditDialog";export const AssetDetailView=()=>{const{selection:e,schemas:t,roles:s,readAsset:i,listCalibrations:a,readCalibration:r,readUsage:l,refreshAssets:n,setSelection:o}=useAms(),{invoke:d}=useContext(EventEmitterContext),[c,m]=useState(null),[p,u]=useState([]),[_,h]=useState(null),[x,f]=useState(!1),[y,j]=useState(null),[b,g]=useState(!1),[v,C]=useState(!1),S=async()=>{if(!e.assetId)return m(null),u([]),void h(null);const t=await i(e.assetId);m(t);const s=await a(e.assetId),n=await Promise.all(s.map(t=>r(e.assetId,t)));u(n.filter(Boolean));const o=await l(e.assetId);h(o)};if(useEffect(()=>{S()},[e.assetId]),!e.assetId)return _jsx("div",{style:{padding:"1rem",color:"#9ca3af"},children:"Select an asset from the registry to see its details."});if(!c)return _jsx("div",{style:{padding:"1rem"},children:"Loading…"});const w=t[c.asset_type]?.label??c.asset_type,A=s[c.asset_type]?.find(e=>e.location===c.location),D=A?A.label??A.location:c.location,E=Array.isArray(t[c.asset_type]?.fields)?t[c.asset_type].fields:[],k=c.custom??{},I=A?.used_by_modules??[];return _jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[_jsxs("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr auto 1fr",gap:"0.5rem 1.5rem",alignItems:"baseline"},children:[_jsx("strong",{children:"Asset ID"})," ",_jsx("span",{children:c.asset_id}),_jsx("strong",{children:"Type"})," ",_jsx("span",{children:w}),_jsx("strong",{children:"Serial"})," ",_jsx("span",{children:c.serial||_jsx("em",{style:{color:"#9ca3af"},children:"(none)"})}),_jsx("strong",{children:"Role"})," ",_jsx("span",{children:D||_jsx("em",{style:{color:"#9ca3af"},children:"(none)"})}),_jsx("strong",{children:"Status"})," ",_jsx("span",{children:c.status}),_jsx("strong",{children:"Installed"}),_jsx("span",{children:c.install_date?new Date(c.install_date).toLocaleString():"—"}),_jsx("strong",{children:"Current Cal"}),_jsx("span",{children:c.current_calibration_id??_jsx("em",{style:{color:"#f59e0b"},children:"none"})}),_jsx("strong",{children:"Cycles"})," ",_jsx("span",{children:_?.cycles??0})]}),I.length>0&&_jsxs("div",{style:{fontSize:"0.875rem",color:"#34d399"},children:["✓ Feeds module ",1===I.length?"config":"configs",":"," ",_jsx("strong",{children:I.join(", ")})," — ","their ",_jsxs("code",{children:["$","${ams.by_location."+c.location+".*}"]})," ","placeholders will resolve to the values below."]}),E.length>0&&_jsxs("div",{children:[_jsx("h4",{style:{margin:"0 0 0.5rem 0"},children:"Nameplate"}),_jsx("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"0.25rem 1rem",alignItems:"baseline",fontSize:"0.9rem"},children:E.map(e=>{const t=k[e.name],s=null!=t&&""!==t,i=e.label??e.name,a=s?e.units?`${t} ${e.units}`:String(t):"(not set)";return _jsxs(React.Fragment,{children:[_jsx("strong",{title:e.description??void 0,children:i}),_jsx("span",{style:{color:s?void 0:"#f59e0b"},children:a})]},e.name)})})]}),_jsx(SubLocationsPanel,{schema:t[c.asset_type]?.sub_locations,values:c.sub_locations}),_jsx(SubLocationsCalibrationPanel,{schema:t[c.asset_type]?.sub_locations,currentCalibration:p.find(e=>e.cal_id===c.current_calibration_id)}),_jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[_jsx("h4",{style:{margin:0},children:"Calibration History"}),_jsxs("div",{style:{display:"flex",gap:"0.5rem"},children:[_jsx(Button,{label:"Edit",icon:"pi pi-pencil",outlined:!0,onClick:()=>C(!0),tooltip:"Edit serial, role, nameplate, and per-axis values. Type and install date are immutable.",tooltipOptions:{position:"left"}}),"active"===c.status&&_jsx(Button,{label:"Retire",icon:"pi pi-power-off",severity:"warning",outlined:!0,onClick:async()=>{if(c)try{const e=await d("ams.update_asset",MessageType.Request,{asset_id:c.asset_id,status:"retired"});e?.success&&(await n(),await S())}catch(e){}},tooltip:"Mark inactive. The asset stays on disk; deletion is a separate action available after retirement.",tooltipOptions:{position:"left"}}),"retired"===c.status&&_jsx(Button,{label:"Delete",icon:"pi pi-trash",severity:"danger",outlined:!0,loading:b,onClick:()=>{c&&confirmDialog({message:_jsxs("div",{children:[_jsxs("p",{style:{marginTop:0},children:["Permanently delete asset ",_jsx("code",{children:c.asset_id}),"?"]}),_jsx("p",{style:{marginBottom:0,fontSize:"0.875rem",color:"#9ca3af"},children:"This removes the AMS record (nameplate, calibration history, usage counters). Historical test records that referenced this asset are unaffected — they carry the full asset and calibration data inline."})]}),header:"Delete asset",icon:"pi pi-exclamation-triangle",acceptLabel:"Delete",rejectLabel:"Cancel",acceptClassName:"p-button-danger",accept:()=>{(async()=>{if(c){g(!0);try{const e=await d("ams.delete_asset",MessageType.Request,{asset_id:c.asset_id});e?.success?(o({assetType:null,assetId:null}),await n()):alert(`Delete failed: ${e?.error_message??"unknown error"}`)}catch(e){alert(`Delete threw: ${e?.message??e}`)}finally{g(!1)}}})()}})},tooltip:"Permanent. Removes the AMS record; historical test snapshots are preserved inline.",tooltipOptions:{position:"left"}}),_jsx(Button,{label:"+ Calibration",icon:"pi pi-plus",onClick:()=>{j(null),f(!0)}})]})]}),_jsx(ConfirmDialog,{}),_jsxs(DataTable,{value:p,size:"small",stripedRows:!0,emptyMessage:"No calibrations recorded for this asset.",children:[_jsx(Column,{field:"cal_id",header:"Cal ID"}),_jsx(Column,{field:"performed_at",header:"Performed",body:e=>e.performed_at?new Date(e.performed_at).toLocaleString():"—"}),_jsx(Column,{field:"performed_by",header:"By"}),_jsx(Column,{field:"expires_at",header:"Expires",body:e=>e.expires_at?new Date(e.expires_at).toLocaleDateString():"—"}),_jsx(Column,{field:"cert_ref",header:"Cert"}),_jsx(Column,{header:"Values",body:e=>_jsx("code",{style:{fontSize:"0.75rem"},children:JSON.stringify(e.values)})}),_jsx(Column,{header:"",style:{width:"5rem"},body:e=>e.cal_id===c.current_calibration_id?_jsx(Button,{icon:"pi pi-pencil",text:!0,rounded:!0,size:"small",tooltip:"Edit this calibration (fixes a typo in place).",tooltipOptions:{position:"left"},onClick:()=>{j(e),f(!0)}}):null})]}),_jsx(CalibrationEntryDialog,{visible:x,assetId:c.asset_id,assetType:c.asset_type,editing:y,onHide:()=>{f(!1),j(null)},onAdded:()=>{S()}}),_jsx(AssetEditDialog,{visible:v,asset:c,onHide:()=>C(!1),onSaved:async()=>{await n(),await S()}})]})};const SubLocationsPanel=({schema:e,values:t})=>{if(!e||!Array.isArray(e.keys)||!Array.isArray(e.fields))return null;const s=t&&"object"==typeof t?t:{};return _jsxs("div",{children:[_jsx("h4",{style:{margin:"0 0 0.5rem 0"},children:e.label??"Sub-locations"}),_jsx("div",{style:{overflowX:"auto"},children:_jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:"0.875rem"},children:[_jsx("thead",{children:_jsxs("tr",{children:[_jsx("th",{style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:e.key_label??"Key"}),e.fields.map(e=>_jsxs("th",{title:e.description??void 0,style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:[e.label??e.name,e.units?` [${e.units}]`:""]},e.name))]})}),_jsx("tbody",{children:e.keys.map(t=>{const i=s[t]??{};return _jsxs("tr",{children:[_jsx("td",{style:{padding:"0.25rem 0.5rem",fontWeight:600},children:t}),e.fields.map(e=>{const t=i[e.name],s=null!=t&&""!==t;return _jsx("td",{style:{padding:"0.25rem 0.5rem",color:s?void 0:"#f59e0b"},children:s?String(t):"(not set)"},e.name)})]},t)})})]})})]})},SubLocationsCalibrationPanel=({schema:e,currentCalibration:t})=>{if(!e||!Array.isArray(e.keys)||!Array.isArray(e.calibration_fields)||0===e.calibration_fields.length)return null;if(!t)return null;const s=t.values&&"object"==typeof t.values?t.values:{};return _jsxs("div",{children:[_jsx("h4",{style:{margin:"0 0 0.5rem 0"},children:"Current Calibration — Per-axis Values"}),_jsx("div",{style:{overflowX:"auto"},children:_jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:"0.875rem"},children:[_jsx("thead",{children:_jsxs("tr",{children:[_jsx("th",{style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:e.key_label??"Key"}),e.calibration_fields.map(e=>_jsxs("th",{style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:[e.label??e.name,e.units?` [${e.units}]`:""]},e.name))]})}),_jsx("tbody",{children:e.keys.map(t=>{const i=s[t]??{};return _jsxs("tr",{children:[_jsx("td",{style:{padding:"0.25rem 0.5rem",fontWeight:600},children:t}),e.calibration_fields.map(e=>{const t=i[e.name],s=null!=t&&""!==t;return _jsx("td",{style:{padding:"0.25rem 0.5rem",color:s?void 0:"#f59e0b"},children:s?String(t):"(not set)"},e.name)})]},t)})})]})})]})};
1
+ import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React,{useContext,useEffect,useState}from"react";import{Button}from"primereact/button";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{ConfirmDialog,confirmDialog}from"primereact/confirmdialog";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useAms}from"./AmsProvider";import{CalibrationEntryDialog}from"./CalibrationEntryDialog";import{AssetEditDialog}from"./AssetEditDialog";import{AssetNameplateGrid}from"./AssetNameplateGrid";export const AssetDetailView=()=>{const{selection:e,schemas:t,roles:s,readAsset:i,listCalibrations:a,readCalibration:r,readUsage:l,refreshAssets:n,setSelection:o}=useAms(),{invoke:d}=useContext(EventEmitterContext),[c,m]=useState(null),[p,_]=useState([]),[u,h]=useState(null),[x,f]=useState(!1),[y,j]=useState(null),[b,g]=useState(!1),[v,C]=useState(!1),S=async()=>{if(!e.assetId)return m(null),_([]),void h(null);const t=await i(e.assetId);m(t);const s=await a(e.assetId),n=await Promise.all(s.map(t=>r(e.assetId,t)));_(n.filter(Boolean));const o=await l(e.assetId);h(o)};if(useEffect(()=>{S()},[e.assetId]),!e.assetId)return _jsx("div",{style:{padding:"1rem",color:"#9ca3af"},children:"Select an asset from the registry to see its details."});if(!c)return _jsx("div",{style:{padding:"1rem"},children:"Loading…"});const A=t[c.asset_type]?.label??c.asset_type,w=s[c.asset_type]?.find(e=>e.location===c.location),D=w?w.label??w.location:c.location,E=Array.isArray(t[c.asset_type]?.fields)?t[c.asset_type].fields:[],k=c.custom??{},I=w?.used_by_modules??[];return _jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[_jsxs("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr auto 1fr",gap:"0.5rem 1.5rem",alignItems:"baseline"},children:[_jsx("strong",{children:"Asset ID"})," ",_jsx("span",{children:c.asset_id}),_jsx("strong",{children:"Type"})," ",_jsx("span",{children:A}),_jsx("strong",{children:"Serial"})," ",_jsx("span",{children:c.serial||_jsx("em",{style:{color:"#9ca3af"},children:"(none)"})}),_jsx("strong",{children:"Role"})," ",_jsx("span",{children:D||_jsx("em",{style:{color:"#9ca3af"},children:"(none)"})}),_jsx("strong",{children:"Status"})," ",_jsx("span",{children:c.status}),_jsx("strong",{children:"Installed"}),_jsx("span",{children:c.install_date?new Date(c.install_date).toLocaleString():"—"}),_jsx("strong",{children:"Current Cal"}),_jsx("span",{children:c.current_calibration_id??_jsx("em",{style:{color:"#f59e0b"},children:"none"})}),_jsx("strong",{children:"Cycles"})," ",_jsx("span",{children:u?.cycles??0})]}),I.length>0&&_jsxs("div",{style:{fontSize:"0.875rem",color:"#34d399"},children:["✓ Feeds module ",1===I.length?"config":"configs",":"," ",_jsx("strong",{children:I.join(", ")})," — ","their ",_jsxs("code",{children:["$","${ams.by_location."+c.location+".*}"]})," ","placeholders will resolve to the values below."]}),E.length>0&&_jsxs("div",{children:[_jsx("h4",{style:{margin:"0 0 0.5rem 0"},children:"Nameplate"}),_jsx(AssetNameplateGrid,{schemaFields:E,custom:k})]}),_jsx(SubLocationsPanel,{schema:t[c.asset_type]?.sub_locations,values:c.sub_locations}),_jsx(SubLocationsCalibrationPanel,{schema:t[c.asset_type]?.sub_locations,currentCalibration:p.find(e=>e.cal_id===c.current_calibration_id)}),_jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[_jsx("h4",{style:{margin:0},children:"Calibration History"}),_jsxs("div",{style:{display:"flex",gap:"0.5rem"},children:[_jsx(Button,{label:"Edit",icon:"pi pi-pencil",outlined:!0,onClick:()=>C(!0),tooltip:"Edit serial, role, nameplate, and per-axis values. Type and install date are immutable.",tooltipOptions:{position:"left"}}),"active"===c.status&&_jsx(Button,{label:"Retire",icon:"pi pi-power-off",severity:"warning",outlined:!0,onClick:async()=>{if(c)try{const e=await d("ams.update_asset",MessageType.Request,{asset_id:c.asset_id,status:"retired"});e?.success&&(await n(),await S())}catch(e){}},tooltip:"Mark inactive. The asset stays on disk; deletion is a separate action available after retirement.",tooltipOptions:{position:"left"}}),"retired"===c.status&&_jsx(Button,{label:"Delete",icon:"pi pi-trash",severity:"danger",outlined:!0,loading:b,onClick:()=>{c&&confirmDialog({message:_jsxs("div",{children:[_jsxs("p",{style:{marginTop:0},children:["Permanently delete asset ",_jsx("code",{children:c.asset_id}),"?"]}),_jsx("p",{style:{marginBottom:0,fontSize:"0.875rem",color:"#9ca3af"},children:"This removes the AMS record (nameplate, calibration history, usage counters). Historical test records that referenced this asset are unaffected — they carry the full asset and calibration data inline."})]}),header:"Delete asset",icon:"pi pi-exclamation-triangle",acceptLabel:"Delete",rejectLabel:"Cancel",acceptClassName:"p-button-danger",accept:()=>{(async()=>{if(c){g(!0);try{const e=await d("ams.delete_asset",MessageType.Request,{asset_id:c.asset_id});e?.success?(o({assetType:null,assetId:null}),await n()):alert(`Delete failed: ${e?.error_message??"unknown error"}`)}catch(e){alert(`Delete threw: ${e?.message??e}`)}finally{g(!1)}}})()}})},tooltip:"Permanent. Removes the AMS record; historical test snapshots are preserved inline.",tooltipOptions:{position:"left"}}),_jsx(Button,{label:"+ Calibration",icon:"pi pi-plus",onClick:()=>{j(null),f(!0)}})]})]}),_jsx(ConfirmDialog,{}),_jsxs(DataTable,{value:p,size:"small",stripedRows:!0,emptyMessage:"No calibrations recorded for this asset.",children:[_jsx(Column,{field:"cal_id",header:"Cal ID"}),_jsx(Column,{field:"performed_at",header:"Performed",body:e=>e.performed_at?new Date(e.performed_at).toLocaleString():"—"}),_jsx(Column,{field:"performed_by",header:"By"}),_jsx(Column,{field:"expires_at",header:"Expires",body:e=>e.expires_at?new Date(e.expires_at).toLocaleDateString():"—"}),_jsx(Column,{field:"cert_ref",header:"Cert"}),_jsx(Column,{header:"Values",body:e=>_jsx("code",{style:{fontSize:"0.75rem"},children:JSON.stringify(e.values)})}),_jsx(Column,{header:"",style:{width:"5rem"},body:e=>e.cal_id===c.current_calibration_id?_jsx(Button,{icon:"pi pi-pencil",text:!0,rounded:!0,size:"small",tooltip:"Edit this calibration (fixes a typo in place).",tooltipOptions:{position:"left"},onClick:()=>{j(e),f(!0)}}):null})]}),_jsx(CalibrationEntryDialog,{visible:x,assetId:c.asset_id,assetType:c.asset_type,editing:y,onHide:()=>{f(!1),j(null)},onAdded:()=>{S()}}),_jsx(AssetEditDialog,{visible:v,asset:c,onHide:()=>C(!1),onSaved:async()=>{await n(),await S()}})]})};const SubLocationsPanel=({schema:e,values:t})=>{if(!e||!Array.isArray(e.keys)||!Array.isArray(e.fields))return null;const s=t&&"object"==typeof t?t:{};return _jsxs("div",{children:[_jsx("h4",{style:{margin:"0 0 0.5rem 0"},children:e.label??"Sub-locations"}),_jsx("div",{style:{overflowX:"auto"},children:_jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:"0.875rem"},children:[_jsx("thead",{children:_jsxs("tr",{children:[_jsx("th",{style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:e.key_label??"Key"}),e.fields.map(e=>_jsxs("th",{title:e.description??void 0,style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:[e.label??e.name,e.units?` [${e.units}]`:""]},e.name))]})}),_jsx("tbody",{children:e.keys.map(t=>{const i=s[t]??{};return _jsxs("tr",{children:[_jsx("td",{style:{padding:"0.25rem 0.5rem",fontWeight:600},children:t}),e.fields.map(e=>{const t=i[e.name],s=null!=t&&""!==t;return _jsx("td",{style:{padding:"0.25rem 0.5rem",color:s?void 0:"#f59e0b"},children:s?String(t):"(not set)"},e.name)})]},t)})})]})})]})},SubLocationsCalibrationPanel=({schema:e,currentCalibration:t})=>{if(!e||!Array.isArray(e.keys)||!Array.isArray(e.calibration_fields)||0===e.calibration_fields.length)return null;if(!t)return null;const s=t.values&&"object"==typeof t.values?t.values:{};return _jsxs("div",{children:[_jsx("h4",{style:{margin:"0 0 0.5rem 0"},children:"Current Calibration — Per-axis Values"}),_jsx("div",{style:{overflowX:"auto"},children:_jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:"0.875rem"},children:[_jsx("thead",{children:_jsxs("tr",{children:[_jsx("th",{style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:e.key_label??"Key"}),e.calibration_fields.map(e=>_jsxs("th",{style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:[e.label??e.name,e.units?` [${e.units}]`:""]},e.name))]})}),_jsx("tbody",{children:e.keys.map(t=>{const i=s[t]??{};return _jsxs("tr",{children:[_jsx("td",{style:{padding:"0.25rem 0.5rem",fontWeight:600},children:t}),e.calibration_fields.map(e=>{const t=i[e.name],s=null!=t&&""!==t;return _jsx("td",{style:{padding:"0.25rem 0.5rem",color:s?void 0:"#f59e0b"},children:s?String(t):"(not set)"},e.name)})]},t)})})]})})]})};
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ export interface AssetNameplateGridProps {
3
+ /** The asset_type schema's `fields` array. */
4
+ schemaFields: any[];
5
+ /** The asset record's `custom` object (nameplate values). */
6
+ custom: Record<string, any>;
7
+ /** Only render these field names, in this order. Omit for all of them. */
8
+ only?: string[];
9
+ /** Font size for the grid; the detail view runs slightly larger. */
10
+ fontSize?: string;
11
+ }
12
+ export declare const AssetNameplateGrid: React.FC<AssetNameplateGridProps>;
13
+ //# sourceMappingURL=AssetNameplateGrid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AssetNameplateGrid.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AssetNameplateGrid.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,WAAW,uBAAuB;IACpC,8CAA8C;IAC9C,YAAY,EAAE,GAAG,EAAE,CAAC;IACpB,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAmChE,CAAC"}
@@ -0,0 +1 @@
1
+ import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React from"react";export const AssetNameplateGrid=({schemaFields:e,custom:t,only:s,fontSize:n="0.9rem"})=>{const r=s?s.map(t=>e.find(e=>e.name===t)).filter(Boolean):e;return 0===r.length?null:_jsx("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"0.25rem 1rem",alignItems:"baseline",fontSize:n},children:r.map(e=>{const s=t[e.name],n=null!=s&&""!==s,r=e.label??e.name,i=n?e.units?`${s} ${e.units}`:String(s):"(not set)";return _jsxs(React.Fragment,{children:[_jsx("strong",{title:e.description??void 0,children:r}),_jsx("span",{style:{color:n?void 0:"#f59e0b"},children:i})]},e.name)})})};
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ export interface InstalledAssetPickerProps {
3
+ /** Asset type the role takes, e.g. `"load_cell"`. */
4
+ assetType: string;
5
+ /** The role's `location` string, e.g. `"press_load_cell"`. */
6
+ location: string;
7
+ /**
8
+ * Heading for the picker. Defaults to the role's label from
9
+ * `ams.list_roles`, then to the raw location.
10
+ */
11
+ label?: string;
12
+ /** One line under the dropdown explaining what this role drives. */
13
+ description?: string;
14
+ /**
15
+ * Nameplate field names folded into each option's text, so the operator can
16
+ * tell two assets apart without opening them (e.g.
17
+ * `["capacity", "capacity_units"]` → "2000 N"). Serial is always shown.
18
+ */
19
+ summaryFields?: string[];
20
+ /** Render the fitted asset's full nameplate below the dropdown. */
21
+ showNameplate?: boolean;
22
+ /** Render the fitted asset's calibration state below the nameplate. */
23
+ showCalibration?: boolean;
24
+ /**
25
+ * Block the swap. Use for "a test is running" — changing the fitted asset
26
+ * mid-run would re-scale a channel under the data being recorded.
27
+ */
28
+ disabled?: boolean;
29
+ /** Why it is blocked. Shown in place of the help text. */
30
+ disabledReason?: string;
31
+ /** Fired after a successful assignment with the new asset id (or null). */
32
+ onAssigned?: (assetId: string | null) => void;
33
+ }
34
+ export declare const InstalledAssetPicker: React.FC<InstalledAssetPickerProps>;
35
+ //# sourceMappingURL=InstalledAssetPicker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InstalledAssetPicker.d.ts","sourceRoot":"","sources":["../../../src/components/ams/InstalledAssetPicker.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAyC,MAAM,OAAO,CAAC;AAQ9D,MAAM,WAAW,yBAAyB;IACtC,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,mEAAmE;IACnE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,uEAAuE;IACvE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2EAA2E;IAC3E,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CACjD;AAKD,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CA8IpE,CAAC"}
@@ -0,0 +1 @@
1
+ import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import React,{useCallback,useMemo,useState}from"react";import{Dropdown}from"primereact/dropdown";import{Button}from"primereact/button";import{Dialog}from"primereact/dialog";import{Message}from"primereact/message";import{AssetNameplateGrid}from"./AssetNameplateGrid";import{useInstalledAsset}from"./useInstalledAsset";const NONE="__none__";export const InstalledAssetPicker=({assetType:e,location:s,label:t,description:a,summaryFields:l=[],showNameplate:i=!0,showCalibration:r=!0,disabled:n=!1,disabledReason:o,onAssigned:d})=>{const{candidates:c,records:m,installed:u,installedRecord:h,calibration:_,calibrationOverdue:p,role:x,schemaFields:g,assign:f}=useInstalledAsset(e,s),[j,b]=useState(null),[y,N]=useState(!1),[v,w]=useState(null),C=t??x?.label??s,A=a??x?.description??null,D=useCallback(e=>{const s=c.find(s=>s.asset_id===e),t=m[e]?.custom??{},a=[s?.serial?`SN ${s.serial}`:e],i=l.map(e=>t[e]).filter(e=>null!=e&&""!==e).join(" ");return i&&a.push(i),a.join(" — ")},[c,m,l]),I=useMemo(()=>[...c.map(e=>({label:D(e.asset_id),value:e.asset_id})),{label:"— nothing fitted —",value:NONE}],[c,D]),S=_?.expires_at?new Date(_.expires_at):null;return _jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"0.5rem"},children:[_jsxs("div",{className:"p-inputgroup",children:[_jsx("span",{className:"p-inputgroup-addon",style:{flexGrow:1},children:C}),_jsx(Dropdown,{value:u?.asset_id??NONE,options:I,disabled:n||y,onChange:e=>{const s=e.value===NONE?null:e.value;s!==(u?.asset_id??null)&&w({assetId:s})},placeholder:"Select the fitted asset",style:{minWidth:"18rem"}})]}),n&&o?_jsx("small",{style:{color:"#f59e0b"},children:o}):A&&_jsx("small",{style:{color:"#9ca3af"},children:A}),0===c.length&&_jsx(Message,{severity:"warn",text:`No active ${e.replace(/_/g," ")} is registered. Add one on the Assets screen first.`}),!u&&c.length>0&&_jsx(Message,{severity:"warn",text:"Nothing is recorded as fitted in this role. Tests that need it will refuse to start."}),j&&_jsx(Message,{severity:"error",text:j}),u&&i&&_jsx(AssetNameplateGrid,{schemaFields:g,custom:h?.custom??{}}),u&&r&&_jsx("small",{style:{color:p?"#f59e0b":"#9ca3af"},children:u.current_calibration_id?p?`Calibration ${u.current_calibration_id} expired ${S.toLocaleDateString()}.`:`Calibration ${u.current_calibration_id}${S?`, in date until ${S.toLocaleDateString()}`:""}.`:"No calibration on record for this asset."}),_jsxs(Dialog,{visible:null!==v,header:`Change the ${C}?`,modal:!0,onHide:()=>w(null),footer:_jsxs(_Fragment,{children:[_jsx(Button,{label:"Cancel",outlined:!0,onClick:()=>w(null)}),_jsx(Button,{label:"Confirm",loading:y,onClick:()=>{(async e=>{N(!0),b(null);const s=await f(e);N(!1),w(null),s?b(s):d?.(e)})(v?.assetId??null)}})]}),children:[_jsx("p",{style:{margin:0},children:_jsxs(_Fragment,v?.assetId?{children:["Record ",_jsx("strong",{children:D(v.assetId)})," as the asset fitted in ",_jsx("strong",{children:C}),"."]}:{children:["Record that ",_jsx("strong",{children:"nothing"})," is fitted in ",_jsx("strong",{children:C}),"."]})}),_jsx("p",{style:{marginBottom:0,color:"#9ca3af"},children:"Confirm only if this matches the hardware actually installed. The machine reconfigures the channel from this asset's nameplate, and every test started from now on records it."})]})]})};
@@ -7,4 +7,9 @@ export { AssetEditDialog } from './AssetEditDialog';
7
7
  export { SubLocationPicker } from './SubLocationPicker';
8
8
  export { PlaceholderHealthPanel } from './PlaceholderHealthPanel';
9
9
  export { MissingAssetsBanner } from './MissingAssetsBanner';
10
+ export { InstalledAssetPicker } from './InstalledAssetPicker';
11
+ export type { InstalledAssetPickerProps } from './InstalledAssetPicker';
12
+ export { AssetNameplateGrid } from './AssetNameplateGrid';
13
+ export { useInstalledAsset } from './useInstalledAsset';
14
+ export type { InstalledAssetState } from './useInstalledAsset';
10
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/ams/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC7H,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/ams/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC7H,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAG5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -1 +1 @@
1
- export{AmsProvider,useAms,useAmsSchemas,useAmsRoles,useAmsAlerts,useAmsAssets,useAmsSelection}from"./AmsProvider";export{AssetRegistryTable}from"./AssetRegistryTable";export{AssetDetailView}from"./AssetDetailView";export{CalibrationEntryDialog}from"./CalibrationEntryDialog";export{AssetEditDialog}from"./AssetEditDialog";export{SubLocationPicker}from"./SubLocationPicker";export{PlaceholderHealthPanel}from"./PlaceholderHealthPanel";export{MissingAssetsBanner}from"./MissingAssetsBanner";
1
+ export{AmsProvider,useAms,useAmsSchemas,useAmsRoles,useAmsAlerts,useAmsAssets,useAmsSelection}from"./AmsProvider";export{AssetRegistryTable}from"./AssetRegistryTable";export{AssetDetailView}from"./AssetDetailView";export{CalibrationEntryDialog}from"./CalibrationEntryDialog";export{AssetEditDialog}from"./AssetEditDialog";export{SubLocationPicker}from"./SubLocationPicker";export{PlaceholderHealthPanel}from"./PlaceholderHealthPanel";export{MissingAssetsBanner}from"./MissingAssetsBanner";export{InstalledAssetPicker}from"./InstalledAssetPicker";export{AssetNameplateGrid}from"./AssetNameplateGrid";export{useInstalledAsset}from"./useInstalledAsset";
@@ -0,0 +1,26 @@
1
+ import { type AmsAssetEntry, type AmsRole } from './AmsProvider';
2
+ export interface InstalledAssetState {
3
+ /** Every ACTIVE asset of this type — the assets that could be fitted. */
4
+ candidates: AmsAssetEntry[];
5
+ /** Full records for `candidates`, keyed by asset_id. */
6
+ records: Record<string, any>;
7
+ /** The registry entry fitted in this role, or null when nothing is. */
8
+ installed: AmsAssetEntry | null;
9
+ /** The fitted asset's full record; nameplate values live under `.custom`. */
10
+ installedRecord: any | null;
11
+ /** The fitted asset's current calibration record, or null. */
12
+ calibration: any | null;
13
+ /** True once the calibration on record has passed its `expires_at`. */
14
+ calibrationOverdue: boolean;
15
+ /** This role's metadata from `ams.list_roles`, when the project declares it. */
16
+ role: AmsRole | null;
17
+ /** The asset_type schema's nameplate `fields` array (empty if unknown). */
18
+ schemaFields: any[];
19
+ /**
20
+ * Declare what is fitted (`null` = nothing). Resolves to `null` on success
21
+ * or the server's refusal message.
22
+ */
23
+ assign: (assetId: string | null) => Promise<string | null>;
24
+ }
25
+ export declare function useInstalledAsset(assetType: string, location: string): InstalledAssetState;
26
+ //# sourceMappingURL=useInstalledAsset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useInstalledAsset.d.ts","sourceRoot":"","sources":["../../../src/components/ams/useInstalledAsset.ts"],"names":[],"mappings":"AAeA,OAAO,EAAU,KAAK,aAAa,EAAE,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AAEzE,MAAM,WAAW,mBAAmB;IAChC,yEAAyE;IACzE,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,uEAAuE;IACvE,SAAS,EAAE,aAAa,GAAG,IAAI,CAAC;IAChC,6EAA6E;IAC7E,eAAe,EAAE,GAAG,GAAG,IAAI,CAAC;IAC5B,8DAA8D;IAC9D,WAAW,EAAE,GAAG,GAAG,IAAI,CAAC;IACxB,uEAAuE;IACvE,kBAAkB,EAAE,OAAO,CAAC;IAC5B,gFAAgF;IAChF,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IACrB,2EAA2E;IAC3E,YAAY,EAAE,GAAG,EAAE,CAAC;IACpB;;;OAGG;IACH,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC9D;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAkE1F"}
@@ -0,0 +1 @@
1
+ import{useCallback,useEffect,useMemo,useState}from"react";import{useAms}from"./AmsProvider";export function useInstalledAsset(e,s){const{assets:t,schemas:a,roles:i,readAsset:r,readCalibration:n,assignRole:l}=useAms(),[o,u]=useState({}),[c,d]=useState(null),f=useMemo(()=>t.filter(s=>s.asset_type===e&&"active"===s.status),[t,e]),m=useMemo(()=>f.find(e=>e.location===s)??null,[f,s]),_=useMemo(()=>(i[e]??[]).find(e=>e.location===s)??null,[i,e,s]),b=f.map(e=>e.asset_id).join(",");useEffect(()=>{let e=!1;return(async()=>{const s={};for(const t of f){const a=await r(t.asset_id);if(e)return;a&&(s[t.asset_id]=a)}e||u(s)})(),()=>{e=!0}},[b,r]),useEffect(()=>{let e=!1;return(async()=>{if(!m?.current_calibration_id)return void d(null);const s=await n(m.asset_id,m.current_calibration_id);e||d(s)})(),()=>{e=!0}},[m?.asset_id,m?.current_calibration_id,n]);const p=useCallback(e=>l(s,e),[l,s]),A=c?.expires_at?new Date(c.expires_at):null;return{candidates:f,records:o,installed:m,installedRecord:m?o[m.asset_id]??null:null,calibration:c,calibrationOverdue:!!A&&A.getTime()<Date.now(),role:_,schemaFields:Array.isArray(a[e]?.fields)?a[e].fields:[],assign:p}}
@@ -128,6 +128,25 @@ export interface TagLookup {
128
128
  } | undefined;
129
129
  rawValues: Record<string, unknown>;
130
130
  }
131
+ /** Is this string a well-formed FQDN bound token? */
132
+ export declare const isBoundToken: (s: string) => boolean;
133
+ /**
134
+ * Render a bound for an authoring text field — the method editor's Min/Max.
135
+ *
136
+ * A text field, not a numeric one, because a bound is a number OR a token. The
137
+ * editor used `InputNumber`, which rendered a token as **NaN** and — worse —
138
+ * coerced anything non-numeric to `undefined` on change, so opening the dialog
139
+ * and saving silently deleted a token bound.
140
+ */
141
+ export declare const boundText: (b: number | string | undefined) => string;
142
+ /**
143
+ * Parse what an author typed into a bound field. Blank clears it; a finite
144
+ * number stores a number; anything else is kept VERBATIM so a half-typed token
145
+ * survives the keystroke. Use `boundInvalid` to flag what will not resolve.
146
+ */
147
+ export declare const parseBound: (text: string) => number | string | undefined;
148
+ /** True when a bound is a string that is not a well-formed token. */
149
+ export declare const boundInvalid: (b: number | string | undefined) => boolean;
131
150
  /**
132
151
  * A declared bound resolved to a plain DISPLAY-unit number, or `undefined` for
133
152
  * "no bound".
@@ -1 +1 @@
1
- {"version":3,"file":"TestFieldRow.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestFieldRow.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TestFieldRow.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestFieldRow.tsx"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React,{useContext}from"react";import{AutoCoreTagContext}from"../../core/AutoCoreTagContext";import{Button}from"primereact/button";import{Dropdown}from"primereact/dropdown";import{ValueInput}from"../ValueInput";import{TextInput}from"../TextInput";export const rawToDisplay=(e,t,n=0)=>(t&&1!==t||n)&&"number"==typeof e&&Number.isFinite(e)?e*(t??1)+n:e;export const displayToRaw=(e,t,n=0)=>(t&&1!==t||n)&&"number"==typeof e&&Number.isFinite(e)?(e-n)/(t??1):e;export const effectiveField=(e,t,n)=>{const l=e.quantity?t?.[e.quantity]:void 0,o=l?l.scale:e.scale,a=l?l.offset??0:0;return{...l?{...e,scale:o,units:l.label,unitOffset:a}:{...e},min:resolveBound(e.min,o,a,n),max:resolveBound(e.max,o,a,n)}};const BOUND_TOKEN_RE=/^\$\{\s*([^}]+?)\s*\}$/;export const resolveBound=(e,t,n,l)=>{if("number"==typeof e)return e;if("string"!=typeof e||!l)return;const o=e.match(BOUND_TOKEN_RE);if(!o)return;const a=l.findTagByFqdn(o[1]),i=a?l.rawValues[a.tagName]:void 0;if("number"!=typeof i||!Number.isFinite(i))return;const r=Number(rawToDisplay(i,t,n));return Number.isFinite(r)?r:void 0};export const labelOf=e=>{const t=e.label&&e.label.length>0?e.label:e.name;return e.units?`${t} [${e.units}]`:t};export const hasDescription=e=>"string"==typeof e.description&&e.description.length>0;export const fieldValueIsEmpty=e=>null==e||"string"==typeof e&&""===e.trim();export const rangeIssue=(e,t)=>{if(fieldValueIsEmpty(t)||""===t)return null;const n="number"==typeof e.min?e.min:void 0,l="number"==typeof e.max?e.max:void 0;if(null==n&&null==l)return null;const o=Number(rawToDisplay(Number(t),e.scale,e.unitOffset??0));if(!Number.isFinite(o))return null;const a=null!=n&&null!=l?Math.min(n,l):n,i=null!=n&&null!=l?Math.max(n,l):l;return null!=a&&o<a?`must be ≥ ${a}`:null!=i&&o>i?`must be ≤ ${i}`:null};export const fieldIssue=(e,t)=>e.required&&fieldValueIsEmpty(t)?"is required":rangeIssue(e,t);export const fieldIsValid=(e,t)=>null===fieldIssue(e,t);export const normalizeOptions=e=>(e??[]).map(e=>null!==e&&"object"==typeof e?{label:String(e.label??e.value),value:e.value}:{label:String(e),value:e});export const TestFieldRow=({field:e,value:t,onChange:n,valid:l,disabled:o,control:a,onInfo:i})=>{const{scales:r,rawValues:s,findTagByFqdn:u}=useContext(AutoCoreTagContext),p=effectiveField(e,r,{findTagByFqdn:u,rawValues:s}),m=p.unitOffset??0,c=l??fieldIsValid(p,t),d=!a&&Array.isArray(p.options)&&p.options.length>0?normalizeOptions(p.options):null,f=!a&&!d&&"string"!==p.type&&"bool"!==p.type,x=a??(d?_jsx(Dropdown,{value:t??null,options:d,onChange:e=>n(e.value),placeholder:`Select ${p.label??p.name}…`,className:"ac-dropdown-clearable"+(c?"":" p-invalid"),showClear:!p.required,disabled:o}):f?_jsx(ValueInput,{label:void 0,value:null!=t?Number(rawToDisplay(Number(t),p.scale,m)):null,min:p.min,max:p.max,onValueChanged:e=>n(displayToRaw(e,p.scale,m)),className:c?"":"p-invalid",disabled:o}):_jsx(TextInput,{label:void 0,value:null!=t?String(t):"",onValueChanged:e=>n(e),className:c?"":"p-invalid",disabled:o}));return _jsxs(React.Fragment,{children:[_jsx("span",{className:"ac-form-label",children:labelOf(p)}),x,i&&hasDescription(p)?_jsx(Button,{icon:"pi pi-info-circle",text:!0,rounded:!0,"aria-label":`About ${labelOf(p)}`,onClick:()=>i(p)}):_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:c?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:c?"pi pi-check":"pi pi-times"})})]})};
1
+ import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React,{useContext}from"react";import{AutoCoreTagContext}from"../../core/AutoCoreTagContext";import{Button}from"primereact/button";import{Dropdown}from"primereact/dropdown";import{ValueInput}from"../ValueInput";import{TextInput}from"../TextInput";export const rawToDisplay=(e,t,n=0)=>(t&&1!==t||n)&&"number"==typeof e&&Number.isFinite(e)?e*(t??1)+n:e;export const displayToRaw=(e,t,n=0)=>(t&&1!==t||n)&&"number"==typeof e&&Number.isFinite(e)?(e-n)/(t??1):e;export const effectiveField=(e,t,n)=>{const l=e.quantity?t?.[e.quantity]:void 0,o=l?l.scale:e.scale,r=l?l.offset??0:0;return{...l?{...e,scale:o,units:l.label,unitOffset:r}:{...e},min:resolveBound(e.min,o,r,n),max:resolveBound(e.max,o,r,n)}};const BOUND_TOKEN_RE=/^\$\{\s*([^}]+?)\s*\}$/;export const isBoundToken=e=>BOUND_TOKEN_RE.test(e.trim());export const boundText=e=>null==e?"":String(e);export const parseBound=e=>{const t=e.trim();if(""===t)return;const n=Number(t);return Number.isFinite(n)?n:t};export const boundInvalid=e=>"string"==typeof e&&!isBoundToken(e);export const resolveBound=(e,t,n,l)=>{if("number"==typeof e)return e;if("string"!=typeof e||!l)return;const o=e.match(BOUND_TOKEN_RE);if(!o)return;const r=l.findTagByFqdn(o[1]),i=r?l.rawValues[r.tagName]:void 0;if("number"!=typeof i||!Number.isFinite(i))return;const s=Number(rawToDisplay(i,t,n));return Number.isFinite(s)?s:void 0};export const labelOf=e=>{const t=e.label&&e.label.length>0?e.label:e.name;return e.units?`${t} [${e.units}]`:t};export const hasDescription=e=>"string"==typeof e.description&&e.description.length>0;export const fieldValueIsEmpty=e=>null==e||"string"==typeof e&&""===e.trim();export const rangeIssue=(e,t)=>{if(fieldValueIsEmpty(t)||""===t)return null;const n="number"==typeof e.min?e.min:void 0,l="number"==typeof e.max?e.max:void 0;if(null==n&&null==l)return null;const o=Number(rawToDisplay(Number(t),e.scale,e.unitOffset??0));if(!Number.isFinite(o))return null;const r=null!=n&&null!=l?Math.min(n,l):n,i=null!=n&&null!=l?Math.max(n,l):l;return null!=r&&o<r?`must be ≥ ${r}`:null!=i&&o>i?`must be ≤ ${i}`:null};export const fieldIssue=(e,t)=>e.required&&fieldValueIsEmpty(t)?"is required":rangeIssue(e,t);export const fieldIsValid=(e,t)=>null===fieldIssue(e,t);export const normalizeOptions=e=>(e??[]).map(e=>null!==e&&"object"==typeof e?{label:String(e.label??e.value),value:e.value}:{label:String(e),value:e});export const TestFieldRow=({field:e,value:t,onChange:n,valid:l,disabled:o,control:r,onInfo:i})=>{const{scales:s,rawValues:a,findTagByFqdn:u}=useContext(AutoCoreTagContext),p=effectiveField(e,s,{findTagByFqdn:u,rawValues:a}),m=p.unitOffset??0,c=l??fieldIsValid(p,t),d=!r&&Array.isArray(p.options)&&p.options.length>0?normalizeOptions(p.options):null,f=!r&&!d&&"string"!==p.type&&"bool"!==p.type,x=r??(d?_jsx(Dropdown,{value:t??null,options:d,onChange:e=>n(e.value),placeholder:`Select ${p.label??p.name}…`,className:"ac-dropdown-clearable"+(c?"":" p-invalid"),showClear:!p.required,disabled:o}):f?_jsx(ValueInput,{label:void 0,value:null!=t?Number(rawToDisplay(Number(t),p.scale,m)):null,min:p.min,max:p.max,onValueChanged:e=>n(displayToRaw(e,p.scale,m)),className:c?"":"p-invalid",disabled:o}):_jsx(TextInput,{label:void 0,value:null!=t?String(t):"",onValueChanged:e=>n(e),className:c?"":"p-invalid",disabled:o}));return _jsxs(React.Fragment,{children:[_jsx("span",{className:"ac-form-label",children:labelOf(p)}),x,i&&hasDescription(p)?_jsx(Button,{icon:"pi pi-info-circle",text:!0,rounded:!0,"aria-label":`About ${labelOf(p)}`,onClick:()=>i(p)}):_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:c?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:c?"pi pi-check":"pi pi-times"})})]})};
@@ -1 +1 @@
1
- {"version":3,"file":"TestFieldDialog.d.ts","sourceRoot":"","sources":["../../../../src/components/tis-editor/editor/TestFieldDialog.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAA2C,MAAM,UAAU,CAAC;AA6CnF,MAAM,WAAW,oBAAoB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,SAAS,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,MAAM,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACnC,8EAA8E;IAC9E,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB;8CAC0C;IAC1C,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;0EACsE;IACtE,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,uEAAuE;IACvE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AAID,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAqR1D,CAAC"}
1
+ {"version":3,"file":"TestFieldDialog.d.ts","sourceRoot":"","sources":["../../../../src/components/tis-editor/editor/TestFieldDialog.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAA2C,MAAM,UAAU,CAAC;AAiDnF,MAAM,WAAW,oBAAoB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,SAAS,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,MAAM,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACnC,8EAA8E;IAC9E,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB;8CAC0C;IAC1C,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;0EACsE;IACtE,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,uEAAuE;IACvE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AAID,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA4R1D,CAAC"}
@@ -1 +1 @@
1
- import{jsx as _jsx,Fragment as _Fragment,jsxs as _jsxs}from"react/jsx-runtime";import{useEffect,useState}from"react";import{Dialog}from"primereact/dialog";import{Button}from"primereact/button";import{InputText}from"primereact/inputtext";import{InputTextarea}from"primereact/inputtextarea";import{Dropdown}from"primereact/dropdown";import{Checkbox}from"primereact/checkbox";import{InputNumber}from"primereact/inputnumber";import{FormRow}from"../../forms/FormRow";const FIELD_TYPES=[{label:"string",value:"string"},{label:"i32",value:"i32"},{label:"i64",value:"i64"},{label:"u32",value:"u32"},{label:"u64",value:"u64"},{label:"f32",value:"f32"},{label:"f64",value:"f64"},{label:"bool",value:"bool"}],AGG_FNS=[{label:"— none (analysis-fed) —",value:""},{label:"avg",value:"avg"},{label:"min",value:"min"},{label:"max",value:"max"},{label:"stddev",value:"stddev"},{label:"count (how many went into the rollup)",value:"count"}],AGG_FROMS=[{label:"cycle field (every cycle, every test)",value:"cycle_fields"},{label:"result (one value per test)",value:"results"}],COUNT_CHOICES=[{label:"cycles in this test",value:"test_cycles",scope:"test",from:"cycle_fields"},{label:"cycles for this sample, all tests",value:"project_cycles",scope:"project",from:"cycle_fields"},{label:"tests for this sample",value:"project_tests",scope:"project",from:"results"}],AGG_SCOPES=[{label:"this test only",value:"test"},{label:"this sample, every test in the project",value:"project"}],blank={name:"",type:"f32"};export const TestFieldDialog=({visible:e,initial:t,onCancel:a,onSave:l,siblingNames:o,isResultsField:r=!1,isCycleConfigField:n=!1,cycleFieldNames:i=[],resultsFieldNames:s=[]})=>{const[u,c]=useState(blank),[d,m]=useState(""),[g,p]=useState(null);useEffect(()=>{e&&(c(t?{...t}:{...blank}),m(null==t?.default?"":String(t.default)),p(null))},[e,t]);const h=u.aggregate,f=h?.from??"cycle_fields",v=h?.scope??"test",b=COUNT_CHOICES.find(e=>e.scope===v&&e.from===f)?.value??"test_cycles",x=("results"===f?s:i).filter(e=>e&&e!==u.name).map(e=>({label:e,value:e}));return _jsxs(Dialog,{header:t?`Edit field: ${t.name}`:"New field",visible:e,onHide:a,style:{width:"36rem"},children:[g&&_jsx("div",{style:{color:"#dc2626",marginBottom:"0.75rem"},children:g}),_jsx(FormRow,{label:"Name",required:!0,hint:"Wire-format key. Also the column name in CSV exports.",children:_jsx(InputText,{value:u.name,onChange:e=>c({...u,name:e.target.value})})}),_jsx(FormRow,{label:"Type",required:!0,children:_jsx(Dropdown,{value:u.type,options:FIELD_TYPES,onChange:e=>c({...u,type:e.value}),editable:!0})}),_jsx(FormRow,{label:"Units",hint:"Display label, appended to form labels (e.g. m/s).",children:_jsx(InputText,{value:u.units??"",onChange:e=>c({...u,units:e.target.value||void 0})})}),_jsx(FormRow,{label:"Label",hint:"Pretty form label. Falls back to name when empty.",children:_jsx(InputText,{value:u.label??"",onChange:e=>c({...u,label:e.target.value||void 0})})}),_jsx(FormRow,{label:"Required",hint:n?"Blocks the machine until the operator fills this in — tis.cycle_ready stays false and start_test is refused. This is what stops an unannotated row from being recorded.":void 0,children:_jsx(Checkbox,{checked:!!u.required,onChange:e=>c({...u,required:!!e.checked})})}),n&&_jsx(FormRow,{label:"Autoclear",hint:"Clear the operator's entry once a row has recorded it, so the next row can't inherit a stale value. Fires after each cycle under 'cycle' scope, at the end of the run under 'run' scope. Pair with Required — on its own it turns stale data into silently missing data.",children:_jsx(Checkbox,{checked:!!u.autoclear,onChange:e=>c({...u,autoclear:!!e.checked})})}),_jsx(FormRow,{label:"Source",hint:"Optional gm.* variable to bind this field to.",children:_jsx(InputText,{value:u.source??"",onChange:e=>c({...u,source:e.target.value||void 0}),placeholder:"gm.<variable_name>"})}),_jsx(FormRow,{label:"Scale",hint:"display = raw × scale. Leave blank for 1.0 (no conversion).",children:_jsx(InputNumber,{value:u.scale??null,onValueChange:e=>c({...u,scale:"number"==typeof e.value?e.value:void 0}),mode:"decimal",minFractionDigits:0,maxFractionDigits:9})}),_jsx(FormRow,{label:"Min",hint:"Optional lower bound the operator can enter (display units). Blank = no minimum.",children:_jsx(InputNumber,{value:u.min??null,onValueChange:e=>c({...u,min:"number"==typeof e.value?e.value:void 0}),mode:"decimal",minFractionDigits:0,maxFractionDigits:9})}),_jsx(FormRow,{label:"Max",hint:"Optional upper bound the operator can enter (display units). Blank = no maximum.",children:_jsx(InputNumber,{value:u.max??null,onValueChange:e=>c({...u,max:"number"==typeof e.value?e.value:void 0}),mode:"decimal",minFractionDigits:0,maxFractionDigits:9})}),_jsx(FormRow,{label:"Default",hint:"Applied every time the method loads (operator can override). A literal (display units) like 5, or an FQDN token like ${gm.safe_speed} that snapshots that value on load.",children:_jsx(InputText,{value:d,onChange:e=>m(e.target.value),placeholder:"e.g. 5 or ${gm.safe_speed}"})}),r&&_jsxs(_Fragment,{children:[_jsx("div",{style:{borderTop:"1px solid #e5e7eb",margin:"0.75rem 0 0.25rem"}}),_jsx(FormRow,{label:"Aggregate",hint:"Cross-test rollup. When set, this result is derived server-side over the non-excluded tests sharing the same sample_id — the analysis script does not supply it.",children:_jsx(Dropdown,{value:h?.fn??"",options:AGG_FNS,onChange:e=>(e=>{if(""===e)return void c({...u,aggregate:void 0});if("count"===e)return void c({...u,aggregate:{fn:"count",from:u.aggregate?.from??"cycle_fields",scope:u.aggregate?.scope??"test"}});const t={fn:e,of:u.aggregate?.of,from:u.aggregate?.from??"cycle_fields",scope:u.aggregate?.scope??"test"};c({...u,aggregate:t})})(e.value)})}),h&&"count"===h.fn&&_jsx(FormRow,{label:"Count",hint:"What is being counted. A test's own cycles is the usual choice — it's the number the operator can see in the Cycle Data table.",children:_jsx(Dropdown,{value:b,options:COUNT_CHOICES,onChange:e=>{const t=COUNT_CHOICES.find(t=>t.value===e.value);t&&c({...u,aggregate:{...h,scope:t.scope,from:t.from}})}})}),h&&"count"!==h.fn&&_jsxs(_Fragment,{children:[_jsx(FormRow,{label:"Scope",hint:"How wide the reduction reaches. 'This test only' is the default: a value on a test's own results panel or report describes that test.",children:_jsx(Dropdown,{value:v,options:AGG_SCOPES,onChange:e=>c({...u,aggregate:{...h,scope:e.value}})})}),_jsx(FormRow,{label:"Aggregate from",hint:"Which per-test source the target field is read from.",children:_jsx(Dropdown,{value:f,options:AGG_FROMS,onChange:e=>c({...u,aggregate:{...h,from:e.value}})})}),_jsx(FormRow,{label:"Aggregate of",required:!0,hint:"The field to reduce across tests.",children:_jsx(Dropdown,{value:h.of??null,options:x,onChange:e=>c({...u,aggregate:{...h,of:e.value||void 0}}),editable:!0,placeholder:x.length?"Select a field":`No ${f} defined yet`})})]})]}),_jsx(FormRow,{label:"Description",hint:"Hover tooltip in the form.",children:_jsx(InputTextarea,{rows:2,value:u.description??"",onChange:e=>c({...u,description:e.target.value||void 0})})}),_jsxs("div",{style:{display:"flex",justifyContent:"flex-end",gap:"0.5rem",marginTop:"1rem"},children:[_jsx(Button,{label:"Cancel",className:"p-button-text",onClick:a}),_jsx(Button,{label:"Save",onClick:()=>{const e={...u},a=(e=>{const t=e.trim();if(""!==t){if(t.startsWith("${"))return t;if("true"===t)return!0;if("false"===t)return!1;if(/^-?\d*\.?\d+$/.test(t)){const e=Number(t);if(Number.isFinite(e))return e}return t}})(d);void 0===a?delete e.default:e.default=a;const r=(n=e).name.trim()?/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(n.name)?o.some(e=>e===n.name&&e!==t?.name)?`A field named "${n.name}" already exists in this array.`:"number"==typeof n.min&&"number"==typeof n.max&&n.min>n.max?`Min (${n.min}) cannot be greater than Max (${n.max}).`:n.aggregate&&"count"!==n.aggregate.fn&&!n.aggregate.of?.trim()?`Aggregate "${n.aggregate.fn}" needs a target field (the field to ${n.aggregate.fn} across tests).`:null:"Name must be a valid identifier (letters, digits, underscore; cannot start with a digit).":"Field name is required.";var n;r?p(r):l(e)}})]})]})};
1
+ import{jsx as _jsx,Fragment as _Fragment,jsxs as _jsxs}from"react/jsx-runtime";import{useEffect,useState}from"react";import{Dialog}from"primereact/dialog";import{Button}from"primereact/button";import{InputText}from"primereact/inputtext";import{InputTextarea}from"primereact/inputtextarea";import{Dropdown}from"primereact/dropdown";import{Checkbox}from"primereact/checkbox";import{InputNumber}from"primereact/inputnumber";import{FormRow}from"../../forms/FormRow";import{boundText,parseBound,boundInvalid}from"../../tis/TestFieldRow";const FIELD_TYPES=[{label:"string",value:"string"},{label:"i32",value:"i32"},{label:"i64",value:"i64"},{label:"u32",value:"u32"},{label:"u64",value:"u64"},{label:"f32",value:"f32"},{label:"f64",value:"f64"},{label:"bool",value:"bool"}],AGG_FNS=[{label:"— none (analysis-fed) —",value:""},{label:"avg",value:"avg"},{label:"min",value:"min"},{label:"max",value:"max"},{label:"stddev",value:"stddev"},{label:"count (how many went into the rollup)",value:"count"}],AGG_FROMS=[{label:"cycle field (every cycle, every test)",value:"cycle_fields"},{label:"result (one value per test)",value:"results"}],COUNT_CHOICES=[{label:"cycles in this test",value:"test_cycles",scope:"test",from:"cycle_fields"},{label:"cycles for this sample, all tests",value:"project_cycles",scope:"project",from:"cycle_fields"},{label:"tests for this sample",value:"project_tests",scope:"project",from:"results"}],AGG_SCOPES=[{label:"this test only",value:"test"},{label:"this sample, every test in the project",value:"project"}],blank={name:"",type:"f32"};export const TestFieldDialog=({visible:e,initial:t,onCancel:a,onSave:l,siblingNames:r,isResultsField:o=!1,isCycleConfigField:n=!1,cycleFieldNames:i=[],resultsFieldNames:s=[]})=>{const[u,c]=useState(blank),[d,m]=useState(""),[h,p]=useState(null);useEffect(()=>{e&&(c(t?{...t}:{...blank}),m(null==t?.default?"":String(t.default)),p(null))},[e,t]);const g=u.aggregate,f=g?.from??"cycle_fields",v=g?.scope??"test",b=COUNT_CHOICES.find(e=>e.scope===v&&e.from===f)?.value??"test_cycles",x=("results"===f?s:i).filter(e=>e&&e!==u.name).map(e=>({label:e,value:e}));return _jsxs(Dialog,{header:t?`Edit field: ${t.name}`:"New field",visible:e,onHide:a,style:{width:"36rem"},children:[h&&_jsx("div",{style:{color:"#dc2626",marginBottom:"0.75rem"},children:h}),_jsx(FormRow,{label:"Name",required:!0,hint:"Wire-format key. Also the column name in CSV exports.",children:_jsx(InputText,{value:u.name,onChange:e=>c({...u,name:e.target.value})})}),_jsx(FormRow,{label:"Type",required:!0,children:_jsx(Dropdown,{value:u.type,options:FIELD_TYPES,onChange:e=>c({...u,type:e.value}),editable:!0})}),_jsx(FormRow,{label:"Units",hint:"Display label, appended to form labels (e.g. m/s).",children:_jsx(InputText,{value:u.units??"",onChange:e=>c({...u,units:e.target.value||void 0})})}),_jsx(FormRow,{label:"Label",hint:"Pretty form label. Falls back to name when empty.",children:_jsx(InputText,{value:u.label??"",onChange:e=>c({...u,label:e.target.value||void 0})})}),_jsx(FormRow,{label:"Required",hint:n?"Blocks the machine until the operator fills this in — tis.cycle_ready stays false and start_test is refused. This is what stops an unannotated row from being recorded.":void 0,children:_jsx(Checkbox,{checked:!!u.required,onChange:e=>c({...u,required:!!e.checked})})}),n&&_jsx(FormRow,{label:"Autoclear",hint:"Clear the operator's entry once a row has recorded it, so the next row can't inherit a stale value. Fires after each cycle under 'cycle' scope, at the end of the run under 'run' scope. Pair with Required — on its own it turns stale data into silently missing data.",children:_jsx(Checkbox,{checked:!!u.autoclear,onChange:e=>c({...u,autoclear:!!e.checked})})}),_jsx(FormRow,{label:"Source",hint:"Optional gm.* variable to bind this field to.",children:_jsx(InputText,{value:u.source??"",onChange:e=>c({...u,source:e.target.value||void 0}),placeholder:"gm.<variable_name>"})}),_jsx(FormRow,{label:"Scale",hint:"display = raw × scale. Leave blank for 1.0 (no conversion).",children:_jsx(InputNumber,{value:u.scale??null,onValueChange:e=>c({...u,scale:"number"==typeof e.value?e.value:void 0}),mode:"decimal",minFractionDigits:0,maxFractionDigits:9})}),_jsx(FormRow,{label:"Min",hint:"Optional lower bound the operator can enter (display units). Blank = no minimum. Or bind it to the machine with an FQDN token like ${gm.press_force_min_n} — for a bound that moves, e.g. a load-cell force ceiling.",children:_jsx(InputText,{value:boundText(u.min),onChange:e=>c({...u,min:parseBound(e.target.value)}),className:boundInvalid(u.min)?"p-invalid":"",placeholder:"number or ${gm.…}"})}),_jsx(FormRow,{label:"Max",hint:"Optional upper bound the operator can enter (display units). Blank = no maximum. Or bind it to the machine with an FQDN token like ${gm.press_force_max_n}.",children:_jsx(InputText,{value:boundText(u.max),onChange:e=>c({...u,max:parseBound(e.target.value)}),className:boundInvalid(u.max)?"p-invalid":"",placeholder:"number or ${gm.…}"})}),_jsx(FormRow,{label:"Default",hint:"Applied every time the method loads (operator can override). A literal (display units) like 5, or an FQDN token like ${gm.safe_speed} that snapshots that value on load.",children:_jsx(InputText,{value:d,onChange:e=>m(e.target.value),placeholder:"e.g. 5 or ${gm.safe_speed}"})}),o&&_jsxs(_Fragment,{children:[_jsx("div",{style:{borderTop:"1px solid #e5e7eb",margin:"0.75rem 0 0.25rem"}}),_jsx(FormRow,{label:"Aggregate",hint:"Cross-test rollup. When set, this result is derived server-side over the non-excluded tests sharing the same sample_id — the analysis script does not supply it.",children:_jsx(Dropdown,{value:g?.fn??"",options:AGG_FNS,onChange:e=>(e=>{if(""===e)return void c({...u,aggregate:void 0});if("count"===e)return void c({...u,aggregate:{fn:"count",from:u.aggregate?.from??"cycle_fields",scope:u.aggregate?.scope??"test"}});const t={fn:e,of:u.aggregate?.of,from:u.aggregate?.from??"cycle_fields",scope:u.aggregate?.scope??"test"};c({...u,aggregate:t})})(e.value)})}),g&&"count"===g.fn&&_jsx(FormRow,{label:"Count",hint:"What is being counted. A test's own cycles is the usual choice — it's the number the operator can see in the Cycle Data table.",children:_jsx(Dropdown,{value:b,options:COUNT_CHOICES,onChange:e=>{const t=COUNT_CHOICES.find(t=>t.value===e.value);t&&c({...u,aggregate:{...g,scope:t.scope,from:t.from}})}})}),g&&"count"!==g.fn&&_jsxs(_Fragment,{children:[_jsx(FormRow,{label:"Scope",hint:"How wide the reduction reaches. 'This test only' is the default: a value on a test's own results panel or report describes that test.",children:_jsx(Dropdown,{value:v,options:AGG_SCOPES,onChange:e=>c({...u,aggregate:{...g,scope:e.value}})})}),_jsx(FormRow,{label:"Aggregate from",hint:"Which per-test source the target field is read from.",children:_jsx(Dropdown,{value:f,options:AGG_FROMS,onChange:e=>c({...u,aggregate:{...g,from:e.value}})})}),_jsx(FormRow,{label:"Aggregate of",required:!0,hint:"The field to reduce across tests.",children:_jsx(Dropdown,{value:g.of??null,options:x,onChange:e=>c({...u,aggregate:{...g,of:e.value||void 0}}),editable:!0,placeholder:x.length?"Select a field":`No ${f} defined yet`})})]})]}),_jsx(FormRow,{label:"Description",hint:"Hover tooltip in the form.",children:_jsx(InputTextarea,{rows:2,value:u.description??"",onChange:e=>c({...u,description:e.target.value||void 0})})}),_jsxs("div",{style:{display:"flex",justifyContent:"flex-end",gap:"0.5rem",marginTop:"1rem"},children:[_jsx(Button,{label:"Cancel",className:"p-button-text",onClick:a}),_jsx(Button,{label:"Save",onClick:()=>{const e={...u},a=(e=>{const t=e.trim();if(""!==t){if(t.startsWith("${"))return t;if("true"===t)return!0;if("false"===t)return!1;if(/^-?\d*\.?\d+$/.test(t)){const e=Number(t);if(Number.isFinite(e))return e}return t}})(d);void 0===a?delete e.default:e.default=a;const o=(e=>{if(!e.name.trim())return"Field name is required.";if(!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(e.name))return"Name must be a valid identifier (letters, digits, underscore; cannot start with a digit).";if(r.some(a=>a===e.name&&a!==t?.name))return`A field named "${e.name}" already exists in this array.`;if("number"==typeof e.min&&"number"==typeof e.max&&e.min>e.max)return`Min (${e.min}) cannot be greater than Max (${e.max}).`;for(const t of["min","max"])if(boundInvalid(e[t]))return`${"min"===t?"Min":"Max"} must be a number or an FQDN token like \${gm.some_var} — "${e[t]}" is neither.`;return e.aggregate&&"count"!==e.aggregate.fn&&!e.aggregate.of?.trim()?`Aggregate "${e.aggregate.fn}" needs a target field (the field to ${e.aggregate.fn} across tests).`:null})(e);o?p(o):l(e)}})]})]})};
@@ -19,8 +19,12 @@ export interface TestField {
19
19
  /** Optional inclusive bounds for the operator's numeric value, authored
20
20
  * in display units (same convention as `default`/`scale`). The form's
21
21
  * numeric input rejects values outside `[min, max]`. */
22
- min?: number;
23
- max?: number;
22
+ /** Inclusive bounds. A number is in display units; a string is an FQDN
23
+ * token `"${gm.<var>}"` resolved live from the machine — see
24
+ * `TestFieldRow.resolveBound`. The editor must preserve a token it does
25
+ * not understand rather than coercing it to a number. */
26
+ min?: number | string;
27
+ max?: number | string;
24
28
  /** results_fields only: a cross-test rollup derived server-side over the
25
29
  * non-excluded runs sharing this test's sample_id. When set, the field
26
30
  * is NOT supplied by the analysis script — the server computes it. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/tis-editor/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/tis-editor/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcops/autocore-react",
3
- "version": "3.5.3",
3
+ "version": "3.5.5",
4
4
  "description": "A React component library for industrial user interfaces.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -105,6 +105,20 @@ export interface AmsContextValue {
105
105
  listCalibrations: (assetId: string) => Promise<string[]>;
106
106
  readCalibration: (assetId: string, calId: string) => Promise<any | null>;
107
107
  readUsage: (assetId: string) => Promise<any | null>;
108
+ /**
109
+ * Declare which asset is INSTALLED at a role (`ams.assign_role`).
110
+ *
111
+ * Pass `null` for `assetId` to record that nothing is fitted. One RPC,
112
+ * not two `updateAsset` calls: the server vacates the role and installs
113
+ * the new asset atomically, so the role is never momentarily empty or
114
+ * double-claimed — `resolve_method_refs` takes the FIRST active asset at
115
+ * a location, so a double-claimed role resolves to an arbitrary one.
116
+ *
117
+ * Resolves to `null` on success, or the server's message on refusal
118
+ * (wrong asset type for the role, asset not active, unknown id) so the
119
+ * caller can show the operator WHY the swap was refused.
120
+ */
121
+ assignRole: (location: string, assetId: string | null) => Promise<string | null>;
108
122
  selection: AmsSelection;
109
123
  setSelection: (patch: Partial<AmsSelection>) => void;
110
124
  }
@@ -125,6 +139,7 @@ const AmsContext = createContext<AmsContextValue>({
125
139
  listCalibrations: async () => [],
126
140
  readCalibration: async () => null,
127
141
  readUsage: async () => null,
142
+ assignRole: async () => 'AmsProvider is not mounted',
128
143
  selection: { assetType: null, assetId: null },
129
144
  setSelection: () => {},
130
145
  });
@@ -257,6 +272,25 @@ export const AmsProvider: React.FC<AmsProviderProps> = ({ children }) => {
257
272
  } catch { return null; }
258
273
  }, [invoke]);
259
274
 
275
+ const assignRole = useCallback(async (location: string, assetId: string | null): Promise<string | null> => {
276
+ try {
277
+ const resp: any = await invoke('ams.assign_role' as any, MessageType.Request, {
278
+ location, asset_id: assetId,
279
+ } as any);
280
+ if (resp?.success) {
281
+ // The server broadcasts ams.asset_changed for every asset it
282
+ // touched, which already triggers a refresh — but the refresh
283
+ // is awaited here too so a caller that re-renders off `assets`
284
+ // sees the new occupant by the time this resolves.
285
+ await refreshAssets();
286
+ return null;
287
+ }
288
+ return resp?.error_message || 'ams.assign_role failed';
289
+ } catch (e: any) {
290
+ return String(e?.message ?? e);
291
+ }
292
+ }, [invoke, refreshAssets]);
293
+
260
294
  const value = useMemo<AmsContextValue>(() => ({
261
295
  schemas,
262
296
  schemasLoaded,
@@ -269,9 +303,10 @@ export const AmsProvider: React.FC<AmsProviderProps> = ({ children }) => {
269
303
  listCalibrations,
270
304
  readCalibration,
271
305
  readUsage,
306
+ assignRole,
272
307
  selection,
273
308
  setSelection,
274
- }), [schemas, schemasLoaded, roles, rolesLoaded, alerts, assets, refreshAssets, readAsset, listCalibrations, readCalibration, readUsage, selection, setSelection]);
309
+ }), [schemas, schemasLoaded, roles, rolesLoaded, alerts, assets, refreshAssets, readAsset, listCalibrations, readCalibration, readUsage, assignRole, selection, setSelection]);
275
310
 
276
311
  return <AmsContext.Provider value={value}>{children}</AmsContext.Provider>;
277
312
  };
@@ -16,6 +16,7 @@ import { MessageType } from '../../hub/CommandMessage';
16
16
  import { useAms } from './AmsProvider';
17
17
  import { CalibrationEntryDialog } from './CalibrationEntryDialog';
18
18
  import { AssetEditDialog } from './AssetEditDialog';
19
+ import { AssetNameplateGrid } from './AssetNameplateGrid';
19
20
 
20
21
  export const AssetDetailView: React.FC = () => {
21
22
  const { selection, schemas, roles, readAsset, listCalibrations, readCalibration, readUsage,
@@ -189,26 +190,7 @@ export const AssetDetailView: React.FC = () => {
189
190
  {schemaFields.length > 0 && (
190
191
  <div>
191
192
  <h4 style={{ margin: '0 0 0.5rem 0' }}>Nameplate</h4>
192
- <div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr',
193
- gap: '0.25rem 1rem', alignItems: 'baseline',
194
- fontSize: '0.9rem' }}>
195
- {schemaFields.map((f: any) => {
196
- const v = custom[f.name];
197
- const present = v !== undefined && v !== null && v !== '';
198
- const label = f.label ?? f.name;
199
- const display = present
200
- ? (f.units ? `${v} ${f.units}` : String(v))
201
- : '(not set)';
202
- return (
203
- <React.Fragment key={f.name}>
204
- <strong title={f.description ?? undefined}>{label}</strong>
205
- <span style={{ color: present ? undefined : '#f59e0b' }}>
206
- {display}
207
- </span>
208
- </React.Fragment>
209
- );
210
- })}
211
- </div>
193
+ <AssetNameplateGrid schemaFields={schemaFields} custom={custom} />
212
194
  </div>
213
195
  )}
214
196
 
@@ -0,0 +1,62 @@
1
+ /*
2
+ * Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
3
+ *
4
+ * <AssetNameplateGrid> — the nameplate field/value grid for one asset.
5
+ *
6
+ * Extracted so <AssetDetailView> (the full record) and
7
+ * <InstalledAssetPicker> (what is fitted right now) render a nameplate the
8
+ * same way. A field the schema declares but the record has not filled in is
9
+ * drawn as an amber "(not set)" rather than blank space: that is exactly the
10
+ * state the placeholder resolver refuses to start a module on, so it is worth
11
+ * drawing the eye to.
12
+ */
13
+
14
+ import React from 'react';
15
+
16
+ export interface AssetNameplateGridProps {
17
+ /** The asset_type schema's `fields` array. */
18
+ schemaFields: any[];
19
+ /** The asset record's `custom` object (nameplate values). */
20
+ custom: Record<string, any>;
21
+ /** Only render these field names, in this order. Omit for all of them. */
22
+ only?: string[];
23
+ /** Font size for the grid; the detail view runs slightly larger. */
24
+ fontSize?: string;
25
+ }
26
+
27
+ export const AssetNameplateGrid: React.FC<AssetNameplateGridProps> = ({
28
+ schemaFields,
29
+ custom,
30
+ only,
31
+ fontSize = '0.9rem',
32
+ }) => {
33
+ const fields = only
34
+ ? only
35
+ .map(name => schemaFields.find((f: any) => f.name === name))
36
+ .filter(Boolean)
37
+ : schemaFields;
38
+
39
+ if (fields.length === 0) return null;
40
+
41
+ return (
42
+ <div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr',
43
+ gap: '0.25rem 1rem', alignItems: 'baseline', fontSize }}>
44
+ {fields.map((f: any) => {
45
+ const v = custom[f.name];
46
+ const present = v !== undefined && v !== null && v !== '';
47
+ const label = f.label ?? f.name;
48
+ const display = present
49
+ ? (f.units ? `${v} ${f.units}` : String(v))
50
+ : '(not set)';
51
+ return (
52
+ <React.Fragment key={f.name}>
53
+ <strong title={f.description ?? undefined}>{label}</strong>
54
+ <span style={{ color: present ? undefined : '#f59e0b' }}>
55
+ {display}
56
+ </span>
57
+ </React.Fragment>
58
+ );
59
+ })}
60
+ </div>
61
+ );
62
+ };
@@ -0,0 +1,209 @@
1
+ /*
2
+ * Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
3
+ *
4
+ * <InstalledAssetPicker> — "which asset is fitted in this role right now?"
5
+ *
6
+ * The operator-facing answer to a swap. A machine with a 2 kN and a 20 kN load
7
+ * cell in the drawer has ONE of them bolted to the press; this picker is where
8
+ * they say which, and everything downstream — the test record's asset
9
+ * snapshot, module placeholders, the control program's AssetWatch — follows
10
+ * from that one statement.
11
+ *
12
+ * It writes through `ams.assign_role`, never two `ams.update_asset` calls: the
13
+ * server vacates the role and installs the replacement atomically, because
14
+ * `resolve_method_refs` takes the FIRST active asset it finds at a location and
15
+ * a momentarily double-claimed role would therefore resolve to an arbitrary one.
16
+ *
17
+ * Deliberately NOT a nameplate editor. The nameplate belongs to the asset
18
+ * record (Assets tab / <AssetDetailView>) — one source of truth. This picker
19
+ * only chooses which record is in force, and shows what that record says so the
20
+ * operator can confirm they grabbed the right hardware before pressing on.
21
+ */
22
+
23
+ import React, { useCallback, useMemo, useState } from 'react';
24
+ import { Dropdown } from 'primereact/dropdown';
25
+ import { Button } from 'primereact/button';
26
+ import { Dialog } from 'primereact/dialog';
27
+ import { Message } from 'primereact/message';
28
+ import { AssetNameplateGrid } from './AssetNameplateGrid';
29
+ import { useInstalledAsset } from './useInstalledAsset';
30
+
31
+ export interface InstalledAssetPickerProps {
32
+ /** Asset type the role takes, e.g. `"load_cell"`. */
33
+ assetType: string;
34
+ /** The role's `location` string, e.g. `"press_load_cell"`. */
35
+ location: string;
36
+ /**
37
+ * Heading for the picker. Defaults to the role's label from
38
+ * `ams.list_roles`, then to the raw location.
39
+ */
40
+ label?: string;
41
+ /** One line under the dropdown explaining what this role drives. */
42
+ description?: string;
43
+ /**
44
+ * Nameplate field names folded into each option's text, so the operator can
45
+ * tell two assets apart without opening them (e.g.
46
+ * `["capacity", "capacity_units"]` → "2000 N"). Serial is always shown.
47
+ */
48
+ summaryFields?: string[];
49
+ /** Render the fitted asset's full nameplate below the dropdown. */
50
+ showNameplate?: boolean;
51
+ /** Render the fitted asset's calibration state below the nameplate. */
52
+ showCalibration?: boolean;
53
+ /**
54
+ * Block the swap. Use for "a test is running" — changing the fitted asset
55
+ * mid-run would re-scale a channel under the data being recorded.
56
+ */
57
+ disabled?: boolean;
58
+ /** Why it is blocked. Shown in place of the help text. */
59
+ disabledReason?: string;
60
+ /** Fired after a successful assignment with the new asset id (or null). */
61
+ onAssigned?: (assetId: string | null) => void;
62
+ }
63
+
64
+ /** Option value standing for "nothing is fitted". */
65
+ const NONE = '__none__';
66
+
67
+ export const InstalledAssetPicker: React.FC<InstalledAssetPickerProps> = ({
68
+ assetType,
69
+ location,
70
+ label,
71
+ description,
72
+ summaryFields = [],
73
+ showNameplate = true,
74
+ showCalibration = true,
75
+ disabled = false,
76
+ disabledReason,
77
+ onAssigned,
78
+ }) => {
79
+ const {
80
+ candidates, records, installed, installedRecord,
81
+ calibration, calibrationOverdue, role, schemaFields, assign,
82
+ } = useInstalledAsset(assetType, location);
83
+
84
+ const [error, setError] = useState<string | null>(null);
85
+ const [busy, setBusy] = useState(false);
86
+ /** Non-null while the confirm dialog is open; carries the pending choice. */
87
+ const [pending, setPending] = useState<{ assetId: string | null } | null>(null);
88
+
89
+ const roleLabel = label ?? role?.label ?? location;
90
+ const roleHelp = description ?? role?.description ?? null;
91
+
92
+ /** "SN 4471-A — 2000 N" — enough to tell two assets apart in a list. */
93
+ const optionText = useCallback((assetId: string): string => {
94
+ const entry = candidates.find(c => c.asset_id === assetId);
95
+ const custom = (records[assetId]?.custom ?? {}) as Record<string, any>;
96
+ const bits: string[] = [entry?.serial ? `SN ${entry.serial}` : assetId];
97
+ const summary = summaryFields
98
+ .map(name => custom[name])
99
+ .filter(v => v !== undefined && v !== null && v !== '')
100
+ .join(' ');
101
+ if (summary) bits.push(summary);
102
+ return bits.join(' — ');
103
+ }, [candidates, records, summaryFields]);
104
+
105
+ const options = useMemo(() => [
106
+ ...candidates.map(c => ({ label: optionText(c.asset_id), value: c.asset_id })),
107
+ { label: '— nothing fitted —', value: NONE },
108
+ ], [candidates, optionText]);
109
+
110
+ const commit = async (assetId: string | null) => {
111
+ setBusy(true);
112
+ setError(null);
113
+ const err = await assign(assetId);
114
+ setBusy(false);
115
+ setPending(null);
116
+ if (err) { setError(err); return; }
117
+ onAssigned?.(assetId);
118
+ };
119
+
120
+ const calExpiry = calibration?.expires_at ? new Date(calibration.expires_at) : null;
121
+
122
+ return (
123
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
124
+ <div className="p-inputgroup">
125
+ <span className="p-inputgroup-addon" style={{ flexGrow: 1 }}>
126
+ {roleLabel}
127
+ </span>
128
+ <Dropdown
129
+ value={installed?.asset_id ?? NONE}
130
+ options={options}
131
+ disabled={disabled || busy}
132
+ onChange={(e) => {
133
+ const next = e.value === NONE ? null : (e.value as string);
134
+ if (next === (installed?.asset_id ?? null)) return;
135
+ setPending({ assetId: next });
136
+ }}
137
+ placeholder="Select the fitted asset"
138
+ style={{ minWidth: '18rem' }}
139
+ />
140
+ </div>
141
+
142
+ {/* Help text, or the reason the operator can't change it. On screen
143
+ rather than in a hover tooltip — this runs on a touchscreen,
144
+ where a tooltip is unreachable. */}
145
+ {disabled && disabledReason
146
+ ? <small style={{ color: '#f59e0b' }}>{disabledReason}</small>
147
+ : roleHelp && <small style={{ color: '#9ca3af' }}>{roleHelp}</small>}
148
+
149
+ {candidates.length === 0 && (
150
+ <Message severity="warn"
151
+ text={`No active ${assetType.replace(/_/g, ' ')} is registered. Add one on the Assets screen first.`} />
152
+ )}
153
+
154
+ {!installed && candidates.length > 0 && (
155
+ <Message severity="warn"
156
+ text="Nothing is recorded as fitted in this role. Tests that need it will refuse to start." />
157
+ )}
158
+
159
+ {error && <Message severity="error" text={error} />}
160
+
161
+ {installed && showNameplate && (
162
+ <AssetNameplateGrid
163
+ schemaFields={schemaFields}
164
+ custom={(installedRecord?.custom ?? {}) as Record<string, any>}
165
+ />
166
+ )}
167
+
168
+ {installed && showCalibration && (
169
+ <small style={{ color: calibrationOverdue ? '#f59e0b' : '#9ca3af' }}>
170
+ {!installed.current_calibration_id
171
+ ? 'No calibration on record for this asset.'
172
+ : calibrationOverdue
173
+ ? `Calibration ${installed.current_calibration_id} expired ${calExpiry!.toLocaleDateString()}.`
174
+ : `Calibration ${installed.current_calibration_id}${
175
+ calExpiry ? `, in date until ${calExpiry.toLocaleDateString()}` : ''}.`}
176
+ </small>
177
+ )}
178
+
179
+ {/* Confirm, because this restates what hardware is bolted to the
180
+ machine: downstream it re-scales a measurement channel and is
181
+ frozen into every test record started afterwards. */}
182
+ <Dialog
183
+ visible={pending !== null}
184
+ header={`Change the ${roleLabel}?`}
185
+ modal
186
+ onHide={() => setPending(null)}
187
+ footer={
188
+ <>
189
+ <Button label="Cancel" outlined onClick={() => setPending(null)} />
190
+ <Button label="Confirm" loading={busy}
191
+ onClick={() => { void commit(pending?.assetId ?? null); }} />
192
+ </>
193
+ }
194
+ >
195
+ <p style={{ margin: 0 }}>
196
+ {pending?.assetId
197
+ ? <>Record <strong>{optionText(pending.assetId)}</strong> as the asset fitted
198
+ in <strong>{roleLabel}</strong>.</>
199
+ : <>Record that <strong>nothing</strong> is fitted in <strong>{roleLabel}</strong>.</>}
200
+ </p>
201
+ <p style={{ marginBottom: 0, color: '#9ca3af' }}>
202
+ Confirm only if this matches the hardware actually installed. The
203
+ machine reconfigures the channel from this asset's nameplate, and
204
+ every test started from now on records it.
205
+ </p>
206
+ </Dialog>
207
+ </div>
208
+ );
209
+ };
@@ -14,3 +14,10 @@ export { AssetEditDialog } from './AssetEditDialog';
14
14
  export { SubLocationPicker } from './SubLocationPicker';
15
15
  export { PlaceholderHealthPanel } from './PlaceholderHealthPanel';
16
16
  export { MissingAssetsBanner } from './MissingAssetsBanner';
17
+ // "What is fitted right now" — the operator's swap surface, and the
18
+ // nameplate grid it shares with <AssetDetailView>.
19
+ export { InstalledAssetPicker } from './InstalledAssetPicker';
20
+ export type { InstalledAssetPickerProps } from './InstalledAssetPicker';
21
+ export { AssetNameplateGrid } from './AssetNameplateGrid';
22
+ export { useInstalledAsset } from './useInstalledAsset';
23
+ export type { InstalledAssetState } from './useInstalledAsset';
@@ -0,0 +1,108 @@
1
+ /*
2
+ * Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
3
+ *
4
+ * useInstalledAsset — "what is fitted in this role, and what does its record
5
+ * say?" for one (assetType, location) pair.
6
+ *
7
+ * The registry cache in <AmsProvider> carries only identity (id, serial,
8
+ * status, location); anything that has to show or act on a NAMEPLATE needs the
9
+ * full record. This hook is the one place that fetch happens, so
10
+ * <InstalledAssetPicker> and a product's own panel (e.g. a load-channel
11
+ * verification table comparing the nameplate against what the hardware
12
+ * actually holds) read the same data instead of each querying for it.
13
+ */
14
+
15
+ import { useCallback, useEffect, useMemo, useState } from 'react';
16
+ import { useAms, type AmsAssetEntry, type AmsRole } from './AmsProvider';
17
+
18
+ export interface InstalledAssetState {
19
+ /** Every ACTIVE asset of this type — the assets that could be fitted. */
20
+ candidates: AmsAssetEntry[];
21
+ /** Full records for `candidates`, keyed by asset_id. */
22
+ records: Record<string, any>;
23
+ /** The registry entry fitted in this role, or null when nothing is. */
24
+ installed: AmsAssetEntry | null;
25
+ /** The fitted asset's full record; nameplate values live under `.custom`. */
26
+ installedRecord: any | null;
27
+ /** The fitted asset's current calibration record, or null. */
28
+ calibration: any | null;
29
+ /** True once the calibration on record has passed its `expires_at`. */
30
+ calibrationOverdue: boolean;
31
+ /** This role's metadata from `ams.list_roles`, when the project declares it. */
32
+ role: AmsRole | null;
33
+ /** The asset_type schema's nameplate `fields` array (empty if unknown). */
34
+ schemaFields: any[];
35
+ /**
36
+ * Declare what is fitted (`null` = nothing). Resolves to `null` on success
37
+ * or the server's refusal message.
38
+ */
39
+ assign: (assetId: string | null) => Promise<string | null>;
40
+ }
41
+
42
+ export function useInstalledAsset(assetType: string, location: string): InstalledAssetState {
43
+ const { assets, schemas, roles, readAsset, readCalibration, assignRole } = useAms();
44
+
45
+ const [records, setRecords] = useState<Record<string, any>>({});
46
+ const [calibration, setCalibration] = useState<any | null>(null);
47
+
48
+ const candidates = useMemo(
49
+ () => assets.filter(a => a.asset_type === assetType && a.status === 'active'),
50
+ [assets, assetType],
51
+ );
52
+ const installed = useMemo(
53
+ () => candidates.find(a => a.location === location) ?? null,
54
+ [candidates, location],
55
+ );
56
+ const role = useMemo(
57
+ () => (roles[assetType] ?? []).find(r => r.location === location) ?? null,
58
+ [roles, assetType, location],
59
+ );
60
+
61
+ // Fetch the full record for every candidate. Keyed on the id list, so a
62
+ // registry refresh that left the candidate set alone does no I/O.
63
+ const candidateKey = candidates.map(c => c.asset_id).join(',');
64
+ useEffect(() => {
65
+ let cancelled = false;
66
+ (async () => {
67
+ const next: Record<string, any> = {};
68
+ for (const c of candidates) {
69
+ const rec = await readAsset(c.asset_id);
70
+ if (cancelled) return;
71
+ if (rec) next[c.asset_id] = rec;
72
+ }
73
+ if (!cancelled) setRecords(next);
74
+ })();
75
+ return () => { cancelled = true; };
76
+ // eslint-disable-next-line react-hooks/exhaustive-deps
77
+ }, [candidateKey, readAsset]);
78
+
79
+ // The fitted asset's current calibration, for the "is it in date?" line.
80
+ useEffect(() => {
81
+ let cancelled = false;
82
+ (async () => {
83
+ if (!installed?.current_calibration_id) { setCalibration(null); return; }
84
+ const cal = await readCalibration(installed.asset_id, installed.current_calibration_id);
85
+ if (!cancelled) setCalibration(cal);
86
+ })();
87
+ return () => { cancelled = true; };
88
+ }, [installed?.asset_id, installed?.current_calibration_id, readCalibration]);
89
+
90
+ const assign = useCallback(
91
+ (assetId: string | null) => assignRole(location, assetId),
92
+ [assignRole, location],
93
+ );
94
+
95
+ const expiry = calibration?.expires_at ? new Date(calibration.expires_at) : null;
96
+
97
+ return {
98
+ candidates,
99
+ records,
100
+ installed,
101
+ installedRecord: installed ? (records[installed.asset_id] ?? null) : null,
102
+ calibration,
103
+ calibrationOverdue: !!expiry && expiry.getTime() < Date.now(),
104
+ role,
105
+ schemaFields: Array.isArray(schemas[assetType]?.fields) ? schemas[assetType].fields : [],
106
+ assign,
107
+ };
108
+ }
@@ -181,6 +181,37 @@ export interface TagLookup {
181
181
  /** Matches a bound token like `${gm.press_force_min_n}` (whole-string). */
182
182
  const BOUND_TOKEN_RE = /^\$\{\s*([^}]+?)\s*\}$/;
183
183
 
184
+ /** Is this string a well-formed FQDN bound token? */
185
+ export const isBoundToken = (s: string): boolean => BOUND_TOKEN_RE.test(s.trim());
186
+
187
+ /**
188
+ * Render a bound for an authoring text field — the method editor's Min/Max.
189
+ *
190
+ * A text field, not a numeric one, because a bound is a number OR a token. The
191
+ * editor used `InputNumber`, which rendered a token as **NaN** and — worse —
192
+ * coerced anything non-numeric to `undefined` on change, so opening the dialog
193
+ * and saving silently deleted a token bound.
194
+ */
195
+ export const boundText = (b: number | string | undefined): string =>
196
+ b === undefined || b === null ? '' : String(b);
197
+
198
+ /**
199
+ * Parse what an author typed into a bound field. Blank clears it; a finite
200
+ * number stores a number; anything else is kept VERBATIM so a half-typed token
201
+ * survives the keystroke. Use `boundInvalid` to flag what will not resolve.
202
+ */
203
+ export const parseBound = (text: string): number | string | undefined => {
204
+ const t = text.trim();
205
+ if (t === '') return undefined;
206
+ const n = Number(t);
207
+ if (Number.isFinite(n)) return n;
208
+ return t;
209
+ };
210
+
211
+ /** True when a bound is a string that is not a well-formed token. */
212
+ export const boundInvalid = (b: number | string | undefined): boolean =>
213
+ typeof b === 'string' && !isBoundToken(b);
214
+
184
215
  /**
185
216
  * A declared bound resolved to a plain DISPLAY-unit number, or `undefined` for
186
217
  * "no bound".
@@ -8,6 +8,10 @@ import { Checkbox } from 'primereact/checkbox';
8
8
  import { InputNumber } from 'primereact/inputnumber';
9
9
  import { FormRow } from '../../forms/FormRow';
10
10
  import type { TestField, AggFn, AggFrom, AggScope, AggregateSpec } from '../types';
11
+ // One home for bound parsing/formatting — shared with the operator-facing
12
+ // form so the editor and the runtime can never disagree on what a bound is.
13
+ import { boundText, parseBound, boundInvalid } from '../../tis/TestFieldRow';
14
+
11
15
 
12
16
  const FIELD_TYPES: { label: string; value: string }[] = [
13
17
  { label: 'string', value: 'string' },
@@ -116,6 +120,13 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
116
120
  if (typeof f.min === 'number' && typeof f.max === 'number' && f.min > f.max) {
117
121
  return `Min (${f.min}) cannot be greater than Max (${f.max}).`;
118
122
  }
123
+ // A bound that is neither a number nor a well-formed token resolves to
124
+ // NO bound at run time — silently unlimited. Refuse the save instead.
125
+ for (const which of ['min', 'max'] as const) {
126
+ if (boundInvalid(f[which])) {
127
+ return `${which === 'min' ? 'Min' : 'Max'} must be a number or an FQDN token like \${gm.some_var} — "${f[which]}" is neither.`;
128
+ }
129
+ }
119
130
  if (f.aggregate && f.aggregate.fn !== 'count' && !f.aggregate.of?.trim()) {
120
131
  return `Aggregate "${f.aggregate.fn}" needs a target field (the field to ${f.aggregate.fn} across tests).`;
121
132
  }
@@ -241,26 +252,26 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
241
252
  maxFractionDigits={9}
242
253
  />
243
254
  </FormRow>
244
- <FormRow label="Min" hint="Optional lower bound the operator can enter (display units). Blank = no minimum.">
245
- <InputNumber
246
- value={draft.min ?? null}
247
- onValueChange={(e) =>
248
- setDraft({ ...draft, min: typeof e.value === 'number' ? e.value : undefined })
249
- }
250
- mode="decimal"
251
- minFractionDigits={0}
252
- maxFractionDigits={9}
255
+ <FormRow
256
+ label="Min"
257
+ hint="Optional lower bound the operator can enter (display units). Blank = no minimum. Or bind it to the machine with an FQDN token like ${gm.press_force_min_n} for a bound that moves, e.g. a load-cell force ceiling."
258
+ >
259
+ <InputText
260
+ value={boundText(draft.min)}
261
+ onChange={(e) => setDraft({ ...draft, min: parseBound(e.target.value) })}
262
+ className={boundInvalid(draft.min) ? 'p-invalid' : ''}
263
+ placeholder="number or ${gm.…}"
253
264
  />
254
265
  </FormRow>
255
- <FormRow label="Max" hint="Optional upper bound the operator can enter (display units). Blank = no maximum.">
256
- <InputNumber
257
- value={draft.max ?? null}
258
- onValueChange={(e) =>
259
- setDraft({ ...draft, max: typeof e.value === 'number' ? e.value : undefined })
260
- }
261
- mode="decimal"
262
- minFractionDigits={0}
263
- maxFractionDigits={9}
266
+ <FormRow
267
+ label="Max"
268
+ hint="Optional upper bound the operator can enter (display units). Blank = no maximum. Or bind it to the machine with an FQDN token like ${gm.press_force_max_n}."
269
+ >
270
+ <InputText
271
+ value={boundText(draft.max)}
272
+ onChange={(e) => setDraft({ ...draft, max: parseBound(e.target.value) })}
273
+ className={boundInvalid(draft.max) ? 'p-invalid' : ''}
274
+ placeholder="number or ${gm.…}"
264
275
  />
265
276
  </FormRow>
266
277
  <FormRow label="Default" hint="Applied every time the method loads (operator can override). A literal (display units) like 5, or an FQDN token like ${gm.safe_speed} that snapshots that value on load.">
@@ -25,8 +25,12 @@ export interface TestField {
25
25
  /** Optional inclusive bounds for the operator's numeric value, authored
26
26
  * in display units (same convention as `default`/`scale`). The form's
27
27
  * numeric input rejects values outside `[min, max]`. */
28
- min?: number;
29
- max?: number;
28
+ /** Inclusive bounds. A number is in display units; a string is an FQDN
29
+ * token `"${gm.<var>}"` resolved live from the machine — see
30
+ * `TestFieldRow.resolveBound`. The editor must preserve a token it does
31
+ * not understand rather than coercing it to a number. */
32
+ min?: number | string;
33
+ max?: number | string;
30
34
  /** results_fields only: a cross-test rollup derived server-side over the
31
35
  * non-excluded runs sharing this test's sample_id. When set, the field
32
36
  * is NOT supplied by the analysis script — the server computes it. */