Welle 3 · Oberfläche: Lucys Freiheiten, Rückfragen mit Ja/Nein, Wochenbericht; dist gebaut
- Einstellungen → Lucy: Freiheit je Aktionsart (freier als der Start nur nach Nachfrage), offene und zuletzt beantwortete Rückfragen, Wochenbericht (nächster Termin, letzten lesen, jetzt schicken) - Übersicht: Feld „Rückfrage an dich“ ganz oben, nur wenn eine offen ist; Ja erst nach Bestätigung - Strg+K: Lucys Freiheiten, Rückfragen, Wochenbericht - Einstieg 126,1 KB gzip Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
12cefb427a
commit
1915813cc0
-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 +0,0 @@
|
|||||||
import{i as e,r as t,s as n}from"./dist-aE7UmubC.js";import{t as r}from"./button-DwYvdWVr.js";import{a as i,i as a,n as o,o as s,r as c,t as l}from"./dialog-BEmPjtjX.js";var u=n({Bestaetigen:()=>p}),d=t(),f=e();function p(e){let t=(0,d.c)(32),{offen:n,titel:u,text:p,knopf:m,neinKnopf:h,gefahr:g,gesperrt:_,onJa:v,onNein:y,children:b}=e,x=h===void 0?`Abbrechen`:h,S;t[0]===y?S=t[1]:(S=e=>!e&&y(),t[0]=y,t[1]=S);let C;t[2]===u?C=t[3]:(C=(0,f.jsx)(s,{className:`pr-10 text-[19px] leading-snug font-semibold break-words`,children:u}),t[2]=u,t[3]=C);let w;t[4]===p?w=t[5]:(w=(0,f.jsx)(c,{className:`text-[15px] leading-relaxed text-text-2`,children:p}),t[4]=p,t[5]=w);let T;t[6]!==C||t[7]!==w?(T=(0,f.jsxs)(i,{children:[C,w]}),t[6]=C,t[7]=w,t[8]=T):T=t[8];let E;t[9]!==x||t[10]!==y?(E=(0,f.jsx)(r,{variant:`outline`,onClick:y,children:x}),t[9]=x,t[10]=y,t[11]=E):E=t[11];let D=g?`gefahr`:`default`,O=_||void 0,k;t[12]!==_||t[13]!==v?(k=()=>{_||v()},t[12]=_,t[13]=v,t[14]=k):k=t[14];let A;t[15]!==_||t[16]!==m||t[17]!==D||t[18]!==O||t[19]!==k?(A=(0,f.jsx)(r,{variant:D,disabled:_,"aria-busy":O,onClick:k,children:m}),t[15]=_,t[16]=m,t[17]=D,t[18]=O,t[19]=k,t[20]=A):A=t[20];let j;t[21]!==A||t[22]!==E?(j=(0,f.jsxs)(a,{className:`sticky bottom-0 mb-0 gap-2 bg-[#121722] sm:gap-2`,children:[E,A]}),t[21]=A,t[22]=E,t[23]=j):j=t[23];let M;t[24]!==b||t[25]!==j||t[26]!==T?(M=(0,f.jsxs)(o,{className:`max-h-[calc(100dvh-2rem)] overflow-y-auto border-white/10 bg-[#0e121a] pb-0 sm:max-w-lg`,children:[T,b,j]}),t[24]=b,t[25]=j,t[26]=T,t[27]=M):M=t[27];let N;return t[28]!==n||t[29]!==M||t[30]!==S?(N=(0,f.jsx)(l,{open:n,onOpenChange:S,children:M}),t[28]=n,t[29]=M,t[30]=S,t[31]=N):N=t[31],N}export{u as n,p as t};
|
|
||||||
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
import{i as e,r as t}from"./dist-aE7UmubC.js";import{F as n,I as r}from"./index-BUHDF9RN.js";import{a as i,i as a,n as o,r as s,t as c}from"./sheet-Dx0TG2uq.js";var l=t(),u=e();function d(e){let t=(0,l.c)(8),{onSchliessen:d}=e,f;t[0]===d?f=t[1]:(f=e=>!e&&d(),t[0]=d,t[1]=f);let p;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(p=(0,u.jsxs)(a,{className:`p-0`,children:[(0,u.jsx)(i,{asChild:!0,children:(0,u.jsx)(`div`,{children:(0,u.jsx)(n,{})})}),(0,u.jsx)(s,{className:`sr-only`,children:`Bereiche und Seiten`})]}),t[2]=p):p=t[2];let m;t[3]===d?m=t[4]:(m=(0,u.jsxs)(o,{side:`left`,className:`flex flex-col gap-7 border-white/[0.06] bg-[#0a0d14] px-4 pt-5 pb-[calc(1.25rem+env(safe-area-inset-bottom))] data-[side=left]:w-[300px]`,children:[p,(0,u.jsx)(r,{onGewaehlt:d})]}),t[3]=d,t[4]=m);let h;return t[5]!==f||t[6]!==m?(h=(0,u.jsx)(c,{open:!0,onOpenChange:f,children:m}),t[5]=f,t[6]=m,t[7]=h):h=t[7],h}export{d as MenueSchublade};
|
import{i as e,r as t}from"./dist-aE7UmubC.js";import{G as n,W as r}from"./index-Bumw6krN.js";import{a as i,i as a,n as o,r as s,t as c}from"./sheet-DG7u1mB-.js";var l=t(),u=e();function d(e){let t=(0,l.c)(8),{onSchliessen:d}=e,f;t[0]===d?f=t[1]:(f=e=>!e&&d(),t[0]=d,t[1]=f);let p;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(p=(0,u.jsxs)(a,{className:`p-0`,children:[(0,u.jsx)(i,{asChild:!0,children:(0,u.jsx)(`div`,{children:(0,u.jsx)(r,{})})}),(0,u.jsx)(s,{className:`sr-only`,children:`Bereiche und Seiten`})]}),t[2]=p):p=t[2];let m;t[3]===d?m=t[4]:(m=(0,u.jsxs)(o,{side:`left`,className:`flex flex-col gap-7 border-white/[0.06] bg-[#0a0d14] px-4 pt-5 pb-[calc(1.25rem+env(safe-area-inset-bottom))] data-[side=left]:w-[300px]`,children:[p,(0,u.jsx)(n,{onGewaehlt:d})]}),t[3]=d,t[4]=m);let h;return t[5]!==f||t[6]!==m?(h=(0,u.jsx)(c,{open:!0,onOpenChange:f,children:m}),t[5]=f,t[6]=m,t[7]=h):h=t[7],h}export{d as MenueSchublade};
|
||||||
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
|||||||
import{i as e,r as t,t as n}from"./dist-aE7UmubC.js";var r=t(),i=e();function a(e){let t=(0,r.c)(12),{titel:a,rechts:o,children:s,className:c,id:l,symbol:u}=e,d;t[0]===c?d=t[1]:(d=n(`flaeche flex min-w-0 flex-col gap-4 rounded-2xl p-5 sm:p-6`,c),t[0]=c,t[1]=d);let f;t[2]!==u||t[3]!==o||t[4]!==a?(f=(a||o)&&(0,i.jsxs)(`div`,{className:`flex flex-wrap items-center justify-between gap-3`,children:[a&&(0,i.jsxs)(`h2`,{className:`flex items-center gap-2.5 text-[17px] leading-tight font-semibold tracking-[0.005em] text-foreground`,children:[u&&(0,i.jsx)(u,{className:`size-[18px] text-text-3`,"aria-hidden":!0}),a]}),o]}),t[2]=u,t[3]=o,t[4]=a,t[5]=f):f=t[5];let p;return t[6]!==s||t[7]!==l||t[8]!==d||t[9]!==f||t[10]!==a?(p=(0,i.jsxs)(`section`,{id:l,"aria-label":a,className:d,children:[f,s]}),t[6]=s,t[7]=l,t[8]=d,t[9]=f,t[10]=a,t[11]=p):p=t[11],p}export{a as t};
|
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
import{i as e,r as t}from"./dist-aE7UmubC.js";import{a as n,i as r,n as i,r as a,t as o}from"./sheet-Dx0TG2uq.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:`text-[19px] font-semibold`,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 bg-black/30 ring-1 ring-white/[0.07] ring-inset 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-white/10 bg-[#0e121a] data-[side=right]:w-full data-[side=right]:sm:max-w-2xl`,children:[h,g]}),t[7]=h,t[8]=g,t[9]=_):_=t[9];let v;return t[10]!==l||t[11]!==p||t[12]!==_?(v=(0,c.jsx)(o,{open:l,onOpenChange:p,children:_}),t[10]=l,t[11]=p,t[12]=_,t[13]=v):v=t[13],v}export{l as Protokollfenster};
|
import{i as e,r as t}from"./dist-aE7UmubC.js";import{a as n,i as r,n as i,r as a,t as o}from"./sheet-DG7u1mB-.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:`text-[19px] font-semibold`,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 bg-black/30 ring-1 ring-white/[0.07] ring-inset 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-white/10 bg-[#0e121a] 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
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
|||||||
import{a as e,i as t,r as n,t as r}from"./dist-aE7UmubC.js";import{Q as i}from"./index-BUHDF9RN.js";import{t as a}from"./button-DwYvdWVr.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-oje4j9hw.js";var p=n();e();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":`dialog`,...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":`dialog-portal`,...n}),t[2]=n,t[3]=r),r}function _(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0`,n),t[3]=n,t[4]=a);let s;return t[5]!==i||t[6]!==a?(s=(0,m.jsx)(o,{"data-slot":`dialog-overlay`,className:a,...i}),t[5]=i,t[6]=a,t[7]=s):s=t[7],s}function v(e){let t=(0,p.c)(15),n,o,s,l;t[0]===e?(n=t[1],o=t[2],s=t[3],l=t[4]):({className:o,children:n,showCloseButton:l,...s}=e,t[0]=e,t[1]=n,t[2]=o,t[3]=s,t[4]=l);let d=l===void 0||l,f;t[5]===Symbol.for(`react.memo_cache_sentinel`)?(f=(0,m.jsx)(_,{}),t[5]=f):f=t[5];let h;t[6]===o?h=t[7]:(h=r(`fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95`,o),t[6]=o,t[7]=h);let v;t[8]===d?v=t[9]:(v=d&&(0,m.jsx)(c,{"data-slot":`dialog-close`,asChild:!0,children:(0,m.jsxs)(a,{variant:`ghost`,className:`absolute top-2 right-2`,size:`icon-sm`,children:[(0,m.jsx)(i,{}),(0,m.jsx)(`span`,{className:`sr-only`,children:`Schließen`})]})}),t[8]=d,t[9]=v);let y;return t[10]!==n||t[11]!==s||t[12]!==h||t[13]!==v?(y=(0,m.jsxs)(g,{children:[f,(0,m.jsxs)(u,{"data-slot":`dialog-content`,className:h,...s,children:[n,v]})]}),t[10]=n,t[11]=s,t[12]=h,t[13]=v,t[14]=y):y=t[14],y}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-2`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(`div`,{"data-slot":`dialog-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)(14),n,i,o,s;t[0]===e?(n=t[1],i=t[2],o=t[3],s=t[4]):({className:i,showCloseButton:s,children:n,...o}=e,t[0]=e,t[1]=n,t[2]=i,t[3]=o,t[4]=s);let l=s!==void 0&&s,u;t[5]===i?u=t[6]:(u=r(`-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end`,i),t[5]=i,t[6]=u);let d;t[7]===l?d=t[8]:(d=l&&(0,m.jsx)(c,{asChild:!0,children:(0,m.jsx)(a,{variant:`outline`,children:`Schließen`})}),t[7]=l,t[8]=d);let f;return t[9]!==n||t[10]!==o||t[11]!==u||t[12]!==d?(f=(0,m.jsxs)(`div`,{"data-slot":`dialog-footer`,className:u,...o,children:[n,d]}),t[9]=n,t[10]=o,t[11]=u,t[12]=d,t[13]=f):f=t[13],f}function x(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`font-heading text-base leading-none font-medium`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(d,{"data-slot":`dialog-title`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}function S(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(s,{"data-slot":`dialog-description`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}export{y as a,b as i,v as n,x as o,S as r,h as t};
|
|
||||||
+41
File diff suppressed because one or more lines are too long
-41
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
+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
@@ -0,0 +1 @@
|
|||||||
|
import{a as e,i as t,r as n,t as r}from"./dist-aE7UmubC.js";import{a as i,c as a,i as o,n as s,o as c,r as l,s as u,t as d}from"./dist-CakcBaML.js";import{t as f}from"./button-BHVKhtQ6.js";var p=n();e();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)(d,{"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)(c,{"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,o,c,u;t[0]===e?(n=t[1],i=t[2],o=t[3],c=t[4],u=t[5]):({className:i,children:n,side:c,showCloseButton:u,...o}=e,t[0]=e,t[1]=n,t[2]=i,t[3]=o,t[4]=c,t[5]=u);let d=c===void 0?`right`:c,h=u===void 0||u,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)(s,{"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)(a,{}),(0,m.jsx)(`span`,{className:`sr-only`,children:`Schließen`})]})}),t[9]=h,t[10]=b);let x;return t[11]!==n||t[12]!==o||t[13]!==d||t[14]!==y||t[15]!==b?(x=(0,m.jsxs)(g,{children:[v,(0,m.jsxs)(l,{"data-slot":`sheet-content`,"data-side":d,className:y,...o,children:[n,b]})]}),t[11]=n,t[12]=o,t[13]=d,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)(u,{"data-slot":`sheet-title`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}function x(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`text-sm text-muted-foreground`,n),t[3]=n,t[4]=a);let s;return t[5]!==i||t[6]!==a?(s=(0,m.jsx)(o,{"data-slot":`sheet-description`,className:a,...i}),t[5]=i,t[6]=a,t[7]=s):s=t[7],s}export{b as a,y as i,v as n,x as r,h as t};
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
import{a as e,i as t,r as n,t as r}from"./dist-aE7UmubC.js";import{Q as i}from"./index-BUHDF9RN.js";import{t as a}from"./button-DwYvdWVr.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-oje4j9hw.js";var p=n();e();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),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0`,n),t[3]=n,t[4]=a);let s;return t[5]!==i||t[6]!==a?(s=(0,m.jsx)(o,{"data-slot":`sheet-overlay`,className:a,...i}),t[5]=i,t[6]=a,t[7]=s):s=t[7],s}function v(e){let t=(0,p.c)(17),n,o,s,l,d;t[0]===e?(n=t[1],o=t[2],s=t[3],l=t[4],d=t[5]):({className:o,children:n,side:l,showCloseButton:d,...s}=e,t[0]=e,t[1]=n,t[2]=o,t[3]=s,t[4]=l,t[5]=d);let f=l===void 0?`right`:l,h=d===void 0||d,v;t[6]===Symbol.for(`react.memo_cache_sentinel`)?(v=(0,m.jsx)(_,{}),t[6]=v):v=t[6];let y;t[7]===o?y=t[8]:(y=r(`fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10`,o),t[7]=o,t[8]=y);let b;t[9]===h?b=t[10]:(b=h&&(0,m.jsx)(c,{"data-slot":`sheet-close`,asChild:!0,children:(0,m.jsxs)(a,{variant:`ghost`,className:`absolute top-3 right-3`,size:`icon-sm`,children:[(0,m.jsx)(i,{}),(0,m.jsx)(`span`,{className:`sr-only`,children:`Schließen`})]})}),t[9]=h,t[10]=b);let x;return t[11]!==n||t[12]!==s||t[13]!==f||t[14]!==y||t[15]!==b?(x=(0,m.jsxs)(g,{children:[v,(0,m.jsxs)(u,{"data-slot":`sheet-content`,"data-side":f,className:y,...s,children:[n,b]})]}),t[11]=n,t[12]=s,t[13]=f,t[14]=y,t[15]=b,t[16]=x):x=t[16],x}function y(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`flex flex-col gap-0.5 p-4`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(`div`,{"data-slot":`sheet-header`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}function b(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`font-heading text-base font-medium text-foreground`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(d,{"data-slot":`sheet-title`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}function x(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`text-sm text-muted-foreground`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(s,{"data-slot":`sheet-description`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}export{b as a,y as i,v as n,x as r,h as t};
|
|
||||||
Vendored
+5
-3
@@ -9,13 +9,15 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
<title>Homelab Orchestrator</title>
|
<title>Homelab Orchestrator</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BUHDF9RN.js"></script>
|
<script type="module" crossorigin src="/assets/index-Bumw6krN.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/dist-aE7UmubC.js">
|
<link rel="modulepreload" crossorigin href="/assets/dist-aE7UmubC.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/abfragen-DKcWnZ8a.js">
|
<link rel="modulepreload" crossorigin href="/assets/button-BHVKhtQ6.js">
|
||||||
|
<link rel="modulepreload" crossorigin href="/assets/dist-CakcBaML.js">
|
||||||
|
<link rel="modulepreload" crossorigin href="/assets/abfragen-pD_qBEtc.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/useSelector-DBNxxw17.js">
|
<link rel="modulepreload" crossorigin href="/assets/useSelector-DBNxxw17.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/useMatch-DDVpPKJI.js">
|
<link rel="modulepreload" crossorigin href="/assets/useMatch-DDVpPKJI.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/useRouterState-DJr4hrdj.js">
|
<link rel="modulepreload" crossorigin href="/assets/useRouterState-DJr4hrdj.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DcqanB_e.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D9NqFm_c.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||||
|
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"
|
||||||
|
import { api, post } from "@/lib/api"
|
||||||
|
import type { LucyAutonomie, Wochenbericht } from "@/lib/typen"
|
||||||
|
import { Freiheiten, WochenberichtAbschnitt } from "./LucyEinstellungen"
|
||||||
|
|
||||||
|
vi.mock("@/lib/api", async (original) => ({
|
||||||
|
...(await original<typeof import("@/lib/api")>()), api: vi.fn(), post: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Einstellungen → Lucy (Ausbauplan Welle 3, Punkte 14 und 16, 25.09.2026).
|
||||||
|
const AUTONOMIE: LucyAutonomie = {
|
||||||
|
arten: [
|
||||||
|
{ id: "neustart", name: "Dienste neu starten", text: "…", stufe: "selbst", standard: "selbst" },
|
||||||
|
{ id: "host_neustart", name: "Proxmox-Host neu starten", text: "Alle Container sind weg.", stufe: "fragen", standard: "fragen" },
|
||||||
|
],
|
||||||
|
stufen: { lesen: "Nur lesen und erklären", fragen: "Erst fragen", selbst: "Selbst handeln, Meldung danach" },
|
||||||
|
}
|
||||||
|
|
||||||
|
function zeigen(ui: React.ReactElement, daten: LucyAutonomie | Wochenbericht) {
|
||||||
|
vi.mocked(api).mockResolvedValue(daten)
|
||||||
|
vi.mocked(post).mockResolvedValue({ ok: true, text: "Gespeichert." })
|
||||||
|
render(<QueryClientProvider client={new QueryClient()}>{ui}</QueryClientProvider>)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Lucys Freiheiten", () => {
|
||||||
|
beforeEach(() => vi.clearAllMocks())
|
||||||
|
|
||||||
|
it("strenger stellen geht sofort", async () => {
|
||||||
|
zeigen(<Freiheiten />, AUTONOMIE)
|
||||||
|
const wahl = await screen.findByRole("combobox", { name: "Freiheit: Dienste neu starten" })
|
||||||
|
expect(within(wahl).getByRole("option", { name: "Selbst handeln (Start)" })).toBeInTheDocument()
|
||||||
|
fireEvent.change(wahl, { target: { value: "fragen" } })
|
||||||
|
await waitFor(() => expect(post).toHaveBeenCalledWith("/api/lucy/autonomie", { stufen: { neustart: "fragen" } }))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("freier als der Start erst nach Nachfrage", async () => {
|
||||||
|
zeigen(<Freiheiten />, AUTONOMIE)
|
||||||
|
fireEvent.change(await screen.findByRole("combobox", { name: "Freiheit: Proxmox-Host neu starten" }),
|
||||||
|
{ target: { value: "selbst" } })
|
||||||
|
expect(post).not.toHaveBeenCalled()
|
||||||
|
const dialog = await screen.findByRole("dialog")
|
||||||
|
fireEvent.click(within(dialog).getByRole("button", { name: "Selbst handeln lassen" }))
|
||||||
|
await waitFor(() => expect(post).toHaveBeenCalledWith("/api/lucy/autonomie", { stufen: { host_neustart: "selbst" } }))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Wochenbericht", () => {
|
||||||
|
beforeEach(() => vi.clearAllMocks())
|
||||||
|
|
||||||
|
it("nächster Termin, der letzte zum Nachlesen, jetzt schicken", async () => {
|
||||||
|
const jetzt = Math.floor(Date.now() / 1000)
|
||||||
|
zeigen(<WochenberichtAbschnitt />, { naechster: jetzt + 3 * 86400, zuletzt: { zeit: jetzt - 86400, text: "Commander, ruhige Woche." } })
|
||||||
|
expect(await screen.findByText(/Nächster: .* · zuletzt gestern/)).toBeInTheDocument()
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Letzten lesen" }))
|
||||||
|
expect(screen.getByText("Commander, ruhige Woche.")).toBeInTheDocument()
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Jetzt schicken" }))
|
||||||
|
await waitFor(() => expect(post).toHaveBeenCalledWith("/api/lucy/wochenbericht"))
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { useState } from "react"
|
||||||
|
import { cn } from "cn"
|
||||||
|
import { Bestaetigen } from "@/components/cockpit/Bestaetigen"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { useLucyAutonomie, useLucyAutonomieSetzen, useWochenbericht, useWochenberichtSenden } from "@/lib/abfragen"
|
||||||
|
import type { LucyStufe } from "@/lib/typen"
|
||||||
|
import { flugplanZeit } from "@/lib/zeit"
|
||||||
|
|
||||||
|
// Einstellungen → Lucy (Ausbauplan Welle 3, Punkte 14 und 16, seit 25.09.2026): wie selbstständig Lucy je Aktionsart
|
||||||
|
// handelt und der Wochenbericht. Start ist „mehr Freiheit“ (User-Entscheid 25.09.): selbst neu starten,
|
||||||
|
// Sicherheitsupdates, Rückweg bei Rot; andere Updates, Aufräumen, Modell-Tests und der Host-Neustart nur nach Ja.
|
||||||
|
|
||||||
|
const STUFEN: { id: LucyStufe; kurz: string }[] = [
|
||||||
|
{ id: "lesen", kurz: "Nur lesen" },
|
||||||
|
{ id: "fragen", kurz: "Erst fragen" },
|
||||||
|
{ id: "selbst", kurz: "Selbst handeln" },
|
||||||
|
]
|
||||||
|
const feld = "h-10 rounded-xl border border-white/12 bg-white/[0.04] px-3 text-[15px] text-foreground outline-none focus:border-lab/60"
|
||||||
|
|
||||||
|
/** Die Freiheiten je Aktionsart. Wird eine Art freier als ihr Start (etwa der Host-Neustart auf „selbst“), fragt die
|
||||||
|
* Seite vorher nach — das ist die eine Stelle, an der Lucy mehr darf, als der Commander am 25.09. entschieden hat. */
|
||||||
|
export function Freiheiten() {
|
||||||
|
const q = useLucyAutonomie()
|
||||||
|
const setzen = useLucyAutonomieSetzen()
|
||||||
|
const [frage, setFrage] = useState<{ id: string; name: string; stufe: LucyStufe } | null>(null)
|
||||||
|
if (!q.data) return <p className="text-sm text-text-3">{q.isPending ? "Wird gelesen …" : "Lucys Freiheiten lassen sich gerade nicht lesen."}</p>
|
||||||
|
const rang = (s: LucyStufe) => STUFEN.findIndex((x) => x.id === s)
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p className="m-0 text-sm text-text-2">
|
||||||
|
Bittest du Lucy ausdrücklich um etwas, tut sie es. Hier stellst du ein, was sie aus eigenem Antrieb darf: nur
|
||||||
|
erklären, dich vorher fragen (in Telegram antwortest du mit „ja“ und der Nummer) oder selbst handeln — dann
|
||||||
|
bekommst du danach eine Meldung.
|
||||||
|
</p>
|
||||||
|
<div role="table" aria-label="Lucys Freiheiten" className="flex flex-col">
|
||||||
|
{q.data.arten.map((a) => (
|
||||||
|
<div role="row" key={a.id} className="flex flex-col gap-2 border-t border-white/[0.06] py-3 first:border-t-0 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<span role="rowheader" className="flex min-w-0 flex-col gap-0.5">
|
||||||
|
<span className="text-[15px]">{a.name}</span>
|
||||||
|
<span className="text-xs text-text-3">{a.text}</span>
|
||||||
|
</span>
|
||||||
|
<span role="cell" className="shrink-0">
|
||||||
|
<select aria-label={`Freiheit: ${a.name}`} value={a.stufe} disabled={setzen.isPending}
|
||||||
|
className={cn(feld, "min-w-44", a.stufe !== a.standard && "border-lab/50")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const stufe = e.target.value as LucyStufe
|
||||||
|
if (rang(stufe) > rang(a.standard) && stufe === "selbst") setFrage({ id: a.id, name: a.name, stufe })
|
||||||
|
else setzen.mutate({ [a.id]: stufe })
|
||||||
|
}}>
|
||||||
|
{STUFEN.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>{s.kurz}{s.id === a.standard ? " (Start)" : ""}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Bestaetigen
|
||||||
|
offen={!!frage}
|
||||||
|
titel={frage ? `„${frage.name}“: Lucy selbst handeln lassen?` : ""}
|
||||||
|
text="Dann tut sie es aus eigenem Antrieb, ohne dich vorher zu fragen, und meldet es erst danach. Zurückstellen kannst du es hier jederzeit."
|
||||||
|
knopf="Selbst handeln lassen"
|
||||||
|
gesperrt={setzen.isPending}
|
||||||
|
onNein={() => setFrage(null)}
|
||||||
|
onJa={() => {
|
||||||
|
if (frage) setzen.mutate({ [frage.id]: frage.stufe })
|
||||||
|
setFrage(null)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Der Wochenbericht: wann der nächste kommt, der letzte zum Nachlesen, „Jetzt schicken“. */
|
||||||
|
export function WochenberichtAbschnitt() {
|
||||||
|
const q = useWochenbericht()
|
||||||
|
const senden = useWochenberichtSenden()
|
||||||
|
const [offen, setOffen] = useState(false)
|
||||||
|
const zuletzt = q.data?.zuletzt
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p className="m-0 text-sm text-text-2">
|
||||||
|
Sonntags um 07:30 schickt Lucy eine Nachricht statt vieler: was eingespielt wurde, was offen ist, Platten,
|
||||||
|
Sicherungen, Zertifikate — und offene App-Updates zum Beantworten mit „ja“.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<Button size="sm" variant="outline" disabled={senden.isPending} onClick={() => senden.mutate()}>
|
||||||
|
{senden.isPending ? "Lucy schreibt …" : "Jetzt schicken"}
|
||||||
|
</Button>
|
||||||
|
<span className="text-xs text-text-3">
|
||||||
|
{q.data ? `Nächster: ${flugplanZeit(new Date(q.data.naechster * 1000).toISOString()).toLowerCase()}` : ""}
|
||||||
|
{zuletzt ? ` · zuletzt ${flugplanZeit(new Date(zuletzt.zeit * 1000).toISOString()).toLowerCase()}` : ""}
|
||||||
|
</span>
|
||||||
|
{zuletzt && (
|
||||||
|
<button type="button" className="text-xs text-cyan hover:underline" onClick={() => setOffen(!offen)}>
|
||||||
|
{offen ? "Letzten zuklappen" : "Letzten lesen"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{offen && zuletzt && (
|
||||||
|
<pre className="flaeche-innen m-0 max-h-96 overflow-y-auto rounded-2xl p-4 font-sans text-sm whitespace-pre-wrap text-text-2">
|
||||||
|
{zuletzt.text}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import type { ReactElement } from "react"
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||||
|
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"
|
||||||
|
import { api, post } from "@/lib/api"
|
||||||
|
import type { Rueckfrage, Rueckfragen } from "@/lib/typen"
|
||||||
|
import { ausgangText, nochText, OffeneRueckfragen, RueckfragenPanel, RueckfragenVerlauf } from "./Rueckfragen"
|
||||||
|
|
||||||
|
vi.mock("@/lib/api", async (original) => ({
|
||||||
|
...(await original<typeof import("@/lib/api")>()), api: vi.fn(), post: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Rückfragen an den Commander (Ausbauplan Welle 3, Punkt 15, 25.09.2026): Ja führt genau die gefragte Aktion aus —
|
||||||
|
// in der Oberfläche erst nach der Bestätigung, Nein sofort.
|
||||||
|
const jetzt = Math.floor(Date.now() / 1000)
|
||||||
|
const frage = (nr: number, mehr: Partial<Rueckfrage> = {}): Rueckfrage => ({
|
||||||
|
nr, zeit: jetzt - 600, frage: `Soll ich Gitea 1.27.${nr} einspielen?`, text: "Snapshot vorher, Prüfung danach.",
|
||||||
|
pfad: "/api/partner/homelab/ziele/ct-104/update", von: "wochenbericht", stufe: "normal", ablauf: jetzt + 20 * 3600 - 60,
|
||||||
|
status: "offen", befund: null, ...mehr,
|
||||||
|
})
|
||||||
|
|
||||||
|
function zeigen(ui: ReactElement, daten: Rueckfragen) {
|
||||||
|
vi.mocked(api).mockResolvedValue(daten)
|
||||||
|
vi.mocked(post).mockResolvedValue({ ok: true, text: "Rückfrage 7 ausgeführt: Gestartet." })
|
||||||
|
render(<QueryClientProvider client={new QueryClient()}>{ui}</QueryClientProvider>)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Rückfragen", () => {
|
||||||
|
beforeEach(() => vi.clearAllMocks())
|
||||||
|
|
||||||
|
it("Ja erst nach der Bestätigung, dann genau diese Rückfrage", async () => {
|
||||||
|
zeigen(<RueckfragenPanel />, { offen: [frage(7)], zuletzt: [] })
|
||||||
|
expect(await screen.findByText("Eine Rückfrage an dich")).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(/Rückfrage 7 · Wochenbericht · vor 10 min · gilt noch 19 h/)).toBeInTheDocument()
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Ja, ausführen" }))
|
||||||
|
expect(post).not.toHaveBeenCalled()
|
||||||
|
const dialog = await screen.findByRole("dialog")
|
||||||
|
expect(within(dialog).getByText(/wie „ja 7“ in Telegram/)).toBeInTheDocument()
|
||||||
|
fireEvent.click(within(dialog).getByRole("button", { name: "Ja, ausführen" }))
|
||||||
|
await waitFor(() => expect(post).toHaveBeenCalledWith("/api/rueckfragen/7/ja"))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Nein ohne Umweg", async () => {
|
||||||
|
zeigen(<OffeneRueckfragen />, { offen: [frage(7), frage(8, { von: "lucy", stufe: "dringend" })], zuletzt: [] })
|
||||||
|
const nein = await screen.findAllByRole("button", { name: "Nein" })
|
||||||
|
fireEvent.click(nein[1])
|
||||||
|
await waitFor(() => expect(post).toHaveBeenCalledWith("/api/rueckfragen/8/nein"))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("ohne offene Rückfrage zeigt die Übersicht nichts, die Einstellungen einen Satz", async () => {
|
||||||
|
zeigen(<><RueckfragenPanel /><OffeneRueckfragen leerText="Gerade wartet keine Rückfrage auf dich." /></>,
|
||||||
|
{ offen: [], zuletzt: [] })
|
||||||
|
expect(await screen.findByText("Gerade wartet keine Rückfrage auf dich.")).toBeInTheDocument()
|
||||||
|
expect(screen.queryByText(/Rückfrage an dich/)).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Verlauf: was daraus wurde und wer geantwortet hat", async () => {
|
||||||
|
zeigen(<RueckfragenVerlauf />, { offen: [], zuletzt: [
|
||||||
|
frage(5, { status: "erledigt", antwort: { zeit: jetzt, wer: "lucy", ja: true } }),
|
||||||
|
frage(4, { status: "fehler", ergebnis: "Es läuft gerade ‚Alle aktualisieren‘.", antwort: { zeit: jetzt, wer: "oberflaeche", ja: true } }),
|
||||||
|
] })
|
||||||
|
expect(await screen.findByText(/ausgeführt · per Telegram/)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(/gescheitert · hier — Es läuft gerade/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Texte", () => {
|
||||||
|
expect(nochText(jetzt + 90 * 60, jetzt * 1000)).toBe("gilt noch 1 h")
|
||||||
|
expect(nochText(jetzt + 20, jetzt * 1000)).toBe("gilt noch 1 min")
|
||||||
|
expect(ausgangText(frage(1, { status: "abgelaufen" }))).toBe("abgelaufen")
|
||||||
|
expect(ausgangText(frage(1, { status: "erledigt", nachgeprueft: "besteht", antwort: { zeit: 0, wer: "oberflaeche", ja: true } })))
|
||||||
|
.toBe("ausgeführt · hier · der Hinweis blieb")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { useState } from "react"
|
||||||
|
import { MessageCircleQuestionMark } from "lucide-react"
|
||||||
|
import { cn } from "cn"
|
||||||
|
import { Bestaetigen } from "@/components/cockpit/Bestaetigen"
|
||||||
|
import { Panel } from "@/components/cockpit/Panel"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { useRueckfrageAntwort, useRueckfragen } from "@/lib/abfragen"
|
||||||
|
import type { Rueckfrage, RueckfrageStatus } from "@/lib/typen"
|
||||||
|
import { vor } from "@/lib/zeit"
|
||||||
|
|
||||||
|
// Rückfragen an den Commander (Ausbauplan Welle 3, Punkt 15, seit 25.09.2026): Lucy, der Wächter und der Wochenbericht
|
||||||
|
// fragen, bevor etwas passiert, das er bestätigen soll. Dieselben Fragen kommen in Telegram mit „Antworte „ja 7““ und
|
||||||
|
// zwei Antworttasten; hier geht es per Knopf. Ja führt genau die gefragte Aktion aus — sonst nichts.
|
||||||
|
|
||||||
|
const VON: Record<string, string> = {
|
||||||
|
lucy: "Lucy", waechter: "Wächter", wochenbericht: "Wochenbericht", wartung: "Wartungsfenster",
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS: Record<RueckfrageStatus, string> = {
|
||||||
|
offen: "offen", laeuft: "läuft", erledigt: "ausgeführt", fehler: "gescheitert", abgelehnt: "abgelehnt",
|
||||||
|
abgelaufen: "abgelaufen", hinfaellig: "hatte sich erledigt",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const vonText = (von: string) => VON[von] ?? von
|
||||||
|
|
||||||
|
/** Wie lange eine Rückfrage noch gilt: „gilt noch 22 h“, „gilt noch 40 min“. */
|
||||||
|
export function nochText(ablauf: number, jetzt = Date.now()): string {
|
||||||
|
const s = Math.max(0, Math.round(ablauf - jetzt / 1000))
|
||||||
|
return s >= 3600 ? `gilt noch ${Math.floor(s / 3600)} h` : `gilt noch ${Math.max(1, Math.round(s / 60))} min`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Eine Zeile des Verlaufs: was daraus wurde, wer geantwortet hat. */
|
||||||
|
export function ausgangText(r: Rueckfrage): string {
|
||||||
|
const wer = r.antwort ? (r.antwort.wer === "oberflaeche" ? "hier" : r.antwort.wer === "lucy" ? "per Telegram" : r.antwort.wer) : null
|
||||||
|
const teile = [STATUS[r.status] ?? r.status]
|
||||||
|
if (wer && r.status !== "abgelaufen") teile.push(wer)
|
||||||
|
if (r.nachgeprueft === "besteht") teile.push("der Hinweis blieb")
|
||||||
|
return teile.join(" · ")
|
||||||
|
}
|
||||||
|
|
||||||
|
function Eintrag({ r, gesperrt, onJa, onNein }: {
|
||||||
|
r: Rueckfrage
|
||||||
|
gesperrt: boolean
|
||||||
|
onJa: () => void
|
||||||
|
onNein: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<li className={cn("flaeche-innen flex flex-col gap-3 rounded-2xl p-4", r.stufe === "dringend" && "ring-1 ring-rot/40 ring-inset")}>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="etikett text-[10.5px] text-text-3">
|
||||||
|
Rückfrage {r.nr} · {vonText(r.von)} · {vor(r.zeit)} · {nochText(r.ablauf)}
|
||||||
|
</span>
|
||||||
|
<span className="text-[15px] font-semibold break-words">{r.frage}</span>
|
||||||
|
{r.text && <span className="text-sm break-words text-text-2">{r.text}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button size="sm" disabled={gesperrt} onClick={onJa}>Ja, ausführen</Button>
|
||||||
|
<Button variant="outline" size="sm" disabled={gesperrt} onClick={onNein}>Nein</Button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Die offenen Rückfragen mit Ja und Nein. Ohne offene Rückfrage zeigt das Feld nichts (Übersicht) bzw. einen Satz. */
|
||||||
|
export function OffeneRueckfragen({ leerText }: { leerText?: string }) {
|
||||||
|
const q = useRueckfragen()
|
||||||
|
const antwort = useRueckfrageAntwort()
|
||||||
|
const [frage, setFrage] = useState<Rueckfrage | null>(null)
|
||||||
|
const offen = q.data?.offen ?? []
|
||||||
|
if (offen.length === 0) return leerText ? <p className="text-sm text-text-2">{leerText}</p> : null
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ul className="m-0 grid list-none gap-3 p-0 md:grid-cols-2">
|
||||||
|
{offen.map((r) => (
|
||||||
|
<Eintrag key={r.nr} r={r} gesperrt={antwort.isPending}
|
||||||
|
onJa={() => setFrage(r)} onNein={() => antwort.mutate({ nr: r.nr, ja: false })} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<Bestaetigen
|
||||||
|
offen={!!frage}
|
||||||
|
titel={frage ? frage.frage : ""}
|
||||||
|
text={frage ? `${frage.text ? `${frage.text} ` : ""}Das führt genau diese Aktion aus — wie „ja ${frage.nr}“ in Telegram.` : ""}
|
||||||
|
knopf="Ja, ausführen"
|
||||||
|
gesperrt={antwort.isPending}
|
||||||
|
onNein={() => setFrage(null)}
|
||||||
|
onJa={() => {
|
||||||
|
if (frage) antwort.mutate({ nr: frage.nr, ja: true })
|
||||||
|
setFrage(null)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Für die Übersicht: ein Feld, das nur erscheint, wenn eine Rückfrage offen ist. */
|
||||||
|
export function RueckfragenPanel() {
|
||||||
|
const q = useRueckfragen()
|
||||||
|
const anzahl = q.data?.offen.length ?? 0
|
||||||
|
if (anzahl === 0) return null
|
||||||
|
return (
|
||||||
|
<Panel titel={anzahl === 1 ? "Eine Rückfrage an dich" : `${anzahl} Rückfragen an dich`} symbol={MessageCircleQuestionMark} id="rueckfragen">
|
||||||
|
<p className="-mt-2 text-[15px] text-text-2">
|
||||||
|
Lucy wartet auf dein Ja. In Telegram reicht „ja“ mit der Nummer — oder hier ein Klick.
|
||||||
|
</p>
|
||||||
|
<OffeneRueckfragen />
|
||||||
|
</Panel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Die zuletzt beantworteten oder abgelaufenen Rückfragen (Einstellungen → Lucy). */
|
||||||
|
export function RueckfragenVerlauf() {
|
||||||
|
const q = useRueckfragen()
|
||||||
|
const zuletzt = (q.data?.zuletzt ?? []).slice(0, 8)
|
||||||
|
if (zuletzt.length === 0) return <p className="text-sm text-text-3">Noch keine beantwortete Rückfrage.</p>
|
||||||
|
return (
|
||||||
|
<ul className="m-0 flex list-none flex-col p-0">
|
||||||
|
{zuletzt.map((r) => (
|
||||||
|
<li key={r.nr} className="flex flex-col gap-0.5 border-t border-white/[0.06] py-2 first:border-t-0">
|
||||||
|
<span className="text-sm break-words">{r.frage}</span>
|
||||||
|
<span className="text-xs text-text-3">
|
||||||
|
Rückfrage {r.nr} · {vonText(r.von)} · {vor(r.zeit)} · {ausgangText(r)}
|
||||||
|
{r.status === "fehler" && r.ergebnis ? ` — ${r.ergebnis}` : ""}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -10,7 +10,8 @@ import { melden } from "./meldungen"
|
|||||||
import { stromVerbunden } from "./strom"
|
import { stromVerbunden } from "./strom"
|
||||||
import type {
|
import type {
|
||||||
Ablauf, AktionsErgebnis, Altrest, AufraeumKandidat, BoxMesswerte, EinstellungenStand, Hinweis, HomelabEinstellungen,
|
Ablauf, AktionsErgebnis, Altrest, AufraeumKandidat, BoxMesswerte, EinstellungenStand, Hinweis, HomelabEinstellungen,
|
||||||
HomelabLauf, HomelabMesswerte, HomelabSpeicher, HomelabStand, HomelabZertifikate, NeuesGeraet, NeuerStack, NeuWahl, DockerLage, Neuerungen, HomelabWartung, WartungsRegel, Instanz, InstanzMitStand, Job, Modell, Nutzung, PartnerStatus,
|
HomelabLauf, HomelabMesswerte, HomelabSpeicher, HomelabStand, HomelabZertifikate, NeuesGeraet, NeuerStack, NeuWahl, DockerLage, Neuerungen, HomelabWartung, WartungsRegel,
|
||||||
|
LucyAutonomie, LucyStufe, Rueckfragen, Wochenbericht, Instanz, InstanzMitStand, Job, Modell, Nutzung, PartnerStatus,
|
||||||
ProtokollEintrag, Radar, SammelPlanSchritt, Sammellauf, Sicherung, Start, TelegramTest, UpdateDetails, ZielAktion,
|
ProtokollEintrag, Radar, SammelPlanSchritt, Sammellauf, Sicherung, Start, TelegramTest, UpdateDetails, ZielAktion,
|
||||||
ZielStand, Zeitraum,
|
ZielStand, Zeitraum,
|
||||||
} from "./typen"
|
} from "./typen"
|
||||||
@@ -511,6 +512,53 @@ export const useNeuerungen = (zid: string, baustein: string, aktiv: boolean) =>
|
|||||||
retry: false,
|
retry: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** Lucys Freiheiten je Aktionsart (seit 25.09.2026, Einstellungen → Lucy). */
|
||||||
|
export const useLucyAutonomie = (aktiv = true) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: ["lucy-autonomie"],
|
||||||
|
queryFn: () => api<LucyAutonomie>("/api/lucy/autonomie"),
|
||||||
|
enabled: aktiv,
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useLucyAutonomieSetzen = () =>
|
||||||
|
useAktion((stufen: Record<string, LucyStufe>) => post<AktionsErgebnis>("/api/lucy/autonomie", { stufen }), {
|
||||||
|
erfolg: (_, e) => (e as AktionsErgebnis).text ?? "Gespeichert.",
|
||||||
|
neuLaden: ["lucy-autonomie"],
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Rückfragen an den Commander (seit 25.09.2026): offene und die zuletzt beantworteten. */
|
||||||
|
export const useRueckfragen = (aktiv = true) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: ["rueckfragen"],
|
||||||
|
queryFn: () => api<Rueckfragen>("/api/rueckfragen"),
|
||||||
|
enabled: aktiv,
|
||||||
|
refetchInterval: takt(30_000),
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Ja oder Nein auf eine Rückfrage — wie „ja 7“ in Telegram: Ja führt genau die gefragte Aktion aus. */
|
||||||
|
export const useRueckfrageAntwort = () =>
|
||||||
|
useAktion(({ nr, ja }: { nr: number; ja: boolean }) => post<AktionsErgebnis>(`/api/rueckfragen/${nr}/${ja ? "ja" : "nein"}`), {
|
||||||
|
erfolg: (_, e) => (e as AktionsErgebnis).text ?? "Erledigt.",
|
||||||
|
neuLaden: ["rueckfragen", "start", "jobs", "homelab", "homelab-laeufe"],
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Der Wochenbericht (seit 25.09.2026): der letzte und wann der nächste kommt. */
|
||||||
|
export const useWochenbericht = (aktiv = true) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: ["wochenbericht"],
|
||||||
|
queryFn: () => api<Wochenbericht>("/api/lucy/wochenbericht"),
|
||||||
|
enabled: aktiv,
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useWochenberichtSenden = () =>
|
||||||
|
useAktion(() => post<AktionsErgebnis>("/api/lucy/wochenbericht"), {
|
||||||
|
erfolg: (_, e) => (e as AktionsErgebnis).text ?? "Der Wochenbericht ist unterwegs.",
|
||||||
|
neuLaden: ["wochenbericht", "rueckfragen"],
|
||||||
|
})
|
||||||
|
|
||||||
/** Wartungsfenster und Regel je Gerät (seit 25.09.2026). */
|
/** Wartungsfenster und Regel je Gerät (seit 25.09.2026). */
|
||||||
export const useWartung = (aktiv: boolean) =>
|
export const useWartung = (aktiv: boolean) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
|
|||||||
@@ -34,4 +34,10 @@ describe("Befehlssuche", () => {
|
|||||||
expect(treffer.filter((b) => b.text === "Updates").map((b) => b.unter)).toEqual(["KI-Box", "Homelab"])
|
expect(treffer.filter((b) => b.text === "Updates").map((b) => b.unter)).toEqual(["KI-Box", "Homelab"])
|
||||||
expect(suchen(alle, "gibtesnicht")).toEqual([])
|
expect(suchen(alle, "gibtesnicht")).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("findet Lucys Freiheiten, Rückfragen und den Wochenbericht (seit 25.09.2026)", () => {
|
||||||
|
expect(suchen(alle, "lucy freiheit")[0]).toMatchObject({ to: "/einstellungen", hash: "lucy" })
|
||||||
|
expect(suchen(alle, "rückfragen")[0]).toMatchObject({ text: "Rückfragen an dich" })
|
||||||
|
expect(suchen(alle, "wochenbericht")[0]).toMatchObject({ text: "Wochenbericht", hash: "lucy" })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ export function befehle(ziele: ZielStand[]): Befehl[] {
|
|||||||
id: `seite:${s.to}`, text: s.label, unter: ORT_NAME[s.ort], to: s.to,
|
id: `seite:${s.to}`, text: s.label, unter: ORT_NAME[s.ort], to: s.to,
|
||||||
stichworte: s.to.replace(/\//g, " "),
|
stichworte: s.to.replace(/\//g, " "),
|
||||||
}))
|
}))
|
||||||
|
// Seit 25.09.2026 (Ausbauplan Welle 3): Lucys Freiheiten, ihre Rückfragen und der Wochenbericht.
|
||||||
|
liste.push(
|
||||||
|
{ id: "lucy:freiheiten", text: "Lucys Freiheiten", unter: "Einstellungen → Lucy", to: "/einstellungen", hash: "lucy",
|
||||||
|
stichworte: "lucy autonomie selbst fragen" },
|
||||||
|
{ id: "lucy:rueckfragen", text: "Rückfragen an dich", unter: "Einstellungen → Lucy", to: "/einstellungen", hash: "lucy",
|
||||||
|
stichworte: "lucy ja nein telegram rueckfrage" },
|
||||||
|
{ id: "lucy:wochenbericht", text: "Wochenbericht", unter: "Einstellungen → Lucy", to: "/einstellungen", hash: "lucy",
|
||||||
|
stichworte: "lucy bericht sonntag" },
|
||||||
|
)
|
||||||
for (const z of ziele) {
|
for (const z of ziele) {
|
||||||
const geraet = { art: z.art, offen: z.offen }
|
const geraet = { art: z.art, offen: z.offen }
|
||||||
const stichworte = `${ART_WORT[z.art] ?? ""} ${z.id}`
|
const stichworte = `${ART_WORT[z.art] ?? ""} ${z.id}`
|
||||||
|
|||||||
@@ -841,3 +841,53 @@ export interface Ablauf {
|
|||||||
status: "ok" | "ueberfaellig" | "gestoppt" | "aus"
|
status: "ok" | "ueberfaellig" | "gestoppt" | "aus"
|
||||||
text: string | null
|
text: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Lucy: Freiheiten, Rückfragen, Wochenbericht (seit 25.09.2026, Ausbauplan Welle 3) ------------------------------
|
||||||
|
|
||||||
|
export type LucyStufe = "lesen" | "fragen" | "selbst"
|
||||||
|
|
||||||
|
/** Eine Aktionsart mit Lucys Freiheit (GET /api/lucy/autonomie). */
|
||||||
|
export interface LucyArt {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
text: string
|
||||||
|
stufe: LucyStufe
|
||||||
|
standard: LucyStufe
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LucyAutonomie {
|
||||||
|
arten: LucyArt[]
|
||||||
|
stufen: Record<LucyStufe, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RueckfrageStatus = "offen" | "laeuft" | "erledigt" | "fehler" | "abgelehnt" | "abgelaufen" | "hinfaellig"
|
||||||
|
|
||||||
|
/** Eine Rückfrage an den Commander (GET /api/rueckfragen) — auch in Telegram mit „ja 7“ zu beantworten. */
|
||||||
|
export interface Rueckfrage {
|
||||||
|
nr: number
|
||||||
|
/** Unix-Sekunden */
|
||||||
|
zeit: number
|
||||||
|
frage: string
|
||||||
|
text: string
|
||||||
|
pfad: string
|
||||||
|
von: string
|
||||||
|
stufe: "dringend" | "normal" | "leise"
|
||||||
|
/** Unix-Sekunden: danach gilt die Rückfrage nicht mehr */
|
||||||
|
ablauf: number
|
||||||
|
status: RueckfrageStatus
|
||||||
|
befund: { bereich: "box" | "homelab"; id: string; titel: string } | null
|
||||||
|
antwort?: { zeit: number; wer: string; ja: boolean }
|
||||||
|
ergebnis?: string
|
||||||
|
nachgeprueft?: "behoben" | "besteht"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Rueckfragen {
|
||||||
|
offen: Rueckfrage[]
|
||||||
|
zuletzt: Rueckfrage[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /api/lucy/wochenbericht: der letzte Bericht und wann der nächste kommt (Unix-Sekunden). */
|
||||||
|
export interface Wochenbericht {
|
||||||
|
zuletzt: { zeit: number; text: string } | null
|
||||||
|
naechster: number
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { type ReactNode, useState } from "react"
|
import { type ReactNode, useEffect, useState } from "react"
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
import { cn } from "cn"
|
import { cn } from "cn"
|
||||||
import { ExternalLink, KeyRound } from "lucide-react"
|
import { ExternalLink, KeyRound } from "lucide-react"
|
||||||
import { Bestaetigen } from "@/components/cockpit/Bestaetigen"
|
import { Bestaetigen } from "@/components/cockpit/Bestaetigen"
|
||||||
import { Chip } from "@/components/cockpit/Chip"
|
import { Chip } from "@/components/cockpit/Chip"
|
||||||
import { Wartungsfenster } from "@/components/homelab/Wartungsfenster"
|
import { Wartungsfenster } from "@/components/homelab/Wartungsfenster"
|
||||||
|
import { Freiheiten, WochenberichtAbschnitt } from "@/components/lucy/LucyEinstellungen"
|
||||||
|
import { OffeneRueckfragen, RueckfragenVerlauf } from "@/components/lucy/Rueckfragen"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
useAdguardZugang, useArcaneEcht, useArcaneSchluessel, useEinstellungenStand, useHomelabEinstellungen, useInstanz, usePartner,
|
useAdguardZugang, useArcaneEcht, useArcaneSchluessel, useEinstellungenStand, useHomelabEinstellungen, useInstanz, usePartner,
|
||||||
@@ -62,10 +64,11 @@ function Abschnitt({ titel, children }: { titel: string; children: ReactNode })
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Eine Karte der Einstellungsseite; KI-Box und Homelab tragen ihre Bereichsfarbe als Schein und Rand (Design v5). */
|
/** Eine Karte der Einstellungsseite; KI-Box und Homelab tragen ihre Bereichsfarbe als Schein und Rand (Design v5). */
|
||||||
function Karte({ titel, text, farbe, children }: {
|
function Karte({ titel, text, farbe, id, children }: {
|
||||||
titel: string
|
titel: string
|
||||||
text: string
|
text: string
|
||||||
farbe?: "box" | "homelab"
|
farbe?: "box" | "homelab"
|
||||||
|
id?: string
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}) {
|
}) {
|
||||||
const glut = farbe === "box" ? "var(--box)" : farbe === "homelab" ? "var(--lab)" : null
|
const glut = farbe === "box" ? "var(--box)" : farbe === "homelab" ? "var(--lab)" : null
|
||||||
@@ -73,7 +76,7 @@ function Karte({ titel, text, farbe, children }: {
|
|||||||
const rand = farbe === "box" ? "[--rand:color-mix(in_srgb,var(--box)_28%,transparent)]"
|
const rand = farbe === "box" ? "[--rand:color-mix(in_srgb,var(--box)_28%,transparent)]"
|
||||||
: farbe === "homelab" ? "[--rand:color-mix(in_srgb,var(--lab)_28%,transparent)]" : ""
|
: farbe === "homelab" ? "[--rand:color-mix(in_srgb,var(--lab)_28%,transparent)]" : ""
|
||||||
return (
|
return (
|
||||||
<section aria-label={titel} className={cn("flaeche relative flex min-w-0 flex-col gap-4 overflow-hidden rounded-3xl p-5 sm:p-6", rand)}>
|
<section id={id} aria-label={titel} className={cn("flaeche relative flex min-w-0 flex-col gap-4 overflow-hidden rounded-3xl p-5 sm:p-6", rand)}>
|
||||||
{glut && (
|
{glut && (
|
||||||
<div aria-hidden className="pointer-events-none absolute -top-40 -left-32 size-[400px] rounded-full"
|
<div aria-hidden className="pointer-events-none absolute -top-40 -left-32 size-[400px] rounded-full"
|
||||||
style={{ background: `radial-gradient(closest-side, color-mix(in srgb, ${glut} 16%, transparent), transparent)` }} />
|
style={{ background: `radial-gradient(closest-side, color-mix(in srgb, ${glut} 16%, transparent), transparent)` }} />
|
||||||
@@ -625,11 +628,29 @@ function HomelabKarte() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Lucy: Freiheiten, Rückfragen, Wochenbericht (seit 25.09.2026, Ausbauplan Welle 3) --------------------------------
|
||||||
|
|
||||||
|
function LucyKarte() {
|
||||||
|
return (
|
||||||
|
<Karte id="lucy" titel="Lucy" text="Was sie aus eigenem Antrieb darf, ihre Rückfragen an dich und der Wochenbericht.">
|
||||||
|
<Abschnitt titel="Wie selbstständig Lucy handelt"><Freiheiten /></Abschnitt>
|
||||||
|
<Abschnitt titel="Offene Rückfragen"><OffeneRueckfragen leerText="Gerade wartet keine Rückfrage auf dich." /></Abschnitt>
|
||||||
|
<Abschnitt titel="Zuletzt beantwortet"><RueckfragenVerlauf /></Abschnitt>
|
||||||
|
<Abschnitt titel="Wochenbericht"><WochenberichtAbschnitt /></Abschnitt>
|
||||||
|
</Karte>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// --- Seite ----------------------------------------------------------------------------------------------
|
// --- Seite ----------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
export function EinstellungenSeite() {
|
export function EinstellungenSeite() {
|
||||||
const box = useEinstellungenStand(true)
|
const box = useEinstellungenStand(true)
|
||||||
const boxProps = { e: box.data, laedt: box.isPending, fehler: box.error?.message }
|
const boxProps = { e: box.data, laedt: box.isPending, fehler: box.error?.message }
|
||||||
|
// Strg+K „Lucys Freiheiten“ führt mit #lucy hierher (beim Öffnen der Seite; ohne Router-Hook, damit die Seite auch
|
||||||
|
// allein testbar bleibt).
|
||||||
|
useEffect(() => {
|
||||||
|
if (window.location.hash === "#lucy") document.getElementById("lucy")?.scrollIntoView({ block: "start" })
|
||||||
|
}, [])
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-5 md:gap-6 xl:grid-cols-2">
|
<div className="grid gap-5 md:gap-6 xl:grid-cols-2">
|
||||||
<Karte titel="KI-Box" farbe="box" text="Wann sie sich aktualisiert, das Modell-Radar und Zugänge.">
|
<Karte titel="KI-Box" farbe="box" text="Wann sie sich aktualisiert, das Modell-Radar und Zugänge.">
|
||||||
@@ -644,6 +665,7 @@ export function EinstellungenSeite() {
|
|||||||
</Abschnitt>
|
</Abschnitt>
|
||||||
</Karte>
|
</Karte>
|
||||||
<HomelabKarte />
|
<HomelabKarte />
|
||||||
|
<LucyKarte />
|
||||||
<Karte titel="Meldungen" text="Telegram, Morgenmeldung und was sofort kommt.">
|
<Karte titel="Meldungen" text="Telegram, Morgenmeldung und was sofort kommt.">
|
||||||
<Meldungen />
|
<Meldungen />
|
||||||
</Karte>
|
</Karte>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { cn } from "cn"
|
|||||||
import { Chip, type ChipArt, Zustandsfeld } from "@/components/cockpit/Chip"
|
import { Chip, type ChipArt, Zustandsfeld } from "@/components/cockpit/Chip"
|
||||||
import { Geraetesymbol } from "@/components/cockpit/Geraetesymbol"
|
import { Geraetesymbol } from "@/components/cockpit/Geraetesymbol"
|
||||||
import { Messring } from "@/components/diagramme/Messring"
|
import { Messring } from "@/components/diagramme/Messring"
|
||||||
|
import { RueckfragenPanel } from "@/components/lucy/Rueckfragen"
|
||||||
import { Lagering, type Segment } from "@/components/uebersicht/Lagering"
|
import { Lagering, type Segment } from "@/components/uebersicht/Lagering"
|
||||||
import {
|
import {
|
||||||
useBoxMesswerte, useBoxZiele, useHomelab, useHomelabHinweise, useHomelabLaeufe, useHomelabMesswerte, useHomelabSpeicher,
|
useBoxMesswerte, useBoxZiele, useHomelab, useHomelabHinweise, useHomelabLaeufe, useHomelabMesswerte, useHomelabSpeicher,
|
||||||
@@ -650,6 +651,9 @@ export function UebersichtSeite() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Rückfragen an den Commander (seit 25.09.2026): nur, wenn eine offen ist — ganz oben, weil sie auf ihn warten. */}
|
||||||
|
<RueckfragenPanel />
|
||||||
|
|
||||||
<Instrumente homelabAktiv={homelabAktiv} />
|
<Instrumente homelabAktiv={homelabAktiv} />
|
||||||
|
|
||||||
<div className="grid gap-5 md:gap-6 xl:grid-cols-2">
|
<div className="grid gap-5 md:gap-6 xl:grid-cols-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user