@marsaude/devtools-shell 0.1.5 → 0.1.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.
@@ -220,7 +220,8 @@ class DevtoolsShellComponent {
220
220
  cursor: this.dragging() ? 'grabbing' : 'grab',
221
221
  };
222
222
  }, ...(ngDevMode ? [{ debugName: "fabStyle" }] : []));
223
- /** Radial speed-dial geometry — ported verbatim (R=268, 5°→87°). */
223
+ /** Radial speed-dial geometry — items fan along a 5°→87° arc; the radius
224
+ * scales with the item count (clamped to the viewport) so they never pile up. */
224
225
  this.dialItems = computed(() => {
225
226
  const S = FAB_SIZE;
226
227
  const W = this.winW();
@@ -233,12 +234,26 @@ class DevtoolsShellComponent {
233
234
  const vy = cy < H * 0.42 ? 1 : -1;
234
235
  const acts = this.actions();
235
236
  const N = acts.length;
236
- const R = 268;
237
237
  const a0 = (5 * Math.PI) / 180;
238
238
  const a1 = (87 * Math.PI) / 180;
239
+ const span = a1 - a0;
240
+ // Tile box + the center-to-center spacing we want between adjacent items.
241
+ // Smaller SPACING pulls the whole fan in closer to the FAB (since every
242
+ // item sits at the same radius R), while staying just above the tile size
243
+ // so they don't overlap.
244
+ const TILE_W = 50;
245
+ const TILE_H = 44;
246
+ const SPACING = 52;
247
+ // Radius that yields that spacing across N items, clamped so the farthest
248
+ // tile still fits the viewport in the fan direction. Scaling R with N is
249
+ // what keeps the items from piling on top of each other as more are added.
250
+ const desiredR = N > 1 ? (SPACING * (N - 1)) / span : 200;
251
+ const availH = (hx > 0 ? W - cx : cx) - TILE_W / 2 - 12;
252
+ const availV = (vy > 0 ? H - cy : cy) - TILE_H / 2 - 12;
253
+ const R = Math.max(160, Math.min(desiredR, availH, availV));
239
254
  return acts.map((action, i) => {
240
255
  const t = N === 1 ? 0.5 : i / (N - 1);
241
- const ang = a0 + (a1 - a0) * t;
256
+ const ang = a0 + span * t;
242
257
  const dx = hx * Math.cos(ang) * R;
243
258
  const dy = vy * Math.sin(ang) * R;
244
259
  return {
@@ -246,10 +261,10 @@ class DevtoolsShellComponent {
246
261
  style: {
247
262
  position: 'fixed',
248
263
  zIndex: '41',
249
- width: '58px',
250
- height: '50px',
251
- left: cx - 29 + 'px',
252
- top: cy - 25 + 'px',
264
+ width: TILE_W + 'px',
265
+ height: TILE_H + 'px',
266
+ left: cx - TILE_W / 2 + 'px',
267
+ top: cy - TILE_H / 2 + 'px',
253
268
  display: 'flex',
254
269
  flexDirection: 'column',
255
270
  alignItems: 'center',
@@ -1645,6 +1660,302 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
1645
1660
  type: Injectable
1646
1661
  }] });
1647
1662
 
1663
+ /** Ring-buffer cap so a long QA session never grows unbounded. */
1664
+ const MAX_ENTRIES$1 = 300;
1665
+ /**
1666
+ * In-memory telemetry of every HTTP call to the API host (the one in
1667
+ * `apiBaseUrl`). Patches `fetch` and `XMLHttpRequest` once so it captures BOTH
1668
+ * the host app's requests and the package's own — independent of which
1669
+ * HttpClient/interceptor issued them. Nothing is persisted: a page reload
1670
+ * clears the log.
1671
+ */
1672
+ class DevtoolsTelemetryService {
1673
+ constructor() {
1674
+ this.config = inject(DEVTOOLS_CONFIG);
1675
+ this._entries = signal([], ...(ngDevMode ? [{ debugName: "_entries" }] : []));
1676
+ this.entries = this._entries.asReadonly();
1677
+ this.installed = false;
1678
+ this.seq = 0;
1679
+ this.origin = this.deriveOrigin();
1680
+ }
1681
+ clear() {
1682
+ this._entries.set([]);
1683
+ }
1684
+ /** Monkeypatch fetch + XHR once. Safe to call multiple times. */
1685
+ install() {
1686
+ if (this.installed || typeof window === 'undefined')
1687
+ return;
1688
+ this.installed = true;
1689
+ this.patchFetch();
1690
+ this.patchXhr();
1691
+ }
1692
+ // ---- capture helpers --------------------------------------------------
1693
+ deriveOrigin() {
1694
+ try {
1695
+ return new URL(this.config.apiBaseUrl).origin;
1696
+ }
1697
+ catch {
1698
+ return this.config.apiBaseUrl;
1699
+ }
1700
+ }
1701
+ matches(url) {
1702
+ try {
1703
+ return new URL(url, window.location.href).origin === this.origin;
1704
+ }
1705
+ catch {
1706
+ return url.includes(this.origin);
1707
+ }
1708
+ }
1709
+ parseParams(url) {
1710
+ try {
1711
+ const out = {};
1712
+ new URL(url, window.location.href).searchParams.forEach((v, k) => (out[k] = v));
1713
+ return out;
1714
+ }
1715
+ catch {
1716
+ return {};
1717
+ }
1718
+ }
1719
+ /** Decode an arraybuffer response to text/JSON; fall back to a size label if binary. */
1720
+ decodeArrayBuffer(buf) {
1721
+ if (!buf)
1722
+ return null;
1723
+ try {
1724
+ const text = new TextDecoder().decode(buf);
1725
+ const parsed = this.tryParse(text);
1726
+ if (typeof parsed !== 'string')
1727
+ return parsed; // parsed JSON object/array/number
1728
+ // Raw string: keep only if it looks like text, otherwise it's binary.
1729
+ return /[\u0000-\u0008\u000e-\u001f]/.test(parsed) ? `[arraybuffer ${buf.byteLength}b]` : parsed;
1730
+ }
1731
+ catch {
1732
+ return `[arraybuffer ${buf.byteLength ?? '?'}b]`;
1733
+ }
1734
+ }
1735
+ tryParse(text) {
1736
+ if (text == null || text === '')
1737
+ return null;
1738
+ try {
1739
+ return JSON.parse(text);
1740
+ }
1741
+ catch {
1742
+ return text;
1743
+ }
1744
+ }
1745
+ normalizeBody(body) {
1746
+ if (body == null)
1747
+ return null;
1748
+ if (typeof body === 'string')
1749
+ return this.tryParse(body);
1750
+ if (typeof FormData !== 'undefined' && body instanceof FormData) {
1751
+ const out = {};
1752
+ body.forEach((v, k) => (out[k] = v instanceof File ? `[File ${v.name}]` : v));
1753
+ return out;
1754
+ }
1755
+ if (typeof Blob !== 'undefined' && body instanceof Blob)
1756
+ return `[Blob ${body.size}b]`;
1757
+ return body;
1758
+ }
1759
+ begin(method, url, body, source) {
1760
+ const entry = {
1761
+ id: ++this.seq,
1762
+ startedAt: Date.now(),
1763
+ method,
1764
+ url,
1765
+ params: this.parseParams(url),
1766
+ requestBody: this.normalizeBody(body),
1767
+ status: null,
1768
+ ok: null,
1769
+ responseBody: null,
1770
+ error: null,
1771
+ durationMs: null,
1772
+ pending: true,
1773
+ source,
1774
+ };
1775
+ this._entries.update((list) => [entry, ...list].slice(0, MAX_ENTRIES$1));
1776
+ return entry;
1777
+ }
1778
+ finish(id, patch) {
1779
+ this._entries.update((list) => list.map((e) => (e.id === id ? { ...e, ...patch, pending: false } : e)));
1780
+ }
1781
+ // ---- patches ----------------------------------------------------------
1782
+ patchFetch() {
1783
+ if (typeof window.fetch !== 'function')
1784
+ return;
1785
+ const orig = window.fetch.bind(window);
1786
+ const self = this;
1787
+ window.fetch = function (input, init) {
1788
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
1789
+ if (!self.matches(url))
1790
+ return orig(input, init);
1791
+ const method = (init?.method || (input instanceof Request ? input.method : 'GET')).toUpperCase();
1792
+ const entry = self.begin(method, url, init?.body, 'fetch');
1793
+ const t0 = performance.now();
1794
+ return orig(input, init).then((res) => {
1795
+ const durationMs = Math.round(performance.now() - t0);
1796
+ res
1797
+ .clone()
1798
+ .text()
1799
+ .then((text) => self.finish(entry.id, {
1800
+ status: res.status,
1801
+ ok: res.ok,
1802
+ responseBody: self.tryParse(text),
1803
+ error: res.ok ? null : `HTTP ${res.status}`,
1804
+ durationMs,
1805
+ }))
1806
+ .catch(() => self.finish(entry.id, { status: res.status, ok: res.ok, durationMs }));
1807
+ return res;
1808
+ }, (err) => {
1809
+ self.finish(entry.id, { error: String(err?.message ?? err), ok: false, durationMs: Math.round(performance.now() - t0) });
1810
+ throw err;
1811
+ });
1812
+ };
1813
+ }
1814
+ patchXhr() {
1815
+ if (typeof XMLHttpRequest === 'undefined')
1816
+ return;
1817
+ const proto = XMLHttpRequest.prototype;
1818
+ const origOpen = proto.open;
1819
+ const origSend = proto.send;
1820
+ const self = this;
1821
+ proto.open = function (method, url, ...rest) {
1822
+ this.__dtMeta = { method: String(method).toUpperCase(), url: String(url) };
1823
+ return origOpen.call(this, method, url, ...rest);
1824
+ };
1825
+ proto.send = function (body) {
1826
+ const meta = this.__dtMeta;
1827
+ if (meta && self.matches(meta.url)) {
1828
+ const entry = self.begin(meta.method, meta.url, body, 'xhr');
1829
+ const t0 = performance.now();
1830
+ this.addEventListener('loadend', () => {
1831
+ const durationMs = Math.round(performance.now() - t0);
1832
+ const status = this.status;
1833
+ if (status === 0) {
1834
+ self.finish(entry.id, { error: 'Network error / aborted', ok: false, durationMs });
1835
+ return;
1836
+ }
1837
+ const ok = status >= 200 && status < 300;
1838
+ let responseBody = null;
1839
+ try {
1840
+ const type = this.responseType;
1841
+ if (type === '' || type === 'text')
1842
+ responseBody = self.tryParse(this.responseText);
1843
+ else if (type === 'json')
1844
+ responseBody = this.response;
1845
+ else if (type === 'arraybuffer')
1846
+ responseBody = self.decodeArrayBuffer(this.response);
1847
+ else if (type === 'blob')
1848
+ responseBody = `[blob ${this.response?.size ?? '?'}b]`;
1849
+ else
1850
+ responseBody = `[${type}]`;
1851
+ }
1852
+ catch {
1853
+ /* ignore */
1854
+ }
1855
+ self.finish(entry.id, { status, ok, responseBody, error: ok ? null : `HTTP ${status}`, durationMs });
1856
+ });
1857
+ }
1858
+ return origSend.call(this, body);
1859
+ };
1860
+ }
1861
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsTelemetryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1862
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsTelemetryService }); }
1863
+ }
1864
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsTelemetryService, decorators: [{
1865
+ type: Injectable
1866
+ }] });
1867
+
1868
+ /** Ring-buffer cap so a noisy session never grows unbounded. */
1869
+ const MAX_ENTRIES = 300;
1870
+ /**
1871
+ * In-memory log of runtime JS/TS errors: uncaught exceptions (`window.error`),
1872
+ * unhandled promise rejections, and anything sent to `console.error` (which is
1873
+ * where Angular's default ErrorHandler reports zone-caught errors). Captures
1874
+ * the host app's errors and the package's alike. Nothing is persisted — a page
1875
+ * reload clears it.
1876
+ */
1877
+ class DevtoolsErrorsService {
1878
+ constructor() {
1879
+ this._entries = signal([], ...(ngDevMode ? [{ debugName: "_entries" }] : []));
1880
+ this.entries = this._entries.asReadonly();
1881
+ this.installed = false;
1882
+ this.seq = 0;
1883
+ }
1884
+ clear() {
1885
+ this._entries.set([]);
1886
+ }
1887
+ /** Attach the listeners + patch console.error once. Safe to call repeatedly. */
1888
+ install() {
1889
+ if (this.installed || typeof window === 'undefined')
1890
+ return;
1891
+ this.installed = true;
1892
+ window.addEventListener('error', (e) => {
1893
+ // Skip resource-load errors (img/script/link) — those carry no error/message.
1894
+ if (!e.error && !e.message)
1895
+ return;
1896
+ this.push('error', e.message || String(e.error), e.error?.stack ?? null, this.loc(e.filename, e.lineno, e.colno));
1897
+ });
1898
+ window.addEventListener('unhandledrejection', (e) => {
1899
+ const reason = e.reason;
1900
+ const message = reason instanceof Error ? reason.message : this.stringify(reason);
1901
+ this.push('rejection', message, reason?.stack ?? null, null);
1902
+ });
1903
+ const orig = console.error.bind(console);
1904
+ const self = this;
1905
+ console.error = function (...args) {
1906
+ orig(...args);
1907
+ try {
1908
+ self.captureConsole(args);
1909
+ }
1910
+ catch {
1911
+ /* never let telemetry break logging */
1912
+ }
1913
+ };
1914
+ }
1915
+ // ---- helpers ----------------------------------------------------------
1916
+ captureConsole(args) {
1917
+ const errArg = args.find((a) => a instanceof Error);
1918
+ const message = args
1919
+ .map((a) => (a instanceof Error ? a.message : typeof a === 'string' ? a : this.stringify(a)))
1920
+ .join(' ')
1921
+ .trim();
1922
+ if (!message)
1923
+ return;
1924
+ this.push('console', message, errArg?.stack ?? null, null);
1925
+ }
1926
+ loc(file, line, col) {
1927
+ if (!file)
1928
+ return null;
1929
+ return `${file}${line != null ? ':' + line : ''}${col != null ? ':' + col : ''}`;
1930
+ }
1931
+ stringify(v) {
1932
+ if (v == null)
1933
+ return String(v);
1934
+ if (typeof v === 'string')
1935
+ return v;
1936
+ try {
1937
+ return JSON.stringify(v);
1938
+ }
1939
+ catch {
1940
+ return String(v);
1941
+ }
1942
+ }
1943
+ push(kind, message, stack, source) {
1944
+ // Collapse an immediate duplicate (e.g. Angular ErrorHandler -> console.error
1945
+ // right after a window 'error' for the same throw).
1946
+ const last = this._entries()[0];
1947
+ if (last && last.message === message && last.stack === stack)
1948
+ return;
1949
+ const entry = { id: ++this.seq, at: Date.now(), kind, message, stack, source };
1950
+ this._entries.update((list) => [entry, ...list].slice(0, MAX_ENTRIES));
1951
+ }
1952
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsErrorsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1953
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsErrorsService }); }
1954
+ }
1955
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsErrorsService, decorators: [{
1956
+ type: Injectable
1957
+ }] });
1958
+
1648
1959
  /**
1649
1960
  * Shared inline-style objects for the DevTools domain panels. Bound via
1650
1961
  * `[style]="UI.x"` so there are no view-encapsulation surprises when the panels
@@ -3038,6 +3349,308 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
3038
3349
  }]
3039
3350
  }] });
3040
3351
 
3352
+ /**
3353
+ * Live, in-memory log of every API call (host app + package). Click a row to
3354
+ * inspect params/body/response/error. Cleared by a page reload or the button.
3355
+ */
3356
+ class TelemetryPanelComponent {
3357
+ constructor() {
3358
+ this.UI = UI;
3359
+ this.telemetry = inject(DevtoolsTelemetryService);
3360
+ this.entries = this.telemetry.entries;
3361
+ this.count = computed(() => this.entries().length, ...(ngDevMode ? [{ debugName: "count" }] : []));
3362
+ this.expanded = signal(null, ...(ngDevMode ? [{ debugName: "expanded" }] : []));
3363
+ this.badge = {
3364
+ fontFamily: "'JetBrains Mono',monospace",
3365
+ fontSize: '9.5px',
3366
+ fontWeight: '700',
3367
+ letterSpacing: '.03em',
3368
+ padding: '2px 6px',
3369
+ borderRadius: '6px',
3370
+ background: '#23252f',
3371
+ color: '#c4c7d0',
3372
+ flex: 'none',
3373
+ };
3374
+ }
3375
+ toggle(id) {
3376
+ this.expanded.update((cur) => (cur === id ? null : id));
3377
+ }
3378
+ clear() {
3379
+ this.telemetry.clear();
3380
+ this.expanded.set(null);
3381
+ }
3382
+ path(url) {
3383
+ try {
3384
+ const u = new URL(url, window.location.href);
3385
+ return u.pathname + u.search;
3386
+ }
3387
+ catch {
3388
+ return url;
3389
+ }
3390
+ }
3391
+ hasParams(e) {
3392
+ return Object.keys(e.params).length > 0;
3393
+ }
3394
+ json(v) {
3395
+ try {
3396
+ return typeof v === 'string' ? v : JSON.stringify(v, null, 2);
3397
+ }
3398
+ catch {
3399
+ return String(v);
3400
+ }
3401
+ }
3402
+ statusColor(e) {
3403
+ if (e.pending)
3404
+ return '#ffc454';
3405
+ if (e.error || (e.status ?? 0) >= 400)
3406
+ return '#ff6b6b';
3407
+ return '#1fbe7e';
3408
+ }
3409
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TelemetryPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3410
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: TelemetryPanelComponent, isStandalone: true, selector: "devtools-telemetry-panel", ngImport: i0, template: `
3411
+ <p [style]="UI.intro" style="margin-bottom:10px">
3412
+ Todas as chamadas à API ({{ count() }}). Em memória — o reload limpa.
3413
+ </p>
3414
+ <button type="button" [style]="UI.ghost" style="margin-top:0;margin-bottom:14px" (click)="clear()">
3415
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:17px;vertical-align:-3px">delete_sweep</span>
3416
+ Limpar log
3417
+ </button>
3418
+
3419
+ @if (!count()) {
3420
+ <p [style]="UI.mono">Nenhuma requisição capturada ainda.</p>
3421
+ }
3422
+
3423
+ @for (e of entries(); track e.id) {
3424
+ <div [style]="UI.card" style="margin-bottom:8px;cursor:pointer;padding:11px 13px" (click)="toggle(e.id)">
3425
+ <div style="display:flex;align-items:center;gap:8px">
3426
+ <span [style]="badge">{{ e.method }}</span>
3427
+ <span style="font-size:11px;font-weight:700;min-width:32px" [style.color]="statusColor(e)">
3428
+ {{ e.pending ? '···' : (e.status ?? 'ERR') }}
3429
+ </span>
3430
+ <span [style]="UI.mono" style="flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
3431
+ {{ path(e.url) }}
3432
+ </span>
3433
+ @if (e.durationMs != null) {
3434
+ <span [style]="UI.mono" style="white-space:nowrap;color:#6a6e7b">{{ e.durationMs }}ms</span>
3435
+ }
3436
+ </div>
3437
+
3438
+ @if (expanded() === e.id) {
3439
+ <div style="margin-top:11px;display:flex;flex-direction:column;gap:7px" (click)="$event.stopPropagation()">
3440
+ <div [style]="UI.label">URL</div>
3441
+ <pre [style]="UI.pre">{{ e.url }}</pre>
3442
+
3443
+ @if (hasParams(e)) {
3444
+ <div [style]="UI.label">Query params</div>
3445
+ <pre [style]="UI.pre">{{ json(e.params) }}</pre>
3446
+ }
3447
+ @if (e.requestBody !== null) {
3448
+ <div [style]="UI.label">Request body</div>
3449
+ <pre [style]="UI.pre">{{ json(e.requestBody) }}</pre>
3450
+ }
3451
+ @if (e.error) {
3452
+ <div [style]="UI.label" style="color:#ff7676">Erro</div>
3453
+ <pre [style]="UI.pre" style="color:#ff9b9b;border-color:#5a2a2a">{{ e.error }}</pre>
3454
+ }
3455
+ @if (e.responseBody !== null) {
3456
+ <div [style]="UI.label">Response</div>
3457
+ <pre [style]="UI.pre">{{ json(e.responseBody) }}</pre>
3458
+ }
3459
+ </div>
3460
+ }
3461
+ </div>
3462
+ }
3463
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3464
+ }
3465
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TelemetryPanelComponent, decorators: [{
3466
+ type: Component,
3467
+ args: [{
3468
+ selector: 'devtools-telemetry-panel',
3469
+ standalone: true,
3470
+ changeDetection: ChangeDetectionStrategy.OnPush,
3471
+ template: `
3472
+ <p [style]="UI.intro" style="margin-bottom:10px">
3473
+ Todas as chamadas à API ({{ count() }}). Em memória — o reload limpa.
3474
+ </p>
3475
+ <button type="button" [style]="UI.ghost" style="margin-top:0;margin-bottom:14px" (click)="clear()">
3476
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:17px;vertical-align:-3px">delete_sweep</span>
3477
+ Limpar log
3478
+ </button>
3479
+
3480
+ @if (!count()) {
3481
+ <p [style]="UI.mono">Nenhuma requisição capturada ainda.</p>
3482
+ }
3483
+
3484
+ @for (e of entries(); track e.id) {
3485
+ <div [style]="UI.card" style="margin-bottom:8px;cursor:pointer;padding:11px 13px" (click)="toggle(e.id)">
3486
+ <div style="display:flex;align-items:center;gap:8px">
3487
+ <span [style]="badge">{{ e.method }}</span>
3488
+ <span style="font-size:11px;font-weight:700;min-width:32px" [style.color]="statusColor(e)">
3489
+ {{ e.pending ? '···' : (e.status ?? 'ERR') }}
3490
+ </span>
3491
+ <span [style]="UI.mono" style="flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
3492
+ {{ path(e.url) }}
3493
+ </span>
3494
+ @if (e.durationMs != null) {
3495
+ <span [style]="UI.mono" style="white-space:nowrap;color:#6a6e7b">{{ e.durationMs }}ms</span>
3496
+ }
3497
+ </div>
3498
+
3499
+ @if (expanded() === e.id) {
3500
+ <div style="margin-top:11px;display:flex;flex-direction:column;gap:7px" (click)="$event.stopPropagation()">
3501
+ <div [style]="UI.label">URL</div>
3502
+ <pre [style]="UI.pre">{{ e.url }}</pre>
3503
+
3504
+ @if (hasParams(e)) {
3505
+ <div [style]="UI.label">Query params</div>
3506
+ <pre [style]="UI.pre">{{ json(e.params) }}</pre>
3507
+ }
3508
+ @if (e.requestBody !== null) {
3509
+ <div [style]="UI.label">Request body</div>
3510
+ <pre [style]="UI.pre">{{ json(e.requestBody) }}</pre>
3511
+ }
3512
+ @if (e.error) {
3513
+ <div [style]="UI.label" style="color:#ff7676">Erro</div>
3514
+ <pre [style]="UI.pre" style="color:#ff9b9b;border-color:#5a2a2a">{{ e.error }}</pre>
3515
+ }
3516
+ @if (e.responseBody !== null) {
3517
+ <div [style]="UI.label">Response</div>
3518
+ <pre [style]="UI.pre">{{ json(e.responseBody) }}</pre>
3519
+ }
3520
+ </div>
3521
+ }
3522
+ </div>
3523
+ }
3524
+ `,
3525
+ }]
3526
+ }] });
3527
+
3528
+ /**
3529
+ * Live, in-memory log of runtime JS/TS errors (uncaught, unhandled rejections,
3530
+ * console.error). Click a row to see the full stack. Cleared by a reload.
3531
+ */
3532
+ class ErrorsPanelComponent {
3533
+ constructor() {
3534
+ this.UI = UI;
3535
+ this.errors = inject(DevtoolsErrorsService);
3536
+ this.entries = this.errors.entries;
3537
+ this.count = computed(() => this.entries().length, ...(ngDevMode ? [{ debugName: "count" }] : []));
3538
+ this.expanded = signal(null, ...(ngDevMode ? [{ debugName: "expanded" }] : []));
3539
+ this.badge = {
3540
+ fontFamily: "'JetBrains Mono',monospace",
3541
+ fontSize: '9.5px',
3542
+ fontWeight: '700',
3543
+ letterSpacing: '.03em',
3544
+ padding: '2px 6px',
3545
+ borderRadius: '6px',
3546
+ background: '#2a2024',
3547
+ flex: 'none',
3548
+ };
3549
+ }
3550
+ toggle(id) {
3551
+ this.expanded.update((cur) => (cur === id ? null : id));
3552
+ }
3553
+ clear() {
3554
+ this.errors.clear();
3555
+ this.expanded.set(null);
3556
+ }
3557
+ kindLabel(kind) {
3558
+ return kind === 'rejection' ? 'PROMISE' : kind === 'console' ? 'CONSOLE' : 'ERROR';
3559
+ }
3560
+ kindColor(kind) {
3561
+ return kind === 'console' ? '#ffb454' : '#ff6b6b';
3562
+ }
3563
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ErrorsPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3564
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ErrorsPanelComponent, isStandalone: true, selector: "devtools-errors-panel", ngImport: i0, template: `
3565
+ <p [style]="UI.intro" style="margin-bottom:10px">
3566
+ Erros de runtime JS/TS ({{ count() }}). Em memória — o reload limpa.
3567
+ </p>
3568
+ <button type="button" [style]="UI.ghost" style="margin-top:0;margin-bottom:14px" (click)="clear()">
3569
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:17px;vertical-align:-3px">delete_sweep</span>
3570
+ Limpar log
3571
+ </button>
3572
+
3573
+ @if (!count()) {
3574
+ <p [style]="UI.mono">Nenhum erro capturado. 🎉</p>
3575
+ }
3576
+
3577
+ @for (e of entries(); track e.id) {
3578
+ <div [style]="UI.card" style="margin-bottom:8px;cursor:pointer;padding:11px 13px;border-color:#3a2a2a" (click)="toggle(e.id)">
3579
+ <div style="display:flex;align-items:center;gap:8px">
3580
+ <span [style]="badge" [style.color]="kindColor(e.kind)">{{ kindLabel(e.kind) }}</span>
3581
+ <span style="flex:1;min-width:0;font-size:12.5px;color:#f0d6d6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
3582
+ {{ e.message }}
3583
+ </span>
3584
+ </div>
3585
+
3586
+ @if (expanded() === e.id) {
3587
+ <div style="margin-top:11px;display:flex;flex-direction:column;gap:7px" (click)="$event.stopPropagation()">
3588
+ <div [style]="UI.label">Mensagem</div>
3589
+ <pre [style]="UI.pre" style="color:#ff9b9b;border-color:#5a2a2a">{{ e.message }}</pre>
3590
+
3591
+ @if (e.source) {
3592
+ <div [style]="UI.label">Origem</div>
3593
+ <pre [style]="UI.pre">{{ e.source }}</pre>
3594
+ }
3595
+ @if (e.stack) {
3596
+ <div [style]="UI.label">Stack</div>
3597
+ <pre [style]="UI.pre">{{ e.stack }}</pre>
3598
+ }
3599
+ </div>
3600
+ }
3601
+ </div>
3602
+ }
3603
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3604
+ }
3605
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ErrorsPanelComponent, decorators: [{
3606
+ type: Component,
3607
+ args: [{
3608
+ selector: 'devtools-errors-panel',
3609
+ standalone: true,
3610
+ changeDetection: ChangeDetectionStrategy.OnPush,
3611
+ template: `
3612
+ <p [style]="UI.intro" style="margin-bottom:10px">
3613
+ Erros de runtime JS/TS ({{ count() }}). Em memória — o reload limpa.
3614
+ </p>
3615
+ <button type="button" [style]="UI.ghost" style="margin-top:0;margin-bottom:14px" (click)="clear()">
3616
+ <span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:17px;vertical-align:-3px">delete_sweep</span>
3617
+ Limpar log
3618
+ </button>
3619
+
3620
+ @if (!count()) {
3621
+ <p [style]="UI.mono">Nenhum erro capturado. 🎉</p>
3622
+ }
3623
+
3624
+ @for (e of entries(); track e.id) {
3625
+ <div [style]="UI.card" style="margin-bottom:8px;cursor:pointer;padding:11px 13px;border-color:#3a2a2a" (click)="toggle(e.id)">
3626
+ <div style="display:flex;align-items:center;gap:8px">
3627
+ <span [style]="badge" [style.color]="kindColor(e.kind)">{{ kindLabel(e.kind) }}</span>
3628
+ <span style="flex:1;min-width:0;font-size:12.5px;color:#f0d6d6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
3629
+ {{ e.message }}
3630
+ </span>
3631
+ </div>
3632
+
3633
+ @if (expanded() === e.id) {
3634
+ <div style="margin-top:11px;display:flex;flex-direction:column;gap:7px" (click)="$event.stopPropagation()">
3635
+ <div [style]="UI.label">Mensagem</div>
3636
+ <pre [style]="UI.pre" style="color:#ff9b9b;border-color:#5a2a2a">{{ e.message }}</pre>
3637
+
3638
+ @if (e.source) {
3639
+ <div [style]="UI.label">Origem</div>
3640
+ <pre [style]="UI.pre">{{ e.source }}</pre>
3641
+ }
3642
+ @if (e.stack) {
3643
+ <div [style]="UI.label">Stack</div>
3644
+ <pre [style]="UI.pre">{{ e.stack }}</pre>
3645
+ }
3646
+ </div>
3647
+ }
3648
+ </div>
3649
+ }
3650
+ `,
3651
+ }]
3652
+ }] });
3653
+
3041
3654
  const BUILTIN_ACTIONS = [
3042
3655
  { id: 'admin', label: 'Admin', icon: 'shield_person', content: AdminLoginPanelComponent, order: 0 },
3043
3656
  { id: 'app-login', label: 'Login App', icon: 'login', content: LoginPanelComponent, order: 1 },
@@ -3047,6 +3660,8 @@ const BUILTIN_ACTIONS = [
3047
3660
  { id: 'exams', label: 'Exames', icon: 'biotech', content: ExamsPanelComponent, order: 5 },
3048
3661
  { id: 'data', label: 'Dados', icon: 'badge', content: DataPanelComponent, order: 6 },
3049
3662
  { id: 'copy', label: 'Copiar', icon: 'content_copy', content: CopyPanelComponent, order: 7 },
3663
+ { id: 'telemetry', label: 'Telemetria', icon: 'monitoring', content: TelemetryPanelComponent, order: 8 },
3664
+ { id: 'errors', label: 'Erros', icon: 'bug_report', content: ErrorsPanelComponent, order: 9 },
3050
3665
  ];
3051
3666
  /**
3052
3667
  * Mount the self-contained DevTools. Add to `bootstrapApplication` providers or
@@ -3110,11 +3725,18 @@ function mountIsolated(parent, config, actions, shell) {
3110
3725
  DevtoolsApiService,
3111
3726
  DevtoolsAuthService,
3112
3727
  DevtoolsSessionService,
3728
+ DevtoolsTelemetryService,
3729
+ DevtoolsErrorsService,
3113
3730
  DevtoolsPositionService,
3114
3731
  DevtoolsToastService,
3115
3732
  ], parent);
3116
- // Consume our own Google OAuth return (guarded) to finish admin login.
3117
- runInInjectionContext(injector, () => inject(DevtoolsAuthService).handleGoogleReturn());
3733
+ runInInjectionContext(injector, () => {
3734
+ // Start capturing API traffic + runtime errors ASAP, then consume our own
3735
+ // Google OAuth return (guarded) to finish admin login.
3736
+ inject(DevtoolsTelemetryService).install();
3737
+ inject(DevtoolsErrorsService).install();
3738
+ inject(DevtoolsAuthService).handleGoogleReturn();
3739
+ });
3118
3740
  const appRef = parent.get(ApplicationRef);
3119
3741
  const host = document.createElement('div');
3120
3742
  host.setAttribute('data-devtools-shell-host', '');