@youtyan/code-viewer 0.5.3 → 0.5.5

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
  }
@@ -9629,20 +9637,33 @@ ${frontmatter.yaml}
9629
9637
  calculatingDiff: "Computing diff…",
9630
9638
  diffError: "Failed to compute diff",
9631
9639
  noChanges: "No changes detected.",
9632
- before: "Before",
9633
9640
  loading: "Loading…",
9634
9641
  loadError: "Failed to load",
9635
9642
  saveNote: "Save",
9636
9643
  noteInputPlaceholder: "Enter a note",
9637
- inserted: "Added",
9638
- updated: "Updated",
9639
- deleted: "Deleted",
9640
9644
  tableCount: (n2) => `${n2} tables`,
9641
9645
  rowCount: (n2) => ` (${n2})`,
9642
9646
  listTitle: (n2) => `Snapshots (${n2})`,
9643
9647
  diffSummary: (changed, unchanged) => `${changed} tables changed` + (unchanged > 0 ? `, ${unchanged} unchanged` : ""),
9644
9648
  showingRows: (shown, total) => `Showing ${shown} of ${total}`,
9645
- label: (date, tables, note) => `${date} (${tables} tables)${note}`
9649
+ label: (date, tables, note) => `${date} (${tables} tables)${note}`,
9650
+ statusRunning: "running",
9651
+ statusError: "error",
9652
+ statusDone: "done",
9653
+ progressLabel: (table2, index, total) => table2 ? `Snapshotting ${table2} (${index + 1} / ${total})` : `Finalizing… (${total} / ${total})`,
9654
+ progressFinalizing: "Finalizing…",
9655
+ aborted: "cancelled",
9656
+ failed: "Failed",
9657
+ errorPrefix: "Error",
9658
+ compareBeforeBadge: "before",
9659
+ compareAfterBadge: "after",
9660
+ diffShowing: (before, after) => `Showing: ${before} → ${after}`,
9661
+ filterPlaceholder: "Filter tables…",
9662
+ tableCounter: (selected, total) => `${selected} / ${total} selected`,
9663
+ coverageBeforeOnly: "not snapshotted in after",
9664
+ coverageAfterOnly: "not snapshotted in before",
9665
+ coverageNoteBeforeOnly: (rows) => `Only in the before snapshot (${rows} rows). Not selected for the after snapshot, so no comparison.`,
9666
+ coverageNoteAfterOnly: (rows) => `Only in the after snapshot (${rows} rows). Not selected for the before snapshot, so no comparison.`
9646
9667
  },
