beast-agent 2.17.0 → 2.18.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.17.0",
4
+ "version": "2.18.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -1830,6 +1830,11 @@ class Engine {
1830
1830
  : 'ROL: GÖZLEMCİ ANALİST 🔍 — tur tur piyasa tararsın ama OTOMATİK İŞLEM KAPALI: mt5_trade/mt5_pending KULLANMA. Fırsatları net öneri olarak yaz (sembol, yön, giriş, SL, TP, sebep); sahibi onaylarsa açılır.'
1831
1831
  : 'ROL: TRADING CO-PİLOT 💬 — sahibinle sohbet ediyorsun. Veri isterse mt5_* araçlarıyla çek, analiz et; işlem istenirse limitler içinde uygula, istenmezse sadece öneri ver.';
1832
1832
  const proj = this._projectInstructions(session);
1833
+ /* Skills kataloğu: finance oturumlarında da skill'ler çağrılabilir
1834
+ (skill aracı _chatTurn'de finance oturumlarına enjekte edilir). */
1835
+ const skList = (() => {
1836
+ try { return skills.scan(); } catch { return []; }
1837
+ })();
1833
1838
  return (
1834
1839
  'Sen BEAST FİNANS\u2019sın — bilgisayardaki MetaTrader 5 (MT5) terminaline köprüyle bağlı trading ajanı.\n' +
1835
1840
  'ORTAM: Beast Finance uygulamasındasın — solda sohbet geçmişi + izleyiciler, ortada bu sohbet, SAĞ panelde MT5 işlem panosu (hesap, pozisyonlar, piyasa, AKIŞ) var.\n' +
@@ -1846,6 +1851,10 @@ class Engine {
1846
1851
  '- Emin olmadığında İŞLEM YOK — "BEKLE: <sebep>" yaz. Sık işlem > iyi işlem.\n' +
1847
1852
  '- Hesap kaldıracı ve serbest marja göre pozisyon boyutunu düşük tut; tek işlemde serbest marjın büyük kısmını riske atma.\n' +
1848
1853
  (session && session.financeStrategy ? `SAHİBİNİN STRATEJİ NOTU (önceliklidir):\n${session.financeStrategy}\n` : '') +
1854
+ (skList.length
1855
+ ? '# SKILLS\nKurulu skill kataloğu: görev bir skill\u2019in tanımına uyarsa skill aracıyla gövdesini (SKILL.md) yükleyip uygula — tekerleği yeniden icat etme. Analysis/rapor/PDF gibi işlerde skill\u2019in prosedürüne uy.\n' +
1856
+ skList.map((s) => `- ${s.name}: ${s.description} [${s.path}]`).join('\n') + '\n'
1857
+ : '') +
1849
1858
  'RAPOR DİSİPLİNİ: kısa ve sayısal — sembol, yön, lot, giriş, SL/TP, sebep tek satırda. Uzun fal açma; tablo/liste kullanabilirsin.\n' +
1850
1859
  `Yerel zaman: ${localDate} ${localTime} — piyasa saatlerine dikkat et (borsa/metal seansları kapalıysa spread genişler; hafta sonu FX kapalıdır).\n` +
1851
1860
  (proj ? '# PROJE TALİMATLARI\n' + proj + '\n' : '') +
@@ -3402,6 +3411,16 @@ class Engine {
3402
3411
  toolsList = await mcp.mergeTools(toolsList);
3403
3412
  /* Beast Apps: kurulu app'lerin araçları (app__<id>__<tool>) modele açılır */
3404
3413
  toolsList = apps.mergeTools(toolsList);
3414
+ /* Beast Finance: skill aracı da açık — kataloğun SKILL.md gövdesi
3415
+ modele açılır (skill handler'ı zaten tüm oturumlar için çalışır,
3416
+ definition yalnız BC'de vardı; finance chat copilot + trader ikisi de)
3417
+ Not: BC oturumları zaten opencode registry'sinden skill'i görür. */
3418
+ if (session && session.finance) {
3419
+ const skillDef = (opencode.toolmap.definitions() || []).find(
3420
+ (d) => d && d.function && d.function.name === 'skill'
3421
+ );
3422
+ if (skillDef) toolsList = [...toolsList, skillDef];
3423
+ }
3405
3424
  /* panel_run yalnız SANDBOX oturumlarında görünsün — diğer panellerde
3406
3425
  modelin alet çantasında olmasın (hook olmadan çalışmaz) */
3407
3426
  if (!(session && session.sbSandbox)) {
@@ -4653,6 +4672,26 @@ const skills = require('./skills');
4653
4672
  return null;
4654
4673
  }
4655
4674
 
4675
+ /* opencode skill aracı gövdesi: katalogdan eşleşen SKILL.md'yi okur.
4676
+ Salt-okur — tüm oturum türlerinde (BC/finance/chat/trader) aynı yol. */
4677
+ _skillBody(args) {
4678
+ const wanted = String((args && args.name) || '').trim();
4679
+ let list = [];
4680
+ try { list = skills.scan(); } catch {}
4681
+ const hit = list.find((s) => s && s.name === wanted) || null;
4682
+ if (!hit) {
4683
+ return {
4684
+ ok: false,
4685
+ error:
4686
+ 'skill bulunamadı: ' + (wanted || '(boş)') +
4687
+ (list.length ? ' — mevcut: ' + list.map((s) => s.name).join(', ') : ''),
4688
+ };
4689
+ }
4690
+ let body = '';
4691
+ try { body = fs.readFileSync(hit.path, 'utf8'); } catch {}
4692
+ return { ok: true, name: hit.name, path: hit.path, content: body.slice(0, 24000) };
4693
+ }
4694
+
4656
4695
  async _execTool(name, args, signal, sessionId) {
4657
4696
  try {
4658
4697
  /* opencode mantığı (BC): model opencode builtin dilinde çağırır —
@@ -4669,6 +4708,12 @@ const skills = require('./skills');
4669
4708
  /* ANA KOD KİLİDİ: korumalı bölgeye yazma/silme girişimi daha kapıdan geçmez */
4670
4709
  const blocked = this._guardTool(name, args);
4671
4710
  if (blocked) return blocked;
4711
+ /* opencode skill aracı: katalogdaki SKILL.md gövdesini modele açar.
4712
+ Salt-okur ve güvenli — BC dahil TÜM oturum türlerinde çalışır
4713
+ (Beast Finance chat copilot + trader buradan kullanır). */
4714
+ if (name === 'skill') {
4715
+ return JSON.stringify(this._skillBody(args));
4716
+ }
4672
4717
  if (isBc) {
4673
4718
  /* opencode permission akışı (tool/external-directory.ts + her aracın
4674
4719
  ctx.ask'ı): önce workspace DIŞI erişim izni, sonra araç izni.
@@ -4695,24 +4740,6 @@ const skills = require('./skills');
4695
4740
  } catch (permErr) {
4696
4741
  return JSON.stringify({ ok: false, error: AskService.errorText(permErr) });
4697
4742
  }
4698
- /* opencode skill aracı: katalogdaki SKILL.md gövdesini modele açar */
4699
- if (name === 'skill') {
4700
- const wanted = String((args && args.name) || '').trim();
4701
- let list = [];
4702
- try { list = skills.scan(); } catch {}
4703
- const hit = list.find((s) => s && s.name === wanted) || null;
4704
- if (!hit) {
4705
- return JSON.stringify({
4706
- ok: false,
4707
- error:
4708
- 'skill bulunamadı: ' + (wanted || '(boş)') +
4709
- (list.length ? ' — mevcut: ' + list.map((s) => s.name).join(', ') : ''),
4710
- });
4711
- }
4712
- let body = '';
4713
- try { body = fs.readFileSync(hit.path, 'utf8'); } catch {}
4714
- return JSON.stringify({ ok: true, name: hit.name, path: hit.path, content: body.slice(0, 24000) });
4715
- }
4716
4743
  } else {
4717
4744
  /* onay kapısı: riskli araçta dış onay bekle; reddedilirse araç çalışmaz.
4718
4745
  "always" onaylı araçlar doğrudan geçer. (BC dışı oturumlar — opencode
@@ -5969,6 +5996,25 @@ Engine.prototype.undoLastTodo = function (sid) {
5969
5996
  return this.undoTodo(sid, j[j.length - 1].todoId);
5970
5997
  };
5971
5998
 
5999
+ /* /deltodo: oturumun todo listesini tamamen temizler. Dosyaya BOŞ t:todo
6000
+ kaydı append edilir — yüklemede son kayıt kazanır, restart sonrası da
6001
+ boş kalır (eski t:todo satırları yeniden canlanamaz). */
6002
+ Engine.prototype.clearTodos = function (sid) {
6003
+ const key = String(sid || '');
6004
+ try {
6005
+ if (!key || !fs.existsSync(this._file(key))) return { ok: false, error: 'oturum bulunamadı' };
6006
+ } catch {
6007
+ return { ok: false, error: 'oturum bulunamadı' };
6008
+ }
6009
+ const prev = this.todos.get(key) || [];
6010
+ this.todos.set(key, []);
6011
+ try {
6012
+ fs.appendFileSync(this._file(key), JSON.stringify({ t: 'todo', items: [] }) + '\n');
6013
+ } catch {}
6014
+ emitSafe(this, key, { type: 'todos', sessionId: key, todos: [] });
6015
+ return { ok: true, count: prev.length };
6016
+ };
6017
+
5972
6018
  module.exports = Engine;
5973
6019
  module.exports.Engine = Engine;
5974
6020
  module.exports.sanitizeTodoItems = sanitizeTodoItems;
@@ -6,7 +6,8 @@
6
6
  kind=logs → Beast log dosyası taranır; son windowMin dakikadaki error/warn sayısı
7
7
  değer olur ("10 dk içinde 3+ hata olursa bağır")
8
8
  Koşul sağlanınca ilgili oturuma mesaj düşer (WA köprüsüne de otomatik akar).
9
- Depo: %APPDATA%\beast\watchers.json */
9
+ Depo: %APPDATA%\beast\watchers.json
10
+ Loglar: %APPDATA%\beast\watcher-logs.json (ana depodan AYRI; izleyici başına denetim kaydı) */
10
11
 
11
12
  const fs = require('fs');
12
13
  const path = require('path');
@@ -18,7 +19,89 @@ function file() {
18
19
  return path.join(beastRoot(), 'watchers.json');
19
20
  }
20
21
 
22
+ /* İzleyici logları ANA depodan AYRI tutulur: watcher-logs.json
23
+ Yapı: { logs: { [watcherId]: [{ ts, ev: ok|trigger|error, value, error }] } }
24
+ İzleyici başına LOG_CAP kayıt tutulur (yenisi eklenince en eskisi düşer). */
25
+ const LOG_CAP = 200;
26
+ const LOG_FILE = 'watcher-logs.json';
27
+
28
+ function logFile() {
29
+ return path.join(beastRoot(), LOG_FILE);
30
+ }
31
+
21
32
  let items = [];
33
+ let logs = {}; // watcherId -> [{ ts, ev, value, error }]
34
+ let logsDirty = false;
35
+
36
+ function loadLogs() {
37
+ try {
38
+ const raw = JSON.parse(fs.readFileSync(logFile(), 'utf8'));
39
+ logs = raw && typeof raw.logs === 'object' && raw.logs !== null ? raw.logs : {};
40
+ } catch {
41
+ logs = {};
42
+ }
43
+ }
44
+
45
+ function saveLogs(force) {
46
+ if (!logsDirty && !force) return;
47
+ try {
48
+ fs.mkdirSync(beastRoot(), { recursive: true });
49
+ fs.writeFileSync(logFile(), JSON.stringify({ logs }, null, 2));
50
+ logsDirty = false;
51
+ } catch {}
52
+ }
53
+
54
+ /* Tek izleyiciye log kaydı düş — değer/seri hale getirilemezse güvenli temsil */
55
+ function pushLog(id, ev, value, error) {
56
+ let v = null;
57
+ if (value !== undefined && value !== null) {
58
+ try {
59
+ v = typeof value === 'object' ? JSON.stringify(value).slice(0, 200) : value;
60
+ } catch {
61
+ v = String(value).slice(0, 200);
62
+ }
63
+ }
64
+ const arr = Array.isArray(logs[id]) ? logs[id] : (logs[id] = []);
65
+ arr.push({
66
+ ts: new Date().toISOString(),
67
+ ev, // 'ok' | 'trigger' | 'error'
68
+ value: v,
69
+ error: error ? String(error).slice(0, 300) : '',
70
+ });
71
+ if (arr.length > LOG_CAP) arr.splice(0, arr.length - LOG_CAP);
72
+ logsDirty = true;
73
+ }
74
+
75
+ /* Bir izleyicinin logları — en yeni en başta */
76
+ function logEntries(id) {
77
+ const arr = Array.isArray(logs[String(id)]) ? logs[String(id)] : [];
78
+ return arr.slice().reverse();
79
+ }
80
+
81
+ /* Tüm izleyicilerin logları — { [watcherId]: [...] }, en yeni en başta */
82
+ function logsAll() {
83
+ const out = {};
84
+ for (const k of Object.keys(logs)) out[k] = logEntries(k);
85
+ return out;
86
+ }
87
+
88
+ /* Logları temizle — id verilirse yalnız o izleyici, yoksa hepsi */
89
+ function logClear(id) {
90
+ if (id) delete logs[String(id)];
91
+ else logs = {};
92
+ saveLogs(true);
93
+ return { ok: true };
94
+ }
95
+
96
+ /* Artık var olmayan izleyicilerin loglarını temizle */
97
+ function pruneLogs() {
98
+ const ids = new Set(items.map((w) => w.id));
99
+ let changed = false;
100
+ for (const k of Object.keys(logs)) {
101
+ if (!ids.has(k)) { delete logs[k]; changed = true; }
102
+ }
103
+ if (changed) saveLogs(true);
104
+ }
22
105
 
23
106
  function load() {
24
107
  try {
@@ -27,6 +110,8 @@ function load() {
27
110
  } catch {
28
111
  items = [];
29
112
  }
113
+ loadLogs();
114
+ pruneLogs();
30
115
  }
31
116
 
32
117
  function save() {
@@ -189,6 +274,7 @@ function remove(id) {
189
274
  const before = items.length;
190
275
  items = items.filter((w) => w.id !== id);
191
276
  save();
277
+ if (items.length < before) logClear(id); /* izleyici gitti → logları da gitsin */
192
278
  return { ok: items.length < before };
193
279
  }
194
280
 
@@ -283,6 +369,7 @@ async function tickOnce(deps = {}) {
283
369
  const r = applyCheck(w, value, now);
284
370
  Object.assign(w, r.patch);
285
371
  w.lastError = '';
372
+ pushLog(w.id, r.triggered ? 'trigger' : 'ok', value);
286
373
  if (r.triggered) {
287
374
  if (typeof hooks.onTrigger === 'function') hooks.onTrigger({ ...w }, value);
288
375
  events.push({ id: w.id, name: w.name, value });
@@ -290,9 +377,11 @@ async function tickOnce(deps = {}) {
290
377
  } catch (e) {
291
378
  w.lastCheckAt = new Date(now).toISOString();
292
379
  w.lastError = String((e && e.message) || e).slice(0, 300);
380
+ pushLog(w.id, 'error', undefined, w.lastError);
293
381
  }
294
382
  }
295
383
  save();
384
+ saveLogs();
296
385
  return events;
297
386
  }
298
387
 
@@ -330,4 +419,7 @@ module.exports = {
330
419
  applyCheck,
331
420
  isDue,
332
421
  cooldownActive,
422
+ logEntries,
423
+ logsAll,
424
+ logClear,
333
425
  };
package/src/cron.js CHANGED
@@ -136,12 +136,13 @@ function list() {
136
136
  return jobs.map((j) => ({ ...j }));
137
137
  }
138
138
 
139
- function add({ name, schedule, prompt, once, sessionId }) {
139
+ function add({ name, schedule, prompt, once, sessionId, kind }) {
140
140
  const n = String(name || '').trim().slice(0, 80) || 'Görev';
141
141
  const s = String(schedule || '').trim();
142
142
  const p = String(prompt || '').trim().slice(0, 4000);
143
143
  if (!isValidSchedule(s)) return { ok: false, error: 'geçersiz cron ifadesi (örn: */15 * * * *)' };
144
144
  if (!p) return { ok: false, error: 'görev metni boş olamaz' };
145
+ const k = String(kind || '').trim().slice(0, 24);
145
146
  const job = {
146
147
  id: uid(),
147
148
  name: n,
@@ -150,6 +151,7 @@ function add({ name, schedule, prompt, once, sessionId }) {
150
151
  sessionId: sessionId || null,
151
152
  once: !!once,
152
153
  enabled: true,
154
+ ...(k ? { kind: k } : {}),
153
155
  createdAt: new Date().toISOString(),
154
156
  lastRunAt: null,
155
157
  nextRunAt: nextRunFrom(s, new Date()),
@@ -186,6 +188,17 @@ function remove(id) {
186
188
  return { ok: jobs.length < before };
187
189
  }
188
190
 
191
+ /* Koşula uyan TÜM görevleri sil (/autodel, "hatırlatmaları sil"). */
192
+ function removeIf(pred) {
193
+ const before = jobs.length;
194
+ jobs = jobs.filter((j) => {
195
+ try { return !pred(j); } catch { return true; }
196
+ });
197
+ const count = before - jobs.length;
198
+ if (count) save();
199
+ return { ok: true, count };
200
+ }
201
+
189
202
  /* Tüm görevleri bellekten ve diskten sil; zamanlayıcıyı durdur.
190
203
  "hepsini sil" / "/cron clear" için kullanılır. */
191
204
  function clearAll() {
@@ -233,17 +246,43 @@ function tick() {
233
246
  }
234
247
  }
235
248
 
249
+ /* Uygulama kapalıyken saati geçen tek-seferlik hatırlatma için teslim penceresi:
250
+ bu kadar süre geçmediyse uyarı GECİKMELİ de olsa teslim edilir. */
251
+ const ONCE_GRACE_MS = 60 * 60 * 1000;
252
+
253
+ /* Açılış uzlaşması (saf): saati geçmiş tek-seferlik hatırlatmalar eski
254
+ davranışta nextRunFrom ile GELECEK YILIN aynı gününe yeniden kuruluyordu —
255
+ bayat kayıt ölümsüzleşip tekrar tekrar önüne düşüyordu. Artık: grace
256
+ içindeyse "fire" listesine (gecikmeli teslim), bayatsa listeden düşer.
257
+ Tekrarlıların nextRunAt'ı her zamanki gibi yeniden hesaplanır. */
258
+ function reconcile(list, now, graceMs) {
259
+ const keep = [];
260
+ const fire = [];
261
+ for (const job of list) {
262
+ if (!job.enabled) { keep.push(job); continue; }
263
+ const nr = job.nextRunAt ? new Date(job.nextRunAt).getTime() : 0;
264
+ if (job.once && nr && nr <= now) {
265
+ if (now - nr <= graceMs) fire.push(job);
266
+ continue; // bayat olan burada düşer
267
+ }
268
+ if (!nr || nr <= now) {
269
+ keep.push({ ...job, nextRunAt: nextRunFrom(job.schedule, new Date(now)) });
270
+ continue;
271
+ }
272
+ keep.push(job);
273
+ }
274
+ return { jobs: keep, fire };
275
+ }
276
+
236
277
  function init(hooks) {
237
278
  onFire = (hooks && hooks.onFire) || null;
238
279
  load();
239
- for (const job of jobs) {
240
- if (job.enabled && (!job.nextRunAt || new Date(job.nextRunAt).getTime() <= Date.now())) {
241
- job.nextRunAt = nextRunFrom(job.schedule, new Date());
242
- }
243
- }
280
+ const r = reconcile(jobs, Date.now(), ONCE_GRACE_MS);
281
+ jobs = r.jobs;
244
282
  save();
245
283
  if (timer) clearInterval(timer);
246
284
  timer = setInterval(tick, 20 * 1000);
285
+ for (const job of r.fire) fire(job); // grace içindeki bayat hatırlatmayı gecikmeli teslim et
247
286
  }
248
287
 
249
288
  /* /stop: zamanlayıcıyı durdur (init ile geri başlar) */
@@ -252,4 +291,4 @@ function stop() {
252
291
  timer = null;
253
292
  }
254
293
 
255
- module.exports = { init, stop, list, add, update, remove, clearAll, toggle, runNow, parseCron, nextRunFrom, isValidSchedule, reminderSchedule };
294
+ module.exports = { init, stop, list, add, update, remove, removeIf, clearAll, toggle, runNow, parseCron, nextRunFrom, isValidSchedule, reminderSchedule, reconcile, ONCE_GRACE_MS };
package/src/main.js CHANGED
@@ -1106,6 +1106,8 @@ function waSlashHelp() {
1106
1106
  '• */usage* – bugünkü kullanım',
1107
1107
  '• */backup* – tüm veriyi ŞİFRELİ yedekle (Beast Kodu imzalı, Masaüstü\\Beast-Backups)',
1108
1108
  '• */status* – bağlantı ve servis durumu',
1109
+ '• */autodel* – tüm otomatik hatırlatmaları (bayat/eski dahil) tek komutla sil · */autodel all*: cron görevleri dahil hepsi',
1110
+ '• */deltodo* – bu oturumun todo listesini temizle · */deltodo all*: tüm oturumların todoları',
1109
1111
  '',
1110
1112
  'Gruplarda beni @mention ile çağır; ardışık mesajlarını tek cevapta birleştiririm.',
1111
1113
  ].join('\n');
@@ -1646,6 +1648,58 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1646
1648
  } else {
1647
1649
  out = 'Kullanım: `/cron list` · `/cron clear` (hepsini sil) · `/cron del <id>`';
1648
1650
  }
1651
+ } else if (cmd === 'autodel') {
1652
+ /* /autodel [all] — TÜM otomatik hatırlatmaları tek komutla sil.
1653
+ Bayat/eski kayıtlar dahil: kind=reminder + eski isim/prompt önekleri eşleşir.
1654
+ /autodel all → cron görevleri dahil her şey (=/cron clear) */
1655
+ const sub = String(arg || '').toLowerCase().trim();
1656
+ if (sub === 'all' || sub === 'hepsi' || sub === 'hepsini') {
1657
+ const r = cron.clearAll();
1658
+ cronEmit();
1659
+ out = r.ok
1660
+ ? `*Tüm zamanlanmış görevler silindi* (${r.count} adet).`
1661
+ : 'Temizlenemedi.';
1662
+ } else {
1663
+ const r = cron.removeIf(isReminderJob);
1664
+ cronEmit();
1665
+ const left = cron.list().filter((j) => j.enabled).length;
1666
+ out = r.count
1667
+ ? `*${r.count} otomatik hatırlatma silindi.* Kalan aktif görev: ${left}`
1668
+ : 'Silinecek otomatik hatırlatma yok — zaten temiz.';
1669
+ }
1670
+ } else if (cmd === 'deltodo') {
1671
+ /* /deltodo [all] — todo listelerini temizler: arg=all → TÜM oturumlar;
1672
+ yoksa yalnız bu sohbetin oturumu. Panelden de düşsün diye 'todos'
1673
+ olayı emit edilir (engine.clearTodos). */
1674
+ const sub = String(arg || '').toLowerCase().trim();
1675
+ if (sub === 'all' || sub === 'hepsi' || sub === 'hepsini') {
1676
+ let n = 0;
1677
+ let items = 0;
1678
+ try {
1679
+ for (const v of engine.listSessions()) {
1680
+ const r = engine.clearTodos(v.id);
1681
+ if (r && r.ok) {
1682
+ n++;
1683
+ items += r.count || 0;
1684
+ }
1685
+ }
1686
+ } catch {}
1687
+ out = n
1688
+ ? `*Tüm oturumların todoları temizlendi* (${n} oturum, ${items} madde).`
1689
+ : 'Temizlenecek todo yok.';
1690
+ } else {
1691
+ const sid = waChats.get(jid) || waSingleSid || '';
1692
+ if (!sid) {
1693
+ out = 'Bu sohbette açık oturum yok — todo listesi de yok.';
1694
+ } else {
1695
+ const r = engine.clearTodos(sid);
1696
+ out = r.ok
1697
+ ? (r.count
1698
+ ? `*${r.count} todo temizlendi.* (tüm oturumlar için: /deltodo all)`
1699
+ : 'Todo listesi zaten boş. (tüm oturumlar için: /deltodo all)')
1700
+ : 'Oturum bulunamadı — todo listesi yok.';
1701
+ }
1702
+ }
1649
1703
  } else if (cmd === 'beastcode') {
1650
1704
  /* /beastcode [görev] — WhatsApp'tan UZAKTAN KODLAMA modu:
1651
1705
  masaüstünde GERÇEK Beast Code paneli açılır (IDE ekranı), sohbet
@@ -2084,6 +2138,22 @@ async function documentToTextAsync(media) {
2084
2138
 
2085
2139
  /* ---------- WA hatırlatıcı ---------- */
2086
2140
 
2141
+ /* Hatırlatma job'ı mı? Yeni kayıtlarda kind='reminder' var; eski (bayat)
2142
+ kayıtlar isim/prompt önekinden tanınır — /autodel ve doğal dil silme
2143
+ bunu kullanır. */
2144
+ function isReminderJob(j) {
2145
+ if (!j || typeof j !== 'object') return false;
2146
+ if (j.kind === 'reminder') return true;
2147
+ const n = String(j.name || '');
2148
+ const p = String(j.prompt || '');
2149
+ return (
2150
+ /^Hatırlatma:/.test(n) ||
2151
+ /^Tekrarlı hatırlatma:/.test(n) ||
2152
+ p.startsWith('[HATIRLATMA ZAMANI]') ||
2153
+ p.startsWith('[TEKRARLI HATIRLATMA]')
2154
+ );
2155
+ }
2156
+
2087
2157
  function scheduleReminder({ when, message, sessionId, repeat }) {
2088
2158
  try {
2089
2159
  const msg = String(message || '').trim();
@@ -2097,6 +2167,7 @@ function scheduleReminder({ when, message, sessionId, repeat }) {
2097
2167
  name: 'Tekrarlı hatırlatma: ' + msg.slice(0, 40),
2098
2168
  schedule: s.schedule,
2099
2169
  prompt: `[TEKRARLI HATIRLATMA] Kullanıcıya şunu hatırlat: "${msg}". Kısaca ve nazikçe bildir.`,
2170
+ kind: 'reminder',
2100
2171
  sessionId: sessionId || undefined,
2101
2172
  });
2102
2173
  if (!r.ok) return r;
@@ -2111,6 +2182,7 @@ function scheduleReminder({ when, message, sessionId, repeat }) {
2111
2182
  name: 'Hatırlatma: ' + msg.slice(0, 40),
2112
2183
  schedule: cronExpr,
2113
2184
  prompt: `[HATIRLATMA ZAMANI] Kullanıcıya şunu hatırlat: "${msg}". Kısaca ve nazikçe bildir.`,
2185
+ kind: 'reminder',
2114
2186
  once: true,
2115
2187
  sessionId: sessionId || undefined,
2116
2188
  });
@@ -2291,6 +2363,23 @@ async function handleWaIncoming(jid, payload, senderNum) {
2291
2363
  return;
2292
2364
  }
2293
2365
 
2366
+ /* "hatırlatmaları sil" / "alarm temizle" / "reminders kaldır" → yalnızca
2367
+ otomatik hatırlatmalar silinir (cron görevlerine dokunulmaz). */
2368
+ if (
2369
+ /hat[iı]rlat|alarm|remind/.test(lc) &&
2370
+ /(sil|sıl|temizle|kaldır|iptal|sıfırla|kapat)/.test(lc)
2371
+ ) {
2372
+ const r = cron.removeIf(isReminderJob);
2373
+ cronEmit();
2374
+ await sendWaSafe(
2375
+ jid,
2376
+ r.count
2377
+ ? `*${r.count} otomatik hatırlatma silindi.* Zamanlanmış kalan görev: ${cron.list().filter((j) => j.enabled).length}`
2378
+ : 'Silinecek otomatik hatırlatma yok — zaten temiz.'
2379
+ ).catch(() => {});
2380
+ return;
2381
+ }
2382
+
2294
2383
  if (isGroup) {
2295
2384
  const g = settings.waGroups || {};
2296
2385
  if (!g.enabled) return;
@@ -3307,11 +3396,11 @@ function reloadBackend() {
3307
3396
  }
3308
3397
  /* CRON → TÜM AKTİF ENTEGRASYONLAR: cron işi bittiğinde cevap (ya da
3309
3398
  hata) sahibin bağlı olduğu her kanala yansıtılır — WA + Telegram +
3310
- Discord. Aynı kanal hem cron oturumuna bağlıysa TEK cevap alır. */
3311
- if ((ev.type === 'done' || ev.type === 'error') && cronAnswerPending.has(String(ev.sessionId))) {
3312
- const cjob = cronAnswerPending.get(String(ev.sessionId));
3313
- cronAnswerPending.delete(String(ev.sessionId));
3314
- if (!ev.aborted) {
3399
+ Discord. Aynı kanal hem cron oturumuna bağlıysa TEK cevap alır.
3400
+ Bayat (TTL aşımı) bekleme kaydı Take ile düşer, yansıtılmaz. */
3401
+ if (ev.type === 'done' || ev.type === 'error') {
3402
+ const cjob = cronAnswerPendingTake(ev.sessionId);
3403
+ if (cjob && !ev.aborted) {
3315
3404
  (async () => {
3316
3405
  try {
3317
3406
  let txt = '';
@@ -6519,8 +6608,40 @@ function cronEmit() {
6519
6608
 
6520
6609
  /* Cron cevabı bekleme haritası: sid -> job. İş 'done' olunca cevap
6521
6610
  SAHİBİN bağlı olduğu TÜM AKTİF entegrasyonlara yansıtılır (kullanıcı
6522
- hangi kanaldan ajanla iletişimde belli değil). */
6611
+ hangi kanaldan ajanla iletişimde belli değil). Kayıtlar TTL'li —
6612
+ done/error hiç gelmezse bayat bekleme sonraki done'a yansıtılmaz. */
6523
6613
  const cronAnswerPending = new Map();
6614
+ const cronAnswerPendingAt = new Map();
6615
+ const CRON_ANSWER_TTL_MS = 30 * 60 * 1000;
6616
+
6617
+ function cronAnswerPendingSet(sid, job) {
6618
+ cronAnswerPendingSweep();
6619
+ cronAnswerPending.set(String(sid), job);
6620
+ cronAnswerPendingAt.set(String(sid), Date.now());
6621
+ }
6622
+
6623
+ function cronAnswerPendingDrop(sid) {
6624
+ cronAnswerPending.delete(String(sid));
6625
+ cronAnswerPendingAt.delete(String(sid));
6626
+ }
6627
+
6628
+ /* Bayat kayıt (TTL aşımı) haritalardan temizlenir */
6629
+ function cronAnswerPendingSweep() {
6630
+ const now = Date.now();
6631
+ for (const [k, at] of cronAnswerPendingAt) {
6632
+ if (now - at > CRON_ANSWER_TTL_MS) cronAnswerPendingDrop(k);
6633
+ }
6634
+ }
6635
+
6636
+ /* Kaydı okuyup düşürür; bayat kayıt ise null döner (yansıtma yok) */
6637
+ function cronAnswerPendingTake(sid) {
6638
+ const k = String(sid);
6639
+ const job = cronAnswerPending.get(k);
6640
+ const at = cronAnswerPendingAt.get(k) || 0;
6641
+ cronAnswerPendingDrop(k);
6642
+ if (!job || Date.now() - at > CRON_ANSWER_TTL_MS) return null;
6643
+ return job;
6644
+ }
6524
6645
 
6525
6646
  /* OTOMATİK YENİ SOHBET YOK: cron/izleyici/fallout tetiklendiğinde oturum
6526
6647
  seçimi — kayıtlı id'nin DOSYASI hâlâ duruyorsa O, değilse EN GÜNCEL
@@ -6600,11 +6721,11 @@ function cronFire(job) {
6600
6721
  if (sid !== String(job.sessionId || '')) {
6601
6722
  cron.update(job.id, { sessionId: sid });
6602
6723
  }
6603
- cronAnswerPending.set(String(sid), job);
6724
+ cronAnswerPendingSet(sid, job);
6604
6725
  const sent = engine.send(sid, {
6605
6726
  text: `[cron: ${job.name}]\n${job.prompt}`,
6606
6727
  });
6607
- if (!sent) cronAnswerPending.delete(String(sid)); // gönderilemedi — bayat bekleme bırakma
6728
+ if (!sent) cronAnswerPendingDrop(sid); // gönderilemedi — bayat bekleme bırakma
6608
6729
  } catch {}
6609
6730
  cronEmit();
6610
6731
  }
@@ -7124,6 +7245,9 @@ ipcMain.handle('watchers:toggle', (_e, id) => {
7124
7245
  watchers.patch(String(id), { enabled: !w.enabled });
7125
7246
  return { ok: true, watcher: watchers.get(String(id)) };
7126
7247
  });
7248
+ /* izleyici logları — ana depodan ayrı watcher-logs.json üzerinden */
7249
+ ipcMain.handle('watchers:logs', () => watchers.logsAll());
7250
+ ipcMain.handle('watchers:logsClear', (_e, id) => watchers.logClear(id ? String(id) : null));
7127
7251
  ipcMain.handle('cron:add', (_e, job) => {
7128
7252
  const r = cron.add(job || {});
7129
7253
  cronEmit();
package/src/preload.js CHANGED
@@ -146,6 +146,10 @@ contextBridge.exposeInMainWorld('beast', {
146
146
 
147
147
  watchersToggle: (id) => ipcRenderer.invoke('watchers:toggle', id),
148
148
 
149
+ watchersLogs: () => ipcRenderer.invoke('watchers:logs'),
150
+
151
+ watchersLogsClear: (id) => ipcRenderer.invoke('watchers:logsClear', id),
152
+
149
153
  providerKeys: () => ipcRenderer.invoke('providers:keys'),
150
154
 
151
155
  openPath: (p) => ipcRenderer.invoke('shell:openPath', p),
@@ -542,6 +542,14 @@
542
542
  bot_no_watchers: 'Bu bota ait izleyici yok.',
543
543
  w_pane: 'İZLEYİCİLER',
544
544
  w_empty: 'Kayıtlı izleyici yok.',
545
+ tipWatchLog: 'İzleyici loglarını gör',
546
+ w_log_title: 'İzleyici Logları',
547
+ w_log_clear: 'Seçili izleyicinin loglarını temizle',
548
+ w_log_cleared: 'Loglar temizlendi',
549
+ w_log_empty: 'Bu izleyici için henüz log kaydı yok.',
550
+ w_log_ev_ok: 'kontrol',
551
+ w_log_ev_trigger: 'tetik',
552
+ w_log_ev_error: 'hata',
545
553
  bot_stats_sub: 'Bu botun kullanım özeti.',
546
554
  bot_stat_numbers: 'Numara',
547
555
  bot_stat_sessions: 'Sohbet',
@@ -1038,6 +1046,14 @@
1038
1046
  cr_toggle_pause: 'Pause',
1039
1047
  w_empty: 'No saved watchers — add from the form above.',
1040
1048
  w_pane: 'WATCHERS',
1049
+ tipWatchLog: 'View watcher logs',
1050
+ w_log_title: 'Watcher Logs',
1051
+ w_log_clear: 'Clear selected watcher logs',
1052
+ w_log_cleared: 'Logs cleared',
1053
+ w_log_empty: 'No log entries for this watcher yet.',
1054
+ w_log_ev_ok: 'check',
1055
+ w_log_ev_trigger: 'trigger',
1056
+ w_log_ev_error: 'error',
1041
1057
  wa_vazgec: 'Cancel',
1042
1058
  wa_kaydet: 'save',
1043
1059
  wa_updated: 'Updated',
@@ -17,7 +17,8 @@
17
17
  <div id="watchPaneHead">
18
18
  <span id="watchPaneTitle" data-i18n="w_pane">İZLEYİCİLER</span>
19
19
  <span class="file-spacer"></span>
20
- <button id="watchPaneOpen" title="İzleyicileri yönet">+</button>
20
+ <button id="watchPaneLog" title="İzleyici loglarını gör" data-i18n-title="tipWatchLog"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg></button>
21
+ <button id="watchPaneOpen" title="İzleyicileri yönet" data-i18n-title="tipWatch">+</button>
21
22
  </div>
22
23
  <div id="watchPaneList"></div>
23
24
  </div>
@@ -528,6 +529,22 @@
528
529
  </div>
529
530
  </div>
530
531
 
532
+ <div id="watchLogOverlay" class="mini-overlay" hidden>
533
+ <div class="mini-dialog">
534
+ <div class="mini-head">
535
+ <span class="mini-title" data-i18n="w_log_title">İzleyici Logları</span>
536
+ <button class="mini-close" id="watchLogClose" title="Kapat" data-i18n-title="btn_close">&#x00D7;</button>
537
+ </div>
538
+ <div class="mini-body">
539
+ <div id="watchLogBar">
540
+ <select id="watchLogSel" class="mini-select"></select>
541
+ <button id="watchLogClear" class="mr-btn" title="Seçili izleyicinin loglarını temizle" data-i18n-title="w_log_clear"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>
542
+ </div>
543
+ <div id="watchLogList" class="mini-list"></div>
544
+ </div>
545
+ </div>
546
+ </div>
547
+
531
548
  <div id="finSymOverlay" class="mini-overlay" hidden>
532
549
  <div class="mini-dialog fin-sym-dialog">
533
550
  <div class="mini-head">
@@ -45,6 +45,7 @@ const els = {
45
45
  watchBtn: $('#watchBtn'),
46
46
  watchPaneList: $('#watchPaneList'),
47
47
  watchPaneOpen: $('#watchPaneOpen'),
48
+ watchPaneLog: $('#watchPaneLog'),
48
49
  cronBtn: $('#cronBtn'),
49
50
  watchOverlay: $('#watchOverlay'),
50
51
  cronOverlay: $('#cronOverlay'),
@@ -52,6 +53,11 @@ const els = {
52
53
  cronClose: $('#cronClose'),
53
54
  watchList: $('#watchList'),
54
55
  cronModalList: $('#cronModalList'),
56
+ watchLogOverlay: $('#watchLogOverlay'),
57
+ watchLogClose: $('#watchLogClose'),
58
+ watchLogSel: $('#watchLogSel'),
59
+ watchLogClear: $('#watchLogClear'),
60
+ watchLogList: $('#watchLogList'),
55
61
  browserBar: $('#browserBar'),
56
62
  bbBack: $('#bbBack'),
57
63
  bbFwd: $('#bbFwd'),
@@ -5611,6 +5617,11 @@ async function renderWatchersPane(force) {
5611
5617
  `</div></div>`;
5612
5618
  const btns = document.createElement('div');
5613
5619
  btns.className = 'wp-btns';
5620
+ const lg = document.createElement('button');
5621
+ lg.className = 'wp-btn';
5622
+ lg.innerHTML = '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>';
5623
+ lg.title = _t('w_log_title');
5624
+ lg.addEventListener('click', (e) => { e.stopPropagation(); openWatchLogModal(w.id); });
5614
5625
  const t = document.createElement('button');
5615
5626
  t.className = 'wp-btn';
5616
5627
  t.textContent = w.enabled ? '❚❚' : '▶';
@@ -5632,7 +5643,7 @@ async function renderWatchersPane(force) {
5632
5643
  renderWatchersPane(true);
5633
5644
  renderWatchersModal(true);
5634
5645
  });
5635
- btns.append(t, d);
5646
+ btns.append(lg, t, d);
5636
5647
  row.appendChild(btns);
5637
5648
  row.addEventListener('click', () => openWatchModal());
5638
5649
  list.appendChild(row);
@@ -5669,6 +5680,11 @@ async function renderWatchersModal(force) {
5669
5680
  `</div></div>`;
5670
5681
  const btns = document.createElement('div');
5671
5682
  btns.style.cssText = 'display:flex;gap:6px;flex:none';
5683
+ const lg = document.createElement('button');
5684
+ lg.className = 'mr-btn';
5685
+ lg.innerHTML = '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>';
5686
+ lg.title = _t('w_log_title');
5687
+ lg.addEventListener('click', () => openWatchLogModal(w.id));
5672
5688
  const t = document.createElement('button');
5673
5689
  t.className = 'mr-btn';
5674
5690
  t.textContent = w.enabled ? '❚❚' : '▶';
@@ -5679,12 +5695,85 @@ async function renderWatchersModal(force) {
5679
5695
  d.textContent = '×';
5680
5696
  d.title = _t('cr_del');
5681
5697
  d.addEventListener('click', async () => { await beast.watchersRemove(w.id); renderWatchersModal(); });
5682
- btns.append(t, d);
5698
+ btns.append(lg, t, d);
5683
5699
  row.appendChild(btns);
5684
5700
  els.watchList.appendChild(row);
5685
5701
  }
5686
5702
  }
5687
5703
 
5704
+ /* ---------- İZLEYİCİ LOG MODALI ----------
5705
+ Her izleyicinin logu AYRI tutulur (watcher-logs.json) ve AYRI gösterilir:
5706
+ üstteki seçiciyle izleyici seç → yalnız onun denetim kayıtları listelenir. */
5707
+ let watchLogSelId = '';
5708
+ let lastWatchLogKey = '';
5709
+ let lastWatchLogSelKey = '';
5710
+
5711
+ async function renderWatchLogModal(force) {
5712
+ if (!els.watchLogOverlay) return;
5713
+ let rows = []; try { rows = (await beast.watchersList()) || []; } catch {}
5714
+ let logsMap = {}; try { logsMap = (await beast.watchersLogs()) || {}; } catch {}
5715
+ /* seçim: yok olan seçilmişse ilk izleyiciye düş */
5716
+ if (!rows.some((w) => w.id === watchLogSelId)) watchLogSelId = rows.length ? rows[0].id : '';
5717
+ /* seçici — izleyici listesi değişmediyse DOM'a dokunma (odak korunur) */
5718
+ const selKey = JSON.stringify(rows.map((w) => [w.id, w.name, w.enabled]));
5719
+ if (selKey !== lastWatchLogSelKey) {
5720
+ lastWatchLogSelKey = selKey;
5721
+ els.watchLogSel.innerHTML = '';
5722
+ if (!rows.length) {
5723
+ const o = document.createElement('option');
5724
+ o.textContent = _t('w_empty');
5725
+ o.value = '';
5726
+ els.watchLogSel.appendChild(o);
5727
+ } else {
5728
+ for (const w of rows) {
5729
+ const o = document.createElement('option');
5730
+ o.value = w.id;
5731
+ o.textContent = (w.enabled ? '' : '⏸ ') + w.name;
5732
+ els.watchLogSel.appendChild(o);
5733
+ }
5734
+ }
5735
+ els.watchLogSel.value = watchLogSelId;
5736
+ }
5737
+ /* log satırları — yalnız seçili izleyicinin kayıtları */
5738
+ const entries = watchLogSelId ? (logsMap[watchLogSelId] || []) : [];
5739
+ const key = JSON.stringify([watchLogSelId, selKey, entries]);
5740
+ if (!force && key === lastWatchLogKey) return;
5741
+ lastWatchLogKey = key;
5742
+ els.watchLogList.innerHTML = '';
5743
+ if (!watchLogSelId || !entries.length) {
5744
+ els.watchLogList.innerHTML = '<div class="mini-empty">' + _t('w_log_empty') + '</div>';
5745
+ return;
5746
+ }
5747
+ const evBadge = { ok: ['ok', _t('w_log_ev_ok')], trigger: ['trigger', _t('w_log_ev_trigger')], error: ['error', _t('w_log_ev_error')] };
5748
+ for (const en of entries) {
5749
+ const [cls, label] = evBadge[en.ev] || ['ok', en.ev];
5750
+ const row = document.createElement('div');
5751
+ row.className = 'wl-row';
5752
+ row.innerHTML =
5753
+ `<span class="wl-time">${escapeHtml(fmtWhen(en.ts))}</span>` +
5754
+ `<span class="wl-badge ${cls}">${escapeHtml(label)}</span>` +
5755
+ `<span class="wl-main">` +
5756
+ (en.ev === 'error'
5757
+ ? `<span class="wl-err">${escapeHtml(String(en.error || '').slice(0, 120))}</span>`
5758
+ : (en.value !== null && en.value !== undefined ? escapeHtml(String(en.value).slice(0, 120)) : '—')) +
5759
+ `</span>`;
5760
+ els.watchLogList.appendChild(row);
5761
+ }
5762
+ }
5763
+
5764
+ function closeWatchLogModal() {
5765
+ toggleMini(els.watchLogOverlay, true);
5766
+ stopMiniRefresh();
5767
+ }
5768
+
5769
+ async function openWatchLogModal(id) {
5770
+ if (id) watchLogSelId = String(id);
5771
+ lastWatchLogSelKey = ''; /* seçici her açılışta TAZE kurulsun */
5772
+ toggleMini(els.watchLogOverlay, false);
5773
+ await renderWatchLogModal(true);
5774
+ startMiniRefresh();
5775
+ }
5776
+
5688
5777
  let lastCronKey = '';
5689
5778
  async function renderCronModal(force) {
5690
5779
  let jobs = [];
@@ -5746,6 +5835,7 @@ function startMiniRefresh() {
5746
5835
  stopMiniRefresh();
5747
5836
  miniRefreshTimer = setInterval(async () => {
5748
5837
  if (els.watchOverlay && !els.watchOverlay.hidden) await renderWatchersModal();
5838
+ else if (els.watchLogOverlay && !els.watchLogOverlay.hidden) await renderWatchLogModal();
5749
5839
  else if (els.cronOverlay && !els.cronOverlay.hidden) await renderCronModal();
5750
5840
  else stopMiniRefresh();
5751
5841
  }, 4000);
@@ -6487,12 +6577,29 @@ async function init() {
6487
6577
  /* cron modülü — olay bağlama */
6488
6578
  if (els.watchBtn) els.watchBtn.addEventListener('click', openWatchModal);
6489
6579
  if (els.watchPaneOpen) els.watchPaneOpen.addEventListener('click', openWatchModal);
6580
+ if (els.watchPaneLog) els.watchPaneLog.addEventListener('click', () => openWatchLogModal());
6490
6581
  if (els.cronBtn) els.cronBtn.addEventListener('click', openCronModal);
6491
6582
  if (els.watchClose) els.watchClose.addEventListener('click', closeWatchModal);
6492
6583
  if (els.cronClose) els.cronClose.addEventListener('click', closeCronModal);
6584
+ if (els.watchLogClose) els.watchLogClose.addEventListener('click', closeWatchLogModal);
6585
+ if (els.watchLogSel) els.watchLogSel.addEventListener('change', () => {
6586
+ watchLogSelId = els.watchLogSel.value;
6587
+ lastWatchLogKey = '';
6588
+ renderWatchLogModal(true);
6589
+ });
6590
+ if (els.watchLogClear) els.watchLogClear.addEventListener('click', async () => {
6591
+ if (!watchLogSelId) return;
6592
+ await beast.watchersLogsClear(watchLogSelId);
6593
+ lastWatchLogKey = '';
6594
+ await renderWatchLogModal(true);
6595
+ toast(_t('w_log_cleared'));
6596
+ });
6493
6597
  if (els.watchOverlay) els.watchOverlay.addEventListener('click', (e) => {
6494
6598
  if (e.target === els.watchOverlay) closeWatchModal();
6495
6599
  });
6600
+ if (els.watchLogOverlay) els.watchLogOverlay.addEventListener('click', (e) => {
6601
+ if (e.target === els.watchLogOverlay) closeWatchLogModal();
6602
+ });
6496
6603
  if (els.cronOverlay) els.cronOverlay.addEventListener('click', (e) => {
6497
6604
  if (e.target === els.cronOverlay) closeCronModal();
6498
6605
  });
@@ -6579,6 +6686,7 @@ async function init() {
6579
6686
  document.addEventListener('keydown', (e) => {
6580
6687
  if (e.key === 'Escape') {
6581
6688
  if ($('#botOverlay') && !$('#botOverlay').hidden) { botOverlaySetOpen(false); return; }
6689
+ if (els.watchLogOverlay && !els.watchLogOverlay.hidden) { closeWatchLogModal(); return; }
6582
6690
  if (els.watchOverlay && !els.watchOverlay.hidden) { closeWatchModal(); return; }
6583
6691
  if (els.cronOverlay && !els.cronOverlay.hidden) { closeCronModal(); return; }
6584
6692
  if (!els.settingsOverlay.hidden) closeSettings();
@@ -133,7 +133,6 @@ body:not(.ide-mode):not(.studio-mode):not(.sandbox-mode) #watchPane { display: f
133
133
  }
134
134
  #watchPaneTitle { font-size: 10.5px; letter-spacing: 2px; color: var(--muted); font-weight: 800; }
135
135
  #watchPaneHead button {
136
- margin-left: auto;
137
136
  background: none;
138
137
  border: 1px solid var(--border);
139
138
  border-radius: 7px;
@@ -142,7 +141,13 @@ body:not(.ide-mode):not(.studio-mode):not(.sandbox-mode) #watchPane { display: f
142
141
  font-size: 12px;
143
142
  line-height: 1.2;
144
143
  padding: 1px 7px;
144
+ height: 22px;
145
+ display: inline-flex;
146
+ align-items: center;
147
+ justify-content: center;
145
148
  }
149
+ /* LOG ve + butonları sağda YAN YANA BİTİŞİK dursun: yalnız ilkinde itici boşluk */
150
+ #watchPaneHead button:first-of-type { margin-left: auto; }
146
151
  #watchPaneHead button:hover { color: var(--text); background: var(--panel2); }
147
152
  #watchPaneList { flex: 1; overflow-y: auto; padding: 4px; }
148
153
  .wp-row {
@@ -1464,6 +1469,35 @@ body.browser-open .mini-overlay { right: var(--bw, 480px); }
1464
1469
  .mr-btn:hover { color: var(--accent); }
1465
1470
  .mr-btn.del:hover { color: var(--err); }
1466
1471
 
1472
+ /* ---------- İzleyici log modalı (her izleyicinin logu ayrı) ---------- */
1473
+ #watchLogBar { display: flex; gap: 6px; align-items: center; margin-bottom: 10px; }
1474
+ #watchLogSel { flex: 1; min-width: 0; }
1475
+ .mini-select {
1476
+ background: var(--panel2);
1477
+ border: 1px solid var(--border);
1478
+ border-radius: 7px;
1479
+ color: var(--text);
1480
+ font-size: 12px;
1481
+ padding: 4px 6px;
1482
+ }
1483
+ .wl-row {
1484
+ display: flex; align-items: baseline; gap: 7px;
1485
+ background: var(--panel); border: 1px solid var(--border);
1486
+ border-radius: 8px; padding: 5px 8px;
1487
+ }
1488
+ .wl-row:hover { background: var(--panel2); }
1489
+ .wl-time { flex: none; color: var(--muted); font-size: 10px; min-width: 56px; }
1490
+ .wl-badge {
1491
+ flex: none; font-size: 9px; font-weight: 800; letter-spacing: 0.5px;
1492
+ padding: 1px 6px; border-radius: 5px; border: 1px solid var(--border);
1493
+ text-transform: uppercase;
1494
+ }
1495
+ .wl-badge.ok { color: var(--muted); }
1496
+ .wl-badge.trigger { color: var(--accent); border-color: var(--accent); }
1497
+ .wl-badge.error { color: var(--err); border-color: var(--err); }
1498
+ .wl-main { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11.5px; }
1499
+ .wl-err { color: var(--err); font-size: 11px; }
1500
+
1467
1501
  /* #26 ajan dosya kartı */
1468
1502
  .file-card {
1469
1503
  display: flex;
@@ -1916,6 +1950,7 @@ body.browser-open #settingsDialog { width: min(70vw, calc(100vw - var(--bw, 480p
1916
1950
  flex: none;
1917
1951
  width: 250px;
1918
1952
  min-width: 250px;
1953
+ min-height: 0;
1919
1954
  background: var(--panel);
1920
1955
  border-left: 1px solid var(--border);
1921
1956
  display: flex;
@@ -1957,9 +1992,19 @@ body.browser-open #settingsDialog { width: min(70vw, calc(100vw - var(--bw, 480p
1957
1992
  }
1958
1993
  #railClear:hover { background: rgba(0, 0, 0, 0.08); }
1959
1994
  #railClear svg { display: block; }
1960
- }
1961
1995
 
1962
- #railList { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 4px 8px; }
1996
+ /* rail listesi: kartlar ekranı aştığında KAYDIRILABİLİR olmalı —
1997
+ flex:1 + min-height:0 + overflow-y:auto üçlüsü zorunlu. overscroll-behavior
1998
+ sayfa geri kaydırma animasyonunu da keser. */
1999
+ #railList {
2000
+ flex: 1 1 auto;
2001
+ min-height: 0;
2002
+ overflow-y: auto;
2003
+ overflow-x: hidden;
2004
+ overscroll-behavior: contain;
2005
+ scrollbar-gutter: stable;
2006
+ padding: 4px 8px;
2007
+ }
1963
2008
 
1964
2009
  .rail-empty {
1965
2010
  color: var(--muted);