phase2c: Oberflaeche heisst Homelab Orchestrator, Bereich Homelab, Anfragen gehen an die richtige Instanz
- /api/instanz: Rolle der ausliefernden Instanz, ohne Netzabfragen - lib/instanz.ts: /api/homelab/... an die Homelab-Instanz, alles andere an die Box, ueber /api/partner/..., wenn die andere Instanz die Seite ausliefert - neue Seite Homelab: Stand der zweiten Instanz (noch nicht eingerichtet / verbunden / antwortet nicht) und was dazukommt - Kopfzeile, Titel und Web-Manifest mit neuem Namen; fuenfter Menuepunkt, 320 px geprueft Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ee2bb9d03c
commit
22bb8c672b
@@ -18,6 +18,15 @@ _GESPERRT = ("partner", "stream")
|
||||
_ZEITLIMIT = httpx.Timeout(30.0, connect=5.0)
|
||||
|
||||
|
||||
@router.get("/instanz")
|
||||
def instanz() -> dict:
|
||||
"""Wer liefert die Seite aus? Ohne Netzabfragen, damit die Oberfläche sofort weiß, welche Anfragen
|
||||
sie selbst beantwortet und welche über /api/partner/… an die andere Instanz gehen."""
|
||||
e = einstellungen()
|
||||
return {"rolle": e.rolle, "instanz": e.instanz,
|
||||
"partner": {"eingerichtet": bool(e.partner_url), "name": e.partner_name}}
|
||||
|
||||
|
||||
@router.get("/partner")
|
||||
def partner_status() -> dict:
|
||||
return partner.status()
|
||||
|
||||
@@ -110,6 +110,12 @@ def test_durchreiche_get_und_post(mit_partner, partner_server):
|
||||
assert json.loads(letzte["koerper"]) == {"ziel": "ct:101"}
|
||||
|
||||
|
||||
def test_instanz_ohne_netz(mit_partner):
|
||||
mit_partner("http://127.0.0.1:9")
|
||||
assert _client().get("/api/instanz").json() == {
|
||||
"rolle": "box", "instanz": "Box-Wart", "partner": {"eingerichtet": True, "name": "Homelab"}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pfad", ["partner/health", "partner", "stream"])
|
||||
def test_durchreiche_sperrt_schleifen_und_strom(mit_partner, partner_server, pfad):
|
||||
url, empfangen = partner_server
|
||||
|
||||
+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{c as e,f as t,i as n,s as r,t as i,u as a}from"./button-Dd8hfusv.js";import{A as o,I as s,K as c,L as l,h as u,o as d}from"./index-BpFIVIMn.js";import{a as f,i as p,n as m,r as h,t as g}from"./sheet-DWG2Q04V.js";var _=r(),v=t(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:t}){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 s(`/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: ${o(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:n(`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&&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{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};
|
||||
@@ -0,0 +1 @@
|
||||
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};
|
||||
@@ -1 +0,0 @@
|
||||
import{c as e,f as t,t as n,u as r}from"./button-Dd8hfusv.js";import{F as i,G as a,I as o,K as s,L as c,V as l}from"./index-BpFIVIMn.js";import{a as u,i as d,n as f,r as p,t as m}from"./sheet-DWG2Q04V.js";var h=t(r(),1),g=e();function _({offen:e,onSchliessen:t}){let r=s(),_=a({queryKey:[`geheimnisse`],queryFn:()=>i(`/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 o(`/api/maintenance/geheimnisse`,{schluessel:`hf_token`,wert:e}),c(`erfolg`,e?`Hugging-Face-Zugang gespeichert.`:`Hugging-Face-Zugang gelöscht.`),y(``),r.invalidateQueries({queryKey:[`geheimnisse`]})}catch(e){c(`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
@@ -0,0 +1 @@
|
||||
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
@@ -1 +0,0 @@
|
||||
import{c as e,s as t}from"./button-Dd8hfusv.js";import{B as n,V as r,z as i}from"./index-BpFIVIMn.js";import{a,i as o,n as s,r as c,t as l}from"./sheet-DWG2Q04V.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)(r,{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)(n,{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)(i,{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{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
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
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};
|
||||
@@ -1 +0,0 @@
|
||||
import{c as e,s as t}from"./button-Dd8hfusv.js";import{a as n,i as r,n as i,r as a,t as o}from"./sheet-DWG2Q04V.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
-11
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
-1
@@ -1 +0,0 @@
|
||||
import{a as e,c as t,i as n,s as r,t as i,u as a}from"./button-Dd8hfusv.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-CgRgLCFz.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
@@ -0,0 +1 @@
|
||||
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>MC2 · Box-Wart</title>
|
||||
<script type="module" crossorigin src="/assets/index-BpFIVIMn.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/button-Dd8hfusv.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B8FdVNTI.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-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>
|
||||
|
||||
Vendored
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Mission Control 2.0",
|
||||
"short_name": "MC2",
|
||||
"name": "Homelab Orchestrator",
|
||||
"short_name": "Orchestrator",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0d1117",
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
<meta name="theme-color" content="#0e1012" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>MC2 · Box-Wart</title>
|
||||
<title>Homelab Orchestrator</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Mission Control 2.0",
|
||||
"short_name": "MC2",
|
||||
"name": "Homelab Orchestrator",
|
||||
"short_name": "Orchestrator",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0d1117",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { lazy, Suspense, useEffect, useState } from "react"
|
||||
import { Link, Outlet } from "@tanstack/react-router"
|
||||
import { Cpu, Ellipsis, ExternalLink, House, RefreshCw, ScrollText, Settings } from "lucide-react"
|
||||
import { Cpu, Ellipsis, ExternalLink, House, RefreshCw, ScrollText, Server, Settings } from "lucide-react"
|
||||
import { cn } from "cn"
|
||||
import { useStrom, useStromLage } from "@/lib/strom"
|
||||
import { Meldungen } from "./Meldungen"
|
||||
@@ -11,10 +11,12 @@ const Dienste = lazy(() => import("@/views/Dienste").then((m) => ({ default: m.D
|
||||
const Einstellungen = lazy(() => import("@/views/Einstellungen").then((m) => ({ default: m.Einstellungen })))
|
||||
const MehrMenue = lazy(() => import("./MehrMenue").then((m) => ({ default: m.MehrMenue })))
|
||||
|
||||
// Start, Updates und Modelle sind der Box-Wart (KI-Box), „Homelab“ der Bereich des Proxmox-PCs.
|
||||
const NAV = [
|
||||
{ to: "/", label: "Start", Icon: House },
|
||||
{ to: "/updates", label: "Updates", Icon: RefreshCw },
|
||||
{ to: "/modelle", label: "Modelle", Icon: Cpu },
|
||||
{ to: "/homelab", label: "Homelab", Icon: Server },
|
||||
] as const
|
||||
|
||||
const uhrzeit = new Intl.DateTimeFormat("de-DE", { hour: "2-digit", minute: "2-digit", timeZone: "Europe/Berlin" })
|
||||
@@ -55,16 +57,19 @@ function Verbindung() {
|
||||
)
|
||||
}
|
||||
|
||||
/** Homelab Orchestrator (vormals MC2, Name seit 24.09.2026). Am Handy reicht das Zeichen: Dort stehen
|
||||
* Verbindung und Uhr daneben, und die Leiste unten sagt, wo man ist. */
|
||||
function Marke() {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" aria-hidden>
|
||||
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" aria-hidden className="shrink-0">
|
||||
<rect x="1.5" y="1.5" width="31" height="31" rx="8" stroke="var(--linie-stark)" strokeWidth="2" />
|
||||
<path d="M7 18h5l3-7 4 13 3-6h5" stroke="var(--bernstein)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
<span className="font-anzeige text-[28px] leading-none font-bold tracking-[0.06em]">MC2</span>
|
||||
<span className="hidden h-6 w-px bg-linie sm:block" />
|
||||
<span className="schild hidden text-base tracking-[0.22em] text-text-3 sm:block">Box-Wart</span>
|
||||
<span className="sr-only sm:not-sr-only sm:flex sm:flex-col sm:leading-none">
|
||||
<span className="font-anzeige text-[22px] font-bold tracking-[0.06em]">Homelab</span>
|
||||
<span className="schild text-[13px] tracking-[0.22em] text-text-3">Orchestrator</span>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -87,7 +92,7 @@ export function Shell() {
|
||||
<Link
|
||||
key={to}
|
||||
to={to}
|
||||
className="schild flex h-11 items-center rounded-[9px] border border-transparent px-5 text-base text-text-3 hover:text-foreground"
|
||||
className="schild flex h-11 items-center rounded-[9px] border border-transparent px-4 text-base text-text-3 hover:text-foreground xl:px-5"
|
||||
activeProps={{ className: "!border-linie-stark bg-erhaben !text-foreground" }}
|
||||
activeOptions={{ exact: true }}
|
||||
>
|
||||
@@ -138,7 +143,7 @@ export function Shell() {
|
||||
{/* Handy und Tablet: Leiste unten, mit Abstand zur Wisch-Leiste des Telefons. */}
|
||||
<nav
|
||||
aria-label="Hauptnavigation"
|
||||
className="fixed inset-x-0 bottom-0 z-40 grid grid-cols-4 gap-1 border-t border-linie bg-[#0e1119]/95 px-2 pt-2 pb-[calc(0.75rem+env(safe-area-inset-bottom))] backdrop-blur lg:hidden"
|
||||
className="fixed inset-x-0 bottom-0 z-40 grid grid-cols-5 gap-1 border-t border-linie bg-[#0e1119]/95 px-2 pt-2 pb-[calc(0.75rem+env(safe-area-inset-bottom))] backdrop-blur lg:hidden"
|
||||
>
|
||||
{NAV.map(({ to, label, Icon }) => (
|
||||
<Link
|
||||
|
||||
@@ -23,8 +23,15 @@ const modelle = createRoute({
|
||||
component: lazyRouteComponent(() => import("@/views/Modelle"), "ModelleSeite"),
|
||||
})
|
||||
|
||||
// Der Homelab-Bereich (Phase 2c): heute Stand der zweiten Instanz, ab Phase 3 die Geräte mit Updates.
|
||||
const homelab = createRoute({
|
||||
getParentRoute: () => wurzel,
|
||||
path: "/homelab",
|
||||
component: lazyRouteComponent(() => import("@/views/Homelab"), "HomelabSeite"),
|
||||
})
|
||||
|
||||
export const router = createRouter({
|
||||
routeTree: wurzel.addChildren([start, updates, modelle]),
|
||||
routeTree: wurzel.addChildren([start, updates, modelle, homelab]),
|
||||
defaultPreload: "intent",
|
||||
defaultErrorComponent: Absturz,
|
||||
})
|
||||
|
||||
@@ -9,7 +9,8 @@ import { fehlerText, laeuftNoch } from "./anzeige"
|
||||
import { melden } from "./meldungen"
|
||||
import { stromVerbunden } from "./strom"
|
||||
import type {
|
||||
AktionsErgebnis, AufraeumKandidat, Job, Modell, Nutzung, Radar, Sicherung, Start, UpdateDetails,
|
||||
AktionsErgebnis, AufraeumKandidat, Instanz, Job, Modell, Nutzung, PartnerStatus, Radar, Sicherung, Start,
|
||||
UpdateDetails,
|
||||
} from "./typen"
|
||||
import { gbText } from "./zeit"
|
||||
|
||||
@@ -253,3 +254,11 @@ export const useRadarSuche = () =>
|
||||
erfolg: () => "Radar sucht nach neuen Modellen.",
|
||||
neuLaden: ["radar"],
|
||||
})
|
||||
|
||||
/** Wer liefert die Seite aus, und gibt es die zweite Instanz? (ändert sich nur mit einem Deploy) */
|
||||
export const useInstanz = () =>
|
||||
useQuery({ queryKey: ["instanz"], queryFn: () => api<Instanz>("/api/instanz"), staleTime: Infinity })
|
||||
|
||||
/** 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 })
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Dünner Zugriff auf das MC2-Backend. Bewusst ohne Anmelde- oder Geheimnis-Felder:
|
||||
// Dünner Zugriff auf das Backend. Bewusst ohne Anmelde- oder Geheimnis-Felder:
|
||||
// Nichts, was der Browser speichert, reist hier mit (Lehre aus v3-Umbau P1).
|
||||
// Seit Phase 2 gibt es zwei Instanzen; welche eine Anfrage beantwortet, entscheidet lib/instanz.ts.
|
||||
|
||||
import { eigeneRolle, zielPfad } from "./instanz"
|
||||
|
||||
export class ApiFehler extends Error {
|
||||
constructor(
|
||||
@@ -22,9 +25,10 @@ function detailAus(body: unknown, status: number): string {
|
||||
}
|
||||
|
||||
export async function api<T>(pfad: string, init: RequestInit = {}): Promise<T> {
|
||||
const ziel = zielPfad(pfad, await eigeneRolle())
|
||||
let antwort: Response
|
||||
try {
|
||||
antwort = await fetch(pfad, {
|
||||
antwort = await fetch(ziel, {
|
||||
...init,
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json", ...init.headers },
|
||||
})
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||
import { eigeneRolle, rolleVergessen, zielPfad } from "./instanz"
|
||||
|
||||
describe("zielPfad", () => {
|
||||
it("die Box beantwortet den Box-Wart selbst und reicht den Homelab-Bereich weiter", () => {
|
||||
expect(zielPfad("/api/start", "box")).toBe("/api/start")
|
||||
expect(zielPfad("/api/homelab/ziele?frisch=1", "box")).toBe("/api/partner/homelab/ziele?frisch=1")
|
||||
expect(zielPfad("/api/homelab", "box")).toBe("/api/partner/homelab")
|
||||
})
|
||||
|
||||
it("der Homelab-Container reicht den Box-Wart weiter", () => {
|
||||
expect(zielPfad("/api/start", "homelab")).toBe("/api/partner/start")
|
||||
expect(zielPfad("/api/homelab/ziele", "homelab")).toBe("/api/homelab/ziele")
|
||||
})
|
||||
|
||||
it("Instanz, Partner und Lebenszeichen bleiben immer lokal", () => {
|
||||
for (const rolle of ["box", "homelab"] as const) {
|
||||
expect(zielPfad("/api/instanz", rolle)).toBe("/api/instanz")
|
||||
expect(zielPfad("/api/partner", rolle)).toBe("/api/partner")
|
||||
expect(zielPfad("/api/partner/health", rolle)).toBe("/api/partner/health")
|
||||
expect(zielPfad("/api/health", rolle)).toBe("/api/health")
|
||||
}
|
||||
// Nur ganze Pfadteile zählen: /api/homelabx gehört nicht zum Homelab-Bereich.
|
||||
expect(zielPfad("/api/homelabx", "box")).toBe("/api/homelabx")
|
||||
expect(zielPfad("/hermes-ui/", "homelab")).toBe("/hermes-ui/")
|
||||
})
|
||||
})
|
||||
|
||||
describe("eigeneRolle", () => {
|
||||
afterEach(() => {
|
||||
rolleVergessen()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it("fragt einmal und merkt sich die Antwort", async () => {
|
||||
const abruf = vi.fn(async () => new Response(JSON.stringify({ rolle: "homelab" }), { status: 200 }))
|
||||
vi.stubGlobal("fetch", abruf)
|
||||
expect(await eigeneRolle()).toBe("homelab")
|
||||
expect(await eigeneRolle()).toBe("homelab")
|
||||
expect(abruf).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("nimmt bei einem Fehler die Box an und fragt beim nächsten Mal neu", async () => {
|
||||
const abruf = vi.fn(async () => new Response("", { status: 404 }))
|
||||
vi.stubGlobal("fetch", abruf)
|
||||
expect(await eigeneRolle()).toBe("box")
|
||||
expect(await eigeneRolle()).toBe("box")
|
||||
expect(abruf).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
// Zwei Instanzen, eine Oberfläche (User-Entscheid 24.09.2026): Die Seite kommt von der KI-Box
|
||||
// (Rolle „box“) oder später vom Container auf dem Proxmox-PC (Rolle „homelab“). Der Homelab-Bereich
|
||||
// fragt unter /api/homelab/…, alles andere gehört zum Box-Wart. Was die ausliefernde Instanz nicht
|
||||
// selbst beantwortet, geht über /api/partner/… an die andere — das entscheidet zielPfad().
|
||||
|
||||
export type Rolle = "box" | "homelab"
|
||||
|
||||
/** Beantwortet immer die Instanz, die die Seite ausliefert. */
|
||||
const IMMER_LOKAL = ["/api/instanz", "/api/partner", "/api/health"]
|
||||
|
||||
const unter = (pfad: string, basis: string) =>
|
||||
pfad === basis || pfad.startsWith(`${basis}/`) || pfad.startsWith(`${basis}?`)
|
||||
|
||||
/** Wohin eine Anfrage der Oberfläche geht, je nach Rolle der Instanz, die die Seite ausliefert. */
|
||||
export function zielPfad(pfad: string, eigene: Rolle): string {
|
||||
if (!pfad.startsWith("/api/") || IMMER_LOKAL.some((b) => unter(pfad, b))) return pfad
|
||||
const bereich: Rolle = unter(pfad, "/api/homelab") ? "homelab" : "box"
|
||||
return bereich === eigene ? pfad : `/api/partner${pfad.slice(4)}`
|
||||
}
|
||||
|
||||
let rolle: Promise<Rolle> | null = null
|
||||
|
||||
/** Rolle der ausliefernden Instanz, einmal gefragt. Scheitert die Frage, gilt „box“ (so war es bis
|
||||
* Phase 2) — und beim nächsten Mal wird neu gefragt. */
|
||||
export function eigeneRolle(): Promise<Rolle> {
|
||||
rolle ??= fetch("/api/instanz", { headers: { Accept: "application/json" } })
|
||||
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(String(r.status)))))
|
||||
.then((d: { rolle?: string }) => (d.rolle === "homelab" ? "homelab" : "box") as Rolle)
|
||||
.catch(() => {
|
||||
rolle = null
|
||||
return "box" as Rolle
|
||||
})
|
||||
return rolle
|
||||
}
|
||||
|
||||
/** Nur für Tests: vergessene Rolle. */
|
||||
export function rolleVergessen() {
|
||||
rolle = null
|
||||
}
|
||||
@@ -222,3 +222,24 @@ export interface Radar {
|
||||
kandidaten: RadarKandidat[]
|
||||
tests: RadarTest[]
|
||||
}
|
||||
|
||||
/** Wer die Seite ausliefert (GET /api/instanz, seit Phase 2). */
|
||||
export interface Instanz {
|
||||
rolle: "box" | "homelab"
|
||||
instanz: string
|
||||
partner: { eingerichtet: boolean; name: string }
|
||||
}
|
||||
|
||||
/** Lebenszeichen der anderen Instanz (GET /api/partner). Ohne zweite Instanz nur `eingerichtet: false`. */
|
||||
export interface PartnerStatus {
|
||||
eingerichtet: boolean
|
||||
name?: string
|
||||
url?: string
|
||||
erreichbar?: boolean
|
||||
rolle?: string | null
|
||||
instanz?: string | null
|
||||
version?: string | null
|
||||
fehler?: string | null
|
||||
/** Unix-Sekunden */
|
||||
geprueft?: number
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Chip, Zustandsfeld } from "@/components/cockpit/Chip"
|
||||
import { Panel } from "@/components/cockpit/Panel"
|
||||
import { useInstanz, usePartner } from "@/lib/abfragen"
|
||||
import type { PartnerStatus } from "@/lib/typen"
|
||||
|
||||
/** Was der Homelab-Teil übernimmt (Bestandsaufnahme vom 24.09.2026; ab Phase 3 kommt die Liste live). */
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
function NochNichtEingerichtet() {
|
||||
return (
|
||||
<>
|
||||
<p className="text-[15px] text-text-2">
|
||||
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.
|
||||
</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
|
||||
zurück.
|
||||
</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="schild text-sm tracking-[0.18em] text-text-3">Dazu kommen</h3>
|
||||
<ul className="flex flex-wrap gap-2">
|
||||
{GERAETE.map((g) => (
|
||||
<li key={g}>
|
||||
<Chip>{g}</Chip>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<p className="text-sm text-text-3">Der Container wird erst nach deiner Freigabe angelegt.</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
? <Chip>Noch nicht eingerichtet</Chip>
|
||||
: p.erreichbar
|
||||
? <Chip art="gruen">Verbunden</Chip>
|
||||
: <Chip art="rot">Antwortet nicht</Chip>
|
||||
|
||||
return (
|
||||
<Panel titel="Homelab" rechts={chip}>
|
||||
{p.eingerichtet ? <Verbindung partner={p} /> : <NochNichtEingerichtet />}
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user