@marsaude/devtools-shell 0.1.4 → 0.1.6
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.
|
@@ -1603,9 +1603,10 @@ class DevtoolsSessionService {
|
|
|
1603
1603
|
return full;
|
|
1604
1604
|
}
|
|
1605
1605
|
/**
|
|
1606
|
-
* Write a stored session into the host's active-session keys and
|
|
1607
|
-
* host app re-reads it
|
|
1608
|
-
* fresh login and switching
|
|
1606
|
+
* Write a stored session into the host's active-session keys and send the user
|
|
1607
|
+
* to '/' (full navigation) so the host app re-reads it from a clean route — it
|
|
1608
|
+
* only reads these keys at bootstrap. Covers both a fresh login and switching
|
|
1609
|
+
* between users.
|
|
1609
1610
|
*/
|
|
1610
1611
|
switchTo(id) {
|
|
1611
1612
|
const session = id === 'admin' ? this.admin() : this._sessions().find((s) => s.id === id) ?? null;
|
|
@@ -1617,7 +1618,7 @@ class DevtoolsSessionService {
|
|
|
1617
1618
|
this.activeId.set(id);
|
|
1618
1619
|
this.storage.set(ACTIVE_KEY, id);
|
|
1619
1620
|
try {
|
|
1620
|
-
window.location.
|
|
1621
|
+
window.location.href = '/';
|
|
1621
1622
|
}
|
|
1622
1623
|
catch {
|
|
1623
1624
|
/* non-browser env — ignore */
|
|
@@ -1644,6 +1645,282 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
1644
1645
|
type: Injectable
|
|
1645
1646
|
}] });
|
|
1646
1647
|
|
|
1648
|
+
/** Ring-buffer cap so a long QA session never grows unbounded. */
|
|
1649
|
+
const MAX_ENTRIES$1 = 300;
|
|
1650
|
+
/**
|
|
1651
|
+
* In-memory telemetry of every HTTP call to the API host (the one in
|
|
1652
|
+
* `apiBaseUrl`). Patches `fetch` and `XMLHttpRequest` once so it captures BOTH
|
|
1653
|
+
* the host app's requests and the package's own — independent of which
|
|
1654
|
+
* HttpClient/interceptor issued them. Nothing is persisted: a page reload
|
|
1655
|
+
* clears the log.
|
|
1656
|
+
*/
|
|
1657
|
+
class DevtoolsTelemetryService {
|
|
1658
|
+
constructor() {
|
|
1659
|
+
this.config = inject(DEVTOOLS_CONFIG);
|
|
1660
|
+
this._entries = signal([], ...(ngDevMode ? [{ debugName: "_entries" }] : []));
|
|
1661
|
+
this.entries = this._entries.asReadonly();
|
|
1662
|
+
this.installed = false;
|
|
1663
|
+
this.seq = 0;
|
|
1664
|
+
this.origin = this.deriveOrigin();
|
|
1665
|
+
}
|
|
1666
|
+
clear() {
|
|
1667
|
+
this._entries.set([]);
|
|
1668
|
+
}
|
|
1669
|
+
/** Monkeypatch fetch + XHR once. Safe to call multiple times. */
|
|
1670
|
+
install() {
|
|
1671
|
+
if (this.installed || typeof window === 'undefined')
|
|
1672
|
+
return;
|
|
1673
|
+
this.installed = true;
|
|
1674
|
+
this.patchFetch();
|
|
1675
|
+
this.patchXhr();
|
|
1676
|
+
}
|
|
1677
|
+
// ---- capture helpers --------------------------------------------------
|
|
1678
|
+
deriveOrigin() {
|
|
1679
|
+
try {
|
|
1680
|
+
return new URL(this.config.apiBaseUrl).origin;
|
|
1681
|
+
}
|
|
1682
|
+
catch {
|
|
1683
|
+
return this.config.apiBaseUrl;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
matches(url) {
|
|
1687
|
+
try {
|
|
1688
|
+
return new URL(url, window.location.href).origin === this.origin;
|
|
1689
|
+
}
|
|
1690
|
+
catch {
|
|
1691
|
+
return url.includes(this.origin);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
parseParams(url) {
|
|
1695
|
+
try {
|
|
1696
|
+
const out = {};
|
|
1697
|
+
new URL(url, window.location.href).searchParams.forEach((v, k) => (out[k] = v));
|
|
1698
|
+
return out;
|
|
1699
|
+
}
|
|
1700
|
+
catch {
|
|
1701
|
+
return {};
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
tryParse(text) {
|
|
1705
|
+
if (text == null || text === '')
|
|
1706
|
+
return null;
|
|
1707
|
+
try {
|
|
1708
|
+
return JSON.parse(text);
|
|
1709
|
+
}
|
|
1710
|
+
catch {
|
|
1711
|
+
return text;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
normalizeBody(body) {
|
|
1715
|
+
if (body == null)
|
|
1716
|
+
return null;
|
|
1717
|
+
if (typeof body === 'string')
|
|
1718
|
+
return this.tryParse(body);
|
|
1719
|
+
if (typeof FormData !== 'undefined' && body instanceof FormData) {
|
|
1720
|
+
const out = {};
|
|
1721
|
+
body.forEach((v, k) => (out[k] = v instanceof File ? `[File ${v.name}]` : v));
|
|
1722
|
+
return out;
|
|
1723
|
+
}
|
|
1724
|
+
if (typeof Blob !== 'undefined' && body instanceof Blob)
|
|
1725
|
+
return `[Blob ${body.size}b]`;
|
|
1726
|
+
return body;
|
|
1727
|
+
}
|
|
1728
|
+
begin(method, url, body, source) {
|
|
1729
|
+
const entry = {
|
|
1730
|
+
id: ++this.seq,
|
|
1731
|
+
startedAt: Date.now(),
|
|
1732
|
+
method,
|
|
1733
|
+
url,
|
|
1734
|
+
params: this.parseParams(url),
|
|
1735
|
+
requestBody: this.normalizeBody(body),
|
|
1736
|
+
status: null,
|
|
1737
|
+
ok: null,
|
|
1738
|
+
responseBody: null,
|
|
1739
|
+
error: null,
|
|
1740
|
+
durationMs: null,
|
|
1741
|
+
pending: true,
|
|
1742
|
+
source,
|
|
1743
|
+
};
|
|
1744
|
+
this._entries.update((list) => [entry, ...list].slice(0, MAX_ENTRIES$1));
|
|
1745
|
+
return entry;
|
|
1746
|
+
}
|
|
1747
|
+
finish(id, patch) {
|
|
1748
|
+
this._entries.update((list) => list.map((e) => (e.id === id ? { ...e, ...patch, pending: false } : e)));
|
|
1749
|
+
}
|
|
1750
|
+
// ---- patches ----------------------------------------------------------
|
|
1751
|
+
patchFetch() {
|
|
1752
|
+
if (typeof window.fetch !== 'function')
|
|
1753
|
+
return;
|
|
1754
|
+
const orig = window.fetch.bind(window);
|
|
1755
|
+
const self = this;
|
|
1756
|
+
window.fetch = function (input, init) {
|
|
1757
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
1758
|
+
if (!self.matches(url))
|
|
1759
|
+
return orig(input, init);
|
|
1760
|
+
const method = (init?.method || (input instanceof Request ? input.method : 'GET')).toUpperCase();
|
|
1761
|
+
const entry = self.begin(method, url, init?.body, 'fetch');
|
|
1762
|
+
const t0 = performance.now();
|
|
1763
|
+
return orig(input, init).then((res) => {
|
|
1764
|
+
const durationMs = Math.round(performance.now() - t0);
|
|
1765
|
+
res
|
|
1766
|
+
.clone()
|
|
1767
|
+
.text()
|
|
1768
|
+
.then((text) => self.finish(entry.id, {
|
|
1769
|
+
status: res.status,
|
|
1770
|
+
ok: res.ok,
|
|
1771
|
+
responseBody: self.tryParse(text),
|
|
1772
|
+
error: res.ok ? null : `HTTP ${res.status}`,
|
|
1773
|
+
durationMs,
|
|
1774
|
+
}))
|
|
1775
|
+
.catch(() => self.finish(entry.id, { status: res.status, ok: res.ok, durationMs }));
|
|
1776
|
+
return res;
|
|
1777
|
+
}, (err) => {
|
|
1778
|
+
self.finish(entry.id, { error: String(err?.message ?? err), ok: false, durationMs: Math.round(performance.now() - t0) });
|
|
1779
|
+
throw err;
|
|
1780
|
+
});
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
patchXhr() {
|
|
1784
|
+
if (typeof XMLHttpRequest === 'undefined')
|
|
1785
|
+
return;
|
|
1786
|
+
const proto = XMLHttpRequest.prototype;
|
|
1787
|
+
const origOpen = proto.open;
|
|
1788
|
+
const origSend = proto.send;
|
|
1789
|
+
const self = this;
|
|
1790
|
+
proto.open = function (method, url, ...rest) {
|
|
1791
|
+
this.__dtMeta = { method: String(method).toUpperCase(), url: String(url) };
|
|
1792
|
+
return origOpen.call(this, method, url, ...rest);
|
|
1793
|
+
};
|
|
1794
|
+
proto.send = function (body) {
|
|
1795
|
+
const meta = this.__dtMeta;
|
|
1796
|
+
if (meta && self.matches(meta.url)) {
|
|
1797
|
+
const entry = self.begin(meta.method, meta.url, body, 'xhr');
|
|
1798
|
+
const t0 = performance.now();
|
|
1799
|
+
this.addEventListener('loadend', () => {
|
|
1800
|
+
const durationMs = Math.round(performance.now() - t0);
|
|
1801
|
+
const status = this.status;
|
|
1802
|
+
if (status === 0) {
|
|
1803
|
+
self.finish(entry.id, { error: 'Network error / aborted', ok: false, durationMs });
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
const ok = status >= 200 && status < 300;
|
|
1807
|
+
let responseBody = null;
|
|
1808
|
+
try {
|
|
1809
|
+
const type = this.responseType;
|
|
1810
|
+
if (type === '' || type === 'text')
|
|
1811
|
+
responseBody = self.tryParse(this.responseText);
|
|
1812
|
+
else if (type === 'json')
|
|
1813
|
+
responseBody = this.response;
|
|
1814
|
+
else
|
|
1815
|
+
responseBody = `[${type}]`;
|
|
1816
|
+
}
|
|
1817
|
+
catch {
|
|
1818
|
+
/* ignore */
|
|
1819
|
+
}
|
|
1820
|
+
self.finish(entry.id, { status, ok, responseBody, error: ok ? null : `HTTP ${status}`, durationMs });
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
return origSend.call(this, body);
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsTelemetryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1827
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsTelemetryService }); }
|
|
1828
|
+
}
|
|
1829
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsTelemetryService, decorators: [{
|
|
1830
|
+
type: Injectable
|
|
1831
|
+
}] });
|
|
1832
|
+
|
|
1833
|
+
/** Ring-buffer cap so a noisy session never grows unbounded. */
|
|
1834
|
+
const MAX_ENTRIES = 300;
|
|
1835
|
+
/**
|
|
1836
|
+
* In-memory log of runtime JS/TS errors: uncaught exceptions (`window.error`),
|
|
1837
|
+
* unhandled promise rejections, and anything sent to `console.error` (which is
|
|
1838
|
+
* where Angular's default ErrorHandler reports zone-caught errors). Captures
|
|
1839
|
+
* the host app's errors and the package's alike. Nothing is persisted — a page
|
|
1840
|
+
* reload clears it.
|
|
1841
|
+
*/
|
|
1842
|
+
class DevtoolsErrorsService {
|
|
1843
|
+
constructor() {
|
|
1844
|
+
this._entries = signal([], ...(ngDevMode ? [{ debugName: "_entries" }] : []));
|
|
1845
|
+
this.entries = this._entries.asReadonly();
|
|
1846
|
+
this.installed = false;
|
|
1847
|
+
this.seq = 0;
|
|
1848
|
+
}
|
|
1849
|
+
clear() {
|
|
1850
|
+
this._entries.set([]);
|
|
1851
|
+
}
|
|
1852
|
+
/** Attach the listeners + patch console.error once. Safe to call repeatedly. */
|
|
1853
|
+
install() {
|
|
1854
|
+
if (this.installed || typeof window === 'undefined')
|
|
1855
|
+
return;
|
|
1856
|
+
this.installed = true;
|
|
1857
|
+
window.addEventListener('error', (e) => {
|
|
1858
|
+
// Skip resource-load errors (img/script/link) — those carry no error/message.
|
|
1859
|
+
if (!e.error && !e.message)
|
|
1860
|
+
return;
|
|
1861
|
+
this.push('error', e.message || String(e.error), e.error?.stack ?? null, this.loc(e.filename, e.lineno, e.colno));
|
|
1862
|
+
});
|
|
1863
|
+
window.addEventListener('unhandledrejection', (e) => {
|
|
1864
|
+
const reason = e.reason;
|
|
1865
|
+
const message = reason instanceof Error ? reason.message : this.stringify(reason);
|
|
1866
|
+
this.push('rejection', message, reason?.stack ?? null, null);
|
|
1867
|
+
});
|
|
1868
|
+
const orig = console.error.bind(console);
|
|
1869
|
+
const self = this;
|
|
1870
|
+
console.error = function (...args) {
|
|
1871
|
+
orig(...args);
|
|
1872
|
+
try {
|
|
1873
|
+
self.captureConsole(args);
|
|
1874
|
+
}
|
|
1875
|
+
catch {
|
|
1876
|
+
/* never let telemetry break logging */
|
|
1877
|
+
}
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
// ---- helpers ----------------------------------------------------------
|
|
1881
|
+
captureConsole(args) {
|
|
1882
|
+
const errArg = args.find((a) => a instanceof Error);
|
|
1883
|
+
const message = args
|
|
1884
|
+
.map((a) => (a instanceof Error ? a.message : typeof a === 'string' ? a : this.stringify(a)))
|
|
1885
|
+
.join(' ')
|
|
1886
|
+
.trim();
|
|
1887
|
+
if (!message)
|
|
1888
|
+
return;
|
|
1889
|
+
this.push('console', message, errArg?.stack ?? null, null);
|
|
1890
|
+
}
|
|
1891
|
+
loc(file, line, col) {
|
|
1892
|
+
if (!file)
|
|
1893
|
+
return null;
|
|
1894
|
+
return `${file}${line != null ? ':' + line : ''}${col != null ? ':' + col : ''}`;
|
|
1895
|
+
}
|
|
1896
|
+
stringify(v) {
|
|
1897
|
+
if (v == null)
|
|
1898
|
+
return String(v);
|
|
1899
|
+
if (typeof v === 'string')
|
|
1900
|
+
return v;
|
|
1901
|
+
try {
|
|
1902
|
+
return JSON.stringify(v);
|
|
1903
|
+
}
|
|
1904
|
+
catch {
|
|
1905
|
+
return String(v);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
push(kind, message, stack, source) {
|
|
1909
|
+
// Collapse an immediate duplicate (e.g. Angular ErrorHandler -> console.error
|
|
1910
|
+
// right after a window 'error' for the same throw).
|
|
1911
|
+
const last = this._entries()[0];
|
|
1912
|
+
if (last && last.message === message && last.stack === stack)
|
|
1913
|
+
return;
|
|
1914
|
+
const entry = { id: ++this.seq, at: Date.now(), kind, message, stack, source };
|
|
1915
|
+
this._entries.update((list) => [entry, ...list].slice(0, MAX_ENTRIES));
|
|
1916
|
+
}
|
|
1917
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsErrorsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1918
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsErrorsService }); }
|
|
1919
|
+
}
|
|
1920
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DevtoolsErrorsService, decorators: [{
|
|
1921
|
+
type: Injectable
|
|
1922
|
+
}] });
|
|
1923
|
+
|
|
1647
1924
|
/**
|
|
1648
1925
|
* Shared inline-style objects for the DevTools domain panels. Bound via
|
|
1649
1926
|
* `[style]="UI.x"` so there are no view-encapsulation surprises when the panels
|
|
@@ -3037,6 +3314,308 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
3037
3314
|
}]
|
|
3038
3315
|
}] });
|
|
3039
3316
|
|
|
3317
|
+
/**
|
|
3318
|
+
* Live, in-memory log of every API call (host app + package). Click a row to
|
|
3319
|
+
* inspect params/body/response/error. Cleared by a page reload or the button.
|
|
3320
|
+
*/
|
|
3321
|
+
class TelemetryPanelComponent {
|
|
3322
|
+
constructor() {
|
|
3323
|
+
this.UI = UI;
|
|
3324
|
+
this.telemetry = inject(DevtoolsTelemetryService);
|
|
3325
|
+
this.entries = this.telemetry.entries;
|
|
3326
|
+
this.count = computed(() => this.entries().length, ...(ngDevMode ? [{ debugName: "count" }] : []));
|
|
3327
|
+
this.expanded = signal(null, ...(ngDevMode ? [{ debugName: "expanded" }] : []));
|
|
3328
|
+
this.badge = {
|
|
3329
|
+
fontFamily: "'JetBrains Mono',monospace",
|
|
3330
|
+
fontSize: '9.5px',
|
|
3331
|
+
fontWeight: '700',
|
|
3332
|
+
letterSpacing: '.03em',
|
|
3333
|
+
padding: '2px 6px',
|
|
3334
|
+
borderRadius: '6px',
|
|
3335
|
+
background: '#23252f',
|
|
3336
|
+
color: '#c4c7d0',
|
|
3337
|
+
flex: 'none',
|
|
3338
|
+
};
|
|
3339
|
+
}
|
|
3340
|
+
toggle(id) {
|
|
3341
|
+
this.expanded.update((cur) => (cur === id ? null : id));
|
|
3342
|
+
}
|
|
3343
|
+
clear() {
|
|
3344
|
+
this.telemetry.clear();
|
|
3345
|
+
this.expanded.set(null);
|
|
3346
|
+
}
|
|
3347
|
+
path(url) {
|
|
3348
|
+
try {
|
|
3349
|
+
const u = new URL(url, window.location.href);
|
|
3350
|
+
return u.pathname + u.search;
|
|
3351
|
+
}
|
|
3352
|
+
catch {
|
|
3353
|
+
return url;
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
hasParams(e) {
|
|
3357
|
+
return Object.keys(e.params).length > 0;
|
|
3358
|
+
}
|
|
3359
|
+
json(v) {
|
|
3360
|
+
try {
|
|
3361
|
+
return typeof v === 'string' ? v : JSON.stringify(v, null, 2);
|
|
3362
|
+
}
|
|
3363
|
+
catch {
|
|
3364
|
+
return String(v);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
statusColor(e) {
|
|
3368
|
+
if (e.pending)
|
|
3369
|
+
return '#ffc454';
|
|
3370
|
+
if (e.error || (e.status ?? 0) >= 400)
|
|
3371
|
+
return '#ff6b6b';
|
|
3372
|
+
return '#1fbe7e';
|
|
3373
|
+
}
|
|
3374
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TelemetryPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3375
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: TelemetryPanelComponent, isStandalone: true, selector: "devtools-telemetry-panel", ngImport: i0, template: `
|
|
3376
|
+
<p [style]="UI.intro" style="margin-bottom:10px">
|
|
3377
|
+
Todas as chamadas à API ({{ count() }}). Em memória — o reload limpa.
|
|
3378
|
+
</p>
|
|
3379
|
+
<button type="button" [style]="UI.ghost" style="margin-top:0;margin-bottom:14px" (click)="clear()">
|
|
3380
|
+
<span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:17px;vertical-align:-3px">delete_sweep</span>
|
|
3381
|
+
Limpar log
|
|
3382
|
+
</button>
|
|
3383
|
+
|
|
3384
|
+
@if (!count()) {
|
|
3385
|
+
<p [style]="UI.mono">Nenhuma requisição capturada ainda.</p>
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
@for (e of entries(); track e.id) {
|
|
3389
|
+
<div [style]="UI.card" style="margin-bottom:8px;cursor:pointer;padding:11px 13px" (click)="toggle(e.id)">
|
|
3390
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
3391
|
+
<span [style]="badge">{{ e.method }}</span>
|
|
3392
|
+
<span style="font-size:11px;font-weight:700;min-width:32px" [style.color]="statusColor(e)">
|
|
3393
|
+
{{ e.pending ? '···' : (e.status ?? 'ERR') }}
|
|
3394
|
+
</span>
|
|
3395
|
+
<span [style]="UI.mono" style="flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
|
|
3396
|
+
{{ path(e.url) }}
|
|
3397
|
+
</span>
|
|
3398
|
+
@if (e.durationMs != null) {
|
|
3399
|
+
<span [style]="UI.mono" style="white-space:nowrap;color:#6a6e7b">{{ e.durationMs }}ms</span>
|
|
3400
|
+
}
|
|
3401
|
+
</div>
|
|
3402
|
+
|
|
3403
|
+
@if (expanded() === e.id) {
|
|
3404
|
+
<div style="margin-top:11px;display:flex;flex-direction:column;gap:7px" (click)="$event.stopPropagation()">
|
|
3405
|
+
<div [style]="UI.label">URL</div>
|
|
3406
|
+
<pre [style]="UI.pre">{{ e.url }}</pre>
|
|
3407
|
+
|
|
3408
|
+
@if (hasParams(e)) {
|
|
3409
|
+
<div [style]="UI.label">Query params</div>
|
|
3410
|
+
<pre [style]="UI.pre">{{ json(e.params) }}</pre>
|
|
3411
|
+
}
|
|
3412
|
+
@if (e.requestBody !== null) {
|
|
3413
|
+
<div [style]="UI.label">Request body</div>
|
|
3414
|
+
<pre [style]="UI.pre">{{ json(e.requestBody) }}</pre>
|
|
3415
|
+
}
|
|
3416
|
+
@if (e.error) {
|
|
3417
|
+
<div [style]="UI.label" style="color:#ff7676">Erro</div>
|
|
3418
|
+
<pre [style]="UI.pre" style="color:#ff9b9b;border-color:#5a2a2a">{{ e.error }}</pre>
|
|
3419
|
+
}
|
|
3420
|
+
@if (e.responseBody !== null) {
|
|
3421
|
+
<div [style]="UI.label">Response</div>
|
|
3422
|
+
<pre [style]="UI.pre">{{ json(e.responseBody) }}</pre>
|
|
3423
|
+
}
|
|
3424
|
+
</div>
|
|
3425
|
+
}
|
|
3426
|
+
</div>
|
|
3427
|
+
}
|
|
3428
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
3429
|
+
}
|
|
3430
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TelemetryPanelComponent, decorators: [{
|
|
3431
|
+
type: Component,
|
|
3432
|
+
args: [{
|
|
3433
|
+
selector: 'devtools-telemetry-panel',
|
|
3434
|
+
standalone: true,
|
|
3435
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3436
|
+
template: `
|
|
3437
|
+
<p [style]="UI.intro" style="margin-bottom:10px">
|
|
3438
|
+
Todas as chamadas à API ({{ count() }}). Em memória — o reload limpa.
|
|
3439
|
+
</p>
|
|
3440
|
+
<button type="button" [style]="UI.ghost" style="margin-top:0;margin-bottom:14px" (click)="clear()">
|
|
3441
|
+
<span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:17px;vertical-align:-3px">delete_sweep</span>
|
|
3442
|
+
Limpar log
|
|
3443
|
+
</button>
|
|
3444
|
+
|
|
3445
|
+
@if (!count()) {
|
|
3446
|
+
<p [style]="UI.mono">Nenhuma requisição capturada ainda.</p>
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
@for (e of entries(); track e.id) {
|
|
3450
|
+
<div [style]="UI.card" style="margin-bottom:8px;cursor:pointer;padding:11px 13px" (click)="toggle(e.id)">
|
|
3451
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
3452
|
+
<span [style]="badge">{{ e.method }}</span>
|
|
3453
|
+
<span style="font-size:11px;font-weight:700;min-width:32px" [style.color]="statusColor(e)">
|
|
3454
|
+
{{ e.pending ? '···' : (e.status ?? 'ERR') }}
|
|
3455
|
+
</span>
|
|
3456
|
+
<span [style]="UI.mono" style="flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
|
|
3457
|
+
{{ path(e.url) }}
|
|
3458
|
+
</span>
|
|
3459
|
+
@if (e.durationMs != null) {
|
|
3460
|
+
<span [style]="UI.mono" style="white-space:nowrap;color:#6a6e7b">{{ e.durationMs }}ms</span>
|
|
3461
|
+
}
|
|
3462
|
+
</div>
|
|
3463
|
+
|
|
3464
|
+
@if (expanded() === e.id) {
|
|
3465
|
+
<div style="margin-top:11px;display:flex;flex-direction:column;gap:7px" (click)="$event.stopPropagation()">
|
|
3466
|
+
<div [style]="UI.label">URL</div>
|
|
3467
|
+
<pre [style]="UI.pre">{{ e.url }}</pre>
|
|
3468
|
+
|
|
3469
|
+
@if (hasParams(e)) {
|
|
3470
|
+
<div [style]="UI.label">Query params</div>
|
|
3471
|
+
<pre [style]="UI.pre">{{ json(e.params) }}</pre>
|
|
3472
|
+
}
|
|
3473
|
+
@if (e.requestBody !== null) {
|
|
3474
|
+
<div [style]="UI.label">Request body</div>
|
|
3475
|
+
<pre [style]="UI.pre">{{ json(e.requestBody) }}</pre>
|
|
3476
|
+
}
|
|
3477
|
+
@if (e.error) {
|
|
3478
|
+
<div [style]="UI.label" style="color:#ff7676">Erro</div>
|
|
3479
|
+
<pre [style]="UI.pre" style="color:#ff9b9b;border-color:#5a2a2a">{{ e.error }}</pre>
|
|
3480
|
+
}
|
|
3481
|
+
@if (e.responseBody !== null) {
|
|
3482
|
+
<div [style]="UI.label">Response</div>
|
|
3483
|
+
<pre [style]="UI.pre">{{ json(e.responseBody) }}</pre>
|
|
3484
|
+
}
|
|
3485
|
+
</div>
|
|
3486
|
+
}
|
|
3487
|
+
</div>
|
|
3488
|
+
}
|
|
3489
|
+
`,
|
|
3490
|
+
}]
|
|
3491
|
+
}] });
|
|
3492
|
+
|
|
3493
|
+
/**
|
|
3494
|
+
* Live, in-memory log of runtime JS/TS errors (uncaught, unhandled rejections,
|
|
3495
|
+
* console.error). Click a row to see the full stack. Cleared by a reload.
|
|
3496
|
+
*/
|
|
3497
|
+
class ErrorsPanelComponent {
|
|
3498
|
+
constructor() {
|
|
3499
|
+
this.UI = UI;
|
|
3500
|
+
this.errors = inject(DevtoolsErrorsService);
|
|
3501
|
+
this.entries = this.errors.entries;
|
|
3502
|
+
this.count = computed(() => this.entries().length, ...(ngDevMode ? [{ debugName: "count" }] : []));
|
|
3503
|
+
this.expanded = signal(null, ...(ngDevMode ? [{ debugName: "expanded" }] : []));
|
|
3504
|
+
this.badge = {
|
|
3505
|
+
fontFamily: "'JetBrains Mono',monospace",
|
|
3506
|
+
fontSize: '9.5px',
|
|
3507
|
+
fontWeight: '700',
|
|
3508
|
+
letterSpacing: '.03em',
|
|
3509
|
+
padding: '2px 6px',
|
|
3510
|
+
borderRadius: '6px',
|
|
3511
|
+
background: '#2a2024',
|
|
3512
|
+
flex: 'none',
|
|
3513
|
+
};
|
|
3514
|
+
}
|
|
3515
|
+
toggle(id) {
|
|
3516
|
+
this.expanded.update((cur) => (cur === id ? null : id));
|
|
3517
|
+
}
|
|
3518
|
+
clear() {
|
|
3519
|
+
this.errors.clear();
|
|
3520
|
+
this.expanded.set(null);
|
|
3521
|
+
}
|
|
3522
|
+
kindLabel(kind) {
|
|
3523
|
+
return kind === 'rejection' ? 'PROMISE' : kind === 'console' ? 'CONSOLE' : 'ERROR';
|
|
3524
|
+
}
|
|
3525
|
+
kindColor(kind) {
|
|
3526
|
+
return kind === 'console' ? '#ffb454' : '#ff6b6b';
|
|
3527
|
+
}
|
|
3528
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ErrorsPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3529
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ErrorsPanelComponent, isStandalone: true, selector: "devtools-errors-panel", ngImport: i0, template: `
|
|
3530
|
+
<p [style]="UI.intro" style="margin-bottom:10px">
|
|
3531
|
+
Erros de runtime JS/TS ({{ count() }}). Em memória — o reload limpa.
|
|
3532
|
+
</p>
|
|
3533
|
+
<button type="button" [style]="UI.ghost" style="margin-top:0;margin-bottom:14px" (click)="clear()">
|
|
3534
|
+
<span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:17px;vertical-align:-3px">delete_sweep</span>
|
|
3535
|
+
Limpar log
|
|
3536
|
+
</button>
|
|
3537
|
+
|
|
3538
|
+
@if (!count()) {
|
|
3539
|
+
<p [style]="UI.mono">Nenhum erro capturado. 🎉</p>
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
@for (e of entries(); track e.id) {
|
|
3543
|
+
<div [style]="UI.card" style="margin-bottom:8px;cursor:pointer;padding:11px 13px;border-color:#3a2a2a" (click)="toggle(e.id)">
|
|
3544
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
3545
|
+
<span [style]="badge" [style.color]="kindColor(e.kind)">{{ kindLabel(e.kind) }}</span>
|
|
3546
|
+
<span style="flex:1;min-width:0;font-size:12.5px;color:#f0d6d6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
|
|
3547
|
+
{{ e.message }}
|
|
3548
|
+
</span>
|
|
3549
|
+
</div>
|
|
3550
|
+
|
|
3551
|
+
@if (expanded() === e.id) {
|
|
3552
|
+
<div style="margin-top:11px;display:flex;flex-direction:column;gap:7px" (click)="$event.stopPropagation()">
|
|
3553
|
+
<div [style]="UI.label">Mensagem</div>
|
|
3554
|
+
<pre [style]="UI.pre" style="color:#ff9b9b;border-color:#5a2a2a">{{ e.message }}</pre>
|
|
3555
|
+
|
|
3556
|
+
@if (e.source) {
|
|
3557
|
+
<div [style]="UI.label">Origem</div>
|
|
3558
|
+
<pre [style]="UI.pre">{{ e.source }}</pre>
|
|
3559
|
+
}
|
|
3560
|
+
@if (e.stack) {
|
|
3561
|
+
<div [style]="UI.label">Stack</div>
|
|
3562
|
+
<pre [style]="UI.pre">{{ e.stack }}</pre>
|
|
3563
|
+
}
|
|
3564
|
+
</div>
|
|
3565
|
+
}
|
|
3566
|
+
</div>
|
|
3567
|
+
}
|
|
3568
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
3569
|
+
}
|
|
3570
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ErrorsPanelComponent, decorators: [{
|
|
3571
|
+
type: Component,
|
|
3572
|
+
args: [{
|
|
3573
|
+
selector: 'devtools-errors-panel',
|
|
3574
|
+
standalone: true,
|
|
3575
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3576
|
+
template: `
|
|
3577
|
+
<p [style]="UI.intro" style="margin-bottom:10px">
|
|
3578
|
+
Erros de runtime JS/TS ({{ count() }}). Em memória — o reload limpa.
|
|
3579
|
+
</p>
|
|
3580
|
+
<button type="button" [style]="UI.ghost" style="margin-top:0;margin-bottom:14px" (click)="clear()">
|
|
3581
|
+
<span class="dts-sym" style="font-family:'Material Symbols Outlined';font-size:17px;vertical-align:-3px">delete_sweep</span>
|
|
3582
|
+
Limpar log
|
|
3583
|
+
</button>
|
|
3584
|
+
|
|
3585
|
+
@if (!count()) {
|
|
3586
|
+
<p [style]="UI.mono">Nenhum erro capturado. 🎉</p>
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
@for (e of entries(); track e.id) {
|
|
3590
|
+
<div [style]="UI.card" style="margin-bottom:8px;cursor:pointer;padding:11px 13px;border-color:#3a2a2a" (click)="toggle(e.id)">
|
|
3591
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
3592
|
+
<span [style]="badge" [style.color]="kindColor(e.kind)">{{ kindLabel(e.kind) }}</span>
|
|
3593
|
+
<span style="flex:1;min-width:0;font-size:12.5px;color:#f0d6d6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
|
|
3594
|
+
{{ e.message }}
|
|
3595
|
+
</span>
|
|
3596
|
+
</div>
|
|
3597
|
+
|
|
3598
|
+
@if (expanded() === e.id) {
|
|
3599
|
+
<div style="margin-top:11px;display:flex;flex-direction:column;gap:7px" (click)="$event.stopPropagation()">
|
|
3600
|
+
<div [style]="UI.label">Mensagem</div>
|
|
3601
|
+
<pre [style]="UI.pre" style="color:#ff9b9b;border-color:#5a2a2a">{{ e.message }}</pre>
|
|
3602
|
+
|
|
3603
|
+
@if (e.source) {
|
|
3604
|
+
<div [style]="UI.label">Origem</div>
|
|
3605
|
+
<pre [style]="UI.pre">{{ e.source }}</pre>
|
|
3606
|
+
}
|
|
3607
|
+
@if (e.stack) {
|
|
3608
|
+
<div [style]="UI.label">Stack</div>
|
|
3609
|
+
<pre [style]="UI.pre">{{ e.stack }}</pre>
|
|
3610
|
+
}
|
|
3611
|
+
</div>
|
|
3612
|
+
}
|
|
3613
|
+
</div>
|
|
3614
|
+
}
|
|
3615
|
+
`,
|
|
3616
|
+
}]
|
|
3617
|
+
}] });
|
|
3618
|
+
|
|
3040
3619
|
const BUILTIN_ACTIONS = [
|
|
3041
3620
|
{ id: 'admin', label: 'Admin', icon: 'shield_person', content: AdminLoginPanelComponent, order: 0 },
|
|
3042
3621
|
{ id: 'app-login', label: 'Login App', icon: 'login', content: LoginPanelComponent, order: 1 },
|
|
@@ -3046,6 +3625,8 @@ const BUILTIN_ACTIONS = [
|
|
|
3046
3625
|
{ id: 'exams', label: 'Exames', icon: 'biotech', content: ExamsPanelComponent, order: 5 },
|
|
3047
3626
|
{ id: 'data', label: 'Dados', icon: 'badge', content: DataPanelComponent, order: 6 },
|
|
3048
3627
|
{ id: 'copy', label: 'Copiar', icon: 'content_copy', content: CopyPanelComponent, order: 7 },
|
|
3628
|
+
{ id: 'telemetry', label: 'Telemetria', icon: 'monitoring', content: TelemetryPanelComponent, order: 8 },
|
|
3629
|
+
{ id: 'errors', label: 'Erros', icon: 'bug_report', content: ErrorsPanelComponent, order: 9 },
|
|
3049
3630
|
];
|
|
3050
3631
|
/**
|
|
3051
3632
|
* Mount the self-contained DevTools. Add to `bootstrapApplication` providers or
|
|
@@ -3109,11 +3690,18 @@ function mountIsolated(parent, config, actions, shell) {
|
|
|
3109
3690
|
DevtoolsApiService,
|
|
3110
3691
|
DevtoolsAuthService,
|
|
3111
3692
|
DevtoolsSessionService,
|
|
3693
|
+
DevtoolsTelemetryService,
|
|
3694
|
+
DevtoolsErrorsService,
|
|
3112
3695
|
DevtoolsPositionService,
|
|
3113
3696
|
DevtoolsToastService,
|
|
3114
3697
|
], parent);
|
|
3115
|
-
|
|
3116
|
-
|
|
3698
|
+
runInInjectionContext(injector, () => {
|
|
3699
|
+
// Start capturing API traffic + runtime errors ASAP, then consume our own
|
|
3700
|
+
// Google OAuth return (guarded) to finish admin login.
|
|
3701
|
+
inject(DevtoolsTelemetryService).install();
|
|
3702
|
+
inject(DevtoolsErrorsService).install();
|
|
3703
|
+
inject(DevtoolsAuthService).handleGoogleReturn();
|
|
3704
|
+
});
|
|
3117
3705
|
const appRef = parent.get(ApplicationRef);
|
|
3118
3706
|
const host = document.createElement('div');
|
|
3119
3707
|
host.setAttribute('data-devtools-shell-host', '');
|