@youtyan/code-viewer 0.5.2 → 0.5.4

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/web/app.js CHANGED
@@ -680,12 +680,16 @@
680
680
  const table = params.get("table") || undefined;
681
681
  const tabRaw = params.get("tab");
682
682
  const tab = tabRaw === "data" || tabRaw === "query" || tabRaw === "schema" || tabRaw === "er" || tabRaw === "search" || tabRaw === "snapshot" ? tabRaw : undefined;
683
+ const diffBefore = params.get("diffBefore") || undefined;
684
+ const diffAfter = params.get("diffAfter") || undefined;
683
685
  return {
684
686
  screen: "database",
685
687
  ...db ? { db } : {},
686
688
  ...schema ? { schema } : {},
687
689
  ...table ? { table } : {},
688
690
  ...tab ? { tab } : {},
691
+ ...diffBefore ? { diffBefore } : {},
692
+ ...diffAfter ? { diffAfter } : {},
689
693
  range
690
694
  };
691
695
  }
@@ -752,6 +756,10 @@
752
756
  params.set("table", route.table);
753
757
  if (route.tab)
754
758
  params.set("tab", route.tab);
759
+ if (route.diffBefore)
760
+ params.set("diffBefore", route.diffBefore);
761
+ if (route.diffAfter)
762
+ params.set("diffAfter", route.diffAfter);
755
763
  const qs = params.toString();
756
764
  return `/database${qs ? `?${qs}` : ""}`;
757
765
  }
@@ -9642,7 +9650,24 @@ ${frontmatter.yaml}
9642
9650
  listTitle: (n2) => `Snapshots (${n2})`,
9643
9651
  diffSummary: (changed, unchanged) => `${changed} tables changed` + (unchanged > 0 ? `, ${unchanged} unchanged` : ""),
9644
9652
  showingRows: (shown, total) => `Showing ${shown} of ${total}`,
9645
- label: (date, tables, note) => `${date} (${tables} tables)${note}`
9653
+ label: (date, tables, note) => `${date} (${tables} tables)${note}`,
9654
+ statusRunning: "running",
9655
+ statusError: "error",
9656
+ statusDone: "done",
9657
+ progressLabel: (table2, index, total) => table2 ? `Snapshotting ${table2} (${index + 1} / ${total})` : `Finalizing… (${total} / ${total})`,
9658
+ progressFinalizing: "Finalizing…",
9659
+ aborted: "cancelled",
9660
+ failed: "Failed",
9661
+ errorPrefix: "Error",
9662
+ compareBeforeBadge: "before",
9663
+ compareAfterBadge: "after",
9664
+ diffShowing: (before, after) => `Showing: ${before} → ${after}`,
9665
+ filterPlaceholder: "Filter tables…",
9666
+ tableCounter: (selected, total) => `${selected} / ${total} selected`,
9667
+ coverageBeforeOnly: "not snapshotted in after",
9668
+ coverageAfterOnly: "not snapshotted in before",
9669
+ coverageNoteBeforeOnly: (rows) => `Only in the before snapshot (${rows} rows). Not selected for the after snapshot, so no comparison.`,
9670
+ coverageNoteAfterOnly: (rows) => `Only in the after snapshot (${rows} rows). Not selected for the before snapshot, so no comparison.`
9646
9671
  },
