opencroc 1.3.0 โ†’ 1.4.0

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.
@@ -14,721 +14,307 @@
14
14
  --red: #e94560;
15
15
  --orange: #f39c12;
16
16
  --blue: #3498db;
17
+ --purple: #9b59b6;
17
18
  --text: #e0e0e0;
18
19
  --text-dim: #888;
19
20
  --pixel-border: 2px solid #333;
20
21
  }
21
-
22
- * { margin: 0; padding: 0; box-sizing: border-box; }
23
-
24
- body {
25
- background: var(--bg-dark);
26
- color: var(--text);
27
- font-family: 'Courier New', 'Consolas', monospace;
28
- overflow: hidden;
29
- height: 100vh;
30
- }
31
-
32
- /* ===== Layout ===== */
33
- .app {
34
- display: grid;
35
- grid-template-rows: 48px 1fr 200px;
36
- grid-template-columns: 240px 1fr;
37
- height: 100vh;
38
- }
39
-
40
- .header {
41
- grid-column: 1 / -1;
42
- background: var(--bg-panel);
43
- border-bottom: var(--pixel-border);
44
- display: flex;
45
- align-items: center;
46
- padding: 0 16px;
47
- gap: 16px;
48
- }
49
-
50
- .header h1 {
51
- font-size: 16px;
52
- color: var(--accent);
53
- display: flex;
54
- align-items: center;
55
- gap: 8px;
56
- }
57
-
58
- .header .logo {
59
- font-size: 24px;
60
- image-rendering: pixelated;
61
- }
62
-
63
- .header .stats {
64
- margin-left: auto;
65
- display: flex;
66
- gap: 16px;
67
- font-size: 12px;
68
- color: var(--text-dim);
69
- }
70
-
71
- .header .stats span {
72
- color: var(--accent);
73
- font-weight: bold;
74
- }
75
-
76
- .header .conn-status {
77
- width: 8px; height: 8px;
78
- border-radius: 50%;
79
- background: var(--red);
80
- transition: background 0.3s;
81
- }
82
-
83
- .header .conn-status.connected { background: var(--accent); }
84
-
85
- .sidebar {
86
- background: var(--bg-panel);
87
- border-right: var(--pixel-border);
88
- overflow-y: auto;
89
- padding: 8px;
90
- }
91
-
92
- .sidebar h3 {
93
- font-size: 11px;
94
- text-transform: uppercase;
95
- color: var(--text-dim);
96
- padding: 8px 4px 4px;
97
- letter-spacing: 1px;
98
- }
99
-
100
- .sidebar .module-item {
101
- padding: 6px 8px;
102
- border-radius: 4px;
103
- font-size: 12px;
104
- cursor: pointer;
105
- display: flex;
106
- align-items: center;
107
- gap: 6px;
108
- transition: background 0.15s;
109
- }
110
-
111
- .sidebar .module-item:hover { background: var(--bg-card); }
112
- .sidebar .module-item .dot {
113
- width: 6px; height: 6px;
114
- border-radius: 50%;
115
- background: var(--text-dim);
116
- }
117
- .sidebar .module-item .dot.idle { background: var(--text-dim); }
118
- .sidebar .module-item .dot.testing { background: var(--orange); animation: blink 0.8s infinite; }
119
- .sidebar .module-item .dot.passed { background: var(--accent); }
120
- .sidebar .module-item .dot.failed { background: var(--red); }
121
-
122
- @keyframes blink { 50% { opacity: 0.3; } }
123
-
124
- /* ===== Main Canvas Area ===== */
125
- .main {
126
- position: relative;
127
- overflow: hidden;
128
- }
129
-
130
- #graph-canvas {
131
- width: 100%;
132
- height: 100%;
133
- display: block;
134
- }
135
-
136
- /* ===== Bottom Panel: Croc Office ===== */
137
- .office-panel {
138
- grid-column: 1 / -1;
139
- background: var(--bg-panel);
140
- border-top: var(--pixel-border);
141
- display: flex;
142
- overflow-x: auto;
143
- padding: 8px;
144
- gap: 8px;
145
- }
146
-
147
- .croc-desk {
148
- flex: 0 0 200px;
149
- background: var(--bg-card);
150
- border: 1px solid #333;
151
- border-radius: 4px;
152
- padding: 8px;
153
- display: flex;
154
- flex-direction: column;
155
- align-items: center;
156
- gap: 4px;
157
- position: relative;
158
- }
159
-
160
- .croc-desk .croc-sprite {
161
- font-size: 48px;
162
- image-rendering: pixelated;
163
- transition: transform 0.3s;
164
- position: relative;
165
- }
166
-
167
- .croc-desk.working .croc-sprite {
168
- animation: croc-work 0.6s infinite alternate;
169
- }
170
-
171
- .croc-desk.thinking .croc-sprite {
172
- animation: croc-think 1s infinite;
173
- }
174
-
175
- @keyframes croc-work {
176
- from { transform: translateY(0) rotate(-2deg); }
177
- to { transform: translateY(-4px) rotate(2deg); }
178
- }
179
-
180
- @keyframes croc-think {
181
- 0%, 100% { transform: scale(1); }
182
- 50% { transform: scale(1.05); }
183
- }
184
-
185
- .croc-desk .croc-name {
186
- font-size: 11px;
187
- font-weight: bold;
188
- color: var(--accent);
189
- }
190
-
191
- .croc-desk .croc-role {
192
- font-size: 9px;
193
- color: var(--text-dim);
194
- text-transform: uppercase;
195
- }
196
-
197
- .croc-desk .croc-task {
198
- font-size: 9px;
199
- color: var(--orange);
200
- text-align: center;
201
- max-width: 180px;
202
- overflow: hidden;
203
- text-overflow: ellipsis;
204
- white-space: nowrap;
205
- min-height: 14px;
206
- }
207
-
208
- .croc-desk .status-badge {
209
- position: absolute;
210
- top: 4px;
211
- right: 4px;
212
- width: 8px;
213
- height: 8px;
214
- border-radius: 50%;
215
- background: var(--text-dim);
216
- }
217
-
218
- .croc-desk .status-badge.idle { background: var(--text-dim); }
219
- .croc-desk .status-badge.working { background: var(--orange); animation: blink 0.6s infinite; }
220
- .croc-desk .status-badge.thinking { background: var(--blue); animation: blink 1s infinite; }
221
- .croc-desk .status-badge.done { background: var(--accent); }
222
- .croc-desk .status-badge.error { background: var(--red); }
223
-
224
- /* ===== Tooltip ===== */
225
- .tooltip {
226
- position: absolute;
227
- background: var(--bg-card);
228
- border: 1px solid var(--accent-dim);
229
- border-radius: 4px;
230
- padding: 8px 12px;
231
- font-size: 11px;
232
- pointer-events: none;
233
- z-index: 100;
234
- display: none;
235
- max-width: 300px;
236
- }
237
-
238
- .tooltip.visible { display: block; }
239
-
240
- /* ===== Action Buttons ===== */
241
- .actions {
242
- display: flex;
243
- gap: 8px;
244
- align-items: center;
245
- }
246
-
247
- .btn {
248
- background: var(--accent-dim);
249
- color: #fff;
250
- border: none;
251
- padding: 6px 14px;
252
- font-family: inherit;
253
- font-size: 11px;
254
- border-radius: 4px;
255
- cursor: pointer;
256
- transition: background 0.2s;
257
- }
258
-
259
- .btn:hover { background: var(--accent); }
260
- .btn.danger { background: #8b2035; }
261
- .btn.danger:hover { background: var(--red); }
262
-
263
- /* Pixel art decorations */
264
- .pixel-floor {
265
- position: absolute;
266
- bottom: 0;
267
- left: 0;
268
- right: 0;
269
- height: 4px;
270
- background: repeating-linear-gradient(90deg, #2a2a4a 0, #2a2a4a 8px, #1a1a3a 8px, #1a1a3a 16px);
271
- }
22
+ * { margin:0; padding:0; box-sizing:border-box; }
23
+ body { background:var(--bg-dark); color:var(--text); font-family:'Courier New','Consolas',monospace; overflow:hidden; height:100vh; }
24
+
25
+ .app { display:grid; grid-template-rows:48px 1fr 220px; grid-template-columns:220px 1fr 280px; height:100vh; }
26
+
27
+ .header { grid-column:1/-1; background:var(--bg-panel); border-bottom:var(--pixel-border); display:flex; align-items:center; padding:0 16px; gap:12px; }
28
+ .header .logo { font-size:24px; }
29
+ .header h1 { font-size:15px; color:var(--accent); }
30
+ .header .actions { display:flex; gap:6px; }
31
+ .btn { background:var(--accent-dim); color:#fff; border:none; padding:5px 12px; font-family:inherit; font-size:11px; border-radius:3px; cursor:pointer; transition:all .2s; }
32
+ .btn:hover { background:var(--accent); transform:translateY(-1px); }
33
+ .btn:active { transform:translateY(0); }
34
+ .btn:disabled { opacity:.4; cursor:not-allowed; transform:none; }
35
+ .btn.danger { background:#8b2035; }
36
+ .btn.danger:hover { background:var(--red); }
37
+ .header .stats { margin-left:auto; display:flex; gap:14px; font-size:11px; color:var(--text-dim); }
38
+ .header .stats span { color:var(--accent); font-weight:bold; }
39
+ .conn-dot { width:8px; height:8px; border-radius:50%; background:var(--red); transition:background .3s; }
40
+ .conn-dot.on { background:var(--accent); }
41
+
42
+ .sidebar { background:var(--bg-panel); border-right:var(--pixel-border); overflow-y:auto; padding:8px; }
43
+ .sidebar h3 { font-size:10px; text-transform:uppercase; color:var(--text-dim); padding:8px 4px 4px; letter-spacing:1px; }
44
+ .mod-item { padding:5px 8px; border-radius:3px; font-size:11px; cursor:pointer; display:flex; align-items:center; gap:6px; transition:background .15s; }
45
+ .mod-item:hover { background:var(--bg-card); }
46
+ .dot { width:6px; height:6px; border-radius:50%; flex-shrink:0; }
47
+ .dot.idle { background:var(--text-dim); }
48
+ .dot.testing,.dot.working { background:var(--orange); animation:blink .7s infinite; }
49
+ .dot.thinking { background:var(--blue); animation:blink 1s infinite; }
50
+ .dot.passed,.dot.done { background:var(--accent); }
51
+ .dot.failed,.dot.error { background:var(--red); }
52
+ @keyframes blink { 50%{opacity:.3} }
53
+
54
+ .main { position:relative; overflow:hidden; }
55
+ #graph-canvas { width:100%; height:100%; display:block; cursor:grab; }
56
+ .tooltip { position:absolute; background:var(--bg-card); border:1px solid var(--accent-dim); border-radius:4px; padding:8px 12px; font-size:11px; pointer-events:none; z-index:100; display:none; max-width:280px; }
57
+ .tooltip.visible { display:block; }
58
+
59
+ .log-panel { background:var(--bg-panel); border-left:var(--pixel-border); display:flex; flex-direction:column; overflow:hidden; }
60
+ .log-panel h3 { font-size:10px; text-transform:uppercase; color:var(--text-dim); padding:10px 10px 6px; letter-spacing:1px; border-bottom:1px solid #222; }
61
+ .log-list { flex:1; overflow-y:auto; padding:4px 8px; font-size:10px; line-height:1.6; }
62
+ .log-list .log-entry { padding:2px 0; border-bottom:1px solid #1a1a2e; word-break:break-all; }
63
+ .log-list .log-entry.warn { color:var(--orange); }
64
+ .log-list .log-entry.error { color:var(--red); }
65
+
66
+ .office { grid-column:1/-1; background:var(--bg-panel); border-top:var(--pixel-border); display:flex; overflow-x:auto; padding:8px; gap:8px; }
67
+ .desk { flex:0 0 180px; background:var(--bg-card); border:1px solid #333; border-radius:4px; padding:8px; display:flex; flex-direction:column; align-items:center; gap:2px; position:relative; overflow:hidden; }
68
+ .desk .badge { position:absolute; top:4px; right:4px; width:8px; height:8px; border-radius:50%; }
69
+ .desk .croc-sprite { font-size:40px; position:relative; z-index:1; }
70
+ .desk.idle .croc-sprite { animation:croc-idle 3s infinite; }
71
+ .desk.working .croc-sprite { animation:croc-work .5s infinite alternate; }
72
+ .desk.thinking .croc-sprite { animation:croc-think 1.2s infinite; }
73
+ .desk.done .croc-sprite { animation:croc-done .8s 1; }
74
+ .desk.error .croc-sprite { animation:croc-error .3s 3; }
75
+ @keyframes croc-idle { 0%,90%,100%{transform:translateY(0)} 95%{transform:translateY(-3px)} }
76
+ @keyframes croc-work { from{transform:translateY(0) rotate(-3deg)} to{transform:translateY(-5px) rotate(3deg)} }
77
+ @keyframes croc-think { 0%,100%{transform:scale(1) rotate(0)} 25%{transform:scale(1.05) rotate(-2deg)} 75%{transform:scale(1.05) rotate(2deg)} }
78
+ @keyframes croc-done { 0%{transform:scale(1)} 50%{transform:scale(1.2) translateY(-8px)} 100%{transform:scale(1)} }
79
+ @keyframes croc-error { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-4px)} 75%{transform:translateX(4px)} }
80
+ .desk .croc-name { font-size:11px; font-weight:bold; color:var(--accent); }
81
+ .desk .croc-role { font-size:9px; color:var(--text-dim); text-transform:uppercase; }
82
+ .desk .croc-task { font-size:9px; color:var(--orange); text-align:center; max-width:160px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-height:13px; }
83
+ .desk .progress-bar { width:90%; height:3px; background:#222; border-radius:2px; margin-top:2px; overflow:hidden; }
84
+ .desk .progress-bar .fill { height:100%; background:var(--accent); transition:width .3s; border-radius:2px; }
85
+ .desk .desk-items { position:absolute; bottom:4px; right:8px; font-size:14px; opacity:.4; }
272
86
  </style>
273
87
  </head>
274
88
  <body>
275
-
276
89
  <div class="app">
277
- <!-- Header -->
278
90
  <header class="header">
279
91
  <div class="logo">๐ŸŠ</div>
280
92
  <h1>OpenCroc Studio</h1>
281
93
  <div class="actions">
282
- <button class="btn" id="btn-scan" title="Re-scan project">๐Ÿ”„ Scan</button>
283
- <button class="btn" id="btn-test" title="Run all tests">โ–ถ Test</button>
94
+ <button class="btn" id="btn-scan" title="Scan project">๐Ÿ” Scan</button>
95
+ <button class="btn" id="btn-pipeline" title="Run full pipeline">โ–ถ Pipeline</button>
96
+ <button class="btn danger" id="btn-reset" title="Reset agents">โน Reset</button>
284
97
  </div>
285
- <div class="stats" id="header-stats">
286
- <div>Modules: <span id="stat-modules">-</span></div>
287
- <div>Models: <span id="stat-models">-</span></div>
288
- <div>APIs: <span id="stat-endpoints">-</span></div>
98
+ <div class="stats">
99
+ <div>Modules: <span id="s-mod">-</span></div>
100
+ <div>Models: <span id="s-mdl">-</span></div>
101
+ <div>APIs: <span id="s-api">-</span></div>
289
102
  </div>
290
- <div class="conn-status" id="conn-status" title="WebSocket"></div>
103
+ <div class="conn-dot" id="conn-dot" title="WebSocket"></div>
291
104
  </header>
292
105
 
293
- <!-- Sidebar: Module List -->
294
- <aside class="sidebar" id="sidebar">
295
- <h3>๐Ÿ—‚ Modules</h3>
296
- <div id="module-list"></div>
297
- <h3 style="margin-top:16px">๐ŸŠ Agents</h3>
298
- <div id="agent-list-sidebar"></div>
106
+ <aside class="sidebar">
107
+ <h3>๐Ÿ“‚ Modules</h3>
108
+ <div id="mod-list"></div>
109
+ <h3 style="margin-top:12px">๐ŸŠ Agents</h3>
110
+ <div id="agent-sidebar"></div>
299
111
  </aside>
300
112
 
301
- <!-- Main: Knowledge Graph Canvas -->
302
113
  <main class="main">
303
114
  <canvas id="graph-canvas"></canvas>
304
115
  <div class="tooltip" id="tooltip"></div>
305
116
  </main>
306
117
 
307
- <!-- Bottom: Pixel Croc Office -->
308
- <section class="office-panel" id="croc-office">
309
- <!-- Populated by JS -->
310
- </section>
118
+ <div class="log-panel">
119
+ <h3>๐Ÿ“‹ Activity Log</h3>
120
+ <div class="log-list" id="log-list"></div>
121
+ </div>
122
+
123
+ <section class="office" id="croc-office"></section>
311
124
  </div>
312
125
 
313
126
  <script>
314
- // ============================================
315
- // OpenCroc Studio โ€” M1 Frontend
316
- // ============================================
317
-
318
- const state = {
319
- project: null,
320
- graph: { nodes: [], edges: [] },
321
- agents: [],
322
- ws: null,
323
- selectedNode: null,
324
- // Canvas rendering state
325
- pan: { x: 0, y: 0 },
326
- zoom: 1,
327
- dragging: false,
328
- dragStart: { x: 0, y: 0 },
329
- nodePositions: new Map(),
330
- hoveredNode: null,
127
+ const S = {
128
+ project:null, graph:{nodes:[],edges:[]}, agents:[], ws:null,
129
+ pan:{x:0,y:0}, zoom:1, dragging:false, dragStart:{x:0,y:0},
130
+ nodePos:new Map(), hoveredNode:null, running:false
331
131
  };
332
132
 
333
- // ============ API ============
334
-
335
- async function fetchProject() {
336
- try {
337
- const res = await fetch('/api/project');
338
- state.project = await res.json();
339
- state.graph = state.project.graph || { nodes: [], edges: [] };
340
- state.agents = state.project.agents || [];
341
- layoutGraph();
342
- updateUI();
343
- } catch (e) {
344
- console.error('Failed to fetch project:', e);
345
- }
133
+ async function fetchProject(){
134
+ try{
135
+ const r=await fetch('/api/project'); S.project=await r.json();
136
+ S.graph=S.project.graph||{nodes:[],edges:[]}; S.agents=S.project.agents||[];
137
+ layoutGraph(); updateAll();
138
+ }catch(e){addLog('Failed to load: '+e.message,'error');}
346
139
  }
347
-
348
- async function refreshProject() {
349
- try {
350
- await fetch('/api/project/refresh', { method: 'POST' });
351
- await fetchProject();
352
- } catch (e) {
353
- console.error('Failed to refresh:', e);
354
- }
140
+ async function doScan(){
141
+ if(S.running)return; S.running=true; updateBtns();
142
+ addLog('๐Ÿ” Starting scan...');
143
+ try{await fetch('/api/scan',{method:'POST'});}
144
+ catch(e){addLog('Scan failed: '+e.message,'error');S.running=false;updateBtns();}
145
+ }
146
+ async function doPipeline(){
147
+ if(S.running)return; S.running=true; updateBtns();
148
+ addLog('โ–ถ Starting pipeline...');
149
+ try{await fetch('/api/pipeline',{method:'POST'});}
150
+ catch(e){addLog('Pipeline failed: '+e.message,'error');S.running=false;updateBtns();}
151
+ }
152
+ async function doReset(){
153
+ try{await fetch('/api/reset',{method:'POST'});}catch(e){addLog('Reset failed','error');}
154
+ S.running=false; updateBtns(); addLog('โน Agents reset');
155
+ }
156
+ function updateBtns(){
157
+ document.getElementById('btn-scan').disabled=S.running;
158
+ document.getElementById('btn-pipeline').disabled=S.running;
355
159
  }
356
160
 
357
- // ============ WebSocket ============
358
-
359
- function connectWS() {
360
- const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
361
- state.ws = new WebSocket(`${protocol}//${location.host}/ws`);
362
-
363
- state.ws.onopen = () => {
364
- document.getElementById('conn-status').classList.add('connected');
365
- };
366
-
367
- state.ws.onmessage = (e) => {
368
- try {
369
- const msg = JSON.parse(e.data);
370
- if (msg.type === 'agent:update' && Array.isArray(msg.payload)) {
371
- state.agents = msg.payload;
372
- renderCrocOffice();
373
- renderAgentSidebar();
374
- } else if (msg.type === 'graph:update') {
375
- state.graph = msg.payload;
376
- layoutGraph();
377
- renderCanvas();
161
+ function connectWS(){
162
+ const p=location.protocol==='https:'?'wss:':'ws:';
163
+ S.ws=new WebSocket(p+'//'+location.host+'/ws');
164
+ S.ws.onopen=()=>{document.getElementById('conn-dot').classList.add('on');};
165
+ S.ws.onmessage=(e)=>{
166
+ try{
167
+ const m=JSON.parse(e.data);
168
+ if(m.type==='agent:update'&&Array.isArray(m.payload)){
169
+ S.agents=m.payload; renderOffice(); renderAgentSB();
170
+ }else if(m.type==='graph:update'){
171
+ S.graph=m.payload; layoutGraph(); renderCanvas(); renderModList(); updateStats();
172
+ }else if(m.type==='log'){
173
+ addLog(m.payload.message, m.payload.level);
174
+ }else if(m.type==='pipeline:complete'){
175
+ S.running=false; updateBtns();
176
+ if(m.payload.status==='success') addLog('โœ… Pipeline complete!');
177
+ else addLog('โŒ Pipeline failed: '+(m.payload.error||''),'error');
178
+ setTimeout(fetchProject,500);
378
179
  }
379
- } catch {}
380
- };
381
-
382
- state.ws.onclose = () => {
383
- document.getElementById('conn-status').classList.remove('connected');
384
- // Reconnect after 3s
385
- setTimeout(connectWS, 3000);
180
+ }catch{}
386
181
  };
182
+ S.ws.onclose=()=>{document.getElementById('conn-dot').classList.remove('on');setTimeout(connectWS,3000);};
387
183
  }
388
184
 
389
- // ============ Graph Layout (Force-directed) ============
390
-
391
- function layoutGraph() {
392
- const nodes = state.graph.nodes;
393
- if (!nodes.length) return;
394
-
395
- const canvas = document.getElementById('graph-canvas');
396
- const w = canvas.width;
397
- const h = canvas.height;
398
- const cx = w / 2;
399
- const cy = h / 2;
400
-
401
- // Group by module
402
- const modules = new Map();
403
- for (const node of nodes) {
404
- const m = node.module || node.id;
405
- if (!modules.has(m)) modules.set(m, []);
406
- modules.get(m).push(node);
407
- }
408
-
409
- const moduleKeys = [...modules.keys()];
410
- const moduleCount = moduleKeys.length || 1;
411
-
412
- for (let mi = 0; mi < moduleKeys.length; mi++) {
413
- const modNodes = modules.get(moduleKeys[mi]);
414
- const angle = (mi / moduleCount) * Math.PI * 2 - Math.PI / 2;
415
- const radius = Math.min(w, h) * 0.3;
416
- const modCx = cx + Math.cos(angle) * radius;
417
- const modCy = cy + Math.sin(angle) * radius;
185
+ function addLog(msg,level){
186
+ level=level||'info';
187
+ const el=document.getElementById('log-list');
188
+ const d=document.createElement('div'); d.className='log-entry '+level;
189
+ d.textContent='['+new Date().toLocaleTimeString()+'] '+msg;
190
+ el.appendChild(d); el.scrollTop=el.scrollHeight;
191
+ }
192
+ function updateStats(){
193
+ document.getElementById('s-mod').textContent=S.graph.nodes.filter(n=>n.type==='module').length;
194
+ document.getElementById('s-mdl').textContent=S.graph.nodes.filter(n=>n.type==='model').length;
195
+ document.getElementById('s-api').textContent=S.graph.nodes.filter(n=>n.type==='controller'||n.type==='api').length;
196
+ }
418
197
 
419
- for (let ni = 0; ni < modNodes.length; ni++) {
420
- const nAngle = (ni / modNodes.length) * Math.PI * 2;
421
- const nRadius = 40 + modNodes.length * 10;
422
- state.nodePositions.set(modNodes[ni].id, {
423
- x: modCx + Math.cos(nAngle) * nRadius,
424
- y: modCy + Math.sin(nAngle) * nRadius,
425
- });
198
+ function layoutGraph(){
199
+ const nodes=S.graph.nodes; if(!nodes.length)return;
200
+ const c=document.getElementById('graph-canvas');
201
+ const w=c.clientWidth||800,h=c.clientHeight||500,cx=w/2,cy=h/2;
202
+ const mods=new Map();
203
+ for(const n of nodes){const m=n.module||n.id;if(!mods.has(m))mods.set(m,[]);mods.get(m).push(n);}
204
+ const keys=[...mods.keys()];
205
+ for(let i=0;i<keys.length;i++){
206
+ const mn=mods.get(keys[i]);
207
+ const a=(i/keys.length)*Math.PI*2-Math.PI/2, r=Math.min(w,h)*.3;
208
+ const mcx=cx+Math.cos(a)*r, mcy=cy+Math.sin(a)*r;
209
+ for(let j=0;j<mn.length;j++){
210
+ const na=(j/mn.length)*Math.PI*2, nr=35+mn.length*8;
211
+ S.nodePos.set(mn[j].id,{x:mcx+Math.cos(na)*nr,y:mcy+Math.sin(na)*nr});
426
212
  }
427
213
  }
428
214
  }
429
215
 
430
- // ============ Canvas Rendering ============
431
-
432
- function renderCanvas() {
433
- const canvas = document.getElementById('graph-canvas');
434
- const ctx = canvas.getContext('2d');
435
- const dpr = window.devicePixelRatio || 1;
436
-
437
- canvas.width = canvas.clientWidth * dpr;
438
- canvas.height = canvas.clientHeight * dpr;
439
- ctx.scale(dpr, dpr);
440
-
441
- const w = canvas.clientWidth;
442
- const h = canvas.clientHeight;
443
-
444
- ctx.clearRect(0, 0, w, h);
445
- ctx.save();
446
- ctx.translate(state.pan.x, state.pan.y);
447
- ctx.scale(state.zoom, state.zoom);
448
-
449
- // Draw grid (pixel art style)
450
- ctx.strokeStyle = '#1a1a3a';
451
- ctx.lineWidth = 0.5;
452
- const gridSize = 32;
453
- for (let x = 0; x < w * 2; x += gridSize) {
454
- ctx.beginPath(); ctx.moveTo(x, -h); ctx.lineTo(x, h * 2); ctx.stroke();
216
+ function renderCanvas(){
217
+ const canvas=document.getElementById('graph-canvas'),ctx=canvas.getContext('2d');
218
+ const dpr=window.devicePixelRatio||1;
219
+ canvas.width=canvas.clientWidth*dpr; canvas.height=canvas.clientHeight*dpr;
220
+ ctx.scale(dpr,dpr);
221
+ const w=canvas.clientWidth,h=canvas.clientHeight;
222
+ ctx.clearRect(0,0,w,h); ctx.save(); ctx.translate(S.pan.x,S.pan.y); ctx.scale(S.zoom,S.zoom);
223
+ // Grid
224
+ ctx.strokeStyle='#151530';ctx.lineWidth=.5;
225
+ for(let x=0;x<w*2;x+=32){ctx.beginPath();ctx.moveTo(x,-h);ctx.lineTo(x,h*2);ctx.stroke();}
226
+ for(let y=0;y<h*2;y+=32){ctx.beginPath();ctx.moveTo(-w,y);ctx.lineTo(w*2,y);ctx.stroke();}
227
+ const edges=S.graph.edges||[],nodes=S.graph.nodes||[];
228
+ // Edges
229
+ for(const e of edges){
230
+ const s=S.nodePos.get(e.source),t=S.nodePos.get(e.target);if(!s||!t)continue;
231
+ ctx.strokeStyle='rgba(78,204,163,.25)';ctx.lineWidth=1.5;
232
+ ctx.beginPath();ctx.moveTo(s.x,s.y);ctx.lineTo(t.x,t.y);ctx.stroke();
233
+ const a=Math.atan2(t.y-s.y,t.x-s.x),al=7,ax=t.x-Math.cos(a)*18,ay=t.y-Math.sin(a)*18;
234
+ ctx.fillStyle='rgba(78,204,163,.4)';ctx.beginPath();ctx.moveTo(ax,ay);
235
+ ctx.lineTo(ax-al*Math.cos(a-.4),ay-al*Math.sin(a-.4));
236
+ ctx.lineTo(ax-al*Math.cos(a+.4),ay-al*Math.sin(a+.4));ctx.closePath();ctx.fill();
237
+ }
238
+ const tc={model:'#4ecca3',controller:'#e94560',api:'#f39c12',dto:'#3498db',module:'#9b59b6'};
239
+ const sc={idle:'#444',testing:'#f39c12',passed:'#4ecca3',failed:'#e94560'};
240
+ const icons={model:'๐Ÿ“ฆ',controller:'๐ŸŽฎ',api:'๐Ÿ”Œ',dto:'๐Ÿ“‹',module:'๐Ÿ“'};
241
+ for(const n of nodes){
242
+ const p=S.nodePos.get(n.id);if(!p)continue;
243
+ const sz=n.type==='module'?22:14,c=tc[n.type]||'#888',ol=sc[n.status]||'#444',hov=S.hoveredNode===n.id;
244
+ if(n.status==='testing'){ctx.shadowColor=sc.testing;ctx.shadowBlur=12;}
245
+ else if(n.status==='passed'){ctx.shadowColor=sc.passed;ctx.shadowBlur=8;}
246
+ else if(n.status==='failed'){ctx.shadowColor=sc.failed;ctx.shadowBlur=10;}
247
+ ctx.fillStyle='rgba(0,0,0,.3)';ctx.fillRect(p.x-sz+2,p.y-sz+2,sz*2,sz*2);
248
+ ctx.fillStyle=c;ctx.fillRect(p.x-sz,p.y-sz,sz*2,sz*2);
249
+ ctx.fillStyle='rgba(255,255,255,.15)';ctx.fillRect(p.x-sz,p.y-sz,sz*2,3);ctx.fillRect(p.x-sz,p.y-sz,3,sz*2);
250
+ ctx.shadowBlur=0;ctx.strokeStyle=hov?'#fff':ol;ctx.lineWidth=hov?3:2;ctx.strokeRect(p.x-sz,p.y-sz,sz*2,sz*2);
251
+ ctx.font=sz+'px serif';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(icons[n.type]||'โฌœ',p.x,p.y);
252
+ ctx.font='10px "Courier New"';ctx.fillStyle=hov?'#fff':'#bbb';ctx.textAlign='center';ctx.textBaseline='top';
253
+ ctx.fillText((n.label||n.id.split(':').pop()).substring(0,20),p.x,p.y+sz+4);
455
254
  }
456
- for (let y = 0; y < h * 2; y += gridSize) {
457
- ctx.beginPath(); ctx.moveTo(-w, y); ctx.lineTo(w * 2, y); ctx.stroke();
458
- }
459
-
460
- const edges = state.graph.edges || [];
461
- const nodes = state.graph.nodes || [];
462
-
463
- // Draw edges
464
- ctx.lineWidth = 1.5;
465
- for (const edge of edges) {
466
- const s = state.nodePositions.get(edge.source);
467
- const t = state.nodePositions.get(edge.target);
468
- if (!s || !t) continue;
469
-
470
- const gradient = ctx.createLinearGradient(s.x, s.y, t.x, t.y);
471
- gradient.addColorStop(0, 'rgba(78, 204, 163, 0.4)');
472
- gradient.addColorStop(1, 'rgba(78, 204, 163, 0.1)');
473
- ctx.strokeStyle = gradient;
474
-
475
- ctx.beginPath();
476
- ctx.moveTo(s.x, s.y);
477
- ctx.lineTo(t.x, t.y);
478
- ctx.stroke();
479
-
480
- // Arrow head
481
- const angle = Math.atan2(t.y - s.y, t.x - s.x);
482
- const arrowLen = 8;
483
- const ax = t.x - Math.cos(angle) * 20;
484
- const ay = t.y - Math.sin(angle) * 20;
485
- ctx.fillStyle = 'rgba(78, 204, 163, 0.5)';
486
- ctx.beginPath();
487
- ctx.moveTo(ax, ay);
488
- ctx.lineTo(ax - arrowLen * Math.cos(angle - 0.4), ay - arrowLen * Math.sin(angle - 0.4));
489
- ctx.lineTo(ax - arrowLen * Math.cos(angle + 0.4), ay - arrowLen * Math.sin(angle + 0.4));
490
- ctx.closePath();
491
- ctx.fill();
492
- }
493
-
494
- // Draw nodes (pixel art style โ€” square blocks)
495
- const typeColors = {
496
- model: '#4ecca3',
497
- controller: '#e94560',
498
- api: '#f39c12',
499
- dto: '#3498db',
500
- module: '#9b59b6',
501
- };
502
-
503
- const statusOutline = {
504
- idle: '#555',
505
- testing: '#f39c12',
506
- passed: '#4ecca3',
507
- failed: '#e94560',
508
- };
509
-
510
- for (const node of nodes) {
511
- const pos = state.nodePositions.get(node.id);
512
- if (!pos) continue;
513
-
514
- const size = node.type === 'module' ? 24 : 16;
515
- const color = typeColors[node.type] || '#888';
516
- const outline = statusOutline[node.status] || '#555';
517
- const isHovered = state.hoveredNode === node.id;
518
-
519
- // Shadow
520
- ctx.fillStyle = 'rgba(0,0,0,0.3)';
521
- ctx.fillRect(pos.x - size + 2, pos.y - size + 2, size * 2, size * 2);
522
-
523
- // Main block
524
- ctx.fillStyle = color;
525
- ctx.fillRect(pos.x - size, pos.y - size, size * 2, size * 2);
526
-
527
- // Pixel highlight (top-left)
528
- ctx.fillStyle = 'rgba(255,255,255,0.2)';
529
- ctx.fillRect(pos.x - size, pos.y - size, size * 2, 3);
530
- ctx.fillRect(pos.x - size, pos.y - size, 3, size * 2);
531
-
532
- // Status outline
533
- ctx.strokeStyle = isHovered ? '#fff' : outline;
534
- ctx.lineWidth = isHovered ? 3 : 2;
535
- ctx.strokeRect(pos.x - size, pos.y - size, size * 2, size * 2);
536
-
537
- // Icon (pixel emoji)
538
- ctx.font = `${size}px serif`;
539
- ctx.textAlign = 'center';
540
- ctx.textBaseline = 'middle';
541
- const icons = { model: '๐Ÿ“ฆ', controller: '๐ŸŽฎ', api: '๐Ÿ”Œ', dto: '๐Ÿ“‹', module: '๐Ÿ“' };
542
- ctx.fillText(icons[node.type] || 'โฌœ', pos.x, pos.y);
543
-
544
- // Label
545
- ctx.font = '10px "Courier New"';
546
- ctx.fillStyle = isHovered ? '#fff' : '#ccc';
547
- ctx.textAlign = 'center';
548
- ctx.textBaseline = 'top';
549
- const label = node.label || node.id.split(':').pop();
550
- ctx.fillText(label, pos.x, pos.y + size + 4);
551
- }
552
-
553
255
  ctx.restore();
554
256
  }
555
257
 
556
- // ============ Canvas Interaction ============
557
-
558
- function setupCanvasInteraction() {
559
- const canvas = document.getElementById('graph-canvas');
560
-
561
- canvas.addEventListener('mousedown', (e) => {
562
- state.dragging = true;
563
- state.dragStart = { x: e.clientX - state.pan.x, y: e.clientY - state.pan.y };
564
- });
565
-
566
- canvas.addEventListener('mousemove', (e) => {
567
- if (state.dragging) {
568
- state.pan.x = e.clientX - state.dragStart.x;
569
- state.pan.y = e.clientY - state.dragStart.y;
570
- renderCanvas();
571
- }
572
-
573
- // Hit-test nodes
574
- const rect = canvas.getBoundingClientRect();
575
- const mx = (e.clientX - rect.left - state.pan.x) / state.zoom;
576
- const my = (e.clientY - rect.top - state.pan.y) / state.zoom;
577
-
578
- let hit = null;
579
- for (const node of state.graph.nodes) {
580
- const pos = state.nodePositions.get(node.id);
581
- if (!pos) continue;
582
- const size = node.type === 'module' ? 24 : 16;
583
- if (mx >= pos.x - size && mx <= pos.x + size && my >= pos.y - size && my <= pos.y + size) {
584
- hit = node;
585
- break;
586
- }
587
- }
588
-
589
- const tooltip = document.getElementById('tooltip');
590
- if (hit) {
591
- state.hoveredNode = hit.id;
592
- tooltip.innerHTML = `<b>${hit.label || hit.id}</b><br>Type: ${hit.type}<br>Status: ${hit.status}${hit.module ? '<br>Module: ' + hit.module : ''}`;
593
- tooltip.style.left = (e.clientX + 12) + 'px';
594
- tooltip.style.top = (e.clientY + 12) + 'px';
595
- tooltip.classList.add('visible');
596
- canvas.style.cursor = 'pointer';
597
- } else {
598
- if (state.hoveredNode) {
599
- state.hoveredNode = null;
600
- renderCanvas();
601
- }
602
- tooltip.classList.remove('visible');
603
- canvas.style.cursor = state.dragging ? 'grabbing' : 'grab';
604
- }
605
- if (hit) renderCanvas();
606
- });
607
-
608
- canvas.addEventListener('mouseup', () => { state.dragging = false; });
609
- canvas.addEventListener('mouseleave', () => {
610
- state.dragging = false;
611
- document.getElementById('tooltip').classList.remove('visible');
612
- });
613
-
614
- canvas.addEventListener('wheel', (e) => {
615
- e.preventDefault();
616
- const delta = e.deltaY > 0 ? 0.9 : 1.1;
617
- state.zoom = Math.max(0.2, Math.min(3, state.zoom * delta));
258
+ function setupCanvas(){
259
+ const c=document.getElementById('graph-canvas');
260
+ c.addEventListener('mousedown',e=>{S.dragging=true;S.dragStart={x:e.clientX-S.pan.x,y:e.clientY-S.pan.y};c.style.cursor='grabbing';});
261
+ c.addEventListener('mousemove',e=>{
262
+ if(S.dragging){S.pan.x=e.clientX-S.dragStart.x;S.pan.y=e.clientY-S.dragStart.y;renderCanvas();}
263
+ const rect=c.getBoundingClientRect(),mx=(e.clientX-rect.left-S.pan.x)/S.zoom,my=(e.clientY-rect.top-S.pan.y)/S.zoom;
264
+ let hit=null;
265
+ for(const n of S.graph.nodes){const p=S.nodePos.get(n.id);if(!p)continue;const sz=n.type==='module'?22:14;if(mx>=p.x-sz&&mx<=p.x+sz&&my>=p.y-sz&&my<=p.y+sz){hit=n;break;}}
266
+ const tt=document.getElementById('tooltip');
267
+ if(hit){
268
+ S.hoveredNode=hit.id;
269
+ const sc={idle:'#888',testing:'#f39c12',passed:'#4ecca3',failed:'#e94560'};
270
+ tt.innerHTML='<b>'+esc(hit.label||hit.id)+'</b><br>Type: '+hit.type+'<br>Status: <span style="color:'+(sc[hit.status]||'#888')+'">'+hit.status+'</span>'+(hit.module?'<br>Module: '+esc(hit.module):'');
271
+ tt.style.left=(e.clientX+12)+'px';tt.style.top=(e.clientY+12)+'px';tt.classList.add('visible');c.style.cursor='pointer';
272
+ }else{if(S.hoveredNode)S.hoveredNode=null;tt.classList.remove('visible');if(!S.dragging)c.style.cursor='grab';}
618
273
  renderCanvas();
619
- }, { passive: false });
620
- }
621
-
622
- // ============ UI Updates ============
623
-
624
- function updateUI() {
625
- if (state.project) {
626
- document.getElementById('stat-modules').textContent = state.project.stats?.modules || 0;
627
- document.getElementById('stat-models').textContent = state.project.stats?.models || 0;
628
- document.getElementById('stat-endpoints').textContent = state.project.stats?.endpoints || 0;
629
- }
630
-
631
- renderModuleList();
632
- renderCrocOffice();
633
- renderAgentSidebar();
634
- renderCanvas();
274
+ });
275
+ c.addEventListener('mouseup',()=>{S.dragging=false;c.style.cursor='grab';});
276
+ c.addEventListener('mouseleave',()=>{S.dragging=false;document.getElementById('tooltip').classList.remove('visible');});
277
+ c.addEventListener('wheel',e=>{e.preventDefault();S.zoom=Math.max(.2,Math.min(3,S.zoom*(e.deltaY>0?.92:1.08)));renderCanvas();},{passive:false});
635
278
  }
636
279
 
637
- function renderModuleList() {
638
- const list = document.getElementById('module-list');
639
- const modules = state.graph.nodes.filter(n => n.type === 'module');
640
-
641
- if (!modules.length) {
642
- list.innerHTML = '<div style="padding:8px;color:#666;font-size:11px">No modules found</div>';
643
- return;
280
+ function updateAll(){
281
+ if(S.project){
282
+ document.getElementById('s-mod').textContent=S.project.stats?.modules||0;
283
+ document.getElementById('s-mdl').textContent=S.project.stats?.models||0;
284
+ document.getElementById('s-api').textContent=S.project.stats?.endpoints||0;
644
285
  }
645
-
646
- list.innerHTML = modules.map(m => `
647
- <div class="module-item" data-id="${esc(m.id)}">
648
- <div class="dot ${m.status}"></div>
649
- ${esc(m.label || m.id.replace('module:', ''))}
650
- </div>
651
- `).join('');
286
+ renderModList();renderOffice();renderAgentSB();renderCanvas();
652
287
  }
653
-
654
- function renderAgentSidebar() {
655
- const list = document.getElementById('agent-list-sidebar');
656
- list.innerHTML = state.agents.map(a => `
657
- <div class="module-item">
658
- <div class="dot ${a.status}"></div>
659
- ${esc(a.name)} <span style="color:#666;font-size:9px">${a.status}</span>
660
- </div>
661
- `).join('');
288
+ function renderModList(){
289
+ const el=document.getElementById('mod-list'),mods=S.graph.nodes.filter(n=>n.type==='module');
290
+ if(!mods.length){el.innerHTML='<div style="padding:8px;color:#555;font-size:10px">No modules found</div>';return;}
291
+ el.innerHTML=mods.map(m=>'<div class="mod-item"><div class="dot '+m.status+'"></div>'+esc(m.label||m.id.replace('module:',''))+'</div>').join('');
662
292
  }
663
-
664
- function renderCrocOffice() {
665
- const office = document.getElementById('croc-office');
666
-
667
- const sprites = {
668
- parser: '๐ŸŠ',
669
- analyzer: '๐ŸŠ',
670
- tester: '๐ŸŠ',
671
- healer: '๐ŸŠ',
672
- planner: '๐ŸŠ',
673
- reporter: '๐ŸŠ',
674
- };
675
-
676
- // Desk decorations per role
677
- const deskDecor = {
678
- parser: '๐Ÿ’ป',
679
- analyzer: '๐Ÿ“Š',
680
- tester: '๐Ÿงช',
681
- healer: '๐Ÿ”ง',
682
- planner: '๐Ÿ“‹',
683
- reporter: '๐Ÿ“',
684
- };
685
-
686
- office.innerHTML = state.agents.map(a => `
687
- <div class="croc-desk ${a.status}">
688
- <div class="status-badge ${a.status}"></div>
689
- <div class="croc-sprite">${sprites[a.role] || '๐ŸŠ'}</div>
690
- <div class="croc-name">${esc(a.name)}</div>
691
- <div class="croc-role">${esc(a.role)} ${deskDecor[a.role] || ''}</div>
692
- <div class="croc-task">${a.currentTask ? esc(a.currentTask) : ''}</div>
693
- <div class="pixel-floor"></div>
694
- </div>
695
- `).join('');
293
+ function renderAgentSB(){
294
+ document.getElementById('agent-sidebar').innerHTML=S.agents.map(a=>
295
+ '<div class="mod-item"><div class="dot '+a.status+'"></div>'+esc(a.name)+' <span style="color:#555;font-size:9px">'+a.status+'</span></div>'
296
+ ).join('');
696
297
  }
697
-
698
- function esc(s) {
699
- if (!s) return '';
700
- return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
298
+ function renderOffice(){
299
+ const dd={parser:'๐Ÿ’ป',analyzer:'๐Ÿ“Š',tester:'๐Ÿงช',healer:'๐Ÿ”ง',planner:'๐Ÿ“‹',reporter:'๐Ÿ“'};
300
+ document.getElementById('croc-office').innerHTML=S.agents.map(a=>{
301
+ const prog=typeof a.progress==='number'?a.progress:0;
302
+ return '<div class="desk '+a.status+'"><div class="badge dot '+a.status+'"></div>'+
303
+ '<div class="croc-sprite">๐ŸŠ</div><div class="croc-name">'+esc(a.name)+'</div>'+
304
+ '<div class="croc-role">'+esc(a.role)+'</div>'+
305
+ '<div class="croc-task">'+(a.currentTask?esc(a.currentTask):'')+'</div>'+
306
+ '<div class="progress-bar"><div class="fill" style="width:'+prog+'%"></div></div>'+
307
+ '<div class="desk-items">'+(dd[a.role]||'')+'</div></div>';
308
+ }).join('');
701
309
  }
310
+ function esc(s){return s?s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'):'';}
702
311
 
703
- // ============ Event Handlers ============
704
-
705
- document.getElementById('btn-scan').addEventListener('click', refreshProject);
706
- document.getElementById('btn-test').addEventListener('click', async () => {
707
- // M2: will trigger real test pipeline
708
- try {
709
- await fetch('/api/agents/tester-croc/task', {
710
- method: 'POST',
711
- headers: { 'Content-Type': 'application/json' },
712
- body: JSON.stringify({ task: 'Running E2E tests...' }),
713
- });
714
- } catch (e) {
715
- console.error('Failed to start test:', e);
716
- }
717
- });
312
+ document.getElementById('btn-scan').addEventListener('click',doScan);
313
+ document.getElementById('btn-pipeline').addEventListener('click',doPipeline);
314
+ document.getElementById('btn-reset').addEventListener('click',doReset);
718
315
 
719
- // ============ Init ============
720
-
721
- (async () => {
722
- setupCanvasInteraction();
723
- await fetchProject();
724
- connectWS();
725
-
726
- // Handle resize
727
- window.addEventListener('resize', () => {
728
- layoutGraph();
729
- renderCanvas();
730
- });
731
- })();
316
+ (async()=>{setupCanvas();await fetchProject();connectWS();addLog('๐ŸŠ OpenCroc Studio ready');
317
+ window.addEventListener('resize',()=>{layoutGraph();renderCanvas();});})();
732
318
  </script>
733
319
  </body>
734
320
  </html>