diff --git a/admin-ui/dist/assets/index-4xN7Sa3G.js b/admin-ui/dist/assets/index-4xN7Sa3G.js deleted file mode 100644 index 35d0c99..0000000 --- a/admin-ui/dist/assets/index-4xN7Sa3G.js +++ /dev/null @@ -1,5 +0,0 @@ -(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))o(i);new MutationObserver(i=>{for(const r of i)if(r.type==="childList")for(const l of r.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&o(l)}).observe(document,{childList:!0,subtree:!0});function n(i){const r={};return i.integrity&&(r.integrity=i.integrity),i.referrerPolicy&&(r.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?r.credentials="include":i.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function o(i){if(i.ep)return;i.ep=!0;const r=n(i);fetch(i.href,r)}})();var G,m,xe,N,ce,Se,Te,Y,D,M,Ce,_e,ee,te,j={},z=[],De=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,J=Array.isArray;function P(t,e){for(var n in e)t[n]=e[n];return t}function ie(t){t&&t.parentNode&&t.parentNode.removeChild(t)}function Fe(t,e,n){var o,i,r,l={};for(r in e)r=="key"?o=e[r]:r=="ref"?i=e[r]:l[r]=e[r];if(arguments.length>2&&(l.children=arguments.length>3?G.call(arguments,2):n),typeof t=="function"&&t.defaultProps!=null)for(r in t.defaultProps)l[r]===void 0&&(l[r]=t.defaultProps[r]);return F(t,l,o,i,null)}function F(t,e,n,o,i){var r={type:t,props:e,key:n,ref:o,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:i??++xe,__i:-1,__u:0};return i==null&&m.vnode!=null&&m.vnode(r),r}function V(t){return t.children}function W(t,e){this.props=t,this.context=e}function I(t,e){if(e==null)return t.__?I(t.__,t.__i+1):null;for(var n;ee&&N.sort(Te),t=N.shift(),e=N.length,We(t)}finally{N.length=q.__r=0}}function Pe(t,e,n,o,i,r,l,a,d,s,u){var p,_,f,y,w,k,b,h=o&&o.__k||z,S=e.length;for(d=Re(n,e,h,d,S),p=0;p0?l=t.__k[r]=F(l.type,l.props,l.key,l.ref?l.ref:null,l.__v):t.__k[r]=l,d=r+_,l.__=t,l.__b=t.__b+1,a=null,(s=l.__i=je(l,n,d,p))!=-1&&(p--,(a=n[s])&&(a.__u|=2)),a==null||a.__v==null?(s==-1&&(i>u?_--:id?_--:_++,l.__u|=4))):t.__k[r]=null;if(p)for(r=0;r(u?1:0)){for(i=n-1,r=n+1;i>=0||r=0?i--:r++])!=null&&!(2&s.__u)&&a==s.key&&d==s.type)return l}return-1}function de(t,e,n){e[0]=="-"?t.setProperty(e,n??""):t[e]=n==null?"":typeof n!="number"||De.test(e)?n:n+"px"}function U(t,e,n,o,i){var r,l;e:if(e=="style")if(typeof n=="string")t.style.cssText=n;else{if(typeof o=="string"&&(t.style.cssText=o=""),o)for(e in o)n&&e in n||de(t.style,e,"");if(n)for(e in n)o&&n[e]==o[e]||de(t.style,e,n[e])}else if(e[0]=="o"&&e[1]=="n")r=e!=(e=e.replace(Ce,"$1")),l=e.toLowerCase(),e=l in t||e=="onFocusOut"||e=="onFocusIn"?l.slice(2):e.slice(2),t.l||(t.l={}),t.l[e+r]=n,n?o?n[M]=o[M]:(n[M]=_e,t.addEventListener(e,r?te:ee,r)):t.removeEventListener(e,r?te:ee,r);else{if(i=="http://www.w3.org/2000/svg")e=e.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(e!="width"&&e!="height"&&e!="href"&&e!="list"&&e!="form"&&e!="tabIndex"&&e!="download"&&e!="rowSpan"&&e!="colSpan"&&e!="role"&&e!="popover"&&e in t)try{t[e]=n??"";break e}catch{}typeof n=="function"||(n==null||n===!1&&e[4]!="-"?t.removeAttribute(e):t.setAttribute(e,e=="popover"&&n==1?"":n))}}function fe(t){return function(e){if(this.l){var n=this.l[e.type+t];if(e[D]==null)e[D]=_e++;else if(e[D]0?t:J(t)?t.map(He):t.constructor!==void 0?null:P({},t)}function ze(t,e,n,o,i,r,l,a,d){var s,u,p,_,f,y,w,k=n.props||j,b=e.props,h=e.type;if(h=="svg"?i="http://www.w3.org/2000/svg":h=="math"?i="http://www.w3.org/1998/Math/MathML":i||(i="http://www.w3.org/1999/xhtml"),r!=null){for(s=0;s=n.__.length&&n.__.push({}),n.__[t]}function x(t){return ne=1,Je(Be,t)}function Je(t,e,n){var o=Le(K++,2);if(o.t=t,!o.__c&&(o.__=[Be(void 0,e),function(a){var d=o.__N?o.__N[0]:o.__[0],s=o.t(d,a);d!==s&&(o.__N=[s,o.__[1]],o.__c.setState({}))}],o.__c=v,!v.__f)){var i=function(a,d,s){if(!o.__c.__H)return!0;var u=!1,p=o.__c.props!==a;if(o.__c.__H.__.some(function(f){if(f.__N){u=!0;var y=f.__[0];f.__=f.__N,f.__N=void 0,y!==f.__[0]&&(p=!0)}}),r){var _=r.call(this,a,d,s);return u?_||p:_}return!u||p};v.__f=!0;var r=v.shouldComponentUpdate,l=v.componentWillUpdate;v.componentWillUpdate=function(a,d,s){if(this.__e){var u=r;r=void 0,i(a,d,s),r=u}l&&l.call(this,a,d,s)},v.shouldComponentUpdate=i}return o.__N||o.__}function Me(t,e){var n=Le(K++,3);!g.__s&&Xe(n.__H,e)&&(n.__=t,n.u=e,v.__H.__h.push(n))}function Ve(){for(var t;t=Oe.shift();){var e=t.__H;if(t.__P&&e)try{e.__h.some(R),e.__h.some(re),e.__h=[]}catch(n){e.__h=[],g.__e(n,t.__v)}}}g.__b=function(t){v=null,he&&he(t)},g.__=function(t,e){t&&e.__k&&e.__k.__m&&(t.__m=e.__k.__m),be&&be(t,e)},g.__r=function(t){me&&me(t),K=0;var e=(v=t.__c).__H;e&&(Z===v?(e.__h=[],v.__h=[],e.__.some(function(n){n.__N&&(n.__=n.__N),n.u=n.__N=void 0})):(e.__h.some(R),e.__h.some(re),e.__h=[],K=0)),Z=v},g.diffed=function(t){ye&&ye(t);var e=t.__c;e&&e.__H&&(e.__H.__h.length&&(Oe.push(e)!==1&&pe===g.requestAnimationFrame||((pe=g.requestAnimationFrame)||Qe)(Ve)),e.__H.__.some(function(n){n.u&&(n.__H=n.u,n.u=void 0)})),Z=v=null},g.__c=function(t,e){e.some(function(n){try{n.__h.some(R),n.__h=n.__h.filter(function(o){return!o.__||re(o)})}catch(o){e.some(function(i){i.__h&&(i.__h=[])}),e=[],g.__e(o,n.__v)}}),ve&&ve(t,e)},g.unmount=function(t){ge&&ge(t);var e,n=t.__c;n&&n.__H&&(n.__H.__.some(function(o){try{R(o)}catch(i){e=i}}),n.__H=void 0,e&&g.__e(e,n.__v))};var ke=typeof requestAnimationFrame=="function";function Qe(t){var e,n=function(){clearTimeout(o),ke&&cancelAnimationFrame(e),setTimeout(t)},o=setTimeout(n,35);ke&&(e=requestAnimationFrame(n))}function R(t){var e=v,n=t.__c;typeof n=="function"&&(t.__c=void 0,n()),v=e}function re(t){var e=v;t.__c=t.__(),v=e}function Xe(t,e){return!t||t.length!==e.length||e.some(function(n,o){return n!==t[o]})}function Be(t,e){return typeof e=="function"?e(t):e}let oe=null;function Ue(t){oe=t}async function T(t,e,n){const o={};oe&&(o.Authorization=`Bearer ${oe}`),n!==void 0&&(o["Content-Type"]="application/json");const i=await fetch(e,{method:t,headers:o,body:n!==void 0?JSON.stringify(n):void 0}),r=await i.text(),l=r?JSON.parse(r):null;if(!i.ok){const a=l?.error??l?.message??`${i.status} ${i.statusText}`;throw new Error(a)}return l}const C={challenge:()=>T("POST","/admin/auth/challenge",{}),verify:(t,e)=>T("POST","/admin/auth/verify",{admin_token:t,challenge:e}),logout:()=>T("POST","/admin/auth/logout",{}),me:()=>T("GET","/admin/me"),features:()=>T("GET","/admin/features"),keys:()=>T("GET","/admin/keys"),addKey:(t,e)=>T("POST","/admin/keys",e?{pubkey:t,label:e}:{pubkey:t}),revokeKey:t=>T("DELETE",`/admin/keys/${t}`),config:()=>T("GET","/admin/config"),putConfig:(t,e)=>T("PUT","/admin/config",{patch:t,expected_versions:e}),invites:()=>T("GET","/admin/invites"),mintInvites:(t,e)=>T("POST","/admin/invites",e?{count:t,label:e}:{count:t})};async function we(t){const e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t));return[...new Uint8Array(e)].map(n=>n.toString(16).padStart(2,"0")).join("")}async function Ye(t,e){const o=`POST -/admin/auth/verify - -${await we("")}`,i=await we(o),r=await e.signEvent({kind:27235,created_at:Math.floor(Date.now()/1e3),content:"",tags:[["u",t.url],["method","POST"],["purpose","admin_login"],["challenge",t.challenge],["payload",i],["instance_id",t.instance_id]]});return`Nostr ${btoa(JSON.stringify(r))}`}function Ze(){const t=window.nostr;return t&&typeof t.signEvent=="function"?t:null}function et({onDone:t}){const[e,n]=x(!1),[o,i]=x(null);return c("div",{style:{maxWidth:420,margin:"12vh auto",padding:24,textAlign:"center"},children:[c("div",{style:{fontSize:40},children:"🐸"}),c("h1",{children:"Smirk Operator Console"}),c("p",{style:{color:"var(--smirk-fg-muted)"},children:"Sign in with an admin Nostr key on your instance's allowlist."}),c("button",{class:"primary",style:{padding:"12px 20px",fontSize:16},disabled:e,onClick:async()=>{n(!0),i(null);try{const l=Ze();if(!l)throw new Error("No NIP-07 signer found. Enable your Smirk wallet (or nos2x/Alby) on this origin, then reload.");const a=await C.challenge(),d=await Ye(a,l),{access_token:s}=await C.verify(d,a.challenge);Ue(s),t()}catch(l){i(l instanceof Error?l.message:"Sign-in failed")}finally{n(!1)}},children:e?"Signing in…":"Sign in with Smirk"}),o&&c("p",{style:{color:"var(--smirk-danger)",marginTop:16},children:o})]})}function H({title:t,children:e}){return c("div",{style:{background:"var(--smirk-bg-elevated)",border:"1px solid var(--smirk-border)",borderRadius:"var(--smirk-radius)",padding:16,marginBottom:16},children:[c("h3",{style:{marginTop:0},children:t}),e]})}function Q(t,e=[]){const[n,o]=x(null),[i,r]=x(null),l=()=>{r(null),t().then(o).catch(a=>r(a instanceof Error?a.message:String(a)))};return Me(l,e),{data:n,err:i,reload:l}}function tt(){const{data:t,err:e}=Q(()=>C.features());return c(H,{title:"Instance features",children:[e&&c("p",{style:{color:"var(--smirk-danger)"},children:e}),c("pre",{style:{overflow:"auto",fontSize:12},children:JSON.stringify(t,null,2)})]})}function nt(){const{data:t,err:e,reload:n}=Q(()=>C.keys()),[o,i]=x(""),[r,l]=x(!1),[a,d]=x(null),s=async()=>{l(!0),d(null);try{await C.addKey(o.trim().toLowerCase()),i(""),n()}catch(p){d(p instanceof Error?p.message:String(p))}finally{l(!1)}},u=async p=>{confirm("Revoke this admin key?")&&(await C.revokeKey(p),n())};return c(H,{title:"Admin keys",children:[e&&c("p",{style:{color:"var(--smirk-danger)"},children:e}),c("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:13},children:c("tbody",{children:(t??[]).map(p=>c("tr",{style:{borderTop:"1px solid var(--smirk-border)"},children:[c("td",{style:{fontFamily:"monospace",padding:"6px 4px"},children:[p.pubkey.slice(0,16),"…"]}),c("td",{style:{padding:"6px 4px"},children:p.status}),c("td",{style:{padding:"6px 4px",textAlign:"right"},children:p.status!=="revoked"&&c("button",{class:"danger",onClick:()=>u(p.id),children:"Revoke"})})]}))})}),c("div",{style:{display:"flex",gap:8,marginTop:12},children:[c("input",{style:{flex:1},placeholder:"new admin pubkey (64 hex)",value:o,onInput:p=>i(p.target.value)}),c("button",{class:"primary",disabled:r||o.trim().length!==64,onClick:s,children:"Add key"})]}),a&&c("p",{style:{color:"var(--smirk-danger)"},children:a})]})}function rt(){const{data:t,err:e,reload:n}=Q(()=>C.invites()),[o,i]=x(5),[r,l]=x(""),[a,d]=x(null),[s,u]=x(!1),p=async()=>{u(!0);try{const _=await C.mintInvites(o,r.trim()||void 0);d(_.codes),n()}finally{u(!1)}};return c(H,{title:`Invite codes${t?` — ${t.unused_count} unused`:""}`,children:[e&&c("p",{style:{color:"var(--smirk-danger)"},children:e}),c("div",{style:{display:"flex",gap:8,alignItems:"center",marginBottom:12},children:[c("input",{type:"number",min:1,max:1e3,style:{width:80},value:o,onInput:_=>i(Number(_.target.value))}),c("input",{style:{flex:1},placeholder:"label (optional)",value:r,onInput:_=>l(_.target.value)}),c("button",{class:"primary",disabled:s,onClick:p,children:"Mint"})]}),a&&c("div",{style:{background:"var(--smirk-bg-sunken)",padding:12,borderRadius:8,marginBottom:12},children:[c("b",{children:"New codes — copy now, they are shown once:"}),c("pre",{style:{margin:"6px 0 0",whiteSpace:"pre-wrap"},children:a.join(` -`)})]}),c("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:13},children:c("tbody",{children:(t?.invites??[]).map(_=>c("tr",{style:{borderTop:"1px solid var(--smirk-border)"},children:[c("td",{style:{fontFamily:"monospace",padding:"6px 4px"},children:[_.code_prefix,"…"]}),c("td",{style:{padding:"6px 4px"},children:_.label??""}),c("td",{style:{padding:"6px 4px",color:"var(--smirk-fg-muted)"},children:_.created_at.slice(0,10)}),c("td",{style:{padding:"6px 4px"},children:_.status})]}))})})]})}function ot(){const{data:t,err:e,reload:n}=Q(()=>C.config()),[o,i]=x({}),[r,l]=x(null);if(Me(()=>{t&&i(structuredClone(t.effective))},[t]),e)return c(H,{title:"Config",children:c("p",{style:{color:"var(--smirk-danger)"},children:e})});if(!t)return c(H,{title:"Config",children:"Loading…"});const a=(s,u,p)=>i(_=>({..._,[s]:{..._[s]??{},[u]:p}})),d=async s=>{l(null);try{await C.putConfig({[s]:o[s]},{[s]:t.versions[s]??0}),l(`Saved ${s}.`),n()}catch(u){l(u instanceof Error?u.message:String(u))}};return c("div",{children:[t.restart_pending&&c("div",{style:{background:"rgba(245,158,11,0.12)",border:"1px solid var(--smirk-warn)",borderRadius:8,padding:10,marginBottom:16},children:"⚠ A saved change needs a restart to take effect (systemctl restart, or auto mode)."}),r&&c("p",{style:{color:"var(--smirk-accent)"},children:r}),Object.keys(o).sort().map(s=>c(H,{title:s,children:[Object.entries(o[s]??{}).map(([u,p])=>{const _=t.runtime_class[`${s}.${u}`]??"restart-required",f=t.overlay[s]?.[u]!==void 0;return c("div",{style:{display:"flex",alignItems:"center",gap:8,padding:"4px 0"},children:[c("label",{style:{flex:1,minWidth:0},children:[u," ",c("span",{style:{fontSize:11,color:"var(--smirk-fg-muted)"},children:["· ",_==="restart-required"?"restart":"live",f?" · db":""]})]}),typeof p=="boolean"?c("input",{type:"checkbox",checked:p,onChange:y=>a(s,u,y.target.checked)}):c("input",{style:{width:220},value:String(p??""),onInput:y=>{const w=y.target.value;a(s,u,typeof p=="number"?Number(w):w)}})]})}),c("button",{class:"primary",style:{marginTop:8},onClick:()=>d(s),children:["Save ",s]})]}))]})}const _t=["Status","Keys","Invites","Config"];function it({onLogout:t}){const[e,n]=x("Status");return c("div",{style:{maxWidth:760,margin:"0 auto",padding:20},children:[c("header",{style:{display:"flex",alignItems:"center",marginBottom:16},children:[c("h2",{style:{margin:0,flex:1},children:"🐸 Smirk Operator Console"}),c("button",{onClick:()=>{C.logout().catch(()=>{}),Ue(null),t()},children:"Sign out"})]}),c("nav",{style:{display:"flex",gap:6,marginBottom:16},children:_t.map(o=>c("button",{class:o===e?"primary":"",onClick:()=>n(o),children:o}))}),e==="Status"&&c(tt,{}),e==="Keys"&&c(nt,{}),e==="Invites"&&c(rt,{}),e==="Config"&&c(ot,{})]})}function lt(){const[t,e]=x(!1);return t?c(it,{onLogout:()=>e(!1)}):c(et,{onDone:()=>e(!0)})}Ke(c(lt,{}),document.getElementById("app")); diff --git a/admin-ui/dist/assets/index-7a1tzG-S.js b/admin-ui/dist/assets/index-7a1tzG-S.js new file mode 100644 index 0000000..96b8d5e --- /dev/null +++ b/admin-ui/dist/assets/index-7a1tzG-S.js @@ -0,0 +1,5 @@ +(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const _ of document.querySelectorAll('link[rel="modulepreload"]'))o(_);new MutationObserver(_=>{for(const r of _)if(r.type==="childList")for(const s of r.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&o(s)}).observe(document,{childList:!0,subtree:!0});function n(_){const r={};return _.integrity&&(r.integrity=_.integrity),_.referrerPolicy&&(r.referrerPolicy=_.referrerPolicy),_.crossOrigin==="use-credentials"?r.credentials="include":_.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function o(_){if(_.ep)return;_.ep=!0;const r=n(_);fetch(_.href,r)}})();var J,y,xe,P,ce,Se,Te,Y,D,M,Ce,ie,ee,te,R={},z=[],De=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,G=Array.isArray;function N(t,e){for(var n in e)t[n]=e[n];return t}function _e(t){t&&t.parentNode&&t.parentNode.removeChild(t)}function Fe(t,e,n){var o,_,r,s={};for(r in e)r=="key"?o=e[r]:r=="ref"?_=e[r]:s[r]=e[r];if(arguments.length>2&&(s.children=arguments.length>3?J.call(arguments,2):n),typeof t=="function"&&t.defaultProps!=null)for(r in t.defaultProps)s[r]===void 0&&(s[r]=t.defaultProps[r]);return F(t,s,o,_,null)}function F(t,e,n,o,_){var r={type:t,props:e,key:n,ref:o,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:_??++xe,__i:-1,__u:0};return _==null&&y.vnode!=null&&y.vnode(r),r}function V(t){return t.children}function j(t,e){this.props=t,this.context=e}function H(t,e){if(e==null)return t.__?H(t.__,t.__i+1):null;for(var n;ee&&P.sort(Te),t=P.shift(),e=P.length,je(t)}finally{P.length=q.__r=0}}function Ne(t,e,n,o,_,r,s,a,f,l,d){var p,i,u,h,w,v,k,m=o&&o.__k||z,S=e.length;for(f=We(n,e,m,f,S),p=0;p0?s=t.__k[r]=F(s.type,s.props,s.key,s.ref?s.ref:null,s.__v):t.__k[r]=s,f=r+i,s.__=t,s.__b=t.__b+1,a=null,(l=s.__i=Re(s,n,f,p))!=-1&&(p--,(a=n[l])&&(a.__u|=2)),a==null||a.__v==null?(l==-1&&(_>d?i--:_f?i--:i++,s.__u|=4))):t.__k[r]=null;if(p)for(r=0;r(d?1:0)){for(_=n-1,r=n+1;_>=0||r=0?_--:r++])!=null&&!(2&l.__u)&&a==l.key&&f==l.type)return s}return-1}function de(t,e,n){e[0]=="-"?t.setProperty(e,n??""):t[e]=n==null?"":typeof n!="number"||De.test(e)?n:n+"px"}function U(t,e,n,o,_){var r,s;e:if(e=="style")if(typeof n=="string")t.style.cssText=n;else{if(typeof o=="string"&&(t.style.cssText=o=""),o)for(e in o)n&&e in n||de(t.style,e,"");if(n)for(e in n)o&&n[e]==o[e]||de(t.style,e,n[e])}else if(e[0]=="o"&&e[1]=="n")r=e!=(e=e.replace(Ce,"$1")),s=e.toLowerCase(),e=s in t||e=="onFocusOut"||e=="onFocusIn"?s.slice(2):e.slice(2),t.l||(t.l={}),t.l[e+r]=n,n?o?n[M]=o[M]:(n[M]=ie,t.addEventListener(e,r?te:ee,r)):t.removeEventListener(e,r?te:ee,r);else{if(_=="http://www.w3.org/2000/svg")e=e.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(e!="width"&&e!="height"&&e!="href"&&e!="list"&&e!="form"&&e!="tabIndex"&&e!="download"&&e!="rowSpan"&&e!="colSpan"&&e!="role"&&e!="popover"&&e in t)try{t[e]=n??"";break e}catch{}typeof n=="function"||(n==null||n===!1&&e[4]!="-"?t.removeAttribute(e):t.setAttribute(e,e=="popover"&&n==1?"":n))}}function fe(t){return function(e){if(this.l){var n=this.l[e.type+t];if(e[D]==null)e[D]=ie++;else if(e[D]0?t:G(t)?t.map(Ae):t.constructor!==void 0?null:N({},t)}function ze(t,e,n,o,_,r,s,a,f){var l,d,p,i,u,h,w,v=n.props||R,k=e.props,m=e.type;if(m=="svg"?_="http://www.w3.org/2000/svg":m=="math"?_="http://www.w3.org/1998/Math/MathML":_||(_="http://www.w3.org/1999/xhtml"),r!=null){for(l=0;l=n.__.length&&n.__.push({}),n.__[t]}function x(t){return ne=1,Ge(Be,t)}function Ge(t,e,n){var o=Le(K++,2);if(o.t=t,!o.__c&&(o.__=[Be(void 0,e),function(a){var f=o.__N?o.__N[0]:o.__[0],l=o.t(f,a);f!==l&&(o.__N=[l,o.__[1]],o.__c.setState({}))}],o.__c=g,!g.__f)){var _=function(a,f,l){if(!o.__c.__H)return!0;var d=!1,p=o.__c.props!==a;if(o.__c.__H.__.some(function(u){if(u.__N){d=!0;var h=u.__[0];u.__=u.__N,u.__N=void 0,h!==u.__[0]&&(p=!0)}}),r){var i=r.call(this,a,f,l);return d?i||p:i}return!d||p};g.__f=!0;var r=g.shouldComponentUpdate,s=g.componentWillUpdate;g.componentWillUpdate=function(a,f,l){if(this.__e){var d=r;r=void 0,_(a,f,l),r=d}s&&s.call(this,a,f,l)},g.shouldComponentUpdate=_}return o.__N||o.__}function Me(t,e){var n=Le(K++,3);!b.__s&&Xe(n.__H,e)&&(n.__=t,n.u=e,g.__H.__h.push(n))}function Ve(){for(var t;t=Ie.shift();){var e=t.__H;if(t.__P&&e)try{e.__h.some(W),e.__h.some(re),e.__h=[]}catch(n){e.__h=[],b.__e(n,t.__v)}}}b.__b=function(t){g=null,he&&he(t)},b.__=function(t,e){t&&e.__k&&e.__k.__m&&(t.__m=e.__k.__m),be&&be(t,e)},b.__r=function(t){me&&me(t),K=0;var e=(g=t.__c).__H;e&&(Z===g?(e.__h=[],g.__h=[],e.__.some(function(n){n.__N&&(n.__=n.__N),n.u=n.__N=void 0})):(e.__h.some(W),e.__h.some(re),e.__h=[],K=0)),Z=g},b.diffed=function(t){ye&&ye(t);var e=t.__c;e&&e.__H&&(e.__H.__h.length&&(Ie.push(e)!==1&&pe===b.requestAnimationFrame||((pe=b.requestAnimationFrame)||Qe)(Ve)),e.__H.__.some(function(n){n.u&&(n.__H=n.u,n.u=void 0)})),Z=g=null},b.__c=function(t,e){e.some(function(n){try{n.__h.some(W),n.__h=n.__h.filter(function(o){return!o.__||re(o)})}catch(o){e.some(function(_){_.__h&&(_.__h=[])}),e=[],b.__e(o,n.__v)}}),ge&&ge(t,e)},b.unmount=function(t){ve&&ve(t);var e,n=t.__c;n&&n.__H&&(n.__H.__.some(function(o){try{W(o)}catch(_){e=_}}),n.__H=void 0,e&&b.__e(e,n.__v))};var ke=typeof requestAnimationFrame=="function";function Qe(t){var e,n=function(){clearTimeout(o),ke&&cancelAnimationFrame(e),setTimeout(t)},o=setTimeout(n,35);ke&&(e=requestAnimationFrame(n))}function W(t){var e=g,n=t.__c;typeof n=="function"&&(t.__c=void 0,n()),g=e}function re(t){var e=g;t.__c=t.__(),g=e}function Xe(t,e){return!t||t.length!==e.length||e.some(function(n,o){return n!==t[o]})}function Be(t,e){return typeof e=="function"?e(t):e}let oe=null;function Ue(t){oe=t}async function T(t,e,n){const o={};oe&&(o.Authorization=`Bearer ${oe}`),n!==void 0&&(o["Content-Type"]="application/json");const _=await fetch(e,{method:t,headers:o,body:n!==void 0?JSON.stringify(n):void 0}),r=await _.text(),s=r?JSON.parse(r):null;if(!_.ok){const a=s?.error??s?.message??`${_.status} ${_.statusText}`;throw new Error(a)}return s}const C={challenge:()=>T("POST","/admin/auth/challenge",{}),verify:(t,e)=>T("POST","/admin/auth/verify",{admin_token:t,challenge:e}),logout:()=>T("POST","/admin/auth/logout",{}),me:()=>T("GET","/admin/me"),features:()=>T("GET","/admin/features"),keys:()=>T("GET","/admin/keys").then(t=>t.keys),addKey:(t,e)=>T("POST","/admin/keys",e?{pubkey:t,label:e}:{pubkey:t}),revokeKey:t=>T("DELETE",`/admin/keys/${t}`),config:()=>T("GET","/admin/config"),putConfig:(t,e)=>T("PUT","/admin/config",{patch:t,expected_versions:e}),invites:()=>T("GET","/admin/invites"),mintInvites:(t,e)=>T("POST","/admin/invites",e?{count:t,label:e}:{count:t})};async function we(t){const e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t));return[...new Uint8Array(e)].map(n=>n.toString(16).padStart(2,"0")).join("")}async function Ye(t,e){const o=`POST +/admin/auth/verify + +${await we("")}`,_=await we(o),r=await e.signEvent({kind:27235,created_at:Math.floor(Date.now()/1e3),content:"",tags:[["u",t.url],["method","POST"],["purpose","admin_login"],["challenge",t.challenge],["payload",_],["instance_id",t.instance_id]]});return`Nostr ${btoa(JSON.stringify(r))}`}function Ze(){const t=window.nostr;return t&&typeof t.signEvent=="function"?t:null}function et({onDone:t}){const[e,n]=x(!1),[o,_]=x(null);return c("div",{style:{maxWidth:420,margin:"12vh auto",padding:24,textAlign:"center"},children:[c("div",{style:{fontSize:40},children:"🐸"}),c("h1",{children:"Smirk Operator Console"}),c("p",{style:{color:"var(--smirk-fg-muted)"},children:"Sign in with an admin Nostr key on your instance's allowlist."}),c("button",{class:"primary",style:{padding:"12px 20px",fontSize:16},disabled:e,onClick:async()=>{n(!0),_(null);try{const s=Ze();if(!s)throw new Error("No NIP-07 signer found. Enable your Smirk wallet (or nos2x/Alby) on this origin, then reload.");const a=await C.challenge(),f=await Ye(a,s),{access_token:l}=await C.verify(f,a.challenge);Ue(l),t()}catch(s){_(s instanceof Error?s.message:"Sign-in failed")}finally{n(!1)}},children:e?"Signing in…":"Sign in with Smirk"}),o&&c("p",{style:{color:"var(--smirk-danger)",marginTop:16},children:o})]})}function A({title:t,children:e}){return c("div",{style:{background:"var(--smirk-bg-elevated)",border:"1px solid var(--smirk-border)",borderRadius:"var(--smirk-radius)",padding:16,marginBottom:16},children:[c("h3",{style:{marginTop:0},children:t}),e]})}function Q(t,e=[]){const[n,o]=x(null),[_,r]=x(null),s=()=>{r(null),t().then(o).catch(a=>r(a instanceof Error?a.message:String(a)))};return Me(s,e),{data:n,err:_,reload:s}}function tt(){const{data:t,err:e}=Q(()=>C.features());return c(A,{title:"Instance features",children:[e&&c("p",{style:{color:"var(--smirk-danger)"},children:e}),c("pre",{style:{overflow:"auto",fontSize:12},children:JSON.stringify(t,null,2)})]})}function nt(){const{data:t,err:e,reload:n}=Q(()=>C.keys()),[o,_]=x(""),[r,s]=x(!1),[a,f]=x(null),l=async()=>{s(!0),f(null);try{await C.addKey(o.trim().toLowerCase()),_(""),n()}catch(p){f(p instanceof Error?p.message:String(p))}finally{s(!1)}},d=async p=>{if(confirm("Revoke this admin key?")){f(null);try{await C.revokeKey(p),n()}catch(i){f(i instanceof Error?i.message:String(i))}}};return c(A,{title:"Admin keys",children:[e&&c("p",{style:{color:"var(--smirk-danger)"},children:e}),c("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:13},children:c("tbody",{children:(t??[]).map(p=>c("tr",{style:{borderTop:"1px solid var(--smirk-border)"},children:[c("td",{style:{fontFamily:"monospace",padding:"6px 4px"},children:[p.pubkey.slice(0,16),"…"]}),c("td",{style:{padding:"6px 4px"},children:p.status}),c("td",{style:{padding:"6px 4px",textAlign:"right"},children:p.status!=="revoked"&&c("button",{class:"danger",onClick:()=>d(p.id),children:"Revoke"})})]}))})}),c("div",{style:{display:"flex",gap:8,marginTop:12},children:[c("input",{style:{flex:1},placeholder:"new admin pubkey (64 hex)",value:o,onInput:p=>_(p.target.value)}),c("button",{class:"primary",disabled:r||o.trim().length!==64,onClick:l,children:"Add key"})]}),a&&c("p",{style:{color:"var(--smirk-danger)"},children:a})]})}function rt(){const{data:t,err:e,reload:n}=Q(()=>C.invites()),[o,_]=x(5),[r,s]=x(""),[a,f]=x(null),[l,d]=x(!1),[p,i]=x(null),u=async()=>{d(!0),i(null);try{const h=await C.mintInvites(o,r.trim()||void 0);f(h.codes),n()}catch(h){i(h instanceof Error?h.message:String(h))}finally{d(!1)}};return c(A,{title:`Invite codes${t?` — ${t.unused_count} unused`:""}`,children:[e&&c("p",{style:{color:"var(--smirk-danger)"},children:e}),c("div",{style:{display:"flex",gap:8,alignItems:"center",marginBottom:12},children:[c("input",{type:"number",min:1,max:1e3,style:{width:80},value:o,onInput:h=>_(Number(h.target.value))}),c("input",{style:{flex:1},placeholder:"label (optional)",value:r,onInput:h=>s(h.target.value)}),c("button",{class:"primary",disabled:l||o<1,onClick:u,children:"Mint"})]}),p&&c("p",{style:{color:"var(--smirk-danger)"},children:p}),a&&c("div",{style:{background:"var(--smirk-bg-sunken)",padding:12,borderRadius:8,marginBottom:12},children:[c("b",{children:"New codes — copy now, they are shown once:"}),c("pre",{style:{margin:"6px 0 0",whiteSpace:"pre-wrap"},children:a.join(` +`)})]}),c("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:13},children:c("tbody",{children:(t?.invites??[]).map(h=>c("tr",{style:{borderTop:"1px solid var(--smirk-border)"},children:[c("td",{style:{fontFamily:"monospace",padding:"6px 4px"},children:[h.code_prefix,"…"]}),c("td",{style:{padding:"6px 4px"},children:h.label??""}),c("td",{style:{padding:"6px 4px",color:"var(--smirk-fg-muted)"},children:h.created_at.slice(0,10)}),c("td",{style:{padding:"6px 4px"},children:h.status})]}))})})]})}function ot(){const{data:t,err:e,reload:n}=Q(()=>C.config()),[o,_]=x({}),[r,s]=x(null);if(Me(()=>{t&&_(l=>{const d={...l};for(const p of Object.keys(t.effective))p in d||(d[p]=structuredClone(t.effective[p]));return d})},[t]),e)return c(A,{title:"Config",children:c("p",{style:{color:"var(--smirk-danger)"},children:e})});if(!t)return c(A,{title:"Config",children:"Loading…"});const a=(l,d,p)=>_(i=>({...i,[l]:{...i[l]??{},[d]:p}})),f=async l=>{s(null);const d=t.effective[l]??{},p=o[l]??{},i={};for(const u of Object.keys(p))JSON.stringify(p[u])!==JSON.stringify(d[u])&&(i[u]=p[u]);if(Object.keys(i).length===0){s(`No changes in ${l}.`);return}try{await C.putConfig({[l]:i},{[l]:t.versions[l]??0}),s(`Saved ${l}.`),n()}catch(u){s(u instanceof Error?u.message:String(u))}};return c("div",{children:[t.restart_pending&&c("div",{style:{background:"rgba(245,158,11,0.12)",border:"1px solid var(--smirk-warn)",borderRadius:8,padding:10,marginBottom:16},children:"⚠ A saved change needs a restart to take effect (systemctl restart, or auto mode)."}),r&&c("p",{style:{color:"var(--smirk-accent)"},children:r}),Object.keys(o).sort().map(l=>c(A,{title:l,children:[Object.entries(o[l]??{}).map(([d,p])=>{const i=t.runtime_class[`${l}.${d}`]??"restart-required",u=t.overlay[l]?.[d]!==void 0;return c("div",{style:{display:"flex",alignItems:"center",gap:8,padding:"4px 0"},children:[c("label",{style:{flex:1,minWidth:0},children:[d," ",c("span",{style:{fontSize:11,color:"var(--smirk-fg-muted)"},children:["· ",i==="restart-required"?"restart":"live",u?" · db":""]})]}),typeof p=="boolean"?c("input",{type:"checkbox",checked:p,onChange:h=>a(l,d,h.target.checked)}):c("input",{style:{width:220},value:String(p??""),onInput:h=>{const w=h.target.value;if(typeof p=="number"){if(w==="")return;const v=Number(w);Number.isNaN(v)||a(l,d,v)}else a(l,d,w)}})]})}),c("button",{class:"primary",style:{marginTop:8},onClick:()=>f(l),children:["Save ",l]})]}))]})}const it=["Status","Keys","Invites","Config"];function _t({onLogout:t}){const[e,n]=x("Status");return c("div",{style:{maxWidth:760,margin:"0 auto",padding:20},children:[c("header",{style:{display:"flex",alignItems:"center",marginBottom:16},children:[c("h2",{style:{margin:0,flex:1},children:"🐸 Smirk Operator Console"}),c("button",{onClick:()=>{C.logout().catch(()=>{}),Ue(null),t()},children:"Sign out"})]}),c("nav",{style:{display:"flex",gap:6,marginBottom:16},children:it.map(o=>c("button",{class:o===e?"primary":"",onClick:()=>n(o),children:o}))}),e==="Status"&&c(tt,{}),e==="Keys"&&c(nt,{}),e==="Invites"&&c(rt,{}),e==="Config"&&c(ot,{})]})}function lt(){const[t,e]=x(!1);return t?c(_t,{onLogout:()=>e(!1)}):c(et,{onDone:()=>e(!0)})}Ke(c(lt,{}),document.getElementById("app")); diff --git a/admin-ui/dist/index.html b/admin-ui/dist/index.html index 6fbb077..ec363f3 100644 --- a/admin-ui/dist/index.html +++ b/admin-ui/dist/index.html @@ -5,7 +5,7 @@ Smirk Operator Console - + diff --git a/admin-ui/src/api.ts b/admin-ui/src/api.ts index 21e5521..d999a77 100644 --- a/admin-ui/src/api.ts +++ b/admin-ui/src/api.ts @@ -64,7 +64,9 @@ export const api = { logout: () => req('POST', '/admin/auth/logout', {}), me: () => req('GET', '/admin/me'), features: () => req('GET', '/admin/features'), - keys: () => req('GET', '/admin/keys') as Promise, + // The backend wraps the list as {keys:[...]} (KeysListResponse); unwrap here so + // callers get the array they expect (KeysTab maps over it directly). + keys: () => req('GET', '/admin/keys').then((r) => (r as { keys: KeyRow[] }).keys), addKey: (pubkey: string, label?: string) => req('POST', '/admin/keys', label ? { pubkey, label } : { pubkey }), revokeKey: (id: string) => req('DELETE', `/admin/keys/${id}`), diff --git a/admin-ui/src/main.tsx b/admin-ui/src/main.tsx index af9e09b..d0d9852 100644 --- a/admin-ui/src/main.tsx +++ b/admin-ui/src/main.tsx @@ -115,8 +115,13 @@ function KeysTab() { }; const revoke = async (id: string) => { if (!confirm('Revoke this admin key?')) return; - await api.revokeKey(id); - reload(); + setMsg(null); + try { + await api.revokeKey(id); + reload(); + } catch (e) { + setMsg(e instanceof Error ? e.message : String(e)); + } }; return ( @@ -163,13 +168,17 @@ function InvitesTab() { const [label, setLabel] = useState(''); const [minted, setMinted] = useState(null); const [busy, setBusy] = useState(false); + const [msg, setMsg] = useState(null); const mint = async () => { setBusy(true); + setMsg(null); try { const r = await api.mintInvites(count, label.trim() || undefined); setMinted(r.codes); reload(); + } catch (e) { + setMsg(e instanceof Error ? e.message : String(e)); } finally { setBusy(false); } @@ -193,10 +202,11 @@ function InvitesTab() { value={label} onInput={(e) => setLabel((e.target as HTMLInputElement).value)} /> - + {msg &&

{msg}

} {minted && (
New codes — copy now, they are shown once: @@ -236,7 +246,17 @@ function ConfigTab() { const [msg, setMsg] = useState(null); useEffect(() => { - if (data) setEdit(structuredClone(data.effective)); + if (!data) return; + // Seed edit state for sections we are NOT already editing, and preserve + // in-progress edits — a reload after saving one section must not discard + // unsaved edits in the others. + setEdit((prev) => { + const next = { ...prev }; + for (const section of Object.keys(data.effective)) { + if (!(section in next)) next[section] = structuredClone(data.effective[section]); + } + return next; + }); }, [data]); if (err) return
{

{err}

}
; @@ -247,11 +267,21 @@ function ConfigTab() { const save = async (section: string) => { setMsg(null); + // Send ONLY the fields that actually changed. Sending the whole section makes the + // backend count every populated field as "touched" — spuriously flagging a restart + // and pinning env-sourced values into the DB overlay. + const orig = (data.effective[section] ?? {}) as Record; + const cur = (edit[section] ?? {}) as Record; + const patch: Record = {}; + for (const k of Object.keys(cur)) { + if (JSON.stringify(cur[k]) !== JSON.stringify(orig[k])) patch[k] = cur[k]; + } + if (Object.keys(patch).length === 0) { + setMsg(`No changes in ${section}.`); + return; + } try { - await api.putConfig( - { [section]: edit[section] }, - { [section]: data.versions[section] ?? 0 }, - ); + await api.putConfig({ [section]: patch }, { [section]: data.versions[section] ?? 0 }); setMsg(`Saved ${section}.`); reload(); } catch (e) { @@ -303,7 +333,14 @@ function ConfigTab() { value={String(value ?? '')} onInput={(e) => { const v = (e.target as HTMLInputElement).value; - setField(section, field, typeof value === 'number' ? Number(v) : v); + if (typeof value === 'number') { + // Ignore empty / non-numeric input instead of coercing to 0. + if (v === '') return; + const n = Number(v); + if (!Number.isNaN(n)) setField(section, field, n); + } else { + setField(section, field, v); + } }} /> )}