phase3+4: Homelab-Teil (Ausfuehrer, Ziele, Jetzt updaten mit Rueckweg) und Seite Homelab fuer alle Geraete
- deploy/homelab/ausfuehrer.py: laeuft als root auf dem Proxmox-Host, holt Auftraege beim Homelab-Teil ab (Pull, kein offener Port), feste Aktionsliste, prueft Etiketten selbst; Bericht gegen den echten Host erprobt (nur lesend). Kein Proxmox-Schluessel im Container noetig. - services/homelab: Kanal mit gemeinsamem Geheimnis, App-Katalog, Inventar -> Ziele im gemeinsamen Modell (GitHub-Versionen, Webpruefung, alte Paketlisten = unklar), Jetzt updaten: Snapshot -> Update -> Pruefung -> bei Rot zurueck + dringende Meldung - Waechter in der Rolle homelab: Ausfuehrer schweigt, Gaeste antworten nicht - kern/github.py fuer beide Rollen (auch untagged Releases mit Version im Namen) - Oberflaeche: Seite Homelab zeigt alle Geraete als Karten (KI-Box ueber /api/ziele, Homelab ueber /api/homelab/ziele) mit Stand, Rueckweg und Knopf samt Rueckfrage - Einrichtung als Skripte (container-anlegen, ausrollen, ausfuehrer-einrichten, box-partner) — noch nicht ausgefuehrt, jeder Schritt braucht das User-OK - frontend-bauen-box.sh: Frontend-Pruefung und Build auf der Box, wenn der PC keinen Speicher hat Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a47489fa85
commit
5e9227c4a3
+2
File diff suppressed because one or more lines are too long
-2
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
-1
@@ -1 +0,0 @@
|
||||
import{i as e,n as t,o as n,r,t as i}from"./dist-BaE5-hhJ.js";import{$ as a,H as o,M as s,R as c,_ as l,o as u,z as d}from"./index-DSxwfDVg.js";import{a as f,i as p,n as m,r as h,t as g}from"./sheet-iGGgLZLg.js";var _=t(),v=n(e(),1),y=r();function b(e){let t=(0,_.c)(4),{unit:n}=e,r=l(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=u(),[l,_]=(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 c(`/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: ${s(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:l===e.unit?`info`:`ghost`,size:`sm`,"aria-expanded":l===e.unit,onClick:()=>_(l===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`})]}),l===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};
|
||||
+1
@@ -0,0 +1 @@
|
||||
import{c as e,i as t,p as n,s as r,t as i,u as a}from"./button-BheHBEml.js";import{H as o,I as s,Q as c,U as l,b as u,s as d}from"./index-SRwQwEnh.js";import{a as f,i as p,n as m,r as h,t as g}from"./sheet-CKp82pOA.js";var _=r(),v=n(a(),1),y=e();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:n}){let r=c(),a=d(),[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: ${s(t)}`),r.invalidateQueries({queryKey:[`dienste`]})}catch(e){l(`fehler`,e.message)}finally{w(null)}}}let E;return E=a.isPending?(0,y.jsx)(`p`,{role:`status`,className:`px-4 text-[15px] text-text-2`,children:`Dienste werden gelesen …`}):a.isError&&!a.data?(0,y.jsxs)(`p`,{role:`alert`,className:`px-4 text-[15px] text-rot-text`,children:[`Die Dienste lassen sich gerade nicht lesen: `,a.error.message]}):a.data?.services.length?(0,y.jsx)(`ul`,{className:`m-0 flex list-none flex-col px-4 pb-4`,children:a.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:t(`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)(i,{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)(i,{variant:`gefahr`,size:`sm`,onClick:()=>T(e),children:e.schlaeft?`Wirklich wecken?`:`Wirklich neu starten?`}),(0,y.jsx)(i,{variant:`ghost`,size:`sm`,onClick:()=>S(null),children:`Nein`})]}):(0,y.jsx)(i,{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&&n(),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};
|
||||
@@ -1 +0,0 @@
|
||||
import{i as e,o as t,r as n}from"./dist-BaE5-hhJ.js";import{$ as r,H as i,L as a,Q as o,R as s,q as c,z as l}from"./index-DSxwfDVg.js";import{a as u,i as d,n as f,r as p,t as m}from"./sheet-iGGgLZLg.js";var h=t(e(),1),g=n();function _({offen:e,onSchliessen:t}){let n=r(),_=o({queryKey:[`geheimnisse`],queryFn:()=>a(`/api/maintenance/geheimnisse`),enabled:e}),[v,y]=(0,h.useState)(``),[b,x]=(0,h.useState)(!1),[S,C]=(0,h.useState)(!1);async function w(e){if(!b){x(!0),C(!1);try{await s(`/api/maintenance/geheimnisse`,{schluessel:`hf_token`,wert:e}),i(`erfolg`,e?`Hugging-Face-Zugang gespeichert.`:`Hugging-Face-Zugang gelöscht.`),y(``),n.invalidateQueries({queryKey:[`geheimnisse`]})}catch(e){i(`fehler`,e.message)}finally{x(!1)}}}let T=_.data,E;return E=_.isPending?`Wird gelesen …`:T?T.hf_token_gesetzt?`Ist hinterlegt.`:`Fehlt.`:`Lässt sich gerade nicht lesen${_.error?`: ${_.error.message}`:`.`}`,(0,g.jsx)(m,{open:e,onOpenChange:e=>!e&&t(),children:(0,g.jsxs)(f,{side:`right`,className:`gap-3 overflow-y-auto border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-lg`,children:[(0,g.jsxs)(d,{children:[(0,g.jsx)(u,{className:`schild text-lg`,children:`Einstellungen`}),(0,g.jsx)(p,{className:`text-text-2`,children:`Was die Box sich merkt.`})]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-3 border-t border-linie px-4 pt-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hugging-Face-Zugang`}),(0,g.jsxs)(`p`,{className:`text-sm text-text-2`,"aria-live":`polite`,children:[(0,g.jsx)(`span`,{className:!T&&!_.isPending?`text-rot-text`:void 0,children:E}),` Nötig für gesperrte Modelle und schnellere Downloads.`,T?.hf_token_aus_env?` Er kommt aus der Dienst-Einstellung und lässt sich hier nicht löschen.`:``]}),T?.schreibbar===!1&&(0,g.jsx)(`p`,{className:`text-sm text-bernstein`,children:`Die Box kann den Zugang gerade nicht speichern: Ihre Ablage ist nicht beschreibbar.`}),(0,g.jsxs)(`form`,{className:`flex gap-2`,onSubmit:e=>{e.preventDefault(),v.trim()&&w(v.trim())},children:[(0,g.jsx)(`label`,{htmlFor:`hf-token`,className:`sr-only`,children:`Hugging-Face-Zugang`}),(0,g.jsx)(`input`,{id:`hf-token`,type:`password`,autoComplete:`off`,value:v,onChange:e=>y(e.target.value),placeholder:`hf_…`,className:`h-11 min-w-0 flex-1 rounded-lg border border-linie-stark bg-erhaben px-3 text-[15px] outline-none focus:border-cyan`}),(0,g.jsx)(l,{type:`submit`,disabled:!v.trim()||T?.schreibbar===!1||b,children:b?`Speichert …`:`Speichern`})]}),T?.hf_token_gesetzt&&!T.hf_token_aus_env&&(S?(0,g.jsxs)(`span`,{className:`flex flex-wrap gap-2`,children:[(0,g.jsx)(l,{variant:`gefahr`,size:`sm`,disabled:b,onClick:()=>w(null),children:`Wirklich löschen?`}),(0,g.jsx)(l,{variant:`ghost`,size:`sm`,onClick:()=>C(!1),children:`Nein`})]}):(0,g.jsx)(l,{variant:`ghost`,size:`sm`,className:`self-start`,disabled:b,onClick:()=>C(!0),children:`Zugang löschen`}))]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-2 border-t border-linie px-4 pt-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hinweise`}),(0,g.jsx)(`p`,{className:`text-sm text-text-2`,children:`Dringende Hinweise gehen zusätzlich per Telegram raus, alles andere bleibt hier. Einfache Probleme wie einen abgestürzten Dienst behebt der Wächter selbst und schreibt es in den Verlauf.`})]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-2 border-t border-linie px-4 pt-4 pb-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hermes`}),(0,g.jsxs)(`a`,{href:`/hermes-ui/`,target:`_blank`,rel:`noopener`,className:`flex h-11 items-center gap-2 self-start rounded-lg border border-linie-stark bg-erhaben px-4 text-[15px] hover:border-text-3`,children:[(0,g.jsx)(c,{className:`size-4`,"aria-hidden":!0}),` Hermes-Dashboard öffnen`]}),(0,g.jsx)(`p`,{className:`text-xs text-text-3`,children:`Chat, Sitzungen und Cron-Jobs verwaltet Hermes selbst. Es fragt nach seiner eigenen Anmeldung.`})]})]})})}export{_ as Einstellungen};
|
||||
@@ -0,0 +1 @@
|
||||
import{c as e,p as t,t as n,u as r}from"./button-BheHBEml.js";import{H as i,Q as a,U as o,V as s,Z as c,q as l}from"./index-SRwQwEnh.js";import{a as u,i as d,n as f,r as p,t as m}from"./sheet-CKp82pOA.js";var h=t(r(),1),g=e();function _({offen:e,onSchliessen:t}){let r=a(),_=c({queryKey:[`geheimnisse`],queryFn:()=>s(`/api/maintenance/geheimnisse`),enabled:e}),[v,y]=(0,h.useState)(``),[b,x]=(0,h.useState)(!1),[S,C]=(0,h.useState)(!1);async function w(e){if(!b){x(!0),C(!1);try{await i(`/api/maintenance/geheimnisse`,{schluessel:`hf_token`,wert:e}),o(`erfolg`,e?`Hugging-Face-Zugang gespeichert.`:`Hugging-Face-Zugang gelöscht.`),y(``),r.invalidateQueries({queryKey:[`geheimnisse`]})}catch(e){o(`fehler`,e.message)}finally{x(!1)}}}let T=_.data,E;return E=_.isPending?`Wird gelesen …`:T?T.hf_token_gesetzt?`Ist hinterlegt.`:`Fehlt.`:`Lässt sich gerade nicht lesen${_.error?`: ${_.error.message}`:`.`}`,(0,g.jsx)(m,{open:e,onOpenChange:e=>!e&&t(),children:(0,g.jsxs)(f,{side:`right`,className:`gap-3 overflow-y-auto border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-lg`,children:[(0,g.jsxs)(d,{children:[(0,g.jsx)(u,{className:`schild text-lg`,children:`Einstellungen`}),(0,g.jsx)(p,{className:`text-text-2`,children:`Was die Box sich merkt.`})]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-3 border-t border-linie px-4 pt-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hugging-Face-Zugang`}),(0,g.jsxs)(`p`,{className:`text-sm text-text-2`,"aria-live":`polite`,children:[(0,g.jsx)(`span`,{className:!T&&!_.isPending?`text-rot-text`:void 0,children:E}),` Nötig für gesperrte Modelle und schnellere Downloads.`,T?.hf_token_aus_env?` Er kommt aus der Dienst-Einstellung und lässt sich hier nicht löschen.`:``]}),T?.schreibbar===!1&&(0,g.jsx)(`p`,{className:`text-sm text-bernstein`,children:`Die Box kann den Zugang gerade nicht speichern: Ihre Ablage ist nicht beschreibbar.`}),(0,g.jsxs)(`form`,{className:`flex gap-2`,onSubmit:e=>{e.preventDefault(),v.trim()&&w(v.trim())},children:[(0,g.jsx)(`label`,{htmlFor:`hf-token`,className:`sr-only`,children:`Hugging-Face-Zugang`}),(0,g.jsx)(`input`,{id:`hf-token`,type:`password`,autoComplete:`off`,value:v,onChange:e=>y(e.target.value),placeholder:`hf_…`,className:`h-11 min-w-0 flex-1 rounded-lg border border-linie-stark bg-erhaben px-3 text-[15px] outline-none focus:border-cyan`}),(0,g.jsx)(n,{type:`submit`,disabled:!v.trim()||T?.schreibbar===!1||b,children:b?`Speichert …`:`Speichern`})]}),T?.hf_token_gesetzt&&!T.hf_token_aus_env&&(S?(0,g.jsxs)(`span`,{className:`flex flex-wrap gap-2`,children:[(0,g.jsx)(n,{variant:`gefahr`,size:`sm`,disabled:b,onClick:()=>w(null),children:`Wirklich löschen?`}),(0,g.jsx)(n,{variant:`ghost`,size:`sm`,onClick:()=>C(!1),children:`Nein`})]}):(0,g.jsx)(n,{variant:`ghost`,size:`sm`,className:`self-start`,disabled:b,onClick:()=>C(!0),children:`Zugang löschen`}))]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-2 border-t border-linie px-4 pt-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hinweise`}),(0,g.jsx)(`p`,{className:`text-sm text-text-2`,children:`Dringende Hinweise gehen zusätzlich per Telegram raus, alles andere bleibt hier. Einfache Probleme wie einen abgestürzten Dienst behebt der Wächter selbst und schreibt es in den Verlauf.`})]}),(0,g.jsxs)(`section`,{className:`flex flex-col gap-2 border-t border-linie px-4 pt-4 pb-4`,children:[(0,g.jsx)(`h3`,{className:`schild text-sm text-text-3`,children:`Hermes`}),(0,g.jsxs)(`a`,{href:`/hermes-ui/`,target:`_blank`,rel:`noopener`,className:`flex h-11 items-center gap-2 self-start rounded-lg border border-linie-stark bg-erhaben px-4 text-[15px] hover:border-text-3`,children:[(0,g.jsx)(l,{className:`size-4`,"aria-hidden":!0}),` Hermes-Dashboard öffnen`]}),(0,g.jsx)(`p`,{className:`text-xs text-text-3`,children:`Chat, Sitzungen und Cron-Jobs verwaltet Hermes selbst. Es fragt nach seiner eigenen Anmeldung.`})]})]})})}export{_ as Einstellungen};
|
||||
-1
@@ -1 +0,0 @@
|
||||
import{n as e,r as t}from"./dist-BaE5-hhJ.js";import{g as n,l as r,n as i,r as a,t as o}from"./index-DSxwfDVg.js";var s=e(),c=t(),l=[`Proxmox-Host`,`AdGuard`,`NPMplus`,`NetBird`,`PVE Scripts Local`,`Gitea`,`PBS`,`Arcane mit Docker`];function u(e){let t=(0,s.c)(11),{partner:n}=e;if(n.erreichbar){let e=n.instanz??n.name,r=n.version?` (Version ${n.version})`:``,i;t[0]!==e||t[1]!==r?(i=(0,c.jsxs)(`p`,{className:`text-[15px] text-text-2`,children:[e,` antwortet`,r,`. Die Übersicht der Geräte mit ihren Updates folgt als Nächstes.`]}),t[0]=e,t[1]=r,t[2]=i):i=t[2];let a;t[3]===n.url?a=t[4]:(a=(0,c.jsx)(`p`,{className:`ziffern text-sm text-text-3`,children:n.url}),t[3]=n.url,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,c.jsxs)(c.Fragment,{children:[i,a]}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}let r=n.fehler??`nicht erreichbar`,i;return t[8]!==n.url||t[9]!==r?(i=(0,c.jsxs)(`p`,{role:`alert`,className:`text-[15px] text-rot-text`,children:[`Der Homelab-Teil (`,n.url,`) ist `,r,`. Hält das an, meldet der Wächter es nach fünf Minuten auf Telegram.`]}),t[8]=n.url,t[9]=r,t[10]=i):i=t[10],i}function d(){let e=(0,s.c)(4),t,n;e[0]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,c.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Der Homelab-Teil läuft als eigene Instanz in einem kleinen Container auf dem Proxmox-PC. Von dort prüft er auch die KI-Box von außen.`}),n=(0,c.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Steht er, zeigt diese Seite alle Geräte mit ihren offenen Updates. Ein Update startest du dann hier mit „Jetzt updaten“: Vorher wird gesichert, danach die Erreichbarkeit geprüft, und ist sie rot, geht es von selbst zurück.`}),e[0]=t,e[1]=n):(t=e[0],n=e[1]);let r;e[2]===Symbol.for(`react.memo_cache_sentinel`)?(r=(0,c.jsx)(`h3`,{className:`schild text-sm tracking-[0.18em] text-text-3`,children:`Dazu kommen`}),e[2]=r):r=e[2];let i;return e[3]===Symbol.for(`react.memo_cache_sentinel`)?(i=(0,c.jsxs)(c.Fragment,{children:[t,n,(0,c.jsxs)(`div`,{className:`flex flex-col gap-2`,children:[r,(0,c.jsx)(`ul`,{className:`flex flex-wrap gap-2`,children:l.map(f)})]}),(0,c.jsx)(`p`,{className:`text-sm text-text-3`,children:`Der Container wird erst nach deiner Freigabe angelegt.`})]}),e[3]=i):i=e[3],i}function f(e){return(0,c.jsx)(`li`,{children:(0,c.jsx)(i,{children:e})},e)}function p(){let e=(0,s.c)(12),t=r(),l=n();if(t.isPending||l.isPending){let t;return e[0]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,c.jsx)(a,{text:`Wird gelesen …`}),e[0]=t):t=e[0],t}if(!l.data){let t=`Der Stand des Homelab-Teils lässt sich nicht lesen: ${l.error?.message??`keine Daten`}`,n;return e[1]===t?n=e[2]:(n=(0,c.jsx)(a,{fehler:!0,text:t}),e[1]=t,e[2]=n),n}if(t.data?.rolle===`homelab`){let t;return e[3]===Symbol.for(`react.memo_cache_sentinel`)?(t=(0,c.jsx)(o,{titel:`Homelab`,rechts:(0,c.jsx)(i,{art:`gruen`,children:`Diese Instanz`}),children:(0,c.jsx)(`p`,{className:`text-[15px] text-text-2`,children:`Diese Seite kommt vom Homelab-Teil auf dem Proxmox-PC.`})}),e[3]=t):t=e[3],t}let f=l.data,p;e[4]!==f.eingerichtet||e[5]!==f.erreichbar?(p=f.eingerichtet?f.erreichbar?(0,c.jsx)(i,{art:`gruen`,children:`Verbunden`}):(0,c.jsx)(i,{art:`rot`,children:`Antwortet nicht`}):(0,c.jsx)(i,{children:`Noch nicht eingerichtet`}),e[4]=f.eingerichtet,e[5]=f.erreichbar,e[6]=p):p=e[6];let m=p,h;e[7]===f?h=e[8]:(h=f.eingerichtet?(0,c.jsx)(u,{partner:f}):(0,c.jsx)(d,{}),e[7]=f,e[8]=h);let g;return e[9]!==m||e[10]!==h?(g=(0,c.jsx)(o,{titel:`Homelab`,rechts:m,children:h}),e[9]=m,e[10]=h,e[11]=g):g=e[11],g}export{p as HomelabSeite};
|
||||
+1
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
||||
import{n as e,r as t}from"./dist-BaE5-hhJ.js";import{G as n,K as r,q as i}from"./index-DSxwfDVg.js";import{a,i as o,n as s,r as c,t as l}from"./sheet-iGGgLZLg.js";var u=e(),d=t();function f(e){let t=(0,u.c)(16),{onDienste:f,onEinstellungen:p,onSchliessen:m}=e,h;t[0]===m?h=t[1]:(h=e=>!e&&m(),t[0]=m,t[1]=h);let g;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(g=(0,d.jsxs)(o,{children:[(0,d.jsx)(a,{className:`schild text-lg`,children:`Mehr`}),(0,d.jsx)(c,{className:`sr-only`,children:`Weitere Bereiche`})]}),t[2]=g):g=t[2];let _;t[3]===Symbol.for(`react.memo_cache_sentinel`)?(_=(0,d.jsxs)(`a`,{href:`/hermes-ui/`,target:`_blank`,rel:`noopener`,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-base`,children:[(0,d.jsx)(i,{className:`size-5`,"aria-hidden":!0}),` Hermes-Dashboard öffnen`]}),t[3]=_):_=t[3];let v;t[4]===Symbol.for(`react.memo_cache_sentinel`)?(v=(0,d.jsx)(r,{className:`size-5`,"aria-hidden":!0}),t[4]=v):v=t[4];let y;t[5]===f?y=t[6]:(y=(0,d.jsxs)(`button`,{type:`button`,onClick:f,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-left text-base`,children:[v,` Dienste und Protokolle`]}),t[5]=f,t[6]=y);let b;t[7]===Symbol.for(`react.memo_cache_sentinel`)?(b=(0,d.jsx)(n,{className:`size-5`,"aria-hidden":!0}),t[7]=b):b=t[7];let x;t[8]===p?x=t[9]:(x=(0,d.jsxs)(`button`,{type:`button`,onClick:p,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-left text-base`,children:[b,` Einstellungen`]}),t[8]=p,t[9]=x);let S;t[10]!==y||t[11]!==x?(S=(0,d.jsxs)(s,{side:`bottom`,className:`border-linie bg-panel pb-[calc(1rem+env(safe-area-inset-bottom))]`,children:[g,(0,d.jsxs)(`div`,{className:`flex flex-col gap-2 px-4`,children:[_,y,x]})]}),t[10]=y,t[11]=x,t[12]=S):S=t[12];let C;return t[13]!==h||t[14]!==S?(C=(0,d.jsx)(l,{open:!0,onOpenChange:h,children:S}),t[13]=h,t[14]=S,t[15]=C):C=t[15],C}export{f as MehrMenue};
|
||||
+1
@@ -0,0 +1 @@
|
||||
import{c as e,s as t}from"./button-BheHBEml.js";import{G as n,K as r,q as i}from"./index-SRwQwEnh.js";import{a,i as o,n as s,r as c,t as l}from"./sheet-CKp82pOA.js";var u=t(),d=e();function f(e){let t=(0,u.c)(16),{onDienste:f,onEinstellungen:p,onSchliessen:m}=e,h;t[0]===m?h=t[1]:(h=e=>!e&&m(),t[0]=m,t[1]=h);let g;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(g=(0,d.jsxs)(o,{children:[(0,d.jsx)(a,{className:`schild text-lg`,children:`Mehr`}),(0,d.jsx)(c,{className:`sr-only`,children:`Weitere Bereiche`})]}),t[2]=g):g=t[2];let _;t[3]===Symbol.for(`react.memo_cache_sentinel`)?(_=(0,d.jsxs)(`a`,{href:`/hermes-ui/`,target:`_blank`,rel:`noopener`,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-base`,children:[(0,d.jsx)(i,{className:`size-5`,"aria-hidden":!0}),` Hermes-Dashboard öffnen`]}),t[3]=_):_=t[3];let v;t[4]===Symbol.for(`react.memo_cache_sentinel`)?(v=(0,d.jsx)(r,{className:`size-5`,"aria-hidden":!0}),t[4]=v):v=t[4];let y;t[5]===f?y=t[6]:(y=(0,d.jsxs)(`button`,{type:`button`,onClick:f,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-left text-base`,children:[v,` Dienste und Protokolle`]}),t[5]=f,t[6]=y);let b;t[7]===Symbol.for(`react.memo_cache_sentinel`)?(b=(0,d.jsx)(n,{className:`size-5`,"aria-hidden":!0}),t[7]=b):b=t[7];let x;t[8]===p?x=t[9]:(x=(0,d.jsxs)(`button`,{type:`button`,onClick:p,className:`flex h-12 items-center gap-3 rounded-lg border border-linie px-4 text-left text-base`,children:[b,` Einstellungen`]}),t[8]=p,t[9]=x);let S;t[10]!==y||t[11]!==x?(S=(0,d.jsxs)(s,{side:`bottom`,className:`border-linie bg-panel pb-[calc(1rem+env(safe-area-inset-bottom))]`,children:[g,(0,d.jsxs)(`div`,{className:`flex flex-col gap-2 px-4`,children:[_,y,x]})]}),t[10]=y,t[11]=x,t[12]=S):S=t[12];let C;return t[13]!==h||t[14]!==S?(C=(0,d.jsx)(l,{open:!0,onOpenChange:h,children:S}),t[13]=h,t[14]=S,t[15]=C):C=t[15],C}export{f as MehrMenue};
|
||||
-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 +0,0 @@
|
||||
import{n as e,r as t}from"./dist-BaE5-hhJ.js";import{a as n,i as r,n as i,r as a,t as o}from"./sheet-iGGgLZLg.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};
|
||||
@@ -0,0 +1 @@
|
||||
import{c as e,s as t}from"./button-BheHBEml.js";import{a as n,i as r,n as i,r as a,t as o}from"./sheet-CKp82pOA.js";var s=t(),c=e();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
File diff suppressed because one or more lines are too long
-2
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
+41
File diff suppressed because one or more lines are too long
-41
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
-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
@@ -0,0 +1 @@
|
||||
import{a as e,c as t,i as n,s as r,t as i,u as a}from"./button-BheHBEml.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-CtXmxyGa.js";var p=r();a();var m=t();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),r,i;t[0]===e?(r=t[1],i=t[2]):({className:r,...i}=e,t[0]=e,t[1]=r,t[2]=i);let a;t[3]===r?a=t[4]:(a=n(`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`,r),t[3]=r,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(t){let r=(0,p.c)(17),a,o,s,l,d;r[0]===t?(a=r[1],o=r[2],s=r[3],l=r[4],d=r[5]):({className:o,children:a,side:l,showCloseButton:d,...s}=t,r[0]=t,r[1]=a,r[2]=o,r[3]=s,r[4]=l,r[5]=d);let f=l===void 0?`right`:l,h=d===void 0||d,v;r[6]===Symbol.for(`react.memo_cache_sentinel`)?(v=(0,m.jsx)(_,{}),r[6]=v):v=r[6];let y;r[7]===o?y=r[8]:(y=n(`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),r[7]=o,r[8]=y);let b;r[9]===h?b=r[10]:(b=h&&(0,m.jsx)(c,{"data-slot":`sheet-close`,asChild:!0,children:(0,m.jsxs)(i,{variant:`ghost`,className:`absolute top-3 right-3`,size:`icon-sm`,children:[(0,m.jsx)(e,{}),(0,m.jsx)(`span`,{className:`sr-only`,children:`Schließen`})]})}),r[9]=h,r[10]=b);let x;return r[11]!==a||r[12]!==s||r[13]!==f||r[14]!==y||r[15]!==b?(x=(0,m.jsxs)(g,{children:[v,(0,m.jsxs)(u,{"data-slot":`sheet-content`,"data-side":f,className:y,...s,children:[a,b]})]}),r[11]=a,r[12]=s,r[13]=f,r[14]=y,r[15]=b,r[16]=x):x=r[16],x}function y(e){let t=(0,p.c)(8),r,i;t[0]===e?(r=t[1],i=t[2]):({className:r,...i}=e,t[0]=e,t[1]=r,t[2]=i);let a;t[3]===r?a=t[4]:(a=n(`flex flex-col gap-0.5 p-4`,r),t[3]=r,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),r,i;t[0]===e?(r=t[1],i=t[2]):({className:r,...i}=e,t[0]=e,t[1]=r,t[2]=i);let a;t[3]===r?a=t[4]:(a=n(`font-heading text-base font-medium text-foreground`,r),t[3]=r,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),r,i;t[0]===e?(r=t[1],i=t[2]):({className:r,...i}=e,t[0]=e,t[1]=r,t[2]=i);let a;t[3]===r?a=t[4]:(a=n(`text-sm text-muted-foreground`,r),t[3]=r,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};
|
||||
-1
@@ -1 +0,0 @@
|
||||
import{i as e,n as t,r as n,t as r}from"./dist-BaE5-hhJ.js";import{a as i,i as a,n as o,o as s,r as c,s as l,t as u}from"./dist-ndv4TK4_.js";import{W as d,z as f}from"./index-DSxwfDVg.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)(u,{"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)(s,{"data-slot":`sheet-portal`,...n}),t[2]=n,t[3]=r),r}function _(e){let t=(0,p.c)(8),n,a;t[0]===e?(n=t[1],a=t[2]):({className:n,...a}=e,t[0]=e,t[1]=n,t[2]=a);let o;t[3]===n?o=t[4]:(o=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]=o);let s;return t[5]!==a||t[6]!==o?(s=(0,m.jsx)(i,{"data-slot":`sheet-overlay`,className:o,...a}),t[5]=a,t[6]=o,t[7]=s):s=t[7],s}function v(e){let t=(0,p.c)(17),n,i,a,s,l;t[0]===e?(n=t[1],i=t[2],a=t[3],s=t[4],l=t[5]):({className:i,children:n,side:s,showCloseButton:l,...a}=e,t[0]=e,t[1]=n,t[2]=i,t[3]=a,t[4]=s,t[5]=l);let u=s===void 0?`right`:s,h=l===void 0||l,v;t[6]===Symbol.for(`react.memo_cache_sentinel`)?(v=(0,m.jsx)(_,{}),t[6]=v):v=t[6];let y;t[7]===i?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`,i),t[7]=i,t[8]=y);let b;t[9]===h?b=t[10]:(b=h&&(0,m.jsx)(o,{"data-slot":`sheet-close`,asChild:!0,children:(0,m.jsxs)(f,{variant:`ghost`,className:`absolute top-3 right-3`,size:`icon-sm`,children:[(0,m.jsx)(d,{}),(0,m.jsx)(`span`,{className:`sr-only`,children:`Schließen`})]})}),t[9]=h,t[10]=b);let x;return t[11]!==n||t[12]!==a||t[13]!==u||t[14]!==y||t[15]!==b?(x=(0,m.jsxs)(g,{children:[v,(0,m.jsxs)(c,{"data-slot":`sheet-content`,"data-side":u,className:y,...a,children:[n,b]})]}),t[11]=n,t[12]=a,t[13]=u,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)(l,{"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 o;t[3]===n?o=t[4]:(o=r(`text-sm text-muted-foreground`,n),t[3]=n,t[4]=o);let s;return t[5]!==i||t[6]!==o?(s=(0,m.jsx)(a,{"data-slot":`sheet-description`,className:o,...i}),t[5]=i,t[6]=o,t[7]=s):s=t[7],s}export{b as a,y as i,v as n,x as r,h as t};
|
||||
Vendored
+17
-17
@@ -1,17 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="de" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#0e1012" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>Homelab Orchestrator</title>
|
||||
<script type="module" crossorigin src="/assets/index-DSxwfDVg.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/dist-BaE5-hhJ.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CcgHCQlF.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
<!doctype html>
|
||||
<html lang="de" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#0e1012" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>Homelab Orchestrator</title>
|
||||
<script type="module" crossorigin src="/assets/index-SRwQwEnh.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/button-BheHBEml.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bpe3m53F.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { fireEvent, render, screen, within } from "@testing-library/react"
|
||||
import type { ZielStand } from "@/lib/typen"
|
||||
import { ZielKarte } from "./ZielKarte"
|
||||
|
||||
const gitea: ZielStand = {
|
||||
id: "ct-104", name: "Gitea", art: "container", instanz: "homelab", erreichbar: true, geprueft: 1_790_000_000,
|
||||
rueckweg: "Snapshot vor jedem Update", zaehlung: { neu: 1, aktuell: 1 }, offen: 1,
|
||||
bausteine: [
|
||||
{
|
||||
id: "app", name: "Gitea", zustand: "neu", installiert: "1.27.2", verfuegbar: "1.27.3", kurz: "1.27.2 → 1.27.3",
|
||||
grund: null,
|
||||
aktion: { methode: "POST", pfad: "/api/homelab/ziele/ct-104/update", label: "Jetzt updaten",
|
||||
frage: "Gitea jetzt aktualisieren? Vorher wird ein Snapshot angelegt." },
|
||||
},
|
||||
{ id: "os", name: "Pakete", zustand: "aktuell", installiert: null, verfuegbar: null, kurz: "", grund: null, aktion: null },
|
||||
],
|
||||
}
|
||||
|
||||
describe("ZielKarte", () => {
|
||||
it("zeigt Stand, Container-Nummer und Rückweg", () => {
|
||||
render(<ZielKarte ziel={gitea} laeuft={null} gesperrt={false} onAktion={() => {}} />)
|
||||
expect(screen.getByText("Container 104")).toBeInTheDocument()
|
||||
expect(screen.getByText("1.27.2 → 1.27.3")).toBeInTheDocument()
|
||||
expect(screen.getByText("Update bereit")).toBeInTheDocument()
|
||||
expect(screen.getByText("Rückweg: Snapshot vor jedem Update")).toBeInTheDocument()
|
||||
expect(screen.getByText("erreichbar")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("fragt mit dem Text des Geräts und startet erst nach dem Ja", () => {
|
||||
const aktion = vi.fn()
|
||||
render(<ZielKarte ziel={gitea} laeuft={null} gesperrt={false} onAktion={aktion} />)
|
||||
fireEvent.click(screen.getByRole("button", { name: "Jetzt updaten" }))
|
||||
const dialog = screen.getByRole("dialog")
|
||||
expect(within(dialog).getByText(/Vorher wird ein Snapshot angelegt/)).toBeInTheDocument()
|
||||
expect(aktion).not.toHaveBeenCalled()
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Jetzt updaten" }))
|
||||
expect(aktion).toHaveBeenCalledWith(gitea.bausteine[0].aktion)
|
||||
})
|
||||
|
||||
it("sperrt die Knöpfe, solange ein Update läuft", () => {
|
||||
render(<ZielKarte ziel={gitea} laeuft="update …" gesperrt={false} onAktion={() => {}} />)
|
||||
expect(screen.getByRole("status")).toHaveTextContent("Update läuft: update …")
|
||||
expect(screen.getByRole("button", { name: "Jetzt updaten" })).toBeDisabled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,117 @@
|
||||
import { useState } from "react"
|
||||
import { cn } from "cn"
|
||||
import { Bestaetigen } from "@/components/cockpit/Bestaetigen"
|
||||
import { Chip, type ChipArt } from "@/components/cockpit/Chip"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import type { BausteinStand, BausteinZustand, ZielAktion, ZielStand } from "@/lib/typen"
|
||||
|
||||
const ZUSTAND: Record<BausteinZustand, { art: ChipArt; text: string }> = {
|
||||
neu: { art: "cyan", text: "Update bereit" },
|
||||
aktuell: { art: "gruen", text: "aktuell" },
|
||||
unbekannt: { art: "bernstein", text: "unklar" },
|
||||
festgehalten: { art: "bernstein", text: "festgehalten" },
|
||||
"wird-geprueft": { art: "grau", text: "wird geprüft" },
|
||||
}
|
||||
|
||||
const ART: Record<string, string> = {
|
||||
"ki-box": "KI-Box",
|
||||
"proxmox-host": "Proxmox-Host",
|
||||
container: "Container",
|
||||
vm: "VM",
|
||||
}
|
||||
|
||||
/** „Container 104“ aus Art und ID („ct-104“). */
|
||||
function artText(ziel: ZielStand): string {
|
||||
const nummer = ziel.id.match(/-(\d+)$/)?.[1]
|
||||
const art = ART[ziel.art] ?? ziel.art
|
||||
return nummer ? `${art} ${nummer}` : art
|
||||
}
|
||||
|
||||
function Stand({ b }: { b: BausteinStand }) {
|
||||
if (b.kurz) return <span className="ziffern">{b.kurz}</span>
|
||||
if (b.installiert && b.verfuegbar) return <span className="ziffern">{b.installiert} → {b.verfuegbar}</span>
|
||||
if (b.installiert) return <span className="ziffern">{b.installiert}</span>
|
||||
return null
|
||||
}
|
||||
|
||||
function Zeile({ b, gesperrt, onAktion }: { b: BausteinStand; gesperrt: boolean; onAktion: (a: ZielAktion) => void }) {
|
||||
const z = ZUSTAND[b.zustand] ?? ZUSTAND.unbekannt
|
||||
return (
|
||||
<li className="flex flex-col gap-2 border-t border-linie py-3 first:border-t-0 first:pt-0 last:pb-0">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="text-[15px] font-medium">{b.name}</span>
|
||||
<span className="text-sm text-text-2">
|
||||
<Stand b={b} />
|
||||
</span>
|
||||
</div>
|
||||
<Chip art={z.art}>{z.text}</Chip>
|
||||
</div>
|
||||
{b.grund && <p className="text-sm text-text-2">{b.grund}</p>}
|
||||
{b.aktion && (
|
||||
<div>
|
||||
<Button size="sm" variant={b.zustand === "neu" ? "default" : "outline"} disabled={gesperrt}
|
||||
onClick={() => b.aktion && onAktion(b.aktion)}>
|
||||
{b.aktion.label}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Gerät (KI-Box, Proxmox-Host, Container, VM) mit seinen Bausteinen — dieselbe Karte für beide
|
||||
* Instanzen, weil beide dasselbe Ziel-Modell liefern (backend/kern/ziele.py). Jeder Knopf fragt vorher
|
||||
* mit dem Text, den das Gerät selbst mitliefert (Rückweg, Neustart, was wegfällt).
|
||||
*/
|
||||
export function ZielKarte({ ziel, laeuft, gesperrt, onAktion }: {
|
||||
ziel: ZielStand
|
||||
/** Letzter Schritt eines laufenden Updates für dieses Gerät, sonst null. */
|
||||
laeuft: string | null
|
||||
gesperrt: boolean
|
||||
onAktion: (a: ZielAktion) => void
|
||||
}) {
|
||||
const [frage, setFrage] = useState<ZielAktion | null>(null)
|
||||
const erreichbar = ziel.erreichbar
|
||||
return (
|
||||
<section aria-label={ziel.name} className="flex min-w-0 flex-col gap-3 rounded-2xl border border-linie bg-panel p-5">
|
||||
<header className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<h3 className="schild text-base font-bold tracking-[0.18em] break-words">{ziel.name}</h3>
|
||||
<span className="text-sm text-text-3">{artText(ziel)}</span>
|
||||
</div>
|
||||
{erreichbar !== null && (
|
||||
<span className={cn("schild flex items-center gap-1.5 text-xs", erreichbar ? "text-text-3" : "text-rot-text")}>
|
||||
<span aria-hidden className={cn("size-2 rounded-full", erreichbar ? "bg-gruen" : "bg-rot")} />
|
||||
{erreichbar ? "erreichbar" : "antwortet nicht"}
|
||||
</span>
|
||||
)}
|
||||
</header>
|
||||
{laeuft && (
|
||||
<p role="status" className="rounded-lg border border-cyan-rand bg-cyan-grund px-3 py-2 text-sm text-cyan-text">
|
||||
Update läuft: {laeuft}
|
||||
</p>
|
||||
)}
|
||||
<ul className="flex flex-col">
|
||||
{ziel.bausteine.map((b) => (
|
||||
<Zeile key={b.id} b={b} gesperrt={gesperrt || !!laeuft} onAktion={setFrage} />
|
||||
))}
|
||||
</ul>
|
||||
{ziel.rueckweg && <p className="text-xs text-text-3">Rückweg: {ziel.rueckweg}</p>}
|
||||
<Bestaetigen
|
||||
offen={!!frage}
|
||||
titel={frage ? `${ziel.name}: ${frage.label}` : ""}
|
||||
text={frage?.frage ?? ""}
|
||||
knopf={frage?.label ?? ""}
|
||||
gefahr={!!frage?.pfad.endsWith("/neustart")}
|
||||
gesperrt={gesperrt}
|
||||
onJa={() => {
|
||||
if (frage) onAktion(frage)
|
||||
setFrage(null)
|
||||
}}
|
||||
onNein={() => setFrage(null)}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import { fehlerText, laeuftNoch } from "./anzeige"
|
||||
import { melden } from "./meldungen"
|
||||
import { stromVerbunden } from "./strom"
|
||||
import type {
|
||||
AktionsErgebnis, AufraeumKandidat, Instanz, Job, Modell, Nutzung, PartnerStatus, Radar, Sicherung, Start,
|
||||
UpdateDetails,
|
||||
AktionsErgebnis, AufraeumKandidat, HomelabLauf, HomelabStand, Instanz, Job, Modell, Nutzung, PartnerStatus, Radar,
|
||||
Sicherung, Start, UpdateDetails, ZielAktion, ZielStand,
|
||||
} from "./typen"
|
||||
import { gbText } from "./zeit"
|
||||
|
||||
@@ -262,3 +262,32 @@ export const useInstanz = () =>
|
||||
/** Lebenszeichen der anderen Instanz — die Box prüft den Homelab-Teil und umgekehrt. */
|
||||
export const usePartner = () =>
|
||||
useQuery({ queryKey: ["partner"], queryFn: () => api<PartnerStatus>("/api/partner"), refetchInterval: 30_000 })
|
||||
|
||||
/** Die KI-Box im gemeinsamen Ziel-Modell (Box-Adapter, GET /api/ziele). */
|
||||
export const useBoxZiele = () =>
|
||||
useQuery({ queryKey: ["ziele-box"], queryFn: () => api<{ ziele: ZielStand[] }>("/api/ziele"), refetchInterval: takt(30_000) })
|
||||
|
||||
/** Proxmox-Host und Gäste — nur, wenn es den Homelab-Teil gibt (sonst gäbe es nur 404). */
|
||||
export const useHomelab = (aktiv: boolean) =>
|
||||
useQuery({
|
||||
queryKey: ["homelab"],
|
||||
queryFn: () => api<HomelabStand>("/api/homelab/ziele"),
|
||||
enabled: aktiv,
|
||||
refetchInterval: 30_000,
|
||||
})
|
||||
|
||||
export const useHomelabLaeufe = (aktiv: boolean) =>
|
||||
useQuery({
|
||||
queryKey: ["homelab-laeufe"],
|
||||
queryFn: () => api<{ laeufe: HomelabLauf[] }>("/api/homelab/laeufe"),
|
||||
enabled: aktiv,
|
||||
// Läuft ein Update, die Schritte zeitnah zeigen.
|
||||
refetchInterval: (q) => (q.state.data?.laeufe.some((l) => l.status === "laeuft") ? 3000 : 30_000),
|
||||
})
|
||||
|
||||
/** Knopf eines Bausteins: die Aktion steht im Baustein selbst (Methode, Pfad, Rückfrage). */
|
||||
export const useZielAktion = () =>
|
||||
useAktion((a: ZielAktion) => api<AktionsErgebnis>(a.pfad, { method: a.methode }), {
|
||||
erfolg: (a) => `${a.label}: gestartet. Das Ergebnis kommt als Meldung.`,
|
||||
neuLaden: ["homelab", "homelab-laeufe", "ziele-box", "jobs", "start"],
|
||||
})
|
||||
|
||||
@@ -243,3 +243,62 @@ export interface PartnerStatus {
|
||||
/** Unix-Sekunden */
|
||||
geprueft?: number
|
||||
}
|
||||
|
||||
// --- Gemeinsames Ziel-Modell (backend/kern/ziele.py, seit Phase 2) ---------------------------------
|
||||
|
||||
export type BausteinZustand = "neu" | "aktuell" | "unbekannt" | "festgehalten" | "wird-geprueft"
|
||||
|
||||
/** Wie ein Update angestoßen wird: eine Schnittstelle der Instanz, die das Gerät pflegt. */
|
||||
export interface ZielAktion {
|
||||
methode: string
|
||||
pfad: string
|
||||
frage: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface BausteinStand {
|
||||
id: string
|
||||
name: string
|
||||
zustand: BausteinZustand
|
||||
installiert: string | null
|
||||
verfuegbar: string | null
|
||||
kurz: string
|
||||
grund: string | null
|
||||
aktion: ZielAktion | null
|
||||
}
|
||||
|
||||
/** Ein Gerät (KI-Box, Proxmox-Host, Container, VM) mit seinen Bausteinen. */
|
||||
export interface ZielStand {
|
||||
id: string
|
||||
name: string
|
||||
art: "ki-box" | "proxmox-host" | "container" | "vm" | string
|
||||
instanz: "box" | "homelab" | string
|
||||
erreichbar: boolean | null
|
||||
bausteine: BausteinStand[]
|
||||
/** Unix-Sekunden */
|
||||
geprueft: number | null
|
||||
rueckweg: string | null
|
||||
zaehlung: Partial<Record<BausteinZustand, number>>
|
||||
offen: number
|
||||
}
|
||||
|
||||
/** GET /api/homelab/ziele */
|
||||
export interface HomelabStand {
|
||||
ziele: ZielStand[]
|
||||
bericht: { empfangen: number; veraltet: boolean } | null
|
||||
ausfuehrer: { verbunden: boolean; zuletzt: number | null }
|
||||
}
|
||||
|
||||
/** Ein „Jetzt updaten“-Lauf im Homelab (GET /api/homelab/laeufe). */
|
||||
export interface HomelabLauf {
|
||||
id: string
|
||||
ziel: string
|
||||
baustein: string
|
||||
name: string
|
||||
status: "laeuft" | "fertig" | "unterbrochen"
|
||||
start: number
|
||||
ende: number | null
|
||||
ergebnis: "eingespielt" | "zurueckgerollt" | "fehler" | "neustart" | null
|
||||
text: string | null
|
||||
schritte: string[]
|
||||
}
|
||||
|
||||
+110
-49
@@ -1,29 +1,18 @@
|
||||
import { Chip, Zustandsfeld } from "@/components/cockpit/Chip"
|
||||
import { Chip, type ChipArt, Zustandsfeld } from "@/components/cockpit/Chip"
|
||||
import { Panel } from "@/components/cockpit/Panel"
|
||||
import { useInstanz, usePartner } from "@/lib/abfragen"
|
||||
import type { PartnerStatus } from "@/lib/typen"
|
||||
import { ZielKarte } from "@/components/cockpit/ZielKarte"
|
||||
import { useBoxZiele, useHomelab, useHomelabLaeufe, useInstanz, usePartner, useZielAktion } from "@/lib/abfragen"
|
||||
import type { HomelabLauf, PartnerStatus, ZielStand } from "@/lib/typen"
|
||||
import { flugplanZeit } from "@/lib/zeit"
|
||||
|
||||
/** Was der Homelab-Teil übernimmt (Bestandsaufnahme vom 24.09.2026; ab Phase 3 kommt die Liste live). */
|
||||
/** Was der Homelab-Teil übernimmt (Bestandsaufnahme vom 24.09.2026). */
|
||||
const GERAETE = ["Proxmox-Host", "AdGuard", "NPMplus", "NetBird", "PVE Scripts Local", "Gitea", "PBS", "Arcane mit Docker"]
|
||||
|
||||
function Verbindung({ partner }: { partner: PartnerStatus }) {
|
||||
if (partner.erreichbar) {
|
||||
return (
|
||||
<>
|
||||
<p className="text-[15px] text-text-2">
|
||||
{partner.instanz ?? partner.name} antwortet{partner.version ? ` (Version ${partner.version})` : ""}. Die
|
||||
Übersicht der Geräte mit ihren Updates folgt als Nächstes.
|
||||
</p>
|
||||
<p className="ziffern text-sm text-text-3">{partner.url}</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<p role="alert" className="text-[15px] text-rot-text">
|
||||
Der Homelab-Teil ({partner.url}) ist {partner.fehler ?? "nicht erreichbar"}. Hält das an, meldet der Wächter es
|
||||
nach fünf Minuten auf Telegram.
|
||||
</p>
|
||||
)
|
||||
const ERGEBNIS: Record<string, { art: ChipArt; text: string }> = {
|
||||
eingespielt: { art: "gruen", text: "eingespielt" },
|
||||
zurueckgerollt: { art: "bernstein", text: "zurückgerollt" },
|
||||
fehler: { art: "rot", text: "Fehler" },
|
||||
neustart: { art: "cyan", text: "Neustart" },
|
||||
}
|
||||
|
||||
function NochNichtEingerichtet() {
|
||||
@@ -34,8 +23,8 @@ function NochNichtEingerichtet() {
|
||||
auch die KI-Box von außen.
|
||||
</p>
|
||||
<p className="text-[15px] text-text-2">
|
||||
Steht er, zeigt diese Seite alle Geräte mit ihren offenen Updates. Ein Update startest du dann hier mit
|
||||
„Jetzt updaten“: Vorher wird gesichert, danach die Erreichbarkeit geprüft, und ist sie rot, geht es von selbst
|
||||
Steht er, erscheinen hier alle Geräte mit ihren offenen Updates. Ein Update startest du mit „Jetzt
|
||||
updaten“: Vorher wird gesichert, danach die Erreichbarkeit geprüft, und ist sie rot, geht es von selbst
|
||||
zurück.
|
||||
</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -53,34 +42,106 @@ function NochNichtEingerichtet() {
|
||||
)
|
||||
}
|
||||
|
||||
export function HomelabSeite() {
|
||||
const instanz = useInstanz()
|
||||
const partner = usePartner()
|
||||
|
||||
if (instanz.isPending || partner.isPending) return <Zustandsfeld text="Wird gelesen …" />
|
||||
if (!partner.data) {
|
||||
return <Zustandsfeld fehler text={`Der Stand des Homelab-Teils lässt sich nicht lesen: ${partner.error?.message ?? "keine Daten"}`} />
|
||||
}
|
||||
|
||||
// Liefert der Homelab-Teil die Seite selbst aus, ist die Box sein Partner (ab Phase 3).
|
||||
if (instanz.data?.rolle === "homelab") {
|
||||
return (
|
||||
<Panel titel="Homelab" rechts={<Chip art="gruen">Diese Instanz</Chip>}>
|
||||
<p className="text-[15px] text-text-2">Diese Seite kommt vom Homelab-Teil auf dem Proxmox-PC.</p>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
|
||||
const p = partner.data
|
||||
const chip = !p.eingerichtet
|
||||
function HomelabFehlt({ partner }: { partner: PartnerStatus }) {
|
||||
const chip = !partner.eingerichtet
|
||||
? <Chip>Noch nicht eingerichtet</Chip>
|
||||
: p.erreichbar
|
||||
? <Chip art="gruen">Verbunden</Chip>
|
||||
: <Chip art="rot">Antwortet nicht</Chip>
|
||||
|
||||
: <Chip art="rot">Antwortet nicht</Chip>
|
||||
return (
|
||||
<Panel titel="Homelab" rechts={chip}>
|
||||
{p.eingerichtet ? <Verbindung partner={p} /> : <NochNichtEingerichtet />}
|
||||
{partner.eingerichtet ? (
|
||||
<p role="alert" className="text-[15px] text-rot-text">
|
||||
Der Homelab-Teil ({partner.url}) ist {partner.fehler ?? "nicht erreichbar"}. Hält das an, meldet der
|
||||
Wächter es nach fünf Minuten auf Telegram.
|
||||
</p>
|
||||
) : (
|
||||
<NochNichtEingerichtet />
|
||||
)}
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
|
||||
function LetzteLaeufe({ laeufe }: { laeufe: HomelabLauf[] }) {
|
||||
const fertig = laeufe.filter((l) => l.status !== "laeuft").slice(0, 6)
|
||||
if (fertig.length === 0) return null
|
||||
return (
|
||||
<Panel titel="Letzte Updates im Homelab">
|
||||
<ul className="flex flex-col gap-3">
|
||||
{fertig.map((l) => {
|
||||
const e = l.ergebnis ? ERGEBNIS[l.ergebnis] : { art: "grau" as ChipArt, text: "unterbrochen" }
|
||||
return (
|
||||
<li key={l.id} className="flex flex-col gap-1 border-t border-linie pt-3 first:border-t-0 first:pt-0">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<span className="text-[15px] font-medium">{l.name}</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="ziffern text-sm text-text-3">{flugplanZeit(new Date(l.start * 1000).toISOString())}</span>
|
||||
<Chip art={e.art}>{e.text}</Chip>
|
||||
</span>
|
||||
</div>
|
||||
{l.text && <p className="text-sm text-text-2">{l.text}</p>}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
|
||||
export function HomelabSeite() {
|
||||
const instanz = useInstanz()
|
||||
const partner = usePartner()
|
||||
const box = useBoxZiele()
|
||||
const homelabHier = instanz.data?.rolle === "homelab"
|
||||
const aktiv = homelabHier || !!(partner.data?.eingerichtet && partner.data.erreichbar)
|
||||
const homelab = useHomelab(aktiv)
|
||||
const laeufe = useHomelabLaeufe(aktiv)
|
||||
const aktion = useZielAktion()
|
||||
|
||||
if (instanz.isPending || partner.isPending) return <Zustandsfeld text="Wird gelesen …" />
|
||||
|
||||
// Laufende Updates je Gerät: der letzte Schritt steht auf der Karte.
|
||||
const laufend = new Map<string, string>()
|
||||
for (const l of laeufe.data?.laeufe ?? []) {
|
||||
if (l.status === "laeuft") laufend.set(l.ziel, l.schritte.at(-1) ?? "startet …")
|
||||
}
|
||||
const ziele: ZielStand[] = [...(box.data?.ziele ?? []), ...(homelab.data?.ziele ?? [])]
|
||||
const offen = ziele.reduce((n, z) => n + z.offen, 0)
|
||||
const bericht = homelab.data?.bericht
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel
|
||||
titel="Alle Geräte"
|
||||
rechts={offen > 0 ? <Chip art="cyan">{offen} {offen === 1 ? "Update" : "Updates"} bereit</Chip> : <Chip art="gruen">aktuell</Chip>}
|
||||
>
|
||||
<p className="text-[15px] text-text-2">
|
||||
Die KI-Box und das Homelab an einem Ort. Updates startest du nur hier per Knopf; jedes fragt vorher und sagt,
|
||||
welcher Rückweg bereitsteht.
|
||||
</p>
|
||||
{aktiv && homelab.data && !homelab.data.ausfuehrer.verbunden && (
|
||||
<p role="alert" className="text-[15px] text-bernstein">
|
||||
Der Ausführer auf dem Proxmox-Host meldet sich nicht
|
||||
{homelab.data.ausfuehrer.zuletzt ? ` (zuletzt ${flugplanZeit(new Date(homelab.data.ausfuehrer.zuletzt * 1000).toISOString())})` : ""}.
|
||||
Die Homelab-Geräte zeigen den letzten bekannten Stand.
|
||||
</p>
|
||||
)}
|
||||
{aktiv && homelab.data && !bericht && (
|
||||
<p className="text-[15px] text-text-2">Der Ausführer auf dem Proxmox-Host hat noch keinen Bericht geschickt.</p>
|
||||
)}
|
||||
{box.isError && <p role="alert" className="text-sm text-rot-text">Die KI-Box antwortet gerade nicht: {box.error.message}</p>}
|
||||
{homelab.isError && <p role="alert" className="text-sm text-rot-text">Der Homelab-Teil antwortet gerade nicht: {homelab.error.message}</p>}
|
||||
</Panel>
|
||||
|
||||
{ziele.length > 0 && (
|
||||
<div className="grid gap-5 md:gap-6 xl:grid-cols-2">
|
||||
{ziele.map((z) => (
|
||||
<ZielKarte key={z.id} ziel={z} laeuft={laufend.get(z.id) ?? null} gesperrt={aktion.isPending}
|
||||
onAktion={(a) => aktion.mutate(a)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!aktiv && partner.data && <HomelabFehlt partner={partner.data} />}
|
||||
{aktiv && <LetzteLaeufe laeufe={laeufe.data?.laeufe ?? []} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user