@omerrgocmen/crewctl 1.0.2 → 1.0.3

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.
Files changed (84) hide show
  1. package/orchestrator/README.md +462 -462
  2. package/orchestrator/config.default.json +71 -71
  3. package/orchestrator/roles/operator.md +77 -77
  4. package/orchestrator/roles/reviewer.md +63 -63
  5. package/orchestrator/skills/acceptance-criteria.md +18 -18
  6. package/orchestrator/skills/accessibility-audit.md +16 -16
  7. package/orchestrator/skills/accessible-forms.md +16 -16
  8. package/orchestrator/skills/api-design.md +27 -27
  9. package/orchestrator/skills/api-documentation.md +16 -16
  10. package/orchestrator/skills/architecture-decision-record.md +18 -18
  11. package/orchestrator/skills/authentication-design.md +16 -16
  12. package/orchestrator/skills/authorization-review.md +16 -16
  13. package/orchestrator/skills/backward-compatibility.md +17 -17
  14. package/orchestrator/skills/changelog-writing.md +16 -16
  15. package/orchestrator/skills/ci-pipeline-design.md +16 -16
  16. package/orchestrator/skills/cli-design.md +16 -16
  17. package/orchestrator/skills/code-review.md +27 -27
  18. package/orchestrator/skills/configuration-management.md +16 -16
  19. package/orchestrator/skills/container-review.md +16 -16
  20. package/orchestrator/skills/contract-testing.md +16 -16
  21. package/orchestrator/skills/dashboard-design.md +16 -16
  22. package/orchestrator/skills/database-migration.md +16 -16
  23. package/orchestrator/skills/database-schema-design.md +16 -16
  24. package/orchestrator/skills/debugging.md +26 -26
  25. package/orchestrator/skills/dependency-review.md +16 -16
  26. package/orchestrator/skills/design-review.md +29 -29
  27. package/orchestrator/skills/design-system.md +16 -16
  28. package/orchestrator/skills/docs-api-reference.md +16 -16
  29. package/orchestrator/skills/docs-troubleshooting.md +16 -16
  30. package/orchestrator/skills/docs-tutorial.md +16 -16
  31. package/orchestrator/skills/docs-writing.md +25 -25
  32. package/orchestrator/skills/empty-error-loading-states.md +16 -16
  33. package/orchestrator/skills/end-to-end-testing.md +16 -16
  34. package/orchestrator/skills/error-handling.md +16 -16
  35. package/orchestrator/skills/frontend-design.md +28 -28
  36. package/orchestrator/skills/git-commit-writing.md +16 -16
  37. package/orchestrator/skills/graphql-design.md +16 -16
  38. package/orchestrator/skills/incident-runbook.md +18 -18
  39. package/orchestrator/skills/input-validation.md +16 -16
  40. package/orchestrator/skills/integration-testing.md +16 -16
  41. package/orchestrator/skills/interaction-design.md +16 -16
  42. package/orchestrator/skills/landing-page-design.md +16 -16
  43. package/orchestrator/skills/observability-design.md +16 -16
  44. package/orchestrator/skills/openapi-contract.md +16 -16
  45. package/orchestrator/skills/performance-profiling.md +16 -16
  46. package/orchestrator/skills/privacy-review.md +16 -16
  47. package/orchestrator/skills/property-based-testing.md +16 -16
  48. package/orchestrator/skills/pull-request-writing.md +16 -16
  49. package/orchestrator/skills/refactoring.md +16 -16
  50. package/orchestrator/skills/release-readiness.md +16 -16
  51. package/orchestrator/skills/responsive-design.md +16 -16
  52. package/orchestrator/skills/secrets-management.md +16 -16
  53. package/orchestrator/skills/secure-file-upload.md +16 -16
  54. package/orchestrator/skills/security-review.md +26 -26
  55. package/orchestrator/skills/semantic-versioning.md +17 -17
  56. package/orchestrator/skills/seo-on-page.md +16 -16
  57. package/orchestrator/skills/seo-structured-data.md +16 -16
  58. package/orchestrator/skills/seo-technical-audit.md +16 -16
  59. package/orchestrator/skills/sql-query-review.md +16 -16
  60. package/orchestrator/skills/supply-chain-security.md +16 -16
  61. package/orchestrator/skills/test-strategy.md +16 -16
  62. package/orchestrator/skills/threat-modeling.md +16 -16
  63. package/orchestrator/skills/unit-testing.md +16 -16
  64. package/orchestrator/skills/write-tests.md +28 -28
  65. package/orchestrator/src/cli-registry.js +616 -616
  66. package/orchestrator/src/cli.js +131 -131
  67. package/orchestrator/src/doctor.js +64 -64
  68. package/orchestrator/src/engine.js +162 -31
  69. package/orchestrator/src/server.js +766 -766
  70. package/orchestrator/src/skill-registry.js +272 -272
  71. package/orchestrator/src/store.js +399 -399
  72. package/orchestrator/web/OrbitControls.js +1417 -1417
  73. package/orchestrator/web/flow.html +740 -740
  74. package/orchestrator/web/index.html +587 -557
  75. package/orchestrator/web/jsm/postprocessing/EffectComposer.js +231 -231
  76. package/orchestrator/web/jsm/postprocessing/MaskPass.js +104 -104
  77. package/orchestrator/web/jsm/postprocessing/Pass.js +95 -95
  78. package/orchestrator/web/jsm/postprocessing/RenderPass.js +99 -99
  79. package/orchestrator/web/jsm/postprocessing/ShaderPass.js +77 -77
  80. package/orchestrator/web/jsm/postprocessing/UnrealBloomPass.js +415 -415
  81. package/orchestrator/web/jsm/shaders/CopyShader.js +45 -45
  82. package/orchestrator/web/jsm/shaders/LuminosityHighPassShader.js +66 -66
  83. package/orchestrator/web/three.module.min.js +6 -6
  84. package/package.json +1 -1
@@ -1,741 +1,741 @@
1
- <!doctype html>
2
- <html lang="tr">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <title>Ekip Akışı · CrewCtl</title>
7
- <link rel="icon" href="/favicon.ico" sizes="any">
8
- <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
9
- <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
1
+ <!doctype html>
2
+ <html lang="tr">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>Ekip Akışı · CrewCtl</title>
7
+ <link rel="icon" href="/favicon.ico" sizes="any">
8
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
9
+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
10
10
  <link rel="apple-touch-icon" href="/apple-touch-icon.png">