9647
9668
  explorer: {
9648
9669
  common: {
@@ -9901,20 +9922,33 @@ ${frontmatter.yaml}
9901
9922
  calculatingDiff: "差分を計算中...",
9902
9923
  diffError: "差分の計算に失敗しました",
9903
9924
  noChanges: "変更は検出されませんでした。",
9904
- before: "更新前",
9905
9925
  loading: "読み込み中...",
9906
9926
  loadError: "読み込みに失敗しました",
9907
9927
  saveNote: "保存",
9908
9928
  noteInputPlaceholder: "メモを入力",
9909
- inserted: "追加",
9910
- updated: "更新",
9911
- deleted: "削除",
9912
9929
  tableCount: (n2) => `${n2}テーブル`,
9913
9930
  rowCount: (n2) => ` (${n2}件)`,
9914
9931
  listTitle: (n2) => `スナップショット (${n2}件)`,
9915
9932
  diffSummary: (changed, unchanged) => `${changed}テーブルに変更あり` + (unchanged > 0 ? `、${unchanged}テーブルは変更なし` : ""),
9916
9933
  showingRows: (shown, total) => `全${total}件中 ${shown}件を表示中`,
9917
- label: (date, tables, note) => `${date} (${tables}テーブル)${note}`
9934
+ label: (date, tables, note) => `${date} (${tables}テーブル)${note}`,
9935
+ statusRunning: "取得中",
9936
+ statusError: "エラー",
9937
+ statusDone: "完了",
9938
+ progressLabel: (table2, index, total) => table2 ? `スナップショット取得中: ${table2} (${index + 1} / ${total})` : `仕上げ中… (${total} / ${total})`,
9939
+ progressFinalizing: "仕上げ中…",
9940
+ aborted: "キャンセル",
9941
+ failed: "失敗",
9942
+ errorPrefix: "エラー",
9943
+ compareBeforeBadge: "比較元",
9944
+ compareAfterBadge: "比較先",
9945
+ diffShowing: (before, after) => `比較中: ${before} → ${after}`,
9946
+ filterPlaceholder: "テーブル名で絞り込み…",
9947
+ tableCounter: (selected, total) => `${selected} / ${total} 件選択中`,
9948
+ coverageBeforeOnly: "比較先で未取得",
9949
+ coverageAfterOnly: "比較元で未取得",
9950
+ coverageNoteBeforeOnly: (rows) => `比較元のみに存在 (${rows}行)。比較先では対象テーブルに選ばれていないため比較できません。`,
9951
+ coverageNoteAfterOnly: (rows) => `比較先のみに存在 (${rows}行)。比較元では対象テーブルに選ばれていないため比較できません。`
9918
9952
  },
9919
9953
  explorer: {
9920
9954
  common: {
@@ -14495,24 +14529,22 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14495
14529
  const sb = [...b2].sort();
14496
14530
  return sa.every((v, i2) => v === sb[i2]);
14497
14531
  }
14498
- function postJson(path, body) {
14499
- return fetch(path, {
14532
+ function createSnapshotView(deps) {
14533
+ const text2 = () => deps.getText?.() ?? dbText("en");
14534
+ const trackLoad = (p2) => deps.trackLoad ? deps.trackLoad(p2) : p2;
14535
+ const postJson = (path, body) => trackLoad(fetch(path, {
14500
14536
  method: "POST",
14501
14537
  headers: {
14502
14538
  "Content-Type": "application/json",
14503
14539
  "X-Code-Viewer-Action": "1"
14504
14540
  },
14505
14541
  body: JSON.stringify(body)
14506
- });
14507
- }
14508
- function createSnapshotView(deps) {
14509
- const text2 = () => deps.getText?.() ?? dbText("en");
14542
+ }));
14543
+ const getJson = (path) => trackLoad(fetch(path));
14510
14544
  const changeTypeLabel = (type) => {
14511
14545
  if (type === "inserted")
14512
- return text2().snapshot.inserted;
14513
- if (type === "updated")
14514
- return text2().snapshot.updated;
14515
- return text2().snapshot.deleted;
14546
+ return "+";
14547
+ return "";
14516
14548
  };
14517
14549
  const snapshotLabel = (s2) => {
14518
14550
  const date = new Date(s2.createdAt).toLocaleString();
@@ -14540,18 +14572,27 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14540
14572
  refreshBtn.type = "button";
14541
14573
  refreshBtn.className = "db-snapshot-refresh-btn";
14542
14574
  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);
14575
+ toolbar.append(createBtn, refreshBtn);
14576
+ const progressArea = document.createElement("div");
14577
+ progressArea.className = "db-snapshot-progress-area";
14578
+ progressArea.hidden = true;
14549
14579
  const tableSelector = document.createElement("div");
14550
14580
  tableSelector.className = "db-snapshot-table-selector";
14551
14581
  tableSelector.hidden = true;
14552
14582
  const tableSelectorHeader = document.createElement("div");
14553
14583
  tableSelectorHeader.className = "db-snapshot-table-selector-header";
14554
- tableSelectorHeader.textContent = text2().snapshot.selectTablesHeader;
14584
+ const tableSelectorHeaderLabel = document.createElement("span");
14585
+ tableSelectorHeaderLabel.textContent = text2().snapshot.selectTablesHeader;
14586
+ const tableSelectorCounter = document.createElement("span");
14587
+ tableSelectorCounter.className = "db-snapshot-table-counter";
14588
+ tableSelectorHeader.append(tableSelectorHeaderLabel, tableSelectorCounter);
14589
+ const filterRow = document.createElement("div");
14590
+ filterRow.className = "db-snapshot-table-filter";
14591
+ const filterInput = document.createElement("input");
14592
+ filterInput.type = "search";
14593
+ filterInput.className = "db-snapshot-table-filter-input";
14594
+ filterInput.placeholder = text2().snapshot.filterPlaceholder;
14595
+ filterRow.appendChild(filterInput);
14555
14596
  const tableCheckboxes = document.createElement("div");
14556
14597
  tableCheckboxes.className = "db-snapshot-table-checkboxes";
14557
14598
  const selectActions = document.createElement("div");
@@ -14562,16 +14603,22 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14562
14603
  const deselectAllBtn = document.createElement("button");
14563
14604
  deselectAllBtn.type = "button";
14564
14605
  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;
14606
+ selectActions.append(selectAllBtn, deselectAllBtn);
14607
+ const noteRow = document.createElement("div");
14608
+ noteRow.className = "db-snapshot-note-row";
14569
14609
  const noteField = document.createElement("label");
14570
14610
  noteField.className = "db-snapshot-note-field";
14571
14611
  const noteLabel = document.createElement("span");
14572
14612
  noteLabel.className = "db-snapshot-note-label";
14573
14613
  noteLabel.textContent = text2().snapshot.noteLabel;
14614
+ const noteInput = document.createElement("input");
14615
+ noteInput.type = "text";
14616
+ noteInput.className = "db-snapshot-note-input";
14617
+ noteInput.placeholder = text2().snapshot.notePlaceholder;
14574
14618
  noteField.append(noteLabel, noteInput);
14619
+ noteRow.appendChild(noteField);
14620
+ const actionRow = document.createElement("div");
14621
+ actionRow.className = "db-snapshot-action-row";
14575
14622
  const confirmBtn = document.createElement("button");
14576
14623
  confirmBtn.type = "button";
14577
14624
  confirmBtn.className = "db-snapshot-confirm-btn";
@@ -14580,41 +14627,178 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14580
14627
  cancelBtn.type = "button";
14581
14628
  cancelBtn.className = "db-snapshot-cancel-btn";
14582
14629
  cancelBtn.textContent = text2().snapshot.cancel;
14583
- selectActions.append(selectAllBtn, deselectAllBtn, noteField, confirmBtn, cancelBtn);
14584
- tableSelector.append(tableSelectorHeader, tableCheckboxes, selectActions);
14630
+ actionRow.append(confirmBtn, cancelBtn);
14631
+ tableSelector.append(tableSelectorHeader, filterRow, tableCheckboxes, selectActions, noteRow, actionRow);
14585
14632
  const mainArea = document.createElement("div");
14586
14633
  mainArea.className = "db-snapshot-main-area";
14587
- el.append(guide, toolbar, tableSelector, mainArea);
14634
+ const splitContainer = document.createElement("div");
14635
+ splitContainer.className = "db-snapshot-split";
14636
+ const leftPane = document.createElement("div");
14637
+ leftPane.className = "db-snapshot-left-pane";
14638
+ const rightPane = document.createElement("div");
14639
+ rightPane.className = "db-snapshot-right-pane";
14640
+ splitContainer.append(leftPane, rightPane);
14641
+ mainArea.appendChild(splitContainer);
14642
+ el.append(guide, toolbar, progressArea, tableSelector, mainArea);
14588
14643
  let snapshots = [];
14589
- let activeSnapshotId = null;
14590
14644
  let currentDiff = null;
14591
14645
  let disposed = false;
14592
14646
  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;
14647
+ const activeJobs = new Map;
14648
+ const cancellingJobs = new Set;
14649
+ const jobClearTimers = new Map;
14650
+ function upsertJob(id, patch) {
14651
+ const prev = activeJobs.get(id) ?? { id, total: 0, done: false };
14652
+ activeJobs.set(id, { ...prev, ...patch, id });
14653
+ renderProgress();
14654
+ }
14655
+ function scheduleJobRemove(id) {
14656
+ const existing = jobClearTimers.get(id);
14657
+ if (existing)
14658
+ clearTimeout(existing);
14659
+ const t2 = setTimeout(() => {
14660
+ jobClearTimers.delete(id);
14661
+ cancellingJobs.delete(id);
14662
+ activeJobs.delete(id);
14663
+ renderProgress();
14664
+ }, 5000);
14665
+ jobClearTimers.set(id, t2);
14666
+ }
14667
+ async function cancelJob(id) {
14668
+ if (cancellingJobs.has(id))
14669
+ return;
14670
+ cancellingJobs.add(id);
14671
+ renderProgress();
14672
+ try {
14673
+ await postJson("/_db/snapshot/cancel", { id });
14674
+ } catch {}
14675
+ }
14676
+ function renderProgress() {
14677
+ progressArea.innerHTML = "";
14678
+ if (activeJobs.size === 0) {
14679
+ progressArea.hidden = true;
14680
+ return;
14681
+ }
14682
+ progressArea.hidden = false;
14683
+ const t2 = text2().snapshot;
14684
+ for (const job of activeJobs.values()) {
14685
+ const row = document.createElement("div");
14686
+ row.className = "db-snapshot-progress-row";
14687
+ row.dataset.snapshotId = job.id;
14688
+ const label = document.createElement("span");
14689
+ label.className = "db-snapshot-progress-label";
14690
+ if (job.errorMessage) {
14691
+ label.textContent = `${t2.errorPrefix}: ${job.errorMessage}`;
14692
+ label.title = job.errorMessage;
14693
+ row.classList.add("error");
14694
+ } else if (job.aborted) {
14695
+ label.textContent = t2.aborted;
14696
+ row.classList.add("aborted");
14697
+ } else if (job.done) {
14698
+ label.textContent = t2.statusDone;
14699
+ row.classList.add("done");
14700
+ } else if (job.total > 0 && job.table) {
14701
+ label.textContent = t2.progressLabel(job.table, job.index ?? 0, job.total);
14702
+ row.classList.add("running");
14703
+ } else {
14704
+ label.textContent = t2.creating;
14705
+ row.classList.add("running");
14706
+ }
14707
+ row.appendChild(label);
14708
+ if (!job.done) {
14709
+ const cancelRowBtn = document.createElement("button");
14710
+ cancelRowBtn.type = "button";
14711
+ cancelRowBtn.className = "db-snapshot-job-cancel-btn";
14712
+ if (cancellingJobs.has(job.id)) {
14713
+ cancelRowBtn.disabled = true;
14714
+ cancelRowBtn.textContent = t2.cancelling;
14715
+ } else {
14716
+ cancelRowBtn.textContent = t2.cancel;
14717
+ cancelRowBtn.addEventListener("click", () => {
14718
+ cancelJob(job.id);
14719
+ });
14720
+ }
14721
+ row.appendChild(cancelRowBtn);
14722
+ }
14723
+ progressArea.appendChild(row);
14724
+ }
14725
+ }
14726
+ let tableSelectorTables = [];
14727
+ const selectedTableSet = new Set;
14728
+ let tableFilterValue = "";
14729
+ function setTableSelectorVisible(visible) {
14730
+ tableSelector.hidden = !visible;
14731
+ createBtn.disabled = visible;
14732
+ createBtn.classList.toggle("is-active-dialog", visible);
14598
14733
  }
14599
14734
  function showTableSelector() {
14600
- const tables = deps.getTables();
14601
- const lastTables = getLastTables();
14735
+ tableSelectorTables = deps.getTables().filter((t2) => t2.type === "table");
14736
+ const currentTableNames = new Set(tableSelectorTables.map((t2) => t2.name));
14737
+ const stored = deps.getStoredSelectedTables?.();
14738
+ const rawTables = stored?.length ? stored : getLastTables();
14739
+ const lastTables = rawTables.filter((name) => currentTableNames.has(name));
14740
+ selectedTableSet.clear();
14741
+ if (lastTables.length === 0) {
14742
+ for (const t2 of tableSelectorTables)
14743
+ selectedTableSet.add(t2.name);
14744
+ } else {
14745
+ for (const name of lastTables)
14746
+ selectedTableSet.add(name);
14747
+ }
14748
+ tableFilterValue = "";
14749
+ filterInput.value = "";
14750
+ renderTableList();
14751
+ setTableSelectorVisible(true);
14752
+ noteInput.value = "";
14753
+ filterInput.focus();
14754
+ }
14755
+ function persistSelectedTables() {
14756
+ deps.setStoredSelectedTables?.(Array.from(selectedTableSet).sort());
14757
+ }
14758
+ function renderTableList() {
14759
+ const needle = tableFilterValue.trim().toLowerCase();
14602
14760
  tableCheckboxes.innerHTML = "";
14603
- for (const t2 of tables) {
14604
- if (t2.type !== "table")
14761
+ const t2 = text2().snapshot;
14762
+ let visible = 0;
14763
+ for (const tbl of tableSelectorTables) {
14764
+ if (needle && !tbl.name.toLowerCase().includes(needle))
14605
14765
  continue;
14606
- const label = document.createElement("label");
14607
- label.className = "db-snapshot-table-label";
14766
+ visible++;
14767
+ const row = document.createElement("label");
14768
+ row.className = "db-snapshot-table-row";
14608
14769
  const cb = document.createElement("input");
14609
14770
  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);
14771
+ cb.value = tbl.name;
14772
+ cb.checked = selectedTableSet.has(tbl.name);
14773
+ cb.addEventListener("change", () => {
14774
+ if (cb.checked)
14775
+ selectedTableSet.add(tbl.name);
14776
+ else
14777
+ selectedTableSet.delete(tbl.name);
14778
+ updateSelectorState();
14779
+ persistSelectedTables();
14780
+ });
14781
+ const name = document.createElement("span");
14782
+ name.className = "db-snapshot-table-row-name";
14783
+ name.textContent = tbl.name;
14784
+ const count = document.createElement("span");
14785
+ count.className = "db-snapshot-table-row-count";
14786
+ count.textContent = tbl.rowCount != null ? t2.rowCount(tbl.rowCount).trim() : "";
14787
+ row.append(cb, name, count);
14788
+ tableCheckboxes.appendChild(row);
14615
14789
  }
14616
- tableSelector.hidden = false;
14617
- noteInput.value = "";
14790
+ if (visible === 0 && needle) {
14791
+ const empty = document.createElement("div");
14792
+ empty.className = "db-snapshot-table-row-empty";
14793
+ empty.textContent = t2.empty;
14794
+ tableCheckboxes.appendChild(empty);
14795
+ }
14796
+ updateSelectorState();
14797
+ }
14798
+ function updateSelectorState() {
14799
+ const t2 = text2().snapshot;
14800
+ tableSelectorCounter.textContent = t2.tableCounter(selectedTableSet.size, tableSelectorTables.length);
14801
+ confirmBtn.disabled = selectedTableSet.size === 0;
14618
14802
  }
14619
14803
  function getLastTables() {
14620
14804
  const dbId = deps.getDbId();
@@ -14624,36 +14808,47 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14624
14808
  return done.length > 0 ? done[0].tables : [];
14625
14809
  }
14626
14810
  function getSelectedTables() {
14627
- return Array.from(tableCheckboxes.querySelectorAll('input[type="checkbox"]:checked')).map((cb) => cb.value);
14811
+ return Array.from(selectedTableSet);
14628
14812
  }
14813
+ filterInput.addEventListener("input", () => {
14814
+ tableFilterValue = filterInput.value;
14815
+ renderTableList();
14816
+ });
14817
+ filterInput.addEventListener("keydown", (e2) => {
14818
+ if (e2.key === "Escape" && filterInput.value) {
14819
+ e2.preventDefault();
14820
+ filterInput.value = "";
14821
+ tableFilterValue = "";
14822
+ renderTableList();
14823
+ }
14824
+ });
14629
14825
  selectAllBtn.addEventListener("click", () => {
14630
- tableCheckboxes.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
14631
- cb.checked = true;
14632
- });
14826
+ const needle = tableFilterValue.trim().toLowerCase();
14827
+ for (const tbl of tableSelectorTables) {
14828
+ if (needle && !tbl.name.toLowerCase().includes(needle))
14829
+ continue;
14830
+ selectedTableSet.add(tbl.name);
14831
+ }
14832
+ renderTableList();
14833
+ persistSelectedTables();
14633
14834
  });
14634
14835
  deselectAllBtn.addEventListener("click", () => {
14635
- tableCheckboxes.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
14636
- cb.checked = false;
14637
- });
14836
+ const needle = tableFilterValue.trim().toLowerCase();
14837
+ if (!needle) {
14838
+ selectedTableSet.clear();
14839
+ } else {
14840
+ for (const tbl of tableSelectorTables) {
14841
+ if (tbl.name.toLowerCase().includes(needle)) {
14842
+ selectedTableSet.delete(tbl.name);
14843
+ }
14844
+ }
14845
+ }
14846
+ renderTableList();
14847
+ persistSelectedTables();
14638
14848
  });