9647
9672
  explorer: {
9648
9673
  common: {
@@ -9914,7 +9939,24 @@ ${frontmatter.yaml}
9914
9939
  listTitle: (n2) => `スナップショット (${n2}件)`,
9915
9940
  diffSummary: (changed, unchanged) => `${changed}テーブルに変更あり` + (unchanged > 0 ? `、${unchanged}テーブルは変更なし` : ""),
9916
9941
  showingRows: (shown, total) => `全${total}件中 ${shown}件を表示中`,
9917
- label: (date, tables, note) => `${date} (${tables}テーブル)${note}`
9942
+ label: (date, tables, note) => `${date} (${tables}テーブル)${note}`,
9943
+ statusRunning: "取得中",
9944
+ statusError: "エラー",
9945
+ statusDone: "完了",
9946
+ progressLabel: (table2, index, total) => table2 ? `スナップショット取得中: ${table2} (${index + 1} / ${total})` : `仕上げ中… (${total} / ${total})`,
9947
+ progressFinalizing: "仕上げ中…",
9948
+ aborted: "キャンセル",
9949
+ failed: "失敗",
9950
+ errorPrefix: "エラー",
9951
+ compareBeforeBadge: "比較元",
9952
+ compareAfterBadge: "比較先",
9953
+ diffShowing: (before, after) => `比較中: ${before} → ${after}`,
9954
+ filterPlaceholder: "テーブル名で絞り込み…",
9955
+ tableCounter: (selected, total) => `${selected} / ${total} 件選択中`,
9956
+ coverageBeforeOnly: "比較先で未取得",
9957
+ coverageAfterOnly: "比較元で未取得",
9958
+ coverageNoteBeforeOnly: (rows) => `比較元のみに存在 (${rows}行)。比較先では対象テーブルに選ばれていないため比較できません。`,
9959
+ coverageNoteAfterOnly: (rows) => `比較先のみに存在 (${rows}行)。比較元では対象テーブルに選ばれていないため比較できません。`
9918
9960
  },
9919
9961
  explorer: {
9920
9962
  common: {
@@ -14495,18 +14537,18 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14495
14537
  const sb = [...b2].sort();
14496
14538
  return sa.every((v, i2) => v === sb[i2]);
14497
14539
  }
14498
- function postJson(path, body) {
14499
- return fetch(path, {
14540
+ function createSnapshotView(deps) {
14541
+ const text2 = () => deps.getText?.() ?? dbText("en");
14542
+ const trackLoad = (p2) => deps.trackLoad ? deps.trackLoad(p2) : p2;
14543
+ const postJson = (path, body) => trackLoad(fetch(path, {
14500
14544
  method: "POST",
14501
14545
  headers: {
14502
14546
  "Content-Type": "application/json",
14503
14547
  "X-Code-Viewer-Action": "1"
14504
14548
  },
14505
14549
  body: JSON.stringify(body)
14506
- });
14507
- }
14508
- function createSnapshotView(deps) {
14509
- const text2 = () => deps.getText?.() ?? dbText("en");
14550
+ }));
14551
+ const getJson = (path) => trackLoad(fetch(path));
14510
14552
  const changeTypeLabel = (type) => {
14511
14553
  if (type === "inserted")
14512
14554
  return text2().snapshot.inserted;
@@ -14540,18 +14582,27 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14540
14582
  refreshBtn.type = "button";
14541
14583
  refreshBtn.className = "db-snapshot-refresh-btn";
14542
14584
  refreshBtn.textContent = text2().snapshot.refresh;
14543
- const snapshotCancelBtn = document.createElement("button");
14544
- snapshotCancelBtn.type = "button";
14545
- snapshotCancelBtn.className = "db-snapshot-job-cancel-btn";
14546
- snapshotCancelBtn.textContent = text2().snapshot.cancel;
14547
- snapshotCancelBtn.hidden = true;
14548
- toolbar.append(createBtn, refreshBtn, snapshotCancelBtn);
14585
+ toolbar.append(createBtn, refreshBtn);
14586
+ const progressArea = document.createElement("div");
14587
+ progressArea.className = "db-snapshot-progress-area";
14588
+ progressArea.hidden = true;
14549
14589
  const tableSelector = document.createElement("div");
14550
14590
  tableSelector.className = "db-snapshot-table-selector";
14551
14591
  tableSelector.hidden = true;
14552
14592
  const tableSelectorHeader = document.createElement("div");
14553
14593
  tableSelectorHeader.className = "db-snapshot-table-selector-header";
14554
- tableSelectorHeader.textContent = text2().snapshot.selectTablesHeader;
14594
+ const tableSelectorHeaderLabel = document.createElement("span");
14595
+ tableSelectorHeaderLabel.textContent = text2().snapshot.selectTablesHeader;
14596
+ const tableSelectorCounter = document.createElement("span");
14597
+ tableSelectorCounter.className = "db-snapshot-table-counter";
14598
+ tableSelectorHeader.append(tableSelectorHeaderLabel, tableSelectorCounter);
14599
+ const filterRow = document.createElement("div");
14600
+ filterRow.className = "db-snapshot-table-filter";
14601
+ const filterInput = document.createElement("input");
14602
+ filterInput.type = "search";
14603
+ filterInput.className = "db-snapshot-table-filter-input";
14604
+ filterInput.placeholder = text2().snapshot.filterPlaceholder;
14605
+ filterRow.appendChild(filterInput);
14555
14606
  const tableCheckboxes = document.createElement("div");
14556
14607
  tableCheckboxes.className = "db-snapshot-table-checkboxes";
14557
14608
  const selectActions = document.createElement("div");
@@ -14562,16 +14613,22 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14562
14613
  const deselectAllBtn = document.createElement("button");
14563
14614
  deselectAllBtn.type = "button";
14564
14615
  deselectAllBtn.textContent = text2().snapshot.deselectAll;
14565
- const noteInput = document.createElement("input");
14566
- noteInput.type = "text";
14567
- noteInput.className = "db-snapshot-note-input";
14568
- noteInput.placeholder = text2().snapshot.notePlaceholder;
14616
+ selectActions.append(selectAllBtn, deselectAllBtn);
14617
+ const noteRow = document.createElement("div");
14618
+ noteRow.className = "db-snapshot-note-row";
14569
14619
  const noteField = document.createElement("label");
14570
14620
  noteField.className = "db-snapshot-note-field";
14571
14621
  const noteLabel = document.createElement("span");
14572
14622
  noteLabel.className = "db-snapshot-note-label";
14573
14623
  noteLabel.textContent = text2().snapshot.noteLabel;
14624
+ const noteInput = document.createElement("input");
14625
+ noteInput.type = "text";
14626
+ noteInput.className = "db-snapshot-note-input";
14627
+ noteInput.placeholder = text2().snapshot.notePlaceholder;
14574
14628
  noteField.append(noteLabel, noteInput);
14629
+ noteRow.appendChild(noteField);
14630
+ const actionRow = document.createElement("div");
14631
+ actionRow.className = "db-snapshot-action-row";
14575
14632
  const confirmBtn = document.createElement("button");
14576
14633
  confirmBtn.type = "button";
14577
14634
  confirmBtn.className = "db-snapshot-confirm-btn";
@@ -14580,41 +14637,178 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14580
14637
  cancelBtn.type = "button";
14581
14638
  cancelBtn.className = "db-snapshot-cancel-btn";
14582
14639
  cancelBtn.textContent = text2().snapshot.cancel;
14583
- selectActions.append(selectAllBtn, deselectAllBtn, noteField, confirmBtn, cancelBtn);
14584
- tableSelector.append(tableSelectorHeader, tableCheckboxes, selectActions);
14640
+ actionRow.append(confirmBtn, cancelBtn);
14641
+ tableSelector.append(tableSelectorHeader, filterRow, tableCheckboxes, selectActions, noteRow, actionRow);
14585
14642
  const mainArea = document.createElement("div");
14586
14643
  mainArea.className = "db-snapshot-main-area";
14587
- el.append(guide, toolbar, tableSelector, mainArea);
14644
+ const splitContainer = document.createElement("div");
14645
+ splitContainer.className = "db-snapshot-split";
14646
+ const leftPane = document.createElement("div");
14647
+ leftPane.className = "db-snapshot-left-pane";
14648
+ const rightPane = document.createElement("div");
14649
+ rightPane.className = "db-snapshot-right-pane";
14650
+ splitContainer.append(leftPane, rightPane);
14651
+ mainArea.appendChild(splitContainer);
14652
+ el.append(guide, toolbar, progressArea, tableSelector, mainArea);
14588
14653
  let snapshots = [];
14589
- let activeSnapshotId = null;
14590
14654
  let currentDiff = null;
14591
14655
  let disposed = false;
14592
14656
  const autoRefreshTimers = new Set;
14593
- function setActiveSnapshotId(id) {
14594
- activeSnapshotId = id;
14595
- snapshotCancelBtn.hidden = !id;
14596
- snapshotCancelBtn.disabled = false;
14597
- snapshotCancelBtn.textContent = text2().snapshot.cancel;
14657
+ const activeJobs = new Map;
14658
+ const cancellingJobs = new Set;
14659
+ const jobClearTimers = new Map;
14660
+ function upsertJob(id, patch) {
14661
+ const prev = activeJobs.get(id) ?? { id, total: 0, done: false };
14662
+ activeJobs.set(id, { ...prev, ...patch, id });
14663
+ renderProgress();
14664
+ }
14665
+ function scheduleJobRemove(id) {
14666
+ const existing = jobClearTimers.get(id);
14667
+ if (existing)
14668
+ clearTimeout(existing);
14669
+ const t2 = setTimeout(() => {
14670
+ jobClearTimers.delete(id);
14671
+ cancellingJobs.delete(id);
14672
+ activeJobs.delete(id);
14673
+ renderProgress();
14674
+ }, 5000);
14675
+ jobClearTimers.set(id, t2);
14676
+ }
14677
+ async function cancelJob(id) {
14678
+ if (cancellingJobs.has(id))
14679
+ return;
14680
+ cancellingJobs.add(id);
14681
+ renderProgress();
14682
+ try {
14683
+ await postJson("/_db/snapshot/cancel", { id });
14684
+ } catch {}
14685
+ }
14686
+ function renderProgress() {
14687
+ progressArea.innerHTML = "";
14688
+ if (activeJobs.size === 0) {
14689
+ progressArea.hidden = true;
14690
+ return;
14691
+ }
14692
+ progressArea.hidden = false;
14693
+ const t2 = text2().snapshot;
14694
+ for (const job of activeJobs.values()) {
14695
+ const row = document.createElement("div");
14696
+ row.className = "db-snapshot-progress-row";
14697
+ row.dataset.snapshotId = job.id;
14698
+ const label = document.createElement("span");
14699
+ label.className = "db-snapshot-progress-label";
14700
+ if (job.errorMessage) {
14701
+ label.textContent = `${t2.errorPrefix}: ${job.errorMessage}`;
14702
+ label.title = job.errorMessage;
14703
+ row.classList.add("error");
14704
+ } else if (job.aborted) {
14705
+ label.textContent = t2.aborted;
14706
+ row.classList.add("aborted");
14707
+ } else if (job.done) {
14708
+ label.textContent = t2.statusDone;
14709
+ row.classList.add("done");
14710
+ } else if (job.total > 0 && job.table) {
14711
+ label.textContent = t2.progressLabel(job.table, job.index ?? 0, job.total);
14712
+ row.classList.add("running");
14713
+ } else {
14714
+ label.textContent = t2.creating;
14715
+ row.classList.add("running");
14716
+ }
14717
+ row.appendChild(label);
14718
+ if (!job.done) {
14719
+ const cancelRowBtn = document.createElement("button");
14720
+ cancelRowBtn.type = "button";
14721
+ cancelRowBtn.className = "db-snapshot-job-cancel-btn";
14722
+ if (cancellingJobs.has(job.id)) {
14723
+ cancelRowBtn.disabled = true;
14724
+ cancelRowBtn.textContent = t2.cancelling;
14725
+ } else {
14726
+ cancelRowBtn.textContent = t2.cancel;
14727
+ cancelRowBtn.addEventListener("click", () => {
14728
+ cancelJob(job.id);
14729
+ });
14730
+ }
14731
+ row.appendChild(cancelRowBtn);
14732
+ }
14733
+ progressArea.appendChild(row);
14734
+ }
14735
+ }
14736
+ let tableSelectorTables = [];
14737
+ const selectedTableSet = new Set;
14738
+ let tableFilterValue = "";
14739
+ function setTableSelectorVisible(visible) {
14740
+ tableSelector.hidden = !visible;
14741
+ createBtn.disabled = visible;
14742
+ createBtn.classList.toggle("is-active-dialog", visible);
14598
14743
  }
14599
14744
  function showTableSelector() {
14600
- const tables = deps.getTables();
14601
- const lastTables = getLastTables();
14745
+ tableSelectorTables = deps.getTables().filter((t2) => t2.type === "table");
14746
+ const currentTableNames = new Set(tableSelectorTables.map((t2) => t2.name));
14747
+ const stored = deps.getStoredSelectedTables?.();
14748
+ const rawTables = stored?.length ? stored : getLastTables();
14749
+ const lastTables = rawTables.filter((name) => currentTableNames.has(name));
14750
+ selectedTableSet.clear();
14751
+ if (lastTables.length === 0) {
14752
+ for (const t2 of tableSelectorTables)
14753
+ selectedTableSet.add(t2.name);
14754
+ } else {
14755
+ for (const name of lastTables)
14756
+ selectedTableSet.add(name);
14757
+ }
14758
+ tableFilterValue = "";
14759
+ filterInput.value = "";
14760
+ renderTableList();
14761
+ setTableSelectorVisible(true);
14762
+ noteInput.value = "";
14763
+ filterInput.focus();
14764
+ }
14765
+ function persistSelectedTables() {
14766
+ deps.setStoredSelectedTables?.(Array.from(selectedTableSet).sort());
14767
+ }
14768
+ function renderTableList() {
14769
+ const needle = tableFilterValue.trim().toLowerCase();
14602
14770
  tableCheckboxes.innerHTML = "";
14603
- for (const t2 of tables) {
14604
- if (t2.type !== "table")
14771
+ const t2 = text2().snapshot;
14772
+ let visible = 0;
14773
+ for (const tbl of tableSelectorTables) {
14774
+ if (needle && !tbl.name.toLowerCase().includes(needle))
14605
14775
  continue;
14606
- const label = document.createElement("label");
14607
- label.className = "db-snapshot-table-label";
14776
+ visible++;
14777
+ const row = document.createElement("label");
14778
+ row.className = "db-snapshot-table-row";
14608
14779
  const cb = document.createElement("input");
14609
14780
  cb.type = "checkbox";
14610
- cb.checked = lastTables.length === 0 || lastTables.includes(t2.name);
14611
- cb.value = t2.name;
14612
- const rowInfo = t2.rowCount != null ? text2().snapshot.rowCount(t2.rowCount) : "";
14613
- label.append(cb, ` ${t2.name}${rowInfo}`);
14614
- tableCheckboxes.appendChild(label);
14781
+ cb.value = tbl.name;
14782
+ cb.checked = selectedTableSet.has(tbl.name);
14783
+ cb.addEventListener("change", () => {
14784
+ if (cb.checked)
14785
+ selectedTableSet.add(tbl.name);
14786
+ else
14787
+ selectedTableSet.delete(tbl.name);
14788
+ updateSelectorState();
14789
+ persistSelectedTables();
14790
+ });
14791
+ const name = document.createElement("span");
14792
+ name.className = "db-snapshot-table-row-name";
14793
+ name.textContent = tbl.name;
14794
+ const count = document.createElement("span");
14795
+ count.className = "db-snapshot-table-row-count";
14796
+ count.textContent = tbl.rowCount != null ? t2.rowCount(tbl.rowCount).trim() : "";
14797
+ row.append(cb, name, count);
14798
+ tableCheckboxes.appendChild(row);
14615
14799
  }
14616
- tableSelector.hidden = false;
14617
- noteInput.value = "";
14800
+ if (visible === 0 && needle) {
14801
+ const empty = document.createElement("div");
14802
+ empty.className = "db-snapshot-table-row-empty";
14803
+ empty.textContent = t2.empty;
14804
+ tableCheckboxes.appendChild(empty);
14805
+ }
14806
+ updateSelectorState();
14807
+ }
14808
+ function updateSelectorState() {
14809
+ const t2 = text2().snapshot;
14810
+ tableSelectorCounter.textContent = t2.tableCounter(selectedTableSet.size, tableSelectorTables.length);
14811
+ confirmBtn.disabled = selectedTableSet.size === 0;
14618
14812
  }
14619
14813
  function getLastTables() {
14620
14814
  const dbId = deps.getDbId();
@@ -14624,36 +14818,47 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14624
14818
  return done.length > 0 ? done[0].tables : [];
14625
14819
  }
14626
14820
  function getSelectedTables() {
14627
- return Array.from(tableCheckboxes.querySelectorAll('input[type="checkbox"]:checked')).map((cb) => cb.value);
14821
+ return Array.from(selectedTableSet);
14628
14822
  }
14823
+ filterInput.addEventListener("input", () => {
14824
+ tableFilterValue = filterInput.value;
14825
+ renderTableList();
14826
+ });
14827
+ filterInput.addEventListener("keydown", (e2) => {
14828
+ if (e2.key === "Escape" && filterInput.value) {
14829
+ e2.preventDefault();
14830
+ filterInput.value = "";
14831
+ tableFilterValue = "";
14832
+ renderTableList();
14833
+ }
14834
+ });
14629
14835
  selectAllBtn.addEventListener("click", () => {
14630
- tableCheckboxes.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
14631
- cb.checked = true;
14632
- });
14836
+ const needle = tableFilterValue.trim().toLowerCase();
14837
+ for (const tbl of tableSelectorTables) {
14838
+ if (needle && !tbl.name.toLowerCase().includes(needle))
14839
+ continue;
14840
+ selectedTableSet.add(tbl.name);
14841
+ }
14842
+ renderTableList();
14843
+ persistSelectedTables();
14633
14844
  });
14634
14845
  deselectAllBtn.addEventListener("click", () => {
14635
- tableCheckboxes.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
14636
- cb.checked = false;
14637
- });
14846
+ const needle = tableFilterValue.trim().toLowerCase();
14847
+ if (!needle) {
14848
+ selectedTableSet.clear();
14849
+ } else {
14850
+ for (const tbl of tableSelectorTables) {
14851
+ if (tbl.name.toLowerCase().includes(needle)) {
14852
+ selectedTableSet.delete(tbl.name);
14853
+ }
14854
+ }
14855
+ }
14856
+ renderTableList();
14857
+ persistSelectedTables();
14638
14858
  });
14639
14859
  createBtn.addEventListener("click", showTableSelector);
14640
14860
  cancelBtn.addEventListener("click", () => {
14641
- tableSelector.hidden = true;
14642
- });
14643
- snapshotCancelBtn.addEventListener("click", async () => {
14644
- if (!activeSnapshotId)
14645
- return;
14646
- const snapshotId = activeSnapshotId;
14647
- snapshotCancelBtn.disabled = true;
14648
- snapshotCancelBtn.textContent = text2().snapshot.cancelling;
14649
- try {
14650
- await postJson("/_db/snapshot/cancel", { id: snapshotId });
14651
- } catch {} finally {
14652
- if (!disposed && activeSnapshotId === snapshotId) {
14653
- snapshotCancelBtn.disabled = false;
14654
- snapshotCancelBtn.textContent = text2().snapshot.cancel;
14655
- }
14656
- }
14861
+ setTableSelectorVisible(false);
14657
14862
  });
14658
14863
  confirmBtn.addEventListener("click", async () => {
14659
14864
  const dbId = deps.getDbId();
@@ -14672,7 +14877,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14672
14877
  tables,
14673
14878
  note: noteInput.value.trim()
14674
14879
  });
14675
- tableSelector.hidden = true;
14880
+ setTableSelectorVisible(false);
14676
14881
  scheduleAutoRefresh(dbId, schema);
14677
14882
  } catch {} finally {
14678
14883
  if (!disposed) {
@@ -14692,7 +14897,20 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14692
14897
  autoRefreshTimers.add(timer);
14693
14898
  }
14694
14899
  refreshBtn.addEventListener("click", refresh);
14900
+ let inflightRefresh = null;
14695
14901
  async function refresh() {
14902
+ if (inflightRefresh) {
14903
+ await inflightRefresh;
14904
+ return;
14905
+ }
14906
+ inflightRefresh = doRefresh();
14907
+ try {
14908
+ await inflightRefresh;
14909
+ } finally {
14910
+ inflightRefresh = null;
14911
+ }
14912
+ }
14913
+ async function doRefresh() {
14696
14914
  const dbId = deps.getDbId();
14697
14915
  const schema = deps.getSchema();
14698
14916
  if (!dbId)
@@ -14701,7 +14919,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14701
14919
  const params = new URLSearchParams({ db: dbId });
14702
14920
  if (schema)
14703
14921
  params.set("schema", schema);
14704
- const snapRes = await fetch(`/_db/snapshot/list?${params}`);
14922
+ const snapRes = await getJson(`/_db/snapshot/list?${params}`);
14705
14923
  if (snapRes.ok) {
14706
14924
  const data = await snapRes.json();
14707
14925
  if (disposed || deps.getDbId() !== dbId || deps.getSchema() !== schema) {
@@ -14712,6 +14930,18 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14712
14930
  if (disposed || deps.getDbId() !== dbId || deps.getSchema() !== schema)
14713
14931
  return;
14714
14932
  renderMain();
14933
+ if (currentDiff) {
14934
+ const stillValid = snapshots.some((s2) => s2.id === currentDiff?.beforeId) && snapshots.some((s2) => s2.id === currentDiff?.afterId);
14935
+ if (!stillValid) {
14936
+ const existing = rightPane.querySelector(".db-snapshot-diff-inline");
14937
+ if (existing) {
14938
+ const prev = existing;
14939
+ prev.__tocObserver?.disconnect();
14940
+ existing.remove();
14941
+ }
14942
+ currentDiff = null;
14943
+ }
14944
+ }
14715
14945
  } catch {}
14716
14946
  }
14717
14947
  async function refreshAndAutoDiff() {
@@ -14726,21 +14956,24 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14726
14956
  showDiffInline(prev.id, newest.id);
14727
14957
  }
14728
14958
  function renderMain() {
14729
- mainArea.innerHTML = "";
14959
+ leftPane.innerHTML = "";
14730
14960
  const done = snapshots.filter((s2) => s2.status === "done");
14731
14961
  if (snapshots.length === 0) {
14732
14962
  const empty = document.createElement("div");
14733
14963
  empty.className = "db-snapshot-empty";
14734
14964
  empty.textContent = text2().snapshot.empty;
14735
- mainArea.appendChild(empty);
14965
+ leftPane.appendChild(empty);
14736
14966
  return;
14737
14967
  }
14968
+ const stickyHeader = document.createElement("div");
14969
+ stickyHeader.className = "db-snapshot-sticky-header";
14738
14970
  const snapshotSection = document.createElement("div");
14739
14971
  snapshotSection.className = "db-snapshot-list-section";
14740
14972
  const snapTitle = document.createElement("h3");
14741
14973
  snapTitle.className = "db-snapshot-section-title";
14742
- snapTitle.textContent = text2().snapshot.listTitle(done.length);
14974
+ snapTitle.textContent = text2().snapshot.listTitle(snapshots.length);
14743
14975
  snapshotSection.appendChild(snapTitle);
14976
+ const t2 = text2().snapshot;
14744
14977
  for (const snap of snapshots) {
14745
14978
  const item = document.createElement("div");
14746
14979
  item.className = "db-snapshot-item";
@@ -14750,17 +14983,27 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14750
14983
  const date = document.createElement("span");
14751
14984
  date.className = "db-snapshot-date";
14752
14985
  date.textContent = new Date(snap.createdAt).toLocaleString();
14986
+ const status = document.createElement("span");
14987
+ status.className = `db-snapshot-status ${snap.status}`;
14988
+ status.textContent = snap.status === "running" ? t2.statusRunning : snap.status === "error" ? t2.statusError : t2.statusDone;
14753
14989
  const tables = document.createElement("span");
14754
14990
  tables.className = "db-snapshot-tables-count";
14755
14991
  tables.title = snap.tables.join(", ");
14756
- tables.textContent = text2().snapshot.tableCount(snap.tables.length);
14757
- info.append(date, tables);
14992
+ tables.textContent = t2.tableCount(snap.tables.length);
14993
+ info.append(date, status, tables);
14758
14994
  if (snap.note) {
14759
14995
  const note = document.createElement("span");
14760
14996
  note.className = "db-snapshot-note";
14761
14997
  note.textContent = snap.note;
14762
14998
  info.appendChild(note);
14763
14999
  }
15000
+ if (snap.status === "error" && snap.errorMessage) {
15001
+ const errMsg = document.createElement("span");
15002
+ errMsg.className = "db-snapshot-error-message";
15003
+ errMsg.textContent = `${t2.errorPrefix}: ${snap.errorMessage}`;
15004
+ errMsg.title = snap.errorMessage;
15005
+ info.appendChild(errMsg);
15006
+ }
14764
15007
  const actions = document.createElement("div");
14765
15008
  actions.className = "db-snapshot-actions";
14766
15009
  const noteBtn = document.createElement("button");
@@ -14797,7 +15040,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14797
15040
  beforeSelect.className = "db-snapshot-diff-select";
14798
15041
  const afterSelect = document.createElement("select");
14799
15042
  afterSelect.className = "db-snapshot-diff-select";
14800
- const sorted = [...done].sort((a2, b2) => new Date(a2.createdAt).getTime() - new Date(b2.createdAt).getTime());
15043
+ const sorted = [...done].sort((a2, b2) => new Date(b2.createdAt).getTime() - new Date(a2.createdAt).getTime());
14801
15044
  for (const s2 of sorted) {
14802
15045
  const optB = document.createElement("option");
14803
15046
  optB.value = s2.id;
@@ -14809,8 +15052,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14809
15052
  afterSelect.appendChild(optA);
14810
15053
  }
14811
15054
  if (sorted.length >= 2) {
14812
- beforeSelect.selectedIndex = sorted.length - 2;
14813
- afterSelect.selectedIndex = sorted.length - 1;
15055
+ beforeSelect.selectedIndex = 1;
15056
+ afterSelect.selectedIndex = 0;
14814
15057
  }
14815
15058
  const diffBtn = document.createElement("button");
14816
15059
  diffBtn.type = "button";
@@ -14820,32 +15063,89 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14820
15063
  diffBtn.disabled = true;
14821
15064
  diffBtn.textContent = text2().snapshot.comparing;
14822
15065
  try {
14823
- showDiffInline(beforeSelect.value, afterSelect.value);
15066
+ await showDiffInline(beforeSelect.value, afterSelect.value);
15067
+ scrollDiffIntoView();
14824
15068
  } finally {
14825
15069
  diffBtn.disabled = false;
14826
15070
  diffBtn.textContent = text2().snapshot.diffManual;
14827
15071
  }
14828
15072
  });
14829
- diffCreate.append(beforeSelect, document.createTextNode(" "), afterSelect, diffBtn);
14830
- snapshotSection.appendChild(diffCreate);
14831
- }
14832
- mainArea.appendChild(snapshotSection);
15073
+ const applyDiffSelectionHighlight = () => {
15074
+ const items = snapshotSection.querySelectorAll(".db-snapshot-item");
15075
+ const before = beforeSelect.value;
15076
+ const after = afterSelect.value;
15077
+ for (const it of items) {
15078
+ it.classList.remove("is-before", "is-after");
15079
+ const old = it.querySelector(".db-snapshot-compare-badge");
15080
+ if (old)
15081
+ old.remove();
15082
+ }
15083
+ for (const it of items) {
15084
+ const id = it.dataset.snapshotId ?? "";
15085
+ let label = null;
15086
+ if (id === before) {
15087
+ it.classList.add("is-before");
15088
+ label = t2.compareBeforeBadge;
15089
+ } else if (id === after) {
15090
+ it.classList.add("is-after");
15091
+ label = t2.compareAfterBadge;
15092
+ }
15093
+ if (label) {
15094
+ const badge = document.createElement("span");
15095
+ badge.className = "db-snapshot-compare-badge";
15096
+ badge.textContent = label;
15097
+ const info = it.querySelector(".db-snapshot-info");
15098
+ if (info)
15099
+ info.appendChild(badge);
15100
+ }
15101
+ }
15102
+ };
15103
+ beforeSelect.addEventListener("change", applyDiffSelectionHighlight);
15104
+ afterSelect.addEventListener("change", applyDiffSelectionHighlight);
15105
+ applyDiffSelectionHighlight();
15106
+ const beforeRow = document.createElement("label");
15107
+ beforeRow.className = "db-snapshot-diff-row is-before";
15108
+ const beforeLabel = document.createElement("span");
15109
+ beforeLabel.className = "db-snapshot-diff-row-label";
15110
+ beforeLabel.textContent = t2.compareBeforeBadge;
15111
+ beforeRow.append(beforeLabel, beforeSelect);
15112
+ const afterRow = document.createElement("label");
15113
+ afterRow.className = "db-snapshot-diff-row is-after";
15114
+ const afterLabel = document.createElement("span");
15115
+ afterLabel.className = "db-snapshot-diff-row-label";
15116
+ afterLabel.textContent = t2.compareAfterBadge;
15117
+ afterRow.append(afterLabel, afterSelect);
15118
+ diffCreate.append(beforeRow, afterRow, diffBtn);
15119
+ stickyHeader.appendChild(diffCreate);
15120
+ }
15121
+ leftPane.append(stickyHeader, snapshotSection);
15122
+ }
15123
+ function scrollDiffIntoView() {
15124
+ const section = rightPane.querySelector(".db-snapshot-diff-inline");
15125
+ if (!section)
15126
+ return;
15127
+ section.scrollIntoView({ behavior: "smooth", block: "start" });
14833
15128
  }
14834
15129
  async function showDiffInline(beforeId, afterId) {
14835
15130
  currentDiff = { beforeId, afterId };
14836
- const existing = mainArea.querySelector(".db-snapshot-diff-inline");
14837
- if (existing)
15131
+ deps.setRouteDiff?.({ before: beforeId, after: afterId });
15132
+ const existing = rightPane.querySelector(".db-snapshot-diff-inline");
15133
+ if (existing) {
15134
+ const prev = existing;
15135
+ prev.__tocObserver?.disconnect();
14838
15136
  existing.remove();
15137
+ }
14839
15138
  const loading = document.createElement("div");
14840
15139
  loading.className = "db-snapshot-diff-inline";
14841
15140
  const loadingText = document.createElement("div");
14842
15141
  loadingText.className = "db-snapshot-loading";
14843
15142
  loadingText.textContent = text2().snapshot.calculatingDiff;
14844
15143
  loading.appendChild(loadingText);
14845
- mainArea.appendChild(loading);
15144
+ rightPane.appendChild(loading);
14846
15145
  try {
14847
- const res = await fetch(`/_db/snapshot/diff/tables?before=${encodeURIComponent(beforeId)}&after=${encodeURIComponent(afterId)}`);
15146
+ const res = await getJson(`/_db/snapshot/diff/tables?before=${encodeURIComponent(beforeId)}&after=${encodeURIComponent(afterId)}`);
14848
15147
  if (!res.ok) {
15148
+ currentDiff = null;
14849
15149
  loading.innerHTML = "";
14850
15150
  const error2 = document.createElement("div");
14851
15151
  error2.className = "db-snapshot-error";
@@ -14857,6 +15157,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14857
15157
  loading.remove();
14858
15158
  renderDiffInline(beforeId, afterId, data.tables);
14859
15159
  } catch {
15160
+ currentDiff = null;
14860
15161
  loading.innerHTML = "";
14861
15162
  const error2 = document.createElement("div");
14862
15163
  error2.className = "db-snapshot-error";
@@ -14865,26 +15166,76 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14865
15166
  }
14866
15167
  }
14867
15168
  function renderDiffInline(beforeId, afterId, tables) {
14868
- const existing = mainArea.querySelector(".db-snapshot-diff-inline");
14869
- if (existing)
15169
+ const existing = rightPane.querySelector(".db-snapshot-diff-inline");
15170
+ if (existing) {
15171
+ const prev = existing;
15172
+ prev.__tocObserver?.disconnect();
14870
15173
  existing.remove();
15174
+ }
14871
15175
  const section = document.createElement("div");
14872
15176
  section.className = "db-snapshot-diff-inline";
14873
- const changedTables = tables.filter((t2) => t2.insertedCount + t2.updatedCount + t2.deletedCount > 0);
15177
+ const beforeMeta = snapshots.find((s2) => s2.id === beforeId);
15178
+ const afterMeta = snapshots.find((s2) => s2.id === afterId);
15179
+ if (beforeMeta && afterMeta) {
15180
+ const header = document.createElement("div");
15181
+ header.className = "db-snapshot-diff-header";
15182
+ const beforeLabel = new Date(beforeMeta.createdAt).toLocaleString();
15183
+ const afterLabel = new Date(afterMeta.createdAt).toLocaleString();
15184
+ header.textContent = text2().snapshot.diffShowing(beforeLabel, afterLabel);
15185
+ section.appendChild(header);
15186
+ }
15187
+ const changedTables = tables.filter((t2) => t2.insertedCount + t2.updatedCount + t2.deletedCount > 0 || t2.coverage && t2.coverage !== "both");
14874
15188
  const unchangedCount = tables.length - changedTables.length;
14875
15189
  const summary = document.createElement("div");
14876
15190
  summary.className = "db-snapshot-diff-summary";
14877
15191
  if (changedTables.length === 0) {
14878
15192
  summary.textContent = text2().snapshot.noChanges;
14879
15193
  section.appendChild(summary);
14880
- mainArea.appendChild(section);
15194
+ rightPane.appendChild(section);
14881
15195
  return;
14882
15196
  }
14883
15197
  summary.textContent = text2().snapshot.diffSummary(changedTables.length, unchangedCount);
14884
15198
  section.appendChild(summary);
15199
+ const toc = document.createElement("div");
15200
+ toc.className = "db-snapshot-diff-toc";
15201
+ const tocChips = new Map;
15202
+ section.appendChild(toc);
15203
+ function buildStatSpans(target, inserted, updated, deleted) {
15204
+ const make = (cls, prefix, n2) => {
15205
+ if (n2 <= 0)
15206
+ return;
15207
+ const s2 = document.createElement("span");
15208
+ s2.className = `db-snapshot-diff-stat ${cls}`;
15209
+ s2.textContent = `${prefix}${n2}`;
15210
+ target.appendChild(s2);
15211
+ };
15212
+ make("add", "+", inserted);
15213
+ make("upd", "~", updated);
15214
+ make("del", "-", deleted);
15215
+ }
15216
+ function renderCoverageOrStats(target, summary2) {
15217
+ const tx = text2().snapshot;
15218
+ if (summary2.coverage === "before-only") {
15219
+ const lbl = document.createElement("span");
15220
+ lbl.className = "db-snapshot-diff-coverage-badge before-only";
15221
+ lbl.textContent = tx.coverageBeforeOnly;
15222
+ target.appendChild(lbl);
15223
+ } else if (summary2.coverage === "after-only") {
15224
+ const lbl = document.createElement("span");
15225
+ lbl.className = "db-snapshot-diff-coverage-badge after-only";
15226
+ lbl.textContent = tx.coverageAfterOnly;
15227
+ target.appendChild(lbl);
15228
+ } else {
15229
+ buildStatSpans(target, summary2.insertedCount, summary2.updatedCount, summary2.deletedCount);
15230
+ }
15231
+ }
15232
+ const pendingLoads = [];
15233
+ let scrollTarget = null;
14885
15234
  for (const t2 of changedTables) {
14886
15235
  const tableEl = document.createElement("div");
14887
15236
  tableEl.className = "db-snapshot-diff-table";
15237
+ const sectionId = `diff-table-${t2.tableName.replace(/[^a-zA-Z0-9_]/g, "_")}`;
15238
+ tableEl.id = sectionId;
14888
15239
  const tableHeader = document.createElement("div");
14889
15240
  tableHeader.className = "db-snapshot-diff-table-header";
14890
15241
  const nameSpan = document.createElement("span");
@@ -14892,26 +15243,82 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14892
15243
  nameSpan.textContent = t2.tableName;
14893
15244
  const statsSpan = document.createElement("span");
14894
15245
  statsSpan.className = "db-snapshot-diff-table-stats";
14895
- const parts = [];
14896
- if (t2.insertedCount > 0)
14897
- parts.push(`+${t2.insertedCount}`);
14898
- if (t2.updatedCount > 0)
14899
- parts.push(`~${t2.updatedCount}`);
14900
- if (t2.deletedCount > 0)
14901
- parts.push(`-${t2.deletedCount}`);
14902
- statsSpan.textContent = parts.join(" ");
15246
+ renderCoverageOrStats(statsSpan, t2);
14903
15247
  tableHeader.append(nameSpan, statsSpan);
14904
15248
  tableHeader.style.cursor = "pointer";
14905
15249
  const rowsContainer = document.createElement("div");
14906
15250
  rowsContainer.className = "db-snapshot-diff-rows-container";
14907
- loadDiffRows(beforeId, afterId, t2.tableName, rowsContainer);
15251
+ if (t2.coverage === "before-only" || t2.coverage === "after-only") {
15252
+ const tx = text2().snapshot;
15253
+ const note = document.createElement("div");
15254
+ note.className = "db-snapshot-diff-coverage-note";
15255
+ const rows = t2.unsnapshottedRowCount ?? 0;
15256
+ note.textContent = t2.coverage === "before-only" ? tx.coverageNoteBeforeOnly(rows) : tx.coverageNoteAfterOnly(rows);
15257
+ rowsContainer.appendChild(note);
15258
+ } else {
15259
+ pendingLoads.push(loadDiffRows(beforeId, afterId, t2.tableName, rowsContainer));
15260
+ }
14908
15261
  tableHeader.addEventListener("click", () => {
14909
15262
  rowsContainer.hidden = !rowsContainer.hidden;
14910
15263
  });
14911
15264
  tableEl.append(tableHeader, rowsContainer);
14912
15265
  section.appendChild(tableEl);
15266
+ const chip = document.createElement("button");
15267
+ chip.type = "button";
15268
+ chip.className = "db-snapshot-diff-toc-chip";
15269
+ chip.dataset.targetId = sectionId;
15270
+ const chipName = document.createElement("span");
15271
+ chipName.className = "db-snapshot-diff-toc-chip-name";
15272
+ chipName.textContent = t2.tableName;
15273
+ const chipStats = document.createElement("span");
15274
+ chipStats.className = "db-snapshot-diff-toc-chip-stats";
15275
+ renderCoverageOrStats(chipStats, t2);
15276
+ if (t2.coverage === "before-only" || t2.coverage === "after-only") {
15277
+ chip.classList.add("coverage-incomplete");
15278
+ }
15279
+ chip.append(chipName, chipStats);
15280
+ chip.addEventListener("click", () => {
15281
+ scrollTarget = tableEl;
15282
+ tableEl.scrollIntoView({ behavior: "smooth", block: "start" });
15283
+ });
15284
+ toc.appendChild(chip);
15285
+ tocChips.set(sectionId, chip);
15286
+ }
15287
+ Promise.allSettled(pendingLoads).then(() => {
15288
+ if (scrollTarget && rightPane.contains(scrollTarget)) {
15289
+ scrollTarget.scrollIntoView({ behavior: "smooth", block: "start" });
15290
+ }
15291
+ });
15292
+ const observer = new IntersectionObserver((entries) => {
15293
+ const intersecting = entries.filter((e2) => e2.isIntersecting);
15294
+ if (intersecting.length === 0)
15295
+ return;
15296
+ intersecting.sort((a2, b2) => a2.boundingClientRect.top - b2.boundingClientRect.top);
15297
+ const activeId = intersecting[0].target.id;
15298
+ for (const [id, chip] of tocChips) {
15299
+ chip.classList.toggle("active", id === activeId);
15300
+ }
15301
+ const activeChip = tocChips.get(activeId);
15302
+ if (activeChip) {
15303
+ const tocBox = toc.getBoundingClientRect();
15304
+ const chipBox = activeChip.getBoundingClientRect();
15305
+ const margin = 16;
15306
+ if (chipBox.left < tocBox.left + margin) {
15307
+ toc.scrollLeft += chipBox.left - tocBox.left - margin;
15308
+ } else if (chipBox.right > tocBox.right - margin) {
15309
+ toc.scrollLeft += chipBox.right - tocBox.right + margin;
15310
+ }
15311
+ }
15312
+ }, {
15313
+ root: rightPane,
15314
+ rootMargin: "-48px 0px -60% 0px",
15315
+ threshold: 0
15316
+ });
15317
+ for (const el2 of section.querySelectorAll(".db-snapshot-diff-table")) {
15318
+ observer.observe(el2);
14913
15319
  }
14914
- mainArea.appendChild(section);
15320
+ section.__tocObserver = observer;
15321
+ rightPane.appendChild(section);
14915
15322
  }
14916
15323
  async function loadDiffRows(beforeId, afterId, table2, container) {
14917
15324
  container.innerHTML = "";
@@ -14920,7 +15327,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14920
15327
  loading.textContent = text2().snapshot.loading;
14921
15328
  container.appendChild(loading);
14922
15329
  try {
14923
- const res = await fetch(`/_db/snapshot/diff/rows?before=${encodeURIComponent(beforeId)}&after=${encodeURIComponent(afterId)}&table=${encodeURIComponent(table2)}&limit=200`);
15330
+ const res = await getJson(`/_db/snapshot/diff/rows?before=${encodeURIComponent(beforeId)}&after=${encodeURIComponent(afterId)}&table=${encodeURIComponent(table2)}&limit=200`);
14924
15331
  if (!res.ok) {
14925
15332
  container.innerHTML = "";
14926
15333
  const error2 = document.createElement("div");
@@ -15069,7 +15476,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15069
15476
  if (item) {
15070
15477
  item.after(dialog);
15071
15478
  } else {
15072
- mainArea.prepend(dialog);
15479
+ leftPane.prepend(dialog);
15073
15480
  }
15074
15481
  input.focus();
15075
15482
  }
@@ -15082,6 +15489,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15082
15489
  refresh();
15083
15490
  }
15084
15491
  function handleSse(data) {
15492
+ if (disposed)
15493
+ return;
15085
15494
  try {
15086
15495
  const parsed = JSON.parse(data);
15087
15496
  const dbId = deps.getDbId();
@@ -15090,21 +15499,57 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15090
15499
  return;
15091
15500
  if (parsed.schema && parsed.schema !== schema)
15092
15501
  return;
15093
- if (parsed.action === "started" && parsed.id) {
15094
- setActiveSnapshotId(parsed.id);
15502
+ const id = parsed.id ?? null;
15503
+ if (parsed.action === "started" && id) {
15504
+ upsertJob(id, {
15505
+ done: false,
15506
+ total: parsed.total ?? 0,
15507
+ index: 0
15508
+ });
15509
+ }
15510
+ if (parsed.action === "progress" && id) {
15511
+ upsertJob(id, {
15512
+ table: parsed.table,
15513
+ index: parsed.index,
15514
+ total: parsed.total ?? 0,
15515
+ done: parsed.done === true
15516
+ });
15095
15517
  }
15096
- if (parsed.action === "created" || parsed.action === "error") {
15097
- setActiveSnapshotId(null);
15518
+ if (parsed.action === "created" && id) {
15519
+ upsertJob(id, { done: true });
15520
+ scheduleJobRemove(id);
15098
15521
  refreshAndAutoDiff();
15099
15522
  }
15523
+ if (parsed.action === "aborted" && id) {
15524
+ upsertJob(id, { done: true, aborted: true });
15525
+ scheduleJobRemove(id);
15526
+ refresh();
15527
+ }
15528
+ if (parsed.action === "error" && id) {
15529
+ upsertJob(id, {
15530
+ done: true,
15531
+ errorMessage: parsed.error || text2().snapshot.failed
15532
+ });
15533
+ scheduleJobRemove(id);
15534
+ refresh();
15535
+ }
15100
15536
  } catch {}
15101
15537
  }
15102
15538
  function dispose() {
15103
15539
  disposed = true;
15104
- setActiveSnapshotId(null);
15105
15540
  for (const timer of autoRefreshTimers)
15106
15541
  clearTimeout(timer);
15107
15542
  autoRefreshTimers.clear();
15543
+ for (const timer of jobClearTimers.values())
15544
+ clearTimeout(timer);
15545
+ jobClearTimers.clear();
15546
+ activeJobs.clear();
15547
+ cancellingJobs.clear();
15548
+ const existing = rightPane.querySelector(".db-snapshot-diff-inline");
15549
+ if (existing) {
15550
+ const prev = existing;
15551
+ prev.__tocObserver?.disconnect();
15552
+ }
15108
15553
  }
15109
15554
  function localize() {
15110
15555
  const t2 = text2().snapshot;
@@ -15112,8 +15557,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15112
15557
  guideBody.textContent = t2.guideBody;
15113
15558
  createBtn.textContent = t2.create;
15114
15559
  refreshBtn.textContent = t2.refresh;
15115
- if (!activeSnapshotId)
15116
- snapshotCancelBtn.textContent = t2.cancel;
15560
+ renderProgress();
15117
15561
  tableSelectorHeader.textContent = t2.selectTablesHeader;
15118
15562
  selectAllBtn.textContent = t2.selectAll;
15119
15563
  deselectAllBtn.textContent = t2.deselectAll;
@@ -15122,21 +15566,54 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15122
15566
  if (!confirmBtn.disabled)
15123
15567
  confirmBtn.textContent = t2.confirm;
15124
15568
  cancelBtn.textContent = t2.cancel;
15125
- const prevSelects = mainArea.querySelectorAll(".db-snapshot-diff-select");
15569
+ const prevSelects = leftPane.querySelectorAll(".db-snapshot-diff-select");
15126
15570
  const prevBeforeVal = prevSelects[0]?.value;
15127
15571
  const prevAfterVal = prevSelects[1]?.value;
15128
- const hadDiff = mainArea.querySelector(".db-snapshot-diff-inline") != null;
15572
+ const hadDiff = rightPane.querySelector(".db-snapshot-diff-inline") != null;
15129
15573
  renderMain();
15130
- const newSelects = mainArea.querySelectorAll(".db-snapshot-diff-select");
15131
- if (newSelects[0] && prevBeforeVal)
15574
+ const newSelects = leftPane.querySelectorAll(".db-snapshot-diff-select");
15575
+ if (newSelects[0] && prevBeforeVal) {
15132
15576
  newSelects[0].value = prevBeforeVal;
15133
- if (newSelects[1] && prevAfterVal)
15577
+ newSelects[0].dispatchEvent(new Event("change"));
15578
+ }
15579
+ if (newSelects[1] && prevAfterVal) {
15134
15580
  newSelects[1].value = prevAfterVal;
15581
+ newSelects[1].dispatchEvent(new Event("change"));
15582
+ }
15135
15583
  if (hadDiff && currentDiff) {
15136
15584
  showDiffInline(currentDiff.beforeId, currentDiff.afterId);
15137
15585
  }
15138
15586
  }
15139
- return { el, refresh, handleSse, dispose, localize };
15587
+ async function restoreDiffFromRoute() {
15588
+ const target = deps.getRouteDiff?.();
15589
+ if (!target || !target.before || !target.after)
15590
+ return;
15591
+ if (currentDiff && currentDiff.beforeId === target.before && currentDiff.afterId === target.after) {
15592
+ return;
15593
+ }
15594
+ if (snapshots.length === 0) {
15595
+ await refresh();
15596
+ }
15597
+ if (disposed)
15598
+ return;
15599
+ const hasBefore = snapshots.some((s2) => s2.id === target.before);
15600
+ const hasAfter = snapshots.some((s2) => s2.id === target.after);
15601
+ if (!hasBefore || !hasAfter) {
15602
+ deps.setRouteDiff?.(null);
15603
+ return;
15604
+ }
15605
+ await showDiffInline(target.before, target.after);
15606
+ }
15607
+ return {
15608
+ el,
15609
+ refresh,
15610
+ handleSse,
15611
+ dispose,
15612
+ localize,
15613
+ restoreDiffFromRoute: () => {
15614
+ restoreDiffFromRoute();
15615
+ }
15616
+ };
15140
15617
  }
15141
15618
 
15142
15619
  // web-src/views/database/table-grid.ts
@@ -17377,6 +17854,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
17377
17854
  onClick: () => {
17378
17855
  setActiveTab("snapshot");
17379
17856
  snapshotView.refresh();
17857
+ snapshotView.restoreDiffFromRoute();
17380
17858
  }
17381
17859
  });
17382
17860
  const disposeInferFkPrefSub = outerDeps.onDbUiPrefChange(() => {
@@ -17481,7 +17959,45 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
17481
17959
  getDbId: () => currentDbInfo?.id || null,
17482
17960
  getSchema: () => currentSchema,
17483
17961
  getTables: () => schemaCache?.tables || [],
17484
- getText: () => paneText()
17962
+ getText: () => paneText(),
17963
+ trackLoad: deps.trackLoad,
17964
+ getStoredSelectedTables: () => {
17965
+ if (!currentDbInfo)
17966
+ return;
17967
+ const scope = dbUiTableExpansionScope(currentDbInfo.id, currentSchema);
17968
+ return outerDeps.getSnapshotSelectedTables(scope);
17969
+ },
17970
+ setStoredSelectedTables: (tables) => {
17971
+ if (!currentDbInfo)
17972
+ return;
17973
+ const scope = dbUiTableExpansionScope(currentDbInfo.id, currentSchema);
17974
+ outerDeps.setSnapshotSelectedTables(scope, tables);
17975
+ },
17976
+ getRouteDiff: () => {
17977
+ const parsed = parseRoute(window.location.pathname, window.location.search, deps.currentRange());
17978
+ if (parsed.screen !== "database")
17979
+ return;
17980
+ const before = parsed.diffBefore;
17981
+ const after = parsed.diffAfter;
17982
+ if (!before && !after)
17983
+ return;
17984
+ return { before, after };
17985
+ },
17986
+ setRouteDiff: (diff) => {
17987
+ const dbId = currentDbInfo?.id;
17988
+ if (!dbId)
17989
+ return;
17990
+ deps.setRoute({
17991
+ screen: "database",
17992
+ db: dbId,
17993
+ schema: currentSchema ?? undefined,
17994
+ table: currentTable ?? undefined,
17995
+ tab: "snapshot",
17996
+ ...diff?.before ? { diffBefore: diff.before } : {},
17997
+ ...diff?.after ? { diffAfter: diff.after } : {},
17998
+ range: deps.currentRange()
17999
+ }, true);
18000
+ }
17485
18001
  });
17486
18002
  const historyView = createQueryHistoryView({
17487
18003
  getDbId: () => currentDbInfo?.id || null,
@@ -18337,6 +18853,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18337
18853
  renderErDiagram();
18338
18854
  } else if (normalizedView === "snapshot") {
18339
18855
  snapshotView.refresh();
18856
+ snapshotView.restoreDiffFromRoute();
18340
18857
  }
18341
18858
  }
18342
18859
  applyAnnotationTarget(options.annotationTarget);
@@ -18610,33 +19127,27 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18610
19127
  return;
18611
19128
  });
