@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 —
|
|
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 +
|
|
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: '
|
|
250
|
-
height: '
|
|
251
|
-
left: cx -
|
|
252
|
-
top: cy -
|
|
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
|
-
|
|
3117
|
-
|
|
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', '');
|