@omerrgocmen/crewctl 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -0
- package/orchestrator/LICENSE +21 -0
- package/orchestrator/README.md +452 -0
- package/orchestrator/config.default.json +65 -0
- package/orchestrator/roles/executor.md +49 -0
- package/orchestrator/roles/operator-chat.md +25 -0
- package/orchestrator/roles/operator.md +75 -0
- package/orchestrator/roles/planner.md +53 -0
- package/orchestrator/roles/reviewer.md +54 -0
- package/orchestrator/skills/acceptance-criteria.md +18 -0
- package/orchestrator/skills/accessibility-audit.md +16 -0
- package/orchestrator/skills/accessible-forms.md +16 -0
- package/orchestrator/skills/api-design.md +27 -0
- package/orchestrator/skills/api-documentation.md +16 -0
- package/orchestrator/skills/architecture-decision-record.md +18 -0
- package/orchestrator/skills/authentication-design.md +16 -0
- package/orchestrator/skills/authorization-review.md +16 -0
- package/orchestrator/skills/backward-compatibility.md +17 -0
- package/orchestrator/skills/changelog-writing.md +16 -0
- package/orchestrator/skills/ci-pipeline-design.md +16 -0
- package/orchestrator/skills/cli-design.md +16 -0
- package/orchestrator/skills/code-review.md +27 -0
- package/orchestrator/skills/configuration-management.md +16 -0
- package/orchestrator/skills/container-review.md +16 -0
- package/orchestrator/skills/contract-testing.md +16 -0
- package/orchestrator/skills/dashboard-design.md +16 -0
- package/orchestrator/skills/database-migration.md +16 -0
- package/orchestrator/skills/database-schema-design.md +16 -0
- package/orchestrator/skills/debugging.md +26 -0
- package/orchestrator/skills/dependency-review.md +16 -0
- package/orchestrator/skills/design-review.md +29 -0
- package/orchestrator/skills/design-system.md +16 -0
- package/orchestrator/skills/docs-api-reference.md +16 -0
- package/orchestrator/skills/docs-troubleshooting.md +16 -0
- package/orchestrator/skills/docs-tutorial.md +16 -0
- package/orchestrator/skills/docs-writing.md +25 -0
- package/orchestrator/skills/empty-error-loading-states.md +16 -0
- package/orchestrator/skills/end-to-end-testing.md +16 -0
- package/orchestrator/skills/error-handling.md +16 -0
- package/orchestrator/skills/frontend-design.md +28 -0
- package/orchestrator/skills/git-commit-writing.md +16 -0
- package/orchestrator/skills/graphql-design.md +16 -0
- package/orchestrator/skills/incident-runbook.md +18 -0
- package/orchestrator/skills/input-validation.md +16 -0
- package/orchestrator/skills/integration-testing.md +16 -0
- package/orchestrator/skills/interaction-design.md +16 -0
- package/orchestrator/skills/landing-page-design.md +16 -0
- package/orchestrator/skills/observability-design.md +16 -0
- package/orchestrator/skills/openapi-contract.md +16 -0
- package/orchestrator/skills/performance-profiling.md +16 -0
- package/orchestrator/skills/privacy-review.md +16 -0
- package/orchestrator/skills/property-based-testing.md +16 -0
- package/orchestrator/skills/pull-request-writing.md +16 -0
- package/orchestrator/skills/refactoring.md +16 -0
- package/orchestrator/skills/release-readiness.md +16 -0
- package/orchestrator/skills/responsive-design.md +16 -0
- package/orchestrator/skills/secrets-management.md +16 -0
- package/orchestrator/skills/secure-file-upload.md +16 -0
- package/orchestrator/skills/security-review.md +26 -0
- package/orchestrator/skills/semantic-versioning.md +17 -0
- package/orchestrator/skills/seo-on-page.md +16 -0
- package/orchestrator/skills/seo-structured-data.md +16 -0
- package/orchestrator/skills/seo-technical-audit.md +16 -0
- package/orchestrator/skills/sql-query-review.md +16 -0
- package/orchestrator/skills/supply-chain-security.md +16 -0
- package/orchestrator/skills/test-strategy.md +16 -0
- package/orchestrator/skills/threat-modeling.md +16 -0
- package/orchestrator/skills/unit-testing.md +16 -0
- package/orchestrator/skills/write-tests.md +28 -0
- package/orchestrator/src/checkpoints.js +187 -0
- package/orchestrator/src/cli-registry.js +579 -0
- package/orchestrator/src/cli.js +135 -0
- package/orchestrator/src/doctor.js +64 -0
- package/orchestrator/src/engine.js +1364 -0
- package/orchestrator/src/schedule.js +126 -0
- package/orchestrator/src/server.js +706 -0
- package/orchestrator/src/skill-registry.js +272 -0
- package/orchestrator/src/store.js +364 -0
- package/orchestrator/web/OrbitControls.js +1417 -0
- package/orchestrator/web/app.css +116 -0
- package/orchestrator/web/app.js +70 -0
- package/orchestrator/web/board.html +141 -0
- package/orchestrator/web/code.html +208 -0
- package/orchestrator/web/flow.html +736 -0
- package/orchestrator/web/index.html +539 -0
- package/orchestrator/web/jsm/postprocessing/EffectComposer.js +231 -0
- package/orchestrator/web/jsm/postprocessing/MaskPass.js +104 -0
- package/orchestrator/web/jsm/postprocessing/Pass.js +95 -0
- package/orchestrator/web/jsm/postprocessing/RenderPass.js +99 -0
- package/orchestrator/web/jsm/postprocessing/ShaderPass.js +77 -0
- package/orchestrator/web/jsm/postprocessing/UnrealBloomPass.js +415 -0
- package/orchestrator/web/jsm/shaders/CopyShader.js +45 -0
- package/orchestrator/web/jsm/shaders/LuminosityHighPassShader.js +66 -0
- package/orchestrator/web/three.module.min.js +6 -0
- package/package.json +51 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/* app.css — paylasilan CILA katmani (uc sayfada inline <style>'dan SONRA yuklenir, esit
|
|
2
|
+
ozgullukte kazanir). Yaklasim: "sakin & keskin" (Linear/Vercel tarzi). Sistem fontu,
|
|
3
|
+
net tipografi, tutarli buton/odak sistemi, ince scrollbar, yumusak-hizli sayfa gecisleri.
|
|
4
|
+
Tamamen kozmetik ve additive; yerlesim/mantik degistirmez, tema token'larini (var(--*)) kullanir. */
|
|
5
|
+
|
|
6
|
+
/* ---- Tipografi: cilali sistem font yigini + smoothing ---- */
|
|
7
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
8
|
+
body {
|
|
9
|
+
font-family: "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, "Inter", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
10
|
+
-webkit-font-smoothing: antialiased;
|
|
11
|
+
-moz-osx-font-smoothing: grayscale;
|
|
12
|
+
text-rendering: optimizeLegibility;
|
|
13
|
+
font-feature-settings: "kern" 1, "liga" 1, "calt" 1;
|
|
14
|
+
letter-spacing: -0.006em;
|
|
15
|
+
}
|
|
16
|
+
h1, .brand { letter-spacing: -0.02em; }
|
|
17
|
+
h1 { font-weight: 700; }
|
|
18
|
+
/* Sayisal degerler daima hizali (tablo rakamlari) — sayaclar/istatistikler icin. */
|
|
19
|
+
.pill, .badge, .tile .v, .diff-stats, #budget, .mission-status, .banner .t, .chip b { font-variant-numeric: tabular-nums; }
|
|
20
|
+
code, pre, .terminal, .diff-code, textarea { font-feature-settings: "kern" 0; }
|
|
21
|
+
|
|
22
|
+
/* ---- Buton sistemi: keskin, tutarli, hafif hareket ---- */
|
|
23
|
+
button, a.btn > button {
|
|
24
|
+
border-radius: 9px;
|
|
25
|
+
font-weight: 500;
|
|
26
|
+
letter-spacing: -0.005em;
|
|
27
|
+
transition: background .16s ease, border-color .16s ease, color .16s ease,
|
|
28
|
+
box-shadow .18s ease, transform .08s ease;
|
|
29
|
+
/* Not: will-change kullanmiyoruz — cok sayida butonda kalici compositor katmani
|
|
30
|
+
GPU baskisi/jank yaratir; translateY(1px) icin gereksiz. */
|
|
31
|
+
}
|
|
32
|
+
button:hover { border-color: color-mix(in srgb, var(--acc) 55%, var(--line)); }
|
|
33
|
+
button:active { transform: translateY(1px); }
|
|
34
|
+
button:disabled { opacity: .5; }
|
|
35
|
+
/* Buton ici metin ASLA sarmasin (aksi halde dar kolonda satirlar ust uste biner). */
|
|
36
|
+
button { white-space: nowrap; }
|
|
37
|
+
|
|
38
|
+
/* ---- Yerlesim: aksiyon satirlari ve header tasmadan alt satira sarsin ---- */
|
|
39
|
+
.task .row { flex-wrap: wrap; row-gap: 6px; }
|
|
40
|
+
header { flex-wrap: wrap; row-gap: 8px; }
|
|
41
|
+
/* Kucuk butonlarda ikon oraninI dengele. */
|
|
42
|
+
button.sm > svg, .sm > svg { width: 13px; height: 13px; }
|
|
43
|
+
/* Ikon + metin hizasi ve keskin SVG ikonlar. */
|
|
44
|
+
button svg, a.btn svg { vertical-align: -0.18em; }
|
|
45
|
+
.i, button > svg { width: 15px; height: 15px; flex: none; }
|
|
46
|
+
button { display: inline-flex; align-items: center; gap: 7px; }
|
|
47
|
+
/* Tek-ikon (kare) butonlar icin dengeli oran. */
|
|
48
|
+
#themeBtn, #helpBtn { justify-content: center; }
|
|
49
|
+
|
|
50
|
+
/* ---- Erisilebilir, tutarli odak halkasi (yalnizca klavye) ---- */
|
|
51
|
+
:focus-visible {
|
|
52
|
+
outline: 2px solid color-mix(in srgb, var(--acc) 70%, transparent);
|
|
53
|
+
outline-offset: 2px;
|
|
54
|
+
border-radius: 8px;
|
|
55
|
+
}
|
|
56
|
+
button:focus-visible, a:focus-visible { outline-offset: 3px; }
|
|
57
|
+
input:focus-visible, textarea:focus-visible, select:focus-visible { outline: none; }
|
|
58
|
+
|
|
59
|
+
/* ---- Metin secimi ---- */
|
|
60
|
+
::selection { background: color-mix(in srgb, var(--acc) 32%, transparent); color: var(--tx); }
|
|
61
|
+
|
|
62
|
+
/* ---- Ince, sessiz scrollbar (tum panellerde tutarli) ---- */
|
|
63
|
+
* { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--line) 85%, transparent) transparent; }
|
|
64
|
+
*::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
65
|
+
*::-webkit-scrollbar-track { background: transparent; }
|
|
66
|
+
*::-webkit-scrollbar-thumb {
|
|
67
|
+
background: color-mix(in srgb, var(--dim) 34%, transparent);
|
|
68
|
+
border: 3px solid transparent;
|
|
69
|
+
border-radius: 999px;
|
|
70
|
+
background-clip: padding-box;
|
|
71
|
+
}
|
|
72
|
+
*::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--dim) 55%, transparent); background-clip: padding-box; }
|
|
73
|
+
|
|
74
|
+
/* ---- Sayfa gecisi: opacity-only fade (translateY tum sayfayi katmanladigi icin agir
|
|
75
|
+
sayfalarda —or. flow.html WebGL— jank yaratir; sadece opacity ucuz ve puruzsuz). ---- */
|
|
76
|
+
@keyframes appPageIn { from { opacity: 0; } to { opacity: 1; } }
|
|
77
|
+
body { animation: appPageIn .26s ease both; }
|
|
78
|
+
/* navTo() cikis animasyonu (app.js ekler). */
|
|
79
|
+
body.app-leaving { opacity: 0; transition: opacity .17s ease; animation: none; }
|
|
80
|
+
|
|
81
|
+
/* ---- Kart/etkilesim mikro-cilasi (sakin; transform animasyonu YOK — jank azalt) ---- */
|
|
82
|
+
.card, .task, .diff-file, .tile, .chip { transition: border-color .18s ease, box-shadow .2s ease; }
|
|
83
|
+
|
|
84
|
+
/* ---- Ikonlar: keskin inline SVG (emoji yerine, currentColor ile temaya uyumlu) ---- */
|
|
85
|
+
.i-sun { display: none; }
|
|
86
|
+
[data-theme=light] .i-sun { display: inline-flex; }
|
|
87
|
+
[data-theme=light] .i-moon { display: none; }
|
|
88
|
+
#themeBtn svg, #helpBtn svg { width: 16px; height: 16px; }
|
|
89
|
+
.ic { display: inline-flex; align-items: center; justify-content: center; }
|
|
90
|
+
.ic svg { width: 13px; height: 13px; display: block; }
|
|
91
|
+
button > svg, .btn svg { stroke-linecap: round; stroke-linejoin: round; }
|
|
92
|
+
|
|
93
|
+
/* ---- Hareketi azalt tercihi: tum sure/animasyonlari kis ---- */
|
|
94
|
+
@media (prefers-reduced-motion: reduce) {
|
|
95
|
+
body { animation: none; }
|
|
96
|
+
body.app-leaving { transition: none; }
|
|
97
|
+
*, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ---- Paylasilan onay modali (native confirm yerine) ---- */
|
|
101
|
+
.app-modal{position:fixed;inset:0;z-index:120;display:flex;align-items:center;justify-content:center;padding:22px;background:color-mix(in srgb,#05070d 62%,transparent);opacity:0;transition:opacity .18s ease}
|
|
102
|
+
.app-modal.show{opacity:1}
|
|
103
|
+
.app-dialog{width:min(444px,100%);background:var(--panel);border:1px solid var(--line);border-radius:16px;box-shadow:0 24px 70px #0007,0 2px 10px #0004;overflow:hidden;transform:translateY(10px) scale(.985);opacity:.6;transition:transform .2s cubic-bezier(.22,.61,.36,1),opacity .2s ease}
|
|
104
|
+
.app-modal.show .app-dialog{transform:none;opacity:1}
|
|
105
|
+
.app-dialog-head{display:flex;gap:13px;align-items:flex-start;padding:18px 18px 2px}
|
|
106
|
+
.app-dialog-ico{flex:none;width:38px;height:38px;border-radius:11px;display:grid;place-items:center;background:color-mix(in srgb,var(--acc) 15%,transparent);color:var(--acc)}
|
|
107
|
+
.app-dialog.danger .app-dialog-ico{background:color-mix(in srgb,var(--err) 15%,transparent);color:var(--err)}
|
|
108
|
+
.app-dialog-ico svg{width:20px;height:20px}
|
|
109
|
+
.app-dialog-title{font-weight:750;font-size:15.5px;letter-spacing:-.01em;margin-top:6px;color:var(--tx)}
|
|
110
|
+
.app-dialog-body{padding:9px 20px 4px 69px;color:var(--dim);font-size:13px;line-height:1.6;white-space:pre-line}
|
|
111
|
+
.app-dialog-foot{display:flex;justify-content:flex-end;gap:9px;padding:16px 18px 18px}
|
|
112
|
+
.app-dialog-foot button{padding:8px 15px;font-weight:600}
|
|
113
|
+
.app-btn-confirm{background:var(--acc);border-color:var(--acc);color:#08122a}
|
|
114
|
+
.app-dialog.danger .app-btn-confirm{background:var(--err);border-color:var(--err);color:#fff}
|
|
115
|
+
.app-btn-confirm:hover{filter:brightness(1.07);border-color:var(--acc)}
|
|
116
|
+
.app-dialog.danger .app-btn-confirm:hover{border-color:var(--err)}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// app.js — paylasilan ekran gecisi cilasi (uc sayfada defer ile yuklenir). Sayfa fade-in
|
|
2
|
+
// CSS'te; burada yumusak fade-OUT + ic .html navigasyonu yakalama var. Tamamen additive.
|
|
3
|
+
(function () {
|
|
4
|
+
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
5
|
+
|
|
6
|
+
// Yumusak cikis: once fade-out, sonra git. Reduced-motion'da aninda gider.
|
|
7
|
+
function navTo(url) {
|
|
8
|
+
if (!url) return;
|
|
9
|
+
if (reduce) { location.href = url; return; }
|
|
10
|
+
try { document.body.classList.add("app-leaving"); } catch {}
|
|
11
|
+
setTimeout(() => { location.href = url; }, 185);
|
|
12
|
+
}
|
|
13
|
+
window.navTo = navTo;
|
|
14
|
+
|
|
15
|
+
// Ic .html linklerini (yeni sekme / modifier / dis link haric) yumusak gecisle ac.
|
|
16
|
+
document.addEventListener("click", (event) => {
|
|
17
|
+
const anchor = event.target.closest && event.target.closest("a[href]");
|
|
18
|
+
if (!anchor) return;
|
|
19
|
+
if (anchor.target === "_blank" || anchor.hasAttribute("download")) return;
|
|
20
|
+
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
21
|
+
const href = anchor.getAttribute("href") || "";
|
|
22
|
+
if (/^(https?:|mailto:|#)/i.test(href)) return;
|
|
23
|
+
if (!/\.html(\?.*)?(#.*)?$/.test(href)) return;
|
|
24
|
+
event.preventDefault();
|
|
25
|
+
navTo(href);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// bfcache geri donuste yaprak/cikis durumunu temizle.
|
|
29
|
+
window.addEventListener("pageshow", (event) => {
|
|
30
|
+
if (event.persisted) document.body.classList.remove("app-leaving");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Paylasilan sik onay modali — native confirm() yerine Promise<boolean> doner.
|
|
34
|
+
// opts: { title, body, confirmLabel, cancelLabel, danger(=true) }
|
|
35
|
+
const ICON_WARN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/></svg>';
|
|
36
|
+
const ICON_INFO = '<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>';
|
|
37
|
+
const escHtml = (s) => String(s == null ? "" : s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
38
|
+
window.appConfirm = function (opts) {
|
|
39
|
+
opts = opts || {};
|
|
40
|
+
const danger = opts.danger !== false;
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const prev = document.activeElement;
|
|
43
|
+
const overlay = document.createElement("div");
|
|
44
|
+
overlay.className = "app-modal";
|
|
45
|
+
overlay.innerHTML =
|
|
46
|
+
`<div class="app-dialog${danger ? " danger" : ""}" role="alertdialog" aria-modal="true">` +
|
|
47
|
+
`<div class="app-dialog-head"><div class="app-dialog-ico">${danger ? ICON_WARN : ICON_INFO}</div>` +
|
|
48
|
+
`<div class="app-dialog-title">${escHtml(opts.title || "Emin misiniz?")}</div></div>` +
|
|
49
|
+
`<div class="app-dialog-body">${escHtml(opts.body || "")}</div>` +
|
|
50
|
+
`<div class="app-dialog-foot"><button class="app-btn-cancel">${escHtml(opts.cancelLabel || "Vazgeç")}</button>` +
|
|
51
|
+
`<button class="app-btn-confirm">${escHtml(opts.confirmLabel || "Onayla")}</button></div></div>`;
|
|
52
|
+
document.body.appendChild(overlay);
|
|
53
|
+
requestAnimationFrame(() => overlay.classList.add("show"));
|
|
54
|
+
const confirmBtn = overlay.querySelector(".app-btn-confirm");
|
|
55
|
+
const cancelBtn = overlay.querySelector(".app-btn-cancel");
|
|
56
|
+
const onKey = (e) => { if (e.key === "Escape") { e.preventDefault(); close(false); } else if (e.key === "Enter") { e.preventDefault(); close(true); } };
|
|
57
|
+
function close(val) {
|
|
58
|
+
overlay.classList.remove("show");
|
|
59
|
+
document.removeEventListener("keydown", onKey, true);
|
|
60
|
+
setTimeout(() => { try { overlay.remove(); } catch {} try { prev && prev.focus && prev.focus(); } catch {} }, 190);
|
|
61
|
+
resolve(val);
|
|
62
|
+
}
|
|
63
|
+
cancelBtn.onclick = () => close(false);
|
|
64
|
+
confirmBtn.onclick = () => close(true);
|
|
65
|
+
overlay.addEventListener("mousedown", (e) => { if (e.target === overlay) close(false); });
|
|
66
|
+
document.addEventListener("keydown", onKey, true);
|
|
67
|
+
setTimeout(() => { try { confirmBtn.focus(); } catch {} }, 60);
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
})();
|
|
@@ -0,0 +1,141 @@
|
|
|
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>Pano · CrewCtl</title>
|
|
7
|
+
<style>
|
|
8
|
+
*{box-sizing:border-box}
|
|
9
|
+
:root{--bg:#090c12;--panel:#111826;--panel2:#182234;--line:#243149;--tx:#e9eef7;--dim:#8494ab;--acc:#5e8bff;--ok:#2fd08a;--warn:#eab24d;--err:#f2565f;--purple:#a98bff;
|
|
10
|
+
--headerbg:#0a0f1acc;--input:#0b111c;--nodebg:#0b1220;--shadow:0 14px 40px #0008;
|
|
11
|
+
--appbg:radial-gradient(1200px 620px at 50% -14%,#5e8bff16,transparent),radial-gradient(1000px 520px at 90% -6%,#a98bff12,transparent),var(--bg)}
|
|
12
|
+
[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;
|
|
13
|
+
--headerbg:#ffffffe6;--input:#f4f7fd;--nodebg:#f7f9fd;--shadow:0 14px 36px #7a90b820;
|
|
14
|
+
--appbg:radial-gradient(1200px 620px at 50% -14%,#3a63f012,transparent),radial-gradient(1000px 520px at 90% -6%,#7350f00e,transparent),var(--bg)}
|
|
15
|
+
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}
|
|
16
|
+
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}
|
|
17
|
+
button:hover{border-color:var(--acc);box-shadow:0 4px 14px color-mix(in srgb,var(--acc) 16%,transparent)}button:active{transform:translateY(1px)}
|
|
18
|
+
button.sm{padding:4px 8px;font-size:12px}button.ok{color:var(--ok)}button.danger{color:var(--err)}
|
|
19
|
+
a.btn{text-decoration:none;display:inline-block}
|
|
20
|
+
/* Header */
|
|
21
|
+
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}
|
|
22
|
+
h1{font-size:15px;margin:0;letter-spacing:.2px}.brand{color:var(--acc);font-weight:800}.sep{color:var(--dim)}.grow{flex:1}
|
|
23
|
+
.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}
|
|
24
|
+
.dot{width:8px;height:8px;border-radius:50%;background:var(--dim)}
|
|
25
|
+
.dot.on{background:var(--ok);box-shadow:0 0 10px var(--ok);animation:beat 1.6s ease-in-out infinite}
|
|
26
|
+
@keyframes beat{50%{box-shadow:0 0 3px var(--ok)}}
|
|
27
|
+
.muted{color:var(--dim);font-size:12px}
|
|
28
|
+
/* Zamanlama şeridi */
|
|
29
|
+
.sched-strip{margin:14px 14px 0;background:linear-gradient(120deg,var(--panel),var(--panel2));border:1px solid var(--line);border-radius:14px;padding:11px 14px;box-shadow:var(--shadow)}
|
|
30
|
+
.sched-strip .kick{font-size:10px;letter-spacing:1.2px;text-transform:uppercase;color:var(--acc);font-weight:800;display:flex;align-items:center;gap:7px}
|
|
31
|
+
.sched-list{display:flex;gap:9px;flex-wrap:wrap;margin-top:8px}
|
|
32
|
+
.sched-chip{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--line);background:var(--panel);border-radius:999px;padding:5px 11px;font-size:11.5px;color:var(--dim)}
|
|
33
|
+
.sched-chip b{color:var(--tx);font-weight:600}.sched-chip .when{color:var(--acc);font-variant-numeric:tabular-nums}
|
|
34
|
+
.sched-chip.off{opacity:.55}
|
|
35
|
+
/* Board */
|
|
36
|
+
main{padding:14px}
|
|
37
|
+
.board{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;align-items:start}
|
|
38
|
+
.col{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:11px;min-width:0;box-shadow:var(--shadow)}
|
|
39
|
+
.col-head{display:flex;align-items:center;gap:8px;margin-bottom:10px}
|
|
40
|
+
.col-head h2{margin:0;font-size:11px;letter-spacing:.7px;text-transform:uppercase;color:var(--dim)}
|
|
41
|
+
.col-head .count{margin-left:auto;font-size:10px;border:1px solid var(--line);border-radius:999px;padding:1px 7px;color:var(--dim);font-variant-numeric:tabular-nums}
|
|
42
|
+
.col .accent{width:8px;height:8px;border-radius:50%;background:var(--dim);flex:none}
|
|
43
|
+
.col.pending .accent{background:var(--acc)}.col.running .accent{background:var(--ok);box-shadow:0 0 9px var(--ok);animation:beat 1.4s ease-in-out infinite}.col.done .accent{background:var(--ok)}.col.failed .accent{background:var(--err)}
|
|
44
|
+
.cards{display:flex;flex-direction:column;gap:9px;min-height:40px}
|
|
45
|
+
.tcard{border:1px solid var(--line);background:var(--panel2);border-radius:10px;padding:10px;animation:pop .3s cubic-bezier(.2,.8,.2,1)}
|
|
46
|
+
@keyframes pop{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
|
47
|
+
.tcard .top{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
48
|
+
.tcard .id{font:10px ui-monospace,Consolas,monospace;color:var(--dim)}
|
|
49
|
+
.badge{font-size:10px;border:1px solid var(--line);border-radius:999px;padding:1px 6px;color:var(--dim)}
|
|
50
|
+
.badge.sched{color:var(--acc);border-color:color-mix(in srgb,var(--acc) 45%,var(--line))}
|
|
51
|
+
.tcard .prompt{margin:6px 0 0;font-size:13px;line-height:1.45;word-break:break-word;display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical;overflow:hidden}
|
|
52
|
+
.tcard .acts{display:flex;gap:6px;flex-wrap:wrap;margin-top:9px}
|
|
53
|
+
.col.running .tcard{border-color:color-mix(in srgb,var(--ok) 42%,var(--line));box-shadow:0 0 0 1px color-mix(in srgb,var(--ok) 20%,transparent)}
|
|
54
|
+
.col.failed .tcard{border-color:color-mix(in srgb,var(--err) 34%,var(--line))}
|
|
55
|
+
.col-empty{color:var(--dim);font-size:12px;padding:18px 8px;text-align:center;border:1px dashed var(--line);border-radius:10px}
|
|
56
|
+
.run-state{font-size:11px;color:var(--ok);margin-top:6px;display:flex;align-items:center;gap:6px}
|
|
57
|
+
.run-state .rd{width:7px;height:7px;border-radius:50%;background:var(--ok);box-shadow:0 0 8px var(--ok);animation:beat 1.3s ease-in-out infinite}
|
|
58
|
+
@media(max-width:1100px){.board{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
59
|
+
@media(max-width:620px){.board{grid-template-columns:1fr}}
|
|
60
|
+
@media(prefers-reduced-motion:reduce){.dot.on,.col.running .accent,.run-state .rd{animation:none}.tcard{animation:none}}
|
|
61
|
+
</style>
|
|
62
|
+
<link rel="stylesheet" href="app.css">
|
|
63
|
+
<script src="app.js" defer></script>
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
<header>
|
|
67
|
+
<h1><span class="brand">CrewCtl</span> <span class="sep">·</span> Pano</h1>
|
|
68
|
+
<span class="pill"><span id="dot" class="dot"></span><span id="engineState">durdu</span></span>
|
|
69
|
+
<span id="modePill" class="pill">boşta</span>
|
|
70
|
+
<div class="grow"></div>
|
|
71
|
+
<a class="btn" href="flow.html"><button>Ekip Akışı</button></a>
|
|
72
|
+
<a class="btn" href="code.html"><button>Canlı Kod</button></a>
|
|
73
|
+
<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>
|
|
74
|
+
<a class="btn" href="index.html"><button>← Komuta Merkezi</button></a>
|
|
75
|
+
</header>
|
|
76
|
+
|
|
77
|
+
<div class="sched-strip" id="schedStrip" hidden>
|
|
78
|
+
<div class="kick"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg> Yaklaşan zamanlanmış görevler</div>
|
|
79
|
+
<div class="sched-list" id="schedList"></div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<main>
|
|
83
|
+
<div class="board">
|
|
84
|
+
<div class="col pending"><div class="col-head"><span class="accent"></span><h2>Bekleyen</h2><span class="count" id="cnt-pending">0</span></div><div class="cards" id="col-pending"></div></div>
|
|
85
|
+
<div class="col running"><div class="col-head"><span class="accent"></span><h2>Çalışıyor</h2><span class="count" id="cnt-running">0</span></div><div class="cards" id="col-running"></div></div>
|
|
86
|
+
<div class="col done"><div class="col-head"><span class="accent"></span><h2>Tamamlanan</h2><span class="count" id="cnt-done">0</span></div><div class="cards" id="col-done"></div></div>
|
|
87
|
+
<div class="col failed"><div class="col-head"><span class="accent"></span><h2>Başarısız</h2><span class="count" id="cnt-failed">0</span></div><div class="cards" id="col-failed"></div></div>
|
|
88
|
+
</div>
|
|
89
|
+
</main>
|
|
90
|
+
|
|
91
|
+
<script>
|
|
92
|
+
const $=s=>document.querySelector(s);
|
|
93
|
+
function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}
|
|
94
|
+
const CLI_LABEL={codex:'Codex',claude:'Claude Code',gemini:'Gemini',opencode:'OpenCode'};
|
|
95
|
+
function cliLabel(id){return CLI_LABEL[id]||(String(id||'').charAt(0).toUpperCase()+String(id||'').slice(1))}
|
|
96
|
+
const SCHED_DAYS=['Paz','Pzt','Sal','Çar','Per','Cum','Cmt'];
|
|
97
|
+
/* ---- Tema ---- */
|
|
98
|
+
function applyTheme(t){document.documentElement.setAttribute('data-theme',t);try{localStorage.setItem('cliTheme',t)}catch{}}
|
|
99
|
+
function toggleTheme(){applyTheme(document.documentElement.getAttribute('data-theme')==='light'?'dark':'light')}
|
|
100
|
+
(function(){let t='dark';try{t=localStorage.getItem('cliTheme')||'dark'}catch{}applyTheme(t)})();
|
|
101
|
+
|
|
102
|
+
const BOARD={queue:{pending:[],approval:[],done:[],failed:[]},status:{},schedules:[]};
|
|
103
|
+
function api(path,method='GET'){return fetch(path,{method}).then(async r=>{const j=await r.json().catch(()=>({}));if(!r.ok)throw new Error(j.error||('HTTP '+r.status));return j})}
|
|
104
|
+
|
|
105
|
+
/* ---- Kart ---- */
|
|
106
|
+
function taskCard(t,state){const opLabel=t.operatorCli?cliLabel(t.operatorCli):'';const sched=t.scheduleId?'<span class="badge sched">⏱ zamanlanmış</span>':'';
|
|
107
|
+
let acts='';
|
|
108
|
+
if(state==='pending')acts=`<button class="danger sm" onclick="delTask('${esc(t.id)}')">Sil</button>`;
|
|
109
|
+
else if((state==='done'||state==='failed')&&(t.delivery&&(t.delivery.files||[]).length))acts=`<a class="btn" href="code.html?task=${esc(t.id)}"><button class="sm">Kodu gör</button></a>`;
|
|
110
|
+
return `<div class="tcard"><div class="top"><span class="id">${esc(t.id)}</span><div class="grow"></div><span class="badge">${esc(t.executionMode||'auto')}</span>${opLabel?`<span class="badge">${esc(opLabel)}</span>`:''}${sched}</div><div class="prompt">${esc(t.prompt||'')}</div>${acts?`<div class="acts">${acts}</div>`:''}</div>`}
|
|
111
|
+
function runningCard(cur){const stage=cur.stage?`<span class="badge">${esc(cur.stage)}</span>`:'';return `<div class="tcard"><div class="top"><span class="id">${esc(cur.id||'')}</span><div class="grow"></div>${stage}</div><div class="prompt">${esc(cur.prompt||'')}</div><div class="run-state"><span class="rd"></span>${esc(cur.agent?cur.agent+' çalışıyor':'operatör çalışıyor')}</div></div>`}
|
|
112
|
+
function fill(id,html,count){$('#col-'+id).innerHTML=html||`<div class="col-empty">—</div>`;$('#cnt-'+id).textContent=count}
|
|
113
|
+
function renderBoard(){const q=BOARD.queue||{},cur=BOARD.status?.current||null,runId=cur&&cur.id;
|
|
114
|
+
const pending=(q.pending||[]).filter(t=>!runId||t.id!==runId);
|
|
115
|
+
fill('pending',pending.map(t=>taskCard(t,'pending')).join(''),pending.length);
|
|
116
|
+
fill('running',cur?runningCard(cur):'',cur?1:0);
|
|
117
|
+
fill('done',(q.done||[]).map(t=>taskCard(t,'done')).join(''),(q.done||[]).length);
|
|
118
|
+
fill('failed',(q.failed||[]).map(t=>taskCard(t,'failed')).join(''),(q.failed||[]).length)}
|
|
119
|
+
function applyStatus(s){BOARD.status=s||{};$('#dot').className='dot'+(s&&s.running?' on':'');$('#engineState').textContent=s&&s.running?'çalışıyor':'durdu';$('#modePill').textContent=s&&s.current?`${esc(s.current.agent||'operatör')} · ${esc(s.current.stage||'')}`:(s&&s.busy?'meşgul':'boşta');renderBoard()}
|
|
120
|
+
|
|
121
|
+
/* ---- Zamanlama şeridi ---- */
|
|
122
|
+
function schedTriggerText(s){const t=s.trigger||{};if(t.type==='interval'){const n=t.everyMinutes||0;return n%60===0&&n>=60?`Her ${n/60} saatte bir`:`Her ${n} dk`}if(t.type==='daily')return `Her gün ${esc(t.at||'')}`;if(t.type==='weekly')return `${(t.days||[]).map(d=>SCHED_DAYS[d]).join(', ')} ${esc(t.at||'')}`;return '—'}
|
|
123
|
+
function schedWhen(s){if(s.enabled===false)return 'devre dışı';if(!s.nextRunAt)return '—';try{return new Date(s.nextRunAt).toLocaleString('tr-TR',{dateStyle:'short',timeStyle:'short'})}catch{return '—'}}
|
|
124
|
+
function renderSchedules(){const list=(BOARD.schedules||[]).slice().sort((a,b)=>{const na=a.nextRunAt?Date.parse(a.nextRunAt):Infinity,nb=b.nextRunAt?Date.parse(b.nextRunAt):Infinity;return na-nb});const strip=$('#schedStrip');if(!list.length){strip.hidden=true;return}strip.hidden=false;$('#schedList').innerHTML=list.map(s=>`<span class="sched-chip${s.enabled===false?' off':''}" title="${esc(s.prompt||'')}"><b>${schedTriggerText(s)}</b><span class="when">${schedWhen(s)}</span></span>`).join('')}
|
|
125
|
+
|
|
126
|
+
/* ---- Eylemler ---- */
|
|
127
|
+
async function delTask(id){if(window.appConfirm&&!await appConfirm({title:'Görev silinsin mi?',body:'Bu bekleyen görev kuyruktan kaldırılacak.',confirmLabel:'Sil',cancelLabel:'Vazgeç',danger:true}))return;try{await api('/api/tasks/'+encodeURIComponent(id),'DELETE')}catch(e){alert(e.message)}}
|
|
128
|
+
|
|
129
|
+
/* ---- SSE ---- */
|
|
130
|
+
const es=new EventSource('/api/events');
|
|
131
|
+
function dispatch(type,data){if(type==='status')applyStatus(data);else if(type==='queue'){BOARD.queue=data;renderBoard()}else if(type==='schedules'){BOARD.schedules=data;renderSchedules()}}
|
|
132
|
+
for(const type of ['status','queue','schedules'])es.addEventListener(type,event=>{try{dispatch(type,JSON.parse(event.data))}catch{}});
|
|
133
|
+
window.addEventListener('pagehide',()=>{try{es.close()}catch{}});
|
|
134
|
+
window.addEventListener('pageshow',x=>{if(x.persisted)location.reload()});
|
|
135
|
+
|
|
136
|
+
/* ---- Açılış ---- */
|
|
137
|
+
async function boot(){try{const st=await api('/api/state');if(st.status)applyStatus(st.status);if(st.queue)BOARD.queue=st.queue;BOARD.schedules=st.schedules||[];renderBoard();renderSchedules()}catch(e){/* sessizce yeniden dene SSE ile */}}
|
|
138
|
+
boot();
|
|
139
|
+
</script>
|
|
140
|
+
</body>
|
|
141
|
+
</html>
|
|
@@ -0,0 +1,208 @@
|
|
|
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>Canlı Kodlama · CrewCtl</title>
|
|
7
|
+
<style>
|
|
8
|
+
*{box-sizing:border-box}
|
|
9
|
+
:root{--bg:#090c12;--panel:#111826;--panel2:#182234;--line:#243149;--tx:#e9eef7;--dim:#8494ab;--acc:#5e8bff;--ok:#2fd08a;--warn:#eab24d;--err:#f2565f;--purple:#a98bff;
|
|
10
|
+
--headerbg:#0a0f1acc;--input:#0b111c;--nodebg:#0b1220;--shadow:0 14px 40px #0008;
|
|
11
|
+
--appbg:radial-gradient(1200px 620px at 50% -14%,#5e8bff16,transparent),radial-gradient(1000px 520px at 90% -6%,#a98bff12,transparent),var(--bg)}
|
|
12
|
+
[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;
|
|
13
|
+
--headerbg:#ffffffe6;--input:#f4f7fd;--nodebg:#f7f9fd;--shadow:0 14px 36px #7a90b820;
|
|
14
|
+
--appbg:radial-gradient(1200px 620px at 50% -14%,#3a63f012,transparent),radial-gradient(1000px 520px at 90% -6%,#7350f00e,transparent),var(--bg)}
|
|
15
|
+
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}
|
|
16
|
+
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}
|
|
17
|
+
button:hover{border-color:var(--acc);box-shadow:0 4px 14px color-mix(in srgb,var(--acc) 16%,transparent)}button:active{transform:translateY(1px)}
|
|
18
|
+
a.btn{text-decoration:none;display:inline-block}
|
|
19
|
+
code,pre{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}
|
|
20
|
+
/* Header */
|
|
21
|
+
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}
|
|
22
|
+
h1{font-size:15px;margin:0;letter-spacing:.2px}.brand{color:var(--acc);font-weight:800}.sep{color:var(--dim)}.grow{flex:1}
|
|
23
|
+
.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}
|
|
24
|
+
.dot{width:8px;height:8px;border-radius:50%;background:var(--dim)}
|
|
25
|
+
.dot.on{background:var(--ok);box-shadow:0 0 10px var(--ok);animation:beat 1.6s ease-in-out infinite}
|
|
26
|
+
@keyframes beat{50%{box-shadow:0 0 3px var(--ok)}}
|
|
27
|
+
/* Banner + tiles */
|
|
28
|
+
.topbar{padding:14px 14px 0}
|
|
29
|
+
.banner{background:linear-gradient(120deg,var(--panel),var(--panel2));border:1px solid var(--line);border-radius:16px;padding:15px 18px;box-shadow:var(--shadow);display:flex;gap:16px;align-items:center;flex-wrap:wrap}
|
|
30
|
+
.banner .kick{font-size:10px;letter-spacing:1.4px;text-transform:uppercase;color:var(--acc);font-weight:800}
|
|
31
|
+
.banner .t{font-weight:800;font-size:17px;line-height:1.28;margin-top:2px;max-width:70ch}
|
|
32
|
+
.now{display:flex;align-items:center;gap:8px;color:var(--dim);font-size:12.5px;margin-top:6px}
|
|
33
|
+
.now .ndot{width:9px;height:9px;border-radius:50%;background:var(--dim);flex:none}
|
|
34
|
+
.now.live .ndot{background:var(--acc);box-shadow:0 0 10px var(--acc);animation:beat 1.3s ease-in-out infinite}
|
|
35
|
+
.now.done .ndot{background:var(--ok);box-shadow:0 0 8px var(--ok)}
|
|
36
|
+
.now.fail .ndot{background:var(--err);box-shadow:0 0 8px var(--err)}
|
|
37
|
+
.now b{color:var(--tx);font-weight:700}
|
|
38
|
+
.tiles{display:grid;grid-template-columns:repeat(4,minmax(96px,1fr));gap:10px;margin-left:auto}
|
|
39
|
+
.tile{background:color-mix(in srgb,var(--panel2) 60%,transparent);border:1px solid var(--line);border-radius:12px;padding:9px 12px;min-width:96px}
|
|
40
|
+
.tile .k{font-size:9.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--dim)}
|
|
41
|
+
.tile .v{font-size:22px;font-weight:800;margin-top:1px;font-variant-numeric:tabular-nums;line-height:1.1}
|
|
42
|
+
.tile.add .v{color:var(--ok)}.tile.del .v{color:var(--err)}
|
|
43
|
+
.chips{display:flex;gap:8px;flex-wrap:wrap;margin:12px 14px 0}
|
|
44
|
+
.chip{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);background:var(--panel);border-radius:999px;padding:5px 11px;font-size:11.5px;color:var(--dim)}
|
|
45
|
+
.chip b{font-variant-numeric:tabular-nums}
|
|
46
|
+
.chip .c-created{color:var(--ok)}.chip .c-modified{color:var(--warn)}.chip .c-deleted{color:var(--err)}
|
|
47
|
+
/* Diff list */
|
|
48
|
+
main{padding:14px}
|
|
49
|
+
.panel{background:var(--panel);border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow);padding:14px}
|
|
50
|
+
.panel-head{display:flex;align-items:center;gap:10px;margin-bottom:10px}.panel-head h2{margin:0;font-size:13px;letter-spacing:.2px}
|
|
51
|
+
.muted{color:var(--dim);font-size:12px}
|
|
52
|
+
.legend{display:flex;gap:12px;flex-wrap:wrap;color:var(--dim);font-size:11px;margin-left:auto}
|
|
53
|
+
.legend i{font-style:normal;font-weight:700}.legend .lg-add{color:var(--ok)}.legend .lg-mod{color:var(--warn)}.legend .lg-del{color:var(--err)}
|
|
54
|
+
.empty{padding:34px 14px;text-align:center;color:var(--dim);border:1px dashed var(--line);border-radius:12px;background:color-mix(in srgb,var(--panel2) 40%,transparent)}
|
|
55
|
+
.files{display:flex;flex-direction:column;gap:8px}
|
|
56
|
+
.diff-file{border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--panel);animation:pop .3s cubic-bezier(.2,.8,.2,1)}
|
|
57
|
+
@keyframes pop{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
|
58
|
+
.diff-file>summary{display:flex;align-items:center;gap:9px;padding:9px 11px;background:var(--panel2);list-style:none;font:11.5px ui-monospace;cursor:pointer}
|
|
59
|
+
.diff-file>summary::-webkit-details-marker{display:none}
|
|
60
|
+
.diff-file>summary:before{content:'›';font-size:17px;color:var(--dim);transition:.16s}.diff-file[open]>summary:before{transform:rotate(90deg)}
|
|
61
|
+
.diff-action{font-weight:800;text-transform:uppercase;font-size:9px;letter-spacing:.04em;padding:2px 6px;border-radius:5px;border:1px solid transparent}
|
|
62
|
+
.diff-action.created{color:var(--ok);background:color-mix(in srgb,var(--ok) 14%,transparent);border-color:color-mix(in srgb,var(--ok) 35%,transparent)}
|
|
63
|
+
.diff-action.modified{color:var(--warn);background:color-mix(in srgb,var(--warn) 14%,transparent);border-color:color-mix(in srgb,var(--warn) 35%,transparent)}
|
|
64
|
+
.diff-action.deleted{color:var(--err);background:color-mix(in srgb,var(--err) 14%,transparent);border-color:color-mix(in srgb,var(--err) 35%,transparent)}
|
|
65
|
+
.diff-path{color:var(--tx);font-weight:700;word-break:break-all}
|
|
66
|
+
.diff-stats{margin-left:auto;display:flex;gap:8px;white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
67
|
+
.diff-add{color:var(--ok)}.diff-del{color:var(--err)}
|
|
68
|
+
.diff-code{overflow:auto;font:11px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;background:var(--input)}
|
|
69
|
+
.diff-hunk{padding:4px 10px;color:var(--acc);background:color-mix(in srgb,var(--acc) 12%,transparent);border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
|
|
70
|
+
.diff-line{display:grid;grid-template-columns:44px 44px 18px minmax(max-content,1fr);min-width:max-content;white-space:pre}
|
|
71
|
+
.diff-line .ln{padding:0 7px;text-align:right;color:var(--dim);user-select:none;border-right:1px solid var(--line);opacity:.75}
|
|
72
|
+
.diff-line .mark{padding-left:6px;user-select:none}.diff-line .code{padding:0 12px 0 5px;color:var(--tx)}
|
|
73
|
+
.diff-line.add{background:color-mix(in srgb,var(--ok) 13%,transparent)}.diff-line.add .mark{color:var(--ok)}
|
|
74
|
+
.diff-line.delete{background:color-mix(in srgb,var(--err) 13%,transparent)}.diff-line.delete .mark{color:var(--err)}
|
|
75
|
+
.diff-note{padding:9px 11px;color:var(--dim);font-size:11px}.diff-note.warn{color:var(--warn)}
|
|
76
|
+
.revert{border-color:color-mix(in srgb,var(--err) 45%,var(--line));color:var(--err);font-weight:700}
|
|
77
|
+
.revert:hover{border-color:var(--err);box-shadow:0 4px 14px color-mix(in srgb,var(--err) 20%,transparent)}
|
|
78
|
+
.revert:disabled{opacity:.45;cursor:default;box-shadow:none}
|
|
79
|
+
#toasts{position:fixed;right:16px;bottom:16px;display:flex;flex-direction:column;gap:8px;z-index:50}
|
|
80
|
+
.toast{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:10px 14px;box-shadow:var(--shadow);font-size:13px;max-width:360px;transition:opacity .3s}
|
|
81
|
+
.toast.ok{border-color:color-mix(in srgb,var(--ok) 45%,var(--line))}.toast.err{border-color:color-mix(in srgb,var(--err) 45%,var(--line))}
|
|
82
|
+
</style>
|
|
83
|
+
<link rel="stylesheet" href="app.css">
|
|
84
|
+
<script src="app.js" defer></script>
|
|
85
|
+
</head>
|
|
86
|
+
<body>
|
|
87
|
+
<header>
|
|
88
|
+
<h1><span class="brand">CrewCtl</span> <span class="sep">·</span> Canlı Kodlama</h1>
|
|
89
|
+
<span class="pill"><span id="dot" class="dot"></span><span id="engineState">durdu</span></span>
|
|
90
|
+
<span id="modePill" class="pill">boşta</span>
|
|
91
|
+
<div class="grow"></div>
|
|
92
|
+
<button id="revertBtn" class="revert" hidden onclick="revertVersion()" title="Bu görev öncesindeki koda dön"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>Önceki sürüme dön</button>
|
|
93
|
+
<a class="btn" href="flow.html"><button>Ekip Akışı</button></a>
|
|
94
|
+
<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>
|
|
95
|
+
<a class="btn" href="index.html"><button>← Komuta Merkezi</button></a>
|
|
96
|
+
</header>
|
|
97
|
+
|
|
98
|
+
<div class="topbar">
|
|
99
|
+
<div class="banner">
|
|
100
|
+
<div style="min-width:0">
|
|
101
|
+
<div class="kick">Canlı Kodlama</div>
|
|
102
|
+
<div class="t" id="missionTitle">Görev bekleniyor</div>
|
|
103
|
+
<div class="now" id="nowLine"><span class="ndot"></span><span id="nowText">Bir görev başladığında burada ne yapıldığı canlı görünür.</span></div>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="tiles">
|
|
106
|
+
<div class="tile"><div class="k">Değişen dosya</div><div class="v" id="tFiles">0</div></div>
|
|
107
|
+
<div class="tile add"><div class="k">Eklenen satır</div><div class="v" id="tAdd">+0</div></div>
|
|
108
|
+
<div class="tile del"><div class="k">Silinen satır</div><div class="v" id="tDel">−0</div></div>
|
|
109
|
+
<div class="tile"><div class="k">Tur · Çağrı</div><div class="v" id="tRounds" style="font-size:15px">0 · 0</div></div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="chips">
|
|
113
|
+
<span class="chip"><span class="c-created">●</span> <b id="cCreated">0</b> oluşturuldu</span>
|
|
114
|
+
<span class="chip"><span class="c-modified">●</span> <b id="cModified">0</b> değişti</span>
|
|
115
|
+
<span class="chip"><span class="c-deleted">●</span> <b id="cDeleted">0</b> silindi</span>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<main>
|
|
120
|
+
<div class="panel">
|
|
121
|
+
<div class="panel-head">
|
|
122
|
+
<h2>Değişen dosyalar</h2>
|
|
123
|
+
<span class="muted" id="filesSub">görev başından bu yana toplam</span>
|
|
124
|
+
<span class="legend"><span class="lg-add"><i>+</i> eklenen</span><span class="lg-del"><i>−</i> silinen</span> <span><i class="lg-add">eklendi</i> · <i class="lg-mod">değişti</i> · <i class="lg-del">silindi</i></span></span>
|
|
125
|
+
</div>
|
|
126
|
+
<div id="files" class="files"><div class="empty">Henüz dosya değişikliği yok. Bir görev çalışırken oluşturulan/değişen dosyalar burada, satır satır farkıyla belirir.</div></div>
|
|
127
|
+
</div>
|
|
128
|
+
</main>
|
|
129
|
+
|
|
130
|
+
<script>
|
|
131
|
+
const $=s=>document.querySelector(s);
|
|
132
|
+
function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}
|
|
133
|
+
/* ---- Tema ---- */
|
|
134
|
+
function applyTheme(t){document.documentElement.setAttribute('data-theme',t);try{localStorage.setItem('cliTheme',t)}catch{}}
|
|
135
|
+
function toggleTheme(){applyTheme(document.documentElement.getAttribute('data-theme')==='light'?'dark':'light')}
|
|
136
|
+
(function(){let t='dark';try{t=localStorage.getItem('cliTheme')||'dark'}catch{}applyTheme(t)})();
|
|
137
|
+
|
|
138
|
+
/* ---- Durum: "şu an ne oluyor" ---- */
|
|
139
|
+
function setNow(kind,text){const line=$('#nowLine');line.className='now'+(kind?(' '+kind):'');$('#nowText').innerHTML=text}
|
|
140
|
+
function roleOf(stage,agent){const s=String(stage||'');if(s.startsWith('operator'))return 'Operatör';return esc(agent||'Uzman')}
|
|
141
|
+
function phaseOf(stage){const s=String(stage||'');if(s==='operator-plan')return 'planı hazırlıyor';if(s==='operator-review')return 'sonucu değerlendiriyor';if(s==='operator-chat')return 'soruyu yanıtlıyor';if(s.startsWith('operator'))return 'ekibi yönetiyor';if(s==='delegate')return 'delege edilen işi uyguluyor';return 'çalışıyor'}
|
|
142
|
+
function applyStatus(s){$('#dot').className='dot'+(s.running?' on':'');$('#engineState').textContent=s.running?'çalışıyor':'durdu';$('#modePill').textContent=s.current?`${roleOf(s.current.stage,s.current.agent)} · ${s.current.stage||''}`:(s.busy?'meşgul':'boşta');engineActive=!!s.current;if(s.current&&s.current.id)currentTaskId=s.current.id;updateRevert()}
|
|
143
|
+
/* ---- Geri dönüş: bu görev öncesindeki sürüme dön ---- */
|
|
144
|
+
let currentTaskId='',engineActive=false;
|
|
145
|
+
function setCurrentTask(id){if(id&&id!==currentTaskId)currentTaskId=id;updateRevert()}
|
|
146
|
+
function updateRevert(){const b=$('#revertBtn');if(!b)return;if(!currentTaskId){b.hidden=true;return}b.hidden=false;b.disabled=!!engineActive;b.title=engineActive?'Görev çalışırken geri alınamaz; bitmesini bekleyin':'Bu görev öncesindeki koda dön'}
|
|
147
|
+
function toast(kind,msg){let box=document.getElementById('toasts');if(!box){box=document.createElement('div');box.id='toasts';document.body.appendChild(box)}const t=document.createElement('div');t.className='toast '+(kind||'');t.textContent=msg;box.appendChild(t);setTimeout(()=>{t.style.opacity='0';setTimeout(()=>t.remove(),300)},4600)}
|
|
148
|
+
async function revertVersion(){
|
|
149
|
+
if(!currentTaskId){toast('err','Geri dönülecek görev yok.');return}
|
|
150
|
+
if(engineActive){toast('err','Görev çalışırken geri alınamaz; bitmesini bekleyin.');return}
|
|
151
|
+
if(!await appConfirm({title:'Önceki sürüme dönülsün mü?',body:'Bu görev çalışmadan önceki koda dönülür:\n• Görev sonrası oluşan dosyalar silinir\n• Değişen/silinen dosyalar eski haline döner\n\nGeri alma işleminin kendisi de bir sürüm oluşturur, yani geri alınabilir.',confirmLabel:'Önceki sürüme dön',cancelLabel:'Vazgeç',danger:true}))return;
|
|
152
|
+
const b=$('#revertBtn');if(b)b.disabled=true;
|
|
153
|
+
try{
|
|
154
|
+
const r=await fetch(`/api/tasks/${currentTaskId}/restore`,{method:'POST'}).then(async res=>{const j=await res.json().catch(()=>({}));if(!res.ok)throw new Error(j.error||('HTTP '+res.status));return j});
|
|
155
|
+
toast('ok',`↩ Önceki sürüme dönüldü · ${r.restored} dosya geri yazıldı · ${r.deleted} silindi`);
|
|
156
|
+
setNow('done',`↩ <b>Önceki sürüme dönüldü</b> · ${r.restored} dosya geri yazıldı · ${r.deleted} sonradan oluşan dosya silindi`);
|
|
157
|
+
}catch(e){toast('err','Geri alınamadı: '+e.message)}
|
|
158
|
+
finally{if(b)b.disabled=false;updateRevert()}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* ---- Toplamlar (tiles + chips) ---- */
|
|
162
|
+
function applyTotals(e){const c=e.counts||{created:0,modified:0,deleted:0},lc=e.lineCounts||{additions:0,deletions:0};const total=(c.created||0)+(c.modified||0)+(c.deleted||0);$('#tFiles').textContent=total;$('#tAdd').textContent='+'+(lc.additions||0);$('#tDel').textContent='−'+(lc.deletions||0);$('#cCreated').textContent=c.created||0;$('#cModified').textContent=c.modified||0;$('#cDeleted').textContent=c.deleted||0}
|
|
163
|
+
function resetTotals(){applyTotals({counts:{created:0,modified:0,deleted:0},lineCounts:{additions:0,deletions:0}})}
|
|
164
|
+
function setRounds(rounds,calls){$('#tRounds').textContent=`${rounds??0} · ${calls??0}`}
|
|
165
|
+
|
|
166
|
+
/* ---- Diff render ---- */
|
|
167
|
+
function diffPreviewMessage(status){return {redacted:'Hassas dosya içeriği güvenlik için gizlendi.',binary:'İkili dosya; satır önizlemesi gösterilemiyor.','too-large':'Dosya canlı satır önizleme sınırından büyük.','too-many-lines':'Dosya canlı önizleme için çok fazla satır içeriyor.','baseline-limit':'Görev başlangıcı içeriği önizleme bütçesine alınmadı.','event-limit':'Bu olayın satır önizleme sınırına ulaşıldı.',unavailable:'Dosya içeriği okunamadı.'}[status]||'Satır önizlemesi kullanılamıyor.'}
|
|
168
|
+
function renderDiffLine(line){const mark=line.type==='add'?'+':line.type==='delete'?'−':' ';return `<div class="diff-line ${esc(line.type)}"><span class="ln">${line.oldNumber??''}</span><span class="ln">${line.newNumber??''}</span><span class="mark">${mark}</span><span class="code">${esc(line.text)}</span></div>`}
|
|
169
|
+
function renderDiffFile(f,expanded){const labels={created:'eklendi',modified:'değişti',deleted:'silindi'},stats=f.additions==null?'<span class="muted">önizleme yok</span>':`<span class="diff-add">+${f.additions||0}</span><span class="diff-del">−${f.deletions||0}</span>`;let body='';if(f.previewStatus)body=`<div class="diff-note warn">${esc(diffPreviewMessage(f.previewStatus))}</div>`;else if(!(f.hunks||[]).length)body='<div class="diff-note">Satır farkı yok; yalnızca dosya metadatası değişmiş olabilir.</div>';else body=(f.hunks||[]).map(h=>`<div class="diff-hunk">@@ -${h.oldStart},${h.oldLines} +${h.newStart},${h.newLines} @@</div>${(h.lines||[]).map(renderDiffLine).join('')}`).join('')+(f.approximate?'<div class="diff-note warn">Büyük dosya: değişen bölüm yaklaşık olarak gösteriliyor.</div>':'')+(f.truncated?'<div class="diff-note warn">Diff önizlemesi kısaltıldı; dosyada daha fazla değişiklik var.</div>':'');return `<details class="diff-file"${expanded?' open':''}><summary><span class="diff-action ${esc(f.action)}">${esc(labels[f.action]||f.action)}</span><span class="diff-path">${esc(f.path)}</span><span class="diff-stats">${stats}</span></summary><div class="diff-code">${body}</div></details>`}
|
|
170
|
+
function renderFiles(e){const box=$('#files'),files=e.files||[];applyTotals(e);if(!files.length){box.innerHTML='<div class="empty">Henüz dosya değişikliği yok. Bir görev çalışırken oluşturulan/değişen dosyalar burada, satır satır farkıyla belirir.</div>';return}const expanded=files.length<=5;box.innerHTML=files.map(f=>renderDiffFile(f,expanded)).join('')}
|
|
171
|
+
function resetView(title){$('#missionTitle').textContent=title||'Görev bekleniyor';resetTotals();setRounds(0,0);$('#files').innerHTML='<div class="empty">Görev başladı; ilk dosya değişiklikleri birazdan görünecek…</div>'}
|
|
172
|
+
|
|
173
|
+
/* ---- Canlı olaylar ---- */
|
|
174
|
+
let liveCalls=0,bootstrapping=true,bufferedEvents=[];
|
|
175
|
+
function onLog(e){if(e.level==='task'){liveCalls=0;const title=String(e.msg||'').replace(/^GOREV\s+[^:]+:\s*/,'').slice(0,240)||'Yeni görev';resetView(title);setNow('live','<b>Operatör</b> hedefi analiz ediyor ve uygun uzmanları seçiyor…')}}
|
|
176
|
+
function onActivity(e){if(e.kind==='process.started'){if(String(e.stage||'').includes('operator')||e.stage==='delegate')liveCalls++;setRounds(currentRounds,liveCalls);setNow('live',`<b>${roleOf(e.stage,e.agent)}</b> ${phaseOf(e.stage)}…`)}else if(e.kind==='stdout'){setNow('live',`<b>${roleOf(e.stage,e.agent)}</b> ${phaseOf(e.stage)}…`)}else if(e.kind==='process.finished'){const sec=((e.durationMs||0)/1000).toFixed(1),failed=Number(e.exitCode)!==0;setNow(failed?'fail':'live',`<b>${roleOf(e.stage,e.agent)}</b> ${failed?'tamamlayamadı':'adımı tamamladı'} · ${sec}s`)}}
|
|
177
|
+
let currentRounds=0;
|
|
178
|
+
function onResult(r){if(r.kind==='operator-chat')return;currentRounds=r.rounds||currentRounds;setRounds(currentRounds,liveCalls);const files=(r.delivery?.files||[]).length;setNow('done',`✓ <b>Görev tamamlandı</b> · ${r.operator||'Operatör'} · ${r.rounds||0} tur · ${files} dosya${r.delivery?.verification?` · ✓ ${esc(r.delivery.verification)}`:''}`)}
|
|
179
|
+
|
|
180
|
+
/* ---- SSE ---- */
|
|
181
|
+
const es=new EventSource('/api/events');
|
|
182
|
+
function dispatchLiveEvent(type,data){if(data&&data.taskId)setCurrentTask(data.taskId);if(type==='status')applyStatus(data);else if(type==='filechange')renderFiles(data);else if(type==='activity')onActivity(data);else if(type==='log')onLog(data);else if(type==='result')onResult(data)}
|
|
183
|
+
function receiveLiveEvent(type,event){const data=JSON.parse(event.data);if(bootstrapping)bufferedEvents.push({type,data});else dispatchLiveEvent(type,data)}
|
|
184
|
+
for(const type of ['status','filechange','activity','log','result'])es.addEventListener(type,event=>receiveLiveEvent(type,event));
|
|
185
|
+
window.addEventListener('pagehide',()=>{try{es.close()}catch{}});
|
|
186
|
+
window.addEventListener('pageshow',x=>{if(x.persisted)location.reload()});
|
|
187
|
+
|
|
188
|
+
/* ---- Açılış: ?task önceliklidir; yoksa aktif görevin geçmişini otomatik yükle ---- */
|
|
189
|
+
function resolveBootTaskId(state,search=location.search){const q=state&&state.queue;const done=(q&&q.done)||[],failed=(q&&q.failed)||[];const recent=done.concat(failed).filter(t=>t&&t.kind!=='operator-chat').sort((a,b)=>a.id<b.id?1:-1)[0];return new URLSearchParams(search).get('task')||state?.status?.current?.id||(recent&&recent.id)||''}
|
|
190
|
+
async function boot(){
|
|
191
|
+
let replayed=[];
|
|
192
|
+
try{
|
|
193
|
+
const st=await fetch('/api/state').then(r=>r.ok?r.json():null);if(st&&st.status)applyStatus(st.status);
|
|
194
|
+
const id=resolveBootTaskId(st);if(!id)return;
|
|
195
|
+
setCurrentTask(id);
|
|
196
|
+
const found=await fetch(`/api/tasks/${id}`).then(r=>r.ok?r.json():null);
|
|
197
|
+
const task=found&&found.task;
|
|
198
|
+
if(task){$('#missionTitle').textContent=String(task.prompt||'').slice(0,240)||id;$('#filesSub').textContent=`${id} · toplam değişiklik`;const d=task.delivery||{};if(task.status==='done')setNow('done',`✓ <b>Tamamlandı</b> · ${task.operatorCli||'Operatör'} · ${d.rounds||0} tur${d.verification?` · ✓ ${esc(d.verification)}`:''}`);else if(task.status==='failed')setNow('fail',`✕ <b>Başarısız</b> · ${esc(String(task.error||'').slice(0,140))}`);setRounds(d.rounds||0,0)}
|
|
199
|
+
replayed=await fetch(`/api/tasks/${id}/events?limit=1500`).then(r=>r.ok?r.json():[]);
|
|
200
|
+
for(const event of replayed){if(event.type==='log'||event.type==='activity'||event.type==='result')dispatchLiveEvent(event.type,event)}
|
|
201
|
+
const last=[...replayed].reverse().find(event=>event.type==='filechange');
|
|
202
|
+
if(last)renderFiles(last);
|
|
203
|
+
}catch{}finally{bootstrapping=false;const seen=new Set(replayed.map(event=>JSON.stringify(event))),pending=bufferedEvents;bufferedEvents=[];for(const event of pending){if(seen.has(JSON.stringify(event.data)))continue;dispatchLiveEvent(event.type,event.data)}}
|
|
204
|
+
}
|
|
205
|
+
boot();
|
|
206
|
+
</script>
|
|
207
|
+
</body>
|
|
208
|
+
</html>
|