14639
14849
  createBtn.addEventListener("click", showTableSelector);
14640
14850
  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
- }
14851
+ setTableSelectorVisible(false);
14657
14852
  });
14658
14853
  confirmBtn.addEventListener("click", async () => {
14659
14854
  const dbId = deps.getDbId();
@@ -14672,7 +14867,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14672
14867
  tables,
14673
14868
  note: noteInput.value.trim()
14674
14869
  });
14675
- tableSelector.hidden = true;
14870
+ setTableSelectorVisible(false);
14676
14871
  scheduleAutoRefresh(dbId, schema);
14677
14872
  } catch {} finally {
14678
14873
  if (!disposed) {
@@ -14692,7 +14887,20 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14692
14887
  autoRefreshTimers.add(timer);
14693
14888
  }
14694
14889
  refreshBtn.addEventListener("click", refresh);
14890
+ let inflightRefresh = null;
14695
14891
  async function refresh() {
14892
+ if (inflightRefresh) {
14893
+ await inflightRefresh;
14894
+ return;
14895
+ }
14896
+ inflightRefresh = doRefresh();
14897
+ try {
14898
+ await inflightRefresh;
14899
+ } finally {
14900
+ inflightRefresh = null;
14901
+ }
14902
+ }
14903
+ async function doRefresh() {
14696
14904
  const dbId = deps.getDbId();
14697
14905
  const schema = deps.getSchema();
14698
14906
  if (!dbId)
@@ -14701,7 +14909,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14701
14909
  const params = new URLSearchParams({ db: dbId });
14702
14910
  if (schema)
14703
14911
  params.set("schema", schema);
14704
- const snapRes = await fetch(`/_db/snapshot/list?${params}`);
14912
+ const snapRes = await getJson(`/_db/snapshot/list?${params}`);
14705
14913
  if (snapRes.ok) {
14706
14914
  const data = await snapRes.json();
14707
14915
  if (disposed || deps.getDbId() !== dbId || deps.getSchema() !== schema) {
@@ -14712,6 +14920,18 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14712
14920
  if (disposed || deps.getDbId() !== dbId || deps.getSchema() !== schema)
14713
14921
  return;
14714
14922
  renderMain();
14923
+ if (currentDiff) {
14924
+ const stillValid = snapshots.some((s2) => s2.id === currentDiff?.beforeId) && snapshots.some((s2) => s2.id === currentDiff?.afterId);
14925
+ if (!stillValid) {
14926
+ const existing = rightPane.querySelector(".db-snapshot-diff-inline");
14927
+ if (existing) {
14928
+ const prev = existing;
14929
+ prev.__tocObserver?.disconnect();
14930
+ existing.remove();
14931
+ }
14932
+ currentDiff = null;
14933
+ }
14934
+ }
14715
14935
  } catch {}
14716
14936
  }
14717
14937
  async function refreshAndAutoDiff() {
@@ -14726,21 +14946,24 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14726
14946
  showDiffInline(prev.id, newest.id);
14727
14947
  }
14728
14948
  function renderMain() {
14729
- mainArea.innerHTML = "";
14949
+ leftPane.innerHTML = "";
14730
14950
  const done = snapshots.filter((s2) => s2.status === "done");
14731
14951
  if (snapshots.length === 0) {
14732
14952
  const empty = document.createElement("div");
14733
14953
  empty.className = "db-snapshot-empty";
14734
14954
  empty.textContent = text2().snapshot.empty;
14735
- mainArea.appendChild(empty);
14955
+ leftPane.appendChild(empty);
14736
14956
  return;
14737
14957
  }
14958
+ const stickyHeader = document.createElement("div");
14959
+ stickyHeader.className = "db-snapshot-sticky-header";
14738
14960
  const snapshotSection = document.createElement("div");
14739
14961
  snapshotSection.className = "db-snapshot-list-section";
14740
14962
  const snapTitle = document.createElement("h3");
14741
14963
  snapTitle.className = "db-snapshot-section-title";
14742
- snapTitle.textContent = text2().snapshot.listTitle(done.length);
14964
+ snapTitle.textContent = text2().snapshot.listTitle(snapshots.length);
14743
14965
  snapshotSection.appendChild(snapTitle);
14966
+ const t2 = text2().snapshot;
14744
14967
  for (const snap of snapshots) {
14745
14968
  const item = document.createElement("div");
14746
14969
  item.className = "db-snapshot-item";
@@ -14750,17 +14973,27 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14750
14973
  const date = document.createElement("span");
14751
14974
  date.className = "db-snapshot-date";
14752
14975
  date.textContent = new Date(snap.createdAt).toLocaleString();
14976
+ const status = document.createElement("span");
14977
+ status.className = `db-snapshot-status ${snap.status}`;
14978
+ status.textContent = snap.status === "running" ? t2.statusRunning : snap.status === "error" ? t2.statusError : t2.statusDone;
14753
14979
  const tables = document.createElement("span");
14754
14980
  tables.className = "db-snapshot-tables-count";
14755
14981
  tables.title = snap.tables.join(", ");
14756
- tables.textContent = text2().snapshot.tableCount(snap.tables.length);
14757
- info.append(date, tables);
14982
+ tables.textContent = t2.tableCount(snap.tables.length);
14983
+ info.append(date, status, tables);
14758
14984
  if (snap.note) {
14759
14985
  const note = document.createElement("span");
14760
14986
  note.className = "db-snapshot-note";
14761
14987
  note.textContent = snap.note;
14762
14988
  info.appendChild(note);
14763
14989
  }
14990
+ if (snap.status === "error" && snap.errorMessage) {
14991
+ const errMsg = document.createElement("span");
14992
+ errMsg.className = "db-snapshot-error-message";
14993
+ errMsg.textContent = `${t2.errorPrefix}: ${snap.errorMessage}`;
14994
+ errMsg.title = snap.errorMessage;
14995
+ info.appendChild(errMsg);
14996
+ }
14764
14997
  const actions = document.createElement("div");
14765
14998
  actions.className = "db-snapshot-actions";
14766
14999
  const noteBtn = document.createElement("button");
@@ -14797,7 +15030,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14797
15030
  beforeSelect.className = "db-snapshot-diff-select";
14798
15031
  const afterSelect = document.createElement("select");
14799
15032
  afterSelect.className = "db-snapshot-diff-select";
14800
- const sorted = [...done].sort((a2, b2) => new Date(a2.createdAt).getTime() - new Date(b2.createdAt).getTime());
15033
+ const sorted = [...done].sort((a2, b2) => new Date(b2.createdAt).getTime() - new Date(a2.createdAt).getTime());
14801
15034
  for (const s2 of sorted) {
14802
15035
  const optB = document.createElement("option");
14803
15036
  optB.value = s2.id;
@@ -14809,8 +15042,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14809
15042
  afterSelect.appendChild(optA);
14810
15043
  }
14811
15044
  if (sorted.length >= 2) {
14812
- beforeSelect.selectedIndex = sorted.length - 2;
14813
- afterSelect.selectedIndex = sorted.length - 1;
15045
+ beforeSelect.selectedIndex = 1;
15046
+ afterSelect.selectedIndex = 0;
14814
15047
  }
14815
15048
  const diffBtn = document.createElement("button");
14816
15049
  diffBtn.type = "button";
@@ -14820,32 +15053,89 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14820
15053
  diffBtn.disabled = true;
14821
15054
  diffBtn.textContent = text2().snapshot.comparing;
14822
15055
  try {
14823
- showDiffInline(beforeSelect.value, afterSelect.value);
15056
+ await showDiffInline(beforeSelect.value, afterSelect.value);
15057
+ scrollDiffIntoView();
14824
15058
  } finally {
14825
15059
  diffBtn.disabled = false;
14826
15060
  diffBtn.textContent = text2().snapshot.diffManual;
14827
15061
  }
14828
15062
  });
14829
- diffCreate.append(beforeSelect, document.createTextNode(" "), afterSelect, diffBtn);
14830
- snapshotSection.appendChild(diffCreate);
14831
- }
14832
- mainArea.appendChild(snapshotSection);
15063
+ const applyDiffSelectionHighlight = () => {
15064
+ const items = snapshotSection.querySelectorAll(".db-snapshot-item");
15065
+ const before = beforeSelect.value;
15066
+ const after = afterSelect.value;
15067
+ for (const it of items) {
15068
+ it.classList.remove("is-before", "is-after");
15069
+ const old = it.querySelector(".db-snapshot-compare-badge");
15070
+ if (old)
15071
+ old.remove();
15072
+ }
15073
+ for (const it of items) {
15074
+ const id = it.dataset.snapshotId ?? "";
15075
+ let label = null;
15076
+ if (id === before) {
15077
+ it.classList.add("is-before");
15078
+ label = t2.compareBeforeBadge;
15079
+ } else if (id === after) {
15080
+ it.classList.add("is-after");
15081
+ label = t2.compareAfterBadge;
15082
+ }
15083
+ if (label) {
15084
+ const badge = document.createElement("span");
15085
+ badge.className = "db-snapshot-compare-badge";
15086
+ badge.textContent = label;
15087
+ const info = it.querySelector(".db-snapshot-info");
15088
+ if (info)
15089
+ info.appendChild(badge);
15090
+ }
15091
+ }
15092
+ };
15093
+ beforeSelect.addEventListener("change", applyDiffSelectionHighlight);
15094
+ afterSelect.addEventListener("change", applyDiffSelectionHighlight);
15095
+ applyDiffSelectionHighlight();
15096
+ const beforeRow = document.createElement("label");
15097
+ beforeRow.className = "db-snapshot-diff-row is-before";
15098
+ const beforeLabel = document.createElement("span");
15099
+ beforeLabel.className = "db-snapshot-diff-row-label";
15100
+ beforeLabel.textContent = t2.compareBeforeBadge;
15101
+ beforeRow.append(beforeLabel, beforeSelect);
15102
+ const afterRow = document.createElement("label");
15103
+ afterRow.className = "db-snapshot-diff-row is-after";
15104
+ const afterLabel = document.createElement("span");
15105
+ afterLabel.className = "db-snapshot-diff-row-label";
15106
+ afterLabel.textContent = t2.compareAfterBadge;
15107
+ afterRow.append(afterLabel, afterSelect);
15108
+ diffCreate.append(beforeRow, afterRow, diffBtn);
15109
+ stickyHeader.appendChild(diffCreate);
15110
+ }
15111
+ leftPane.append(stickyHeader, snapshotSection);
15112
+ }
15113
+ function scrollDiffIntoView() {
15114
+ const section = rightPane.querySelector(".db-snapshot-diff-inline");
15115
+ if (!section)
15116
+ return;
15117
+ section.scrollIntoView({ behavior: "smooth", block: "start" });
14833
15118
  }
