@appaflytech/zeyrek-ai-mcp 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/.env.example +2 -2
  2. package/.env.production.example +9 -1
  3. package/README.md +14 -38
  4. package/dist/config.js +36 -3
  5. package/dist/knowledge/backendConstraints.js +982 -0
  6. package/dist/knowledge/componentCatalog.js +549 -92
  7. package/dist/knowledge/componentEvents.js +120 -25
  8. package/dist/knowledge/coverage.js +43 -0
  9. package/dist/knowledge/flowGuards.js +30 -0
  10. package/dist/knowledge/functionGuards.js +134 -0
  11. package/dist/knowledge/guideSections.js +49 -0
  12. package/dist/knowledge/hydrate.js +97 -0
  13. package/dist/knowledge/languageCodes.js +36 -0
  14. package/dist/knowledge/languages.js +105 -0
  15. package/dist/knowledge/layoutSummary.js +10 -0
  16. package/dist/knowledge/localization.js +51 -0
  17. package/dist/knowledge/normalize.js +500 -16
  18. package/dist/knowledge/outlines.js +167 -12
  19. package/dist/knowledge/policy.js +14 -5
  20. package/dist/knowledge/propTypes.js +80 -0
  21. package/dist/knowledge/rules.js +961 -44
  22. package/dist/knowledge/schemaGuard.js +199 -0
  23. package/dist/knowledge/styleResolve.js +63 -4
  24. package/dist/observability/trace.js +27 -3
  25. package/dist/platform/csv.js +123 -0
  26. package/dist/platform/errors.js +107 -6
  27. package/dist/platform/http.js +34 -4
  28. package/dist/platform/patch.js +27 -0
  29. package/dist/platform/refs.js +104 -0
  30. package/dist/platform/responses.js +25 -6
  31. package/dist/platform/screenshot.js +544 -16
  32. package/dist/platform/secrets.js +40 -0
  33. package/dist/platform/tokenVault.js +89 -0
  34. package/dist/platform/writeDiff.js +281 -0
  35. package/dist/resources/examples/flow-call-function.json +66 -0
  36. package/dist/resources/examples/function-total.json +70 -0
  37. package/dist/resources/examples/layout-app-shell.json +1 -1
  38. package/dist/resources/examples/microfunction-node-sum.json +56 -0
  39. package/dist/resources/examples/microservice-table-list.json +2 -4
  40. package/dist/resources/examples/page-dynamic-form.json +3 -3
  41. package/dist/resources/examples/page-flex-sections.json +99 -0
  42. package/dist/resources/examples/page-sign-in.json +4 -3
  43. package/dist/resources/guides/component.md +81 -8
  44. package/dist/resources/guides/entity.md +712 -20
  45. package/dist/resources/guides/expressions.md +47 -2
  46. package/dist/resources/guides/external-component.md +9 -0
  47. package/dist/resources/guides/functions.md +262 -0
  48. package/dist/resources/guides/getting-started.md +82 -7
  49. package/dist/resources/guides/layout.md +53 -10
  50. package/dist/resources/guides/merge.md +231 -0
  51. package/dist/resources/guides/microfunction.md +166 -0
  52. package/dist/resources/guides/microservice.md +287 -15
  53. package/dist/resources/guides/middleware.md +89 -1
  54. package/dist/resources/guides/modify-existing.md +23 -0
  55. package/dist/resources/guides/page.md +402 -49
  56. package/dist/resources/guides/translation.md +150 -8
  57. package/dist/resources/guides/view.md +16 -1
  58. package/dist/resources/guides/workflow.md +199 -28
  59. package/dist/resources/knowledge.js +5 -0
  60. package/dist/resources/rules.js +27 -0
  61. package/dist/resources/schema-fingerprint.json +8 -0
  62. package/dist/resources/schemas/AnalyzeImportQuery.json +51 -0
  63. package/dist/resources/schemas/ApplyMergePlanCommand.json +19 -0
  64. package/dist/resources/schemas/CreateBulkRowCommand.json +36 -0
  65. package/dist/resources/schemas/CreateDatasourceCommand.json +15 -6
  66. package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +19 -8
  67. package/dist/resources/schemas/CreateEntityCommand.json +52 -12
  68. package/dist/resources/schemas/CreateEnvironmentCommand.json +31 -0
  69. package/dist/resources/schemas/CreateGlobalFunctionCommand.json +95 -0
  70. package/dist/resources/schemas/CreateImportPlanCommand.json +278 -0
  71. package/dist/resources/schemas/CreateMergePlanCommand.json +34 -0
  72. package/dist/resources/schemas/CreateMicrofunctionCommand.json +117 -0
  73. package/dist/resources/schemas/CreateMicrofunctionRuntimeCommand.json +51 -0
  74. package/dist/resources/schemas/CreateMicroserviceCommand.json +145 -3
  75. package/dist/resources/schemas/CreateQueryCommand.json +56 -3
  76. package/dist/resources/schemas/CreateRowCommand.json +38 -0
  77. package/dist/resources/schemas/CreateViewCommand.json +13 -10
  78. package/dist/resources/schemas/CreateWorkflowCommand.json +106 -14
  79. package/dist/resources/schemas/DataJobDto.json +267 -0
  80. package/dist/resources/schemas/ExportRowsCommand.json +172 -0
  81. package/dist/resources/schemas/Filter.json +3 -1
  82. package/dist/resources/schemas/GetRowsQuery.json +6 -1
  83. package/dist/resources/schemas/GlobalFunctionDto.json +89 -0
  84. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +249 -0
  85. package/dist/resources/schemas/ImportAnalysisDto.json +401 -0
  86. package/dist/resources/schemas/ImportPlanDto.json +260 -0
  87. package/dist/resources/schemas/ImportSampleDto.json +90 -0
  88. package/dist/resources/schemas/InvokeMicrofunctionCommand.json +25 -0
  89. package/dist/resources/schemas/PreviewMicroserviceCommand.json +135 -1
  90. package/dist/resources/schemas/PreviewQueryCommand.json +5 -2
  91. package/dist/resources/schemas/PreviewWorkflowCommand.json +45 -9
  92. package/dist/resources/schemas/ResolveMergeConflictsCommand.json +58 -0
  93. package/dist/resources/schemas/RevertMergePlanCommand.json +19 -0
  94. package/dist/resources/schemas/RunImportCommand.json +62 -0
  95. package/dist/resources/schemas/StepDto.json +34 -3
  96. package/dist/resources/schemas/UpdateBulkRowCommand.json +36 -0
  97. package/dist/resources/schemas/UpdateDatasourceCommand.json +15 -6
  98. package/dist/resources/schemas/UpdateEntityCommand.json +52 -11
  99. package/dist/resources/schemas/UpdateGlobalFunctionCommand.json +66 -0
  100. package/dist/resources/schemas/UpdateMicrofunctionCommand.json +121 -0
  101. package/dist/resources/schemas/UpdateMicrofunctionRuntimeCommand.json +54 -0
  102. package/dist/resources/schemas/UpdateMicroserviceCommand.json +147 -4
  103. package/dist/resources/schemas/UpdateQueryCommand.json +13 -3
  104. package/dist/resources/schemas/UpdateRowCommand.json +40 -0
  105. package/dist/resources/schemas/UpdateViewCommand.json +15 -10
  106. package/dist/resources/schemas/UpdateWorkflowCommand.json +106 -14
  107. package/dist/server.js +17 -1
  108. package/dist/tools/access.js +3 -3
  109. package/dist/tools/activities.js +40 -0
  110. package/dist/tools/applications.js +1 -1
  111. package/dist/tools/automations.js +76 -34
  112. package/dist/tools/common.js +55 -0
  113. package/dist/tools/components.js +16 -7
  114. package/dist/tools/datasources.js +83 -21
  115. package/dist/tools/dependencies.js +1 -1
  116. package/dist/tools/deployments.js +34 -2
  117. package/dist/tools/dtos.js +25 -12
  118. package/dist/tools/entities.js +113 -28
  119. package/dist/tools/environments.js +52 -0
  120. package/dist/tools/externalComponents.js +9 -3
  121. package/dist/tools/files.js +57 -42
  122. package/dist/tools/flows.js +205 -13
  123. package/dist/tools/folders.js +7 -2
  124. package/dist/tools/functions.js +331 -0
  125. package/dist/tools/globalFunctions.js +63 -0
  126. package/dist/tools/importSchemas.js +79 -0
  127. package/dist/tools/imports.js +241 -0
  128. package/dist/tools/layouts.js +103 -29
  129. package/dist/tools/merges.js +113 -0
  130. package/dist/tools/microfunctions.js +155 -0
  131. package/dist/tools/microservices.js +270 -40
  132. package/dist/tools/organizations.js +5 -6
  133. package/dist/tools/pages.js +421 -104
  134. package/dist/tools/platformInfo.js +155 -11
  135. package/dist/tools/projects.js +183 -33
  136. package/dist/tools/queries.js +32 -10
  137. package/dist/tools/revisions.js +94 -0
  138. package/dist/tools/rows.js +484 -50
  139. package/dist/tools/screenWrite.js +68 -0
  140. package/dist/tools/screens.js +28 -0
  141. package/dist/tools/templates.js +32 -5
  142. package/dist/tools/translations.js +24 -8
  143. package/dist/tools/views.js +20 -10
  144. package/dist/tools/workflows.js +56 -21
  145. package/package.json +7 -2