11
- <link rel="manifest" href="/site.webmanifest">
12
- <style>
13
- *{box-sizing:border-box}
14
- :root{--bg:#090c12;--panel:#111826;--panel2:#182234;--line:#243149;--tx:#e9eef7;--dim:#8494ab;--acc:#5e8bff;--ok:#2fd08a;--warn:#eab24d;--err:#f2565f;--purple:#a98bff;
15
- --headerbg:#0a0f1acc;--term:#05080f;--nodebg:#0b1220;--shadow:0 14px 40px #0008;
16
- --core:radial-gradient(circle at 50% 34%,#2a2060,#141031);--coretx:#efeaff;
17
- --appbg:radial-gradient(1200px 620px at 50% -14%,#5e8bff16,transparent),radial-gradient(1000px 520px at 90% -6%,#a98bff16,transparent),radial-gradient(900px 500px at 5% 10%,#2fd08a0e,transparent),var(--bg)}
18
- [data-theme=light]{--bg:#edf1f8;--panel:#ffffff;--panel2:#f3f6fc;--line:#dce4ef;--tx:#131a27;--dim:#5a6779;--acc:#3a63f0;--ok:#0ca678;--warn:#a76a10;--err:#dc434e;--purple:#7350f0;
19
- --headerbg:#ffffffe6;--term:#0d1420;--nodebg:#f7f9fd;--shadow:0 14px 36px #7a90b820;
20
- --core:radial-gradient(circle at 50% 34%,#f1ecff,#e2dbff);--coretx:#3a2b73;
21
- --appbg:radial-gradient(1200px 620px at 50% -14%,#3a63f012,transparent),radial-gradient(1000px 520px at 90% -6%,#7350f00e,transparent),var(--bg)}
22
- body{margin:0;background:var(--appbg);background-attachment:fixed;color:var(--tx);font:14px/1.5 Inter,Segoe UI,system-ui,sans-serif;transition:background .3s,color .3s}
23
- button{cursor:pointer;color:var(--tx);background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:7px 11px;font:inherit;transition:border-color .18s,transform .1s,box-shadow .18s}
24
- button:hover{border-color:var(--acc);box-shadow:0 4px 14px color-mix(in srgb,var(--acc) 16%,transparent)}button:active{transform:translateY(1px)}
25
- a.btn{text-decoration:none;display:inline-block}
26
- /* Header */
27
- header{height:58px;display:flex;align-items:center;gap:12px;padding:0 16px;background:var(--headerbg);backdrop-filter:blur(10px);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:20}
28
- h1{font-size:15px;margin:0;letter-spacing:.2px}.brand{color:var(--acc);font-weight:800}.sep{color:var(--dim)}.grow{flex:1}
29
- .pill{border:1px solid var(--line);background:var(--panel2);border-radius:999px;padding:5px 11px;color:var(--dim);font-size:12px;display:inline-flex;align-items:center;gap:6px}
30
- .dot{width:8px;height:8px;border-radius:50%;background:var(--dim);box-shadow:0 0 0 0 transparent}
31
- .dot.on{background:var(--ok);box-shadow:0 0 10px var(--ok);animation:beat 1.6s ease-in-out infinite}
32
- @keyframes beat{50%{box-shadow:0 0 3px var(--ok)}}
33
- /* KPI şeridi */
34
- .topbar{padding:14px 14px 0}
35
- .banner{background:linear-gradient(120deg,var(--panel),var(--panel2));border:1px solid var(--line);border-radius:16px;padding:14px 18px;box-shadow:var(--shadow);display:flex;gap:16px;align-items:center;flex-wrap:wrap}
36
- .banner .kick{font-size:10px;letter-spacing:1.4px;text-transform:uppercase;color:var(--acc);font-weight:800}
37
- .banner .t{font-weight:800;font-size:18px;line-height:1.25}.banner .s{color:var(--dim);font-size:12px;margin-top:2px}
38
- .tiles{display:grid;grid-template-columns:repeat(6,minmax(92px,1fr));gap:10px;margin-left:auto}
39
- .tile{background:#ffffff06;border:1px solid var(--line);border-radius:12px;padding:9px 12px;min-width:92px}
40
- .tile .k{font-size:9.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--dim)}
41
- .tile .v{font-size:21px;font-weight:800;margin-top:1px;font-variant-numeric:tabular-nums;line-height:1.1}
42
- .tile.op .v{font-size:15px}
43
- /* Ana ızgara */
44
- .wrap{display:grid;grid-template-columns:272px minmax(420px,1fr) 320px;gap:14px;padding:14px;align-items:start}
45
- .card{background:var(--panel);border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow)}
46
- .card-h{display:flex;align-items:center;gap:8px;padding:12px 14px;border-bottom:1px solid var(--line)}
47
- .card-h h2{font-size:11px;letter-spacing:.8px;text-transform:uppercase;color:var(--dim);margin:0}
48
- .card-h .count{margin-left:auto;font-size:11px;color:var(--dim);background:var(--panel2);border:1px solid var(--line);border-radius:999px;padding:2px 8px}
49
- .card-b{padding:12px 14px}
50
- /* Ajan filosu (yan liste) */
51
- .roster{display:flex;flex-direction:column;gap:12px;max-height:calc(100vh - 210px);overflow:auto;padding:6px 4px 4px}
52
- .agent-card{--cli:var(--acc);position:relative;display:flex;gap:12px;align-items:center;padding:11px 12px;border:1px solid color-mix(in srgb,var(--cli) 24%,var(--line));border-radius:14px;background:linear-gradient(150deg,color-mix(in srgb,var(--cli) 9%,var(--nodebg)),var(--nodebg) 62%);box-shadow:0 10px 22px #0004;cursor:pointer;transition:border-color .25s,box-shadow .25s,transform .15s}
53
- .agent-card:hover{border-color:color-mix(in srgb,var(--cli) 46%,var(--line));box-shadow:0 16px 34px #0006;transform:translateY(-1px)}
54
- .agent-card.selected{border-color:var(--cli);box-shadow:0 0 0 1px var(--cli),0 16px 34px #0006}
55
- .agent-card:before{content:'';position:absolute;left:0;top:14%;bottom:14%;width:3px;border-radius:0 3px 3px 0;background:linear-gradient(180deg,transparent,var(--cli),transparent);opacity:.55}
56
- .agent-card.working{border-color:color-mix(in srgb,var(--ok) 48%,var(--line))}.agent-card.working:before{background:var(--ok);opacity:.85}
57
- .agent-card.failed{border-color:color-mix(in srgb,var(--err) 55%,var(--line))}.agent-card.failed:before{background:var(--err)}
58
- .avatar{position:relative;width:46px;height:46px;flex:none;border-radius:50%;background:radial-gradient(circle at 36% 30%,#ffffff,#ffffffde 4%,#ffffff00 22%),radial-gradient(circle at 50% 128%,color-mix(in srgb,var(--cli,#8b98b0) 55%,#ffffff),transparent 42%),radial-gradient(circle at 42% 36%,color-mix(in srgb,var(--cli,#8b98b0) 78%,#ffffff),var(--cli,#8b98b0) 40%,color-mix(in srgb,var(--cli,#8b98b0) 40%,#000) 100%);box-shadow:inset -6px -8px 16px #000000ad,inset 6px 7px 13px #ffffff45,0 0 0 1px #0006,0 12px 16px -6px #000c}
59
- .avatar .led{position:absolute;z-index:2;right:-2px;bottom:-1px;width:12px;height:12px;border-radius:50%;background:var(--dim);border:3px solid var(--panel);box-shadow:0 0 0 1px var(--line)}
60
- .agent-card.working .led{background:var(--ok);box-shadow:0 0 5px color-mix(in srgb,var(--ok) 60%,transparent)}.agent-card.done .led{background:var(--acc)}.agent-card.failed .led{background:var(--err)}
61
- .ai{min-width:0;flex:1}
62
- .ai .n{font-weight:750;font-size:13px;letter-spacing:-.1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
63
- .ai .m{display:flex;gap:5px;align-items:center;margin-top:2px;flex-wrap:wrap}
64
- .chip{font-size:9.5px;border:1px solid var(--line);border-radius:999px;padding:1px 6px;color:var(--dim)}
65
- .chip.cli{color:#fff;font-weight:700;border:none;text-shadow:0 1px 1px #0006}
66
- .ai .stt{font-size:10.5px;color:var(--dim);margin-top:3px;display:flex;align-items:center;justify-content:space-between;gap:6px}
67
- .run-count{font:9.5px ui-monospace,Consolas,monospace;color:var(--dim);border:1px solid var(--line);border-radius:999px;padding:1px 6px;white-space:nowrap}
68
- .agent-card.working .stt{color:var(--ok)}
69
-
70
- /* ===== 3B Sahne ===== */
71
- .stage{position:relative;background:radial-gradient(120% 90% at 50% 8%,#26375c,#141f37 55%,#0c1426);border:1px solid var(--line);border-radius:16px;min-height:600px;height:calc(100vh - 210px);overflow:hidden;box-shadow:var(--shadow)}
72
- [data-theme=light] .stage{background:radial-gradient(120% 90% at 50% 8%,#f2f5fd,#e3e9f7 60%,#d7deef)}
73
- #scene{position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none;cursor:grab}
74
- #scene:active{cursor:grabbing}
75
- .hud{position:absolute;z-index:3;pointer-events:none}
76
- .hud-tl{top:14px;left:14px;display:flex;flex-direction:column;gap:7px;max-width:60%}
77
- .hud-op{display:inline-flex;align-items:center;gap:8px;background:#0b1220b0;border:1px solid var(--line);border-radius:12px;padding:7px 12px;backdrop-filter:blur(8px);width:fit-content}
78
- .hud-op .hud-lbl{font-size:9px;letter-spacing:2px;color:var(--purple);font-weight:800}
79
- .hud-op #opName{font-size:15px;font-weight:800;color:var(--coretx)}
80
- .op-phase{width:fit-content;font-size:12px;color:var(--dim);background:#0b1220b0;border:1px solid var(--line);border-radius:999px;padding:5px 13px;backdrop-filter:blur(8px)}
81
- .op-phase b{color:var(--purple)}
82
- .hud-tr{top:14px;right:14px;display:flex;gap:8px;pointer-events:auto}
83
- .mini{background:#0b1220b0;border:1px solid var(--line);border-radius:10px;padding:6px 10px;font-size:12px;color:var(--tx);backdrop-filter:blur(8px)}
84
- .scene-legend{position:absolute;z-index:3;left:14px;bottom:12px;display:flex;gap:12px;flex-wrap:wrap;background:#0b1220a0;border:1px solid var(--line);border-radius:12px;padding:7px 12px;backdrop-filter:blur(8px);pointer-events:none}
85
- .scene-legend span{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--dim)}
86
- .lg-dot{width:8px;height:8px;border-radius:50%;box-shadow:0 0 6px currentColor}
87
- .scene-hint{position:absolute;z-index:3;right:14px;bottom:12px;font-size:10.5px;color:var(--dim);background:#0b1220a0;border:1px solid var(--line);border-radius:999px;padding:5px 12px;backdrop-filter:blur(8px);pointer-events:none}
88
- /* 3B sahne her iki temada da koyudur; HUD çipleri koyu arka planlı olduğundan light
89
- temada metinler görünmez oluyordu. Light temada yazı/kenarları sabit açık renge çek. */
90
- [data-theme=light] .mini{color:#eef2fa}
91
- [data-theme=light] .op-phase,[data-theme=light] .scene-legend span,[data-theme=light] .scene-hint,[data-theme=light] .scene-fallback{color:#c3d0e6}
92
- [data-theme=light] .hud-op #opName{color:#efeaff}
93
- [data-theme=light] .op-phase b,[data-theme=light] .hud-op .hud-lbl{color:#c3b0ff}
94
- [data-theme=light] .scene-fallback h3{color:#f2f6ff}
95
- [data-theme=light] .hud-op,[data-theme=light] .op-phase,[data-theme=light] .mini,[data-theme=light] .scene-legend,[data-theme=light] .scene-hint{border-color:#2c3a55}
96
- .scene-fallback{position:absolute;inset:0;z-index:4;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:8px;padding:32px;color:var(--dim)}
97
- .scene-fallback .sf-ic{font-size:40px}.scene-fallback h3{margin:0;color:var(--tx);font-size:16px}.scene-fallback p{margin:0;max-width:420px;font-size:12.5px;line-height:1.6}
98
- /* 3B yükleme animasyonu */
99
- .scene-loader{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;background:radial-gradient(120% 90% at 50% 40%,#1a2745,#0b1224);transition:opacity .45s ease}
100
- .scene-loader.gone{opacity:0;pointer-events:none}
101
- .orbit-spinner{position:relative;width:64px;height:64px}
102
- .orbit-spinner .sun{position:absolute;inset:0;margin:auto;width:16px;height:16px;border-radius:50%;background:radial-gradient(circle at 40% 35%,#e6dcff,#a98bff);box-shadow:0 0 16px #a98bff}
103
- .orbit-spinner .ring{position:absolute;inset:0;border-radius:50%;border:1.5px solid #2a3d63}
104
- .orbit-spinner .ring:nth-child(2){inset:8px}.orbit-spinner .ring:nth-child(3){inset:18px}
105
- .orbit-spinner .planet{position:absolute;top:-4px;left:calc(50% - 4px);width:8px;height:8px;border-radius:50%;background:#5e8bff;box-shadow:0 0 8px #5e8bff}
106
- .orbit-spinner .o1{animation:orbitspin 2.4s linear infinite}.orbit-spinner .o2{animation:orbitspin 3.6s linear infinite reverse}.orbit-spinner .o3{animation:orbitspin 5s linear infinite}
107
- .orbit-spinner .o2 .planet{background:#2fd08a;box-shadow:0 0 8px #2fd08a}.orbit-spinner .o3 .planet{background:#d97757;box-shadow:0 0 8px #d97757}
108
- @keyframes orbitspin{to{transform:rotate(360deg)}}
109
- .scene-loader .lt{font-size:12px;letter-spacing:.4px;color:var(--dim)}
110
- @media(prefers-reduced-motion:reduce){.orbit-spinner .o1,.orbit-spinner .o2,.orbit-spinner .o3{animation:none}}
111
- /* Yardım (?) modalı */
112
- .help-overlay{position:fixed;inset:0;z-index:50;background:#050912cc;backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;padding:20px;animation:panelIn .2s ease}
113
- .help-overlay[hidden]{display:none}
114
- .help-card{background:var(--panel);border:1px solid var(--line);border-radius:18px;box-shadow:0 30px 80px #000a;max-width:560px;width:100%;max-height:86vh;overflow:auto}
115
- .help-card .hc-head{display:flex;align-items:center;gap:10px;padding:16px 18px;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--panel)}
116
- .help-card .hc-head .ic{font-size:20px}.help-card .hc-head h2{margin:0;font-size:16px}.help-card .hc-head .brand{color:var(--acc)}
117
- .help-card .hc-close{margin-left:auto;width:30px;height:30px;padding:0;border-radius:8px;font-size:15px}
118
- .help-card .hc-body{padding:16px 18px;font-size:13px;line-height:1.65;color:var(--tx)}
119
- .help-card .hc-body p{margin:0 0 12px}
120
- .hc-row{display:flex;gap:11px;align-items:flex-start;padding:10px 0;border-top:1px solid var(--line)}
121
- .hc-row .hc-ic{flex:none;width:30px;height:30px;border-radius:9px;display:grid;place-items:center;font-size:15px;background:var(--panel2);border:1px solid var(--line)}
122
- .hc-row .hc-t{font-weight:750;font-size:12.5px}.hc-row .hc-d{color:var(--dim);font-size:12px;margin-top:2px}
123
- .hc-keys{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
124
- .hc-keys kbd{font:11px ui-monospace,Consolas,monospace;background:var(--panel2);border:1px solid var(--line);border-bottom-width:2px;border-radius:6px;padding:2px 7px;color:var(--tx)}
125
- #helpBtn{width:32px;height:32px;padding:0;border-radius:50%;display:inline-grid;place-items:center;font-weight:800;font-size:15px;line-height:1;color:var(--dim)}
126
- #helpBtn:hover{color:var(--acc);border-color:var(--acc)}
127
- .hc-row{align-items:baseline}.hc-num{flex:none;width:20px;color:var(--acc);font-weight:800;font-size:12.5px}
128
- /* Detay paneli (tıklamada) */
129
- .agent-panel{position:absolute;z-index:5;top:14px;right:14px;width:min(340px,64%);max-height:calc(100% - 28px);display:flex;flex-direction:column;background:#0b1220ee;border:1px solid var(--line);border-radius:16px;box-shadow:0 24px 60px #000a;backdrop-filter:blur(14px);overflow:hidden;animation:panelIn .22s cubic-bezier(.2,.8,.2,1)}
130
- @keyframes panelIn{from{opacity:0;transform:translateX(12px)}to{opacity:1;transform:none}}
131
- .ap-head{display:flex;align-items:center;gap:11px;padding:13px 14px;border-bottom:1px solid var(--line)}
132
- .ap-av{width:42px;height:42px;flex:none;border-radius:50%;background:radial-gradient(circle at 36% 30%,#ffffff,#ffffffde 4%,#ffffff00 22%),radial-gradient(circle at 42% 36%,color-mix(in srgb,var(--cli,#8b98b0) 78%,#ffffff),var(--cli,#8b98b0) 40%,color-mix(in srgb,var(--cli,#8b98b0) 40%,#000) 100%);box-shadow:inset -5px -6px 12px #000000ad,inset 5px 6px 10px #ffffff45,0 0 0 1px #0006}
133
- .ap-id{min-width:0;flex:1}
134
- .ap-id .name{font-weight:800;font-size:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
135
- .ap-id .sub{display:flex;gap:5px;margin-top:3px;flex-wrap:wrap}
136
- .ap-close{flex:none;width:28px;height:28px;padding:0;border-radius:8px;font-size:15px;line-height:1}
137
- .ap-body{padding:12px 14px;overflow:auto}
138
- .ap-status{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:700;padding:8px 11px;border-radius:10px;border:1px solid var(--line);background:#ffffff06;margin-bottom:11px}
139
- .ap-status .sdot{width:9px;height:9px;border-radius:50%;background:var(--dim);box-shadow:0 0 8px currentColor}
140
- .ap-status.working{color:var(--ok)}.ap-status.working .sdot{background:var(--ok)}
141
- .ap-status.done{color:var(--acc)}.ap-status.done .sdot{background:var(--acc)}
142
- .ap-status.failed{color:var(--err)}.ap-status.failed .sdot{background:var(--err)}
143
- .ap-sec{font-size:9.5px;letter-spacing:1px;text-transform:uppercase;color:var(--dim);font-weight:800;margin:12px 0 6px}
144
- .ap-task{font-size:12px;line-height:1.5;color:var(--tx);background:#ffffff06;border:1px solid var(--line);border-radius:10px;padding:9px 11px;white-space:pre-wrap;word-break:break-word}
145
- .ap-tail{margin:0;background:var(--term);color:#9fb0c6;font:11px/1.5 ui-monospace,Consolas,monospace;border:1px solid var(--line);border-radius:10px;padding:9px 11px;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-word}
146
- .ap-tail:empty::after{content:'Henüz canlı çıktı yok.';color:var(--dim)}
147
- .ap-meta{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:11px;font:10px ui-monospace,Consolas,monospace;color:var(--dim)}
148
- .ap-empty{color:var(--dim);font-size:12px;padding:6px 0}
149
- /* Zaman çizelgesi */
150
- .timeline{display:flex;flex-direction:column;gap:9px;max-height:calc(100vh - 210px);overflow:auto;padding-right:2px}
151
- .tl{display:flex;gap:9px}
152
- .tl .num{flex:none;width:24px;height:24px;border-radius:8px;display:grid;place-items:center;font:11px ui-monospace;font-weight:700;background:var(--panel2);border:1px solid var(--line);color:var(--dim)}
153
- .tl.delegation .num{border-color:var(--purple);color:#e6d8ff}.tl.result .num{border-color:var(--ok);color:var(--ok)}.tl.failure .num,.tl.blocked .num{border-color:var(--err);color:var(--err)}.tl.system .num{border-color:var(--acc);color:var(--acc)}
154
- .tl .body{flex:1;min-width:0;border:1px solid var(--line);background:var(--panel2);border-radius:4px 11px 11px 4px;border-left:3px solid var(--purple);padding:7px 10px}
155
- .tl.result .body{border-left-color:var(--ok)}.tl.failure .body,.tl.blocked .body{border-left-color:var(--err)}.tl.system .body{border-left-color:var(--acc)}
156
- .tl .route{font-size:11px;font-weight:700;color:var(--purple);display:flex;align-items:center;gap:5px}.tl.result .route{color:var(--ok)}.tl.failure .route,.tl.blocked .route{color:var(--err)}.tl.system .route{color:var(--acc)}
157
- .tl .txt{font-size:11.5px;color:var(--tx);margin-top:3px;opacity:.9;white-space:pre-wrap;word-break:break-word;display:-webkit-box;-webkit-line-clamp:3;line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
158
- .tl time{color:var(--dim);font:10px ui-monospace;margin-left:auto}
159
- .tl-empty{color:var(--dim);font-size:12px;border:1px dashed var(--line);border-radius:11px;padding:18px 14px;text-align:center}
160
- @media(max-width:1180px){.wrap{grid-template-columns:1fr}.tiles{grid-template-columns:repeat(3,1fr)}.roster,.timeline{max-height:none}.stage{height:70vh}}
161
- </style>
162
- <link rel="stylesheet" href="app.css">
163
- <script src="app.js" defer></script>
164
- </head>
165
- <body>
166
- <header>
167
- <h1><span class="brand">CrewCtl</span> <span class="sep">·</span> Orkestrasyon Merkezi</h1>
168
- <span class="pill"><span id="dot" class="dot"></span><span id="engineState">durdu</span></span>
169
- <span id="modePill" class="pill">boşta</span>
170
- <div class="grow"></div>
171
- <button id="helpBtn" title="Bu sistem nedir?"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9.2"/><path d="M12 11v5M12 7.6h.01"/></svg></button>
172
- <button id="themeBtn" onclick="toggleTheme()" title="Açık / koyu tema"><svg class="i i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><svg class="i i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg></button>
173
- <a class="btn" href="index.html"><button>← Komuta Merkezi</button></a>
174
- </header>
175
-
176
- <div class="topbar">
177
- <div class="banner">
178
- <div>
179
- <div class="kick" id="mKick">Görev bekleniyor</div>
180
- <div class="t" id="mTitle">Takım görevi bekleniyor</div>
181
- <div class="s" id="mSub">Operatör bir hedef aldığında ekibini burada canlı olarak kuracak.</div>
182
- </div>
183
- <div class="tiles">
184
- <div class="tile op"><div class="k">Operatör</div><div class="v" id="kOp">—</div></div>
185
- <div class="tile"><div class="k">Tur</div><div class="v" id="kRound">0</div></div>
186
- <div class="tile"><div class="k">Delegasyon</div><div class="v" id="kDeleg">0</div></div>
187
- <div class="tile"><div class="k">Aktif ajan</div><div class="v" id="kActive">0</div></div>
188
- <div class="tile"><div class="k">Dosya</div><div class="v" id="kFiles">0</div></div>
189
- <div class="tile"><div class="k">Çağrı</div><div class="v" id="kCalls">0</div></div>
190
- </div>
191
- </div>
192
- </div>
193
-
194
- <div class="wrap">
195
- <aside class="card">
196
- <div class="card-h"><h2>Ajan Filosu</h2><span class="count" id="rosterCount">0 ajan</span></div>
197
- <div class="card-b"><div class="roster" id="roster"><div class="tl-empty">Ajanlar yükleniyor…</div></div>
198
- <a class="btn" href="index.html" style="display:block;margin-top:10px"><button style="width:100%">⚙ Ajanları yönet</button></a>
199
- </div>
200
- </aside>
201
-
202
- <section class="stage" id="stage">
203
- <canvas id="scene"></canvas>
204
- <div class="scene-loader" id="sceneLoader">
205
- <div class="orbit-spinner"><span class="sun"></span><span class="ring o1"><span class="planet"></span></span><span class="ring o2"><span class="planet"></span></span><span class="ring o3"><span class="planet"></span></span></div>
206
- <div class="lt">3B ekip evreni hazırlanıyor…</div>
207
- </div>
208
- <div class="hud hud-tl">
209
- <div class="hud-op"><span class="hud-lbl">OPERATÖR</span><span id="opName">—</span></div>
210
- <div class="op-phase" id="opState">bir görev bekleniyor</div>
211
- </div>
212
- <div class="hud hud-tr">
213
- <button id="autoRotBtn" class="mini" title="Otomatik döndürme">⏸ Döndür</button>
214
- <button id="camResetBtn" class="mini" title="Kamerayı sıfırla">⟲ Kamera</button>
215
- </div>
216
- <div class="scene-legend">
217
- <span><i class="lg-dot" style="color:#2fd08a;background:#2fd08a"></i>Çalışıyor</span>
218
- <span><i class="lg-dot" style="color:#5e8bff;background:#5e8bff"></i>Tamamlandı</span>
219
- <span><i class="lg-dot" style="color:#f2565f;background:#f2565f"></i>Sorun</span>
220
- <span><i class="lg-dot" style="color:#8494ab;background:#8494ab"></i>Beklemede</span>
221
- </div>
222
- <div class="scene-hint">Sürükle → döndür · tekerlek → yakınlaş · ajana tıkla → detay</div>
223
- <div class="agent-panel" id="agentPanel" hidden></div>
224
- </section>
225
-
226
- <aside class="card">
227
- <div class="card-h"><h2>Zaman Çizelgesi</h2><span class="count" id="tlCount">sıralı</span></div>
228
- <div class="card-b"><div class="timeline" id="timeline"><div class="tl-empty">Bir görev başlayınca operatörün her adımı sırayla burada listelenir.</div></div></div>
229
- </aside>
230
- </div>
231
-
232
- <div class="help-overlay" id="helpModal" hidden>
233
- <div class="help-card">
234
- <div class="hc-head"><span class="ic">🛰️</span><h2><span class="brand">CrewCtl</span> Ekip Akışı nedir?</h2><button class="hc-close" id="helpClose" title="Kapat">✕</button></div>
235
- <div class="hc-body">
236
- <p>CrewCtl, kurulu CLI kodlama ajanlarınızı (Codex, Claude, Gemini, OpenCode) <b>tek bir operatör‑liderliğindeki takım</b> gibi çalıştırır. Bu sayfa o takımın canlı çalışmasını 3B bir sahnede gösterir.</p>
237
- <div class="hc-row"><div><div class="hc-t">Operatör</div><div class="hc-d">Merkezdeki çekirdek. Hedefi analiz eder, işi alt görevlere böler, doğru uzmana delege eder ve sonuçları değerlendirir.</div></div></div>
238
- <div class="hc-row"><div><div class="hc-t">Uzman ajanlar</div><div class="hc-d">Operatörün çevresindeki her düğüm bir CLI uzmanıdır; rengi hangi CLI olduğunu gösterir. Birine tıklayınca o an ne yaptığını, canlı çıktısını ve durumunu görürsünüz.</div></div></div>
239
- <div class="hc-row"><div><div class="hc-t">Delegasyon akışı</div><div class="hc-d">Operatörden uzmana giden hüzme bir görev delegasyonu, geri dönen hüzme ise teslimattır.</div></div></div>
240
- <div class="hc-row"><div><div class="hc-t">Durum renkleri</div><div class="hc-d"><b style="color:#2fd08a">Yeşil</b> çalışıyor · <b style="color:#5e8bff">Mavi</b> tamamlandı · <b style="color:#f2565f">Kırmızı</b> sorun · <b style="color:#8494ab">Gri</b> beklemede.</div></div></div>
241
- <div class="hc-row"><div><div class="hc-t">Kontroller</div><div class="hc-d">Sahneyi döndürüp yakınlaşabilirsiniz. Soldaki filo ve sağdaki zaman çizelgesi 3B sahne ile bağlıdır — birine tıklamak ilgili gezegeni seçer.</div>
242
- <div class="hc-keys"><kbd>Sürükle</kbd> döndür <kbd>Tekerlek</kbd> yakınlaş <kbd>Tıkla</kbd> detay</div></div></div>
243
- <p style="margin:14px 0 0;color:var(--dim);font-size:12px">Bir görev başlatmak için <a href="index.html" style="color:var(--acc)">Komuta Merkezi</a>'ne dönüp <b>▶ Başlat</b>'a basın.</p>
244
- </div>
245
- </div>
246
- </div>
247
-
248
- <script type="importmap">
249
- {"imports":{"three":"./three.module.min.js"}}
250
- </script>
251
- <script type="module">
252
- import * as THREE from 'three';
253
- import { OrbitControls } from './OrbitControls.js';
254
- import { EffectComposer } from './jsm/postprocessing/EffectComposer.js';
255
- import { RenderPass } from './jsm/postprocessing/RenderPass.js';
256
- import { UnrealBloomPass } from './jsm/postprocessing/UnrealBloomPass.js';
257
-
258
- const reduceMotion=matchMedia('(prefers-reduced-motion:reduce)').matches;
259
- const $=s=>document.querySelector(s);
260
- const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
261
- const time=at=>new Date(at||Date.now()).toLocaleTimeString();
262
- const isOpStage=s=>String(s||'').startsWith('operator');
263
- const CLI_META={codex:{label:'Codex',c:'#10a37f'},claude:{label:'Claude',c:'#d97757'},gemini:{label:'Gemini',c:'#4285f4'},opencode:{label:'OpenCode',c:'#0ea5e9'},custom:{label:'CLI',c:'#8b98b0'}};
264
- function adapterOf(a){const b=String(a?.adapter||a?.cmd||a||'').toLowerCase();return ['codex','claude','gemini','opencode'].find(x=>b.includes(x))||'custom'}
265
- const cliLabel=id=>(CLI_META[id]||CLI_META.custom).label;
266
- const cliColor=id=>(CLI_META[id]||CLI_META.custom).c;
267
- function cssId(s){return String(s).replace(/[^a-z0-9_-]/gi,'_')}
268
- function applyTheme(t){document.documentElement.setAttribute('data-theme',t);try{localStorage.setItem('cliTheme',t)}catch{};Scene.setTheme?.(t)}
269
- window.toggleTheme=function(){applyTheme(document.documentElement.getAttribute('data-theme')==='light'?'dark':'light')}
270
-
271
- /* ============================================================
272
- 3B SAHNE — gerçek WebGL/Three.js ajan görselleştirmesi
273
- Her ajan bağımsız bir 3B model. Operatör merkezde çekirdek.
274
- Tıklanabilir (raycast), durum renkleriyle canlı güncellenir.
275
- ============================================================ */
276
- const Scene=(()=>{
277
- let renderer,composer,bloom,scene,camera,controls,raycaster,pointer,clock,rafId,ok=false;
278
- let operator,opGlow,opRings=[],ground,particles,fog,orbitPlane;
279
- const orbitRings=[];
280
- const PLANET_SCALE=.66; // gezegenler operatöre göre küçük dursun
281
- let selectionRing,hovered=null,selected=null,onSelect=null;
282
- const agents=new Map(); // name -> agent obj
283
- const pickables=[]; // {mesh,name}
284
- const beams=new Map(); // name -> beam obj
285
- const pulses=[]; // aktif akış darbeleri
286
- const ripples=[]; // varış/çarpma dalgaları
287
- const tmp=new THREE.Vector3();
288
- const HOME={target:new THREE.Vector3(0,1.4,0),dist:20};
289
- const focus={target:new THREE.Vector3(0,1.4,0),dist:20,active:false};
290
- const STAT={
291
- working:{col:0x2fd08a,emis:1.25,ring:2.4,bob:.16},
292
- done:{col:0x5e8bff,emis:.8,ring:.5,bob:.06},
293
- failed:{col:0xf2565f,emis:.95,ring:.2,bob:.03},
294
- idle:{col:0x8494ab,emis:.5,ring:.7,bob:.09}
295
- };
296
- // paylaşılan geometriler (bir kez)
297
- let G={},glowTex;
298
-
299
- function radialTexture(){const c=document.createElement('canvas');c.width=c.height=128;const x=c.getContext('2d');const g=x.createRadialGradient(64,64,0,64,64,64);g.addColorStop(0,'#ffffff');g.addColorStop(.25,'#ffffffcc');g.addColorStop(1,'#ffffff00');x.fillStyle=g;x.fillRect(0,0,128,128);const t=new THREE.CanvasTexture(c);return t}
300
- function labelSprite(text,hex){
301
- const pad=26,fs=44,f=`800 ${fs}px Inter,Segoe UI,sans-serif`;
302
- const m=document.createElement('canvas').getContext('2d');m.font=f;const w=Math.ceil(m.measureText(text).width)+pad*2,h=fs+pad;
303
- const c=document.createElement('canvas');c.width=w;c.height=h;const x=c.getContext('2d');
304
- x.font=f;x.textBaseline='middle';x.textAlign='center';
305
- const r=18;x.fillStyle='rgba(9,14,26,.82)';x.strokeStyle=hex;x.lineWidth=3;
306
- x.beginPath();x.moveTo(r,3);x.arcTo(w-3,3,w-3,h-3,r);x.arcTo(w-3,h-3,3,h-3,r);x.arcTo(3,h-3,3,3,r);x.arcTo(3,3,w-3,3,r);x.closePath();x.fill();x.stroke();
307
- x.fillStyle='#fff';x.fillText(text,w/2,h/2+1);
308
- const t=new THREE.CanvasTexture(c);t.anisotropy=4;
309
- const sp=new THREE.Sprite(new THREE.SpriteMaterial({map:t,transparent:true,depthTest:false}));
310
- sp.scale.set(w/h*.62,.62,1);sp.renderOrder=10;return sp;
311
- }
312
-
313
- // GEZEGEN — operatör (güneş) ailesinden ama farklı: simetrik küre gövde,
314
- // kendi eğik halkası (Satürn gibi) ve telkafes kabuk; renk CLI'ye göre, durumla parlar.
315
- function buildAgentModel(hex){
316
- const col=new THREE.Color(hex),g=new THREE.Group();
317
- // iç çekirdek (koyu, derinlik hissi)
318
- const body=new THREE.Mesh(G.coreInner,new THREE.MeshStandardMaterial({color:0x16233b,metalness:.7,roughness:.4}));g.add(body);
319
- // gezegen yüzeyi — durum rengiyle parlayan simetrik küre (ana gövde)
320
- const visor=new THREE.Mesh(G.planet,new THREE.MeshStandardMaterial({color:col,emissive:col,emissiveIntensity:1.3,metalness:.35,roughness:.35,toneMapped:false}));g.add(visor);
321
- // telkafes kabuk (dönen)
322
- const shell=new THREE.LineSegments(G.shell,new THREE.LineBasicMaterial({color:col,transparent:true,opacity:.6}));g.add(shell);
323
- // Satürn benzeri eğik gezegen halkası — ana ayırt edici öğe
324
- const tip=new THREE.Mesh(G.ring,new THREE.MeshBasicMaterial({color:col,transparent:true,opacity:.85,side:THREE.DoubleSide,toneMapped:false,blending:THREE.AdditiveBlending,depthWrite:false}));
325
- tip.rotation.set(Math.PI*.5,0,Math.PI*.16);g.add(tip);
326
- // ikinci ince halka (çapraz eksende — jiroskop hissi)
327
- const halo=new THREE.Mesh(G.halo,new THREE.MeshBasicMaterial({color:col,transparent:true,opacity:.5,side:THREE.DoubleSide,toneMapped:false}));
328
- halo.rotation.set(Math.PI*.5,Math.PI*.5,0);g.add(halo);
329
- // yumuşak dış parıltı (atmosfer/korona hissi)
330
- const glow=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex,color:col,transparent:true,opacity:.34,blending:THREE.AdditiveBlending,depthWrite:false}));
331
- glow.scale.set(2.7,2.7,1);g.add(glow);
332
- // görünmez tıklama küresi
333
- const pick=new THREE.Mesh(G.pick,new THREE.MeshBasicMaterial({visible:false}));g.add(pick);
334
- return {g,body,shell,visor,halo,tip,glow,pick};
335
- }
336
-
337
- function buildOperator(){
338
- const grp=new THREE.Group();grp.position.y=1.4;
339
- const col=new THREE.Color(0xa98bff);
340
- const core=new THREE.Mesh(new THREE.IcosahedronGeometry(.9,1),new THREE.MeshStandardMaterial({color:0x2a2060,emissive:col,emissiveIntensity:1.1,metalness:.6,roughness:.25,flatShading:true,toneMapped:false}));
341
- grp.add(core);grp.userData.core=core;
342
- const wire=new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.IcosahedronGeometry(1.24,1)),new THREE.LineBasicMaterial({color:col,transparent:true,opacity:.5}));
343
- grp.add(wire);grp.userData.wire=wire;
344
- for(let i=0;i<3;i++){const r=new THREE.Mesh(new THREE.TorusGeometry(1.5+i*.14,.02,10,90),new THREE.MeshBasicMaterial({color:col,transparent:true,opacity:.55-i*.12,toneMapped:false}));r.rotation.x=Math.PI*(.2+i*.28);r.rotation.y=i*.6;opRings.push(r);grp.add(r)}
345
- opGlow=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex,color:col,transparent:true,opacity:.55,blending:THREE.AdditiveBlending,depthWrite:false}));
346
- opGlow.scale.set(6.2,6.2,1);grp.add(opGlow);
347
- // korona — güneş hissi için geniş, soluk dış hale
348
- const corona=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex,color:0xc3b6e8,transparent:true,opacity:.11,blending:THREE.AdditiveBlending,depthWrite:false}));
349
- corona.scale.set(12,12,1);grp.add(corona);
350
- // güneşin ışığı gezegenleri aydınlatır — daha soluk ve daha az mor (nötr)
351
- const light=new THREE.PointLight(0xc4cbe6,1.1,42,1.8);light.position.set(0,0,0);grp.add(light);grp.userData.light=light;
352
- grp.add(labelSprite('OPERATÖR','#a98bff').translateY(2.2));
353
- return grp;
354
- }
355
-
356
- function layout(){
357
- const list=[...agents.values()],n=list.length;
358
- // eski yörünge çizgilerini temizle
359
- for(const r of orbitRings){orbitPlane.remove(r);r.geometry.dispose();r.material.dispose()}orbitRings.length=0;
360
- if(!n)return;
361
- list.forEach((a,i)=>{
362
- a.orbitR=3.9+i*1.15; // operatöre orta mesafe, yörüngeler daha sık
363
- a.orbitAngle=(i/n)*Math.PI*2; // eşit dağılmış sabit konum
364
- a.orbitSpeed=0; // gezegen kendi başına dönmez
365
- a.orbitY=1.4;a.phase=i*1.3;
366
- a.g.position.set(Math.cos(a.orbitAngle)*a.orbitR,a.orbitY,Math.sin(a.orbitAngle)*a.orbitR);
367
- // yörünge çizgisi (güneşin çevresinde ince halka)
368
- const ring=new THREE.Mesh(new THREE.RingGeometry(a.orbitR-.012,a.orbitR+.012,180),new THREE.MeshBasicMaterial({color:a.color,transparent:true,opacity:.22,side:THREE.DoubleSide,toneMapped:false}));
369
- ring.rotation.x=-Math.PI/2;ring.position.y=a.orbitY;orbitPlane.add(ring);orbitRings.push(ring);
370
- });
371
- }
372
-
373
- function init(canvas){
374
- renderer=new THREE.WebGLRenderer({canvas,antialias:true});
375
- renderer.setPixelRatio(Math.min(devicePixelRatio,2));
376
- scene=new THREE.Scene();
377
- scene.background=new THREE.Color(0x0b1224); // bloom opak pipeline'da uzay zemini
378
- fog=new THREE.FogExp2(0x14203a,.013);scene.fog=fog;
379
- camera=new THREE.PerspectiveCamera(52,1,.1,200);camera.position.set(0,9.5,18);
380
- controls=new OrbitControls(camera,canvas);
381
- controls.enableDamping=true;controls.dampingFactor=.1;controls.rotateSpeed=.95;controls.zoomSpeed=1.1;controls.enablePan=false;
382
- controls.minDistance=6;controls.maxDistance=44;controls.maxPolarAngle=1.5;
383
- controls.target.copy(HOME.target);controls.autoRotate=!reduceMotion;controls.autoRotateSpeed=.5;
384
- controls.update();controls.saveState();
385
- // ışıklar — sahne belirgin biçimde aydınlık
386
- scene.add(new THREE.AmbientLight(0x6c82a6,1.35));
387
- scene.add(new THREE.HemisphereLight(0xcfe0ff,0x2a3550,1.15));
388
- const key=new THREE.DirectionalLight(0xffffff,1.75);key.position.set(6,13,9);scene.add(key);
389
- const rim=new THREE.DirectionalLight(0x8fb4ff,1.15);rim.position.set(-9,5,-7);scene.add(rim);
390
- const fill=new THREE.DirectionalLight(0xbcd0ff,.85);fill.position.set(0,4,12);scene.add(fill);
391
- glowTex=radialTexture();
392
- // Güneş sistemi — zemin yok; gezegenler güneşin çevresinde yörüngede döner.
393
- orbitPlane=new THREE.Group();scene.add(orbitPlane); // yörünge çizgileri layout()'ta eklenir
394
- // seçim halkası — seçili gezegenin çevresinde belirir (animate konumlandırır)
395
- selectionRing=new THREE.Mesh(new THREE.RingGeometry(.86,1.02,56),new THREE.MeshBasicMaterial({color:0xffffff,transparent:true,opacity:.9,side:THREE.DoubleSide,toneMapped:false}));
396
- selectionRing.rotation.x=-Math.PI/2;selectionRing.visible=false;scene.add(selectionRing);
397
- // yıldız alanı — derin uzay
398
- const pc=900,pos=new Float32Array(pc*3);
399
- for(let i=0;i<pc;i++){const r=15+Math.random()*42,th=Math.random()*Math.PI*2,ph=(Math.random()-.5)*Math.PI*.75;pos[i*3]=Math.cos(th)*Math.cos(ph)*r;pos[i*3+1]=Math.sin(ph)*r*.6+2;pos[i*3+2]=Math.sin(th)*Math.cos(ph)*r}
400
- const pg=new THREE.BufferGeometry();pg.setAttribute('position',new THREE.BufferAttribute(pos,3));
401
- particles=new THREE.Points(pg,new THREE.PointsMaterial({color:0xbcd0ff,size:.09,transparent:true,opacity:.7,depthWrite:false}));scene.add(particles);
402
- // paylaşılan geometriler
403
- G={planet:new THREE.IcosahedronGeometry(.5,4),coreInner:new THREE.IcosahedronGeometry(.36,2),
404
- shell:new THREE.EdgesGeometry(new THREE.IcosahedronGeometry(.63,1)),
405
- ring:new THREE.RingGeometry(.86,.95,72),halo:new THREE.RingGeometry(.58,.63,60),
406
- pick:new THREE.SphereGeometry(1.2,12,12),packet:new THREE.SphereGeometry(.075,10,10),arrow:new THREE.ConeGeometry(.13,.42,14)};
407
- operator=buildOperator();scene.add(operator);
408
- // Bloom — emissive güneş/gezegenler gerçekten parlar
409
- composer=new EffectComposer(renderer);
410
- composer.addPass(new RenderPass(scene,camera));
411
- bloom=new UnrealBloomPass(new THREE.Vector2(1,1),.42,.5,.5); // güç düşük, eşik yüksek → göz yormaz
412
- composer.addPass(bloom);
413
- raycaster=new THREE.Raycaster();pointer=new THREE.Vector2();clock=new THREE.Clock();
414
- canvas.addEventListener('pointermove',onPointerMove);
415
- canvas.addEventListener('pointerdown',onPointerDown);
416
- new ResizeObserver(resize).observe(canvas.parentElement);resize();
417
- ok=true;animate();
418
- }
419
-
420
- function resize(){if(!renderer)return;const el=renderer.domElement.parentElement,w=el.clientWidth,h=el.clientHeight;if(!w||!h)return;renderer.setSize(w,h,false);composer.setSize(w,h);bloom.setSize(w,h);camera.aspect=w/h;camera.updateProjectionMatrix()}
421
-
422
- // Not: G geometrileri ve glowTex paylaşımlıdır — yalnızca ajana özel materyalleri
423
- // ve benzersiz etiket dokusunu serbest bırak (paylaşımlı kaynaklara dokunma).
424
- function disposeAgent(a){scene.remove(a.g);a.g.traverse(o=>{if(o.material)(Array.isArray(o.material)?o.material:[o.material]).forEach(m=>m.dispose())});if(a.label&&a.label.material.map)a.label.material.map.dispose()}
425
- function setAgents(list){
426
- if(!scene)return;
427
- for(const name of [...beams.keys()])setBeam(name,false);
428
- for(const a of agents.values())disposeAgent(a);agents.clear();pickables.length=0;
429
- for(const it of list){
430
- const m=buildAgentModel(it.color);
431
- m.g.userData.name=it.name;m.pick.userData.name=it.name;m.g.scale.setScalar(PLANET_SCALE);
432
- const label=labelSprite(it.name,it.color);label.position.y=2.2;label.material.opacity=.0;m.g.add(label);
433
- const a={name:it.name,color:new THREE.Color(it.color),...m,label,status:'idle',tgt:STAT.idle,flare:0};
434
- scene.add(m.g);agents.set(it.name,a);pickables.push(m.pick);
435
- }
436
- layout();
437
- }
438
- function updateAgent(name,status){const a=agents.get(name);if(!a)return;a.status=status;a.tgt=STAT[status]||STAT.idle}
439
- function setBeam(name,active){
440
- if(!ok)return;
441
- if(active){if(beams.has(name))return;const a=agents.get(name);if(!a)return;
442
- const n=32,arr=new Float32Array(n*3);const geo=new THREE.BufferGeometry();geo.setAttribute('position',new THREE.BufferAttribute(arr,3));
443
- const line=new THREE.Line(geo,new THREE.LineBasicMaterial({color:a.color,transparent:true,opacity:.72,blending:THREE.AdditiveBlending,toneMapped:false,depthWrite:false}));
444
- const arrow=new THREE.Mesh(G.arrow,new THREE.MeshBasicMaterial({color:a.color,transparent:true,opacity:.92,toneMapped:false,blending:THREE.AdditiveBlending,depthWrite:false}));
445
- const packets=[];for(let i=0;i<3;i++){const packet=new THREE.Mesh(G.packet,new THREE.MeshBasicMaterial({color:a.color,transparent:true,opacity:.95,toneMapped:false,blending:THREE.AdditiveBlending,depthWrite:false}));
446
- const glow=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex,color:a.color,transparent:true,opacity:.65,blending:THREE.AdditiveBlending,depthWrite:false}));glow.scale.set(.7,.7,1);packet.add(glow);packets.push(packet);scene.add(packet)}
447
- scene.add(line);scene.add(arrow);beams.set(name,{line,arrow,packets,arr,n,curve:null});updateBeam(name);
448
- }else{const b=beams.get(name);if(b){scene.remove(b.line);scene.remove(b.arrow);for(const packet of b.packets){scene.remove(packet);packet.traverse(o=>{if(o.material)o.material.dispose()})}b.line.geometry.dispose();b.line.material.dispose();b.arrow.material.dispose();beams.delete(name)}}
449
- }
450
- function updateBeam(name){const b=beams.get(name),a=agents.get(name);if(!b||!a)return;const curve=beamCurve(a);
451
- for(let i=0;i<b.n;i++){const p=curve.getPoint(i/(b.n-1));b.arr[i*3]=p.x;b.arr[i*3+1]=p.y;b.arr[i*3+2]=p.z}
452
- b.line.geometry.attributes.position.needsUpdate=true;b.line.geometry.computeBoundingSphere();b.curve=curve;
453
- const u=.93,pos=curve.getPoint(u),tangent=curve.getTangent(u).normalize();b.arrow.position.copy(pos);b.arrow.quaternion.setFromUnitVectors(tmp.set(0,1,0),tangent)}
454
- function beamCurve(a){const start=operator.position.clone(),end=a.g.position.clone();const mid=start.clone().lerp(end,.5);mid.y+=1.4;return new THREE.QuadraticBezierCurve3(start,mid,end)}
455
- function pulse(name,dir){if(!ok)return;const a=agents.get(name);if(!a)return;const curve=beamCurve(a);
456
- const col=dir==='down'?a.color:0xa98bff;
457
- // izli enerji paketi: parlak çekirdek + hâle + arkada solan iz çizgisi
458
- const m=new THREE.Mesh(new THREE.SphereGeometry(.15,14,14),new THREE.MeshBasicMaterial({color:col,toneMapped:false}));
459
- const gl=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex,color:col,transparent:true,opacity:.9,blending:THREE.AdditiveBlending,depthWrite:false}));gl.scale.set(1.3,1.3,1);m.add(gl);
460
- const tn=14,tarr=new Float32Array(tn*3);const tg=new THREE.BufferGeometry();tg.setAttribute('position',new THREE.BufferAttribute(tarr,3));
461
- const s0=curve.getPoint(dir==='up'?1:0);for(let k=0;k<tn;k++){tarr[k*3]=s0.x;tarr[k*3+1]=s0.y;tarr[k*3+2]=s0.z}
462
- const trail=new THREE.Line(tg,new THREE.LineBasicMaterial({color:col,transparent:true,opacity:.55,blending:THREE.AdditiveBlending,toneMapped:false,depthWrite:false}));trail.frustumCulled=false;
463
- scene.add(m);scene.add(trail);pulses.push({m,trail,tarr,tn,curve,dir,t:0,col,name});
464
- }
465
- function spawnRipple(pos,col){const geo=new THREE.RingGeometry(.2,.34,40);const mesh=new THREE.Mesh(geo,new THREE.MeshBasicMaterial({color:col,transparent:true,opacity:.85,side:THREE.DoubleSide,toneMapped:false,blending:THREE.AdditiveBlending,depthWrite:false}));
466
- mesh.position.copy(pos);mesh.rotation.x=-Math.PI/2;scene.add(mesh);ripples.push({mesh,t:0})}
467
- function setOperator(label){/* operatör kimliği HUD'da gösterilir; 3B etiket sabittir */}
468
- function setOperatorPhase(active){if(operator)operator.userData.active=!!active}
469
- function reset(){for(const n of [...beams.keys()])setBeam(n,false);for(const p of pulses){scene.remove(p.m);scene.remove(p.trail)}pulses.length=0;for(const r of ripples)scene.remove(r.mesh);ripples.length=0;for(const a of agents.values())updateAgent(a.name,'idle')}
470
- // Uzay her iki temada da koyu kalır (light temada beyaza yıkanıp görünmez olmasın).
471
- // Işık temada bir tık daha açık lacivert kullanıyoruz ama gezegenler net görünür.
472
- function setTheme(t){if(!scene)return;const dark=t!=='light';fog.color.set(dark?0x14203a:0x18233d);fog.density=dark?.012:.014;if(scene.background)scene.background.set(dark?0x0b1224:0x111b30);if(ground)ground.material.color.set(dark?0x1a2740:0x22304c)}
473
-
474
- // Sinematik odak — seçilen gezegene yumuşak kayış (animate uygular)
475
- function focusOnAgent(a){if(!controls)return;if(a){focus.target.copy(a.g.position);focus.dist=6.5;focus.active=true;controls.autoRotate=false}
476
- else{focus.target.copy(HOME.target);focus.dist=HOME.dist;focus.active=false;controls.autoRotate=!reduceMotion}syncAutoBtn()}
477
- function select(name){
478
- if(selected){const p=agents.get(selected);if(p)p.label.material.opacity=0}
479
- selected=name;const a=agents.get(name);
480
- if(a){a.label.material.opacity=1;focusOnAgent(a)}
481
- else{if(selectionRing)selectionRing.visible=false;focusOnAgent(null)}
482
- onSelect&&onSelect(name);
483
- }
484
- function deselect(){if(selected){const p=agents.get(selected);if(p)p.label.material.opacity=0}selected=null;if(selectionRing)selectionRing.visible=false;focusOnAgent(null);onSelect&&onSelect(null)}
485
-
486
- function onPointerMove(e){const r=renderer.domElement.getBoundingClientRect();pointer.x=((e.clientX-r.left)/r.width)*2-1;pointer.y=-((e.clientY-r.top)/r.height)*2+1;
487
- raycaster.setFromCamera(pointer,camera);const hit=raycaster.intersectObjects(pickables,false)[0];
488
- const name=hit?hit.object.userData.name:null;hovered=name;renderer.domElement.style.cursor=name?'pointer':'grab';
489
- }
490
- let downXY=null;
491
- function onPointerDown(e){downXY=[e.clientX,e.clientY]}
492
- renderer_click_guard();
493
- function renderer_click_guard(){document.addEventListener('pointerup',e=>{if(!downXY||!renderer)return;const moved=Math.hypot(e.clientX-downXY[0],e.clientY-downXY[1]);downXY=null;if(moved>6)return;
494
- const r=renderer.domElement.getBoundingClientRect();if(e.clientX<r.left||e.clientX>r.right||e.clientY<r.top||e.clientY>r.bottom)return;
495
- pointer.x=((e.clientX-r.left)/r.width)*2-1;pointer.y=-((e.clientY-r.top)/r.height)*2+1;raycaster.setFromCamera(pointer,camera);
496
- const hit=raycaster.intersectObjects(pickables,false)[0];if(hit)select(hit.object.userData.name);else deselect();
497
- })}
498
-
499
- function animate(){rafId=requestAnimationFrame(animate);const dt=Math.min(clock.getDelta(),.05),t=clock.elapsedTime;const anim=reduceMotion?0:1;
500
- // operatör
501
- if(operator){const act=operator.userData.active;operator.userData.core.rotation.y+=dt*.5*anim;operator.userData.core.rotation.x+=dt*.22*anim;
502
- opRings.forEach((r,i)=>{r.rotation.z+=dt*(.3+i*.25)*(act?2:1)*anim;r.rotation.y+=dt*.15*anim});
503
- const pu=(Math.sin(t*(act?4:1.6))*.5+.5)*anim;operator.userData.core.material.emissiveIntensity=(act?.8:.5)+pu*.25;
504
- operator.userData.light.intensity=(act?1.1:.7)+pu*.25;opGlow.material.opacity=.17+pu*.1;}
505
- // ajanlar
506
- for(const a of agents.values()){
507
- const tg=a.tgt;
508
- // renk her zaman ajanın kendi rengi (sabit); durum + varış "flare"ı parlaklıkla belli olur
509
- const breathe=(Math.sin(t*1.8+a.phase)*.5+.5)*anim; // 0..1 yumuşak nefes
510
- const workBoost=a.status==='working'?(Math.sin(t*3.5+a.phase)*.5+.5)*anim:0;
511
- if(a.flare>0)a.flare=Math.max(0,a.flare-dt*1.6); // varış parlaması söner
512
- a.visor.material.emissiveIntensity+=((tg.emis+breathe*.14+workBoost*.35+a.flare*1.3)-a.visor.material.emissiveIntensity)*.14;
513
- a.glow.material.opacity=.1+breathe*.1+workBoost*.22+a.flare*.4; // operatörden belirgin az
514
- a.glow.scale.setScalar(2.1+breathe*.22+workBoost*.4+a.flare*1);
515
- // konum sabit (yörünge hızı 0) + hafif süzülme; sistem bütün olarak kamerayla döner
516
- a.g.position.set(Math.cos(a.orbitAngle)*a.orbitR, a.orbitY+Math.sin(t*1.2+a.phase)*.07*anim, Math.sin(a.orbitAngle)*a.orbitR);
517
- const want=PLANET_SCALE*((hovered===a.name||selected===a.name)?1.16:1);a.g.scale.lerp(tmp.set(want,want,want),.15);
518
- }
519
- // seçim halkası ajanı takip eder
520
- if(selected&&agents.has(selected)){const a=agents.get(selected);selectionRing.visible=true;selectionRing.position.set(a.g.position.x,a.orbitY,a.g.position.z);selectionRing.material.color.copy(a.color);selectionRing.rotation.z+=dt*.6*anim;selectionRing.material.opacity=.6+Math.sin(t*3)*.25*anim}
521
- else selectionRing.visible=false;
522
- // sinematik odak — yalnızca geçiş anında; hedefe varınca kontrolü kullanıcıya bırak
523
- if(focus.active){controls.target.lerp(focus.target,.08);
524
- const dir=tmp.copy(camera.position).sub(controls.target);const curD=dir.length()||.001;const wantD=curD+(focus.dist-curD)*.08;
525
- dir.setLength(wantD);camera.position.copy(controls.target).add(dir);
526
- if(controls.target.distanceTo(focus.target)<.06&&Math.abs(curD-focus.dist)<.12)focus.active=false;}
527
- // akış darbeleri (izli enerji paketi + varışta çarpma dalgası)
528
- for(let i=pulses.length-1;i>=0;i--){const p=pulses[i];p.t+=dt/.85;const tt=p.dir==='up'?1-Math.min(p.t,1):Math.min(p.t,1);
529
- const pt=p.curve.getPoint(tt);p.m.position.copy(pt);
530
- for(let k=p.tn-1;k>0;k--){p.tarr[k*3]=p.tarr[(k-1)*3];p.tarr[k*3+1]=p.tarr[(k-1)*3+1];p.tarr[k*3+2]=p.tarr[(k-1)*3+2]}
531
- p.tarr[0]=pt.x;p.tarr[1]=pt.y;p.tarr[2]=pt.z;p.trail.geometry.attributes.position.needsUpdate=true;
532
- if(p.t>=1){spawnRipple(pt,p.col);if(p.dir==='down'){const a=agents.get(p.name);if(a)a.flare=1}
533
- scene.remove(p.m);scene.remove(p.trail);p.trail.geometry.dispose();pulses.splice(i,1)}}
534
- // çarpma dalgaları — genişleyip söner
535
- for(let i=ripples.length-1;i>=0;i--){const r=ripples[i];r.t+=dt/.7;const s=.2+r.t*2.4;r.mesh.scale.set(s,s,s);r.mesh.material.opacity=.85*(1-r.t);if(r.t>=1){scene.remove(r.mesh);r.mesh.geometry.dispose();ripples.splice(i,1)}}
536
- // delegasyon bağları — hat, ok ucu ve veri paketleri gezegen hareketini takip eder.
537
- // Paketler her zaman operatörden hedef agente akar; renk hedef agentın CLI rengidir.
538
- for(const [name,b] of beams){updateBeam(name);b.line.material.opacity=.5+Math.sin(t*5)*.18*anim;b.arrow.material.opacity=.72+Math.sin(t*4)*.2*anim;
539
- for(let i=0;i<b.packets.length;i++){const u=((reduceMotion?0:t*.28)+i/b.packets.length)%1,packet=b.packets[i],pt=b.curve.getPoint(u);packet.position.copy(pt);const fade=Math.sin(Math.PI*u);packet.material.opacity=.35+fade*.65;packet.scale.setScalar(.75+fade*.55)}}
540
- // parçacıklar
541
- if(particles)particles.rotation.y+=dt*.02*anim;
542
- controls.update();composer.render();
543
- }
544
-
545
- function syncAutoBtn(){const b=$('#autoRotBtn');if(!b||!controls)return;b.textContent=controls.autoRotate?'⏸ Döndür':'▶ Döndür'}
546
- function toggleAuto(){if(!controls)return;controls.autoRotate=!controls.autoRotate;syncAutoBtn()}
547
- function camReset(){if(!controls)return;controls.reset();focus.target.copy(HOME.target);focus.dist=HOME.dist;focus.active=false;controls.autoRotate=!reduceMotion;syncAutoBtn()}
548
-
549
- function setHover(name){if(ok)hovered=name}
550
- return {init,setAgents,updateAgent,setBeam,pulse,setOperator,setOperatorPhase,reset,resize,setTheme,deselect,selectByName:select,setHover,
551
- set onSelect(fn){onSelect=fn},get selected(){return selected},toggleAuto,camReset,syncAutoBtn};
552
- })();
553
-
554
- /* ============================================================
555
- Uygulama durumu + SSE — 3B sahneyi besler
556
- ============================================================ */
557
- let operatorName='',nodes=new Map(),roster=new Map(),agentMeta={},delegCount=0,roundNo=0,stepNo=0,missionActive=false,activeSet=new Set();
558
- let selectedAgent=null;
559
-
560
- (function(){let t='dark';try{t=localStorage.getItem('cliTheme')||'dark'}catch{}applyTheme(t)})();
561
-
562
- /* ---- Üst durum / KPI ---- */
563
- function setEngine(s){$('#dot').className='dot'+(s.running?' on':'');$('#engineState').textContent=s.running?'çalışıyor':'durdu';
564
- $('#modePill').textContent=s.current?`${s.current.stage} · ${cliLabel(adapterOf(s.current.agent))}`:(s.busy?'meşgul':'boşta');
565
- if(typeof s.callsToday==='number')$('#kCalls').textContent=s.callsToday;
566
- if(s.defaultOperator)setOperator(s.defaultOperator)}
567
- function setOperator(name){if(!name)return;operatorName=name;const lbl=cliLabel(adapterOf(name));$('#opName').textContent=lbl;$('#kOp').textContent=lbl;Scene.setOperator(lbl)}
568
- function opState(html,active){$('#opState').innerHTML=html;Scene.setOperatorPhase(active)}
569
- function bumpStep(){stepNo++;return stepNo}
570
- function refreshActive(){$('#kActive').textContent=activeSet.size}
571
-
572
- /* ---- Ajan filosu (yan liste + 3B modeller) ---- */
573
- function renderRoster(agents){roster.clear();agentMeta={};const box=$('#roster');const entries=Object.entries(agents||{}).filter(([,a])=>a.enabled!==false);
574
- $('#rosterCount').textContent=entries.length+' ajan';
575
- if(!entries.length){box.innerHTML='<div class="tl-empty">Tanımlı uzman ajan yok.</div>';Scene.setAgents([]);return}
576
- box.innerHTML='';const sceneList=[];
577
- for(const [name,a] of entries){const cli=adapterOf(a);const role=(a.roleFile||'').split('/').pop().replace('.md','')||'uzman';agentMeta[name]={cli,role};
578
- const el=document.createElement('div');el.className='agent-card';el.id='ros-'+cssId(name);el.style.setProperty('--cli',cliColor(cli));
579
- el.innerHTML=`<div class="avatar" style="--cli:${cliColor(cli)}"><span class="led"></span></div>
580
- <div class="ai"><div class="n">${esc(name)}</div><div class="m"><span class="chip cli" style="background:${cliColor(cli)}">${esc(cliLabel(cli))}</span><span class="chip">${esc(role)}</span>${a.costTier?`<span class="chip">${esc(a.costTier)}</span>`:''}</div><div class="stt"><span class="status-text">hazır · sırada</span><span class="run-count">0 çalışma</span></div></div>`;
581
- el.addEventListener('click',()=>Scene.selectByName(name));
582
- box.appendChild(el);roster.set(name,{el,stt:el.querySelector('.status-text'),count:el.querySelector('.run-count'),status:'idle'});
583
- sceneList.push({name,cli,role,color:cliColor(cli)});
584
- }
585
- Scene.setAgents(sceneList);
586
- }
587
- function syncRoster(name){const r=roster.get(name);if(!r)return;const jobs=[...nodes.values()].filter(n=>n.agent===name),active=jobs.filter(n=>n.status==='working'||n.status==='queued'),failed=jobs.filter(n=>n.status==='failed'||n.status==='blocked');
588
- let status='idle',text='hazır · sırada';if(active.length){status='working';text=`çalışıyor · #${active[active.length-1].step}`}else if(jobs.length){status=failed.length&&failed.length===jobs.length?'failed':'done';text=failed.length?`${jobs.length} iş · ${failed.length} sorun`:`${jobs.length} işi tamamladı`}
589
- r.el.classList.remove('working','done','failed','used');if(status!=='idle')r.el.classList.add(status,'used');r.stt.textContent=text;r.count.textContent=`${jobs.length} çalışma`;r.status=status;
590
- if(active.length)activeSet.add(name);else activeSet.delete(name);refreshActive();
591
- Scene.updateAgent(name,status);Scene.setBeam(name,!!active.length);
592
- if(selectedAgent===name)renderPanel(name);
593
- }
594
-
595
- /* ---- Sahne düğümleri (veri; DOM yok) ---- */
596
- function ensureNode(id,name,instruction){
597
- const key=String(id||`${name}-live`);if(nodes.has(key)){const found=nodes.get(key);if(instruction){found.instruction=instruction}return found}
598
- const step=nodes.size+1;
599
- const node={id:key,agent:name,step,raw:'',instruction:instruction||'',status:'queued',startedAt:null,durationMs:null};
600
- nodes.set(key,node);syncRoster(name);return node;
601
- }
602
- function setNode(id,name,status,text,meta={}){const n=ensureNode(id,name,meta.instruction);n.status=status||n.status;if(text)n.statusText=text;
603
- if(meta.instruction)n.instruction=meta.instruction;
604
- if(meta.startedAt&&!n.startedAt)n.startedAt=new Date(meta.startedAt).getTime();if(meta.durationMs!=null)n.durationMs=meta.durationMs;
605
- syncRoster(n.agent);if(selectedAgent===n.agent)renderPanel(n.agent);return n}
606
- function appendTail(id,name,text){const n=ensureNode(id,name);n.raw=(n.raw+text).slice(-4000);if(selectedAgent===name)updatePanelTail(name)}
607
-
608
- /* ---- Akış darbeleri ---- */
609
- function pulse(id,dir){const n=nodes.get(String(id));if(!n)return;Scene.pulse(n.agent,dir)}
610
-
611
- /* ---- Detay paneli ---- */
612
- // Hem 3B tıklaması hem yan listedeki kart tıklaması Scene.selectByName üzerinden
613
- // buraya (onSelect callback) düşer; tek yol, tek panel.
614
- function onSceneSelect(name){selectedAgent=name;highlightRoster(name);if(!name){const p=$('#agentPanel');p.hidden=true;p.innerHTML='';return}renderPanel(name)}
615
- function highlightRoster(name){for(const [n,r] of roster)r.el.classList.toggle('selected',n===name)}
616
-
617
- function agentDetail(name){const meta=agentMeta[name]||{};const jobs=[...nodes.values()].filter(n=>n.agent===name);
618
- const active=jobs.filter(j=>j.status==='working'||j.status==='queued');
619
- const latest=active[active.length-1]||jobs[jobs.length-1]||null;
620
- const r=roster.get(name);
621
- return {name,cli:meta.cli||adapterOf(name),role:meta.role||'uzman',status:r?r.status:'idle',jobs:jobs.length,latest};
622
- }
623
- function statusLabel(s){return s==='working'?'Çalışıyor':s==='done'?'Tamamlandı':s==='failed'?'Sorun oluştu':'Beklemede'}
624
- function renderPanel(name){const d=agentDetail(name),p=$('#agentPanel');p.hidden=false;const cc=cliColor(d.cli);
625
- const j=d.latest;
626
- p.style.setProperty('--cli',cc);
627
- p.innerHTML=`<div class="ap-head" style="--cli:${cc}">
628
- <div class="ap-av" style="--cli:${cc}"></div>
629
- <div class="ap-id"><div class="name">${esc(name)}</div><div class="sub"><span class="chip cli" style="background:${cc}">${esc(cliLabel(d.cli))}</span><span class="chip">${esc(d.role)}</span></div></div>
630
- <button class="ap-close" title="Kapat">✕</button>
631
- </div>
632
- <div class="ap-body">
633
- <div class="ap-status ${esc(d.status)}"><span class="sdot"></span>${statusLabel(d.status)} · ${d.jobs} çalışma</div>
634
- ${j?`<div class="ap-sec">Şu an ne yapıyor</div><div class="ap-task">${esc(j.instruction||j.statusText||'Görev ayrıntısı bekleniyor…')}</div>
635
- <div class="ap-sec">Canlı çıktı</div><pre class="ap-tail">${esc(j.raw||'')}</pre>
636
- <div class="ap-meta"><span>iş: ${esc(j.id)}</span><span>adım: #${j.step}</span>${j.durationMs!=null?`<span>süre: ${(j.durationMs/1000).toFixed(1)} sn</span>`:''}</div>`
637
- :`<div class="ap-empty">Bu ajan henüz bir görev almadı. Operatör delege ettiğinde çalışması burada canlı görünecek.</div>`}
638
- </div>`;
639
- p.querySelector('.ap-close').addEventListener('click',()=>{selectedAgent=null;Scene.deselect();p.hidden=true;highlightRoster(null)});
640
- }
641
- function updatePanelTail(name){const p=$('#agentPanel');if(p.hidden)return;const el=p.querySelector('.ap-tail');if(!el)return renderPanel(name);const d=agentDetail(name);if(d.latest){el.textContent=d.latest.raw||'';el.scrollTop=el.scrollHeight}}
642
-
643
- /* ---- Zaman çizelgesi ---- */
644
- function addTimeline(kind,route,txt,num,at,agent){const tl=$('#timeline');if(tl.querySelector('.tl-empty'))tl.innerHTML='';const d=document.createElement('div');d.className='tl '+kind;d.innerHTML=`<div class="num">${num??'•'}</div><div class="body"><div class="route">${esc(route)}<time>${time(at)}</time></div>${txt?`<div class="txt">${esc(txt)}</div>`:''}</div>`;
645
- if(agent&&roster.has(agent)){d.style.cursor='pointer';d.addEventListener('mouseenter',()=>Scene.setHover(agent));d.addEventListener('mouseleave',()=>Scene.setHover(null));d.addEventListener('click',()=>Scene.selectByName(agent))}
646
- tl.appendChild(d);tl.scrollTop=tl.scrollHeight}
647
-
648
- /* ---- Görev yaşam döngüsü ---- */
649
- function startMission(title,at){missionActive=true;delegCount=0;roundNo=0;stepNo=0;activeSet.clear();refreshActive();nodes.clear();Scene.reset();$('#timeline').innerHTML='';
650
- for(const [name,r] of roster){r.el.classList.remove('working','done','failed','used');r.stt.textContent='hazır · sırada';r.count.textContent='0 çalışma';r.status='idle';Scene.updateAgent(name,'idle')}
651
- if(selectedAgent)renderPanel(selectedAgent);
652
- $('#mKick').textContent='● Görev yürütülüyor';$('#mTitle').textContent=title||'Yeni takım görevi';$('#mSub').textContent='Operatör hedefi analiz ediyor ve uygun uzmanları seçiyor.';
653
- $('#kRound').textContent='0';$('#kDeleg').textContent='0';$('#kFiles').textContent='0';
654
- opState('hedefi <b>analiz ediyor</b>',true);addTimeline('system','Görev alındı',title,'▶',at)}
655
-
656
- /* ---- SSE işleyicileri ---- */
657
- function onActivity(e){
658
- if(isOpStage(e.stage)){setOperator(e.agent);
659
- if(e.kind==='process.started')opState(e.stage==='operator-plan'?'ekip <b>planını hazırlıyor</b>':e.stage==='operator-review'?'sonuçları <b>değerlendiriyor</b>':'<b>çalışıyor</b>',true);
660
- else if(e.kind==='process.finished')opState(Number(e.exitCode)!==0?'karar veremedi':'ekibi <b>yönlendirdi</b>',true);
661
- return}
662
- const id=e.assignmentId||`${e.agent}-live`;ensureNode(id,e.agent);
663
- if(e.kind==='process.started')setNode(id,e.agent,'working','görevi yürütüyor',{startedAt:e.at});
664
- else if(e.kind==='stdout'||e.kind==='stderr'){appendTail(id,e.agent,e.text);setNode(id,e.agent,'working','canlı çıktı üretiyor')}
665
- else if(e.kind==='process.finished'){const failed=Number(e.exitCode)!==0;setNode(id,e.agent,failed?'failed':'done',failed?'başarısız':'uzman çalışmasını bitirdi',{durationMs:e.durationMs})}
666
- else if(e.kind==='process.failed'||e.kind==='process.timeout')setNode(id,e.agent,'failed',e.kind==='process.timeout'?'zaman aşımı':'çalıştırma hatası',{durationMs:e.durationMs});
667
- }
668
- function onMessage(e){const from=e.from,to=e.to,type=e.messageType||'';
669
- const id=String(e.assignmentId||`${from}-${delegCount+1}`);
670
- if(type==='delegation'){delegCount++;$('#kDeleg').textContent=delegCount;setOperator(from);ensureNode(id,to,e.body);
671
- addTimeline('delegation',`#${id} · ${cliLabel(adapterOf(from))} → ${to}`,e.body,bumpStep(),e.at,to);setNode(id,to,'working','operatörden görev aldı',{instruction:e.body,startedAt:e.at});pulse(id,'down');opState('uzmana <b>delege ediyor</b>',true)}
672
- else if(type==='result'){addTimeline('result',`#${id} · ${from} → operatör`,e.body,bumpStep(),e.at,from);const n=setNode(id,from,'done','teslimat operatöre ulaştı');if(!n.raw){n.raw=String(e.body||'').slice(0,4000);if(selectedAgent===from)updatePanelTail(from)}pulse(id,'up');opState('teslimatı <b>topluyor</b>',true)}
673
- else if(type==='failure'){addTimeline('failure',`#${id} · ${from} → operatör`,e.body,bumpStep(),e.at,from);const n=setNode(id,from,'failed','agent çalışması başarısız');n.raw=String(e.body||'').slice(0,4000);if(selectedAgent===from)updatePanelTail(from);pulse(id,'up')}
674
- else if(type==='blocked'){addTimeline('blocked',`#${id} · bağımlılık engeli`,e.body,bumpStep(),e.at);const known=nodes.get(id);setNode(id,known?.agent||'engellenen-görev','blocked','bağımlılık nedeniyle çalışmadı',{instruction:e.body})}
675
- }
676
- function onLog(e){
677
- if(e.level==='task'){startMission(String(e.msg||'').replace(/^GOREV\s+[^:]+:\s*/,'').slice(0,160),e.at);return}
678
- if(String(e.msg||'').startsWith('Calisma modu'))$('#mSub').textContent=e.msg;
679
- if(e.level==='error'){$('#mKick').textContent='! Son çalışma başarısız';$('#mSub').textContent=String(e.msg||'Görev tamamlanamadı.');opState('görevi <b>tamamlayamadı</b>',false);for(const [id,n] of nodes){if(n.status==='working'||n.status==='queued')setNode(id,n.agent,'failed','görev hata ile kapandı')}missionActive=false}
680
- }
681
- function onResult(e){
682
- if(e.kind==='operator-chat')return;
683
- roundNo=e.rounds||roundNo;$('#kRound').textContent=roundNo;
684
- const changed=((e.changes?.created||[]).length)+((e.changes?.modified||[]).length);$('#kFiles').textContent=changed;
685
- $('#mKick').textContent='✓ Son çalışma tamamlandı';
686
- $('#mSub').textContent=`${cliLabel(adapterOf(e.operator||operatorName))} · ${roundNo} takım turu · ${changed} dosya değişikliği`;
687
- opState('görevi <b>tamamladı</b>',false);
688
- activeSet.clear();refreshActive();
689
- for(const [id,n] of nodes){if(n.status==='working'||n.status==='queued')setNode(id,n.agent,'done','görev kapanışında tamamlandı')}
690
- addTimeline('system','Operatör görevi tamamladı',e.summary||(e.delivery?.verification||''),'✓',e.at);missionActive=false;
691
- }
692
-
693
- /* ---- 3B sahne kancaları ---- */
694
- const loaderStart=performance.now();
695
- function hideSceneLoader(){const l=$('#sceneLoader');if(!l)return;const wait=Math.max(0,900-(performance.now()-loaderStart));
696
- setTimeout(()=>{l.classList.add('gone');setTimeout(()=>l.remove(),500)},wait)}
697
- const canvas=$('#scene');
698
- try{Scene.init(canvas);requestAnimationFrame(()=>requestAnimationFrame(hideSceneLoader))}
699
- catch(err){console.warn('WebGL 3B sahne başlatılamadı:',err);showSceneFallback();hideSceneLoader()}
700
- Scene.onSelect=(name)=>onSceneSelect(name);
701
- Scene.setTheme(document.documentElement.getAttribute('data-theme'));
702
- $('#autoRotBtn').addEventListener('click',()=>Scene.toggleAuto());
703
- $('#camResetBtn').addEventListener('click',()=>Scene.camReset());
704
- Scene.syncAutoBtn();
705
- function showSceneFallback(){const st=$('#stage'),c=$('#scene'),tr=document.querySelector('.hud-tr');
706
- if(c)c.style.display='none';if(tr)tr.style.display='none';
707
- const d=document.createElement('div');d.className='scene-fallback';
708
- d.innerHTML='<div class="sf-ic">🛰️</div><h3>3B görünüm başlatılamadı</h3><p>Tarayıcınız WebGL desteklemiyor olabilir. Ekip verileri soldaki filo ve sağdaki zaman çizelgesinde canlı çalışmaya devam ediyor; bir ajanı seçmek için filodan tıklayın.</p>';
709
- st.appendChild(d);}
710
- /* ---- Yardım (?) modalı ---- */
711
- (function(){const m=$('#helpModal');const open=()=>{m.hidden=false};const close=()=>{m.hidden=true};
712
- $('#helpBtn').addEventListener('click',open);$('#helpClose').addEventListener('click',close);
713
- m.addEventListener('click',e=>{if(e.target===m)close()});
714
- document.addEventListener('keydown',e=>{if(e.key==='Escape'&&!m.hidden)close()});})();
715
-
716
- const es=new EventSource('/api/events');
717
- es.addEventListener('status',e=>setEngine(JSON.parse(e.data)));
718
- es.addEventListener('activity',e=>onActivity(JSON.parse(e.data)));
719
- es.addEventListener('message',e=>onMessage(JSON.parse(e.data)));
720
- es.addEventListener('log',e=>onLog(JSON.parse(e.data)));
721
- es.addEventListener('result',e=>onResult(JSON.parse(e.data)));
722
- // Sayfadan ayrılırken (Komuta Merkezi'ne dönüş / bfcache) açık SSE bağlantısını kapat ki
723
- // donmuş bağlantılar birikip diğer sayfadaki fetch'leri kilitlemesin; geri dönüşte tazele.
724
- window.addEventListener('pagehide',()=>{try{es.close()}catch{}});
725
- window.addEventListener('pageshow',e=>{if(e.persisted)location.reload()});
726
-
727
- /* ---- İlk yükleme ---- */
728
- (async()=>{try{
729
- const s=await(await fetch('/api/state')).json();
730
- renderRoster(s.config&&s.config.agents);
731
- setEngine(s.status);
732
- const cur=s.status.current;
733
- const recent=cur||(s.queue?.done||[])[0]||(s.queue?.failed||[])[0];
734
- if(recent&&recent.id){
735
- const ev=await(await fetch(`/api/tasks/${recent.id}/events?limit=1500`)).json();
736
- missionActive=!!cur;
737
- for(const e of ev){if(e.type==='log')onLog(e);else if(e.type==='activity')onActivity(e);else if(e.type==='message')onMessage(e);else if(e.type==='result')onResult(e)}
738
- }
739
- }catch(err){console.warn(err)}})();
740
- </script>
741
- </body></html>
11
+ <link rel="manifest" href="/site.webmanifest">
12
+ <style>
13
+ *{box-sizing:border-box}
14
+ :root{--bg:#090c12;--panel:#111826;--panel2:#182234;--line:#243149;--tx:#e9eef7;--dim:#8494ab;--acc:#5e8bff;--ok:#2fd08a;--warn:#eab24d;--err:#f2565f;--purple:#a98bff;
15
+ --headerbg:#0a0f1acc;--term:#05080f;--nodebg:#0b1220;--shadow:0 14px 40px #0008;
16
+ --core:radial-gradient(circle at 50% 34%,#2a2060,#141031);--coretx:#efeaff;
17
+ --appbg:radial-gradient(1200px 620px at 50% -14%,#5e8bff16,transparent),radial-gradient(1000px 520px at 90% -6%,#a98bff16,transparent),radial-gradient(900px 500px at 5% 10%,#2fd08a0e,transparent),var(--bg)}
18
+ [data-theme=light]{--bg:#edf1f8;--panel:#ffffff;--panel2:#f3f6fc;--line:#dce4ef;--tx:#131a27;--dim:#5a6779;--acc:#3a63f0;--ok:#0ca678;--warn:#a76a10;--err:#dc434e;--purple:#7350f0;
19
+ --headerbg:#ffffffe6;--term:#0d1420;--nodebg:#f7f9fd;--shadow:0 14px 36px #7a90b820;
20
+ --core:radial-gradient(circle at 50% 34%,#f1ecff,#e2dbff);--coretx:#3a2b73;
21
+ --appbg:radial-gradient(1200px 620px at 50% -14%,#3a63f012,transparent),radial-gradient(1000px 520px at 90% -6%,#7350f00e,transparent),var(--bg)}
22
+ body{margin:0;background:var(--appbg);background-attachment:fixed;color:var(--tx);font:14px/1.5 Inter,Segoe UI,system-ui,sans-serif;transition:background .3s,color .3s}
23
+ button{cursor:pointer;color:var(--tx);background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:7px 11px;font:inherit;transition:border-color .18s,transform .1s,box-shadow .18s}
24
+ button:hover{border-color:var(--acc);box-shadow:0 4px 14px color-mix(in srgb,var(--acc) 16%,transparent)}button:active{transform:translateY(1px)}
25
+ a.btn{text-decoration:none;display:inline-block}
26
+ /* Header */
27
+ header{height:58px;display:flex;align-items:center;gap:12px;padding:0 16px;background:var(--headerbg);backdrop-filter:blur(10px);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:20}
28
+ h1{font-size:15px;margin:0;letter-spacing:.2px}.brand{color:var(--acc);font-weight:800}.sep{color:var(--dim)}.grow{flex:1}
29
+ .pill{border:1px solid var(--line);background:var(--panel2);border-radius:999px;padding:5px 11px;color:var(--dim);font-size:12px;display:inline-flex;align-items:center;gap:6px}
30
+ .dot{width:8px;height:8px;border-radius:50%;background:var(--dim);box-shadow:0 0 0 0 transparent}
31
+ .dot.on{background:var(--ok);box-shadow:0 0 10px var(--ok);animation:beat 1.6s ease-in-out infinite}
32
+ @keyframes beat{50%{box-shadow:0 0 3px var(--ok)}}
33
+ /* KPI şeridi */
34
+ .topbar{padding:14px 14px 0}
35
+ .banner{background:linear-gradient(120deg,var(--panel),var(--panel2));border:1px solid var(--line);border-radius:16px;padding:14px 18px;box-shadow:var(--shadow);display:flex;gap:16px;align-items:center;flex-wrap:wrap}
36
+ .banner .kick{font-size:10px;letter-spacing:1.4px;text-transform:uppercase;color:var(--acc);font-weight:800}
37
+ .banner .t{font-weight:800;font-size:18px;line-height:1.25}.banner .s{color:var(--dim);font-size:12px;margin-top:2px}
38
+ .tiles{display:grid;grid-template-columns:repeat(6,minmax(92px,1fr));gap:10px;margin-left:auto}
39
+ .tile{background:#ffffff06;border:1px solid var(--line);border-radius:12px;padding:9px 12px;min-width:92px}
40
+ .tile .k{font-size:9.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--dim)}
41
+ .tile .v{font-size:21px;font-weight:800;margin-top:1px;font-variant-numeric:tabular-nums;line-height:1.1}
42
+ .tile.op .v{font-size:15px}
43
+ /* Ana ızgara */
44
+ .wrap{display:grid;grid-template-columns:272px minmax(420px,1fr) 320px;gap:14px;padding:14px;align-items:start}
45
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow)}
46
+ .card-h{display:flex;align-items:center;gap:8px;padding:12px 14px;border-bottom:1px solid var(--line)}
47
+ .card-h h2{font-size:11px;letter-spacing:.8px;text-transform:uppercase;color:var(--dim);margin:0}
48
+ .card-h .count{margin-left:auto;font-size:11px;color:var(--dim);background:var(--panel2);border:1px solid var(--line);border-radius:999px;padding:2px 8px}
49
+ .card-b{padding:12px 14px}
50
+ /* Ajan filosu (yan liste) */
51
+ .roster{display:flex;flex-direction:column;gap:12px;max-height:calc(100vh - 210px);overflow:auto;padding:6px 4px 4px}
52
+ .agent-card{--cli:var(--acc);position:relative;display:flex;gap:12px;align-items:center;padding:11px 12px;border:1px solid color-mix(in srgb,var(--cli) 24%,var(--line));border-radius:14px;background:linear-gradient(150deg,color-mix(in srgb,var(--cli) 9%,var(--nodebg)),var(--nodebg) 62%);box-shadow:0 10px 22px #0004;cursor:pointer;transition:border-color .25s,box-shadow .25s,transform .15s}
53
+ .agent-card:hover{border-color:color-mix(in srgb,var(--cli) 46%,var(--line));box-shadow:0 16px 34px #0006;transform:translateY(-1px)}
54
+ .agent-card.selected{border-color:var(--cli);box-shadow:0 0 0 1px var(--cli),0 16px 34px #0006}
55
+ .agent-card:before{content:'';position:absolute;left:0;top:14%;bottom:14%;width:3px;border-radius:0 3px 3px 0;background:linear-gradient(180deg,transparent,var(--cli),transparent);opacity:.55}
56
+ .agent-card.working{border-color:color-mix(in srgb,var(--ok) 48%,var(--line))}.agent-card.working:before{background:var(--ok);opacity:.85}
57
+ .agent-card.failed{border-color:color-mix(in srgb,var(--err) 55%,var(--line))}.agent-card.failed:before{background:var(--err)}
58
+ .avatar{position:relative;width:46px;height:46px;flex:none;border-radius:50%;background:radial-gradient(circle at 36% 30%,#ffffff,#ffffffde 4%,#ffffff00 22%),radial-gradient(circle at 50% 128%,color-mix(in srgb,var(--cli,#8b98b0) 55%,#ffffff),transparent 42%),radial-gradient(circle at 42% 36%,color-mix(in srgb,var(--cli,#8b98b0) 78%,#ffffff),var(--cli,#8b98b0) 40%,color-mix(in srgb,var(--cli,#8b98b0) 40%,#000) 100%);box-shadow:inset -6px -8px 16px #000000ad,inset 6px 7px 13px #ffffff45,0 0 0 1px #0006,0 12px 16px -6px #000c}
59
+ .avatar .led{position:absolute;z-index:2;right:-2px;bottom:-1px;width:12px;height:12px;border-radius:50%;background:var(--dim);border:3px solid var(--panel);box-shadow:0 0 0 1px var(--line)}
60
+ .agent-card.working .led{background:var(--ok);box-shadow:0 0 5px color-mix(in srgb,var(--ok) 60%,transparent)}.agent-card.done .led{background:var(--acc)}.agent-card.failed .led{background:var(--err)}
61
+ .ai{min-width:0;flex:1}
62
+ .ai .n{font-weight:750;font-size:13px;letter-spacing:-.1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
63
+ .ai .m{display:flex;gap:5px;align-items:center;margin-top:2px;flex-wrap:wrap}
64
+ .chip{font-size:9.5px;border:1px solid var(--line);border-radius:999px;padding:1px 6px;color:var(--dim)}
65
+ .chip.cli{color:#fff;font-weight:700;border:none;text-shadow:0 1px 1px #0006}
66
+ .ai .stt{font-size:10.5px;color:var(--dim);margin-top:3px;display:flex;align-items:center;justify-content:space-between;gap:6px}
67
+ .run-count{font:9.5px ui-monospace,Consolas,monospace;color:var(--dim);border:1px solid var(--line);border-radius:999px;padding:1px 6px;white-space:nowrap}
68
+ .agent-card.working .stt{color:var(--ok)}
69
+
70
+ /* ===== 3B Sahne ===== */
71
+ .stage{position:relative;background:radial-gradient(120% 90% at 50% 8%,#26375c,#141f37 55%,#0c1426);border:1px solid var(--line);border-radius:16px;min-height:600px;height:calc(100vh - 210px);overflow:hidden;box-shadow:var(--shadow)}
72
+ [data-theme=light] .stage{background:radial-gradient(120% 90% at 50% 8%,#f2f5fd,#e3e9f7 60%,#d7deef)}
73
+ #scene{position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none;cursor:grab}
74
+ #scene:active{cursor:grabbing}
75
+ .hud{position:absolute;z-index:3;pointer-events:none}
76
+ .hud-tl{top:14px;left:14px;display:flex;flex-direction:column;gap:7px;max-width:60%}
77
+ .hud-op{display:inline-flex;align-items:center;gap:8px;background:#0b1220b0;border:1px solid var(--line);border-radius:12px;padding:7px 12px;backdrop-filter:blur(8px);width:fit-content}
78
+ .hud-op .hud-lbl{font-size:9px;letter-spacing:2px;color:var(--purple);font-weight:800}
79
+ .hud-op #opName{font-size:15px;font-weight:800;color:var(--coretx)}
80
+ .op-phase{width:fit-content;font-size:12px;color:var(--dim);background:#0b1220b0;border:1px solid var(--line);border-radius:999px;padding:5px 13px;backdrop-filter:blur(8px)}
81
+ .op-phase b{color:var(--purple)}
82
+ .hud-tr{top:14px;right:14px;display:flex;gap:8px;pointer-events:auto}
83
+ .mini{background:#0b1220b0;border:1px solid var(--line);border-radius:10px;padding:6px 10px;font-size:12px;color:var(--tx);backdrop-filter:blur(8px)}
84
+ .scene-legend{position:absolute;z-index:3;left:14px;bottom:12px;display:flex;gap:12px;flex-wrap:wrap;background:#0b1220a0;border:1px solid var(--line);border-radius:12px;padding:7px 12px;backdrop-filter:blur(8px);pointer-events:none}
85
+ .scene-legend span{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--dim)}
86
+ .lg-dot{width:8px;height:8px;border-radius:50%;box-shadow:0 0 6px currentColor}
87
+ .scene-hint{position:absolute;z-index:3;right:14px;bottom:12px;font-size:10.5px;color:var(--dim);background:#0b1220a0;border:1px solid var(--line);border-radius:999px;padding:5px 12px;backdrop-filter:blur(8px);pointer-events:none}
88
+ /* 3B sahne her iki temada da koyudur; HUD çipleri koyu arka planlı olduğundan light
89
+ temada metinler görünmez oluyordu. Light temada yazı/kenarları sabit açık renge çek. */
90
+ [data-theme=light] .mini{color:#eef2fa}
91
+ [data-theme=light] .op-phase,[data-theme=light] .scene-legend span,[data-theme=light] .scene-hint,[data-theme=light] .scene-fallback{color:#c3d0e6}
92
+ [data-theme=light] .hud-op #opName{color:#efeaff}
93
+ [data-theme=light] .op-phase b,[data-theme=light] .hud-op .hud-lbl{color:#c3b0ff}
94
+ [data-theme=light] .scene-fallback h3{color:#f2f6ff}
95
+ [data-theme=light] .hud-op,[data-theme=light] .op-phase,[data-theme=light] .mini,[data-theme=light] .scene-legend,[data-theme=light] .scene-hint{border-color:#2c3a55}
96
+ .scene-fallback{position:absolute;inset:0;z-index:4;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:8px;padding:32px;color:var(--dim)}
97
+ .scene-fallback .sf-ic{font-size:40px}.scene-fallback h3{margin:0;color:var(--tx);font-size:16px}.scene-fallback p{margin:0;max-width:420px;font-size:12.5px;line-height:1.6}
98
+ /* 3B yükleme animasyonu */
99
+ .scene-loader{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;background:radial-gradient(120% 90% at 50% 40%,#1a2745,#0b1224);transition:opacity .45s ease}
100
+ .scene-loader.gone{opacity:0;pointer-events:none}
101
+ .orbit-spinner{position:relative;width:64px;height:64px}
102
+ .orbit-spinner .sun{position:absolute;inset:0;margin:auto;width:16px;height:16px;border-radius:50%;background:radial-gradient(circle at 40% 35%,#e6dcff,#a98bff);box-shadow:0 0 16px #a98bff}
103
+ .orbit-spinner .ring{position:absolute;inset:0;border-radius:50%;border:1.5px solid #2a3d63}
104
+ .orbit-spinner .ring:nth-child(2){inset:8px}.orbit-spinner .ring:nth-child(3){inset:18px}
105
+ .orbit-spinner .planet{position:absolute;top:-4px;left:calc(50% - 4px);width:8px;height:8px;border-radius:50%;background:#5e8bff;box-shadow:0 0 8px #5e8bff}
106
+ .orbit-spinner .o1{animation:orbitspin 2.4s linear infinite}.orbit-spinner .o2{animation:orbitspin 3.6s linear infinite reverse}.orbit-spinner .o3{animation:orbitspin 5s linear infinite}
107
+ .orbit-spinner .o2 .planet{background:#2fd08a;box-shadow:0 0 8px #2fd08a}.orbit-spinner .o3 .planet{background:#d97757;box-shadow:0 0 8px #d97757}
108
+ @keyframes orbitspin{to{transform:rotate(360deg)}}
109
+ .scene-loader .lt{font-size:12px;letter-spacing:.4px;color:var(--dim)}
110
+ @media(prefers-reduced-motion:reduce){.orbit-spinner .o1,.orbit-spinner .o2,.orbit-spinner .o3{animation:none}}
111
+ /* Yardım (?) modalı */
112
+ .help-overlay{position:fixed;inset:0;z-index:50;background:#050912cc;backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;padding:20px;animation:panelIn .2s ease}
113
+ .help-overlay[hidden]{display:none}
114
+ .help-card{background:var(--panel);border:1px solid var(--line);border-radius:18px;box-shadow:0 30px 80px #000a;max-width:560px;width:100%;max-height:86vh;overflow:auto}
115
+ .help-card .hc-head{display:flex;align-items:center;gap:10px;padding:16px 18px;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--panel)}
116
+ .help-card .hc-head .ic{font-size:20px}.help-card .hc-head h2{margin:0;font-size:16px}.help-card .hc-head .brand{color:var(--acc)}
117
+ .help-card .hc-close{margin-left:auto;width:30px;height:30px;padding:0;border-radius:8px;font-size:15px}
118
+ .help-card .hc-body{padding:16px 18px;font-size:13px;line-height:1.65;color:var(--tx)}
119
+ .help-card .hc-body p{margin:0 0 12px}
120
+ .hc-row{display:flex;gap:11px;align-items:flex-start;padding:10px 0;border-top:1px solid var(--line)}
121
+ .hc-row .hc-ic{flex:none;width:30px;height:30px;border-radius:9px;display:grid;place-items:center;font-size:15px;background:var(--panel2);border:1px solid var(--line)}
122
+ .hc-row .hc-t{font-weight:750;font-size:12.5px}.hc-row .hc-d{color:var(--dim);font-size:12px;margin-top:2px}
123
+ .hc-keys{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
124
+ .hc-keys kbd{font:11px ui-monospace,Consolas,monospace;background:var(--panel2);border:1px solid var(--line);border-bottom-width:2px;border-radius:6px;padding:2px 7px;color:var(--tx)}
125
+ #helpBtn{width:32px;height:32px;padding:0;border-radius:50%;display:inline-grid;place-items:center;font-weight:800;font-size:15px;line-height:1;color:var(--dim)}
126
+ #helpBtn:hover{color:var(--acc);border-color:var(--acc)}
127
+ .hc-row{align-items:baseline}.hc-num{flex:none;width:20px;color:var(--acc);font-weight:800;font-size:12.5px}
128
+ /* Detay paneli (tıklamada) */
129
+ .agent-panel{position:absolute;z-index:5;top:14px;right:14px;width:min(340px,64%);max-height:calc(100% - 28px);display:flex;flex-direction:column;background:#0b1220ee;border:1px solid var(--line);border-radius:16px;box-shadow:0 24px 60px #000a;backdrop-filter:blur(14px);overflow:hidden;animation:panelIn .22s cubic-bezier(.2,.8,.2,1)}
130
+ @keyframes panelIn{from{opacity:0;transform:translateX(12px)}to{opacity:1;transform:none}}
131
+ .ap-head{display:flex;align-items:center;gap:11px;padding:13px 14px;border-bottom:1px solid var(--line)}
132
+ .ap-av{width:42px;height:42px;flex:none;border-radius:50%;background:radial-gradient(circle at 36% 30%,#ffffff,#ffffffde 4%,#ffffff00 22%),radial-gradient(circle at 42% 36%,color-mix(in srgb,var(--cli,#8b98b0) 78%,#ffffff),var(--cli,#8b98b0) 40%,color-mix(in srgb,var(--cli,#8b98b0) 40%,#000) 100%);box-shadow:inset -5px -6px 12px #000000ad,inset 5px 6px 10px #ffffff45,0 0 0 1px #0006}
133
+ .ap-id{min-width:0;flex:1}
134
+ .ap-id .name{font-weight:800;font-size:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
135
+ .ap-id .sub{display:flex;gap:5px;margin-top:3px;flex-wrap:wrap}
136
+ .ap-close{flex:none;width:28px;height:28px;padding:0;border-radius:8px;font-size:15px;line-height:1}
137
+ .ap-body{padding:12px 14px;overflow:auto}
138
+ .ap-status{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:700;padding:8px 11px;border-radius:10px;border:1px solid var(--line);background:#ffffff06;margin-bottom:11px}
139
+ .ap-status .sdot{width:9px;height:9px;border-radius:50%;background:var(--dim);box-shadow:0 0 8px currentColor}
140
+ .ap-status.working{color:var(--ok)}.ap-status.working .sdot{background:var(--ok)}
141
+ .ap-status.done{color:var(--acc)}.ap-status.done .sdot{background:var(--acc)}
142
+ .ap-status.failed{color:var(--err)}.ap-status.failed .sdot{background:var(--err)}
143
+ .ap-sec{font-size:9.5px;letter-spacing:1px;text-transform:uppercase;color:var(--dim);font-weight:800;margin:12px 0 6px}
144
+ .ap-task{font-size:12px;line-height:1.5;color:var(--tx);background:#ffffff06;border:1px solid var(--line);border-radius:10px;padding:9px 11px;white-space:pre-wrap;word-break:break-word}
145
+ .ap-tail{margin:0;background:var(--term);color:#9fb0c6;font:11px/1.5 ui-monospace,Consolas,monospace;border:1px solid var(--line);border-radius:10px;padding:9px 11px;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-word}
146
+ .ap-tail:empty::after{content:'Henüz canlı çıktı yok.';color:var(--dim)}
147
+ .ap-meta{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:11px;font:10px ui-monospace,Consolas,monospace;color:var(--dim)}
148
+ .ap-empty{color:var(--dim);font-size:12px;padding:6px 0}
149
+ /* Zaman çizelgesi */
150
+ .timeline{display:flex;flex-direction:column;gap:9px;max-height:calc(100vh - 210px);overflow:auto;padding-right:2px}
151
+ .tl{display:flex;gap:9px}
152
+ .tl .num{flex:none;width:24px;height:24px;border-radius:8px;display:grid;place-items:center;font:11px ui-monospace;font-weight:700;background:var(--panel2);border:1px solid var(--line);color:var(--dim)}
153
+ .tl.delegation .num{border-color:var(--purple);color:#e6d8ff}.tl.result .num{border-color:var(--ok);color:var(--ok)}.tl.failure .num,.tl.blocked .num{border-color:var(--err);color:var(--err)}.tl.system .num{border-color:var(--acc);color:var(--acc)}
154
+ .tl .body{flex:1;min-width:0;border:1px solid var(--line);background:var(--panel2);border-radius:4px 11px 11px 4px;border-left:3px solid var(--purple);padding:7px 10px}
155
+ .tl.result .body{border-left-color:var(--ok)}.tl.failure .body,.tl.blocked .body{border-left-color:var(--err)}.tl.system .body{border-left-color:var(--acc)}
156
+ .tl .route{font-size:11px;font-weight:700;color:var(--purple);display:flex;align-items:center;gap:5px}.tl.result .route{color:var(--ok)}.tl.failure .route,.tl.blocked .route{color:var(--err)}.tl.system .route{color:var(--acc)}
157
+ .tl .txt{font-size:11.5px;color:var(--tx);margin-top:3px;opacity:.9;white-space:pre-wrap;word-break:break-word;display:-webkit-box;-webkit-line-clamp:3;line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
158
+ .tl time{color:var(--dim);font:10px ui-monospace;margin-left:auto}
159
+ .tl-empty{color:var(--dim);font-size:12px;border:1px dashed var(--line);border-radius:11px;padding:18px 14px;text-align:center}
160
+ @media(max-width:1180px){.wrap{grid-template-columns:1fr}.tiles{grid-template-columns:repeat(3,1fr)}.roster,.timeline{max-height:none}.stage{height:70vh}}
161
+ </style>
162
+ <link rel="stylesheet" href="app.css">
163
+ <script src="app.js" defer></script>
164
+ </head>
165
+ <body>
166
+ <header>
167
+ <h1><span class="brand">CrewCtl</span> <span class="sep">·</span> Orkestrasyon Merkezi</h1>
168
+ <span class="pill"><span id="dot" class="dot"></span><span id="engineState">durdu</span></span>
169
+ <span id="modePill" class="pill">boşta</span>
170
+ <div class="grow"></div>
171
+ <button id="helpBtn" title="Bu sistem nedir?"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9.2"/><path d="M12 11v5M12 7.6h.01"/></svg></button>
172
+ <button id="themeBtn" onclick="toggleTheme()" title="Açık / koyu tema"><svg class="i i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><svg class="i i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg></button>
173
+ <a class="btn" href="index.html"><button>← Komuta Merkezi</button></a>
174
+ </header>
175
+
176
+ <div class="topbar">
177
+ <div class="banner">
178
+ <div>
179
+ <div class="kick" id="mKick">Görev bekleniyor</div>
180
+ <div class="t" id="mTitle">Takım görevi bekleniyor</div>
181
+ <div class="s" id="mSub">Operatör bir hedef aldığında ekibini burada canlı olarak kuracak.</div>
182
+ </div>
183
+ <div class="tiles">
184
+ <div class="tile op"><div class="k">Operatör</div><div class="v" id="kOp">—</div></div>
185
+ <div class="tile"><div class="k">Tur</div><div class="v" id="kRound">0</div></div>
186
+ <div class="tile"><div class="k">Delegasyon</div><div class="v" id="kDeleg">0</div></div>
187
+ <div class="tile"><div class="k">Aktif ajan</div><div class="v" id="kActive">0</div></div>
188
+ <div class="tile"><div class="k">Dosya</div><div class="v" id="kFiles">0</div></div>
189
+ <div class="tile"><div class="k">Çağrı</div><div class="v" id="kCalls">0</div></div>
190
+ </div>
191
+ </div>
192
+ </div>
193
+
194
+ <div class="wrap">
195
+ <aside class="card">
196
+ <div class="card-h"><h2>Ajan Filosu</h2><span class="count" id="rosterCount">0 ajan</span></div>
197
+ <div class="card-b"><div class="roster" id="roster"><div class="tl-empty">Ajanlar yükleniyor…</div></div>
198
+ <a class="btn" href="index.html" style="display:block;margin-top:10px"><button style="width:100%">⚙ Ajanları yönet</button></a>
199
+ </div>
200
+ </aside>
201
+
202
+ <section class="stage" id="stage">
203
+ <canvas id="scene"></canvas>
204
+ <div class="scene-loader" id="sceneLoader">
205
+ <div class="orbit-spinner"><span class="sun"></span><span class="ring o1"><span class="planet"></span></span><span class="ring o2"><span class="planet"></span></span><span class="ring o3"><span class="planet"></span></span></div>
206
+ <div class="lt">3B ekip evreni hazırlanıyor…</div>
207
+ </div>
208
+ <div class="hud hud-tl">
209
+ <div class="hud-op"><span class="hud-lbl">OPERATÖR</span><span id="opName">—</span></div>
210
+ <div class="op-phase" id="opState">bir görev bekleniyor</div>
211
+ </div>
212
+ <div class="hud hud-tr">
213
+ <button id="autoRotBtn" class="mini" title="Otomatik döndürme">⏸ Döndür</button>
214
+ <button id="camResetBtn" class="mini" title="Kamerayı sıfırla">⟲ Kamera</button>
215
+ </div>
216
+ <div class="scene-legend">
217
+ <span><i class="lg-dot" style="color:#2fd08a;background:#2fd08a"></i>Çalışıyor</span>
218
+ <span><i class="lg-dot" style="color:#5e8bff;background:#5e8bff"></i>Tamamlandı</span>
219
+ <span><i class="lg-dot" style="color:#f2565f;background:#f2565f"></i>Sorun</span>
220
+ <span><i class="lg-dot" style="color:#8494ab;background:#8494ab"></i>Beklemede</span>
221
+ </div>
222
+ <div class="scene-hint">Sürükle → döndür · tekerlek → yakınlaş · ajana tıkla → detay</div>
223
+ <div class="agent-panel" id="agentPanel" hidden></div>
224
+ </section>
225
+
226
+ <aside class="card">
227
+ <div class="card-h"><h2>Zaman Çizelgesi</h2><span class="count" id="tlCount">sıralı</span></div>
228
+ <div class="card-b"><div class="timeline" id="timeline"><div class="tl-empty">Bir görev başlayınca operatörün her adımı sırayla burada listelenir.</div></div></div>
229
+ </aside>
230
+ </div>
231
+
232
+ <div class="help-overlay" id="helpModal" hidden>
233
+ <div class="help-card">
234
+ <div class="hc-head"><span class="ic">🛰️</span><h2><span class="brand">CrewCtl</span> Ekip Akışı nedir?</h2><button class="hc-close" id="helpClose" title="Kapat">✕</button></div>
235
+ <div class="hc-body">
236
+ <p>CrewCtl, kurulu CLI kodlama ajanlarınızı (Codex, Claude, Gemini, OpenCode) <b>tek bir operatör‑liderliğindeki takım</b> gibi çalıştırır. Bu sayfa o takımın canlı çalışmasını 3B bir sahnede gösterir.</p>
237
+ <div class="hc-row"><div><div class="hc-t">Operatör</div><div class="hc-d">Merkezdeki çekirdek. Hedefi analiz eder, işi alt görevlere böler, doğru uzmana delege eder ve sonuçları değerlendirir.</div></div></div>
238
+ <div class="hc-row"><div><div class="hc-t">Uzman ajanlar</div><div class="hc-d">Operatörün çevresindeki her düğüm bir CLI uzmanıdır; rengi hangi CLI olduğunu gösterir. Birine tıklayınca o an ne yaptığını, canlı çıktısını ve durumunu görürsünüz.</div></div></div>
239
+ <div class="hc-row"><div><div class="hc-t">Delegasyon akışı</div><div class="hc-d">Operatörden uzmana giden hüzme bir görev delegasyonu, geri dönen hüzme ise teslimattır.</div></div></div>
240
+ <div class="hc-row"><div><div class="hc-t">Durum renkleri</div><div class="hc-d"><b style="color:#2fd08a">Yeşil</b> çalışıyor · <b style="color:#5e8bff">Mavi</b> tamamlandı · <b style="color:#f2565f">Kırmızı</b> sorun · <b style="color:#8494ab">Gri</b> beklemede.</div></div></div>
241
+ <div class="hc-row"><div><div class="hc-t">Kontroller</div><div class="hc-d">Sahneyi döndürüp yakınlaşabilirsiniz. Soldaki filo ve sağdaki zaman çizelgesi 3B sahne ile bağlıdır — birine tıklamak ilgili gezegeni seçer.</div>
242
+ <div class="hc-keys"><kbd>Sürükle</kbd> döndür <kbd>Tekerlek</kbd> yakınlaş <kbd>Tıkla</kbd> detay</div></div></div>
243
+ <p style="margin:14px 0 0;color:var(--dim);font-size:12px">Bir görev başlatmak için <a href="index.html" style="color:var(--acc)">Komuta Merkezi</a>'ne dönüp <b>▶ Başlat</b>'a basın.</p>
244
+ </div>
245
+ </div>
246
+ </div>
247
+
248
+ <script type="importmap">
249
+ {"imports":{"three":"./three.module.min.js"}}
250
+ </script>
251
+ <script type="module">
252
+ import * as THREE from 'three';
253
+ import { OrbitControls } from './OrbitControls.js';
254
+ import { EffectComposer } from './jsm/postprocessing/EffectComposer.js';
255
+ import { RenderPass } from './jsm/postprocessing/RenderPass.js';
256
+ import { UnrealBloomPass } from './jsm/postprocessing/UnrealBloomPass.js';
257
+
258
+ const reduceMotion=matchMedia('(prefers-reduced-motion:reduce)').matches;
259
+ const $=s=>document.querySelector(s);
260
+ const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
261
+ const time=at=>new Date(at||Date.now()).toLocaleTimeString();
262
+ const isOpStage=s=>String(s||'').startsWith('operator');
263
+ const CLI_META={codex:{label:'Codex',c:'#10a37f'},claude:{label:'Claude',c:'#d97757'},gemini:{label:'Gemini',c:'#4285f4'},opencode:{label:'OpenCode',c:'#0ea5e9'},custom:{label:'CLI',c:'#8b98b0'}};
264
+ function adapterOf(a){const b=String(a?.adapter||a?.cmd||a||'').toLowerCase();return ['codex','claude','gemini','opencode'].find(x=>b.includes(x))||'custom'}
265
+ const cliLabel=id=>(CLI_META[id]||CLI_META.custom).label;
266
+ const cliColor=id=>(CLI_META[id]||CLI_META.custom).c;
267
+ function cssId(s){return String(s).replace(/[^a-z0-9_-]/gi,'_')}
268
+ function applyTheme(t){document.documentElement.setAttribute('data-theme',t);try{localStorage.setItem('cliTheme',t)}catch{};Scene.setTheme?.(t)}
269
+ window.toggleTheme=function(){applyTheme(document.documentElement.getAttribute('data-theme')==='light'?'dark':'light')}
270
+
271
+ /* ============================================================
272
+ 3B SAHNE — gerçek WebGL/Three.js ajan görselleştirmesi
273
+ Her ajan bağımsız bir 3B model. Operatör merkezde çekirdek.
274
+ Tıklanabilir (raycast), durum renkleriyle canlı güncellenir.
275
+ ============================================================ */
276
+ const Scene=(()=>{
277
+ let renderer,composer,bloom,scene,camera,controls,raycaster,pointer,clock,rafId,ok=false;
278
+ let operator,opGlow,opRings=[],ground,particles,fog,orbitPlane;
279
+ const orbitRings=[];
280
+ const PLANET_SCALE=.66; // gezegenler operatöre göre küçük dursun
281
+ let selectionRing,hovered=null,selected=null,onSelect=null;
282
+ const agents=new Map(); // name -> agent obj
283
+ const pickables=[]; // {mesh,name}
284
+ const beams=new Map(); // name -> beam obj
285
+ const pulses=[]; // aktif akış darbeleri
286
+ const ripples=[]; // varış/çarpma dalgaları
287
+ const tmp=new THREE.Vector3();
288
+ const HOME={target:new THREE.Vector3(0,1.4,0),dist:20};
289
+ const focus={target:new THREE.Vector3(0,1.4,0),dist:20,active:false};
290
+ const STAT={
291
+ working:{col:0x2fd08a,emis:1.25,ring:2.4,bob:.16},
292
+ done:{col:0x5e8bff,emis:.8,ring:.5,bob:.06},
293
+ failed:{col:0xf2565f,emis:.95,ring:.2,bob:.03},
294
+ idle:{col:0x8494ab,emis:.5,ring:.7,bob:.09}
295
+ };
296
+ // paylaşılan geometriler (bir kez)
297
+ let G={},glowTex;
298
+
299
+ function radialTexture(){const c=document.createElement('canvas');c.width=c.height=128;const x=c.getContext('2d');const g=x.createRadialGradient(64,64,0,64,64,64);g.addColorStop(0,'#ffffff');g.addColorStop(.25,'#ffffffcc');g.addColorStop(1,'#ffffff00');x.fillStyle=g;x.fillRect(0,0,128,128);const t=new THREE.CanvasTexture(c);return t}
300
+ function labelSprite(text,hex){
301
+ const pad=26,fs=44,f=`800 ${fs}px Inter,Segoe UI,sans-serif`;
302
+ const m=document.createElement('canvas').getContext('2d');m.font=f;const w=Math.ceil(m.measureText(text).width)+pad*2,h=fs+pad;
303
+ const c=document.createElement('canvas');c.width=w;c.height=h;const x=c.getContext('2d');
304
+ x.font=f;x.textBaseline='middle';x.textAlign='center';
305
+ const r=18;x.fillStyle='rgba(9,14,26,.82)';x.strokeStyle=hex;x.lineWidth=3;
306
+ x.beginPath();x.moveTo(r,3);x.arcTo(w-3,3,w-3,h-3,r);x.arcTo(w-3,h-3,3,h-3,r);x.arcTo(3,h-3,3,3,r);x.arcTo(3,3,w-3,3,r);x.closePath();x.fill();x.stroke();
307
+ x.fillStyle='#fff';x.fillText(text,w/2,h/2+1);
308
+ const t=new THREE.CanvasTexture(c);t.anisotropy=4;
309
+ const sp=new THREE.Sprite(new THREE.SpriteMaterial({map:t,transparent:true,depthTest:false}));
310
+ sp.scale.set(w/h*.62,.62,1);sp.renderOrder=10;return sp;
311
+ }
312
+
313
+ // GEZEGEN — operatör (güneş) ailesinden ama farklı: simetrik küre gövde,
314
+ // kendi eğik halkası (Satürn gibi) ve telkafes kabuk; renk CLI'ye göre, durumla parlar.
315
+ function buildAgentModel(hex){
316
+ const col=new THREE.Color(hex),g=new THREE.Group();
317
+ // iç çekirdek (koyu, derinlik hissi)
318
+ const body=new THREE.Mesh(G.coreInner,new THREE.MeshStandardMaterial({color:0x16233b,metalness:.7,roughness:.4}));g.add(body);
319
+ // gezegen yüzeyi — durum rengiyle parlayan simetrik küre (ana gövde)
320
+ const visor=new THREE.Mesh(G.planet,new THREE.MeshStandardMaterial({color:col,emissive:col,emissiveIntensity:1.3,metalness:.35,roughness:.35,toneMapped:false}));g.add(visor);
321
+ // telkafes kabuk (dönen)
322
+ const shell=new THREE.LineSegments(G.shell,new THREE.LineBasicMaterial({color:col,transparent:true,opacity:.6}));g.add(shell);
323
+ // Satürn benzeri eğik gezegen halkası — ana ayırt edici öğe
324
+ const tip=new THREE.Mesh(G.ring,new THREE.MeshBasicMaterial({color:col,transparent:true,opacity:.85,side:THREE.DoubleSide,toneMapped:false,blending:THREE.AdditiveBlending,depthWrite:false}));
325
+ tip.rotation.set(Math.PI*.5,0,Math.PI*.16);g.add(tip);
326
+ // ikinci ince halka (çapraz eksende — jiroskop hissi)
327
+ const halo=new THREE.Mesh(G.halo,new THREE.MeshBasicMaterial({color:col,transparent:true,opacity:.5,side:THREE.DoubleSide,toneMapped:false}));
328
+ halo.rotation.set(Math.PI*.5,Math.PI*.5,0);g.add(halo);
329
+ // yumuşak dış parıltı (atmosfer/korona hissi)
330
+ const glow=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex,color:col,transparent:true,opacity:.34,blending:THREE.AdditiveBlending,depthWrite:false}));
331
+ glow.scale.set(2.7,2.7,1);g.add(glow);
332
+ // görünmez tıklama küresi
333
+ const pick=new THREE.Mesh(G.pick,new THREE.MeshBasicMaterial({visible:false}));g.add(pick);
334
+ return {g,body,shell,visor,halo,tip,glow,pick};
335
+ }
336
+
337
+ function buildOperator(){
338
+ const grp=new THREE.Group();grp.position.y=1.4;
339
+ const col=new THREE.Color(0xa98bff);
340
+ const core=new THREE.Mesh(new THREE.IcosahedronGeometry(.9,1),new THREE.MeshStandardMaterial({color:0x2a2060,emissive:col,emissiveIntensity:1.1,metalness:.6,roughness:.25,flatShading:true,toneMapped:false}));
341
+ grp.add(core);grp.userData.core=core;
342
+ const wire=new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.IcosahedronGeometry(1.24,1)),new THREE.LineBasicMaterial({color:col,transparent:true,opacity:.5}));
343
+ grp.add(wire);grp.userData.wire=wire;
344
+ for(let i=0;i<3;i++){const r=new THREE.Mesh(new THREE.TorusGeometry(1.5+i*.14,.02,10,90),new THREE.MeshBasicMaterial({color:col,transparent:true,opacity:.55-i*.12,toneMapped:false}));r.rotation.x=Math.PI*(.2+i*.28);r.rotation.y=i*.6;opRings.push(r);grp.add(r)}
345
+ opGlow=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex,color:col,transparent:true,opacity:.55,blending:THREE.AdditiveBlending,depthWrite:false}));
346
+ opGlow.scale.set(6.2,6.2,1);grp.add(opGlow);
347
+ // korona — güneş hissi için geniş, soluk dış hale
348
+ const corona=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex,color:0xc3b6e8,transparent:true,opacity:.11,blending:THREE.AdditiveBlending,depthWrite:false}));
349
+ corona.scale.set(12,12,1);grp.add(corona);
350
+ // güneşin ışığı gezegenleri aydınlatır — daha soluk ve daha az mor (nötr)
351
+ const light=new THREE.PointLight(0xc4cbe6,1.1,42,1.8);light.position.set(0,0,0);grp.add(light);grp.userData.light=light;
352
+ grp.add(labelSprite('OPERATÖR','#a98bff').translateY(2.2));
353
+ return grp;
354
+ }
355
+
356
+ function layout(){
357
+ const list=[...agents.values()],n=list.length;
358
+ // eski yörünge çizgilerini temizle
359
+ for(const r of orbitRings){orbitPlane.remove(r);r.geometry.dispose();r.material.dispose()}orbitRings.length=0;
360
+ if(!n)return;
361
+ list.forEach((a,i)=>{
362
+ a.orbitR=3.9+i*1.15; // operatöre orta mesafe, yörüngeler daha sık
363
+ a.orbitAngle=(i/n)*Math.PI*2; // eşit dağılmış sabit konum
364
+ a.orbitSpeed=0; // gezegen kendi başına dönmez
365
+ a.orbitY=1.4;a.phase=i*1.3;
366
+ a.g.position.set(Math.cos(a.orbitAngle)*a.orbitR,a.orbitY,Math.sin(a.orbitAngle)*a.orbitR);
367
+ // yörünge çizgisi (güneşin çevresinde ince halka)
368
+ const ring=new THREE.Mesh(new THREE.RingGeometry(a.orbitR-.012,a.orbitR+.012,180),new THREE.MeshBasicMaterial({color:a.color,transparent:true,opacity:.22,side:THREE.DoubleSide,toneMapped:false}));
369
+ ring.rotation.x=-Math.PI/2;ring.position.y=a.orbitY;orbitPlane.add(ring);orbitRings.push(ring);
370
+ });
371
+ }
372
+
373
+ function init(canvas){
374
+ renderer=new THREE.WebGLRenderer({canvas,antialias:true});
375
+ renderer.setPixelRatio(Math.min(devicePixelRatio,2));
376
+ scene=new THREE.Scene();
377
+ scene.background=new THREE.Color(0x0b1224); // bloom opak pipeline'da uzay zemini
378
+ fog=new THREE.FogExp2(0x14203a,.013);scene.fog=fog;
379
+ camera=new THREE.PerspectiveCamera(52,1,.1,200);camera.position.set(0,9.5,18);
380
+ controls=new OrbitControls(camera,canvas);
381
+ controls.enableDamping=true;controls.dampingFactor=.1;controls.rotateSpeed=.95;controls.zoomSpeed=1.1;controls.enablePan=false;
382
+ controls.minDistance=6;controls.maxDistance=44;controls.maxPolarAngle=1.5;
383
+ controls.target.copy(HOME.target);controls.autoRotate=!reduceMotion;controls.autoRotateSpeed=.5;
384
+ controls.update();controls.saveState();
385
+ // ışıklar — sahne belirgin biçimde aydınlık
386
+ scene.add(new THREE.AmbientLight(0x6c82a6,1.35));
387
+ scene.add(new THREE.HemisphereLight(0xcfe0ff,0x2a3550,1.15));
388
+ const key=new THREE.DirectionalLight(0xffffff,1.75);key.position.set(6,13,9);scene.add(key);
389
+ const rim=new THREE.DirectionalLight(0x8fb4ff,1.15);rim.position.set(-9,5,-7);scene.add(rim);
390
+ const fill=new THREE.DirectionalLight(0xbcd0ff,.85);fill.position.set(0,4,12);scene.add(fill);
391
+ glowTex=radialTexture();
392
+ // Güneş sistemi — zemin yok; gezegenler güneşin çevresinde yörüngede döner.
393
+ orbitPlane=new THREE.Group();scene.add(orbitPlane); // yörünge çizgileri layout()'ta eklenir
394
+ // seçim halkası — seçili gezegenin çevresinde belirir (animate konumlandırır)
395
+ selectionRing=new THREE.Mesh(new THREE.RingGeometry(.86,1.02,56),new THREE.MeshBasicMaterial({color:0xffffff,transparent:true,opacity:.9,side:THREE.DoubleSide,toneMapped:false}));
396
+ selectionRing.rotation.x=-Math.PI/2;selectionRing.visible=false;scene.add(selectionRing);
397
+ // yıldız alanı — derin uzay
398
+ const pc=900,pos=new Float32Array(pc*3);
399
+ for(let i=0;i<pc;i++){const r=15+Math.random()*42,th=Math.random()*Math.PI*2,ph=(Math.random()-.5)*Math.PI*.75;pos[i*3]=Math.cos(th)*Math.cos(ph)*r;pos[i*3+1]=Math.sin(ph)*r*.6+2;pos[i*3+2]=Math.sin(th)*Math.cos(ph)*r}
400
+ const pg=new THREE.BufferGeometry();pg.setAttribute('position',new THREE.BufferAttribute(pos,3));
401
+ particles=new THREE.Points(pg,new THREE.PointsMaterial({color:0xbcd0ff,size:.09,transparent:true,opacity:.7,depthWrite:false}));scene.add(particles);
402
+ // paylaşılan geometriler
403
+ G={planet:new THREE.IcosahedronGeometry(.5,4),coreInner:new THREE.IcosahedronGeometry(.36,2),
404
+ shell:new THREE.EdgesGeometry(new THREE.IcosahedronGeometry(.63,1)),
405
+ ring:new THREE.RingGeometry(.86,.95,72),halo:new THREE.RingGeometry(.58,.63,60),
406
+ pick:new THREE.SphereGeometry(1.2,12,12),packet:new THREE.SphereGeometry(.075,10,10),arrow:new THREE.ConeGeometry(.13,.42,14)};
407
+ operator=buildOperator();scene.add(operator);
408
+ // Bloom — emissive güneş/gezegenler gerçekten parlar
409
+ composer=new EffectComposer(renderer);
410
+ composer.addPass(new RenderPass(scene,camera));
411
+ bloom=new UnrealBloomPass(new THREE.Vector2(1,1),.42,.5,.5); // güç düşük, eşik yüksek → göz yormaz
412
+ composer.addPass(bloom);
413
+ raycaster=new THREE.Raycaster();pointer=new THREE.Vector2();clock=new THREE.Clock();
414
+ canvas.addEventListener('pointermove',onPointerMove);
415
+ canvas.addEventListener('pointerdown',onPointerDown);
416
+ new ResizeObserver(resize).observe(canvas.parentElement);resize();
417
+ ok=true;animate();
418
+ }
419
+
420
+ function resize(){if(!renderer)return;const el=renderer.domElement.parentElement,w=el.clientWidth,h=el.clientHeight;if(!w||!h)return;renderer.setSize(w,h,false);composer.setSize(w,h);bloom.setSize(w,h);camera.aspect=w/h;camera.updateProjectionMatrix()}
421
+
422
+ // Not: G geometrileri ve glowTex paylaşımlıdır — yalnızca ajana özel materyalleri
423
+ // ve benzersiz etiket dokusunu serbest bırak (paylaşımlı kaynaklara dokunma).
424
+ function disposeAgent(a){scene.remove(a.g);a.g.traverse(o=>{if(o.material)(Array.isArray(o.material)?o.material:[o.material]).forEach(m=>m.dispose())});if(a.label&&a.label.material.map)a.label.material.map.dispose()}
425
+ function setAgents(list){
426
+ if(!scene)return;
427
+ for(const name of [...beams.keys()])setBeam(name,false);
428
+ for(const a of agents.values())disposeAgent(a);agents.clear();pickables.length=0;
429
+ for(const it of list){
430
+ const m=buildAgentModel(it.color);
431
+ m.g.userData.name=it.name;m.pick.userData.name=it.name;m.g.scale.setScalar(PLANET_SCALE);
432
+ const label=labelSprite(it.name,it.color);label.position.y=2.2;label.material.opacity=.0;m.g.add(label);
433
+ const a={name:it.name,color:new THREE.Color(it.color),...m,label,status:'idle',tgt:STAT.idle,flare:0};
434
+ scene.add(m.g);agents.set(it.name,a);pickables.push(m.pick);
435
+ }
436
+ layout();
437
+ }
438
+ function updateAgent(name,status){const a=agents.get(name);if(!a)return;a.status=status;a.tgt=STAT[status]||STAT.idle}
439
+ function setBeam(name,active){
440
+ if(!ok)return;
441
+ if(active){if(beams.has(name))return;const a=agents.get(name);if(!a)return;
442
+ const n=32,arr=new Float32Array(n*3);const geo=new THREE.BufferGeometry();geo.setAttribute('position',new THREE.BufferAttribute(arr,3));
443
+ const line=new THREE.Line(geo,new THREE.LineBasicMaterial({color:a.color,transparent:true,opacity:.72,blending:THREE.AdditiveBlending,toneMapped:false,depthWrite:false}));
444
+ const arrow=new THREE.Mesh(G.arrow,new THREE.MeshBasicMaterial({color:a.color,transparent:true,opacity:.92,toneMapped:false,blending:THREE.AdditiveBlending,depthWrite:false}));
445
+ const packets=[];for(let i=0;i<3;i++){const packet=new THREE.Mesh(G.packet,new THREE.MeshBasicMaterial({color:a.color,transparent:true,opacity:.95,toneMapped:false,blending:THREE.AdditiveBlending,depthWrite:false}));
446
+ const glow=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex,color:a.color,transparent:true,opacity:.65,blending:THREE.AdditiveBlending,depthWrite:false}));glow.scale.set(.7,.7,1);packet.add(glow);packets.push(packet);scene.add(packet)}
447
+ scene.add(line);scene.add(arrow);beams.set(name,{line,arrow,packets,arr,n,curve:null});updateBeam(name);
448
+ }else{const b=beams.get(name);if(b){scene.remove(b.line);scene.remove(b.arrow);for(const packet of b.packets){scene.remove(packet);packet.traverse(o=>{if(o.material)o.material.dispose()})}b.line.geometry.dispose();b.line.material.dispose();b.arrow.material.dispose();beams.delete(name)}}
449
+ }
450
+ function updateBeam(name){const b=beams.get(name),a=agents.get(name);if(!b||!a)return;const curve=beamCurve(a);
451
+ for(let i=0;i<b.n;i++){const p=curve.getPoint(i/(b.n-1));b.arr[i*3]=p.x;b.arr[i*3+1]=p.y;b.arr[i*3+2]=p.z}
452
+ b.line.geometry.attributes.position.needsUpdate=true;b.line.geometry.computeBoundingSphere();b.curve=curve;
453
+ const u=.93,pos=curve.getPoint(u),tangent=curve.getTangent(u).normalize();b.arrow.position.copy(pos);b.arrow.quaternion.setFromUnitVectors(tmp.set(0,1,0),tangent)}
454
+ function beamCurve(a){const start=operator.position.clone(),end=a.g.position.clone();const mid=start.clone().lerp(end,.5);mid.y+=1.4;return new THREE.QuadraticBezierCurve3(start,mid,end)}
455
+ function pulse(name,dir){if(!ok)return;const a=agents.get(name);if(!a)return;const curve=beamCurve(a);
456
+ const col=dir==='down'?a.color:0xa98bff;
457
+ // izli enerji paketi: parlak çekirdek + hâle + arkada solan iz çizgisi
458
+ const m=new THREE.Mesh(new THREE.SphereGeometry(.15,14,14),new THREE.MeshBasicMaterial({color:col,toneMapped:false}));
459
+ const gl=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex,color:col,transparent:true,opacity:.9,blending:THREE.AdditiveBlending,depthWrite:false}));gl.scale.set(1.3,1.3,1);m.add(gl);
460
+ const tn=14,tarr=new Float32Array(tn*3);const tg=new THREE.BufferGeometry();tg.setAttribute('position',new THREE.BufferAttribute(tarr,3));
461
+ const s0=curve.getPoint(dir==='up'?1:0);for(let k=0;k<tn;k++){tarr[k*3]=s0.x;tarr[k*3+1]=s0.y;tarr[k*3+2]=s0.z}
462
+ const trail=new THREE.Line(tg,new THREE.LineBasicMaterial({color:col,transparent:true,opacity:.55,blending:THREE.AdditiveBlending,toneMapped:false,depthWrite:false}));trail.frustumCulled=false;
463
+ scene.add(m);scene.add(trail);pulses.push({m,trail,tarr,tn,curve,dir,t:0,col,name});
464
+ }
465
+ function spawnRipple(pos,col){const geo=new THREE.RingGeometry(.2,.34,40);const mesh=new THREE.Mesh(geo,new THREE.MeshBasicMaterial({color:col,transparent:true,opacity:.85,side:THREE.DoubleSide,toneMapped:false,blending:THREE.AdditiveBlending,depthWrite:false}));
466
+ mesh.position.copy(pos);mesh.rotation.x=-Math.PI/2;scene.add(mesh);ripples.push({mesh,t:0})}
467
+ function setOperator(label){/* operatör kimliği HUD'da gösterilir; 3B etiket sabittir */}
468
+ function setOperatorPhase(active){if(operator)operator.userData.active=!!active}
469
+ function reset(){for(const n of [...beams.keys()])setBeam(n,false);for(const p of pulses){scene.remove(p.m);scene.remove(p.trail)}pulses.length=0;for(const r of ripples)scene.remove(r.mesh);ripples.length=0;for(const a of agents.values())updateAgent(a.name,'idle')}
470
+ // Uzay her iki temada da koyu kalır (light temada beyaza yıkanıp görünmez olmasın).
471
+ // Işık temada bir tık daha açık lacivert kullanıyoruz ama gezegenler net görünür.
472
+ function setTheme(t){if(!scene)return;const dark=t!=='light';fog.color.set(dark?0x14203a:0x18233d);fog.density=dark?.012:.014;if(scene.background)scene.background.set(dark?0x0b1224:0x111b30);if(ground)ground.material.color.set(dark?0x1a2740:0x22304c)}
473
+
474
+ // Sinematik odak — seçilen gezegene yumuşak kayış (animate uygular)
475
+ function focusOnAgent(a){if(!controls)return;if(a){focus.target.copy(a.g.position);focus.dist=6.5;focus.active=true;controls.autoRotate=false}
476
+ else{focus.target.copy(HOME.target);focus.dist=HOME.dist;focus.active=false;controls.autoRotate=!reduceMotion}syncAutoBtn()}
477
+ function select(name){
478
+ if(selected){const p=agents.get(selected);if(p)p.label.material.opacity=0}
479
+ selected=name;const a=agents.get(name);
480
+ if(a){a.label.material.opacity=1;focusOnAgent(a)}
481
+ else{if(selectionRing)selectionRing.visible=false;focusOnAgent(null)}
482
+ onSelect&&onSelect(name);
483
+ }
484
+ function deselect(){if(selected){const p=agents.get(selected);if(p)p.label.material.opacity=0}selected=null;if(selectionRing)selectionRing.visible=false;focusOnAgent(null);onSelect&&onSelect(null)}
485
+
486
+ function onPointerMove(e){const r=renderer.domElement.getBoundingClientRect();pointer.x=((e.clientX-r.left)/r.width)*2-1;pointer.y=-((e.clientY-r.top)/r.height)*2+1;
487
+ raycaster.setFromCamera(pointer,camera);const hit=raycaster.intersectObjects(pickables,false)[0];
488
+ const name=hit?hit.object.userData.name:null;hovered=name;renderer.domElement.style.cursor=name?'pointer':'grab';
489
+ }
490
+ let downXY=null;
491
+ function onPointerDown(e){downXY=[e.clientX,e.clientY]}
492
+ renderer_click_guard();
493
+ function renderer_click_guard(){document.addEventListener('pointerup',e=>{if(!downXY||!renderer)return;const moved=Math.hypot(e.clientX-downXY[0],e.clientY-downXY[1]);downXY=null;if(moved>6)return;
494
+ const r=renderer.domElement.getBoundingClientRect();if(e.clientX<r.left||e.clientX>r.right||e.clientY<r.top||e.clientY>r.bottom)return;
495
+ pointer.x=((e.clientX-r.left)/r.width)*2-1;pointer.y=-((e.clientY-r.top)/r.height)*2+1;raycaster.setFromCamera(pointer,camera);
496
+ const hit=raycaster.intersectObjects(pickables,false)[0];if(hit)select(hit.object.userData.name);else deselect();
497
+ })}
498
+
499
+ function animate(){rafId=requestAnimationFrame(animate);const dt=Math.min(clock.getDelta(),.05),t=clock.elapsedTime;const anim=reduceMotion?0:1;
500
+ // operatör
501
+ if(operator){const act=operator.userData.active;operator.userData.core.rotation.y+=dt*.5*anim;operator.userData.core.rotation.x+=dt*.22*anim;
502
+ opRings.forEach((r,i)=>{r.rotation.z+=dt*(.3+i*.25)*(act?2:1)*anim;r.rotation.y+=dt*.15*anim});
503
+ const pu=(Math.sin(t*(act?4:1.6))*.5+.5)*anim;operator.userData.core.material.emissiveIntensity=(act?.8:.5)+pu*.25;
504
+ operator.userData.light.intensity=(act?1.1:.7)+pu*.25;opGlow.material.opacity=.17+pu*.1;}
505
+ // ajanlar
506
+ for(const a of agents.values()){
507
+ const tg=a.tgt;
508
+ // renk her zaman ajanın kendi rengi (sabit); durum + varış "flare"ı parlaklıkla belli olur
509
+ const breathe=(Math.sin(t*1.8+a.phase)*.5+.5)*anim; // 0..1 yumuşak nefes
510
+ const workBoost=a.status==='working'?(Math.sin(t*3.5+a.phase)*.5+.5)*anim:0;
511
+ if(a.flare>0)a.flare=Math.max(0,a.flare-dt*1.6); // varış parlaması söner
512
+ a.visor.material.emissiveIntensity+=((tg.emis+breathe*.14+workBoost*.35+a.flare*1.3)-a.visor.material.emissiveIntensity)*.14;
513
+ a.glow.material.opacity=.1+breathe*.1+workBoost*.22+a.flare*.4; // operatörden belirgin az
514
+ a.glow.scale.setScalar(2.1+breathe*.22+workBoost*.4+a.flare*1);
515
+ // konum sabit (yörünge hızı 0) + hafif süzülme; sistem bütün olarak kamerayla döner
516
+ a.g.position.set(Math.cos(a.orbitAngle)*a.orbitR, a.orbitY+Math.sin(t*1.2+a.phase)*.07*anim, Math.sin(a.orbitAngle)*a.orbitR);
517
+ const want=PLANET_SCALE*((hovered===a.name||selected===a.name)?1.16:1);a.g.scale.lerp(tmp.set(want,want,want),.15);
518
+ }
519
+ // seçim halkası ajanı takip eder
520
+ if(selected&&agents.has(selected)){const a=agents.get(selected);selectionRing.visible=true;selectionRing.position.set(a.g.position.x,a.orbitY,a.g.position.z);selectionRing.material.color.copy(a.color);selectionRing.rotation.z+=dt*.6*anim;selectionRing.material.opacity=.6+Math.sin(t*3)*.25*anim}
521
+ else selectionRing.visible=false;
522
+ // sinematik odak — yalnızca geçiş anında; hedefe varınca kontrolü kullanıcıya bırak
523
+ if(focus.active){controls.target.lerp(focus.target,.08);
524
+ const dir=tmp.copy(camera.position).sub(controls.target);const curD=dir.length()||.001;const wantD=curD+(focus.dist-curD)*.08;
525
+ dir.setLength(wantD);camera.position.copy(controls.target).add(dir);
526
+ if(controls.target.distanceTo(focus.target)<.06&&Math.abs(curD-focus.dist)<.12)focus.active=false;}
527
+ // akış darbeleri (izli enerji paketi + varışta çarpma dalgası)
528
+ for(let i=pulses.length-1;i>=0;i--){const p=pulses[i];p.t+=dt/.85;const tt=p.dir==='up'?1-Math.min(p.t,1):Math.min(p.t,1);
529
+ const pt=p.curve.getPoint(tt);p.m.position.copy(pt);
530
+ for(let k=p.tn-1;k>0;k--){p.tarr[k*3]=p.tarr[(k-1)*3];p.tarr[k*3+1]=p.tarr[(k-1)*3+1];p.tarr[k*3+2]=p.tarr[(k-1)*3+2]}
531
+ p.tarr[0]=pt.x;p.tarr[1]=pt.y;p.tarr[2]=pt.z;p.trail.geometry.attributes.position.needsUpdate=true;
532
+ if(p.t>=1){spawnRipple(pt,p.col);if(p.dir==='down'){const a=agents.get(p.name);if(a)a.flare=1}
533
+ scene.remove(p.m);scene.remove(p.trail);p.trail.geometry.dispose();pulses.splice(i,1)}}
534
+ // çarpma dalgaları — genişleyip söner
535
+ for(let i=ripples.length-1;i>=0;i--){const r=ripples[i];r.t+=dt/.7;const s=.2+r.t*2.4;r.mesh.scale.set(s,s,s);r.mesh.material.opacity=.85*(1-r.t);if(r.t>=1){scene.remove(r.mesh);r.mesh.geometry.dispose();ripples.splice(i,1)}}
536
+ // delegasyon bağları — hat, ok ucu ve veri paketleri gezegen hareketini takip eder.
537
+ // Paketler her zaman operatörden hedef agente akar; renk hedef agentın CLI rengidir.
538
+ for(const [name,b] of beams){updateBeam(name);b.line.material.opacity=.5+Math.sin(t*5)*.18*anim;b.arrow.material.opacity=.72+Math.sin(t*4)*.2*anim;
539
+ for(let i=0;i<b.packets.length;i++){const u=((reduceMotion?0:t*.28)+i/b.packets.length)%1,packet=b.packets[i],pt=b.curve.getPoint(u);packet.position.copy(pt);const fade=Math.sin(Math.PI*u);packet.material.opacity=.35+fade*.65;packet.scale.setScalar(.75+fade*.55)}}
540
+ // parçacıklar
541
+ if(particles)particles.rotation.y+=dt*.02*anim;
542
+ controls.update();composer.render();
543
+ }
544
+
545
+ function syncAutoBtn(){const b=$('#autoRotBtn');if(!b||!controls)return;b.textContent=controls.autoRotate?'⏸ Döndür':'▶ Döndür'}
546
+ function toggleAuto(){if(!controls)return;controls.autoRotate=!controls.autoRotate;syncAutoBtn()}
547
+ function camReset(){if(!controls)return;controls.reset();focus.target.copy(HOME.target);focus.dist=HOME.dist;focus.active=false;controls.autoRotate=!reduceMotion;syncAutoBtn()}
548
+
549
+ function setHover(name){if(ok)hovered=name}
550
+ return {init,setAgents,updateAgent,setBeam,pulse,setOperator,setOperatorPhase,reset,resize,setTheme,deselect,selectByName:select,setHover,
551
+ set onSelect(fn){onSelect=fn},get selected(){return selected},toggleAuto,camReset,syncAutoBtn};
552
+ })();
553
+
554
+ /* ============================================================
555
+ Uygulama durumu + SSE — 3B sahneyi besler
556
+ ============================================================ */
557
+ let operatorName='',nodes=new Map(),roster=new Map(),agentMeta={},delegCount=0,roundNo=0,stepNo=0,missionActive=false,activeSet=new Set();
558
+ let selectedAgent=null;
559
+
560
+ (function(){let t='dark';try{t=localStorage.getItem('cliTheme')||'dark'}catch{}applyTheme(t)})();
561
+
562
+ /* ---- Üst durum / KPI ---- */
563
+ function setEngine(s){$('#dot').className='dot'+(s.running?' on':'');$('#engineState').textContent=s.running?'çalışıyor':'durdu';
564
+ $('#modePill').textContent=s.current?`${s.current.stage} · ${cliLabel(adapterOf(s.current.agent))}`:(s.busy?'meşgul':'boşta');
565
+ if(typeof s.callsToday==='number')$('#kCalls').textContent=s.callsToday;
566
+ if(s.defaultOperator)setOperator(s.defaultOperator)}
567
+ function setOperator(name){if(!name)return;operatorName=name;const lbl=cliLabel(adapterOf(name));$('#opName').textContent=lbl;$('#kOp').textContent=lbl;Scene.setOperator(lbl)}
568
+ function opState(html,active){$('#opState').innerHTML=html;Scene.setOperatorPhase(active)}
569
+ function bumpStep(){stepNo++;return stepNo}
570
+ function refreshActive(){$('#kActive').textContent=activeSet.size}
571
+
572
+ /* ---- Ajan filosu (yan liste + 3B modeller) ---- */
573
+ function renderRoster(agents){roster.clear();agentMeta={};const box=$('#roster');const entries=Object.entries(agents||{}).filter(([,a])=>a.enabled!==false);
574
+ $('#rosterCount').textContent=entries.length+' ajan';
575
+ if(!entries.length){box.innerHTML='<div class="tl-empty">Tanımlı uzman ajan yok.</div>';Scene.setAgents([]);return}
576
+ box.innerHTML='';const sceneList=[];
577
+ for(const [name,a] of entries){const cli=adapterOf(a);const role=(a.roleFile||'').split('/').pop().replace('.md','')||'uzman';agentMeta[name]={cli,role};
578
+ const el=document.createElement('div');el.className='agent-card';el.id='ros-'+cssId(name);el.style.setProperty('--cli',cliColor(cli));
579
+ el.innerHTML=`<div class="avatar" style="--cli:${cliColor(cli)}"><span class="led"></span></div>
580
+ <div class="ai"><div class="n">${esc(name)}</div><div class="m"><span class="chip cli" style="background:${cliColor(cli)}">${esc(cliLabel(cli))}</span><span class="chip">${esc(role)}</span>${a.costTier?`<span class="chip">${esc(a.costTier)}</span>`:''}</div><div class="stt"><span class="status-text">hazır · sırada</span><span class="run-count">0 çalışma</span></div></div>`;
581
+ el.addEventListener('click',()=>Scene.selectByName(name));
582
+ box.appendChild(el);roster.set(name,{el,stt:el.querySelector('.status-text'),count:el.querySelector('.run-count'),status:'idle'});
583
+ sceneList.push({name,cli,role,color:cliColor(cli)});
584
+ }
585
+ Scene.setAgents(sceneList);
586
+ }
587
+ function syncRoster(name){const r=roster.get(name);if(!r)return;const jobs=[...nodes.values()].filter(n=>n.agent===name),active=jobs.filter(n=>n.status==='working'||n.status==='queued'),failed=jobs.filter(n=>n.status==='failed'||n.status==='blocked');
588
+ let status='idle',text='hazır · sırada';if(active.length){status='working';text=`çalışıyor · #${active[active.length-1].step}`}else if(jobs.length){status=failed.length&&failed.length===jobs.length?'failed':'done';text=failed.length?`${jobs.length} iş · ${failed.length} sorun`:`${jobs.length} işi tamamladı`}
589
+ r.el.classList.remove('working','done','failed','used');if(status!=='idle')r.el.classList.add(status,'used');r.stt.textContent=text;r.count.textContent=`${jobs.length} çalışma`;r.status=status;
590
+ if(active.length)activeSet.add(name);else activeSet.delete(name);refreshActive();
591
+ Scene.updateAgent(name,status);Scene.setBeam(name,!!active.length);
592
+ if(selectedAgent===name)renderPanel(name);
593
+ }
594
+
595
+ /* ---- Sahne düğümleri (veri; DOM yok) ---- */
596
+ function ensureNode(id,name,instruction){
597
+ const key=String(id||`${name}-live`);if(nodes.has(key)){const found=nodes.get(key);if(instruction){found.instruction=instruction}return found}
598
+ const step=nodes.size+1;
599
+ const node={id:key,agent:name,step,raw:'',instruction:instruction||'',status:'queued',startedAt:null,durationMs:null};
600
+ nodes.set(key,node);syncRoster(name);return node;
601
+ }
602
+ function setNode(id,name,status,text,meta={}){const n=ensureNode(id,name,meta.instruction);n.status=status||n.status;if(text)n.statusText=text;
603
+ if(meta.instruction)n.instruction=meta.instruction;
604
+ if(meta.startedAt&&!n.startedAt)n.startedAt=new Date(meta.startedAt).getTime();if(meta.durationMs!=null)n.durationMs=meta.durationMs;
605
+ syncRoster(n.agent);if(selectedAgent===n.agent)renderPanel(n.agent);return n}
606
+ function appendTail(id,name,text){const n=ensureNode(id,name);n.raw=(n.raw+text).slice(-4000);if(selectedAgent===name)updatePanelTail(name)}
607
+
608
+ /* ---- Akış darbeleri ---- */
609
+ function pulse(id,dir){const n=nodes.get(String(id));if(!n)return;Scene.pulse(n.agent,dir)}
610
+
611
+ /* ---- Detay paneli ---- */
612
+ // Hem 3B tıklaması hem yan listedeki kart tıklaması Scene.selectByName üzerinden
613
+ // buraya (onSelect callback) düşer; tek yol, tek panel.
614
+ function onSceneSelect(name){selectedAgent=name;highlightRoster(name);if(!name){const p=$('#agentPanel');p.hidden=true;p.innerHTML='';return}renderPanel(name)}
615
+ function highlightRoster(name){for(const [n,r] of roster)r.el.classList.toggle('selected',n===name)}
616
+
617
+ function agentDetail(name){const meta=agentMeta[name]||{};const jobs=[...nodes.values()].filter(n=>n.agent===name);
618
+ const active=jobs.filter(j=>j.status==='working'||j.status==='queued');
619
+ const latest=active[active.length-1]||jobs[jobs.length-1]||null;
620
+ const r=roster.get(name);
621
+ return {name,cli:meta.cli||adapterOf(name),role:meta.role||'uzman',status:r?r.status:'idle',jobs:jobs.length,latest};
622
+ }
623
+ function statusLabel(s){return s==='working'?'Çalışıyor':s==='done'?'Tamamlandı':s==='failed'?'Sorun oluştu':'Beklemede'}
624
+ function renderPanel(name){const d=agentDetail(name),p=$('#agentPanel');p.hidden=false;const cc=cliColor(d.cli);
625
+ const j=d.latest;
626
+ p.style.setProperty('--cli',cc);
627
+ p.innerHTML=`<div class="ap-head" style="--cli:${cc}">
628
+ <div class="ap-av" style="--cli:${cc}"></div>
629
+ <div class="ap-id"><div class="name">${esc(name)}</div><div class="sub"><span class="chip cli" style="background:${cc}">${esc(cliLabel(d.cli))}</span><span class="chip">${esc(d.role)}</span></div></div>
630
+ <button class="ap-close" title="Kapat">✕</button>
631
+ </div>
632
+ <div class="ap-body">
633
+ <div class="ap-status ${esc(d.status)}"><span class="sdot"></span>${statusLabel(d.status)} · ${d.jobs} çalışma</div>
634
+ ${j?`<div class="ap-sec">Şu an ne yapıyor</div><div class="ap-task">${esc(j.instruction||j.statusText||'Görev ayrıntısı bekleniyor…')}</div>
635
+ <div class="ap-sec">Canlı çıktı</div><pre class="ap-tail">${esc(j.raw||'')}</pre>
636
+ <div class="ap-meta"><span>iş: ${esc(j.id)}</span><span>adım: #${j.step}</span>${j.durationMs!=null?`<span>süre: ${(j.durationMs/1000).toFixed(1)} sn</span>`:''}</div>`
637
+ :`<div class="ap-empty">Bu ajan henüz bir görev almadı. Operatör delege ettiğinde çalışması burada canlı görünecek.</div>`}
638
+ </div>`;
639
+ p.querySelector('.ap-close').addEventListener('click',()=>{selectedAgent=null;Scene.deselect();p.hidden=true;highlightRoster(null)});
640
+ }
641
+ function updatePanelTail(name){const p=$('#agentPanel');if(p.hidden)return;const el=p.querySelector('.ap-tail');if(!el)return renderPanel(name);const d=agentDetail(name);if(d.latest){el.textContent=d.latest.raw||'';el.scrollTop=el.scrollHeight}}
642
+
643
+ /* ---- Zaman çizelgesi ---- */
644
+ function addTimeline(kind,route,txt,num,at,agent){const tl=$('#timeline');if(tl.querySelector('.tl-empty'))tl.innerHTML='';const d=document.createElement('div');d.className='tl '+kind;d.innerHTML=`<div class="num">${num??'•'}</div><div class="body"><div class="route">${esc(route)}<time>${time(at)}</time></div>${txt?`<div class="txt">${esc(txt)}</div>`:''}</div>`;
645
+ if(agent&&roster.has(agent)){d.style.cursor='pointer';d.addEventListener('mouseenter',()=>Scene.setHover(agent));d.addEventListener('mouseleave',()=>Scene.setHover(null));d.addEventListener('click',()=>Scene.selectByName(agent))}
646
+ tl.appendChild(d);tl.scrollTop=tl.scrollHeight}
647
+
648
+ /* ---- Görev yaşam döngüsü ---- */
649
+ function startMission(title,at){missionActive=true;delegCount=0;roundNo=0;stepNo=0;activeSet.clear();refreshActive();nodes.clear();Scene.reset();$('#timeline').innerHTML='';
650
+ for(const [name,r] of roster){r.el.classList.remove('working','done','failed','used');r.stt.textContent='hazır · sırada';r.count.textContent='0 çalışma';r.status='idle';Scene.updateAgent(name,'idle')}
651
+ if(selectedAgent)renderPanel(selectedAgent);
652
+ $('#mKick').textContent='● Görev yürütülüyor';$('#mTitle').textContent=title||'Yeni takım görevi';$('#mSub').textContent='Operatör hedefi analiz ediyor ve uygun uzmanları seçiyor.';
653
+ $('#kRound').textContent='0';$('#kDeleg').textContent='0';$('#kFiles').textContent='0';
654
+ opState('hedefi <b>analiz ediyor</b>',true);addTimeline('system','Görev alındı',title,'▶',at)}
655
+
656
+ /* ---- SSE işleyicileri ---- */
657
+ function onActivity(e){
658
+ if(isOpStage(e.stage)){setOperator(e.agent);
659
+ if(e.kind==='process.started')opState(e.stage==='operator-plan'?'ekip <b>planını hazırlıyor</b>':e.stage==='operator-review'?'sonuçları <b>değerlendiriyor</b>':'<b>çalışıyor</b>',true);
660
+ else if(e.kind==='process.finished')opState(Number(e.exitCode)!==0?'karar veremedi':'ekibi <b>yönlendirdi</b>',true);
661
+ return}
662
+ const id=e.assignmentId||`${e.agent}-live`;ensureNode(id,e.agent);
663
+ if(e.kind==='process.started')setNode(id,e.agent,'working','görevi yürütüyor',{startedAt:e.at});
664
+ else if(e.kind==='stdout'||e.kind==='stderr'){appendTail(id,e.agent,e.text);setNode(id,e.agent,'working','canlı çıktı üretiyor')}
665
+ else if(e.kind==='process.finished'){const failed=Number(e.exitCode)!==0;setNode(id,e.agent,failed?'failed':'done',failed?'başarısız':'uzman çalışmasını bitirdi',{durationMs:e.durationMs})}
666
+ else if(e.kind==='process.failed'||e.kind==='process.timeout')setNode(id,e.agent,'failed',e.kind==='process.timeout'?'zaman aşımı':'çalıştırma hatası',{durationMs:e.durationMs});
667
+ }
668
+ function onMessage(e){const from=e.from,to=e.to,type=e.messageType||'';
669
+ const id=String(e.assignmentId||`${from}-${delegCount+1}`);
670
+ if(type==='delegation'){delegCount++;$('#kDeleg').textContent=delegCount;setOperator(from);ensureNode(id,to,e.body);
671
+ addTimeline('delegation',`#${id} · ${cliLabel(adapterOf(from))} → ${to}`,e.body,bumpStep(),e.at,to);setNode(id,to,'working','operatörden görev aldı',{instruction:e.body,startedAt:e.at});pulse(id,'down');opState('uzmana <b>delege ediyor</b>',true)}
672
+ else if(type==='result'){addTimeline('result',`#${id} · ${from} → operatör`,e.body,bumpStep(),e.at,from);const n=setNode(id,from,'done','teslimat operatöre ulaştı');if(!n.raw){n.raw=String(e.body||'').slice(0,4000);if(selectedAgent===from)updatePanelTail(from)}pulse(id,'up');opState('teslimatı <b>topluyor</b>',true)}
673
+ else if(type==='failure'){addTimeline('failure',`#${id} · ${from} → operatör`,e.body,bumpStep(),e.at,from);const n=setNode(id,from,'failed','agent çalışması başarısız');n.raw=String(e.body||'').slice(0,4000);if(selectedAgent===from)updatePanelTail(from);pulse(id,'up')}
674
+ else if(type==='blocked'){addTimeline('blocked',`#${id} · bağımlılık engeli`,e.body,bumpStep(),e.at);const known=nodes.get(id);setNode(id,known?.agent||'engellenen-görev','blocked','bağımlılık nedeniyle çalışmadı',{instruction:e.body})}
675
+ }
676
+ function onLog(e){
677
+ if(e.level==='task'){startMission(String(e.msg||'').replace(/^GOREV\s+[^:]+:\s*/,'').slice(0,160),e.at);return}
678
+ if(String(e.msg||'').startsWith('Calisma modu'))$('#mSub').textContent=e.msg;
679
+ if(e.level==='error'){$('#mKick').textContent='! Son çalışma başarısız';$('#mSub').textContent=String(e.msg||'Görev tamamlanamadı.');opState('görevi <b>tamamlayamadı</b>',false);for(const [id,n] of nodes){if(n.status==='working'||n.status==='queued')setNode(id,n.agent,'failed','görev hata ile kapandı')}missionActive=false}
680
+ }
681
+ function onResult(e){
682
+ if(e.kind==='operator-chat')return;
683
+ roundNo=e.rounds||roundNo;$('#kRound').textContent=roundNo;
684
+ const changed=((e.changes?.created||[]).length)+((e.changes?.modified||[]).length);$('#kFiles').textContent=changed;
685
+ $('#mKick').textContent='✓ Son çalışma tamamlandı';
686
+ $('#mSub').textContent=`${cliLabel(adapterOf(e.operator||operatorName))} · ${roundNo} takım turu · ${changed} dosya değişikliği`;
687
+ opState('görevi <b>tamamladı</b>',false);
688
+ activeSet.clear();refreshActive();
689
+ for(const [id,n] of nodes){if(n.status==='working'||n.status==='queued')setNode(id,n.agent,'done','görev kapanışında tamamlandı')}
690
+ addTimeline('system','Operatör görevi tamamladı',e.summary||(e.delivery?.verification||''),'✓',e.at);missionActive=false;
691
+ }
692
+
693
+ /* ---- 3B sahne kancaları ---- */
694
+ const loaderStart=performance.now();
695
+ function hideSceneLoader(){const l=$('#sceneLoader');if(!l)return;const wait=Math.max(0,900-(performance.now()-loaderStart));
696
+ setTimeout(()=>{l.classList.add('gone');setTimeout(()=>l.remove(),500)},wait)}
697
+ const canvas=$('#scene');
698
+ try{Scene.init(canvas);requestAnimationFrame(()=>requestAnimationFrame(hideSceneLoader))}
699
+ catch(err){console.warn('WebGL 3B sahne başlatılamadı:',err);showSceneFallback();hideSceneLoader()}
700
+ Scene.onSelect=(name)=>onSceneSelect(name);
701
+ Scene.setTheme(document.documentElement.getAttribute('data-theme'));
702
+ $('#autoRotBtn').addEventListener('click',()=>Scene.toggleAuto());
703
+ $('#camResetBtn').addEventListener('click',()=>Scene.camReset());
704
+ Scene.syncAutoBtn();
705
+ function showSceneFallback(){const st=$('#stage'),c=$('#scene'),tr=document.querySelector('.hud-tr');
706
+ if(c)c.style.display='none';if(tr)tr.style.display='none';
707
+ const d=document.createElement('div');d.className='scene-fallback';
708
+ d.innerHTML='<div class="sf-ic">🛰️</div><h3>3B görünüm başlatılamadı</h3><p>Tarayıcınız WebGL desteklemiyor olabilir. Ekip verileri soldaki filo ve sağdaki zaman çizelgesinde canlı çalışmaya devam ediyor; bir ajanı seçmek için filodan tıklayın.</p>';
709
+ st.appendChild(d);}
710
+ /* ---- Yardım (?) modalı ---- */
711
+ (function(){const m=$('#helpModal');const open=()=>{m.hidden=false};const close=()=>{m.hidden=true};
712
+ $('#helpBtn').addEventListener('click',open);$('#helpClose').addEventListener('click',close);
713
+ m.addEventListener('click',e=>{if(e.target===m)close()});
714
+ document.addEventListener('keydown',e=>{if(e.key==='Escape'&&!m.hidden)close()});})();
715
+
716
+ const es=new EventSource('/api/events');
717
+ es.addEventListener('status',e=>setEngine(JSON.parse(e.data)));
718
+ es.addEventListener('activity',e=>onActivity(JSON.parse(e.data)));
719
+ es.addEventListener('message',e=>onMessage(JSON.parse(e.data)));
720
+ es.addEventListener('log',e=>onLog(JSON.parse(e.data)));
721
+ es.addEventListener('result',e=>onResult(JSON.parse(e.data)));
722
+ // Sayfadan ayrılırken (Komuta Merkezi'ne dönüş / bfcache) açık SSE bağlantısını kapat ki
723
+ // donmuş bağlantılar birikip diğer sayfadaki fetch'leri kilitlemesin; geri dönüşte tazele.
724
+ window.addEventListener('pagehide',()=>{try{es.close()}catch{}});
725
+ window.addEventListener('pageshow',e=>{if(e.persisted)location.reload()});
726
+
727
+ /* ---- İlk yükleme ---- */
728
+ (async()=>{try{
729
+ const s=await(await fetch('/api/state')).json();
730
+ renderRoster(s.config&&s.config.agents);
731
+ setEngine(s.status);
732
+ const cur=s.status.current;
733
+ const recent=cur||(s.queue?.done||[])[0]||(s.queue?.failed||[])[0];
734
+ if(recent&&recent.id){
735
+ const ev=await(await fetch(`/api/tasks/${recent.id}/events?limit=1500`)).json();
736
+ missionActive=!!cur;
737
+ for(const e of ev){if(e.type==='log')onLog(e);else if(e.type==='activity')onActivity(e);else if(e.type==='message')onMessage(e);else if(e.type==='result')onResult(e)}
738
+ }
739
+ }catch(err){console.warn(err)}})();
740
+ </script>
741
+ </body></html>