PageSourceSearch

https://blockautomation.ai/assets/index-BxUltzRO.js

js blockautomation.ai collected 2026-10-05 13:09:43 UTC 16,739 bytes, 1 lines download raw bytes

1import{r,j as e,ab as m,C as f,L as ne}from"./main-9st80poY.js";import{P as T}from"./PageContainer-CG0sj27n.js";import{P as A}from"./PageHeader-C09CNHjc.js";import{E as Y}from"./EmptyState-CCg2F7Dg.js";import{S as le}from"./Skeleton-BNR6vjoo.js";import{a as w}from"./api-BzhAchE7.js";import{h as ie}from"./operatingHours-DWcMLb6I.js";import"./checkoutApi-B8Quyt35.js";const oe=t=>{if(!t)return"Never";const i=Date.now()-new Date(t).getTime();if(!Number.isFinite(i)||i<0)return"Just now";const l=Math.floor(i/6e4);if(l<1)return"Just now";if(l<60)return`${l}m ago`;const o=Math.floor(l/60);return o<24?`${o}h ago`:`${Math.floor(o/24)}d ago`},d=(t,i)=>`${t} ${i}${t===1?"":"s"}`;function n({title:t,desc:i,owner:l,kind:o,children:b}){return e.jsxs("li",{className:"fr-step",children:[e.jsxs("div",{className:"fr-step__body",children:[e.jsxs("div",{className:"fr-step__head",children:[e.jsx("span",{className:"fr-step__title",children:t}),e.jsx("span",{className:`fr-chip ${l==="Device firmware"?"fr-chip--fw":"fr-chip--cloud"}`,children:l})]}),e.jsx("p",{className:"fr-step__desc",children:i}),b]}),e.jsxs("div",{className:"fr-step__control",children:[o==="locked"&&e.jsxs("span",{className:"fr-lock",title:"Not remotely controllable",children:[e.jsx("span",{"aria-hidden":"true",children:"🔒"})," Fixed — requires a firmware change"]}),o==="always-on"&&e.jsx("span",{className:"fr-lock",children:"Always on — code-owned"})]})]})}function je(){var G,Q;const[t,i]=r.useState(null),[l,o]=r.useState(null),[b,M]=r.useState(!0),[Z,N]=r.useState(!1),[x,O]=r.useState(null),[j,D]=r.useState(!1),[g,P]=r.useState(null),[v,C]=r.useState(null),[R,$]=r.useState(!1),[y,B]=r.useState(null),[F,H]=r.useState(null),[L,I]=r.useState(!1),[h,V]=r.useState(null),[W,K]=r.useState(null),k=r.useCallback(async()=>{M(!0),o(null);try{i(await w.block.getFleetRoutines())}catch(s){o(s.message||"Failed to load")}finally{M(!1)}},[]);r.useEffect(()=>{k()},[k]);const c=(t==null?void 0:t.keepPrime)||{offValue:0,floorMin:15,maxMin:1440,defaultMin:180},u=(t==null?void 0:t.blocks)||[],z=((Q=(G=t==null?void 0:t.loops)==null?void 0:G.uvPush)==null?void 0:Q.cadenceMinutes)||60,ee=r.useMemo(()=>{if(u.length===0)return"Off";const s=u.map(p=>{var X;return((X=p.keep_prime)==null?void 0:X.desired_min)??null}),a=[...new Set(s)];return a.every(p=>p===null)?"Off (device default — never commanded)":a.every(p=>p===0||p===null)?"Off everywhere":a.length===1?`On — every ${a[0]} min`:"Mixed — see per-block states below"},[u]),q=r.useMemo(()=>u.filter(s=>{var a;return(a=s.keep_prime)==null?void 0:a.push_pending}).length,[u]),U=async s=>{D(!0),C(null),P(null);try{const a=await w.block.setFleetKeepPrime(s);P(a),N(!1),await k()}catch(a){C(a.message||"The fleet update failed")}finally{D(!1)}},se=async()=>{$(!0),H(null),B(null);try{B(await w.block.sendOpsTestAlert())}catch(s){H(s.message||"The test alert could not be sent")}finally{$(!1)}},S=(t==null?void 0:t.slotRuntimes)||{},_=Number(S.stale||0),J=Number(S.estimated||0),E=Number(S.total||0),te=E===0?"No slot has a flow rate yet — measure a pump from its block’s Tools tab.":_===0?`Every calibrated slot (${E}) is running the run-time its dose and flow imply.`:`${d(_,"slot")} of ${E} is running a run-time that no longer matches its dose and flow.`,re=async()=>{I(!0),K(null),V(null);try{V(await w.block.resyncFleetSlotRuntimes()),await k()}catch(s){K(s.message||"The run-time re-sync failed")}finally{I(!1)}},ae=s=>{const a=s.keep_prime||{},p=a.desired_min==null?"Device default (off)":a.desired_min===0?"Off":`Every ${a.desired_min} min`;return e.jsxs("span",{className:"fr-kp-cell",children:[e.jsx("span",{children:p}),a.push_pending&&e.jsx("span",{className:"fr-pending",title:a.push_reason||"Device unreachable",children:"Will apply on next check-in"})]})};return b&&!t?e.jsxs(T,{"data-testid":"fleet-routines-page",children:[e.jsx(A,{title:"Fleet routines",subtitle:"What every block does across a day, and what you can change fleet-wide."}),e.jsx(le,{lines:8})]}):l?e.jsxs(T,{"data-testid":"fleet-routines-page",children:[e.jsx(A,{title:"Fleet routines"}),e.jsx(Y,{title:"Couldn’t load fleet routines",description:l,action:e.jsx(m,{variant:"primary",onClick:k,children:"Try again"})})]}):e.jsxs(T,{"data-testid":"fleet-routines-page",children:[e.jsx(A,{title:"Fleet routines",subtitle:"The real daily sequence each block runs — device firmware plus the cloud loops — and the controls that exist today."}),e.jsxs("div",{className:"fr-phases",children:[e.jsxs(f,{className:"fr-phase",children:[e.jsx("h3",{className:"fr-phase__title",children:"Morning startup"}),e.jsx("p",{className:"fr-phase__when",children:"At each block’s local opening time"}),e.jsxs("ul",{className:"fr-steps",children:[e.jsx(n,{title:"Opens for business",owner:"Device firmware",kind:"locked",desc:"The block checks its sc
1hedule every 10 seconds and flips from closed (INACTIVE) to selling (PAID) at opening time. The schedule and timezone offset are pushed from the cloud and stored on the device."}),e.jsx(n,{title:"Operating hours",owner:"Cloud → device",kind:"link",desc:"Each block follows its location’s schedule (or its own, if it has no location schedule). Edits are pushed to the device with the right timezone offset.",children:e.jsx(ne,{className:"fr-link",to:"/dashboard/locations",children:"Edit in Locations →"})}),e.jsx(n,{title:"Hand sensor wakes + recalibrates",owner:"Device firmware",kind:"locked",desc:"The infrared proximity engine is re-energized and its baseline re-measured (~1 s) — the overnight baseline is a dark frame and is never trusted. Self-healing: the chip state is re-checked every 5 seconds all day."}),e.jsx(n,{title:"Heartbeats speed up",owner:"Device firmware",kind:"locked",desc:"Check-in cadence moves from hourly (closed) to every 15 minutes (open)."})]})]}),e.jsxs(f,{className:"fr-phase",children:[e.jsx("h3",{className:"fr-phase__title",children:"During open hours"}),e.jsx("p",{className:"fr-phase__when",children:"While a block is open"}),e.jsxs("ul",{className:"fr-steps",children:[e.jsx(n,{title:"Heartbeat every 15 minutes",owner:"Device firmware",kind:"locked",desc:"Battery, temperature, slots, signal and health land in the platform on every beat. Lost cloud pushes (refills, doses, keep-prime) are re-delivered on the next beat."}),e.jsx(n,{title:"Sensor sweep every 30 seconds",owner:"Device firmware",kind:"locked",desc:"On-device sensor readings refresh continuously while open (every 60 s when closed)."}),e.jsx(n,{title:`UV index push (every ${z===60?"hour":`${z} min`})`,owner:"Cloud loop",kind:"always-on",desc:"At the top of each hour the cloud records every deployed block’s weather/UV reading and pushes the UV index to each reachable block — at most one push per block per hour. There is no runtime kill switch; disabling it is a code change."}),e.jsx(n,{title:"PrimeBoost idle compensation",owner:"Device firmware",kind:"always-on",desc:"Every customer dispense automatically adds a catch-up boost sized to how long the line sat idle — the standing fix for the first-shot-of-the-morning problem. Reactive only: it never dispenses on its own."}),e.jsx("li",{className:"fr-step fr-step--control","data-testid":"keep-prime-control",children:e.jsxs("div",{className:"fr-step__body",children:[e.jsxs("div",{className:"fr-step__head",children:[e.jsx("span",{className:"fr-step__title",children:"Keep-prime self-dispense"}),e.jsx("span",{className:"fr-chip fr-chip--fw",children:"Device firmware"}),e.jsx("span",{className:"fr-chip fr-chip--optin",children:"Opt-in"})]}),e.jsxs("p",{className:"fr-step__desc",children:["A scheduled maintenance pulse that keeps the line wet on idle blocks. It dispenses real product into the cup on a timer, so it is ",e.jsx("strong",{children:"off by default"})," — PrimeBoost above already compensates when a customer arrives. Enable it only if a block’s first-dispense quality needs the proactive help."]}),e.jsxs("div",{className:"fr-kp",children:[e.jsx("span",{className:"fr-kp__status","data-testid":"keep-prime-status",children:ee}),q>0&&e.jsxs("span",{className:"fr-pending",children:[d(q,"block")," still waiting for delivery"]}),e.jsxs("div",{className:"fr-kp__actions",children:[e.jsx(m,{variant:"ghost",disabled:j,onClick:()=>U(c.offValue),children:"Turn off everywhere"}),e.jsx(m,{variant:"primary",disabled:j,onClick:()=>{O(c.defaultMin),C(null),N(!0)},children:"Enable keep-prime…"})]}),g&&e.jsxs("p",{className:"fr-kp__result",role:"status",children:["Applied to ",d(g.summary.applied,"block"),".",g.summary.pending>0&&e.jsxs(e.Fragment,{children:[" ",d(g.summary.pending,"block")," will get it on its next check-in."]})]}),v&&e.jsx("p",{className:"fr-kp__error",role:"alert",children:v})]})]})}),e.jsx("li",{className:"fr-step fr-step--control","data-testid":"slot-runtime-control",children:e.jsxs("div",{className:"fr-step__body",children:[e.jsxs("div",{className:"fr-step__head",children:[e.jsx("span",{className:"fr-step__title",children:"Pump run-times"}),e.jsx("span",{className:"fr-chip fr-chip--fw",children:"Cloud → device"})]}),e.jsx("p",{className:"fr-step__desc",children:"Each slot pours for a run-time the cloud derives from its dose and its flow rate. When a flow rate changes, the run-time already on the block does not — it is whatever that block last accepted. Re-sync pushes the current number to every slot that is out of date and leaves the rest alone."}),e.jsxs("div",{className:"fr-kp",children:[e.jsx("span",{className:"fr-kp__status","data-testid":"slot-runtime-status",children:te}),J>0&&e.jsxs("span",{className:"fr-pending",children:[d(J,"slot")," still on the fleet estimate — pour-measure each pump from its Tools tab"]}),e.jsx("div",{className:"fr-kp__actions",children:e.jsx(m,{variant:"primary",disabled:L||_===0,"data-testid":"resync-runtimes",onClick:re,children:L?"Re-syncing…":`Re-sync run-times (${_} out of date)`})}),h&&e.jsxs("p",{className:"fr-kp__result",role:"status","data-testid":"resync-result",children:["Applied to ",d(h.summary.applied,"slot"),".",h.summary.pending>0&&e.jsxs(e.Fragment,{children:[" ",d(h.summary.pending,"slot")," will get it on the next check-in."]}),h.summary.needs_dose>0&&e.jsxs(e.Fragment,{children:[" ",d(h.summary.needs_dose,"slot")," has no dose to derive from."]}),h.summary.skipped>0&&e.jsxs(e.Fragment,{children:[" ",d(h.summary.skipped,"slot")," was already up to date."]})]}),W&&e.jsx("p",{className:"fr-kp__error",role:"alert","data-testid":"resync-error",children:W})]})]})})]})]}),e.jsxs(f,{className:"fr-phase",children:[e.jsx("h3",{className:"fr-phase__title",children:"Evening wind-down"}),e.jsx("p",{className:"fr-phase__when",children:"At closing time (plus up to 1 h grace if a customer is mid-session)"}),e.jsxs("ul",{className:"fr-steps",children:[e.jsx(n,{title:"Closes",owner:"Device firmware",kind:"locked",desc:"The block flips to INACTIVE at closing time. A live payment session gets a one-hour grace peri
1od before the switch."}),e.jsx(n,{title:"Hand sensor parks",owner:"Device firmware",kind:"locked",desc:"The infrared LED is switched off for the night — a closed block can’t sell, so the sensor stops burning battery (~20 mAh/day saved)."}),e.jsx(n,{title:"Heartbeats slow to hourly",owner:"Device firmware",kind:"locked",desc:"Cadence drops to one check-in per hour to save power while closed."})]})]}),e.jsxs(f,{className:"fr-phase",children:[e.jsx("h3",{className:"fr-phase__title",children:"Overnight"}),e.jsx("p",{className:"fr-phase__when",children:"While closed"}),e.jsxs("ul",{className:"fr-steps",children:[e.jsx(n,{title:"Stays reachable",owner:"Device firmware",kind:"locked",desc:"Closed is a business state, not a connectivity state: the block stays cloud-connected on hourly heartbeats so refills, tools and schedule pushes keep working all night. (Deep sleep was deliberately removed.)"}),e.jsx(n,{title:"Midnight reset",owner:"Device firmware",kind:"locked",desc:"At local midnight the block zeroes its daily reboot counter and publishes a reset event — bookkeeping only, no motor action, and the operator-set mode is preserved."}),e.jsx(n,{title:"Cloud automation",owner:"Cloud loops",kind:"always-on",desc:"The reactive loop keeps running every 15 minutes (dispatch, escalation, replenishment) — it never auto-acts on money or hardware. The nightly money-ledger sweep runs at 7 AM."})]})]})]}),e.jsx("p",{className:"fr-footnote",children:"Honesty note: setting a block’s mode by hand (device tools) pauses its automatic open/close schedule for up to 24 hours — that is firmware behavior, shown here so an overridden block isn’t mistaken for a broken schedule."}),e.jsxs(f,{className:"fr-pager","data-testid":"ops-pager-card",children:[e.jsx("h3",{className:"fr-phase__title",children:"Pager"}),e.jsx("p",{className:"fr-phase__when",children:"One daily summary at 7:00 AM Pacific, plus one hourly roll-up only when that hour had a critical."}),e.jsxs("p",{className:"fr-step__desc",children:["Two transports, either or both: ",e.jsx("code",{children:"OPS_CHAT_SPACES"})," (the Block Ops Chat app posts into your spaces and answers",e.jsx("code",{children:"/status"}),") and ",e.jsx("code",{children:"OPS_ALERT_WEBHOOK"})," (an ntfy topic, a Slack hook, a Chat space webhook). Individual alerts are recorded in the audit log and reach the phone through the digests. This button is the one immediate push, so the channel is proven rather than assumed."]}),e.jsxs("div",{className:"fr-kp",children:[e.jsx(m,{variant:"secondary",onClick:se,disabled:R,children:R?"Sending…":"Send test alert"}),y&&(y.sent?e.jsxs("span",{className:"fr-pill fr-pill--on","data-testid":"pager-result",children:["Delivered via ",(y.channels||[]).join(", ")," — check your phone"]}):e.jsx("span",{className:"fr-pill fr-pill--off","data-testid":"pager-result",children:y.webhookConfigured||y.chatConfigured?"A transport is configured but nothing delivered — see the function logs":"Nothing delivered — set OPS_CHAT_SPACES and/or OPS_ALERT_WEBHOOK, then redeploy"})),F&&e.jsx("span",{className:"fr-pending","data-testid":"pager-result",children:F})]})]}),e.jsxs(f,{children:[e.jsx("h3",{className:"fr-phase__title",children:"Per-block state"}),u.length===0?e.jsx(Y,{title:"No deployed blocks",description:"Blocks in storage or retired don’t run a daily routine."}):e.jsx("div",{className:"fr-table-wrap",children:e.jsxs("table",{className:"fr-table","data-testid":"fleet-routines-blocks",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Block"}),e.jsx("th",{children:"Location"}),e.jsx("th",{children:"Hours"}),e.jsx("th",{children:"Status"}),e.jsx("th",{children:"Keep-prime"}),e.jsx("th",{children:"Last seen"})]})}),e.jsx("tbody",{children:u.map(s=>e.jsxs("tr",{"data-testid":`routine-row-${s.serial}`,children:[e.jsxs("td",{children:[e.jsx("span",{className:"fr-block-name",children:s.name}),e.jsx("span",{className:"fr-serial",children:s.serial})]}),e.jsx("td",{children:s.location_name||"—"}),e.jsx("td",{children:s.operating_hours?ie(s.operating_hours):"Default (6 AM – 10 PM)"}),e.jsxs("td",{children:[e.jsx("span",{className:`fr-pill ${s.is_online?"fr-pill--on":"fr-pill--off"}`,children:s.is_online?"Online":"Offline"}),s.mode?e.jsx("span",{className:"fr-mode",children:s.mode}):null]}),e.jsx("td",{children:ae(s)}),e.jsx("td",{children:oe(s.last_heartbeat)})]},s.serial))})]})})]}),Z&&e.jsx("div",{className:"fr-confirm",role:"dialog","aria-modal":"true","aria-label":"Enable keep-prime for the fleet",children:e.jsxs(f,{className:"fr-confirm__card",children:[e.jsx("h3",{className:"fr-phase__title",children:"Enable keep-prime for the fleet?"}),e.jsxs("p",{className:"fr-step__desc",children:["This opts every deployed block into ",e.jsx("strong",{children:"scheduled self-dispenses"}),": each pulse consumes product into the cup with no customer present (boost-only, hard-capped at ~12 ml/day per block by the firmware budget). Blocks that are offline right now will pick it up on their next check-in."]}),e.jsxs("label",{className:"fr-confirm__label",htmlFor:"fr-cadence",children:["Cadence (minutes between pulses, ",c.floorMin,"–",c.maxMin,")"]}),e.jsx("input",{id:"fr-cadence",className:"fr-confirm__input",type:"number",min:c.floorMin,max:c.maxMin,value:x??c.defaultMin,onChange:s=>O(s.target.value===""?"":Number(s.target.value))}),v&&e.jsx("p",{className:"fr-kp__error",role:"alert",children:v}),e.jsxs("div",{className:"fr-confirm__actions",children:[e.jsx(m,{variant:"ghost",disabled:j,onClick:()=>N(!1),children:"Cancel"}),e.jsx(m,{variant:"primary",loading:j,disabled:j||!(Number.isInteger(x)&&x>=c.floorMin&&x<=c.maxMin),onClick:()=>U(x),children:"Enable for the fleet"})]})]})})]})}export{je as default};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.