@@ -1,18 +1,344 @@
1
1
  import { PlatformApiError } from './errors.js';
2
+ import { INTERNAL_ROLE_SEPARATOR, knownInternalRoles } from '../knowledge/componentCatalog.js';
2
3
  import { logStep } from '../observability/trace.js';
4
+ /**
5
+ * Ziyaretçinin tema tercihinin yaşadığı localStorage anahtarı — `theme-toggle` bunu yazar,
6
+ * `Theme` okur ve projenin kendi modunun önüne geçirir
7
+ * (`zeyrek-frontend/packages/ng-apps-ui/src/shared/theme/stores.ts`). Anahtar uygulama başına
8
+ * DEĞİL tarayıcı başına tektir.
9
+ */
10
+ const VIEWER_MODE_STORAGE_KEY = 'zeyrek-viewer-theme-mode';
11
+ /**
12
+ * G-24 — gateway sayfayı kurabiliyor, okuyabiliyor ve çizdirebiliyordu ama üzerinde OLAY
13
+ * TETİKLEYEMİYORDU: "filtre değişince veri yenileniyor" iddiası yalnız şema okumasıyla
14
+ * kanıtlanabiliyordu. 09-05 koşusunda üç olay bağının ikisi şemada "bağlı" görünüp gerçekte
15
+ * çalışmıyordu (form submit hiç doğmuyordu, `code` adımının gövdesi kaydedilmemişti) ve ikisi de
16
+ * yalnız gerçek tıklamayla görüldü.
17
+ *
18
+ * Değer yazarken NATIVE setter kullanılır ve `input`+`change` olayları bubbles ile atılır: DOM
19
+ * value'sunu düz atamak framework'ün duyduğu olayı üretmez, alan dolu görünür ama akış çalışmaz.
20
+ * `resource` sayacı "istek gerçekten gitti mi" sorusunu cevaplar — koşuda kusuru kesinleştiren
21
+ * gözlem tam olarak buydu (ağ kaydında hiç istek yoktu).
22
+ */
23
+ export function actionScript(action) {
24
+ const selector = JSON.stringify(`[data-element-id="${String(action.target ?? '')}"]`);
25
+ const value = JSON.stringify(action.value ?? '');
26
+ if (action.action === 'click') {
27
+ return `(function () {
28
+ var count = performance.getEntriesByType('resource').length;
29
+ var node = document.querySelector(${selector});
30
+ if (!node) return { found: false, applied: false, requests: count };
31
+ var target = node.matches('button, a, [role="button"]') ? node : (node.querySelector('button, a, [role="button"]') || node);
32
+ target.click();
33
+ return { found: true, applied: true, requests: count };
34
+ })()`;
35
+ }
36
+ return `(function () {
37
+ var count = performance.getEntriesByType('resource').length;
38
+ var node = document.querySelector(${selector});
39
+ if (!node) return { found: false, applied: false, requests: count };
40
+ var wanted = String(${value});
41
+
42
+ // Açılır kutu (dropdown / multi-select) NATIVE alan değildir: değeri yazmak
43
+ // hiçbir şey yapmaz, seçeneğe TIKLANMASI gerekir. Liste ancak kutu açıkken
44
+ // basılır, o yüzden önce kutu açılır, sonra data-value ile seçenek bulunur
45
+ // (bulunamazsa görünen metinle eşlenir).
46
+ var combobox = node.querySelector('[role="combobox"], .ng-select__value-container, .ng-multiselect__value-container');
47
+ if (combobox) {
48
+ combobox.click();
49
+
50
+ // Liste tıklamadan SONRA basılır; aynı tick'te sorgulamak boş sonuç verir.
51
+ // evaluate döndürülen promise'i bekler, o yüzden seçim bir sonraki tur yapılır.
52
+ return new Promise(function (resolve) {
53
+ setTimeout(function () {
54
+ var options = Array.prototype.slice.call(document.querySelectorAll('[role="treeitem"][data-value]'));
55
+ var match = options.filter(function (option) { return option.getAttribute('data-value') === wanted; })[0]
56
+ || options.filter(function (option) { return (option.textContent || '').trim() === wanted; })[0];
57
+ if (!match) {
58
+ resolve({ found: true, applied: false, note: "açılır kutu açıldı ama bu değere karşılık gelen seçenek yok: " + wanted + " — options listesini page_runtime_get ile doğrulayın; seçenek sayısı: " + options.length, requests: count });
59
+ return;
60
+ }
61
+ (match.querySelector('button') || match).click();
62
+ resolve({ found: true, applied: true, requests: count });
63
+ }, 300);
64
+ });
65
+ }
66
+
67
+ var field = node.matches('input, select, textarea') ? node : node.querySelector('input, select, textarea');
68
+ if (!field) return { found: true, applied: false, note: 'düğümde input/select/textarea yok — bu komponent native alan kullanmıyor olabilir, click ile deneyin', requests: count };
69
+ var proto = field instanceof HTMLSelectElement ? HTMLSelectElement.prototype
70
+ : (field instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype);
71
+ if (field instanceof HTMLInputElement && (field.type === 'checkbox' || field.type === 'radio')) {
72
+ var shouldCheck = wanted === 'true' || wanted === '1' || wanted === 'on';
73
+ if (field.checked !== shouldCheck) field.click();
74
+ return { found: true, applied: true, requests: count };
75
+ }
76
+ var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
77
+ setter.call(field, ${value});
78
+ field.dispatchEvent(new Event('input', { bubbles: true }));
79
+ field.dispatchEvent(new Event('change', { bubbles: true }));
80
+ return { found: true, applied: true, requests: count };
81
+ })()`;
82
+ }
83
+ /**
84
+ * fullPage karesi Chromium'un viewport ötesi yakalamasıyla değil, viewport'u içerik boyuna
85
+ * büyüterek alınır: Studio runtime'ı kendi kaydırma kabını `100vh` üzerine kuruyor ve viewport
86
+ * ötesi yakalama o kabı bir kez daha çiziyordu — aynı sayfa karede iki kez görünüyordu (09-04
87
+ * Y-23). Tek karede çekilen görüntüde o birleştirme adımı hiç yok.
88
+ */
89
+ export const contentHeightScript = [
90
+ '(function () {',
91
+ ' var doc = document.documentElement;',
92
+ ' var body = document.body;',
93
+ ' var viewportHeight = window.innerHeight;',
94
+ ' var heights = [doc ? doc.scrollHeight : 0, body ? body.scrollHeight : 0];',
95
+ ' var nodes = document.querySelectorAll("*");',
96
+ ' for (var index = 0; index < nodes.length && index < 4000; index += 1) {',
97
+ ' var node = nodes[index];',
98
+ ' if (node.clientHeight >= viewportHeight * 0.6 && node.scrollHeight > node.clientHeight + 1) heights.push(node.scrollHeight);',
99
+ ' }',
100
+ ' return { contentHeight: Math.max.apply(null, heights), viewportHeight: viewportHeight };',
101
+ '})()'
102
+ ].join('\n');
103
+ /**
104
+ * Kare taşmayı gösterir ama ölçmez: aynı sayfa yüklemesinden neredeyse bedavaya çıkan ikinci
105
+ * çıktı her runtime düğümünün efektif piksel kutusudur. Düğümler `data-element-id` taşımıyorsa
106
+ * (F-04 dev'e çıkana dek) liste boş döner — tool uydurmaz, layoutReportUnavailable der.
107
+ */
108
+ export const layoutReportScript = [
109
+ 'Array.from(document.querySelectorAll("[data-element-id]")).slice(0, 500).map(function (node) {',
110
+ ' var rect = node.getBoundingClientRect();',
111
+ ' return {',
112
+ ' elementId: node.getAttribute("data-element-id"),',
113
+ ' x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height),',
114
+ ' overflowX: node.scrollWidth > node.clientWidth + 1,',
115
+ ' overflowY: node.scrollHeight > node.clientHeight + 1',
116
+ ' };',
117
+ '})'
118
+ ].join('\n');
119
+ /**
120
+ * F-09 — sayfa akışı yalnız tarayıcıda koşar, bu yüzden bir setter iteratee'sinin
121
+ * patlamasının gateway'e ulaşan HİÇBİR yolu yoktu: akış 200 dönüyor, hedef prop
122
+ * yazılmıyor ve tek belirti yığında kaybolan bir bildirim oluyordu. Runtime artık
123
+ * yapısal hata defterini bu global anahtarda yayınlıyor (`@ng-apps/flow` →
124
+ * flow-runtime-errors.ts); kare çekilirken bedavaya okunur.
125
+ */
126
+ export const flowRuntimeErrorsScript = 'Array.isArray(window.__ngAppsFlowRuntimeErrors) ? window.__ngAppsFlowRuntimeErrors.slice(0, 50) : []';
127
+ /**
128
+ * Kare ne zaman hazır? (G-30 → B-6, 2026-09-07)
129
+ *
130
+ * `delayMs` TAHMİNDİ; 09-05'te runtime koşan akış sayacını yayınlamaya başladı (`@ng-apps/flow`
131
+ * → flow-runtime-errors.ts) ve bekleme "running sıfır, completed > 0" olunca bitiyordu. Bu şart
132
+ * mount akışı OLMAYAN sayfada (giriş ekranı) hiç sağlanmaz; akış tetiklemeyen `set` eylemi de
133
+ * `completed`'ı artırmaz. 09-06·2 koşusunda üç eylemli giriş denemesi bu yüzden 47 sn sürdü;
134
+ * sentetik ölçümde sayfa 0.3 sn'de hazırken aynı çağrı 48 sn'ydi — süre sayfanın değil, bu
135
+ * döngünün. Hazırlık artık TEK anlık görüntüden okunur: akış sayacı + Studio'nun yükleme
136
+ * iskeleti (`ViewDetail.svelte` → `.view-detail > .bx--skeleton__placeholder`) + runtime
137
+ * düğümleri (`[data-element-id]`) + hata/bulunamadı hâli + adres. Karar `decideReadiness`'ta.
138
+ *
139
+ * Yüklenmemiş kare ASLA dönmez: bütçe dolduğunda sayfa hâlâ hazır değilse çağrı hatayla düşer
140
+ * (`captureUnready: true` teşhis kipi dışında). Bozuk sayfayı görmek istiyorsan hata mesajı
141
+ * neyin görüldüğünü zaten söyler.
142
+ */
143
+ export const readinessScript = `(function () { /* readinessSnapshot */
144
+ var activity = window.__ngAppsFlowActivity;
145
+ return {
146
+ activity: activity && typeof activity.running === 'number' && typeof activity.completed === 'number' ? { running: activity.running, completed: activity.completed } : null,
147
+ runtimeNodes: document.querySelectorAll('[data-element-id]').length,
148
+ skeleton: !!document.querySelector('.view-detail > .bx--skeleton__placeholder'),
149
+ viewDetail: !!document.querySelector('.view-detail'),
150
+ errorState: !!document.querySelector('.view-detail__error, .view-detail .error-page'),
151
+ readyState: String(document.readyState),
152
+ href: String(location.href)
153
+ };
154
+ })()`;
155
+ /** Okunan değer gerçekten sayaç mı; değilse sayfa sayaç yayınlamıyordur (eski runtime). */
156
+ export function isFlowActivity(activity) {
157
+ if (!activity || typeof activity !== 'object')
158
+ return false;
159
+ const { running, completed } = activity;
160
+ return typeof running === 'number' && typeof completed === 'number';
161
+ }
162
+ /** Tarayıcıdan dönen değer anlık görüntü mü; yönlendirme sırasında evaluate düşer ve undefined kalır. */
163
+ export function parseReadiness(raw) {
164
+ if (!raw || typeof raw !== 'object')
165
+ return undefined;
166
+ const value = raw;
167
+ if (typeof value.runtimeNodes !== 'number' || typeof value.href !== 'string')
168
+ return undefined;
169
+ return {
170
+ activity: isFlowActivity(value.activity) ? { running: value.activity.running, completed: value.activity.completed } : null,
171
+ runtimeNodes: value.runtimeNodes,
172
+ skeleton: value.skeleton === true,
173
+ viewDetail: value.viewDetail === true,
174
+ errorState: value.errorState === true,
175
+ readyState: String(value.readyState ?? ''),
176
+ href: value.href
177
+ };
178
+ }
179
+ /** `/dev` ve `/view` aynı sayfadır (Studio `hooks.ts` reroute); yönlendirme kararı yalnız yoldan verilir. */
180
+ export function runtimePath(url) {
181
+ try {
182
+ return new URL(url).pathname.replace(/^\/dev\//, '/view/').replace(/\/+$/, '');
183
+ }
184
+ catch {
185
+ return url;
186
+ }
187
+ }
188
+ const maxCaptureHeight = 16384;
3
189
  const navigationTimeoutMs = 20000;
4
190
  const apiResponseTimeoutMs = 15000;
5
191
  const settleMs = 600;
192
+ /** Akış bittikten sonra Svelte'in son render'ına bırakılan pay. */
193
+ const flowSettleGraceMs = 250;
194
+ const flowPollMs = 120;
195
+ /** "Hiç akış koşmadı" kararı için DOM'un kaç yoklama değişmemesi gerekir (~600 ms = eski settleMs). */
196
+ const quietPolls = 5;
197
+ /** Sayaç yayınlanmayan runtime'da tek kanıt DOM durulmasıdır; daha uzun beklenir (~1 sn). */
198
+ const legacyQuietPolls = 8;
199
+ /** Runtime dışına yönlendirme: hedef sayfanın kendi JS'i çizmeyi bitirsin (~1.5 sn). */
200
+ const redirectQuietPolls = 12;
6
201
  const idleCloseMs = 60000;
202
+ /** Etkileşim adımındaki `wait` üst sınırı; delayMs tavanıyla aynı çizgide. */
203
+ const maxActionWaitMs = 15000;
204
+ /**
205
+ * Navigasyon zaman aşımında toplam deneme sayısı. Soğuk Studio'da ilk `page.goto` Vite
206
+ * derlemesini ISITIR; ikinci deneme neredeyse her zaman geçer (09-08 S-8: aynı kare üç
207
+ * çağrıya ve ~3.5 dakikaya mal oldu). İkiden fazlası turu uzatır, bir şey kazandırmaz.
208
+ */
209
+ const navigationAttempts = 2;
210
+ /**
211
+ * Yalnız zaman aşımı yeniden denenir. `net::ERR_CONNECTION_REFUSED` / DNS gibi hızlı düşen
212
+ * hatalarda Studio ayakta değildir; tekrar yalnız bekleme süresini iki katına çıkarır ve
213
+ * çağırana yanlış ipucu verir.
214
+ */
215
+ function isNavigationTimeout(message) {
216
+ return /timeout/i.test(message);
217
+ }
218
+ /**
219
+ * Tek anlık görüntüden "kare alınabilir" kararı; iki uçta da yanlış beklememek için tek yerde.
220
+ * `quiet` görüntünün kaç yoklamadır değişmediğidir, `completedBefore` bu beklemenin başındaki
221
+ * bitmiş akış sayısı (sayfa değiştiyse çağıran sıfırlar). İskelet görünüyorsa hiçbir karar yok.
222
+ */
223
+ export function decideReadiness(snapshot, quiet, completedBefore, baselineUrl) {
224
+ if (snapshot.skeleton)
225
+ return undefined;
226
+ const { activity } = snapshot;
227
+ const idle = activity !== null && activity.running === 0;
228
+ if (idle && activity.completed > completedBefore && snapshot.runtimeNodes > 0)
229
+ return 'ready';
230
+ if (idle && snapshot.runtimeNodes > 0 && quiet >= quietPolls)
231
+ return 'ready-no-flows';
232
+ if (activity === null && snapshot.runtimeNodes > 0 && quiet >= legacyQuietPolls)
233
+ return 'ready-no-counter';
234
+ if (idle && snapshot.errorState && quiet >= quietPolls)
235
+ return 'error';
236
+ if (!snapshot.viewDetail && snapshot.readyState === 'complete' && baselineUrl !== undefined && runtimePath(snapshot.href) !== runtimePath(baselineUrl) && quiet >= redirectQuietPolls)
237
+ return 'redirected';
238
+ return undefined;
239
+ }
240
+ /** Hata mesajı için: bütçe dolduğunda sayfada NE görüldü. */
241
+ export function describeUnready(readiness, apiNote) {
242
+ const snapshot = readiness.snapshot;
243
+ const suffix = apiNote === undefined ? '' : ` · ${apiNote}`;
244
+ if (!snapshot)
245
+ return `tarayıcı sayfayı hiç değerlendiremedi (sürekli yönlendirme ya da bağlam düştü)${suffix}`;
246
+ if (snapshot.skeleton)
247
+ return `sayfa hâlâ yükleme iskeletinde${suffix}`;
248
+ if (snapshot.activity && snapshot.activity.running > 0)
249
+ return `${snapshot.activity.running} akış hâlâ koşuyor (${snapshot.activity.completed} bitti)${suffix}`;
250
+ if (snapshot.runtimeNodes === 0)
251
+ return `runtime hiç render olmadı — data-element-id taşıyan düğüm yok${snapshot.viewDetail ? '' : ', .view-detail kabı da yok'}${suffix}`;
252
+ return `sayfa durulmadı (${snapshot.runtimeNodes} düğüm, sayaç ${snapshot.activity ? `${snapshot.activity.running} koşan / ${snapshot.activity.completed} biten` : 'yok'})${suffix}`;
253
+ }
254
+ async function measureContentHeight(page) {
255
+ const measured = await page.evaluate(contentHeightScript).catch(() => undefined);
256
+ const height = measured?.contentHeight;
257
+ return typeof height === 'number' && Number.isFinite(height) && height > 0 ? Math.ceil(height) : undefined;
258
+ }
259
+ /**
260
+ * Playwright bağlamı ve tarayıcısı bu sınıfta PAYLAŞILIR. İki çekim aynı anda koştuğunda
261
+ * 2026-09-06 koşusunda ikisi de `Target page, context or browser has been closed` ile düştü
262
+ * (çağrı #204/#205, 64 sn kayıp, ajan yeniden giriş yapmak zorunda kaldı — `S-3`). Kök neden
263
+ * iki yerdeydi: (1) paylaşılan tarayıcı üstünde eşzamanlı bağlam açma, (2) her çekimin `finally`
264
+ * bloğunda ÖNCEKİ boşta-kapatma zamanlayıcısı temizlenmeden yenisinin kurulması — kaçan
265
+ * zamanlayıcı sonraki çekimin ortasında tarayıcıyı kapatıyordu.
266
+ *
267
+ * Çözüm bir seri kuyruk: çekimler tek tek koşar, bekleyen çağrı `queuedMs` ile ne kadar
268
+ * beklediğini söyler. Sıcak tarayıcı havuzu bu işin dışıdır (Ç2 · tur ekonomisi).
269
+ */
7
270
  export class StudioScreenshotter {
8
271
  config;
9
272
  launcher;
10
273
  browserPromise;
11
274
  idleTimer;
275
+ queue = Promise.resolve();
276
+ pending = 0;
277
+ recycleNext = false;
12
278
  constructor(config, launcher) {
13
279
  this.config = config;
14
280
  this.launcher = launcher;
15
281
  }
282
+ /**
283
+ * Kuyruk sınırsız birikmesin: bekleme üst sınırı aşılırsa çağrı tarayıcıya HİÇ girmeden
284
+ * anlaşılır bir hatayla döner ve sırasını öndekine zincirleyip çıkar, böylece kuyruğun geri
285
+ * kalanı serileşmiş kalır (Kısıt: tek yavaş çekimin arkasında kuyruk şişmemeli).
286
+ */
287
+ async capture(request) {
288
+ const queuedAt = Date.now();
289
+ // Sayaç SENKRON artar: `Promise.all` ile aynı turda gelen iki çağrıda ikincisi beklediğini
290
+ // ancak böyle bilebilir (async bir bayrak o turda henüz kurulmamış olurdu).
291
+ const waited = this.pending > 0;
292
+ const previous = this.queue;
293
+ this.pending += 1;
294
+ let release;
295
+ this.queue = new Promise(resolve => {
296
+ release = resolve;
297
+ });
298
+ let timer;
299
+ const capExceeded = await Promise.race([
300
+ previous.then(() => false, () => false),
301
+ new Promise(resolve => {
302
+ timer = setTimeout(() => resolve(true), this.config.screenshotQueueWaitMs);
303
+ timer.unref?.();
304
+ })
305
+ ]);
306
+ if (timer)
307
+ clearTimeout(timer);
308
+ if (capExceeded) {
309
+ this.pending -= 1;
310
+ void previous.then(release, release);
311
+ throw new PlatformApiError(`Ekran görüntüsü kuyruğunda ${Date.now() - queuedAt} ms beklendi ve üst sınır (${this.config.screenshotQueueWaitMs} ms) aşıldı — çekim başlatılmadı. Çekimleri seri çağırın ya da SCREENSHOT_QUEUE_WAIT_MS ile sınırı yükseltin.`, 503);
312
+ }
313
+ const queuedMs = Date.now() - queuedAt;
314
+ const recycled = this.recycleNext;
315
+ this.recycleNext = false;
316
+ if (waited)
317
+ logStep('tarayıcı ⏸', `çekim kuyrukta ${queuedMs} ms bekledi — eşzamanlı çekimler seri koşar`);
318
+ try {
319
+ const result = await this.captureOne(request);
320
+ return {
321
+ ...result,
322
+ ...(waited ? { queuedMs: Math.max(queuedMs, 1) } : {}),
323
+ ...(recycled ? { contextRecycled: true } : {})
324
+ };
325
+ }
326
+ catch (error) {
327
+ // Bağlam/tarayıcı bozulduysa bir sonraki çağrı TEMİZ bir tarayıcıyla başlasın; hata
328
+ // kuyruktaki komşuya bulaşmasın.
329
+ if (/browser has been closed|target (page|closed)|context.*closed/i.test(error instanceof Error ? error.message : String(error))) {
330
+ this.recycleNext = true;
331
+ const expired = this.browserPromise;
332
+ this.browserPromise = undefined;
333
+ expired?.then(instance => instance.close()).catch(() => undefined);
334
+ }
335
+ throw error;
336
+ }
337
+ finally {
338
+ this.pending -= 1;
339
+ release();
340
+ }
341
+ }
16
342
  async launch() {
17
343
  if (this.launcher)
18
344
  return this.launcher();
@@ -48,7 +374,124 @@ export class StudioScreenshotter {
48
374
  * işaretlenir ve kare yine çekilir, çünkü bozuk/boş sayfayı görmek de bir doğrulamadır. Veri
49
375
  * yanıtından sonra fontlar + kısa yerleşme beklemesi Svelte hydrate'ine alan bırakır.
50
376
  */
51
- async capture(request) {
377
+ async runActions(page, request) {
378
+ if (!request.actions || request.actions.length === 0)
379
+ return undefined;
380
+ const outcomes = [];
381
+ for (const [index, action] of request.actions.entries()) {
382
+ if (action.action === 'wait') {
383
+ await page.waitForTimeout(Math.min(Math.max(Number(action.ms ?? settleMs), 0), maxActionWaitMs));
384
+ outcomes.push({ index, action: 'wait', found: true, applied: true, requests: 0 });
385
+ continue;
386
+ }
387
+ const snapshotBefore = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
388
+ const activityBefore = snapshotBefore?.activity ?? null;
389
+ const raw = await page.evaluate(actionScript(action)).catch(error => ({
390
+ found: false,
391
+ applied: false,
392
+ note: error instanceof Error ? error.message : String(error),
393
+ requests: 0
394
+ }));
395
+ const before = (raw ?? {});
396
+ // Hedef bulunamadıysa ajan neyi yanlış yazdığını KAREDEN çıkaramaz. `__` taşıyan hedef bir
397
+ // iç rol denemesidir: bilinen roller sayılır. Kök hedefi bulunamadıysa sorun elementId'nin
398
+ // kendisindedir, şemayı okumaya yönlendirilir (P1-3 Step 3).
399
+ if (before.found === false && !before.note && typeof action.target === 'string') {
400
+ before.note = action.target.includes(INTERNAL_ROLE_SEPARATOR)
401
+ ? `'${action.target}' sayfada yok. Bu bir İÇ ROL hedefi: biçim <elementId>__<rol> ve bilinen roller — ${knownInternalRoles().join(' · ')}. Kolon rolleri kolonun SLUG'ından türer (başlığından değil); satır sırası 0 tabanlıdır ve sayfa değişince yeniden numaralanır.`
402
+ : `'${action.target}' elementId'si sayfada yok. page_schema_get ile elementId'leri listeleyin; bir komponentin İÇ kontrolünü hedefliyorsanız <elementId>__<rol> biçimini kullanın (component_get çıktısındaki internalRoles).`;
403
+ }
404
+ // Olay akışı uç çağırıyor: yerleşme beklemeden sonraki istek sayısını ölçmek "istek gitti mi"
405
+ // sorusunu cevaplar — koşuda kusuru kesinleştiren gözlem buydu.
406
+ //
407
+ // Bekleme ana yükleme döngüsüyle AYNI hazırlık kararıdır: akış koştuysa bitince, hiç akış
408
+ // tetiklenmediyse (metin alanına `set`) DOM durulunca, eylem sayfayı değiştirdiyse yeni
409
+ // sayfa hazır olunca çıkar. Eskiden akış tetiklemeyen her eylem `settleMs + delayMs`'in
410
+ // tamamını bekliyordu: 09-06·2 koşusunda üç eylemli giriş denemesi 47 sn (B-6).
411
+ const readiness = await this.waitForReady(page, {
412
+ budgetMs: apiResponseTimeoutMs + request.delayMs,
413
+ baselineUrl: snapshotBefore?.href ?? request.url,
414
+ completedBefore: activityBefore?.completed ?? Number.POSITIVE_INFINITY
415
+ });
416
+ if (readiness.state === 'not-ready' && request.captureUnready !== true) {
417
+ throw new PlatformApiError(`Etkileşim adımı ${index} (${action.action}${action.target === undefined ? '' : ` '${action.target}'`}) sonrasında sayfa ${readiness.waitedMs} ms içinde durulmadı, kare ALINMADI — ${describeUnready(readiness)}. Bekleme bütçesi ${apiResponseTimeoutMs + request.delayMs} ms; akış gerçekten uzun sürüyorsa delayMs ile büyütün. Yüklenmemiş hâli yine de görmek için captureUnready: true verin (kare pageState: 'not-ready' ile döner).`, 504);
418
+ }
419
+ const after = await page.evaluate("performance.getEntriesByType('resource').length").catch(() => undefined);
420
+ const requests = typeof after === 'number' && typeof before.requests === 'number' ? Math.max(after - before.requests, 0) : 0;
421
+ // Yerel bir akış (yalnız prop yazan setter) hiç uca gitmez ama işini YAPAR. Sayaç bu ayrımı
422
+ // veren tek kanıttır; sayaç yoksa flowRuns bilinmez kalır. Eylem sayfayı değiştirdiyse
423
+ // yeni sayfanın sayacı sıfırdan başlar, orada biten her akış bu eyleme yazılır.
424
+ const activityAfter = readiness.snapshot?.activity ?? null;
425
+ const navigated = snapshotBefore !== undefined && readiness.snapshot !== undefined && runtimePath(readiness.snapshot.href) !== runtimePath(snapshotBefore.href);
426
+ const flowRuns = activityAfter === null
427
+ ? undefined
428
+ : navigated
429
+ ? activityAfter.completed
430
+ : activityBefore === null ? undefined : Math.max(activityAfter.completed - activityBefore.completed, 0);
431
+ outcomes.push({
432
+ index,
433
+ action: String(action.action),
434
+ ...(action.target === undefined ? {} : { target: action.target }),
435
+ found: before.found === true,
436
+ applied: before.applied === true,
437
+ ...(before.note === undefined ? {} : { note: before.note }),
438
+ requests,
439
+ ...(flowRuns === undefined ? {} : { flowRuns }),
440
+ pageState: readiness.state
441
+ });
442
+ }
443
+ logStep('tarayıcı ⤵', `${outcomes.length} etkileşim adımı koştu`, outcomes.map(outcome => `${outcome.index}. ${outcome.action}${outcome.target ? ` '${outcome.target}'` : ''} → ${outcome.found ? (outcome.applied ? 'uygulandı' : 'bulundu ama uygulanamadı') : 'hedef BULUNAMADI'} · ${outcome.requests} ağ isteği`));
444
+ return outcomes;
445
+ }
446
+ /**
447
+ * Sayfa hazır olana kadar yoklar; hazır kararını `decideReadiness` verir. Karar verildikten
448
+ * sonra kısa bir pay bırakılır ve görüntü BİR KEZ DAHA doğrulanır: giriş akışının son adımı
449
+ * yönlendirmeyse "bitti" sinyali yeni sayfa açılırken gelir ve doğrulama olmadan kare o
450
+ * geçişte alınırdı. Adres değiştiyse sayaç yeni sayfada sıfırdan başlar; baz da sıfırlanır.
451
+ *
452
+ * Döngü hem süreyle hem TUR sayısıyla sınırlıdır: sahte saatle koşan testte `Date.now()`
453
+ * ilerlemez ve yalnız süreye bakan bir döngü hiç bitmezdi.
454
+ */
455
+ async waitForReady(page, options) {
456
+ const startedAt = Date.now();
457
+ const deadline = startedAt + options.budgetMs;
458
+ const maxPolls = Math.ceil(options.budgetMs / flowPollMs) + 1;
459
+ let completedBefore = options.completedBefore;
460
+ let previousKey;
461
+ let quiet = 0;
462
+ let last;
463
+ for (let poll = 0; poll < maxPolls; poll++) {
464
+ const snapshot = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
465
+ if (snapshot === undefined) {
466
+ // Yönlendirme sırasında evaluate düşer: sessizlik sayacı sıfırlanır, yoklama sürer.
467
+ previousKey = undefined;
468
+ quiet = 0;
469
+ }
470
+ else {
471
+ last = snapshot;
472
+ if (runtimePath(snapshot.href) !== runtimePath(options.baselineUrl))
473
+ completedBefore = 0;
474
+ const key = JSON.stringify(snapshot);
475
+ quiet = key === previousKey ? quiet + 1 : 0;
476
+ previousKey = key;
477
+ const state = decideReadiness(snapshot, quiet, completedBefore, options.baselineUrl);
478
+ if (state !== undefined) {
479
+ await page.waitForTimeout(flowSettleGraceMs);
480
+ const confirmed = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
481
+ if (confirmed !== undefined && !confirmed.skeleton && confirmed.href === snapshot.href && (confirmed.activity === null || confirmed.activity.running === 0)) {
482
+ return { state, waitedMs: Date.now() - startedAt, snapshot: confirmed };
483
+ }
484
+ previousKey = undefined;
485
+ quiet = 0;
486
+ }
487
+ }
488
+ if (Date.now() >= deadline || poll === maxPolls - 1)
489
+ break;
490
+ await page.waitForTimeout(flowPollMs);
491
+ }
492
+ return { state: 'not-ready', waitedMs: Date.now() - startedAt, ...(last === undefined ? {} : { snapshot: last }) };
493
+ }
494
+ async captureOne(request) {
52
495
  const startedAt = Date.now();
53
496
  if (this.idleTimer)
54
497
  clearTimeout(this.idleTimer);
@@ -69,32 +512,117 @@ export class StudioScreenshotter {
69
512
  });
70
513
  context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor: 1 });
71
514
  }
72
- logStep('tarayıcı →', `GET ${request.url} · viewport ${request.viewport.width}x${request.viewport.height}${request.fullPage ? ' · fullPage' : ''}${request.token ? '' : ' · ANONİM (çerez YOK)'}`);
515
+ logStep('tarayıcı →', `GET ${request.url} · viewport ${request.viewport.width}x${request.viewport.height}${request.fullPage ? ' · fullPage' : ''}${request.token ? '' : ' · gateway jetonu YOK'}${(request.cookies ?? []).length > 0 ? ` · caller çerezleri: ${(request.cookies ?? []).map(cookie => cookie.name).join(', ')}` : ''}${request.token || (request.cookies ?? []).length > 0 ? '' : ' · ANONİM (çerez YOK)'}${request.colorScheme ? ` · tema ${request.colorScheme}` : ''}`);
73
516
  try {
74
- if (request.token) {
75
- await context.addCookies([{ name: 'NG_AccessToken', value: request.token, url: request.cookieUrl, httpOnly: false }]);
517
+ const cookies = new Map();
518
+ if (request.token)
519
+ cookies.set('NG_AccessToken', request.token);
520
+ for (const cookie of request.cookies ?? [])
521
+ cookies.set(cookie.name, cookie.value);
522
+ if (cookies.size > 0) {
523
+ await context.addCookies([...cookies].map(([name, value]) => ({ name, value, url: request.cookieUrl, httpOnly: false })));
524
+ }
525
+ // Tema tercihi çerezde değil localStorage'da yaşar ve tema store'u değerini modül İLK
526
+ // değerlendirildiğinde okur: sayfa açıldıktan sonra yazmak bir sonraki yüklemeye kadar
527
+ // etkisizdir. Bu yüzden tercih, sayfanın script'lerinden önce koşan bağlam script'iyle
528
+ // ekilir. Yeteneği olmayan bağlamda çağrı SESSİZCE açık temada kare almaz, hata verir.
529
+ if (request.colorScheme) {
530
+ if (!context.addInitScript) {
531
+ throw new PlatformApiError(`colorScheme='${request.colorScheme}' uygulanamadı: tarayıcı bağlamı addInitScript desteklemiyor. Tema tercihi sayfa açılmadan ekilmek zorunda; desteksiz bağlamda kare projenin varsayılan modunda çıkardı ve bu sessizce yanlış olurdu.`);
532
+ }
533
+ await context.addInitScript(`try { localStorage.setItem(${JSON.stringify(VIEWER_MODE_STORAGE_KEY)}, ${JSON.stringify(request.colorScheme)}) } catch (error) { /* localStorage kapalıysa proje varsayılanı çizilir */ }`);
76
534
  }
77
535
  const page = await context.newPage();
78
- const observedApiStatus = page
79
- .waitForResponse(response => response.url().includes(request.apiPathMarker), { timeout: apiResponseTimeoutMs })
536
+ // Soğuk Vite derlemesinde sayfanın kendi veri isteği 15 sn'yi aşabiliyor; delayMs bu
537
+ // bütçeyi de uzatır (09-02 E-2). Yanıt hazırlık döngüsünü BLOKLAMAZ: runtime dışına
538
+ // yönlendirilen sayfa bu isteği hiç atmaz ve eskiden 15 sn boşa bekleniyordu.
539
+ const readyBudgetMs = apiResponseTimeoutMs + request.delayMs;
540
+ const watchApiResponse = () => page
541
+ .waitForResponse(response => response.url().includes(request.apiPathMarker), { timeout: readyBudgetMs })
80
542
  .then(response => response.status(), () => undefined);
81
- try {
82
- await page.goto(request.url, { waitUntil: 'domcontentloaded', timeout: navigationTimeoutMs });
543
+ // `delayMs` navigasyon bütçesini de uzatır. Eskiden sabit 20 sn'ydi ve soğuk Studio'da
544
+ // ajanın elinde tek çıkış çağrıyı tekrarlamaktı (09-08 S-8) — "sabrım bu kadar" demenin
545
+ // tek anlamı olsun diye iki bütçe de aynı düğmeye bağlı.
546
+ const navigationTimeout = navigationTimeoutMs + request.delayMs;
547
+ let observedApiStatus = watchApiResponse();
548
+ let navigationRetried = false;
549
+ for (let attempt = 1;; attempt++) {
550
+ try {
551
+ await page.goto(request.url, { waitUntil: 'domcontentloaded', timeout: navigationTimeout });
552
+ break;
553
+ }
554
+ catch (error) {
555
+ const message = error instanceof Error ? error.message : String(error);
556
+ const timedOut = isNavigationTimeout(message);
557
+ if (!timedOut || attempt >= navigationAttempts) {
558
+ const attemptNote = attempt > 1 ? ` (${attempt} deneme)` : '';
559
+ const budgetNote = timedOut
560
+ ? ` Navigasyon bütçesi ${navigationTimeout} ms'ydi; soğuk Studio'da delayMs'i büyütün — o değer artık navigasyonu da uzatır.`
561
+ : '';
562
+ throw new PlatformApiError(`Studio'ya ulaşılamadı${attemptNote}: ${request.url} — ${message}.${budgetNote} STUDIO_BASE_URL doğru mu, arayüz ayakta mı? (yerelde zeyrek-frontend/apps/studio içinde \`pnpm dev\`)`);
563
+ }
564
+ navigationRetried = true;
565
+ // Veri isteği gözlemi de sıfırlanır: ilk denemeyi bekleyen sayaç çoktan dolmuş olur ve
566
+ // yeniden denenen navigasyonun yanıtını kaçırıp boş loader karesi damgalardı.
567
+ observedApiStatus = watchApiResponse();
568
+ logStep('tarayıcı ⟳', `navigasyon ${navigationTimeout} ms'de düştü, bir kez daha deneniyor — ilk deneme soğuk derlemeyi ısıttı`);
569
+ }
83
570
  }
84
- catch (error) {
85
- const message = error instanceof Error ? error.message : String(error);
86
- throw new PlatformApiError(`Studio'ya ulaşılamadı: ${request.url} — ${message}. STUDIO_BASE_URL doğru mu, arayüz ayakta mı? (yerelde zeyrek-frontend/apps/studio içinde \`pnpm dev\`)`);
571
+ const readiness = await this.waitForReady(page, { budgetMs: readyBudgetMs, baselineUrl: request.url, completedBefore: 0 });
572
+ // Gözlendiyse dolu, hâlâ bekleniyorsa (yönlendirme, hata) undefined — ikinci vaat hemen çözülür.
573
+ const apiStatus = await Promise.race([observedApiStatus, Promise.resolve(undefined)]);
574
+ const apiNote = apiStatus === undefined ? 'sayfanın veri isteği gözlenmedi' : `veri isteği HTTP ${apiStatus}`;
575
+ if (readiness.state === 'not-ready' && request.captureUnready !== true) {
576
+ throw new PlatformApiError(`Sayfa ${readiness.waitedMs} ms içinde yüklenmedi, kare ALINMADI — ${describeUnready(readiness, apiNote)}. Bekleme bütçesi ${readyBudgetMs} ms; sayfa yalnız yavaşsa delayMs ile büyütün. Sayfa gerçekten bozuksa page_runtime_get ile şemayı, STUDIO_BASE_URL ve slug'ları doğrulayın. Yüklenmemiş hâli yine de görmek için captureUnready: true verin (kare pageState: 'not-ready' ile döner).`, 504);
87
577
  }
88
- const apiStatus = await observedApiStatus;
89
578
  await page.evaluate('document.fonts.ready.then(() => true)').catch(() => undefined);
90
- await page.waitForTimeout(settleMs + request.delayMs);
91
- const png = await page.screenshot({ type: 'png', fullPage: request.fullPage });
579
+ const flowsSettled = readiness.state === 'ready' || readiness.state === 'ready-no-flows'
580
+ ? true
581
+ : readiness.state === 'not-ready' ? false : undefined;
582
+ const finalHref = readiness.snapshot?.href;
583
+ const finalUrl = finalHref !== undefined && runtimePath(finalHref) !== runtimePath(request.url) ? finalHref : undefined;
584
+ const measured = request.fullPage ? await measureContentHeight(page) : undefined;
585
+ const captureHeight = measured === undefined ? undefined : Math.min(measured, maxCaptureHeight);
586
+ if (captureHeight !== undefined && captureHeight > request.viewport.height) {
587
+ await page.setViewportSize({ width: request.viewport.width, height: captureHeight });
588
+ await page.waitForTimeout(settleMs);
589
+ }
590
+ const actions = await this.runActions(page, request);
591
+ const layout = request.layoutReport === true
592
+ ? await page.evaluate(layoutReportScript).then(value => Array.isArray(value) ? value : []).catch(() => [])
593
+ : undefined;
594
+ const runtimeErrors = await page.evaluate(flowRuntimeErrorsScript)
595
+ .then(value => Array.isArray(value) ? value : [])
596
+ .catch(() => []);
597
+ const png = await page.screenshot({ type: 'png', fullPage: false });
92
598
  const elapsedMs = Date.now() - startedAt;
93
- logStep('tarayıcı ←', `PNG ${png.byteLength} B · ${elapsedMs} ms · sayfa API yanıtı ${apiStatus === undefined ? 'gözlenmedi' : `HTTP ${apiStatus}`}`);
94
- return { png, apiStatus, renderWaitTimedOut: apiStatus === undefined, elapsedMs };
599
+ logStep('tarayıcı ←', `PNG ${png.byteLength} B · ${elapsedMs} ms · hazırlık ${readiness.state} (${readiness.waitedMs} ms) · sayfa API yanıtı ${apiStatus === undefined ? 'gözlenmedi' : `HTTP ${apiStatus}`}${finalUrl === undefined ? '' : ` · yönlendi → ${finalUrl}`}`);
600
+ if (runtimeErrors.length > 0) {
601
+ logStep('akış hatası ✕', `${runtimeErrors.length} akış adımı hata bıraktı — sayfa yine 200 döndü`, runtimeErrors.map(error => `${error.step?.name ?? '?'}${error.element ? ` → ${error.element}.${error.property ?? '?'}` : ''}: ${error.message ?? ''}`));
602
+ }
603
+ return {
604
+ png,
605
+ apiStatus,
606
+ renderWaitTimedOut: apiStatus === undefined,
607
+ elapsedMs,
608
+ pageState: readiness.state,
609
+ readyAfterMs: readiness.waitedMs,
610
+ ...(finalUrl === undefined ? {} : { finalUrl }),
611
+ ...(measured === undefined ? {} : { contentHeight: measured }),
612
+ ...(measured !== undefined && measured > maxCaptureHeight ? { fullPageTruncated: true } : {}),
613
+ ...(layout === undefined ? {} : { layout }),
614
+ ...(actions === undefined ? {} : { actions }),
615
+ ...(runtimeErrors.length > 0 ? { runtimeErrors } : {}),
616
+ ...(flowsSettled === undefined ? {} : { flowsSettled }),
617
+ ...(navigationRetried ? { navigationRetried: true } : {})
618
+ };
95
619
  }
96
620
  finally {
97
621
  await context.close().catch(() => undefined);
622
+ // Zamanlayıcı ÖNCE temizlenir: eskiden her çekim yenisini kurup eskisini bırakıyordu ve
623
+ // kaçan zamanlayıcı sonraki çekimin ortasında tarayıcıyı kapatıyordu (S-3).
624
+ if (this.idleTimer)
625
+ clearTimeout(this.idleTimer);
98
626
  this.idleTimer = setTimeout(() => {
99
627
  const expired = this.browserPromise;
100
628
  this.browserPromise = undefined;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Desen `trace.ts`'in eski kopyasından DARALTILDI: `pass` parçası `passed`/`bypass` gibi
3
+ * anahtarları da yakalıyordu. Günlükte zararsızdı, tool yanıtında veri kaybı olurdu.
4
+ */
5
+ export const secretKeyPattern = /password|passwd|secret|token|authorization|api[-_]?key|credential|connection[-_]?string/i;
6
+ import { looksLikeJwt, rememberToken } from './tokenVault.js';
7
+ const maskedValue = '***';
8
+ const maxPaths = 20;
9
+ const maxDepth = 12;
10
+ function walk(value, path, masked, refs, depth) {
11
+ if (depth > maxDepth)
12
+ return value;
13
+ if (Array.isArray(value))
14
+ return value.map((entry, index) => walk(entry, `${path}[${index}]`, masked, refs, depth + 1));
15
+ if (value && typeof value === 'object') {
16
+ const result = {};
17
+ for (const [key, entry] of Object.entries(value)) {
18
+ const child = path === '' ? key : `${path}.${key}`;
19
+ if (secretKeyPattern.test(key) && entry !== null && entry !== undefined && entry !== '') {
20
+ if (masked.length < maxPaths)
21
+ masked.push(child);
22
+ // Maskelenen değer bir JWT ise kimlik testinin yolu kapanmasın: jeton kasada tutulur ve
23
+ // yanıta yalnız referansı döner (page_screenshot/page_runtime_get asUser onu çözer).
24
+ // Parola ve API anahtarı bu şekle uymadığı için referans almaz.
25
+ if (looksLikeJwt(entry) && Object.keys(refs).length < maxPaths)
26
+ refs[child] = rememberToken(entry);
27
+ result[key] = maskedValue;
28
+ continue;
29
+ }
30
+ result[key] = walk(entry, child, masked, refs, depth + 1);
31
+ }
32
+ return result;
33
+ }
34
+ return value;
35
+ }
36
+ export function maskSecrets(payload) {
37
+ const masked = [];
38
+ const tokenRefs = {};
39
+ return { payload: walk(payload, '', masked, tokenRefs, 0), masked, tokenRefs };
40
+ }