analogger 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +187 -0
- package/ana-logger.d.cts +63 -0
- package/browser/ana-logger.mjs +354 -0
- package/dist/analogger-browser.min.mjs +51 -8
- package/dist/html-to-image-plugin.min.mjs +51 -8
- package/esm/ana-logger.mjs +379 -0
- package/package.json +1 -1
- package/src/ana-logger.cjs +379 -0
package/esm/ana-logger.mjs
CHANGED
|
@@ -3578,6 +3578,385 @@ class ____AnaLogger
|
|
|
3578
3578
|
return false;
|
|
3579
3579
|
}
|
|
3580
3580
|
|
|
3581
|
+
// ─── Snapshot storage ─────────────────────────────────────────────────────
|
|
3582
|
+
// Browser → localStorage
|
|
3583
|
+
// Node.js → os.tmpdir()/analogger-snapshots/<key>.json (persists across runs)
|
|
3584
|
+
|
|
3585
|
+
/**
|
|
3586
|
+
* Resolve the file path used to persist a snapshot in Node.js.
|
|
3587
|
+
* The directory is created on first use.
|
|
3588
|
+
* @param {string} key – storage key, e.g. "analogger_snapshot_default_SNAP01"
|
|
3589
|
+
* @returns {string} – absolute file path
|
|
3590
|
+
*/
|
|
3591
|
+
#_snapshotFilePath(key) {
|
|
3592
|
+
|
|
3593
|
+
const dir = path.join(os.tmpdir(), "analogger-snapshots");
|
|
3594
|
+
if (!fs.existsSync(dir)) {
|
|
3595
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
3596
|
+
}
|
|
3597
|
+
// Sanitise the key so it is always a safe filename
|
|
3598
|
+
const safeName = key.replace(/[^a-zA-Z0-9_\-]/g, "_") + ".json";
|
|
3599
|
+
return path.join(dir, safeName);
|
|
3600
|
+
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
#_snapshotGet(key) {
|
|
3604
|
+
// Browser path
|
|
3605
|
+
try {
|
|
3606
|
+
if (this.isBrowser() && typeof localStorage !== "undefined") {
|
|
3607
|
+
return localStorage.getItem(key);
|
|
3608
|
+
}
|
|
3609
|
+
} catch (e) {}
|
|
3610
|
+
|
|
3611
|
+
// Node.js path – read from temp file
|
|
3612
|
+
|
|
3613
|
+
try {
|
|
3614
|
+
const filePath = this.#_snapshotFilePath(key);
|
|
3615
|
+
if (fs.existsSync(filePath)) {
|
|
3616
|
+
return fs.readFileSync(filePath, "utf8");
|
|
3617
|
+
}
|
|
3618
|
+
} catch (e) {
|
|
3619
|
+
console.error("[AnaLogger] snapshot read error:", e.message);
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
|
|
3623
|
+
return null;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
#_snapshotSet(key, value) {
|
|
3627
|
+
// Browser path
|
|
3628
|
+
try {
|
|
3629
|
+
if (this.isBrowser() && typeof localStorage !== "undefined") {
|
|
3630
|
+
localStorage.setItem(key, value);
|
|
3631
|
+
return;
|
|
3632
|
+
}
|
|
3633
|
+
} catch (e) {}
|
|
3634
|
+
|
|
3635
|
+
// Node.js path – write to temp file
|
|
3636
|
+
|
|
3637
|
+
try {
|
|
3638
|
+
const filePath = this.#_snapshotFilePath(key);
|
|
3639
|
+
fs.writeFileSync(filePath, value, "utf8");
|
|
3640
|
+
console.log(`[AnaLogger] Snapshot persisted to: ${filePath}`);
|
|
3641
|
+
} catch (e) {
|
|
3642
|
+
console.error("[AnaLogger] snapshot write error:", e.message);
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
#_snapshotKey(snapshotID) {
|
|
3648
|
+
return `analogger_snapshot_${this.instanceName || "default"}_${snapshotID}`;
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
/**
|
|
3652
|
+
* Harvest lid entries from the current log history.
|
|
3653
|
+
* Only entries logged after the last startSnapshotProcess() call are included
|
|
3654
|
+
* when a snapshot window is active.
|
|
3655
|
+
* @param {{ messages?: boolean, context?: boolean }} opts
|
|
3656
|
+
* @returns {object[]}
|
|
3657
|
+
*/
|
|
3658
|
+
#_harvestSnapshot(opts = {}) {
|
|
3659
|
+
const { messages = true, context: includeContext = true } = opts;
|
|
3660
|
+
const fullHistory = this.getRawLogHistory ? this.getRawLogHistory() : (this.logHistory || []);
|
|
3661
|
+
|
|
3662
|
+
// If a snapshot window is active, slice from the marker index
|
|
3663
|
+
const history = this._snapshotWindowStart !== undefined
|
|
3664
|
+
? fullHistory.slice(this._snapshotWindowStart)
|
|
3665
|
+
: fullHistory;
|
|
3666
|
+
|
|
3667
|
+
/** Deduplicate by lid - keep the last occurrence */
|
|
3668
|
+
const seen = new Map();
|
|
3669
|
+
for (const entry of history) {
|
|
3670
|
+
const ctx = entry.context || {};
|
|
3671
|
+
const lid = ctx.lid;
|
|
3672
|
+
if (!lid) continue;
|
|
3673
|
+
const record = { lid };
|
|
3674
|
+
if (messages) record.message = entry.message ?? "";
|
|
3675
|
+
if (includeContext) record.context = { ...ctx };
|
|
3676
|
+
seen.set(lid, record);
|
|
3677
|
+
}
|
|
3678
|
+
return Array.from(seen.values());
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
/**
|
|
3682
|
+
* Mark the start of a snapshot window.
|
|
3683
|
+
* When called, any subsequent takeSnapshot() will only capture lids logged
|
|
3684
|
+
* from this point forward, ignoring everything already in the history.
|
|
3685
|
+
* Calling it again moves the window start to the current position.
|
|
3686
|
+
*
|
|
3687
|
+
* @example
|
|
3688
|
+
* anaLogger.keepLogHistory();
|
|
3689
|
+
* anaLogger.log({ lid: "WEB0001" }, "boot"); // excluded
|
|
3690
|
+
* anaLogger.startSnapshotProcess();
|
|
3691
|
+
* anaLogger.log({ lid: "WEB0002" }, "step 1"); // included
|
|
3692
|
+
* anaLogger.takeSnapshot("SNAP01"); // captures only WEB0002
|
|
3693
|
+
*/
|
|
3694
|
+
startSnapshotProcess() {
|
|
3695
|
+
const history = this.getRawLogHistory ? this.getRawLogHistory() : (this.logHistory || []);
|
|
3696
|
+
this._snapshotWindowStart = history.length;
|
|
3697
|
+
console.log(
|
|
3698
|
+
`[AnaLogger] Snapshot window started at history index ${this._snapshotWindowStart}.`
|
|
3699
|
+
);
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
/**
|
|
3703
|
+
* Capture all lids logged since the last startSnapshotProcess() (or since the
|
|
3704
|
+
* beginning of the session if startSnapshotProcess was never called) into a
|
|
3705
|
+
* named snapshot stored in localStorage / temp file.
|
|
3706
|
+
*
|
|
3707
|
+
* @param {string} snapshotID - unique label, e.g. "SNAP01"
|
|
3708
|
+
* @param {{ messages?: boolean, context?: boolean }} [opts]
|
|
3709
|
+
* messages: true - also store the log message per lid (default: true)
|
|
3710
|
+
* context: true - also store the full context object (default: true)
|
|
3711
|
+
* @returns {object[]} The array of snapshot entries that were saved
|
|
3712
|
+
*
|
|
3713
|
+
* @example
|
|
3714
|
+
* anaLogger.keepLogHistory();
|
|
3715
|
+
* anaLogger.log({ lid: "WEB0001" }, "step 1");
|
|
3716
|
+
* const entries = anaLogger.takeSnapshot("SNAP01");
|
|
3717
|
+
* anaLogger.log({ lid: "WEB0002" }, "step 2");
|
|
3718
|
+
* anaLogger.takeSnapshot("SNAP02", { messages: false, context: false });
|
|
3719
|
+
*/
|
|
3720
|
+
takeSnapshot(snapshotID, opts = {}) {
|
|
3721
|
+
try {
|
|
3722
|
+
if (!snapshotID || typeof snapshotID !== "string") {
|
|
3723
|
+
console.warn("[AnaLogger] takeSnapshot: snapshotID must be a non-empty string.");
|
|
3724
|
+
return [];
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
const { messages = true, context: includeContext = true } = opts;
|
|
3728
|
+
const entries = this.#_harvestSnapshot({ messages, context: includeContext });
|
|
3729
|
+
|
|
3730
|
+
const payload = {
|
|
3731
|
+
snapshotID,
|
|
3732
|
+
timestamp : Date.now(),
|
|
3733
|
+
options : { messages, context: includeContext },
|
|
3734
|
+
entries,
|
|
3735
|
+
};
|
|
3736
|
+
|
|
3737
|
+
this.#_snapshotSet(this.#_snapshotKey(snapshotID), JSON.stringify(payload));
|
|
3738
|
+
|
|
3739
|
+
console.log(
|
|
3740
|
+
`[AnaLogger] Snapshot "${snapshotID}" saved - ${entries.length} lid(s) captured.`
|
|
3741
|
+
);
|
|
3742
|
+
|
|
3743
|
+
return entries;
|
|
3744
|
+
} catch (e) {
|
|
3745
|
+
console.error("[AnaLogger] takeSnapshot error:", e.message);
|
|
3746
|
+
return [];
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
/**
|
|
3751
|
+
* Compare two previously saved snapshots and print a side-by-side diff table
|
|
3752
|
+
* showing which lids are shared, present only in snap1, or only in snap2.
|
|
3753
|
+
*
|
|
3754
|
+
* @param {string} snapshotID1
|
|
3755
|
+
* @param {string} snapshotID2
|
|
3756
|
+
* @param {[{ messages?: boolean }, { messages?: boolean }]} [displayOpts]
|
|
3757
|
+
* Optional per-snapshot display options.
|
|
3758
|
+
* displayOpts[0].messages = true - add a message column for snapshotID1
|
|
3759
|
+
* displayOpts[1].messages = true - add a message column for snapshotID2
|
|
3760
|
+
* @returns {{ onlyInSnap1: string[], onlyInSnap2: string[], inBoth: string[] } | null}
|
|
3761
|
+
*
|
|
3762
|
+
* @example
|
|
3763
|
+
* anaLogger.compareSnapshots("SNAP01", "SNAP02");
|
|
3764
|
+
* anaLogger.compareSnapshots("SNAP01", "SNAP02", [{ messages: true }, { messages: true }]);
|
|
3765
|
+
* anaLogger.compareSnapshots("SNAP01", "SNAP02", [{ messages: true }, { messages: false }]);
|
|
3766
|
+
*/
|
|
3767
|
+
compareSnapshots(snapshotID1, snapshotID2, displayOpts = []) {
|
|
3768
|
+
try {
|
|
3769
|
+
const raw1 = this.#_snapshotGet(this.#_snapshotKey(snapshotID1));
|
|
3770
|
+
const raw2 = this.#_snapshotGet(this.#_snapshotKey(snapshotID2));
|
|
3771
|
+
|
|
3772
|
+
if (!raw1) console.warn(`[AnaLogger] compareSnapshots: snapshot "${snapshotID1}" not found.`);
|
|
3773
|
+
if (!raw2) console.warn(`[AnaLogger] compareSnapshots: snapshot "${snapshotID2}" not found.`);
|
|
3774
|
+
if (!raw1 || !raw2) return null;
|
|
3775
|
+
|
|
3776
|
+
const snap1 = JSON.parse(raw1);
|
|
3777
|
+
const snap2 = JSON.parse(raw2);
|
|
3778
|
+
|
|
3779
|
+
// Resolve per-snapshot message display flags
|
|
3780
|
+
const showMsg1 = !!(displayOpts[0] && displayOpts[0].messages);
|
|
3781
|
+
const showMsg2 = !!(displayOpts[1] && displayOpts[1].messages);
|
|
3782
|
+
|
|
3783
|
+
const map1 = new Map(snap1.entries.map((e) => [e.lid, e]));
|
|
3784
|
+
const map2 = new Map(snap2.entries.map((e) => [e.lid, e]));
|
|
3785
|
+
const allLids = new Set([...map1.keys(), ...map2.keys()]);
|
|
3786
|
+
|
|
3787
|
+
const inBoth = [];
|
|
3788
|
+
const onlyIn1 = [];
|
|
3789
|
+
const onlyIn2 = [];
|
|
3790
|
+
|
|
3791
|
+
for (const lid of allLids) {
|
|
3792
|
+
const in1 = map1.has(lid);
|
|
3793
|
+
const in2 = map2.has(lid);
|
|
3794
|
+
if (in1 && in2) inBoth.push(lid);
|
|
3795
|
+
else if (in1) onlyIn1.push(lid);
|
|
3796
|
+
else onlyIn2.push(lid);
|
|
3797
|
+
}
|
|
3798
|
+
|
|
3799
|
+
// Build display rows: shared lids paired, then staggered exclusives
|
|
3800
|
+
const rows = [];
|
|
3801
|
+
for (const lid of inBoth) {
|
|
3802
|
+
rows.push({
|
|
3803
|
+
left: lid, right: lid, status: "both",
|
|
3804
|
+
msgLeft : map1.get(lid)?.message ?? "",
|
|
3805
|
+
msgRight: map2.get(lid)?.message ?? "",
|
|
3806
|
+
});
|
|
3807
|
+
}
|
|
3808
|
+
const maxOnly = Math.max(onlyIn1.length, onlyIn2.length);
|
|
3809
|
+
for (let i = 0; i < maxOnly; i++) {
|
|
3810
|
+
const l = onlyIn1[i] ?? "";
|
|
3811
|
+
const r = onlyIn2[i] ?? "";
|
|
3812
|
+
rows.push({
|
|
3813
|
+
left: l, right: r,
|
|
3814
|
+
status: l && r ? "diff" : (l ? "left" : "right"),
|
|
3815
|
+
msgLeft : l ? (map1.get(l)?.message ?? "") : "",
|
|
3816
|
+
msgRight: r ? (map2.get(r)?.message ?? "") : "",
|
|
3817
|
+
});
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
this.#_renderSnapshotTable(
|
|
3821
|
+
snapshotID1, snapshotID2, rows, snap1, snap2,
|
|
3822
|
+
{ showMsg1, showMsg2 }
|
|
3823
|
+
);
|
|
3824
|
+
|
|
3825
|
+
return { onlyInSnap1: onlyIn1, onlyInSnap2: onlyIn2, inBoth };
|
|
3826
|
+
} catch (e) {
|
|
3827
|
+
console.error("[AnaLogger] compareSnapshots error:", e.message);
|
|
3828
|
+
return null;
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3832
|
+
// ─── Snapshot table renderers ─────────────────────────────────────────────
|
|
3833
|
+
|
|
3834
|
+
#_renderSnapshotTable(id1, id2, rows, snap1, snap2, { showMsg1 = false, showMsg2 = false } = {}) {
|
|
3835
|
+
const ts = (snap) =>
|
|
3836
|
+
snap.timestamp ? new Date(snap.timestamp).toLocaleTimeString() : "-";
|
|
3837
|
+
|
|
3838
|
+
// ── Header ───────────────────────────────────────────────────────────
|
|
3839
|
+
console.log(`\nSnapshot comparison: "${id1}" vs "${id2}"`);
|
|
3840
|
+
console.log(` ${id1} captured at ${ts(snap1)} - ${snap1.entries.length} lid(s)`);
|
|
3841
|
+
console.log(` ${id2} captured at ${ts(snap2)} - ${snap2.entries.length} lid(s)\n`);
|
|
3842
|
+
|
|
3843
|
+
// ── console.table (auto-aligned in Node and browser devtools) ────────
|
|
3844
|
+
// Column keys are the snapshot IDs so headers are self-documenting.
|
|
3845
|
+
// Message columns are appended only when requested.
|
|
3846
|
+
const msgKey1 = `${id1} message`;
|
|
3847
|
+
const msgKey2 = `${id2} message`;
|
|
3848
|
+
|
|
3849
|
+
const tableData = rows.length
|
|
3850
|
+
? rows.map((r) => {
|
|
3851
|
+
const row = { [id1]: r.left, [id2]: r.right };
|
|
3852
|
+
if (showMsg1) row[msgKey1] = r.msgLeft;
|
|
3853
|
+
if (showMsg2) row[msgKey2] = r.msgRight;
|
|
3854
|
+
return row;
|
|
3855
|
+
})
|
|
3856
|
+
: [(() => {
|
|
3857
|
+
const row = { [id1]: "(empty)", [id2]: "(empty)" };
|
|
3858
|
+
if (showMsg1) row[msgKey1] = "";
|
|
3859
|
+
if (showMsg2) row[msgKey2] = "";
|
|
3860
|
+
return row;
|
|
3861
|
+
})()];
|
|
3862
|
+
|
|
3863
|
+
console.table(tableData);
|
|
3864
|
+
|
|
3865
|
+
// ── Browser HTML widget (progressive enhancement) ────────────────────
|
|
3866
|
+
if (typeof document !== "undefined") {
|
|
3867
|
+
this.#_renderSnapshotHtml(id1, id2, rows, snap1, snap2, ts, { showMsg1, showMsg2 });
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
#_renderSnapshotHtml(id1, id2, rows, snap1, snap2, ts, { showMsg1 = false, showMsg2 = false } = {}) {
|
|
3872
|
+
try {
|
|
3873
|
+
const C = {
|
|
3874
|
+
both: "#1e293b", bothBg: "#f8fafc",
|
|
3875
|
+
left: "#7f1d1d", leftBg: "#fef2f2",
|
|
3876
|
+
right: "#14532d", rightBg: "#f0fdf4",
|
|
3877
|
+
msg: "#475569", msgBg: "#f8fafc",
|
|
3878
|
+
header: "#1e40af", border: "#e2e8f0",
|
|
3879
|
+
};
|
|
3880
|
+
|
|
3881
|
+
const sharedCount = snap1.entries.filter(e => snap2.entries.some(e2 => e2.lid === e.lid)).length;
|
|
3882
|
+
const only1Count = snap1.entries.length - sharedCount;
|
|
3883
|
+
const only2Count = snap2.entries.length - sharedCount;
|
|
3884
|
+
|
|
3885
|
+
// Total visible columns: lid1, [msg1], sym, lid2, [msg2]
|
|
3886
|
+
const totalCols = 3 + (showMsg1 ? 1 : 0) + (showMsg2 ? 1 : 0);
|
|
3887
|
+
const td = (extra = "") =>
|
|
3888
|
+
`padding:4px 10px;font-family:monospace;font-size:12px;border-bottom:1px solid ${C.border};${extra}`;
|
|
3889
|
+
const tdMsg = (extra = "") =>
|
|
3890
|
+
`padding:4px 10px;font-size:11px;color:${C.msg};border-bottom:1px solid ${C.border};max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;${extra}`;
|
|
3891
|
+
const thStyle = `padding:5px 10px;text-align:left;background:#f1f5f9;font-size:11px;color:#475569;border-bottom:2px solid ${C.border}`;
|
|
3892
|
+
|
|
3893
|
+
const mkRow = (row) => {
|
|
3894
|
+
const lBg = row.status === "left" ? C.leftBg : C.bothBg;
|
|
3895
|
+
const rBg = row.status === "right" ? C.rightBg : C.bothBg;
|
|
3896
|
+
const lCl = row.status === "left" ? C.left : row.status === "right" ? "#94a3b8" : C.both;
|
|
3897
|
+
const rCl = row.status === "right" ? C.right : row.status === "left" ? "#94a3b8" : C.both;
|
|
3898
|
+
const sym = row.status === "both" ? "=" : row.status === "left" ? "◀" : row.status === "right" ? "▶" : "≠";
|
|
3899
|
+
|
|
3900
|
+
return `<tr>
|
|
3901
|
+
<td style="${td(`background:${lBg};color:${lCl}`)}">${row.left || ""}</td>
|
|
3902
|
+
${showMsg1 ? `<td style="${tdMsg(`background:${lBg}`)}" title="${(row.msgLeft || "").replace(/"/g, """)}">${row.msgLeft || ""}</td>` : ""}
|
|
3903
|
+
<td style="width:20px;text-align:center;border-bottom:1px solid ${C.border};color:#94a3b8;font-size:10px">${sym}</td>
|
|
3904
|
+
<td style="${td(`background:${rBg};color:${rCl}`)}">${row.right || ""}</td>
|
|
3905
|
+
${showMsg2 ? `<td style="${tdMsg(`background:${rBg}`)}" title="${(row.msgRight || "").replace(/"/g, """)}">${row.msgRight || ""}</td>` : ""}
|
|
3906
|
+
</tr>`;
|
|
3907
|
+
};
|
|
3908
|
+
|
|
3909
|
+
const html = `
|
|
3910
|
+
<div id="analogger-snapshot-cmp" style="
|
|
3911
|
+
font-family:system-ui,sans-serif;font-size:13px;
|
|
3912
|
+
border:1px solid ${C.border};border-radius:8px;overflow:hidden;
|
|
3913
|
+
max-width:${showMsg1 || showMsg2 ? "820px" : "520px"};margin:12px 0;box-shadow:0 2px 8px rgba(0,0,0,.08)">
|
|
3914
|
+
<div style="background:${C.header};color:#fff;padding:8px 14px;font-weight:600">
|
|
3915
|
+
Snapshot diff —
|
|
3916
|
+
<code style="font-size:12px">${id1}</code>
|
|
3917
|
+
<span style="opacity:.7"> vs </span>
|
|
3918
|
+
<code style="font-size:12px">${id2}</code>
|
|
3919
|
+
</div>
|
|
3920
|
+
<div style="display:flex;background:#e2e8f0;font-size:11px;color:#64748b;padding:3px 0">
|
|
3921
|
+
<span style="flex:1;padding-left:10px">${id1} · ${ts(snap1)} · ${snap1.entries.length} lids</span>
|
|
3922
|
+
<span style="width:20px"></span>
|
|
3923
|
+
<span style="flex:1;padding-left:10px">${id2} · ${ts(snap2)} · ${snap2.entries.length} lids</span>
|
|
3924
|
+
</div>
|
|
3925
|
+
<table style="width:100%;border-collapse:collapse">
|
|
3926
|
+
<thead>
|
|
3927
|
+
<tr>
|
|
3928
|
+
<th style="${thStyle}">${id1}</th>
|
|
3929
|
+
${showMsg1 ? `<th style="${thStyle};color:#94a3b8;font-style:italic">message</th>` : ""}
|
|
3930
|
+
<th style="width:20px;background:#f1f5f9;border-bottom:2px solid ${C.border}"></th>
|
|
3931
|
+
<th style="${thStyle}">${id2}</th>
|
|
3932
|
+
${showMsg2 ? `<th style="${thStyle};color:#94a3b8;font-style:italic">message</th>` : ""}
|
|
3933
|
+
</tr>
|
|
3934
|
+
</thead>
|
|
3935
|
+
<tbody>
|
|
3936
|
+
${rows.length
|
|
3937
|
+
? rows.map(mkRow).join("")
|
|
3938
|
+
: `<tr><td colspan="${totalCols}" style="padding:10px;color:#94a3b8;text-align:center">(no lids in either snapshot)</td></tr>`}
|
|
3939
|
+
</tbody>
|
|
3940
|
+
</table>
|
|
3941
|
+
<div style="background:#f8fafc;padding:5px 10px;font-size:11px;color:#64748b;display:flex;gap:16px">
|
|
3942
|
+
<span>✔ ${sharedCount} shared</span>
|
|
3943
|
+
<span style="color:${C.left}">◄ ${only1Count} only in ${id1}</span>
|
|
3944
|
+
<span style="color:${C.right}">► ${only2Count} only in ${id2}</span>
|
|
3945
|
+
</div>
|
|
3946
|
+
</div>`;
|
|
3947
|
+
|
|
3948
|
+
const $old = document.getElementById("analogger-snapshot-cmp");
|
|
3949
|
+
if ($old) $old.remove();
|
|
3950
|
+
|
|
3951
|
+
const $mount = document.querySelector("#analogger") || document.body;
|
|
3952
|
+
const $wrapper = document.createElement("div");
|
|
3953
|
+
$wrapper.innerHTML = html;
|
|
3954
|
+
$mount.appendChild($wrapper.firstElementChild);
|
|
3955
|
+
} catch (e) {
|
|
3956
|
+
// Non-fatal - plain-text already printed
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3581
3960
|
/**
|
|
3582
3961
|
* Set default targets, contexts and log levels
|
|
3583
3962
|
* @returns {boolean}
|