project-graph-mcp 2.1.6 → 2.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/network/local-gateway.js +1 -1
- package/src/network/web-server.js +3 -3
- package/web/dashboard.js +3 -3
- package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -1
- package/web/panels/SettingsPanel/SettingsPanel.js +3 -2
- package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -1
package/package.json
CHANGED
|
@@ -8,4 +8,4 @@ export function registerService(t,e,r={}){const o=`${t}.local`,s=p();if(r.projec
|
|
|
8
8
|
function u(t,e,r){const o=r[t];if(!o)return null;if(o.routes){const t=Object.keys(o.routes).sort((t,e)=>e.length-t.length);for(const r of t)if(e===r||e.startsWith(r+"/")){const t=o.routes[r];try{process.kill(t.pid,0)}catch{continue}const n=e.slice(r.length)||"/";return{port:t.port,rewritePath:n,prefix:r}}for(const r of t)try{const t=o.routes[r];process.kill(t.pid,0);const n="/"===e||""===e?"/dashboard.html":e;return{port:t.port,rewritePath:n}}catch{continue}}if(o.port){const t="/"===e||""===e?"/dashboard.html":e;return{port:o.port,rewritePath:t}}return null}
|
|
9
9
|
function h(){try{const t=r.readFileSync(i,"utf8");return t.trim().startsWith("{")?JSON.parse(t):{pid:parseInt(t,10),port:80}}catch{return null}}
|
|
10
10
|
export function getGatewayPort(){const t=h();return t?.port||80}
|
|
11
|
-
function f(){if(!function(){const t=h();if(!t)return!1;try{return process.kill(t.pid,0),!0}catch{return!1}}())try{process.on("uncaughtException",t=>{});process.on("unhandledRejection",t=>{});const o=t.createServer((e,r)=>{const o=(e.headers.host||"").split(":")[0];let n=p(),s=u(o,e.url,n);if(!s&&(d(),n=p(),s=u(o,e.url,n),!s))return r.writeHead(404,{"Content-Type":"text/plain"}),void r.end(`Unknown host: ${o}\nRegistered: ${Object.keys(n).join(", ")}`);if("/api/gateway-info"===e.url){const t=JSON.stringify(n["project-graph.local"]||{routes:{}});return r.writeHead(200,{"Content-Type":"application/json"}),void r.end(t)}if("POST"===e.method&&"/api/remove-project"===e.url){let t="";return e.on("data",e=>t+=e),void e.on("end",()=>{try{const e=JSON.parse(t).route,o=p();if(o["project-graph.local"]?.routes?.[e]){const t=o["project-graph.local"].routes[e];try{process.kill(t.pid,9)}catch{}delete o["project-graph.local"].routes[e],l(o)}r.writeHead(200,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!0}))}catch(t){r.writeHead(400,{"Content-Type":"application/json"}),r.end(JSON.stringify({error:t.message}))}})}const c=t.request({hostname:"127.0.0.1",port:s.port,path:s.rewritePath,method:e.method,headers:{...e.headers,host:`localhost:${s.port}`}},t=>{if((t.headers["content-type"]||"").includes("text/html")&&s.prefix){const e=[];t.on("data",t=>e.push(t)),t.on("end",()=>{let o=Buffer.concat(e).toString("utf8");const n=`<base href="${s.prefix}/">`;o=o.includes("<head>")?o.replace("<head>",`<head>\n ${n}`):n+"\n"+o;const c=Buffer.from(o,"utf8"),i={...t.headers};i["content-length"]=c.length,delete i["transfer-encoding"];try{r.writeHead(t.statusCode,i),r.end(c)}catch{}})}else{try{r.writeHead(t.statusCode,t.headers)}catch{}t.pipe(r).on("error",()=>{})}});c.on("error",()=>{try{r.writeHead(502,{"Content-Type":"text/plain"}),r.end(`Backend unavailable on port ${s.port}`)}catch{}}),e.pipe(c).on("error",()=>{})});function n(t){o.listen(t,"0.0.0.0",()=>{const t=o.address().port;r.mkdirSync(s,{recursive:!0}),r.writeFileSync(i,JSON.stringify({pid:process.pid,port:t})),d()})}o.on("upgrade",(t,r,o)=>{const n=(t.headers.host||"").split(":")[0],s=p(),c=u(n,t.url,s);if(!c||c.isDashboard)return void r.destroy();const i=e.createConnection({host:"127.0.0.1",port:c.port},()=>{const e=c.rewritePath,n=`${t.method} ${e} HTTP/1.1\r\n`+Object.entries(t.headers).map(([t,e])=>`${t}: ${e}`).join("\r\n")+"\r\n\r\n";i.write(n),o.length&&i.write(o);let s=Buffer.alloc(0);i.on("data",function t(e){s=Buffer.concat([s,e]),-1!==s.indexOf("\r\n\r\n")&&(r.write(s),i.removeListener("data",t),r.pipe(i).on("error",()=>{}),i.pipe(r).on("error",()=>{}))})});i.on("error",()=>{try{r.destroy()}catch{}}),r.on("error",()=>{try{i.destroy()}catch{}})}),o.on("error",t=>{"EACCES"===t.code&&!1===o.listening?n(8080):"EADDRINUSE"===t.code&&o.listening}),n(80)}catch{}}
|
|
11
|
+
function f(){if(!function(){const t=h();if(!t)return!1;try{return process.kill(t.pid,0),!0}catch{return!1}}())try{process.on("uncaughtException",t=>{});process.on("unhandledRejection",t=>{});const o=t.createServer((e,r)=>{const o=(e.headers.host||"").split(":")[0];let n=p(),s=u(o,e.url,n);if(!s&&(d(),n=p(),s=u(o,e.url,n),!s))return r.writeHead(404,{"Content-Type":"text/plain"}),void r.end(`Unknown host: ${o}\nRegistered: ${Object.keys(n).join(", ")}`);if("/api/gateway-info"===e.url){const t=JSON.stringify(n["project-graph.local"]||{routes:{}});return r.writeHead(200,{"Content-Type":"application/json"}),void r.end(t)}if("POST"===e.method&&"/api/remove-project"===e.url){let t="";return e.on("data",e=>t+=e),void e.on("end",()=>{try{const e=JSON.parse(t).route,o=p();if(o["project-graph.local"]?.routes?.[e]){const t=o["project-graph.local"].routes[e];try{process.kill(t.pid,9)}catch{}delete o["project-graph.local"].routes[e],l(o)}r.writeHead(200,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!0}))}catch(t){r.writeHead(400,{"Content-Type":"application/json"}),r.end(JSON.stringify({error:t.message}))}})}if("/api/instances"===e.url){const n2=p(),rt=n2["project-graph.local"]?.routes||{};const list=Object.entries(rt).map(([k,v])=>({name:v.projectName||k,project:v.projectPath||"",pid:v.pid,port:v.port,prefix:k,startedAt:v.startedAt||0}));r.writeHead(200,{"Content-Type":"application/json"});return void r.end(JSON.stringify(list))}if("/api/project-info"===e.url){const n2=p(),rt=n2["project-graph.local"]?.routes||{};r.writeHead(200,{"Content-Type":"application/json"});return void r.end(JSON.stringify({name:"Gateway",path:"project-graph.local",agents:Object.keys(rt).length,pid:process.pid}))}if("/api/server-status"===e.url){const n2=p(),rt=n2["project-graph.local"]?.routes||{};r.writeHead(200,{"Content-Type":"application/json"});return void r.end(JSON.stringify({uptime:Math.round(process.uptime()),agents:0,monitors:Object.keys(rt).length,shutdownAt:null}))}const c=t.request({hostname:"127.0.0.1",port:s.port,path:s.rewritePath,method:e.method,headers:{...e.headers,host:`localhost:${s.port}`}},t=>{if((t.headers["content-type"]||"").includes("text/html")&&s.prefix){const e=[];t.on("data",t=>e.push(t)),t.on("end",()=>{let o=Buffer.concat(e).toString("utf8");const n=`<base href="${s.prefix}/">`;o=o.includes("<head>")?o.replace("<head>",`<head>\n ${n}`):n+"\n"+o;const c=Buffer.from(o,"utf8"),i={...t.headers};i["content-length"]=c.length,delete i["transfer-encoding"];try{r.writeHead(t.statusCode,i),r.end(c)}catch{}})}else{try{r.writeHead(t.statusCode,t.headers)}catch{}t.pipe(r).on("error",()=>{})}});c.on("error",()=>{try{r.writeHead(502,{"Content-Type":"text/plain"}),r.end(`Backend unavailable on port ${s.port}`)}catch{}}),e.pipe(c).on("error",()=>{})});function n(t){o.listen(t,"0.0.0.0",()=>{const t=o.address().port;r.mkdirSync(s,{recursive:!0}),r.writeFileSync(i,JSON.stringify({pid:process.pid,port:t})),d()})}o.on("upgrade",(t,r,o)=>{const n=(t.headers.host||"").split(":")[0],s=p(),c=u(n,t.url,s);if(!c||c.isDashboard)return void r.destroy();const i=e.createConnection({host:"127.0.0.1",port:c.port},()=>{const e=c.rewritePath,n=`${t.method} ${e} HTTP/1.1\r\n`+Object.entries(t.headers).map(([t,e])=>`${t}: ${e}`).join("\r\n")+"\r\n\r\n";i.write(n),o.length&&i.write(o);let s=Buffer.alloc(0);i.on("data",function t(e){s=Buffer.concat([s,e]),-1!==s.indexOf("\r\n\r\n")&&(r.write(s),i.removeListener("data",t),r.pipe(i).on("error",()=>{}),i.pipe(r).on("error",()=>{}))})});i.on("error",()=>{try{r.destroy()}catch{}}),r.on("error",()=>{try{i.destroy()}catch{}})}),o.on("error",t=>{"EACCES"===t.code&&!1===o.listening?n(8080):"EADDRINUSE"===t.code&&o.listening}),n(80)}catch{}}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @ctx .context/src/network/web-server.ctx
|
|
2
|
-
import e from"node:http";import t from"node:fs";import o from"node:path";import n from"node:crypto";import{fileURLToPath as a}from"node:url";import{WebSocketServer as s}from"ws";import{createServer as i}from"../mcp/mcp-server.js";import c from"../core/event-bus.js";import{registerService as r}from"./local-gateway.js";import{compressFile as _cf}from"../compact/compress.js";import{setRoots as _setRoots}from"../core/workspace.js";
|
|
3
|
-
const d=o.dirname(a(import.meta.url)),p=o.join(d,"..","..");function _rv(k,n){const a=o.join(p,"node_modules",
|
|
2
|
+
import e from"node:http";import t from"node:fs";import o from"node:path";import n from"node:crypto";import{fileURLToPath as a}from"node:url";import{createRequire as _createRequire}from"node:module";import{WebSocketServer as s}from"ws";import{createServer as i}from"../mcp/mcp-server.js";import c from"../core/event-bus.js";import{registerService as r}from"./local-gateway.js";import{compressFile as _cf}from"../compact/compress.js";import{setRoots as _setRoots}from"../core/workspace.js";
|
|
3
|
+
const d=o.dirname(a(import.meta.url)),p=o.join(d,"..","..");function _rv(k,n){const _n=n||k;const a=o.join(p,"node_modules",_n);if(t.existsSync(a))return a;const b=o.join(p,"..","..","node_modules",_n);if(t.existsSync(b))return b;const c=o.join(p,"..",_n);if(t.existsSync(c))return c;try{const _rq=_createRequire(import.meta.url);const r=_rq.resolve(k+"/package.json");return o.dirname(r)}catch{}return a}const m=o.join(p,"web"),h={"symbiote-node":_rv("symbiote-node"),symbiote:_rv("symbiote",o.join("@symbiotejs","symbiote"))},f={".html":"text/html",".js":"text/javascript",".mjs":"text/javascript",".css":"text/css",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".ico":"image/x-icon",".woff2":"font/woff2"};
|
|
4
4
|
function g(e,n){const a=o.normalize(e).replace(/^(\.\.[/\\])+/,""),s=a.match(/^[/\\]?vendor[/\\]([^/\\]+)[/\\]?(.*)/);let i,c;if(s&&h[s[1]]?(c=h[s[1]],i=o.join(c,s[2]||"index.js")):(c=m,i=o.join(m,"/"===a?"index.html":a)),!i.startsWith(c))return n.writeHead(403),void n.end("Forbidden");if(t.existsSync(i)&&t.statSync(i).isDirectory()&&(i=o.join(i,"index.html")),!t.existsSync(i))return n.writeHead(404),void n.end("Not Found");const r=o.extname(i),l=f[r]||"application/octet-stream",d=t.readFileSync(i);n.writeHead(200,{"Content-Type":l,"Cache-Control":"no-cache, no-store, must-revalidate"}),n.end(d)}
|
|
5
5
|
function u(e){return n.createHash("sha1").update(e+"258EAFA5-E914-47DA-95CA-5AB5ADF35C70").digest("base64")}
|
|
6
6
|
function y(e){const t=Buffer.from(e,"utf8"),o=t.length;let n;return o<126?(n=Buffer.alloc(2),n[0]=129,n[1]=o):o<65536?(n=Buffer.alloc(4),n[0]=129,n[1]=126,n.writeUInt16BE(o,2)):(n=Buffer.alloc(10),n[0]=129,n[1]=127,n.writeBigUInt64BE(BigInt(o),2)),Buffer.concat([n,t])}
|
|
7
7
|
function w(e){if(e.length<2)return null;const t=15&e[0],o=!!(128&e[1]);let n=127&e[1],a=2;if(126===n){if(e.length<4)return null;n=e.readUInt16BE(2),a=4}else if(127===n){if(e.length<10)return null;n=Number(e.readBigUInt64BE(2)),a=10}if(o){if(e.length<a+4+n)return null;const o=e.slice(a,a+4);a+=4;const s=e.slice(a,a+n);for(let e=0;e<s.length;e++)s[e]^=o[e%4];return{opcode:t,data:s.toString("utf8"),totalLen:a+n}}return e.length<a+n?null:{opcode:t,data:e.slice(a,a+n).toString("utf8"),totalLen:a+n}}
|
|
8
|
-
export function startWebServer(t,a){_setRoots([{uri:"file://"+o.resolve(t)}]);const d=i(()=>{}),p=o.basename(o.resolve(t))||"root";let m=1;const h=o.resolve(t),f=n.createHash("md5").update(h).digest("hex"),v=parseInt(f.slice(0,4),16)%360,j={project:{name:p,path:h,color:`hsl(${v}, 65%, 55%)`,agents:0,pid:process.pid},skeleton:null,events:[]};function x(e,t){const o=JSON.stringify({jsonrpc:"2.0",method:e,params:t});for(const e of T)try{e.send(o)}catch{T.delete(e)}}function S(e,t){const o=e.split(".");let n=j;for(let e=0;e<o.length-1;e++)n=n[o[e]];n[o[o.length-1]]=t,x("patch",{path:e,value:t})}async function k(){if(!j.skeleton)try{j.skeleton=await d.executeTool("get_skeleton",{path:t})}catch(e){console.error("[project-graph] Failed to load skeleton:",e.message)}return j.skeleton}const b=new Map,T=new Set;let C=null;const _cache={cs:null,cst:0,as:null,ast:0,fa:null,fat:0};function _clearCache(){_cache.cs=null;_cache.cst=0;_cache.as=null;_cache.ast=0;_cache.fa=null;_cache.fat=0;j.skeleton=null}function N(){return b.size>0||T.size>0}function O(){C&&(clearTimeout(C),C=null)}function P(){N()||(O(),C=setTimeout(()=>{N()||(console.log("[project-graph] No clients for 15 min — shutting down."),process.exit(0))},9e5))}function z(){O(),P()}async function A(e,a,s,i){try{let c;const r=a.get("path")||t;switch(e){case"/api/skeleton":c=await d.executeTool("get_skeleton",{path:r});break;case"/api/file":{const e=a.get("path");if(e){const{resolve:n,basename:a,extname:s}=await import("path"),{readFileSync:i,existsSync:r}=await import("fs"),d=n(t,e),p=a(e,s(e))+".ctx",m=o.resolve(t,".context",o.dirname(e),p),f=await _cf(d,{beautify:false,legend:false}),y=r(m)?Math.ceil(i(m,"utf-8").length/4):0,w=f.compressed+y;c={code:f.code,file:e,codeTok:f.compressed,ctxTok:y,totalTok:w,expanded:f.expanded||f.original,savings:f.savings}}else c={code:"// No file specified",file:""};break}case"/api/compact-file":{const e=a.get("path");if(e){const{resolve:n}=await import("path"),s=n(t,e),i=await _cf(s,{beautify:!1,legend:!1});c={code:i.code,file:e,original:i.original,compressed:i.compressed,savings:i.savings}}else c={code:"// No file specified",file:""};break}case"/api/raw-file":{const e=a.get("path");try{const{readFileSync:o}=await import("fs"),{resolve:n,relative:a}=await import("path");c={content:o(n(t,e),"utf-8"),file:e}}catch(t){c={content:`// Cannot read: ${t.message}`,file:e}}break}case"/api/compression-stats":if(_cache.cs&&Date.now()-_cache.cst<6e4){c=_cache.cs;break}{const{readdirSync:e,statSync:n,readFileSync:a,existsSync:s}=await import("fs"),{join:i,extname:r,basename:d,dirname:p,relative:m}=await import("path"),h=new Set([".js",".mjs"]),f=[],g=["node_modules",".git","vendor",".context",".expanded","web"];!function t(o){try{for(const a of e(o)){if(a.startsWith("."))continue;const e=i(o,a);n(e).isDirectory()?g.includes(a)||t(e):h.has(r(a))&&f.push(e)}}catch{}}(o.resolve(t,"src"));let u=0,y=0,w=0,v=0,_og=0;const j=o.resolve(t);for(const e of f){try{const t=m(j,e),i=d(t,".js"),c=o.resolve(j,".context",p(t),i+".ctx");let r=0;s(c)&&(r=n(c).size,w+=r);try{const t=s(c)?a(c,"utf-8"):null;{const _r=await _cf(e,{beautify:false,legend:false});v+=(_r.expanded||_r.original);u+=_r.compressed;_og+=_r.original}}catch{v+=Math.ceil(1.3*n(e).size/4);_og+=Math.ceil(n(e).size/4)}}catch{continue}y++}c={files:y,codeTok:u,ctxTok:Math.ceil(w/4),totalTok:u+Math.ceil(w/4),expanded:v,original:_og};_cache.cs=c;_cache.cst=Date.now();break}case"/api/docs":{const e=a.get("file");if(e){try{const{readFileSync:n,existsSync:a}=await import("fs"),{basename:s,extname:i,dirname:r}=await import("path"),d=s(e,i(e))+".ctx",p=o.resolve(t,".context",r(e),d);c=a(p)?{docs:n(p,"utf-8"),file:e}:{docs:"",file:e}}catch(t){c={docs:"",file:e}}}else{c=await d.executeTool("docs",{action:"get",path:r})}break}case"/api/analysis":if(_cache.fa&&Date.now()-_cache.fat<12e4){c=_cache.fa}else{c=await d.executeTool("analyze",{action:"full_analysis",path:r});_cache.fa=c;_cache.fat=Date.now()}break;case"/api/analysis-summary":if(_cache.as&&Date.now()-_cache.ast<12e4){c=_cache.as}else{c=await d.executeTool("analyze",{action:"analysis_summary",path:r});_cache.as=c;_cache.ast=Date.now()}break;case"/api/deps":c=await d.executeTool("navigate",{action:"deps",symbol:a.get("symbol")});break;case"/api/usages":c=await d.executeTool("navigate",{action:"usages",symbol:a.get("symbol")});break;case"/api/expand":c=await d.executeTool("navigate",{action:"expand",symbol:a.get("symbol")});break;case"/api/chain":c=await d.executeTool("navigate",{action:"call_chain",from:a.get("from"),to:a.get("to")});break;case"/api/project-info":{const e=o.resolve(t),a=n.createHash("md5").update(e).digest("hex"),s=parseInt(a.slice(0,4),16)%360;c={name:p,path:e,color:`hsl(${s}, 65%, 55%)`,agents:b.size,pid:process.pid};break}case"/api/instances":try{const{listBackends:e}=await import("./backend-lifecycle.js");c=e()}catch{c=[{name:p,path:o.resolve(t),agents:b.size}]}break;default:return"POST"===s&&"/api/restart"===e?(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache"}),i.end(JSON.stringify({ok:!0,message:"Server restarting..."})),void setTimeout(async()=>{const{spawn:e}=await import("child_process"),{removePortFile:n}=await import("./backend-lifecycle.js"),{fileURLToPath:a}=await import("url"),s=o.join(o.dirname(a(import.meta.url)),"backend.js");n(t),e(process.execPath,[s,o.resolve(t)],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref(),setTimeout(()=>process.exit(0),300)},200)):(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),void i.end(JSON.stringify({error:"Unknown API endpoint"})))}i.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Cache-Control":"no-cache"}),i.end(JSON.stringify(c))}catch(e){i.writeHead(500,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),i.end(JSON.stringify({error:e.message}))}}P(),k(),c.on("tool:call",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e);const _n=e.tool||e.name||"";if("invalidate_cache"===_n||"set_custom_rule"===_n||"delete_custom_rule"===_n)_clearCache();else if("docs"===_n||"compact"===_n||"filters"===_n){const _a=e.args?.action||"";if("generate"===_a||"set_mode"===_a||"set"===_a||"reset"===_a)_clearCache()}}),c.on("tool:result",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e)});const B=e.createServer((e,t)=>{const o=new URL(e.url,`http://localhost:${a||0}`);return"OPTIONS"===e.method?(t.writeHead(204,{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST","Access-Control-Allow-Headers":"Content-Type"}),void t.end()):o.pathname.startsWith("/api/")?(z(),void A(o.pathname,o.searchParams,e.method,t)):("/ws/monitor"===o.pathname&&console.log("UNEXPECTED HTTP /ws/monitor",e.headers),void g(o.pathname,t))}),M=new s({noServer:!0});M.on("connection",async e=>{T.add(e),z();const t={project:j.project};try{e.send(JSON.stringify({jsonrpc:"2.0",method:"snapshot",params:{state:t}}))}catch{}e.on("message",async t=>{let n;z();try{n=JSON.parse(t.toString())}catch{return}if(n.jsonrpc&&n.id&&n.method)if("tool"===n.method){const{name:a,args:s}=n.params||{};try{let t;if("compact"===a&&"compact_file"===s?.action&&s?.path){const e=o.resolve(h,s.path),n=o.basename(s.path,o.extname(s.path))+".ctx",a=o.resolve(h,".context",o.dirname(s.path),n);let i=null;try{const{readFileSync:e,existsSync:t}=await import("fs");t(a)&&(i=e(a,"utf-8"))}catch{}const c=await _cf(e,{beautify:false,legend:false}),p=i?Math.ceil(i.length/4):0,m=c.compressed+p;t={code:c.code,file:s.path,codeTok:c.compressed,ctxTok:p,totalTok:m,expanded:c.expanded||c.original,savings:c.savings}}else t=await d.executeTool(a,s||{});e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,result:t}))}catch(t){e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32e3,message:t.message}}))}}else e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32601,message:`Unknown method: ${n.method}`}}))}),e.on("close",()=>{T.delete(e),P()}),e.on("error",()=>{T.delete(e),P()})}),B.on("upgrade",(e,t,o)=>{const n=e.headers["sec-websocket-key"];if(n)if("/ws/monitor"!==e.url){if("/mcp-ws"===e.url){const e=u(n);t.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${e}\r\n\r\n`);const o="agent-"+m++,a=i(e=>{try{t.write(y(JSON.stringify(e)))}catch{}});b.set(t,{agentId:o,mcpServer:a,connectedAt:Date.now()}),O(),S("project.agents",b.size),x("event",{type:"agent_connect",agentId:o,agents:b.size,ts:Date.now()});let s=Buffer.alloc(0);return t.on("data",e=>{for(s=Buffer.concat([s,e]);s.length>=2;){const n=w(s);if(!n)break;if(s=s.slice(n.totalLen),8===n.opcode)return b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),t.end(),void(0===b.size&&P());if(9===n.opcode){const o=Buffer.from(e);o[0]=240&o[0]|10,t.write(o);continue}1===n.opcode&&(async()=>{try{const e=JSON.parse(n.data),o=await a.handleMessage(e);null!==o&&t.write(y(JSON.stringify(o)))}catch(e){t.write(y(JSON.stringify({jsonrpc:"2.0",error:{code:-32700,message:"Parse error"}})))}})()}}),t.on("close",()=>{b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),0===b.size&&P()}),void t.on("error",()=>{b.delete(t),0===b.size&&P()})}t.destroy()}else M.handleUpgrade(e,t,o,t=>{M.emit("connection",t,e)});else t.destroy()});const H=!a,J=a||0;return B.listen(J,"127.0.0.1",()=>{const e=B.address().port;if(H){const n=r("project-graph",e,{projectPath:o.resolve(t),projectName:p});setTimeout(()=>{const a=n.url;console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → ${a}`),console.log(` → ${n.directUrl} (direct)`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)},200)}else console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${e}/`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)}),B}
|
|
8
|
+
export function startWebServer(t,a){_setRoots([{uri:"file://"+o.resolve(t)}]);const d=i(()=>{}),p=o.basename(o.resolve(t))||"root";let m=1;const _startedAt=Date.now();const h=o.resolve(t),f=n.createHash("md5").update(h).digest("hex"),v=parseInt(f.slice(0,4),16)%360,j={project:{name:p,path:h,color:`hsl(${v}, 65%, 55%)`,agents:0,pid:process.pid},skeleton:null,events:[]};function x(e,t){const o=JSON.stringify({jsonrpc:"2.0",method:e,params:t});for(const e of T)try{e.send(o)}catch{T.delete(e)}}function S(e,t){const o=e.split(".");let n=j;for(let e=0;e<o.length-1;e++)n=n[o[e]];n[o[o.length-1]]=t,x("patch",{path:e,value:t})}async function k(){if(!j.skeleton)try{j.skeleton=await d.executeTool("get_skeleton",{path:t})}catch(e){console.error("[project-graph] Failed to load skeleton:",e.message)}return j.skeleton}const b=new Map,T=new Set;let C=null;const _cache={cs:null,cst:0,as:null,ast:0,fa:null,fat:0};function _clearCache(){_cache.cs=null;_cache.cst=0;_cache.as=null;_cache.ast=0;_cache.fa=null;_cache.fat=0;j.skeleton=null}function N(){return b.size>0||T.size>0}function O(){C&&(clearTimeout(C),C=null);_shutdownAt=0}let _shutdownAt=0;function P(){N()||(O(),_shutdownAt=Date.now()+9e5,C=setTimeout(()=>{N()||(console.log("[project-graph] No clients for 15 min — shutting down."),process.exit(0))},9e5))}function z(){O(),P()}async function A(e,a,s,i){try{let c;const r=a.get("path")||t;switch(e){case"/api/skeleton":c=await d.executeTool("get_skeleton",{path:r});break;case"/api/file":{const e=a.get("path");if(e){const{resolve:n,basename:a,extname:s}=await import("path"),{readFileSync:i,existsSync:r}=await import("fs"),d=n(t,e),p=a(e,s(e))+".ctx",m=o.resolve(t,".context",o.dirname(e),p),f=await _cf(d,{beautify:false,legend:false}),y=r(m)?Math.ceil(i(m,"utf-8").length/4):0,w=f.compressed+y;c={code:f.code,file:e,codeTok:f.compressed,ctxTok:y,totalTok:w,expanded:f.expanded||f.original,savings:f.savings}}else c={code:"// No file specified",file:""};break}case"/api/compact-file":{const e=a.get("path");if(e){const{resolve:n}=await import("path"),s=n(t,e),i=await _cf(s,{beautify:!1,legend:!1});c={code:i.code,file:e,original:i.original,compressed:i.compressed,savings:i.savings}}else c={code:"// No file specified",file:""};break}case"/api/raw-file":{const e=a.get("path");try{const{readFileSync:o}=await import("fs"),{resolve:n,relative:a}=await import("path");c={content:o(n(t,e),"utf-8"),file:e}}catch(t){c={content:`// Cannot read: ${t.message}`,file:e}}break}case"/api/compression-stats":if(_cache.cs&&Date.now()-_cache.cst<6e4){c=_cache.cs;break}{const{readdirSync:e,statSync:n,readFileSync:a,existsSync:s}=await import("fs"),{join:i,extname:r,basename:d,dirname:p,relative:m}=await import("path"),h=new Set([".js",".mjs"]),f=[],g=["node_modules",".git","vendor",".context",".expanded","web"];!function t(o){try{for(const a of e(o)){if(a.startsWith("."))continue;const e=i(o,a);n(e).isDirectory()?g.includes(a)||t(e):h.has(r(a))&&f.push(e)}}catch{}}(o.resolve(t,"src"));let u=0,y=0,w=0,v=0,_og=0;const j=o.resolve(t);for(const e of f){try{const t=m(j,e),i=d(t,".js"),c=o.resolve(j,".context",p(t),i+".ctx");let r=0;s(c)&&(r=n(c).size,w+=r);try{const t=s(c)?a(c,"utf-8"):null;{const _r=await _cf(e,{beautify:false,legend:false});v+=(_r.expanded||_r.original);u+=_r.compressed;_og+=_r.original}}catch{v+=Math.ceil(1.3*n(e).size/4);_og+=Math.ceil(n(e).size/4)}}catch{continue}y++}c={files:y,codeTok:u,ctxTok:Math.ceil(w/4),totalTok:u+Math.ceil(w/4),expanded:v,original:_og};_cache.cs=c;_cache.cst=Date.now();break}case"/api/docs":{const e=a.get("file");if(e){try{const{readFileSync:n,existsSync:a}=await import("fs"),{basename:s,extname:i,dirname:r}=await import("path"),d=s(e,i(e))+".ctx",p=o.resolve(t,".context",r(e),d);c=a(p)?{docs:n(p,"utf-8"),file:e}:{docs:"",file:e}}catch(t){c={docs:"",file:e}}}else{c=await d.executeTool("docs",{action:"get",path:r})}break}case"/api/analysis":if(_cache.fa&&Date.now()-_cache.fat<12e4){c=_cache.fa}else{c=await d.executeTool("analyze",{action:"full_analysis",path:r});_cache.fa=c;_cache.fat=Date.now()}break;case"/api/analysis-summary":if(_cache.as&&Date.now()-_cache.ast<12e4){c=_cache.as}else{c=await d.executeTool("analyze",{action:"analysis_summary",path:r});_cache.as=c;_cache.ast=Date.now()}break;case"/api/deps":c=await d.executeTool("navigate",{action:"deps",symbol:a.get("symbol")});break;case"/api/usages":c=await d.executeTool("navigate",{action:"usages",symbol:a.get("symbol")});break;case"/api/expand":c=await d.executeTool("navigate",{action:"expand",symbol:a.get("symbol")});break;case"/api/chain":c=await d.executeTool("navigate",{action:"call_chain",from:a.get("from"),to:a.get("to")});break;case"/api/server-status":{const _now=Date.now();c={uptime:Math.round((_now-_startedAt)/1e3),agents:b.size,monitors:T.size,shutdownAt:_shutdownAt?Math.max(0,Math.round((_shutdownAt-_now)/1e3)):null};break}case"/api/stop":i.writeHead(200,{"Content-Type":"application/json"});i.end(JSON.stringify({ok:true}));setTimeout(()=>process.exit(0),200);return;case"/api/project-info":{const e=o.resolve(t),a=n.createHash("md5").update(e).digest("hex"),s=parseInt(a.slice(0,4),16)%360;c={name:p,path:e,color:`hsl(${s}, 65%, 55%)`,agents:b.size,pid:process.pid};break}case"/api/instances":try{const{listBackends:e}=await import("./backend-lifecycle.js");c=e()}catch{c=[{name:p,path:o.resolve(t),agents:b.size}]}break;default:return"POST"===s&&"/api/restart"===e?(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache"}),i.end(JSON.stringify({ok:!0,message:"Server restarting..."})),void setTimeout(async()=>{const{spawn:e}=await import("child_process"),{removePortFile:n}=await import("./backend-lifecycle.js"),{fileURLToPath:a}=await import("url"),s=o.join(o.dirname(a(import.meta.url)),"backend.js");n(t),e(process.execPath,[s,o.resolve(t)],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref(),setTimeout(()=>process.exit(0),300)},200)):(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),void i.end(JSON.stringify({error:"Unknown API endpoint"})))}i.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Cache-Control":"no-cache"}),i.end(JSON.stringify(c))}catch(e){i.writeHead(500,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),i.end(JSON.stringify({error:e.message}))}}P(),k(),c.on("tool:call",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e);const _n=e.tool||e.name||"";if("invalidate_cache"===_n||"set_custom_rule"===_n||"delete_custom_rule"===_n)_clearCache();else if("docs"===_n||"compact"===_n||"filters"===_n){const _a=e.args?.action||"";if("generate"===_a||"set_mode"===_a||"set"===_a||"reset"===_a)_clearCache()}}),c.on("tool:result",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e)});const B=e.createServer((e,t)=>{const o=new URL(e.url,`http://localhost:${a||0}`);return"OPTIONS"===e.method?(t.writeHead(204,{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST","Access-Control-Allow-Headers":"Content-Type"}),void t.end()):o.pathname.startsWith("/api/")?(z(),void A(o.pathname,o.searchParams,e.method,t)):("/ws/monitor"===o.pathname&&console.log("UNEXPECTED HTTP /ws/monitor",e.headers),void g(o.pathname,t))}),M=new s({noServer:!0});M.on("connection",async e=>{T.add(e),z();const t={project:j.project};try{e.send(JSON.stringify({jsonrpc:"2.0",method:"snapshot",params:{state:t}}))}catch{}e.on("message",async t=>{let n;z();try{n=JSON.parse(t.toString())}catch{return}if(n.jsonrpc&&n.id&&n.method)if("tool"===n.method){const{name:a,args:s}=n.params||{};try{let t;if("compact"===a&&"compact_file"===s?.action&&s?.path){const e=o.resolve(h,s.path),n=o.basename(s.path,o.extname(s.path))+".ctx",a=o.resolve(h,".context",o.dirname(s.path),n);let i=null;try{const{readFileSync:e,existsSync:t}=await import("fs");t(a)&&(i=e(a,"utf-8"))}catch{}const c=await _cf(e,{beautify:false,legend:false}),p=i?Math.ceil(i.length/4):0,m=c.compressed+p;t={code:c.code,file:s.path,codeTok:c.compressed,ctxTok:p,totalTok:m,expanded:c.expanded||c.original,savings:c.savings}}else t=await d.executeTool(a,s||{});e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,result:t}))}catch(t){e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32e3,message:t.message}}))}}else e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32601,message:`Unknown method: ${n.method}`}}))}),e.on("close",()=>{T.delete(e),P()}),e.on("error",()=>{T.delete(e),P()})}),B.on("upgrade",(e,t,o)=>{const n=e.headers["sec-websocket-key"];if(n)if("/ws/monitor"!==e.url){if("/mcp-ws"===e.url){const e=u(n);t.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${e}\r\n\r\n`);const o="agent-"+m++,a=i(e=>{try{t.write(y(JSON.stringify(e)))}catch{}});b.set(t,{agentId:o,mcpServer:a,connectedAt:Date.now()}),O(),S("project.agents",b.size),x("event",{type:"agent_connect",agentId:o,agents:b.size,ts:Date.now()});let s=Buffer.alloc(0);return t.on("data",e=>{for(s=Buffer.concat([s,e]);s.length>=2;){const n=w(s);if(!n)break;if(s=s.slice(n.totalLen),8===n.opcode)return b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),t.end(),void(0===b.size&&P());if(9===n.opcode){const o=Buffer.from(e);o[0]=240&o[0]|10,t.write(o);continue}1===n.opcode&&(async()=>{try{const e=JSON.parse(n.data),o=await a.handleMessage(e);null!==o&&t.write(y(JSON.stringify(o)))}catch(e){t.write(y(JSON.stringify({jsonrpc:"2.0",error:{code:-32700,message:"Parse error"}})))}})()}}),t.on("close",()=>{b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),0===b.size&&P()}),void t.on("error",()=>{b.delete(t),0===b.size&&P()})}t.destroy()}else M.handleUpgrade(e,t,o,t=>{M.emit("connection",t,e)});else t.destroy()});const H=!a,J=a||0;return B.listen(J,"127.0.0.1",()=>{const e=B.address().port;if(H){const n=r("project-graph",e,{projectPath:o.resolve(t),projectName:p});setTimeout(()=>{const a=n.url;console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → ${a}`),console.log(` → ${n.directUrl} (direct)`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)},200)}else console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${e}/`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)}),B}
|
package/web/dashboard.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @ctx .context/web/dashboard.ctx
|
|
2
|
-
import{Layout as e,LayoutTree as t,applyTheme as o}from"symbiote-node";import{CARBON as r}from"./vendor/symbiote-node/themes/carbon.js";import"./panels/ProjectList/ProjectList.js";import"./panels/ActionBoard/ActionBoard.js";import{state as a,events as n,emit as s}from"./dashboard-state.js";async function c(){const e=await fetch("/api/gateway-info");if(!e.ok){const t=await e.text();throw console.error("[dashboard] fetchGatewayInfo failed:",e.status,t),new Error(`Gateway info failed: ${e.status}`)}return e.json()}
|
|
2
|
+
import{Layout as e,LayoutTree as t,applyTheme as o}from"symbiote-node";import{CARBON as r}from"./vendor/symbiote-node/themes/carbon.js";import"./panels/ProjectList/ProjectList.js";import"./panels/ActionBoard/ActionBoard.js";import"./panels/SettingsPanel/SettingsPanel.js";import{state as a,events as n,emit as s}from"./dashboard-state.js";async function c(){const e=await fetch("/api/gateway-info");if(!e.ok){const t=await e.text();throw console.error("[dashboard] fetchGatewayInfo failed:",e.status,t),new Error(`Gateway info failed: ${e.status}`)}return e.json()}
|
|
3
3
|
function p(e){if(!e.length)return void console.warn("[dashboard] No projects to connect WebSockets for");
|
|
4
4
|
const t="https:"===location.protocol?"wss://":"ws://",o=location.host;for(const r of e)i(r,t,o)}
|
|
5
5
|
function i(e,t,o,_att=0){const r=`${t}${o}${e.prefix}/ws/monitor`,n=new WebSocket(r);n.onopen=()=>{_att=0;console.log("[dashboard] WS connected:",e.projectName)},n.onmessage=t=>{let o;try{o=JSON.parse(t.data)}catch{return}if("snapshot"===o.method&&o.params?.state){const t=o.params.state,r=a.projects.find(t=>t.prefix===e.prefix);return void(r&&t.project&&(Object.assign(r,{projectName:t.project.name,projectPath:t.project.path,color:t.project.color,agents:t.project.agents,pid:t.project.pid,connected:!0}),s("projects-updated",a.projects)))}if("patch"===o.method&&o.params){const t=a.projects.find(t=>t.prefix===e.prefix);return void(t&&"project.agents"===o.params.path&&(t.agents=o.params.value,s("projects-updated",a.projects)))}if("event"===o.method&&o.params){const t=o.params;return t._projectPrefix=e.prefix,t._projectName=e.projectName,a.events.push(t),a.events.length>1e3&&a.events.shift(),void s("global-tool-event",t)}o.type&&(o._projectPrefix=e.prefix,o._projectName=e.projectName,a.events.push(o),a.events.length>1e3&&a.events.shift(),s("global-tool-event",o))},n.onerror=()=>{console.error("[dashboard] WS error:",e.projectName)},n.onclose=r=>{console.warn("[dashboard] WS closed:",e.projectName,r.code);
|
|
6
|
-
const n=a.projects.find(t=>t.prefix===e.prefix);n&&(n.connected=!1,s("projects-updated",a.projects)),setTimeout(()=>i(e,t,o,_att+1),Math.min(500*Math.pow(2,_att),3e4))}}const d={"project-list":{title:"Projects",icon:"dashboard",component:"pg-project-list"},"action-board":{title:"Action Board",icon:"monitor_heart",component:"pg-action-board"}};async function l(){o(document.documentElement,r);
|
|
7
|
-
const e=document.querySelector(".app-workspace").querySelector(".app-content"),n=document.createElement("panel-layout");n.setAttribute("storage-key","pg-dashboard-layout"),n.setAttribute("min-panel-size","200"),n.id="dashboard-layout",
|
|
6
|
+
const n=a.projects.find(t=>t.prefix===e.prefix);n&&(n.connected=!1,s("projects-updated",a.projects)),setTimeout(()=>i(e,t,o,_att+1),Math.min(500*Math.pow(2,_att),3e4))}}const d={"project-list":{title:"Projects",icon:"dashboard",component:"pg-project-list"},"action-board":{title:"Action Board",icon:"monitor_heart",component:"pg-action-board"},settings:{title:"Settings",icon:"settings",component:"pg-settings-panel"}};const _sections=[{id:"dashboard",icon:"dashboard",label:"Dashboard"},{id:"settings",icon:"settings",label:"Settings"}];const _layouts={dashboard:()=>t.createSplit("vertical",t.createPanel("project-list"),t.createPanel("action-board"),.3),settings:()=>t.createPanel("settings")};async function l(){o(document.documentElement,r);
|
|
7
|
+
const e=document.querySelector(".app-workspace"),sb=document.createElement("layout-sidebar");e.prepend(sb);const _c=e.querySelector(".app-content"),n=document.createElement("panel-layout");n.setAttribute("storage-key","pg-dashboard-layout"),n.setAttribute("min-panel-size","200"),n.id="dashboard-layout",_c.appendChild(n),requestAnimationFrame(async()=>{for(const[e,t]of Object.entries(d))n.registerPanelType(e,t);function _route(){const h=location.hash.replace("#","")||"dashboard";const sec=_layouts[h]?"dashboard"===h||"settings"===h?h:"dashboard":"dashboard";_layouts[sec]&&n.setLayout(_layouts[sec]())}sb.setSections(_sections);window.addEventListener("hashchange",_route);if(!location.hash||"#"===location.hash){n.setLayout(_layouts.dashboard())}else{_route()}
|
|
8
8
|
const e=await c();a.projects=Object.entries(e.routes||{}).map(([e,t])=>({prefix:e,...t,connected:!1,agents:0})),s("projects-updated",a.projects),p(a.projects)})}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",l):setTimeout(l,100);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @ctx .context/web/panels/SettingsPanel/SettingsPanel.css.ctx
|
|
2
|
-
export default"\npg-settings-panel {\n display: block;\n height: 100%;\n overflow-y: auto;\n padding: 16px;\n font-family: var(--sn-font, 'Inter', -apple-system, sans-serif);\n}\n\n.pg-stg-card {\n background: var(--sn-node-bg);\n border: 1px solid var(--sn-node-border);\n border-radius: 8px;\n padding: 14px;\n margin-bottom: 12px;\n}\n\n.pg-stg-title {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--sn-text-dim);\n margin-bottom: 8px;\n}\n\n.pg-stg-metric {\n display: flex;\n justify-content: space-between;\n padding: 5px 0;\n border-bottom: 1px solid var(--sn-node-hover);\n font-size: 12px;\n color: var(--sn-text);\n}\n\n.pg-stg-metric:last-child {\n border-bottom: none;\n}\n\n.pg-stg-val {\n font-weight: 600;\n font-family: 'JetBrains Mono', 'Fira Code', monospace;\n}\n\n.pg-stg-ok {\n color: var(--sn-success-color, #4caf50);\n}\n\n.pg-stg-btn {\n background: var(--sn-node-bg);\n border: 1px solid var(--sn-node-border);\n color: var(--sn-text);\n padding: 6px 14px;\n border-radius: 8px;\n cursor: pointer;\n font-size: 12px;\n font-family: inherit;\n transition: border-color 0.15s;\n}\n\n.pg-stg-btn:hover {\n border-color: var(--sn-node-selected, #4c8bf5);\n}\n\n.pg-stg-btn-danger {\n border-color: var(--sn-danger-color, #f44336);\n color: var(--sn-danger-color, #f44336);\n}\n\n.pg-stg-btn-danger:hover {\n background: var(--sn-danger-color, #f44336);\n color: #fff;\n border-color: var(--sn-danger-color, #f44336);\n}\n\n.pg-stg-placeholder {\n color: var(--sn-text-dim);\n text-align: center;\n padding: 20px;\n font-style: italic;\n font-size: 13px;\n}\n\n.pg-stg-pulse {\n animation: pg-stg-pulse 1.5s ease infinite;\n}\n\n@keyframes pg-stg-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n";
|
|
2
|
+
export default"\npg-settings-panel {\n display: block;\n height: 100%;\n overflow-y: auto;\n padding: 16px;\n font-family: var(--sn-font, 'Inter', -apple-system, sans-serif);\n}\n\n.pg-stg-card {\n background: var(--sn-node-bg);\n border: 1px solid var(--sn-node-border);\n border-radius: 8px;\n padding: 14px;\n margin-bottom: 12px;\n}\n\n.pg-stg-title {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--sn-text-dim);\n margin-bottom: 8px;\n}\n\n.pg-stg-metric {\n display: flex;\n justify-content: space-between;\n padding: 5px 0;\n border-bottom: 1px solid var(--sn-node-hover);\n font-size: 12px;\n color: var(--sn-text);\n}\n\n.pg-stg-metric:last-child {\n border-bottom: none;\n}\n\n.pg-stg-val {\n font-weight: 600;\n font-family: 'JetBrains Mono', 'Fira Code', monospace;\n}\n\n.pg-stg-ok {\n color: var(--sn-success-color, #4caf50);\n}\n\n.pg-stg-warn {\n color: var(--sn-warning-color, #ff9800);\n}\n\n.pg-stg-btn {\n background: var(--sn-node-bg);\n border: 1px solid var(--sn-node-border);\n color: var(--sn-text);\n padding: 6px 14px;\n border-radius: 8px;\n cursor: pointer;\n font-size: 12px;\n font-family: inherit;\n transition: border-color 0.15s;\n}\n\n.pg-stg-btn:hover {\n border-color: var(--sn-node-selected, #4c8bf5);\n}\n\n.pg-stg-btn-danger {\n border-color: var(--sn-danger-color, #f44336);\n color: var(--sn-danger-color, #f44336);\n}\n\n.pg-stg-btn-danger:hover {\n background: var(--sn-danger-color, #f44336);\n color: #fff;\n border-color: var(--sn-danger-color, #f44336);\n}\n\n.pg-stg-placeholder {\n color: var(--sn-text-dim);\n text-align: center;\n padding: 20px;\n font-style: italic;\n font-size: 13px;\n}\n\n.pg-stg-pulse {\n animation: pg-stg-pulse 1.5s ease infinite;\n}\n\n@keyframes pg-stg-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ctx .context/web/panels/SettingsPanel/SettingsPanel.ctx
|
|
2
2
|
import t from"@symbiotejs/symbiote";import e from"./SettingsPanel.css.js";import n from"./SettingsPanel.tpl.js";function r(t,e,n=""){return`<div class="pg-stg-metric"><span>${t}</span><span class="pg-stg-val ${n}">${e}</span></div>`}
|
|
3
|
-
|
|
3
|
+
function _fmtTime(s){if(s<=0)return"now";const m=Math.floor(s/60),sec=s%60;return m>0?`${m}m ${sec}s`:`${sec}s`}
|
|
4
|
+
export class SettingsPanel extends t{init$={};_statusInterval=null;renderCallback(){this.ref.refreshBtn.onclick=()=>this.fetchInfo(),this.ref.restartBtn.onclick=()=>this.restartServer(),this.ref.stopBtn.onclick=()=>this.stopServer(),this.fetchInfo(),this._startStatusPolling()}disconnectedCallback(){super.disconnectedCallback&&super.disconnectedCallback();this._statusInterval&&(clearInterval(this._statusInterval),this._statusInterval=null)}_startStatusPolling(){this._fetchStatus();this._statusInterval=setInterval(()=>this._fetchStatus(),5e3)}async _fetchStatus(){try{const r=await fetch("/api/server-status").then(r=>r.json());this.ref.uptimeVal.textContent=_fmtTime(r.uptime);if(r.shutdownAt!==null&&r.shutdownAt>0){this.ref.shutdownTimer.textContent=_fmtTime(r.shutdownAt);this.ref.shutdownTimer.className="pg-stg-val pg-stg-warn"}else{const clients=r.agents+r.monitors;this.ref.shutdownTimer.textContent=`Active (${clients} client${clients!==1?"s":""})`;this.ref.shutdownTimer.className="pg-stg-val pg-stg-ok"}}catch{this.ref.shutdownTimer.textContent="—";this.ref.uptimeVal.textContent="—"}}async stopServer(){if(!confirm("Stop the server? It will not restart automatically."))return;try{await fetch("/api/stop",{method:"POST"});this.ref.restartStatus.textContent="⏹ Server stopped.";this.ref.restartStatus.style.color="var(--sn-danger-color, #f44336)"}catch(e){this.ref.restartStatus.textContent=`Error: ${e.message}`}}async restartServer(){const t=this.ref.restartStatus;t.textContent="⏳ Restarting server…",t.style.color="var(--sn-warning-color, #ff9800)";try{await fetch("/api/restart",{method:"POST"}),t.textContent="Server stopped. Reconnecting…";
|
|
4
5
|
let e=0;
|
|
5
|
-
const n=setInterval(async()=>{e++;try{if((await fetch("/api/project-info")).ok)return clearInterval(n),t.textContent="✅ Server restarted successfully",t.style.color="var(--sn-success-color, #4caf50)",this.fetchInfo(),void setTimeout(()=>{t.textContent=""},3e3)}catch{}e>15&&(clearInterval(n),t.textContent="⚠ Server did not come back. Refresh the page manually.",t.style.color="var(--sn-danger-color, #f44336)")},1e3)}catch(e){t.textContent=`Error: ${e.message}`,t.style.color="var(--sn-danger-color, #f44336)"}}async fetchInfo(){this.ref.backendCard.innerHTML='<div class="pg-stg-placeholder pg-stg-pulse">Loading…</div>';try{const[t,e]=await Promise.all([fetch("/api/project-info").then(t=>t.json()),fetch("/api/instances").then(t=>t.json())]);this.ref.backendCard.innerHTML=[r("Status","Running","pg-stg-ok"),r("Project",t.name||"—"),r("Path",t.path||"—"),r("PID",t.pid||"—"),r("Connected Agents",t.agents??"—")
|
|
6
|
+
const n=setInterval(async()=>{e++;try{if((await fetch("/api/project-info")).ok)return clearInterval(n),t.textContent="✅ Server restarted successfully",t.style.color="var(--sn-success-color, #4caf50)",this.fetchInfo(),void setTimeout(()=>{t.textContent=""},3e3)}catch{}e>15&&(clearInterval(n),t.textContent="⚠ Server did not come back. Refresh the page manually.",t.style.color="var(--sn-danger-color, #f44336)")},1e3)}catch(e){t.textContent=`Error: ${e.message}`,t.style.color="var(--sn-danger-color, #f44336)"}}async fetchInfo(){this.ref.backendCard.innerHTML='<div class="pg-stg-placeholder pg-stg-pulse">Loading…</div>';try{const[t,e]=await Promise.all([fetch("/api/project-info").then(t=>t.json()),fetch("/api/instances").then(t=>t.json())]);this.ref.backendCard.innerHTML=[r("Status","Running","pg-stg-ok"),r("Project",t.name||"—"),r("Path",t.path||"—"),r("PID",t.pid||"—"),r("Connected Agents",t.agents??"—")].join("");
|
|
6
7
|
const n=this.ref.instanceList;if(n.innerHTML="",Array.isArray(e)&&e.length>0)for(const t of e){const e=t.startedAt?Math.round((Date.now()-t.startedAt)/6e4):"?",s=document.createElement("div");s.className="pg-stg-card",s.innerHTML=[r("Name",t.name||"unknown"),r("Path",t.project||"—"),r("PID",t.pid),r("Port",t.port),r("Uptime",`${e} min`)].join(""),n.appendChild(s)}else n.innerHTML='<div class="pg-stg-placeholder">No active instances</div>'}catch(t){console.error("[SettingsPanel] fetch error:",t),this.ref.backendCard.innerHTML=`<div class="pg-stg-placeholder" style="color:var(--sn-danger-color)">Error: ${t.message}</div>`}}}SettingsPanel.template=n,SettingsPanel.rootStyles=e,SettingsPanel.reg("pg-settings-panel");
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @ctx .context/web/panels/SettingsPanel/SettingsPanel.tpl.ctx
|
|
2
|
-
export default'\n<div class="pg-stg-title">Backend</div>\n<div class="pg-stg-card" ref="backendCard"></div>\n\n<div class="pg-stg-title">Active Instances</div>\n<div ref="instanceList"></div>\n\n<div class="pg-stg-title">Actions</div>\n<div style="display:flex;gap:8px">\n<button class="pg-stg-btn" ref="refreshBtn">↻ Refresh</button>\n<button class="pg-stg-btn pg-stg-btn-danger" ref="restartBtn">⟳ Restart
|
|
2
|
+
export default'\n<div class="pg-stg-title">Backend</div>\n<div class="pg-stg-card" ref="backendCard"></div>\n\n<div class="pg-stg-title">Active Instances</div>\n<div ref="instanceList"></div>\n\n<div class="pg-stg-title">Server Lifecycle</div>\n<div class="pg-stg-card" ref="lifecycleCard">\n <div class="pg-stg-metric"><span>Auto-shutdown</span><span class="pg-stg-val" ref="shutdownTimer">—</span></div>\n <div class="pg-stg-metric"><span>Uptime</span><span class="pg-stg-val" ref="uptimeVal">—</span></div>\n</div>\n\n<div class="pg-stg-title">Actions</div>\n<div style="display:flex;gap:8px">\n<button class="pg-stg-btn" ref="refreshBtn">↻ Refresh</button>\n<button class="pg-stg-btn pg-stg-btn-danger" ref="restartBtn">⟳ Restart</button>\n<button class="pg-stg-btn pg-stg-btn-danger" ref="stopBtn">⏹ Stop</button>\n</div>\n<div ref="restartStatus" style="margin-top:8px;font-size:11px;color:var(--sn-text-dim)"></div>\n';
|