18612
19129
  }
18613
- function getExpandedTables(scopeKey) {
18614
- return [...dbUiState.expandedTables?.[scopeKey] || []];
18615
- }
18616
- function setExpandedTable(scopeKey, table2, expanded) {
18617
- const expandedTables = { ...dbUiState.expandedTables || {} };
18618
- const tables = new Set(expandedTables[scopeKey] || []);
18619
- if (expanded)
18620
- tables.add(table2);
18621
- else
18622
- tables.delete(table2);
19130
+ function updateScopedTableList(stateKey, scopeKey, mutate) {
19131
+ const current = { ...dbUiState[stateKey] || {} };
19132
+ const tables = new Set(current[scopeKey] || []);
19133
+ mutate(tables);
18623
19134
  const nextTables = [...tables].sort((a2, b2) => a2.localeCompare(b2));
18624
19135
  if (nextTables.length > 0)
18625
- expandedTables[scopeKey] = nextTables;
19136
+ current[scopeKey] = nextTables;
18626
19137
  else
18627
- delete expandedTables[scopeKey];
18628
- const nextExpanded = Object.keys(expandedTables).length > 0 ? expandedTables : undefined;
19138
+ delete current[scopeKey];
19139
+ const nextMap = Object.keys(current).length > 0 ? current : undefined;
18629
19140
  const nextState = { ...dbUiState, version: 1 };
18630
- if (nextExpanded)
18631
- nextState.expandedTables = nextExpanded;
19141
+ if (nextMap)
19142
+ nextState[stateKey] = nextMap;
18632
19143
  else
18633
- delete nextState.expandedTables;
19144
+ delete nextState[stateKey];
18634
19145
  applyDbUiState(nextState);
18635
19146
  fetch("/_db/ui", {
18636
19147
  method: "PATCH",
18637
19148
  headers: actionHeaders(),
18638
19149
  body: JSON.stringify({
18639
- expandedTables: {
19150
+ [stateKey]: {
18640
19151
  [scopeKey]: nextTables.length > 0 ? nextTables : null
18641
19152
  }
18642
19153
  })
@@ -18644,6 +19155,27 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18644
19155
  return;
18645
19156
  });
18646
19157
  }
19158
+ function getExpandedTables(scopeKey) {
19159
+ return [...dbUiState.expandedTables?.[scopeKey] || []];
19160
+ }
19161
+ function setExpandedTable(scopeKey, table2, expanded) {
19162
+ updateScopedTableList("expandedTables", scopeKey, (tables) => {
19163
+ if (expanded)
19164
+ tables.add(table2);
19165
+ else
19166
+ tables.delete(table2);
19167
+ });
19168
+ }
19169
+ function getSnapshotSelectedTables(scopeKey) {
19170
+ return [...dbUiState.snapshotSelectedTables?.[scopeKey] || []];
19171
+ }
19172
+ function setSnapshotSelectedTables(scopeKey, tables) {
19173
+ updateScopedTableList("snapshotSelectedTables", scopeKey, (set2) => {
19174
+ set2.clear();
19175
+ for (const t2 of tables)
19176
+ set2.add(t2);
19177
+ });
19178
+ }
18647
19179
  const dbUiPrefListeners = new Set;
18648
19180
  function applyDbUiState(next) {
18649
19181
  dbUiState = next;
@@ -18865,12 +19397,22 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18865
19397
  if (!entry)
18866
19398
  return;
18867
19399
  const s2 = entry.pane.getState();
19400
+ const isSnapshot = s2.view === "snapshot";
19401
+ let diffBefore;
19402
+ let diffAfter;
19403
+ if (isSnapshot) {
19404
+ const url = new URL(window.location.href);
19405
+ diffBefore = url.searchParams.get("diffBefore") ?? undefined;
19406
+ diffAfter = url.searchParams.get("diffAfter") ?? undefined;
19407
+ }
18868
19408
  deps.setRoute({
18869
19409
  screen: "database",
18870
19410
  db: s2.dbId ?? undefined,
18871
19411
  schema: s2.schema ?? undefined,
18872
19412
  table: s2.table ?? undefined,
18873
19413
  tab: s2.view === "data" ? undefined : s2.view,
19414
+ ...diffBefore ? { diffBefore } : {},
19415
+ ...diffAfter ? { diffAfter } : {},
18874
19416
  range: deps.currentRange()
18875
19417
  }, true);
18876
19418
  }
@@ -19120,6 +19662,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
19120
19662
  setColumnWidths,
19121
19663
  getExpandedTables,
19122
19664
  setExpandedTable,
19665
+ getSnapshotSelectedTables,
19666
+ setSnapshotSelectedTables,
19123
19667
  getDbUiPref,
19124
19668
  setDbUiPref,
19125
19669
  onDbUiPrefChange,