14834
15119
  async function showDiffInline(beforeId, afterId) {
14835
15120
  currentDiff = { beforeId, afterId };
14836
- const existing = mainArea.querySelector(".db-snapshot-diff-inline");
14837
- if (existing)
15121
+ deps.setRouteDiff?.({ before: beforeId, after: afterId });
15122
+ const existing = rightPane.querySelector(".db-snapshot-diff-inline");
15123
+ if (existing) {
15124
+ const prev = existing;
15125
+ prev.__tocObserver?.disconnect();
14838
15126
  existing.remove();
15127
+ }
14839
15128
  const loading = document.createElement("div");
14840
15129
  loading.className = "db-snapshot-diff-inline";
14841
15130
  const loadingText = document.createElement("div");
14842
15131
  loadingText.className = "db-snapshot-loading";
14843
15132
  loadingText.textContent = text2().snapshot.calculatingDiff;
14844
15133
  loading.appendChild(loadingText);
14845
- mainArea.appendChild(loading);
15134
+ rightPane.appendChild(loading);
14846
15135
  try {
14847
- const res = await fetch(`/_db/snapshot/diff/tables?before=${encodeURIComponent(beforeId)}&after=${encodeURIComponent(afterId)}`);
15136
+ const res = await getJson(`/_db/snapshot/diff/tables?before=${encodeURIComponent(beforeId)}&after=${encodeURIComponent(afterId)}`);
14848
15137
  if (!res.ok) {
15138
+ currentDiff = null;
14849
15139
  loading.innerHTML = "";
14850
15140
  const error2 = document.createElement("div");
14851
15141
  error2.className = "db-snapshot-error";
@@ -14857,6 +15147,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14857
15147
  loading.remove();
14858
15148
  renderDiffInline(beforeId, afterId, data.tables);
14859
15149
  } catch {
15150
+ currentDiff = null;
14860
15151
  loading.innerHTML = "";
14861
15152
  const error2 = document.createElement("div");
14862
15153
  error2.className = "db-snapshot-error";
@@ -14865,26 +15156,76 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14865
15156
  }
14866
15157
  }
14867
15158
  function renderDiffInline(beforeId, afterId, tables) {
14868
- const existing = mainArea.querySelector(".db-snapshot-diff-inline");
14869
- if (existing)
15159
+ const existing = rightPane.querySelector(".db-snapshot-diff-inline");
15160
+ if (existing) {
15161
+ const prev = existing;
15162
+ prev.__tocObserver?.disconnect();
14870
15163
  existing.remove();
15164
+ }
14871
15165
  const section = document.createElement("div");
14872
15166
  section.className = "db-snapshot-diff-inline";
14873
- const changedTables = tables.filter((t2) => t2.insertedCount + t2.updatedCount + t2.deletedCount > 0);
15167
+ const beforeMeta = snapshots.find((s2) => s2.id === beforeId);
15168
+ const afterMeta = snapshots.find((s2) => s2.id === afterId);
15169
+ if (beforeMeta && afterMeta) {
15170
+ const header = document.createElement("div");
15171
+ header.className = "db-snapshot-diff-header";
15172
+ const beforeLabel = new Date(beforeMeta.createdAt).toLocaleString();
15173
+ const afterLabel = new Date(afterMeta.createdAt).toLocaleString();
15174
+ header.textContent = text2().snapshot.diffShowing(beforeLabel, afterLabel);
15175
+ section.appendChild(header);
15176
+ }
15177
+ const changedTables = tables.filter((t2) => t2.insertedCount + t2.updatedCount + t2.deletedCount > 0 || t2.coverage && t2.coverage !== "both");
14874
15178
  const unchangedCount = tables.length - changedTables.length;
14875
15179
  const summary = document.createElement("div");
14876
15180
  summary.className = "db-snapshot-diff-summary";
14877
15181
  if (changedTables.length === 0) {
14878
15182
  summary.textContent = text2().snapshot.noChanges;
14879
15183
  section.appendChild(summary);
14880
- mainArea.appendChild(section);
15184
+ rightPane.appendChild(section);
14881
15185
  return;
14882
15186
  }
14883
15187
  summary.textContent = text2().snapshot.diffSummary(changedTables.length, unchangedCount);
14884
15188
  section.appendChild(summary);
15189
+ const toc = document.createElement("div");
15190
+ toc.className = "db-snapshot-diff-toc";
15191
+ const tocChips = new Map;
15192
+ section.appendChild(toc);
15193
+ function buildStatSpans(target, inserted, updated, deleted) {
15194
+ const make = (cls, prefix, n2) => {
15195
+ if (n2 <= 0)
15196
+ return;
15197
+ const s2 = document.createElement("span");
15198
+ s2.className = `db-snapshot-diff-stat ${cls}`;
15199
+ s2.textContent = `${prefix}${n2}`;
15200
+ target.appendChild(s2);
15201
+ };
15202
+ make("add", "+", inserted);
15203
+ make("upd", "~", updated);
15204
+ make("del", "-", deleted);
15205
+ }
15206
+ function renderCoverageOrStats(target, summary2) {
15207
+ const tx = text2().snapshot;
15208
+ if (summary2.coverage === "before-only") {
15209
+ const lbl = document.createElement("span");
15210
+ lbl.className = "db-snapshot-diff-coverage-badge before-only";
15211
+ lbl.textContent = tx.coverageBeforeOnly;
15212
+ target.appendChild(lbl);
15213
+ } else if (summary2.coverage === "after-only") {
15214
+ const lbl = document.createElement("span");
15215
+ lbl.className = "db-snapshot-diff-coverage-badge after-only";
15216
+ lbl.textContent = tx.coverageAfterOnly;
15217
+ target.appendChild(lbl);
15218
+ } else {
15219
+ buildStatSpans(target, summary2.insertedCount, summary2.updatedCount, summary2.deletedCount);
15220
+ }
15221
+ }
15222
+ const pendingLoads = [];
15223
+ let scrollTarget = null;
14885
15224
  for (const t2 of changedTables) {
14886
15225
  const tableEl = document.createElement("div");
14887
15226
  tableEl.className = "db-snapshot-diff-table";
15227
+ const sectionId = `diff-table-${t2.tableName.replace(/[^a-zA-Z0-9_]/g, "_")}`;
15228
+ tableEl.id = sectionId;
14888
15229
  const tableHeader = document.createElement("div");
14889
15230
  tableHeader.className = "db-snapshot-diff-table-header";
14890
15231
  const nameSpan = document.createElement("span");
@@ -14892,26 +15233,82 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14892
15233
  nameSpan.textContent = t2.tableName;
14893
15234
  const statsSpan = document.createElement("span");
14894
15235
  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(" ");
15236
+ renderCoverageOrStats(statsSpan, t2);
14903
15237
  tableHeader.append(nameSpan, statsSpan);
14904
15238
  tableHeader.style.cursor = "pointer";
14905
15239
  const rowsContainer = document.createElement("div");
14906
15240
  rowsContainer.className = "db-snapshot-diff-rows-container";
14907
- loadDiffRows(beforeId, afterId, t2.tableName, rowsContainer);
15241
+ if (t2.coverage === "before-only" || t2.coverage === "after-only") {
15242
+ const tx = text2().snapshot;
15243
+ const note = document.createElement("div");
15244
+ note.className = "db-snapshot-diff-coverage-note";
15245
+ const rows = t2.unsnapshottedRowCount ?? 0;
15246
+ note.textContent = t2.coverage === "before-only" ? tx.coverageNoteBeforeOnly(rows) : tx.coverageNoteAfterOnly(rows);
15247
+ rowsContainer.appendChild(note);
15248
+ } else {
15249
+ pendingLoads.push(loadDiffRows(beforeId, afterId, t2.tableName, rowsContainer));
15250
+ }
14908
15251
  tableHeader.addEventListener("click", () => {
14909
15252
  rowsContainer.hidden = !rowsContainer.hidden;
14910
15253
  });
14911
15254
  tableEl.append(tableHeader, rowsContainer);
14912
15255
  section.appendChild(tableEl);
15256
+ const chip = document.createElement("button");
15257
+ chip.type = "button";
15258
+ chip.className = "db-snapshot-diff-toc-chip";
15259
+ chip.dataset.targetId = sectionId;
15260
+ const chipName = document.createElement("span");
15261
+ chipName.className = "db-snapshot-diff-toc-chip-name";
15262
+ chipName.textContent = t2.tableName;
15263
+ const chipStats = document.createElement("span");
15264
+ chipStats.className = "db-snapshot-diff-toc-chip-stats";
15265
+ renderCoverageOrStats(chipStats, t2);
15266
+ if (t2.coverage === "before-only" || t2.coverage === "after-only") {
15267
+ chip.classList.add("coverage-incomplete");
15268
+ }
15269
+ chip.append(chipName, chipStats);
15270
+ chip.addEventListener("click", () => {
15271
+ scrollTarget = tableEl;
15272
+ tableEl.scrollIntoView({ behavior: "smooth", block: "start" });
15273
+ });
15274
+ toc.appendChild(chip);
15275
+ tocChips.set(sectionId, chip);
15276
+ }
15277
+ Promise.allSettled(pendingLoads).then(() => {
15278
+ if (scrollTarget && rightPane.contains(scrollTarget)) {
15279
+ scrollTarget.scrollIntoView({ behavior: "smooth", block: "start" });
15280
+ }
15281
+ });
15282
+ const observer = new IntersectionObserver((entries) => {
15283
+ const intersecting = entries.filter((e2) => e2.isIntersecting);
15284
+ if (intersecting.length === 0)
15285
+ return;
15286
+ intersecting.sort((a2, b2) => a2.boundingClientRect.top - b2.boundingClientRect.top);
15287
+ const activeId = intersecting[0].target.id;
15288
+ for (const [id, chip] of tocChips) {
15289
+ chip.classList.toggle("active", id === activeId);
15290
+ }
15291
+ const activeChip = tocChips.get(activeId);
15292
+ if (activeChip) {
15293
+ const tocBox = toc.getBoundingClientRect();
15294
+ const chipBox = activeChip.getBoundingClientRect();
15295
+ const margin = 16;
15296
+ if (chipBox.left < tocBox.left + margin) {
15297
+ toc.scrollLeft += chipBox.left - tocBox.left - margin;
15298
+ } else if (chipBox.right > tocBox.right - margin) {
15299
+ toc.scrollLeft += chipBox.right - tocBox.right + margin;
15300
+ }
15301
+ }
15302
+ }, {
15303
+ root: rightPane,
15304
+ rootMargin: "-48px 0px -60% 0px",
15305
+ threshold: 0
15306
+ });
15307
+ for (const el2 of section.querySelectorAll(".db-snapshot-diff-table")) {
15308
+ observer.observe(el2);
14913
15309
  }
14914
- mainArea.appendChild(section);
15310
+ section.__tocObserver = observer;
15311
+ rightPane.appendChild(section);
14915
15312
  }
14916
15313
  async function loadDiffRows(beforeId, afterId, table2, container) {
14917
15314
  container.innerHTML = "";
@@ -14920,7 +15317,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14920
15317
  loading.textContent = text2().snapshot.loading;
14921
15318
  container.appendChild(loading);
14922
15319
  try {
14923
- const res = await fetch(`/_db/snapshot/diff/rows?before=${encodeURIComponent(beforeId)}&after=${encodeURIComponent(afterId)}&table=${encodeURIComponent(table2)}&limit=200`);
15320
+ const res = await getJson(`/_db/snapshot/diff/rows?before=${encodeURIComponent(beforeId)}&after=${encodeURIComponent(afterId)}&table=${encodeURIComponent(table2)}&limit=200`);
14924
15321
  if (!res.ok) {
14925
15322
  container.innerHTML = "";
14926
15323
  const error2 = document.createElement("div");
@@ -14953,11 +15350,19 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14953
15350
  allCols.add(k);
14954
15351
  }
14955
15352
  const columns = [...allCols];
14956
- const table2 = document.createElement("table");
14957
- table2.className = "db-snap-diff-grid";
15353
+ const colHeaderWrap = document.createElement("div");
15354
+ colHeaderWrap.className = "db-snapshot-diff-col-header-wrap";
15355
+ const colHeaderTable = document.createElement("table");
15356
+ colHeaderTable.className = "db-snap-diff-grid";
15357
+ const colHeaderColgroup = document.createElement("colgroup");
15358
+ for (let i2 = 0;i2 < columns.length + 1; i2++) {
15359
+ colHeaderColgroup.appendChild(document.createElement("col"));
15360
+ }
15361
+ colHeaderTable.appendChild(colHeaderColgroup);
14958
15362
  const thead = document.createElement("thead");
14959
15363
  const headRow = document.createElement("tr");
14960
15364
  const thType = document.createElement("th");
15365
+ thType.className = "snap-diff-type-cell";
14961
15366
  thType.textContent = "";
14962
15367
  headRow.appendChild(thType);
14963
15368
  for (const col of columns) {
@@ -14966,16 +15371,25 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14966
15371
  headRow.appendChild(th);
14967
15372
  }
14968
15373
  thead.appendChild(headRow);
14969
- table2.appendChild(thead);
15374
+ colHeaderTable.appendChild(thead);
15375
+ colHeaderWrap.appendChild(colHeaderTable);
15376
+ const rowsViewport = document.createElement("div");
15377
+ rowsViewport.className = "db-snapshot-diff-rows-viewport";
15378
+ const bodyTable = document.createElement("table");
15379
+ bodyTable.className = "db-snap-diff-grid";
15380
+ const bodyColgroup = document.createElement("colgroup");
15381
+ for (let i2 = 0;i2 < columns.length + 1; i2++) {
15382
+ bodyColgroup.appendChild(document.createElement("col"));
15383
+ }
15384
+ bodyTable.appendChild(bodyColgroup);
14970
15385
  const tbody = document.createElement("tbody");
14971
15386
  for (const row of rows) {
14972
15387
  if (row.changeType === "updated" && row.beforeValues && row.afterValues) {
14973
15388
  const trBefore = document.createElement("tr");
14974
- trBefore.className = "snap-diff-del";
15389
+ trBefore.className = "snap-diff-del snap-diff-record-start";
14975
15390
  const tdTypeBefore = document.createElement("td");
14976
15391
  tdTypeBefore.className = "snap-diff-type-cell";
14977
- tdTypeBefore.textContent = text2().snapshot.before;
14978
- tdTypeBefore.rowSpan = 2;
15392
+ tdTypeBefore.textContent = "−";
14979
15393
  trBefore.appendChild(tdTypeBefore);
14980
15394
  for (const col of columns) {
14981
15395
  const td = document.createElement("td");
@@ -14988,7 +15402,11 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
14988
15402
  }
14989
15403
  tbody.appendChild(trBefore);
14990
15404
  const trAfter = document.createElement("tr");
14991
- trAfter.className = "snap-diff-add";
15405
+ trAfter.className = "snap-diff-add snap-diff-record-end";
15406
+ const tdTypeAfter = document.createElement("td");
15407
+ tdTypeAfter.className = "snap-diff-type-cell";
15408
+ tdTypeAfter.textContent = "+";
15409
+ trAfter.appendChild(tdTypeAfter);
14992
15410
  for (const col of columns) {
14993
15411
  const td = document.createElement("td");
14994
15412
  const bs = row.beforeValues[col] == null ? "NULL" : String(row.beforeValues[col]);
@@ -15001,7 +15419,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15001
15419
  tbody.appendChild(trAfter);
15002
15420
  } else {
15003
15421
  const tr = document.createElement("tr");
15004
- tr.className = row.changeType === "inserted" ? "snap-diff-add" : "snap-diff-del";
15422
+ const sideClass = row.changeType === "inserted" ? "snap-diff-add" : "snap-diff-del";
15423
+ tr.className = `${sideClass} snap-diff-record-start snap-diff-record-end`;
15005
15424
  const tdType = document.createElement("td");
15006
15425
  tdType.className = "snap-diff-type-cell";
15007
15426
  tdType.textContent = changeTypeLabel(row.changeType);
@@ -15009,6 +15428,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15009
15428
  const values = row.changeType === "inserted" ? row.afterValues : row.beforeValues;
15010
15429
  for (const col of columns) {
15011
15430
  const td = document.createElement("td");
15431
+ td.classList.add("snap-diff-changed-cell");
15012
15432
  const v = values?.[col];
15013
15433
  td.textContent = v == null ? "NULL" : String(v);
15014
15434
  tr.appendChild(td);
@@ -15016,8 +15436,32 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15016
15436
  tbody.appendChild(tr);
15017
15437
  }
15018
15438
  }
15019
- table2.appendChild(tbody);
15020
- container.appendChild(table2);
15439
+ bodyTable.appendChild(tbody);
15440
+ rowsViewport.appendChild(bodyTable);
15441
+ container.append(colHeaderWrap, rowsViewport);
15442
+ rowsViewport.addEventListener("scroll", () => {
15443
+ colHeaderWrap.scrollLeft = rowsViewport.scrollLeft;
15444
+ });
15445
+ const syncColumnWidths = () => {
15446
+ if (disposed)
15447
+ return;
15448
+ const firstRow = tbody.querySelector("tr");
15449
+ if (!firstRow)
15450
+ return;
15451
+ const cells = firstRow.children;
15452
+ const bodyCols = bodyColgroup.children;
15453
+ const headerCols = colHeaderColgroup.children;
15454
+ for (let i2 = 0;i2 < cells.length; i2++) {
15455
+ const w = cells[i2].offsetWidth;
15456
+ if (bodyCols[i2])
15457
+ bodyCols[i2].style.width = `${w}px`;
15458
+ if (headerCols[i2])
15459
+ headerCols[i2].style.width = `${w}px`;
15460
+ }
15461
+ };
15462
+ requestAnimationFrame(syncColumnWidths);
15463
+ const ro = new ResizeObserver(syncColumnWidths);
15464
+ ro.observe(bodyTable);
15021
15465
  if (total > rows.length) {
15022
15466
  const more = document.createElement("div");
15023
15467
  more.className = "db-snapshot-diff-more";
@@ -15069,7 +15513,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15069
15513
  if (item) {
15070
15514
  item.after(dialog);
15071
15515
  } else {
15072
- mainArea.prepend(dialog);
15516
+ leftPane.prepend(dialog);
15073
15517
  }
15074
15518
  input.focus();
15075
15519
  }
@@ -15082,6 +15526,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15082
15526
  refresh();
15083
15527
  }
15084
15528
  function handleSse(data) {
15529
+ if (disposed)
15530
+ return;
15085
15531
  try {
15086
15532
  const parsed = JSON.parse(data);
15087
15533
  const dbId = deps.getDbId();
@@ -15090,21 +15536,57 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15090
15536
  return;
15091
15537
  if (parsed.schema && parsed.schema !== schema)
15092
15538
  return;
15093
- if (parsed.action === "started" && parsed.id) {
15094
- setActiveSnapshotId(parsed.id);
15539
+ const id = parsed.id ?? null;
15540
+ if (parsed.action === "started" && id) {
15541
+ upsertJob(id, {
15542
+ done: false,
15543
+ total: parsed.total ?? 0,
15544
+ index: 0
15545
+ });
15546
+ }
15547
+ if (parsed.action === "progress" && id) {
15548
+ upsertJob(id, {
15549
+ table: parsed.table,
15550
+ index: parsed.index,
15551
+ total: parsed.total ?? 0,
15552
+ done: parsed.done === true
15553
+ });
15095
15554
  }
15096
- if (parsed.action === "created" || parsed.action === "error") {
15097
- setActiveSnapshotId(null);
15555
+ if (parsed.action === "created" && id) {
15556
+ upsertJob(id, { done: true });
15557
+ scheduleJobRemove(id);
15098
15558
  refreshAndAutoDiff();
15099
15559
  }
15560
+ if (parsed.action === "aborted" && id) {
15561
+ upsertJob(id, { done: true, aborted: true });
15562
+ scheduleJobRemove(id);
15563
+ refresh();
15564
+ }
15565
+ if (parsed.action === "error" && id) {
15566
+ upsertJob(id, {
15567
+ done: true,
15568
+ errorMessage: parsed.error || text2().snapshot.failed
15569
+ });
15570
+ scheduleJobRemove(id);
15571
+ refresh();
15572
+ }
15100
15573
  } catch {}
15101
15574
  }
15102
15575
  function dispose() {
15103
15576
  disposed = true;
15104
- setActiveSnapshotId(null);
15105
15577
  for (const timer of autoRefreshTimers)
15106
15578
  clearTimeout(timer);
15107
15579
  autoRefreshTimers.clear();
15580
+ for (const timer of jobClearTimers.values())
15581
+ clearTimeout(timer);
15582
+ jobClearTimers.clear();
15583
+ activeJobs.clear();
15584
+ cancellingJobs.clear();
15585
+ const existing = rightPane.querySelector(".db-snapshot-diff-inline");
15586
+ if (existing) {
15587
+ const prev = existing;
15588
+ prev.__tocObserver?.disconnect();
15589
+ }
15108
15590
  }
15109
15591
  function localize() {
15110
15592
  const t2 = text2().snapshot;
@@ -15112,8 +15594,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15112
15594
  guideBody.textContent = t2.guideBody;
15113
15595
  createBtn.textContent = t2.create;
15114
15596
  refreshBtn.textContent = t2.refresh;
15115
- if (!activeSnapshotId)
15116
- snapshotCancelBtn.textContent = t2.cancel;
15597
+ renderProgress();
15117
15598
  tableSelectorHeader.textContent = t2.selectTablesHeader;
15118
15599
  selectAllBtn.textContent = t2.selectAll;
15119
15600
  deselectAllBtn.textContent = t2.deselectAll;
@@ -15122,21 +15603,54 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15122
15603
  if (!confirmBtn.disabled)
15123
15604
  confirmBtn.textContent = t2.confirm;
15124
15605
  cancelBtn.textContent = t2.cancel;
15125
- const prevSelects = mainArea.querySelectorAll(".db-snapshot-diff-select");
15606
+ const prevSelects = leftPane.querySelectorAll(".db-snapshot-diff-select");
15126
15607
  const prevBeforeVal = prevSelects[0]?.value;
15127
15608
  const prevAfterVal = prevSelects[1]?.value;
15128
- const hadDiff = mainArea.querySelector(".db-snapshot-diff-inline") != null;
15609
+ const hadDiff = rightPane.querySelector(".db-snapshot-diff-inline") != null;
15129
15610
  renderMain();
15130
- const newSelects = mainArea.querySelectorAll(".db-snapshot-diff-select");
15131
- if (newSelects[0] && prevBeforeVal)
15611
+ const newSelects = leftPane.querySelectorAll(".db-snapshot-diff-select");
15612
+ if (newSelects[0] && prevBeforeVal) {
15132
15613
  newSelects[0].value = prevBeforeVal;
15133
- if (newSelects[1] && prevAfterVal)
15614
+ newSelects[0].dispatchEvent(new Event("change"));
15615
+ }
15616
+ if (newSelects[1] && prevAfterVal) {
15134
15617
  newSelects[1].value = prevAfterVal;
15618
+ newSelects[1].dispatchEvent(new Event("change"));
15619
+ }
15135
15620
  if (hadDiff && currentDiff) {
15136
15621
  showDiffInline(currentDiff.beforeId, currentDiff.afterId);
15137
15622
  }
15138
15623
  }
15139
- return { el, refresh, handleSse, dispose, localize };
15624
+ async function restoreDiffFromRoute() {
15625
+ const target = deps.getRouteDiff?.();
15626
+ if (!target || !target.before || !target.after)
15627
+ return;
15628
+ if (currentDiff && currentDiff.beforeId === target.before && currentDiff.afterId === target.after) {
15629
+ return;
15630
+ }
15631
+ if (snapshots.length === 0) {
15632
+ await refresh();
15633
+ }
15634
+ if (disposed)
15635
+ return;
15636
+ const hasBefore = snapshots.some((s2) => s2.id === target.before);
15637
+ const hasAfter = snapshots.some((s2) => s2.id === target.after);
15638
+ if (!hasBefore || !hasAfter) {
15639
+ deps.setRouteDiff?.(null);
15640
+ return;
15641
+ }
15642
+ await showDiffInline(target.before, target.after);
15643
+ }
15644
+ return {
15645
+ el,
15646
+ refresh,
15647
+ handleSse,
15648
+ dispose,
15649
+ localize,
15650
+ restoreDiffFromRoute: () => {
15651
+ restoreDiffFromRoute();
15652
+ }
15653
+ };
15140
15654
  }
15141
15655
 
15142
15656
  // web-src/views/database/table-grid.ts
@@ -17377,6 +17891,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
17377
17891
  onClick: () => {
17378
17892
  setActiveTab("snapshot");
17379
17893
  snapshotView.refresh();
17894
+ snapshotView.restoreDiffFromRoute();
17380
17895
  }
17381
17896
  });
17382
17897
  const disposeInferFkPrefSub = outerDeps.onDbUiPrefChange(() => {
@@ -17481,7 +17996,45 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
17481
17996
  getDbId: () => currentDbInfo?.id || null,
17482
17997
  getSchema: () => currentSchema,
17483
17998
  getTables: () => schemaCache?.tables || [],
17484
- getText: () => paneText()
17999
+ getText: () => paneText(),
18000
+ trackLoad: deps.trackLoad,
18001
+ getStoredSelectedTables: () => {
18002
+ if (!currentDbInfo)
18003
+ return;
18004
+ const scope = dbUiTableExpansionScope(currentDbInfo.id, currentSchema);
18005
+ return outerDeps.getSnapshotSelectedTables(scope);
18006
+ },
18007
+ setStoredSelectedTables: (tables) => {
18008
+ if (!currentDbInfo)
18009
+ return;
18010
+ const scope = dbUiTableExpansionScope(currentDbInfo.id, currentSchema);
18011
+ outerDeps.setSnapshotSelectedTables(scope, tables);
18012
+ },
18013
+ getRouteDiff: () => {
18014
+ const parsed = parseRoute(window.location.pathname, window.location.search, deps.currentRange());
18015
+ if (parsed.screen !== "database")
18016
+ return;
18017
+ const before = parsed.diffBefore;
18018
+ const after = parsed.diffAfter;
18019
+ if (!before && !after)
18020
+ return;
18021
+ return { before, after };
18022
+ },
18023
+ setRouteDiff: (diff) => {
18024
+ const dbId = currentDbInfo?.id;
18025
+ if (!dbId)
18026
+ return;
18027
+ deps.setRoute({
18028
+ screen: "database",
18029
+ db: dbId,
18030
+ schema: currentSchema ?? undefined,
18031
+ table: currentTable ?? undefined,
18032
+ tab: "snapshot",
18033
+ ...diff?.before ? { diffBefore: diff.before } : {},
18034
+ ...diff?.after ? { diffAfter: diff.after } : {},
18035
+ range: deps.currentRange()
18036
+ }, true);
18037
+ }
17485
18038
  });
17486
18039
  const historyView = createQueryHistoryView({
17487
18040
  getDbId: () => currentDbInfo?.id || null,
@@ -18337,6 +18890,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18337
18890
  renderErDiagram();
18338
18891
  } else if (normalizedView === "snapshot") {
18339
18892
  snapshotView.refresh();
18893
+ snapshotView.restoreDiffFromRoute();
18340
18894
  }
18341
18895
  }
18342
18896
  applyAnnotationTarget(options.annotationTarget);
@@ -18610,33 +19164,27 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18610
19164
  return;
18611
19165
  });
18612
19166
  }
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);
19167
+ function updateScopedTableList(stateKey, scopeKey, mutate) {
19168
+ const current = { ...dbUiState[stateKey] || {} };
19169
+ const tables = new Set(current[scopeKey] || []);
19170
+ mutate(tables);
18623
19171
  const nextTables = [...tables].sort((a2, b2) => a2.localeCompare(b2));
18624
19172
  if (nextTables.length > 0)
18625
- expandedTables[scopeKey] = nextTables;
19173
+ current[scopeKey] = nextTables;
18626
19174
  else
18627
- delete expandedTables[scopeKey];
18628
- const nextExpanded = Object.keys(expandedTables).length > 0 ? expandedTables : undefined;
19175
+ delete current[scopeKey];
19176
+ const nextMap = Object.keys(current).length > 0 ? current : undefined;
18629
19177
  const nextState = { ...dbUiState, version: 1 };
18630
- if (nextExpanded)
18631
- nextState.expandedTables = nextExpanded;
19178
+ if (nextMap)
19179
+ nextState[stateKey] = nextMap;
18632
19180
  else
18633
- delete nextState.expandedTables;
19181
+ delete nextState[stateKey];
18634
19182
  applyDbUiState(nextState);
18635
19183
  fetch("/_db/ui", {
18636
19184
  method: "PATCH",
18637
19185
  headers: actionHeaders(),
18638
19186
  body: JSON.stringify({
18639
- expandedTables: {
19187
+ [stateKey]: {
18640
19188
  [scopeKey]: nextTables.length > 0 ? nextTables : null
18641
19189
  }
18642
19190
  })
@@ -18644,6 +19192,27 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18644
19192
  return;
18645
19193
  });
18646
19194
  }
19195
+ function getExpandedTables(scopeKey) {
19196
+ return [...dbUiState.expandedTables?.[scopeKey] || []];
19197
+ }
19198
+ function setExpandedTable(scopeKey, table2, expanded) {
19199
+ updateScopedTableList("expandedTables", scopeKey, (tables) => {
19200
+ if (expanded)
19201
+ tables.add(table2);
19202
+ else
19203
+ tables.delete(table2);
19204
+ });
19205
+ }
19206
+ function getSnapshotSelectedTables(scopeKey) {
19207
+ return [...dbUiState.snapshotSelectedTables?.[scopeKey] || []];
19208
+ }
19209
+ function setSnapshotSelectedTables(scopeKey, tables) {
19210
+ updateScopedTableList("snapshotSelectedTables", scopeKey, (set2) => {
19211
+ set2.clear();
19212
+ for (const t2 of tables)
19213
+ set2.add(t2);
19214
+ });
19215
+ }
18647
19216
  const dbUiPrefListeners = new Set;
18648
19217
  function applyDbUiState(next) {
18649
19218
  dbUiState = next;
@@ -18865,12 +19434,22 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18865
19434
  if (!entry)
18866
19435
  return;
18867
19436
  const s2 = entry.pane.getState();
19437
+ const isSnapshot = s2.view === "snapshot";
19438
+ let diffBefore;
19439
+ let diffAfter;
19440
+ if (isSnapshot) {
19441
+ const url = new URL(window.location.href);
19442
+ diffBefore = url.searchParams.get("diffBefore") ?? undefined;
19443
+ diffAfter = url.searchParams.get("diffAfter") ?? undefined;
19444
+ }
18868
19445
  deps.setRoute({
18869
19446
  screen: "database",
18870
19447
  db: s2.dbId ?? undefined,
18871
19448
  schema: s2.schema ?? undefined,
18872
19449
  table: s2.table ?? undefined,
18873
19450
  tab: s2.view === "data" ? undefined : s2.view,
19451
+ ...diffBefore ? { diffBefore } : {},
19452
+ ...diffAfter ? { diffAfter } : {},
18874
19453
  range: deps.currentRange()
18875
19454
  }, true);
18876
19455
  }
@@ -19120,6 +19699,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
19120
19699
  setColumnWidths,
19121
19700
  getExpandedTables,
19122
19701
  setExpandedTable,
19702
+ getSnapshotSelectedTables,
19703
+ setSnapshotSelectedTables,
19123
19704
  getDbUiPref,
19124
19705
  setDbUiPref,
19125
19706
  onDbUiPrefChange,