aufträge und „Alle aktualisieren“: beendete lassen sich ausblenden
User 25.09.: Die Karten waren „immer da“ und ließen sich weder bestätigen noch wegklicken. Beendete Aufträge der KI-Box und das letzte „Alle aktualisieren“ im Homelab haben jetzt einen Knopf „Ausblenden“ (quittiert wird auf dem Server, gilt also auf allen Geräten; das Protokoll behält alles). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0df593c05a
commit
54a3649d5e
@@ -126,6 +126,13 @@ def sammellauf_stand() -> dict:
|
|||||||
return {"sammellauf": sammellauf.aktueller()}
|
return {"sammellauf": sammellauf.aktueller()}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sammellauf/{sl_id}/quittieren")
|
||||||
|
def sammellauf_quittieren(sl_id: str) -> dict:
|
||||||
|
"""Beendetes „Alle aktualisieren“ als gesehen markieren (die Update-Seite blendet es aus)."""
|
||||||
|
from services.homelab import sammellauf
|
||||||
|
return {"ok": sammellauf.quittieren(sl_id)}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/protokoll")
|
@router.get("/protokoll")
|
||||||
def protokoll_lesen(grenze: int = 200, berichte: bool = False) -> dict:
|
def protokoll_lesen(grenze: int = 200, berichte: bool = False) -> dict:
|
||||||
from services.homelab import protokoll
|
from services.homelab import protokoll
|
||||||
|
|||||||
@@ -155,6 +155,12 @@ def cancel(job_id: str) -> dict:
|
|||||||
return {"ok": jobengine.cancel_job(job_id)}
|
return {"ok": jobengine.cancel_job(job_id)}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/jobs/{job_id}/quittieren")
|
||||||
|
def quittieren(job_id: str) -> dict:
|
||||||
|
"""Beendeten Auftrag als gesehen markieren (die Oberfläche blendet ihn aus)."""
|
||||||
|
return {"ok": jobengine.quittieren(job_id)}
|
||||||
|
|
||||||
|
|
||||||
class RoleReq(BaseModel):
|
class RoleReq(BaseModel):
|
||||||
role: str | None = None
|
role: str | None = None
|
||||||
|
|
||||||
|
|||||||
@@ -89,9 +89,24 @@ def liste() -> list[dict]:
|
|||||||
|
|
||||||
|
|
||||||
def aktueller() -> dict | None:
|
def aktueller() -> dict | None:
|
||||||
"""Der laufende oder, wenn keiner läuft, der letzte Sammellauf."""
|
"""Der laufende oder, wenn keiner läuft, der letzte Sammellauf — solange ihn niemand quittiert hat (seit
|
||||||
|
25.09.2026: vorher stand der letzte für immer auf der Update-Seite)."""
|
||||||
alle = _alle()
|
alle = _alle()
|
||||||
return oeffentlich(next((x for x in reversed(alle) if x.get("status") == "laeuft"), alle[-1] if alle else None))
|
if laufend := next((x for x in reversed(alle) if x.get("status") == "laeuft"), None):
|
||||||
|
return oeffentlich(laufend)
|
||||||
|
letzter = alle[-1] if alle else None
|
||||||
|
return None if not letzter or letzter.get("quittiert") else oeffentlich(letzter)
|
||||||
|
|
||||||
|
|
||||||
|
def quittieren(sl_id: str) -> bool:
|
||||||
|
"""Einen beendeten Sammellauf als gesehen markieren; das Protokoll behält ihn."""
|
||||||
|
with _lock:
|
||||||
|
sl = next((x for x in _alle() if x["id"] == sl_id), None)
|
||||||
|
if not sl or sl.get("status") == "laeuft":
|
||||||
|
return False
|
||||||
|
sl["quittiert"] = True
|
||||||
|
_merken(sl)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def laeuft() -> bool:
|
def laeuft() -> bool:
|
||||||
|
|||||||
@@ -567,6 +567,19 @@ def cancel_job(job_id: str) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def quittieren(job_id: str) -> bool:
|
||||||
|
"""Einen beendeten Auftrag als gesehen markieren — die Oberfläche blendet ihn dann aus (seit 25.09.2026;
|
||||||
|
vorher blieben beendete Aufträge 24 Stunden stehen, ohne dass man sie wegklicken konnte)."""
|
||||||
|
with _LOCK:
|
||||||
|
_laden()
|
||||||
|
job = JOBS.get(job_id)
|
||||||
|
if not job or job["state"] not in _ENDE:
|
||||||
|
return False
|
||||||
|
job["quittiert"] = True
|
||||||
|
_speichern(job)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def public_jobs(mit_log: bool = True) -> list[dict]:
|
def public_jobs(mit_log: bool = True) -> list[dict]:
|
||||||
"""Aufträge ohne interne Felder für die API; das Protokoll kommt aus der Datei (Ende)."""
|
"""Aufträge ohne interne Felder für die API; das Protokoll kommt aus der Datei (Ende)."""
|
||||||
with _LOCK:
|
with _LOCK:
|
||||||
|
|||||||
@@ -386,3 +386,14 @@ def test_schnittstellen(homelab):
|
|||||||
fertig = c.get("/api/homelab/sammellauf").json()["sammellauf"]
|
fertig = c.get("/api/homelab/sammellauf").json()["sammellauf"]
|
||||||
assert fertig["status"] == "fertig" and fertig["id"] == antwort.json()["sammellauf"]["id"]
|
assert fertig["status"] == "fertig" and fertig["id"] == antwort.json()["sammellauf"]["id"]
|
||||||
assert "meldung" not in fertig and "dringend" not in fertig
|
assert "meldung" not in fertig and "dringend" not in fertig
|
||||||
|
|
||||||
|
|
||||||
|
def test_quittiert_verschwindet_von_der_update_seite(homelab):
|
||||||
|
"""25.09.2026: Das fertige „Alle aktualisieren“ ließ sich nicht wegklicken."""
|
||||||
|
antwort = sammellauf.starten()
|
||||||
|
assert sammellauf.quittieren(antwort["sammellauf"]["id"]) is False # läuft noch
|
||||||
|
sl = _auf_ende()
|
||||||
|
assert sammellauf.aktueller()["id"] == sl["id"]
|
||||||
|
assert sammellauf.quittieren(sl["id"]) is True
|
||||||
|
assert sammellauf.aktueller() is None
|
||||||
|
assert sammellauf.liste()[0]["quittiert"] is True # das Protokoll behält ihn
|
||||||
|
|||||||
@@ -194,3 +194,16 @@ def test_systemd_start_scheitert_sauber(motor, monkeypatch):
|
|||||||
def test_zeilen_aus_ueberschreibt_mit_wagenruecklauf():
|
def test_zeilen_aus_ueberschreibt_mit_wagenruecklauf():
|
||||||
assert jobengine.zeilen_aus("a\n 1%\r 50%\r100%\nb\r\n") == ["a", "100%", "b"]
|
assert jobengine.zeilen_aus("a\n 1%\r 50%\r100%\nb\r\n") == ["a", "100%", "b"]
|
||||||
assert jobengine.zeilen_aus("läuft\r 7%") == [" 7%"]
|
assert jobengine.zeilen_aus("läuft\r 7%") == [" 7%"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_quittieren_nur_wenn_beendet(motor):
|
||||||
|
"""25.09.2026: Beendete Aufträge standen 24 Stunden auf der Update-Seite, ohne dass man sie wegklicken konnte."""
|
||||||
|
job_id = jobengine.start_job([PY, "-c", "import time; time.sleep(30)"], "Lang")
|
||||||
|
time.sleep(0.2)
|
||||||
|
assert jobengine.quittieren(job_id) is False # läuft noch
|
||||||
|
jobengine.cancel_job(job_id)
|
||||||
|
_warte(job_id)
|
||||||
|
assert jobengine.quittieren(job_id) is True and _oeffentlich(job_id)["quittiert"] is True
|
||||||
|
akte = json.loads((motor / f"{job_id}.json").read_text(encoding="utf-8"))
|
||||||
|
assert akte["quittiert"] is True
|
||||||
|
assert jobengine.quittieren("gibt-es-nicht") is False
|
||||||
|
|||||||
-2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
|||||||
import{i as e,n as t,r as n,s as r,t as i}from"./dist-D4CVSex9.js";import{Bt as a,Dt as o,I as s,Zt as c,jt as l,nt as u}from"./index-DW9Crlr_.js";import{t as d}from"./button-D8rvET0Z.js";import{a as f,i as p,n as m,r as h,t as g}from"./sheet-ClRIvGlH.js";var _=t(),v=r(e(),1),y=n();function b(e){let t=(0,_.c)(4),{unit:n}=e,r=u(n),i;if(r.isPending)i=`Wird gelesen …`;else if(r.isError)i=`Das Protokoll lässt sich gerade nicht lesen: ${r.error.message}`;else if(r.data.ok===!1)i=r.data.err||r.data.text||`Das Protokoll lässt sich nicht lesen.`;else{let e;t[0]===r.data.text?e=t[1]:(e=r.data.text?.trim()||`Noch keine Einträge.`,t[0]=r.data.text,t[1]=e),i=e}let a;return t[2]===i?a=t[3]:(a=(0,y.jsx)(`pre`,{className:`ziffern m-0 max-h-[50vh] w-full basis-full overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap break-all text-text-2`,children:i}),t[2]=i,t[3]=a),a}function x({offen:e,onSchliessen:t}){let n=c(),r=s(),[u,_]=(0,v.useState)(null),[x,S]=(0,v.useState)(null),[C,w]=(0,v.useState)(null);async function T(e){if(!C){S(null),w(e.unit);try{let t=await a(`/api/maintenance/restart`,{service:e.unit});t.ok?o(`erfolg`,`${e.name} ${e.schlaeft?`wird geweckt`:`startet neu`}.`):o(`fehler`,`${e.name} ließ sich nicht starten: ${l(t)}`),n.invalidateQueries({queryKey:[`dienste`]})}catch(e){o(`fehler`,e.message)}finally{w(null)}}}let E;return E=r.isPending?(0,y.jsx)(`p`,{role:`status`,className:`px-4 text-[15px] text-text-2`,children:`Dienste werden gelesen …`}):r.isError&&!r.data?(0,y.jsxs)(`p`,{role:`alert`,className:`px-4 text-[15px] text-rot-text`,children:[`Die Dienste lassen sich gerade nicht lesen: `,r.error.message]}):r.data?.services.length?(0,y.jsx)(`ul`,{className:`m-0 flex list-none flex-col px-4 pb-4`,children:r.data.services.map(e=>(0,y.jsxs)(`li`,{className:`flex flex-wrap items-center justify-between gap-2 border-t border-linie py-2.5`,children:[(0,y.jsxs)(`span`,{className:`flex min-w-0 items-center gap-2.5`,children:[(0,y.jsx)(`span`,{className:i(`size-2.5 shrink-0 rounded-full`,e.ok?`bg-gruen`:e.schlaeft?`bg-text-3`:`bg-rot`),"aria-hidden":!0}),(0,y.jsxs)(`span`,{className:`min-w-0`,children:[(0,y.jsx)(`span`,{className:`block text-[15px]`,children:e.name}),(0,y.jsxs)(`span`,{className:`ziffern text-xs text-text-3`,children:[e.unit,` · `,e.ok?`läuft`:e.schlaeft?`schläft (abgeschaltet)`:`antwortet nicht`]})]})]}),(0,y.jsxs)(`span`,{className:`flex flex-wrap gap-2`,children:[(0,y.jsx)(d,{variant:u===e.unit?`info`:`ghost`,size:`sm`,"aria-expanded":u===e.unit,onClick:()=>_(u===e.unit?null:e.unit),children:`Protokoll`}),x===e.unit?(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(d,{variant:`gefahr`,size:`sm`,onClick:()=>T(e),children:e.schlaeft?`Wirklich wecken?`:`Wirklich neu starten?`}),(0,y.jsx)(d,{variant:`ghost`,size:`sm`,onClick:()=>S(null),children:`Nein`})]}):(0,y.jsx)(d,{variant:`outline`,size:`sm`,disabled:C!==null,onClick:()=>S(e.unit),children:C===e.unit?`Startet …`:e.schlaeft?`Wecken`:`Neu starten`})]}),u===e.unit&&(0,y.jsx)(b,{unit:e.unit})]},e.unit))}):(0,y.jsx)(`p`,{className:`px-4 text-[15px] text-text-2`,children:`Keine Dienste gefunden.`}),(0,y.jsx)(g,{open:e,onOpenChange:e=>!e&&t(),children:(0,y.jsxs)(m,{side:`right`,className:`gap-3 overflow-y-auto border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-2xl`,children:[(0,y.jsxs)(p,{children:[(0,y.jsx)(f,{className:`schild text-lg`,children:`Dienste und Protokolle`}),(0,y.jsx)(h,{className:`text-text-2`,children:`Welcher Dienst läuft, was er zuletzt geschrieben hat.`})]}),E]})})}export{x as Dienste};
|
import{i as e,n as t,r as n,s as r,t as i}from"./dist-D4CVSex9.js";import{$t as a,Ht as o,I as s,Nt as c,kt as l,rt as u}from"./index-CHHAAyIc.js";import{t as d}from"./button-D8rvET0Z.js";import{a as f,i as p,n as m,r as h,t as g}from"./sheet-BwqJxN7_.js";var _=t(),v=r(e(),1),y=n();function b(e){let t=(0,_.c)(4),{unit:n}=e,r=u(n),i;if(r.isPending)i=`Wird gelesen …`;else if(r.isError)i=`Das Protokoll lässt sich gerade nicht lesen: ${r.error.message}`;else if(r.data.ok===!1)i=r.data.err||r.data.text||`Das Protokoll lässt sich nicht lesen.`;else{let e;t[0]===r.data.text?e=t[1]:(e=r.data.text?.trim()||`Noch keine Einträge.`,t[0]=r.data.text,t[1]=e),i=e}let a;return t[2]===i?a=t[3]:(a=(0,y.jsx)(`pre`,{className:`ziffern m-0 max-h-[50vh] w-full basis-full overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap break-all text-text-2`,children:i}),t[2]=i,t[3]=a),a}function x({offen:e,onSchliessen:t}){let n=a(),r=s(),[u,_]=(0,v.useState)(null),[x,S]=(0,v.useState)(null),[C,w]=(0,v.useState)(null);async function T(e){if(!C){S(null),w(e.unit);try{let t=await o(`/api/maintenance/restart`,{service:e.unit});t.ok?l(`erfolg`,`${e.name} ${e.schlaeft?`wird geweckt`:`startet neu`}.`):l(`fehler`,`${e.name} ließ sich nicht starten: ${c(t)}`),n.invalidateQueries({queryKey:[`dienste`]})}catch(e){l(`fehler`,e.message)}finally{w(null)}}}let E;return E=r.isPending?(0,y.jsx)(`p`,{role:`status`,className:`px-4 text-[15px] text-text-2`,children:`Dienste werden gelesen …`}):r.isError&&!r.data?(0,y.jsxs)(`p`,{role:`alert`,className:`px-4 text-[15px] text-rot-text`,children:[`Die Dienste lassen sich gerade nicht lesen: `,r.error.message]}):r.data?.services.length?(0,y.jsx)(`ul`,{className:`m-0 flex list-none flex-col px-4 pb-4`,children:r.data.services.map(e=>(0,y.jsxs)(`li`,{className:`flex flex-wrap items-center justify-between gap-2 border-t border-linie py-2.5`,children:[(0,y.jsxs)(`span`,{className:`flex min-w-0 items-center gap-2.5`,children:[(0,y.jsx)(`span`,{className:i(`size-2.5 shrink-0 rounded-full`,e.ok?`bg-gruen`:e.schlaeft?`bg-text-3`:`bg-rot`),"aria-hidden":!0}),(0,y.jsxs)(`span`,{className:`min-w-0`,children:[(0,y.jsx)(`span`,{className:`block text-[15px]`,children:e.name}),(0,y.jsxs)(`span`,{className:`ziffern text-xs text-text-3`,children:[e.unit,` · `,e.ok?`läuft`:e.schlaeft?`schläft (abgeschaltet)`:`antwortet nicht`]})]})]}),(0,y.jsxs)(`span`,{className:`flex flex-wrap gap-2`,children:[(0,y.jsx)(d,{variant:u===e.unit?`info`:`ghost`,size:`sm`,"aria-expanded":u===e.unit,onClick:()=>_(u===e.unit?null:e.unit),children:`Protokoll`}),x===e.unit?(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(d,{variant:`gefahr`,size:`sm`,onClick:()=>T(e),children:e.schlaeft?`Wirklich wecken?`:`Wirklich neu starten?`}),(0,y.jsx)(d,{variant:`ghost`,size:`sm`,onClick:()=>S(null),children:`Nein`})]}):(0,y.jsx)(d,{variant:`outline`,size:`sm`,disabled:C!==null,onClick:()=>S(e.unit),children:C===e.unit?`Startet …`:e.schlaeft?`Wecken`:`Neu starten`})]}),u===e.unit&&(0,y.jsx)(b,{unit:e.unit})]},e.unit))}):(0,y.jsx)(`p`,{className:`px-4 text-[15px] text-text-2`,children:`Keine Dienste gefunden.`}),(0,y.jsx)(g,{open:e,onOpenChange:e=>!e&&t(),children:(0,y.jsxs)(m,{side:`right`,className:`gap-3 overflow-y-auto border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-2xl`,children:[(0,y.jsxs)(p,{children:[(0,y.jsx)(f,{className:`schild text-lg`,children:`Dienste und Protokolle`}),(0,y.jsx)(h,{className:`text-text-2`,children:`Welcher Dienst läuft, was er zuletzt geschrieben hat.`})]}),E]})})}export{x as Dienste};
|
||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
import{n as e,r as t}from"./dist-D4CVSex9.js";import{g as n,h as r}from"./index-DW9Crlr_.js";import{a as i,i as a,n as o,r as s,t as c}from"./sheet-ClRIvGlH.js";var l=e(),u=t();function d(e){let t=(0,l.c)(9),{onDienste:d,onSchliessen:f}=e,p;t[0]===f?p=t[1]:(p=e=>!e&&f(),t[0]=f,t[1]=p);let m;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(m=(0,u.jsxs)(a,{className:`p-0`,children:[(0,u.jsx)(i,{asChild:!0,children:(0,u.jsx)(`div`,{children:(0,u.jsx)(r,{})})}),(0,u.jsx)(s,{className:`sr-only`,children:`Bereiche und Seiten`})]}),t[2]=m):m=t[2];let h;t[3]!==d||t[4]!==f?(h=(0,u.jsxs)(o,{side:`left`,className:`flex flex-col gap-6 border-linie bg-[#0c0f12] px-4 pt-5 pb-[calc(1.25rem+env(safe-area-inset-bottom))] data-[side=left]:w-[300px]`,children:[m,(0,u.jsx)(n,{onDienste:d,onGewaehlt:f})]}),t[3]=d,t[4]=f,t[5]=h):h=t[5];let g;return t[6]!==p||t[7]!==h?(g=(0,u.jsx)(c,{open:!0,onOpenChange:p,children:h}),t[6]=p,t[7]=h,t[8]=g):g=t[8],g}export{d as MenueSchublade};
|
import{n as e,r as t}from"./dist-D4CVSex9.js";import{g as n,h as r}from"./index-CHHAAyIc.js";import{a as i,i as a,n as o,r as s,t as c}from"./sheet-BwqJxN7_.js";var l=e(),u=t();function d(e){let t=(0,l.c)(9),{onDienste:d,onSchliessen:f}=e,p;t[0]===f?p=t[1]:(p=e=>!e&&f(),t[0]=f,t[1]=p);let m;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(m=(0,u.jsxs)(a,{className:`p-0`,children:[(0,u.jsx)(i,{asChild:!0,children:(0,u.jsx)(`div`,{children:(0,u.jsx)(r,{})})}),(0,u.jsx)(s,{className:`sr-only`,children:`Bereiche und Seiten`})]}),t[2]=m):m=t[2];let h;t[3]!==d||t[4]!==f?(h=(0,u.jsxs)(o,{side:`left`,className:`flex flex-col gap-6 border-linie bg-[#0c0f12] px-4 pt-5 pb-[calc(1.25rem+env(safe-area-inset-bottom))] data-[side=left]:w-[300px]`,children:[m,(0,u.jsx)(n,{onDienste:d,onGewaehlt:f})]}),t[3]=d,t[4]=f,t[5]=h):h=t[5];let g;return t[6]!==p||t[7]!==h?(g=(0,u.jsx)(c,{open:!0,onOpenChange:p,children:h}),t[6]=p,t[7]=h,t[8]=g):g=t[8],g}export{d as MenueSchublade};
|
||||||
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
|||||||
import{n as e,r as t}from"./dist-D4CVSex9.js";import{a as n,i as r,n as i,r as a,t as o}from"./sheet-ClRIvGlH.js";var s=e(),c=t();function l(e){let t=(0,s.c)(14),{offen:l,titel:u,text:d,onSchliessen:f}=e,p;t[0]===f?p=t[1]:(p=e=>!e&&f(),t[0]=f,t[1]=p);let m;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(m=(0,c.jsx)(n,{className:`schild text-lg`,children:`Protokoll`}),t[2]=m):m=t[2];let h;t[3]===u?h=t[4]:(h=(0,c.jsxs)(r,{children:[m,(0,c.jsx)(a,{className:`text-text-2`,children:u})]}),t[3]=u,t[4]=h);let g;t[5]===d?g=t[6]:(g=(0,c.jsx)(`pre`,{className:`ziffern mx-4 mb-4 flex-1 overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap text-text-2`,children:d}),t[5]=d,t[6]=g);let _;t[7]!==h||t[8]!==g?(_=(0,c.jsxs)(i,{side:`right`,className:`border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-2xl`,children:[h,g]}),t[7]=h,t[8]=g,t[9]=_):_=t[9];let v;return t[10]!==l||t[11]!==p||t[12]!==_?(v=(0,c.jsx)(o,{open:l,onOpenChange:p,children:_}),t[10]=l,t[11]=p,t[12]=_,t[13]=v):v=t[13],v}export{l as Protokollfenster};
|
import{n as e,r as t}from"./dist-D4CVSex9.js";import{a as n,i as r,n as i,r as a,t as o}from"./sheet-BwqJxN7_.js";var s=e(),c=t();function l(e){let t=(0,s.c)(14),{offen:l,titel:u,text:d,onSchliessen:f}=e,p;t[0]===f?p=t[1]:(p=e=>!e&&f(),t[0]=f,t[1]=p);let m;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(m=(0,c.jsx)(n,{className:`schild text-lg`,children:`Protokoll`}),t[2]=m):m=t[2];let h;t[3]===u?h=t[4]:(h=(0,c.jsxs)(r,{children:[m,(0,c.jsx)(a,{className:`text-text-2`,children:u})]}),t[3]=u,t[4]=h);let g;t[5]===d?g=t[6]:(g=(0,c.jsx)(`pre`,{className:`ziffern mx-4 mb-4 flex-1 overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap text-text-2`,children:d}),t[5]=d,t[6]=g);let _;t[7]!==h||t[8]!==g?(_=(0,c.jsxs)(i,{side:`right`,className:`border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-2xl`,children:[h,g]}),t[7]=h,t[8]=g,t[9]=_):_=t[9];let v;return t[10]!==l||t[11]!==p||t[12]!==_?(v=(0,c.jsx)(o,{open:l,onOpenChange:p,children:_}),t[10]=l,t[11]=p,t[12]=_,t[13]=v):v=t[13],v}export{l as Protokollfenster};
|
||||||
+2
-2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
import{Kt as e}from"./index-DW9Crlr_.js";var t={name:`chevron-right`,size:24,node:[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]};t.node;var n=e(t);export{n as t};
|
import{Jt as e}from"./index-CHHAAyIc.js";var t={name:`chevron-right`,size:24,node:[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]};t.node;var n=e(t);export{n as t};
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+11
File diff suppressed because one or more lines are too long
-11
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
|||||||
import{Kt as e}from"./index-DW9Crlr_.js";var t={name:`search`,size:24,node:[[`path`,{d:`m21 21-4.34-4.34`,key:`14j7rj`}],[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}]]};t.node;var n=e(t);export{n as t};
|
import{Jt as e}from"./index-CHHAAyIc.js";var t={name:`search`,size:24,node:[[`path`,{d:`m21 21-4.34-4.34`,key:`14j7rj`}],[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}]]};t.node;var n=e(t);export{n as t};
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
import{i as e,n as t,r as n,t as r}from"./dist-D4CVSex9.js";import{Vt as i}from"./index-DW9Crlr_.js";import{t as a}from"./button-D8rvET0Z.js";import{a as o,i as s,n as c,o as l,r as u,s as d,t as f}from"./dist-C9MWG2Tl.js";var p=t();e();var m=n();function h(e){let t=(0,p.c)(4),n;t[0]===e?n=t[1]:({...n}=e,t[0]=e,t[1]=n);let r;return t[2]===n?r=t[3]:(r=(0,m.jsx)(f,{"data-slot":`sheet`,...n}),t[2]=n,t[3]=r),r}function g(e){let t=(0,p.c)(4),n;t[0]===e?n=t[1]:({...n}=e,t[0]=e,t[1]=n);let r;return t[2]===n?r=t[3]:(r=(0,m.jsx)(l,{"data-slot":`sheet-portal`,...n}),t[2]=n,t[3]=r),r}function _(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0`,n),t[3]=n,t[4]=a);let s;return t[5]!==i||t[6]!==a?(s=(0,m.jsx)(o,{"data-slot":`sheet-overlay`,className:a,...i}),t[5]=i,t[6]=a,t[7]=s):s=t[7],s}function v(e){let t=(0,p.c)(17),n,o,s,l,d;t[0]===e?(n=t[1],o=t[2],s=t[3],l=t[4],d=t[5]):({className:o,children:n,side:l,showCloseButton:d,...s}=e,t[0]=e,t[1]=n,t[2]=o,t[3]=s,t[4]=l,t[5]=d);let f=l===void 0?`right`:l,h=d===void 0||d,v;t[6]===Symbol.for(`react.memo_cache_sentinel`)?(v=(0,m.jsx)(_,{}),t[6]=v):v=t[6];let y;t[7]===o?y=t[8]:(y=r(`fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10`,o),t[7]=o,t[8]=y);let b;t[9]===h?b=t[10]:(b=h&&(0,m.jsx)(c,{"data-slot":`sheet-close`,asChild:!0,children:(0,m.jsxs)(a,{variant:`ghost`,className:`absolute top-3 right-3`,size:`icon-sm`,children:[(0,m.jsx)(i,{}),(0,m.jsx)(`span`,{className:`sr-only`,children:`Schließen`})]})}),t[9]=h,t[10]=b);let x;return t[11]!==n||t[12]!==s||t[13]!==f||t[14]!==y||t[15]!==b?(x=(0,m.jsxs)(g,{children:[v,(0,m.jsxs)(u,{"data-slot":`sheet-content`,"data-side":f,className:y,...s,children:[n,b]})]}),t[11]=n,t[12]=s,t[13]=f,t[14]=y,t[15]=b,t[16]=x):x=t[16],x}function y(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`flex flex-col gap-0.5 p-4`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(`div`,{"data-slot":`sheet-header`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}function b(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`font-heading text-base font-medium text-foreground`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(d,{"data-slot":`sheet-title`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}function x(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`text-sm text-muted-foreground`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(s,{"data-slot":`sheet-description`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}export{b as a,y as i,v as n,x as r,h as t};
|
import{i as e,n as t,r as n,t as r}from"./dist-D4CVSex9.js";import{Ut as i}from"./index-CHHAAyIc.js";import{t as a}from"./button-D8rvET0Z.js";import{a as o,i as s,n as c,o as l,r as u,s as d,t as f}from"./dist-BWAe-t7-.js";var p=t();e();var m=n();function h(e){let t=(0,p.c)(4),n;t[0]===e?n=t[1]:({...n}=e,t[0]=e,t[1]=n);let r;return t[2]===n?r=t[3]:(r=(0,m.jsx)(f,{"data-slot":`sheet`,...n}),t[2]=n,t[3]=r),r}function g(e){let t=(0,p.c)(4),n;t[0]===e?n=t[1]:({...n}=e,t[0]=e,t[1]=n);let r;return t[2]===n?r=t[3]:(r=(0,m.jsx)(l,{"data-slot":`sheet-portal`,...n}),t[2]=n,t[3]=r),r}function _(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0`,n),t[3]=n,t[4]=a);let s;return t[5]!==i||t[6]!==a?(s=(0,m.jsx)(o,{"data-slot":`sheet-overlay`,className:a,...i}),t[5]=i,t[6]=a,t[7]=s):s=t[7],s}function v(e){let t=(0,p.c)(17),n,o,s,l,d;t[0]===e?(n=t[1],o=t[2],s=t[3],l=t[4],d=t[5]):({className:o,children:n,side:l,showCloseButton:d,...s}=e,t[0]=e,t[1]=n,t[2]=o,t[3]=s,t[4]=l,t[5]=d);let f=l===void 0?`right`:l,h=d===void 0||d,v;t[6]===Symbol.for(`react.memo_cache_sentinel`)?(v=(0,m.jsx)(_,{}),t[6]=v):v=t[6];let y;t[7]===o?y=t[8]:(y=r(`fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10`,o),t[7]=o,t[8]=y);let b;t[9]===h?b=t[10]:(b=h&&(0,m.jsx)(c,{"data-slot":`sheet-close`,asChild:!0,children:(0,m.jsxs)(a,{variant:`ghost`,className:`absolute top-3 right-3`,size:`icon-sm`,children:[(0,m.jsx)(i,{}),(0,m.jsx)(`span`,{className:`sr-only`,children:`Schließen`})]})}),t[9]=h,t[10]=b);let x;return t[11]!==n||t[12]!==s||t[13]!==f||t[14]!==y||t[15]!==b?(x=(0,m.jsxs)(g,{children:[v,(0,m.jsxs)(u,{"data-slot":`sheet-content`,"data-side":f,className:y,...s,children:[n,b]})]}),t[11]=n,t[12]=s,t[13]=f,t[14]=y,t[15]=b,t[16]=x):x=t[16],x}function y(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`flex flex-col gap-0.5 p-4`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(`div`,{"data-slot":`sheet-header`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}function b(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`font-heading text-base font-medium text-foreground`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(d,{"data-slot":`sheet-title`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}function x(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`text-sm text-muted-foreground`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(s,{"data-slot":`sheet-description`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}export{b as a,y as i,v as n,x as r,h as t};
|
||||||
Vendored
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
<title>Homelab Orchestrator</title>
|
<title>Homelab Orchestrator</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DW9Crlr_.js"></script>
|
<script type="module" crossorigin src="/assets/index-CHHAAyIc.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/dist-D4CVSex9.js">
|
<link rel="modulepreload" crossorigin href="/assets/dist-D4CVSex9.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/useSelector-uUwrspuX.js">
|
<link rel="modulepreload" crossorigin href="/assets/useSelector-uUwrspuX.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/useMatch-CiyhmkMV.js">
|
<link rel="modulepreload" crossorigin href="/assets/useMatch-CiyhmkMV.js">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useJobAbbrechen, useJobs } from "@/lib/abfragen"
|
import { useJobAbbrechen, useJobQuittieren, useJobs } from "@/lib/abfragen"
|
||||||
import { jobTitel, sichtbareJobs } from "@/lib/wartung"
|
import { jobTitel, sichtbareJobs } from "@/lib/wartung"
|
||||||
import { JobKarte } from "./JobKarte"
|
import { JobKarte } from "./JobKarte"
|
||||||
import { Panel } from "./Panel"
|
import { Panel } from "./Panel"
|
||||||
@@ -10,6 +10,7 @@ import { Panel } from "./Panel"
|
|||||||
export function Auftraege({ alsPanel = false }: { alsPanel?: boolean }) {
|
export function Auftraege({ alsPanel = false }: { alsPanel?: boolean }) {
|
||||||
const jobs = useJobs()
|
const jobs = useJobs()
|
||||||
const abbrechen = useJobAbbrechen()
|
const abbrechen = useJobAbbrechen()
|
||||||
|
const quittieren = useJobQuittieren()
|
||||||
const liste = sichtbareJobs(jobs.data?.jobs ?? [])
|
const liste = sichtbareJobs(jobs.data?.jobs ?? [])
|
||||||
|
|
||||||
let inhalt
|
let inhalt
|
||||||
@@ -23,6 +24,7 @@ export function Auftraege({ alsPanel = false }: { alsPanel?: boolean }) {
|
|||||||
ebene={alsPanel ? 3 : 4}
|
ebene={alsPanel ? 3 : 4}
|
||||||
abbrechenGesperrt={abbrechen.isPending}
|
abbrechenGesperrt={abbrechen.isPending}
|
||||||
onAbbrechen={(job) => abbrechen.mutate({ id: job.id, titel: jobTitel(job) })}
|
onAbbrechen={(job) => abbrechen.mutate({ id: job.id, titel: jobTitel(job) })}
|
||||||
|
onQuittieren={(job) => quittieren.mutate(job.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -97,3 +97,16 @@ describe("sichtbareJobs", () => {
|
|||||||
expect(liste.map((j) => j.id)).toEqual(["download", "update", "f4", "f3", "f2"])
|
expect(liste.map((j) => j.id)).toEqual(["download", "update", "f4", "f3", "f2"])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("Ausblenden (seit 25.09.2026)", () => {
|
||||||
|
it("bietet es nur für beendete Aufträge an und blendet quittierte aus", () => {
|
||||||
|
const onQuittieren = vi.fn()
|
||||||
|
const { rerender } = render(<JobKarte job={job({})} onQuittieren={onQuittieren} />)
|
||||||
|
expect(screen.queryByRole("button", { name: "Ausblenden" })).not.toBeInTheDocument()
|
||||||
|
const fertig = job({ state: "failed", returncode: 1, finished_at: JETZT - 60 })
|
||||||
|
rerender(<JobKarte job={fertig} onQuittieren={onQuittieren} />)
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Ausblenden" }))
|
||||||
|
expect(onQuittieren).toHaveBeenCalledWith(fertig)
|
||||||
|
expect(sichtbareJobs([fertig, { ...fertig, id: "j2", quittiert: true }], JETZT).map((j) => j.id)).toEqual(["j1"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -41,9 +41,11 @@ function useJetzt(aktiv: boolean) {
|
|||||||
* die letzte Meldung, auf Wunsch das Protokoll. Abbrechen nur, wo es wirklich stoppt (`abbrechbar`).
|
* die letzte Meldung, auf Wunsch das Protokoll. Abbrechen nur, wo es wirklich stoppt (`abbrechbar`).
|
||||||
* `ebene` ist die Überschriftenebene (unter einem Panel-Titel h3, unter einer Zwischenüberschrift h4).
|
* `ebene` ist die Überschriftenebene (unter einem Panel-Titel h3, unter einer Zwischenüberschrift h4).
|
||||||
*/
|
*/
|
||||||
export function JobKarte({ job, onAbbrechen, abbrechenGesperrt, ebene = 4 }: {
|
export function JobKarte({ job, onAbbrechen, onQuittieren, abbrechenGesperrt, ebene = 4 }: {
|
||||||
job: Job
|
job: Job
|
||||||
onAbbrechen?: (job: Job) => void
|
onAbbrechen?: (job: Job) => void
|
||||||
|
/** Beendete Aufträge lassen sich ausblenden. */
|
||||||
|
onQuittieren?: (job: Job) => void
|
||||||
abbrechenGesperrt?: boolean
|
abbrechenGesperrt?: boolean
|
||||||
ebene?: 3 | 4
|
ebene?: 3 | 4
|
||||||
}) {
|
}) {
|
||||||
@@ -95,7 +97,7 @@ export function JobKarte({ job, onAbbrechen, abbrechenGesperrt, ebene = 4 }: {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(darfAbbrechen || protokollZeilen.length > 1) && (
|
{(darfAbbrechen || protokollZeilen.length > 1 || (!laeuft && onQuittieren)) && (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{darfAbbrechen && (
|
{darfAbbrechen && (
|
||||||
<Button variant="outline" size="sm" disabled={abbrechenGesperrt} onClick={() => setFrage(true)}>
|
<Button variant="outline" size="sm" disabled={abbrechenGesperrt} onClick={() => setFrage(true)}>
|
||||||
@@ -107,6 +109,11 @@ export function JobKarte({ job, onAbbrechen, abbrechenGesperrt, ebene = 4 }: {
|
|||||||
{protokoll ? "Protokoll zu" : "Protokoll"}
|
{protokoll ? "Protokoll zu" : "Protokoll"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{!laeuft && onQuittieren && (
|
||||||
|
<Button variant="outline" size="sm" className="ml-auto" onClick={() => onQuittieren(job)}>
|
||||||
|
Ausblenden
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ const LAUF: Record<Sammellauf["status"], { art: ChipArt; text: string }> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Fortschritt des Sammellaufs: jeder Schritt mit Zustand; fertige Läufe bleiben bis zum nächsten stehen. */
|
/** Fortschritt des Sammellaufs: jeder Schritt mit Zustand; fertige Läufe bleiben bis zum nächsten stehen. */
|
||||||
export function SammellaufFortschritt({ lauf }: { lauf: Sammellauf }) {
|
export function SammellaufFortschritt({ lauf, onQuittieren }: { lauf: Sammellauf; onQuittieren?: () => void }) {
|
||||||
const fertig = lauf.schritte.filter((s) => !["wartet", "laeuft"].includes(s.status)).length
|
const fertig = lauf.schritte.filter((s) => !["wartet", "laeuft"].includes(s.status)).length
|
||||||
const l = LAUF[lauf.status]
|
const l = LAUF[lauf.status]
|
||||||
return (
|
return (
|
||||||
@@ -104,7 +104,16 @@ export function SammellaufFortschritt({ lauf }: { lauf: Sammellauf }) {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</ol>
|
</ol>
|
||||||
{lauf.text && <p className="text-sm text-text-2">{lauf.text}</p>}
|
{(lauf.text || (lauf.status !== "laeuft" && onQuittieren)) && (
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
{lauf.text && <p className="text-sm text-text-2">{lauf.text}</p>}
|
||||||
|
{lauf.status !== "laeuft" && onQuittieren && (
|
||||||
|
<Button variant="outline" size="sm" className="ml-auto" onClick={onQuittieren}>
|
||||||
|
Ausblenden
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,3 +73,19 @@ describe("SammellaufFortschritt", () => {
|
|||||||
expect(screen.getByText(/118 Pakete · wartet/)).toBeInTheDocument()
|
expect(screen.getByText(/118 Pakete · wartet/)).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("SammellaufFortschritt ausblenden", () => {
|
||||||
|
it("zeigt „Ausblenden“ erst, wenn der Lauf fertig ist", () => {
|
||||||
|
const fertig: Sammellauf = {
|
||||||
|
id: "s2", status: "fertig", start: 1_790_000_000, ende: 1_790_001_000, aktuell: null,
|
||||||
|
text: "8 Updates eingespielt, alle Prüfungen grün.",
|
||||||
|
schritte: [{ ziel: "ct-104", name: "Gitea", baustein: "os", was: "73 Pakete", status: "eingespielt", lauf: "l1", text: null }],
|
||||||
|
}
|
||||||
|
const weg = vi.fn()
|
||||||
|
const { rerender } = render(<SammellaufFortschritt lauf={{ ...fertig, status: "laeuft", ende: null }} onQuittieren={weg} />)
|
||||||
|
expect(screen.queryByRole("button", { name: "Ausblenden" })).not.toBeInTheDocument()
|
||||||
|
rerender(<SammellaufFortschritt lauf={fertig} onQuittieren={weg} />)
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Ausblenden" }))
|
||||||
|
expect(weg).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -143,7 +143,8 @@ function useAktion<V, E extends AktionsErgebnis | Record<string, unknown>>(
|
|||||||
const ergebnis = e as AktionsErgebnis
|
const ergebnis = e as AktionsErgebnis
|
||||||
if (ergebnis && ergebnis.ok === false) {
|
if (ergebnis && ergebnis.ok === false) {
|
||||||
melden("fehler", opt.fehler?.(v, ergebnis) ?? fehlerText(ergebnis))
|
melden("fehler", opt.fehler?.(v, ergebnis) ?? fehlerText(ergebnis))
|
||||||
} else {
|
} else if (opt.erfolg(v, e)) {
|
||||||
|
// Leerer Text: keine Erfolgsmeldung (etwa beim Ausblenden — da sieht man es ja).
|
||||||
melden("erfolg", opt.erfolg(v, e))
|
melden("erfolg", opt.erfolg(v, e))
|
||||||
}
|
}
|
||||||
for (const k of opt.neuLaden) qc.invalidateQueries({ queryKey: [k] })
|
for (const k of opt.neuLaden) qc.invalidateQueries({ queryKey: [k] })
|
||||||
@@ -189,6 +190,13 @@ export const useJobAbbrechen = () =>
|
|||||||
neuLaden: ["jobs"],
|
neuLaden: ["jobs"],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** Einen beendeten Auftrag ausblenden (seit 25.09.2026; vorher blieb er 24 Stunden stehen). */
|
||||||
|
export const useJobQuittieren = () =>
|
||||||
|
useAktion((id: string) => post<AktionsErgebnis>(`/api/jobs/${encodeURIComponent(id)}/quittieren`), {
|
||||||
|
erfolg: () => "",
|
||||||
|
neuLaden: ["jobs"],
|
||||||
|
})
|
||||||
|
|
||||||
/** Ein Modell von Hugging Face holen (Hintergrund-Auftrag, mit optionaler Rolle nach dem Download). */
|
/** Ein Modell von Hugging Face holen (Hintergrund-Auftrag, mit optionaler Rolle nach dem Download). */
|
||||||
export const useInstallieren = () =>
|
export const useInstallieren = () =>
|
||||||
useAktion(
|
useAktion(
|
||||||
@@ -414,6 +422,13 @@ export const useAlleAktualisieren = () =>
|
|||||||
neuLaden: ["homelab-sammellauf", "homelab-laeufe", "homelab"],
|
neuLaden: ["homelab-sammellauf", "homelab-laeufe", "homelab"],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** Ein beendetes „Alle aktualisieren“ ausblenden (das Protokoll behält es). */
|
||||||
|
export const useSammellaufQuittieren = () =>
|
||||||
|
useAktion((id: string) => post<AktionsErgebnis>(`/api/homelab/sammellauf/${encodeURIComponent(id)}/quittieren`), {
|
||||||
|
erfolg: () => "",
|
||||||
|
neuLaden: ["homelab-sammellauf"],
|
||||||
|
})
|
||||||
|
|
||||||
export const useHomelabProtokoll = (aktiv: boolean, berichte: boolean) =>
|
export const useHomelabProtokoll = (aktiv: boolean, berichte: boolean) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: ["homelab-protokoll", berichte],
|
queryKey: ["homelab-protokoll", berichte],
|
||||||
|
|||||||
@@ -173,6 +173,8 @@ export interface Job {
|
|||||||
started_at: number
|
started_at: number
|
||||||
finished_at: number | null
|
finished_at: number | null
|
||||||
canceled?: boolean
|
canceled?: boolean
|
||||||
|
/** Vom Nutzer ausgeblendet (seit 25.09.2026). */
|
||||||
|
quittiert?: boolean
|
||||||
/** Nur bei Downloads: Fortschritt 0…100 und die Bytes dazu. */
|
/** Nur bei Downloads: Fortschritt 0…100 und die Bytes dazu. */
|
||||||
progress?: number | null
|
progress?: number | null
|
||||||
total_bytes?: number
|
total_bytes?: number
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export function letzteMeldung(j: Pick<Job, "log">): string | null {
|
|||||||
export function sichtbareJobs(jobs: Job[], jetzt = Date.now() / 1000): Job[] {
|
export function sichtbareJobs(jobs: Job[], jetzt = Date.now() / 1000): Job[] {
|
||||||
const laufend = jobs.filter(laeuftNoch).sort((a, b) => a.started_at - b.started_at)
|
const laufend = jobs.filter(laeuftNoch).sort((a, b) => a.started_at - b.started_at)
|
||||||
const beendet = jobs
|
const beendet = jobs
|
||||||
.filter((j) => !laeuftNoch(j) && jetzt - (j.finished_at ?? j.started_at) < 86_400)
|
.filter((j) => !laeuftNoch(j) && !j.quittiert && jetzt - (j.finished_at ?? j.started_at) < 86_400)
|
||||||
.sort((a, b) => (b.finished_at ?? b.started_at) - (a.finished_at ?? a.started_at))
|
.sort((a, b) => (b.finished_at ?? b.started_at) - (a.finished_at ?? a.started_at))
|
||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
return [...laufend, ...beendet]
|
return [...laufend, ...beendet]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { AlleKnopf, SammellaufFortschritt } from "@/components/homelab/Sammellau
|
|||||||
import { UpdateTabelle } from "@/components/homelab/UpdateTabelle"
|
import { UpdateTabelle } from "@/components/homelab/UpdateTabelle"
|
||||||
import {
|
import {
|
||||||
useHomelab, useHomelabHinweise, useHomelabLaeufe, useHomelabMesswerte, useHomelabSpeicher, useInstanz, usePartner,
|
useHomelab, useHomelabHinweise, useHomelabLaeufe, useHomelabMesswerte, useHomelabSpeicher, useInstanz, usePartner,
|
||||||
useSammellauf,
|
useSammellauf, useSammellaufQuittieren,
|
||||||
useZielAktion,
|
useZielAktion,
|
||||||
} from "@/lib/abfragen"
|
} from "@/lib/abfragen"
|
||||||
import { lage, lampeFuer, offeneUpdates } from "@/lib/homelab"
|
import { lage, lampeFuer, offeneUpdates } from "@/lib/homelab"
|
||||||
@@ -242,6 +242,7 @@ export function HomelabSeite() {
|
|||||||
export function HomelabUpdatesSeite() {
|
export function HomelabUpdatesSeite() {
|
||||||
const { ersatz, stand, laufend, laeufe, aktion } = useHomelabBereich()
|
const { ersatz, stand, laufend, laeufe, aktion } = useHomelabBereich()
|
||||||
const sammel = useSammellauf(!!stand)
|
const sammel = useSammellauf(!!stand)
|
||||||
|
const quittieren = useSammellaufQuittieren()
|
||||||
if (ersatz || !stand) return ersatz
|
if (ersatz || !stand) return ersatz
|
||||||
|
|
||||||
const lauf = sammel.data?.sammellauf ?? null
|
const lauf = sammel.data?.sammellauf ?? null
|
||||||
@@ -259,7 +260,7 @@ export function HomelabUpdatesSeite() {
|
|||||||
</p>
|
</p>
|
||||||
<AusfuehrerHinweis stand={stand} />
|
<AusfuehrerHinweis stand={stand} />
|
||||||
{/* Ein fertiger Lauf bleibt sechs Stunden stehen — gemessen am letzten Bericht, nicht an der Uhr beim Zeichnen. */}
|
{/* Ein fertiger Lauf bleibt sechs Stunden stehen — gemessen am letzten Bericht, nicht an der Uhr beim Zeichnen. */}
|
||||||
{lauf && (sammelLaeuft || (lauf.ende && (stand.bericht?.empfangen ?? 0) - lauf.ende < 6 * 3600)) && <SammellaufFortschritt lauf={lauf} />}
|
{lauf && (sammelLaeuft || (lauf.ende && (stand.bericht?.empfangen ?? 0) - lauf.ende < 6 * 3600)) && <SammellaufFortschritt lauf={lauf} onQuittieren={() => quittieren.mutate(lauf.id)} />}
|
||||||
<UpdateTabelle
|
<UpdateTabelle
|
||||||
ziele={stand.ziele}
|
ziele={stand.ziele}
|
||||||
laufend={laufend}
|
laufend={laufend}
|
||||||
|
|||||||
Reference in New Issue
Block a user