/* charger-test.jsx — laddboxtest per laddbox (TILL_DESIGN_laddboxtest_2026-09-29, SPEC_laddboxtest v2, André 29/9). CtCustomer: kundens block överst i laddboxen (K1–K4, K6). CtMark: märkningarna vid batterivalen i Bil-fliken (K5). CtAdmin: Driftcentralens panel Kund (D1–D4). Skäl och villkorstexter kommer från servern och visas som de är. Ström per box finns inte i mätningen (bara ev_w), så sidan visar uppmätt effekt och den ström HEMS begär, och räknar inte fram ampere. */ (function(){ const {useState,useEffect,useRef}=React; const CT=()=>window.HemsCT; const ORDER_ALL=['max','dynamic','min','stop','restart','battery_conditions','pause','house_only']; const ORDER_BAT=['battery_conditions','pause','house_only']; const LBL={max:'Högsta ström',dynamic:'Ändrad ström',min:'Lägsta ström',stop:'Stopp',restart:'Omstart',bat_probe:'Förbereder batteridelen',battery_conditions:'Batteriet: villkor',pause:'Paus',house_only:'Täck huset',done:'Avslutar'}; const LIM={box:'boxen','säkring':'säkringen',effekttak:'effekttaket',bil_eller_box:'bilen eller boxen'}; const RES={ok:'OK',fail:'Fel',not_tested:'Kunde inte testas'}; const num=(v)=>v!=null&&v!==''&&Number.isFinite(Number(v))?Number(v):null; const dec=(v,d)=>String(Math.round(v*Math.pow(10,d))/Math.pow(10,d)).replace('.',','); const W=(w)=>{ const n=num(w); if(n==null) return null; return Math.abs(n)>=1000?dec(n/1000,1)+' kW':Math.round(n)+' W'; }; const A=(a)=>{ const n=num(a); return n==null?null:dec(n,1)+' A'; }; const S=(s)=>{ const n=num(s); return n==null?null:Math.round(n)+' s'; }; const ph=(n)=>Number(n)===1?'1 fas':`${n} faser`; const pad=(n)=>String(n).padStart(2,'0'); const tm=(sec)=>{ const n=num(sec); if(n==null) return '—'; const d=new Date(n*1000); return `${d.getDate()}/${d.getMonth()+1} ${pad(d.getHours())}:${pad(d.getMinutes())}`; }; const hms=(sec)=>{ const n=num(sec); if(n==null) return '—'; const d=new Date(n*1000); return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; }; /* v763 (backend 29/9, TILL_DESIGN_laddboxtest_45min): testets längsta tid ur servern. test_max_s i approval, total_max_s i status. Saknas fältet: 45 min. */ const maxMin=(ap)=>{ const s=num(ap&&ap.test_max_s); return s>0?Math.round(s/60):45; }; const mmss=(s)=>{ const n=Math.max(0,Math.round(num(s)||0)); return `${Math.floor(n/60)}:${pad(n%60)}`; }; /* v761 (TILL_DESIGN_laddboxtest_nedrakning 29/9): tider per steg. Saknas ett fält visas inte delen. response_s null i paus/täck huset = växelriktaren följde aldrig. */ const up=(t)=>t?t[0].toUpperCase()+t.slice(1):t; const tLine=(s)=>[num(s.response_s)!=null&&`svar efter ${S(s.response_s)}`,num(s.stable_s)!=null&&`jämn efter ${S(s.stable_s)}`].filter(Boolean).join(' · ')||null; const bLine=(s)=>{ const p=[s.response_s===null?'inget svar':num(s.response_s)!=null&&`svar efter ${S(s.response_s)}`,num(s.stable_s)!=null&&`bedömd efter ${S(s.stable_s)}`].filter(Boolean); return p.length?'Efter sändningen: '+p.join(' · '):null; }; const cmdTxt=(c)=>{ if(!c) return null; const m=/^charge@(\d+(?:\.\d+)?)$/.exec(c); if(m) return dec(Number(m[1]),1)+' A'; return c==='stop'?'stopp':c; }; const bcmdTxt=(c)=>{ if(!c) return null; const m=/^(pause|selfconsumption)@(-?\d+(?:\.\d+)?)$/.exec(c); if(!m) return c; return m[1]==='pause'?'paus':'täck huset '+dec(Number(m[2]),2)+' kW'; }; const loadsTxt=(l)=>l&&typeof l==='object'&&Object.keys(l).length?Object.entries(l).map(([k,v])=>`${k} ${W(v)}`).join(', '):null; const stTxt=(st,n)=>st==='approved'?`Godkänd på ${ph(n)}`:st==='not_approved'?`Ej godkänd på ${ph(n)}`:`${ph(n)}: ej testad`; const stCls=(st)=>st==='approved'?'ok':st==='not_approved'?'bad':''; const rCls=(r)=>r==='ok'?'ok':r==='fail'?'bad':'nt'; const msgOf=(e,fb)=>e&&e.message&&!/^HTTP \d+$/.test(e.message)?e.message:fb; const statusIn=(ap,n)=>n===num(ap.phases)?(ap.status||'untested'):((ap.other_phases||{})[String(n)]||'untested'); const firstFail=(steps)=>{ const s=steps||{}; for(const k of ORDER_ALL){ if(s[k]&&s[k].result==='fail') return s[k].reason||null; } return null; }; function Btn({skin,pri,wide,children,onClick,disabled}){ return ; } /* v758 (André 29/9: "Gör denna snyggare. ser väldigt konstig ut"): kundens status som rubrik med fasläget som etikett, samma rad om testet som i Bil-fliken. */ function CtHero({ap,n,res}){ const st=statusIn(ap,n), o=n===1?3:1, ost=statusIn(ap,o), e=res&&res.by_phases&&res.by_phases[String(n)], r=e&&e.last_result; const why=st==='not_approved'&&r?firstFail(r.steps):null, grace=st==='untested'?num(ap.grace_days_left):null; const title=st==='approved'?'Laddboxen är godkänd':st==='not_approved'?'Laddboxen är inte godkänd':'Laddboxen är inte testad'; const days=(g)=>`${g} ${g===1?'dag':'dagar'}`; /* v759 (André 29/9: "Den behöver vara tydligare att detta är något som behöver göras. nu ser det ut som en vanlig inställning"): otestad box som en uppgift. */ if(st==='untested') return
Testa din laddbox{`Inte testad på ${ph(n)}`}{grace>0?{days(grace)} kvar:ap.gate===true?Fast ström:null}
{(ost==='approved'||ost==='not_approved')&&
{stTxt(ost,o)}
}
HEMS provar laddboxen med din bil: högsta och lägsta ström, ändrad ström, stopp och omstart, och om batteriet kan pausas medan bilen laddar. Testet tar upp till {maxMin(ap)} minuter.
{grace>0&&
Testa inom {days(grace)}, annars laddar HEMS bara på fast ström.
} {!(grace>0)&&ap.gate===true&&
HEMS laddar på fast ström och startar/stoppar bara tills boxen är godkänd.
}
; return
{title}{ph(n)}
{(ost==='approved'||ost==='not_approved')&&
{stTxt(ost,o)}
} {st==='untested'&&
HEMS provar laddboxen med din bil: högsta och lägsta ström, ändrad ström, stopp och omstart, och om batteriet kan pausas medan bilen laddar. Testet tar upp till {maxMin(ap)} minuter.
} {why&&
{why}
} {st!=='untested'&&r&&num(r.finished_at)!=null&&
Testad {tm(r.finished_at)} av {r.started_by&&r.started_by.kind==='support'?'support':'dig'}
} {grace>0&&
Testa inom {grace} dagar, annars laddar HEMS bara på fast ström.
} {st!=='approved'&&!(grace>0)&&ap.gate===true&&
HEMS laddar på fast ström och startar/stoppar bara tills boxen är godkänd.
}
; } /* K4: stegens värden. Saknas ett fält visas inte raden (spec §4: fält kan saknas när steget inte hann mäta). */ function stepLines(k,s,set){ const L=[], add=(t,cls)=>{ if(t) L.push({t,cls}); }; if(!s) return L; if((s.result==='fail'||s.result==='not_tested')&&s.reason) add(s.reason,s.result==='fail'?'bad':''); if(k==='max'&&s.result==='ok'&&num(s.measured_a)!=null) add(`Högsta ${A(s.measured_a)}${num(s.measured_w)!=null?` (${W(s.measured_w)})`:''}${s.limited_by?`, begränsad av: ${LIM[s.limited_by]||s.limited_by}`:''}`); if(k==='max') add(up(tLine(s))); if(k==='dynamic'){ const t=tLine(s); if(t) add(s.result==='ok'&&num(s.target_a)!=null?`Till ${A(s.target_a)}: ${t}`:up(t)); const pu=num(s.phases_used), pc=num(set&&set.phases); if(pu&&pc&&pu!==pc) add(`Bilen laddar på ${ph(pu)}. Din inställning säger ${ph(pc)}.`,'note'); } if(k==='min'){ if(num(s.measured_min_a)!=null) add(`Lägsta ström ${A(s.measured_min_a)}${num(s.floor_a)!=null?` (golv ${A(s.floor_a)})`:''}`); if(Array.isArray(s.levels)) s.levels.forEach(v=>{ const t=v&&tLine(v); if(t&&num(v.a)!=null) add(`${A(v.a)}: ${t}`); }); if(num(s.stopped_at_a)!=null&&num(s.measured_min_a)!=null) add(`Bilen slutade ladda på ${A(s.stopped_at_a)} — lägsta som fungerar är ${A(s.measured_min_a)}`,'note'); if(s.resumed===false) add('Bilen återupptog inte laddningen','note'); if(num(s.switched_to_1ph_at_a)!=null) add(`Boxen växlade till 1 fas vid ${A(s.switched_to_1ph_at_a)}${num(s.measured_min_w)!=null?` — lägsta effekt ${W(s.measured_min_w)}`:''}`,'find'); } if(k==='stop'&&s.result==='ok'&&num(s.time_s)!=null) add(`Stannade efter ${S(s.time_s)}`); if(k==='restart'){ const t=tLine({response_s:num(s.response_s)!=null?s.response_s:s.time_s,stable_s:s.stable_s}); if(t) add(s.result==='ok'&&num(s.requested_a)!=null?`På ${A(s.requested_a)}: ${t}`:up(t)); if(s.current_check==='not_tested') add('Strömkontrollen kunde inte testas',''); } if(k==='battery_conditions'&&Array.isArray(s.failed)) s.failed.forEach(f=>add(f&&f.text,'note')); if(k==='pause'&&s.result==='ok'){ const p=[num(s.discharge_w)!=null&&`Urladdning ${W(s.discharge_w)}`,num(s.grid_w)!=null&&`nät ${W(s.grid_w)}`,num(s.house_w)!=null&&`hus ${W(s.house_w)}`,num(s.ev_w)!=null&&`bil ${W(s.ev_w)}`,num(s.grid_charge_w)!=null&&`laddning från nätet ${W(s.grid_charge_w)}`].filter(Boolean); if(p.length) add(p.join(' · ')); } if(k==='house_only'){ if(num(s.discharge_w)!=null&&num(s.target_w)!=null) add(`Urladdning ${W(s.discharge_w)} mot hus − sol ${W(s.target_w)}`); const lt=loadsTxt(s.loads_w); if(lt&&num(s.house_w)!=null) add(`Hus ${W(s.house_w)}, varav ${lt}`); } if((k==='pause'||k==='house_only')&&s.result!=='not_tested'){ const sw=num(s.send_wait_s); if(sw!=null) add(sw>0?`Väntade ${S(sw)} på växelriktarens spärr`:'Skickades direkt'); add(bLine(s)); } return L; } /* K1: status för boxens fasläge stort, det andra läget litet om det är testat. */ function CtStatus({ap,n,res}){ const st=statusIn(ap,n), o=n===1?3:1, ost=statusIn(ap,o), e=res&&res.by_phases&&res.by_phases[String(n)]; const why=st==='not_approved'&&e&&e.last_result?firstFail(e.last_result.steps):null, grace=st==='untested'?num(ap.grace_days_left):null; return
{stTxt(st,n)}{(ost==='approved'||ost==='not_approved')&&{stTxt(ost,o)}}
{why&&

{why}

} {grace>0&&

Testa inom {grace} dagar, annars laddar HEMS bara på fast ström.

} {st!=='approved'&&!(grace>0)&&ap.gate===true&&

HEMS laddar på fast ström och startar/stoppar bara tills boxen är godkänd.

}
; } /* K2 och D4: ruta före start. */ function CtAsk({scope,err,busy,onStart,onClose,skin,adm,mins}){ return
{adm?

Testet styr kundens laddbox och batteri i upp till {mins||45} minuter. Kunden ser att support testar.

:<>

Koppla in bilen. Den får inte vara fulladdad.

Testet tar upp till {mins||45} minuter och laddar in ungefär 1–3 kWh.

HEMS styr laddboxen och batteriet under testet. Du kan avbryta när som helst.

} {scope==='battery'&&

Bara batteridelen: paus och täck huset medan bilen laddar.

} {err&&

{err}

}
{busy?'Startar…':'Starta testet'}Avbryt
; } /* K3 och D2: förloppet. */ /* v761: fasen i steget som pågår. Serverns text som den är, tid kvar räknad ned lokalt varje sekund från svaret (nästa svar skriver över), stannar på 0:00 tills servern dömt. send = batterikommandot väntar på växelriktarens spärr. */ function CtPhase({st,ago}){ const p=st.phase; if(!p) return null; const max=num(st.phase_max_s), l0=num(st.phase_left_s), left=l0==null?null:Math.max(0,l0-ago), send=p==='send'; return
{st.phase_label||''}{left!=null&&{send?`om ${mmss(left)}`:`${mmss(left)} kvar`}} {max>0&&left!=null&&}
; } function CtProgress({st,onCancel,cancelLbl,busy,skin,who}){ const order=st.scope==='battery'?ORDER_BAT:ORDER_ALL, steps=st.steps||{}, cur=st.step, l=st.last||{}; const [,setTick]=useState(0); useEffect(()=>{ const iv=setInterval(()=>setTick(x=>x+1),1000); return ()=>clearInterval(iv); },[]); const ago=st._rx?Math.max(0,Math.floor((Date.now()-st._rx)/1000)):0; const bat=cur==='pause'||cur==='house_only'; const meas=[num(l.ev_w)!=null&&`Bilen laddar ${W(l.ev_w)}`,l.cmd==='stop'?'HEMS har skickat stopp':cmdTxt(l.cmd)&&`HEMS begär ${cmdTxt(l.cmd)}`,bat&&num(l.battery_w)!=null&&`batteri ${W(l.battery_w)}`,cur==='house_only'&&num(l.house_w)!=null&&`hus ${W(l.house_w)}`].filter(Boolean).join(' · '); return
{st.step_label||'Testet startar'}{mmss((num(st.elapsed_s)||0)+ago)} av högst {mmss(num(st.total_max_s)>0?st.total_max_s:2700)}
{who&&
Startat av {who}
} {!('phase' in st)&&num(st.battery_wait_s)>0&&

Väntar på växelriktarens spärr, {mmss(st.battery_wait_s)} kvar

}
    {order.map(k=>{ const r=steps[k], on=!r&&k===cur; return
  1. {LBL[k]}{r?(RES[r.result]||r.result):on?'pågår':'väntar'}{on&&}{on&&meas&&{meas}}
  2. ; })}
{onCancel&&
{st.cancel_requested?'Avbryter…':cancelLbl}
}
; } /* K4 och D3: senaste avgörande körningen steg för steg. */ function CtResult({e,who,skin,onBattery,busy}){ const r=e&&e.last_result, a=e&&e.last_attempt; if(!r&&!a) return null; const aborted=a&&a.outcome==='aborted'&&(!r||num(a.finished_at)>num(r.finished_at)); const steps=(r&&r.steps)||{}, order=r&&r.scope==='battery'?ORDER_BAT:ORDER_ALL; const needBat=!!r&&ORDER_BAT.some(k=>steps[k]&&steps[k].result==='not_tested'); return
{aborted&&

Senaste försöket {tm(a.finished_at)} · {who(a.started_by)} avbröts{a.abort_reason?`: ${a.abort_reason}`:'.'}

} {r&&
Avgörande test {tm(r.finished_at)} · {who(r.started_by)}
} {r&&}
; } /* Poll var 5:e s medan en körning pågår (spec), annars inte. */ function useCtRun(api,cid,ap,onDone){ const [st,setSt]=useState(null); const running=st?!!st.running:!!(ap&&ap.running); useEffect(()=>{ if(!running||!cid) return; let on=true; const go=async()=>{ try{ const s=await api.status(cid); if(!on) return; if(s&&s.running) setSt({...s,_rx:Date.now()}); else { setSt({running:false}); onDone&&onDone(); } }catch(_){} }; go(); const iv=setInterval(go,5000); return ()=>{ on=false; clearInterval(iv); }; },[running,cid]); return [st,setSt,running]; } function CtCustomer({charger:c,approval}){ /* Identitetsläget (3/10): en ny box har inget gammalt id (sidan skapar inga). Då går anropet med boxens slot, samma form som Driftcentralen redan använder (ev_1, ev_2). Frågan till backend står i BACKEND_BEHOV. */ const cid=c&&(c.id||c.slot), feat=CT().demo()||approval!==undefined, api=CT().customer; const phUi=Number(c&&c.phases)===1?1:3, phUiRef=useRef(phUi); phUiRef.current=phUi; const ph0=useRef(phUi); const [res,setRes]=useState(null), [rs,setRs]=useState('idle'), [ask,setAsk]=useState(null), [busy,setBusy]=useState(false), [open,setOpen]=useState(false); const loadRes=async()=>{ if(!cid) return null; setRs(x=>x==='idle'?'busy':x); try{ const r=await api.result(cid); setRes(r); setRs('ok'); return r; }catch(e){ setRs(e&&e.status===404?'none':'err'); return null; } }; useEffect(()=>{ if(!feat) return; setRes(null); setRs('idle'); loadRes(); const f=()=>{ ph0.current=phUiRef.current; loadRes(); }; window.addEventListener('hems:settings-saved',f); return ()=>window.removeEventListener('hems:settings-saved',f); },[cid,feat]); const ap=(res&&res.approval)||approval||null; const [st,setSt,running]=useCtRun(api,cid,ap,()=>{ loadRes(); setOpen(true); }); if(!feat) return null; if(!ap) return rs==='none'||rs==='err'?
Laddboxtest
{rs==='none'?'Spara laddboxen först, sedan kan du testa den.':'Kunde inte hämta laddboxtestet just nu.'}
:null; const unsaved=phUi!==ph0.current, n=unsaved?phUi:(num(ap.phases)||phUi); const e=res&&res.by_phases&&res.by_phases[String(n)], has=!!(e&&(e.last_result||e.last_attempt)); const bySup=running&&(st&&st.running?(st.started_by&&st.started_by.kind==='support'):(ap.running&&ap.running.by==='support')); const start=async(scope)=>{ setBusy(true); setAsk(a=>({...(a||{}),scope,err:null})); try{ await api.start(cid,scope); setAsk(null); setSt({running:true,scope,step:null,steps:{}}); }catch(er){ setAsk({scope,err:msgOf(er,'Kunde inte starta testet.')}); } setBusy(false); }; const cancel=async()=>{ setBusy(true); try{ await api.cancel(cid); setSt(s=>({...(s||{running:true,steps:{}}),cancel_requested:true})); }catch(_){ setSt(null); loadRes(); } setBusy(false); }; const who=(sb)=>sb&&sb.kind==='support'?'support':'du'; const st0=statusIn(ap,n); return
{bySup&&
Support testar din laddbox just nu{st&&st.cancel_requested?'Avbryter…':'Avbryt'}
} {running&&(st&&st.running&&st.step?:

Testet startar…

)} {!running&&ask&&start(ask.scope)} onClose={()=>setAsk(null)}/>} {!running&&!ask&&setAsk({scope:'all'})}>{has?'Testa igen':'Testa laddboxen'}} {!running&&!ask&&unsaved&&

Spara först, så testar HEMS på {ph(phUi)}.

} {!running&&has&&
{open&&setAsk({scope:'battery'})}/>}
}
; } /* K5: märkningen vid batterivalen i Bil-fliken. Informerar, spärrar inte. Kör batteridelen leder till laddboxen i Inställningar (André 29/9). */ function CtMark({ap,kind}){ if(!ap) return null; const m=(kind==='pause'?ap.pause_mark:ap.house_only_mark)||'not_tested'; if(m==='ok') return null; if(m==='fail') return
{kind==='pause'?'Fungerar inte med din växelriktare.':'Fungerar inte med din växelriktare — HEMS pausar batteriet i stället.'}
; const href='settings.html'+(CT().demo()?'?laddtest=demo':'')+'#grp-ev'; return
Ej testad än. e.stopPropagation()} className="font-semibold text-sky-300 hover:text-sky-200">Kör batteridelen
; } /* v756 (André 29/9: "Att testet inte är kört behöver synas på Bil fliken också. Och länka till inställningar. När testet är kört så ska knappen inte finnas"): rutan i Bil-fliken när boxen är otestad i sitt fasläge. Godkänd eller ej godkänd = testet är kört, då visas ingenting. Pågår ett test visas ingenting heller. */ function CtBilNote({ap}){ if(!ap||ap.running||(ap.status||'untested')!=='untested') return null; const n=num(ap.phases), grace=num(ap.grace_days_left), href='settings.html'+(CT().demo()?'?laddtest=demo':'')+'#grp-ev', days=(g)=>`${g} ${g===1?'dag':'dagar'}`; return
Testa din laddbox{`Inte testad på ${ph(n||3)}`} {grace>0?{days(grace)} kvar:ap.gate===true?Fast ström:null}
HEMS provar laddboxen med din bil: högsta och lägsta ström, ändrad ström, stopp och omstart, och om batteriet kan pausas medan bilen laddar. Testet tar upp till {maxMin(ap)} minuter.
{grace>0?
Testa inom {days(grace)}, annars laddar HEMS bara på fast ström.
:ap.gate===true?
HEMS laddar på fast ström och startar/stoppar bara tills boxen är godkänd.
:null} e.stopPropagation()} className="mt-3 flex min-h-[44px] items-center justify-center gap-1.5 rounded-lg bg-sky-400 px-3 text-sm font-bold text-slate-950 no-underline hover:bg-sky-300">Testa laddboxen i Inställningar
; } /* D1–D4. Driftcentralen känner inte kundens laddar-id, så boxarna hämtas med de positionsid servern tar emot (ev_1, ev_2; högst två boxar). 404 med egen text = boxen finns inte. 404 utan text = anropet saknas hos servern. */ const mkWho=(nm)=>(sb)=>!sb?'—':sb.kind==='customer'?'kunden':'support'+(sb.admin?` (${nm(sb.admin)})`:''); function CtLog({rows}){ const [open,setOpen]=useState(false); return
{open&&
{rows.map((r,i)=>)}
TidStegTill boxenBilBatteriHusSolNätTill batterietLaster
{hms(r.t)}{LBL[r.step]||r.step}{cmdTxt(r.cmd)||'—'}{W(r.ev_w)||'—'}{W(r.battery_w)||'—'}{W(r.house_w)||'—'}{W(r.solar_w)||'—'}{W(r.grid_w)||'—'}{bcmdTxt(r.battery_cmd)||'—'}{loadsTxt(r.loads_w)||'—'}
}
; } function CtHist({rows,who}){ return
Historik
{rows.map((r,i)=>
{tm(r.finished_at||r.started_at)}{who(r.started_by)} · {r.scope==='battery'?'batteridelen':'hela testet'} · {r.outcome==='aborted'?`avbrutet${r.abort_reason?': '+r.abort_reason:''}`:'klart'}
)}
; } /* Namnet ur Driftcentralens boxlista (backend 4/10): GET /api/admin/customers/{code}/settings bär ev_chargers [{name, slot, uid}] på toppnivå. Testets result har inget namn; uid matchas mot listan. name null = ej döpt. */ const ctNameOf=(list,res)=>{ if(!Array.isArray(list)||!res||!res.uid) return null; const b=list.find(x=>x&&x.uid===res.uid); return b&&b.name?String(b.name):null; }; function CtAdminBox({code,env,cid,n,res0,who,list}){ const api=CT().admin(code,env); const [res,setRes]=useState(res0), [ask,setAsk]=useState(null), [busy,setBusy]=useState(false), [err,setErr]=useState(null); const loadRes=async()=>{ try{ const r=await api.result(cid); setRes(r); return r; }catch(_){ return null; } }; const ap=res&&res.approval; const [st,setSt,running]=useCtRun(api,cid,ap,loadRes); const phN=num(res&&res.phases)||num(ap&&ap.phases)||3, e=res&&res.by_phases&&res.by_phases[String(phN)], last=e&&(e.last_attempt||e.last_result); const start=async(scope)=>{ setBusy(true); setAsk(a=>({...(a||{}),scope,err:null})); try{ await api.start(cid,scope); setAsk(null); setSt({running:true,scope,step:null,steps:{}}); }catch(er){ setAsk({scope,err:msgOf(er,'Kunde inte starta testet.')}); } setBusy(false); }; const cancel=async()=>{ setBusy(true); setErr(null); try{ await api.cancel(cid); setSt(s=>({...(s||{running:true,steps:{}}),cancel_requested:true})); }catch(er){ setErr(msgOf(er,'Kunde inte avbryta.')); setSt(null); loadRes(); } setBusy(false); }; return
{/* D7 (identitetsläget 3/10): namn, slot och de fyra första tecknen i uid. Namnet ur boxlistan på uid (4/10). */}
{(res&&(res.charger_name||res.name))||ctNameOf(list,res)||`Laddbox ${n}`}{cid}{res&&res.uid?` · ${String(res.uid).slice(0,4)}`:''}
{ap&&} {last&&
Senaste körning {tm(last.finished_at)} · {who(last.started_by)} · {last.outcome==='aborted'?'avbruten':'klar'}
} {running&&(st&&st.running&&st.step?:

Testet startar…

)} {err&&

{err}

} {!running&&ask&&start(ask.scope)} onClose={()=>setAsk(null)}/>} {!running&&!ask&&
setAsk({scope:'all'})}>Testa laddboxsetAsk({scope:'battery'})}>Kör batteridelen
} {!running&&e&&} {!running&&e&&e.last_attempt&&Array.isArray(e.last_attempt.log)&&e.last_attempt.log.length>0&&} {e&&Array.isArray(e.history)&&e.history.length>0&&}
; } function CtAdmin({t,list}){ const code=(t.customer&&t.customer.code)||t.code, env=/^PROD-/.test(String(t.id||''))||t.env==='prod'?'prod':'staging'; const [boxes,setBoxes]=useState(null), [names,setNames]=useState({}); useEffect(()=>{ let on=true; const TK=window.HemsTickets; if(TK&&TK.admins) TK.admins().then(l=>{ if(!on) return; const m={}; (l||[]).forEach(a=>{ m[a.id]=a.name; }); setNames(m); }).catch(()=>{}); return ()=>{ on=false; }; },[]); const who=mkWho((id)=>names[id]||id); useEffect(()=>{ let on=true; setBoxes(null); (async()=>{ const api=CT().admin(code,env), out=[]; let missing=0, bad=0; for(const [i,cid] of ['ev_1','ev_2'].entries()){ try{ out.push({cid,n:i+1,res:await api.result(cid)}); }catch(e){ if(e&&e.status===404){ if(!e.message||/^(HTTP 404|Not Found)$/i.test(e.message)) missing++; } else bad++; } } if(on) setBoxes(out.length?out:missing===2?'missing':bad?'err':[]); })(); return ()=>{ on=false; }; },[code,env]); return
Laddboxtest
{boxes===null&&
Hämtar…
} {boxes==='missing'&&
Väntar på backend
} {boxes==='err'&&
Kunde inte hämta laddboxtestet.
} {Array.isArray(boxes)&&boxes.length===0&&
Kunden har ingen laddbox.
} {Array.isArray(boxes)&&boxes.map(b=>)}
; } Object.assign(window,{CtCustomer,CtMark,CtBilNote,CtAdmin}); })();