@youtyan/code-viewer 0.1.51 → 0.1.53

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
@@ -516,6 +516,19 @@
516
516
  range
517
517
  };
518
518
  }
519
+ case "/database": {
520
+ const db = params.get("db") || undefined;
521
+ const table = params.get("table") || undefined;
522
+ const tabRaw = params.get("tab");
523
+ const tab = tabRaw === "data" || tabRaw === "query" || tabRaw === "schema" || tabRaw === "er" || tabRaw === "search" || tabRaw === "snapshot" ? tabRaw : undefined;
524
+ return {
525
+ screen: "database",
526
+ ...db ? { db } : {},
527
+ ...table ? { table } : {},
528
+ ...tab ? { tab } : {},
529
+ range
530
+ };
531
+ }
519
532
  default:
520
533
  return {
521
534
  screen: "unknown",
@@ -562,6 +575,17 @@
562
575
  const qs = params.toString();
563
576
  return `/history${qs ? `?${qs}` : ""}`;
564
577
  }
578
+ case "database": {
579
+ const params = new URLSearchParams;
580
+ if (route.db)
581
+ params.set("db", route.db);
582
+ if (route.table)
583
+ params.set("table", route.table);
584
+ if (route.tab)
585
+ params.set("tab", route.tab);
586
+ const qs = params.toString();
587
+ return `/database${qs ? `?${qs}` : ""}`;
588
+ }
565
589
  case "unknown":
566
590
  return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
567
591
  default:
@@ -7138,8 +7162,13 @@ ${frontmatter.yaml}
7138
7162
  function setAnnotationPanelOpen(open) {
7139
7163
  annotationPanel.hidden = !open;
7140
7164
  document.body.classList.toggle("annotation-panel-open", open);
7141
- if (open)
7165
+ if (open) {
7142
7166
  annotationPanelDismissed = false;
7167
+ const qhPanel = document.getElementById("query-history-panel");
7168
+ if (qhPanel)
7169
+ qhPanel.hidden = true;
7170
+ document.body.classList.remove("query-history-panel-open");
7171
+ }
7143
7172
  localStorage.setItem("gdp:annotation-panel", open ? "1" : "0");
7144
7173
  }
7145
7174
  function annotationLineTarget(entry) {
@@ -7771,177 +7800,3099 @@ ${frontmatter.yaml}
7771
7800
  };
7772
7801
  }
7773
7802
 
7774
- // web-src/views/diff-line-select.ts
7775
- var SELECTED_CLASS = "gdp-diff-line-selected";
7776
- function cardPath(el) {
7777
- return el.closest(".gdp-file-shell[data-path]")?.dataset.path || "";
7778
- }
7779
- function afterLineFromCell(cell) {
7780
- const sideCell = cell.closest("td.d2h-code-side-linenumber");
7781
- if (sideCell) {
7782
- const side = sideCell.closest(".d2h-file-side-diff");
7783
- const wrapper = sideCell.closest(".d2h-file-wrapper");
7784
- if (!side || !wrapper)
7785
- return null;
7786
- const sides = wrapper.querySelectorAll(".d2h-file-side-diff");
7787
- if (sides.length < 2 || side !== sides[1])
7788
- return null;
7789
- const line2 = Number((sideCell.textContent || "").trim());
7790
- return Number.isInteger(line2) && line2 > 0 ? line2 : null;
7803
+ // web-src/views/database/er-diagram.ts
7804
+ var mermaidPromise2 = null;
7805
+ var mermaidInitialized2 = false;
7806
+ async function loadMermaid2() {
7807
+ if (!mermaidPromise2) {
7808
+ mermaidPromise2 = import("/mermaid.js").then((mod) => {
7809
+ const typed = mod;
7810
+ const mermaid = typed.default;
7811
+ if (!mermaidInitialized2) {
7812
+ mermaid.initialize({
7813
+ startOnLoad: false,
7814
+ securityLevel: "strict",
7815
+ theme: "default",
7816
+ er: { useMaxWidth: false }
7817
+ });
7818
+ mermaidInitialized2 = true;
7819
+ }
7820
+ return mermaid;
7821
+ }).catch(() => null);
7791
7822
  }
7792
- const numCell = cell.closest("td.d2h-code-linenumber");
7793
- if (!numCell)
7794
- return null;
7795
- const raw = (numCell.querySelector(".line-num2")?.textContent || "").trim();
7796
- const line = Number(raw);
7797
- return Number.isInteger(line) && line > 0 ? line : null;
7823
+ return mermaidPromise2;
7824
+ }
7825
+ function mermaidType(sqlType) {
7826
+ const upper = sqlType.toUpperCase();
7827
+ if (upper.includes("INT"))
7828
+ return "int";
7829
+ if (upper.includes("TEXT") || upper.includes("VARCHAR") || upper.includes("CHAR"))
7830
+ return "string";
7831
+ if (upper.includes("REAL") || upper.includes("FLOAT") || upper.includes("DOUBLE"))
7832
+ return "float";
7833
+ if (upper.includes("BLOB"))
7834
+ return "blob";
7835
+ if (upper.includes("BOOL"))
7836
+ return "bool";
7837
+ if (upper.includes("DATE") || upper.includes("TIME"))
7838
+ return "datetime";
7839
+ if (upper.includes("NUMERIC") || upper.includes("DECIMAL"))
7840
+ return "decimal";
7841
+ return sqlType.replace(/[^a-zA-Z0-9]/g, "_") || "text";
7842
+ }
7843
+ function sanitizeMermaidId(name) {
7844
+ return name.replace(/[^a-zA-Z0-9_]/g, "_");
7845
+ }
7846
+ function buildErMarkup(schema, columnsMap) {
7847
+ const lines = ["erDiagram"];
7848
+ for (const table2 of schema.tables) {
7849
+ if (table2.type === "view")
7850
+ continue;
7851
+ if (!columnsMap.has(table2.name))
7852
+ continue;
7853
+ const id = sanitizeMermaidId(table2.name);
7854
+ const cols = columnsMap.get(table2.name) || [];
7855
+ lines.push(` ${id} {`);
7856
+ for (const col of cols) {
7857
+ const markers = [];
7858
+ if (col.primaryKey)
7859
+ markers.push("PK");
7860
+ const fk = schema.foreignKeys.find((f2) => f2.fromTable === table2.name && f2.fromColumn === col.name);
7861
+ if (fk)
7862
+ markers.push("FK");
7863
+ const comment2 = markers.length ? `"${markers.join(",")}"` : "";
7864
+ lines.push(` ${mermaidType(col.type)} ${sanitizeMermaidId(col.name)}${comment2 ? ` ${comment2}` : ""}`);
7865
+ }
7866
+ lines.push(" }");
7867
+ }
7868
+ const seen = new Set;
7869
+ for (const fk of schema.foreignKeys) {
7870
+ const fromId = sanitizeMermaidId(fk.fromTable);
7871
+ const toId = sanitizeMermaidId(fk.toTable);
7872
+ const key = `${fromId}--${toId}`;
7873
+ if (seen.has(key))
7874
+ continue;
7875
+ seen.add(key);
7876
+ if (!columnsMap.has(fk.fromTable) || !columnsMap.has(fk.toTable))
7877
+ continue;
7878
+ const toTable = schema.tables.find((t2) => t2.name === fk.toTable);
7879
+ if (!toTable || toTable.type === "view")
7880
+ continue;
7881
+ lines.push(` ${toId} ||--o{ ${fromId} : "${fk.fromColumn}"`);
7882
+ }
7883
+ return lines.join(`
7884
+ `);
7798
7885
  }
7799
- function rowsWithAfterLines(card) {
7800
- const out = [];
7801
- card.querySelectorAll("table.d2h-diff-table tr").forEach((row) => {
7802
- const cell = row.querySelector("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
7803
- if (!cell)
7886
+ function createErDiagram() {
7887
+ const el = document.createElement("div");
7888
+ el.className = "db-er-diagram";
7889
+ el.hidden = true;
7890
+ const toolbar = document.createElement("div");
7891
+ toolbar.className = "db-er-toolbar";
7892
+ const zoomIn = document.createElement("button");
7893
+ zoomIn.type = "button";
7894
+ zoomIn.className = "db-btn db-er-zoom-btn";
7895
+ zoomIn.textContent = "+";
7896
+ zoomIn.title = "Zoom in";
7897
+ const zoomOut = document.createElement("button");
7898
+ zoomOut.type = "button";
7899
+ zoomOut.className = "db-btn db-er-zoom-btn";
7900
+ zoomOut.textContent = "−";
7901
+ zoomOut.title = "Zoom out";
7902
+ const zoomReset = document.createElement("button");
7903
+ zoomReset.type = "button";
7904
+ zoomReset.className = "db-btn db-er-zoom-btn";
7905
+ zoomReset.textContent = "1:1";
7906
+ zoomReset.title = "Reset zoom";
7907
+ const copyBtn = document.createElement("button");
7908
+ copyBtn.type = "button";
7909
+ copyBtn.className = "db-btn db-er-zoom-btn";
7910
+ copyBtn.textContent = "Copy Mermaid";
7911
+ copyBtn.title = "Copy mermaid source to clipboard";
7912
+ toolbar.append(zoomIn, zoomOut, zoomReset, copyBtn);
7913
+ const container = document.createElement("div");
7914
+ container.className = "db-er-container";
7915
+ const svgWrap = document.createElement("div");
7916
+ svgWrap.className = "db-er-svg-wrap";
7917
+ container.appendChild(svgWrap);
7918
+ el.append(toolbar, container);
7919
+ let scale = 1;
7920
+ let lastMarkup = "";
7921
+ function applyZoom() {
7922
+ svgWrap.style.transform = `scale(${scale})`;
7923
+ svgWrap.style.transformOrigin = "top left";
7924
+ }
7925
+ zoomIn.addEventListener("click", () => {
7926
+ scale = Math.min(3, scale + 0.2);
7927
+ applyZoom();
7928
+ });
7929
+ zoomOut.addEventListener("click", () => {
7930
+ scale = Math.max(0.2, scale - 0.2);
7931
+ applyZoom();
7932
+ });
7933
+ zoomReset.addEventListener("click", () => {
7934
+ scale = 1;
7935
+ applyZoom();
7936
+ });
7937
+ copyBtn.addEventListener("click", () => {
7938
+ if (lastMarkup) {
7939
+ navigator.clipboard.writeText(lastMarkup).then(() => {
7940
+ copyBtn.textContent = "Copied!";
7941
+ setTimeout(() => {
7942
+ copyBtn.textContent = "Copy Mermaid";
7943
+ }, 1500);
7944
+ }, () => {});
7945
+ }
7946
+ });
7947
+ let dragState = null;
7948
+ container.addEventListener("mousedown", (e2) => {
7949
+ if (e2.button !== 0)
7804
7950
  return;
7805
- const line = afterLineFromCell(cell);
7806
- if (line !== null)
7807
- out.push({ row, line });
7951
+ dragState = {
7952
+ x: e2.clientX,
7953
+ y: e2.clientY,
7954
+ sl: container.scrollLeft,
7955
+ st: container.scrollTop
7956
+ };
7957
+ container.style.cursor = "grabbing";
7958
+ e2.preventDefault();
7808
7959
  });
7809
- return out;
7810
- }
7811
- function createDiffLineSelect(deps) {
7812
- let drag = null;
7813
- let selection = null;
7814
- function clearHighlights() {
7815
- document.querySelectorAll(`.${SELECTED_CLASS}`).forEach((row) => {
7816
- row.classList.remove(SELECTED_CLASS);
7817
- });
7818
- }
7819
- function applySelection(next) {
7820
- selection = next;
7821
- clearHighlights();
7822
- if (!next) {
7823
- deps.pill.hide();
7960
+ window.addEventListener("mousemove", (e2) => {
7961
+ if (!dragState)
7824
7962
  return;
7963
+ container.scrollLeft = dragState.sl - (e2.clientX - dragState.x);
7964
+ container.scrollTop = dragState.st - (e2.clientY - dragState.y);
7965
+ });
7966
+ window.addEventListener("mouseup", () => {
7967
+ if (dragState) {
7968
+ dragState = null;
7969
+ container.style.cursor = "";
7825
7970
  }
7826
- const start = Math.min(next.start, next.end);
7827
- const end = Math.max(next.start, next.end);
7828
- const card = document.querySelector(`.gdp-file-shell[data-path="${CSS.escape(next.path)}"]`);
7829
- if (card) {
7830
- for (const item of rowsWithAfterLines(card)) {
7831
- if (item.line >= start && item.line <= end)
7832
- item.row.classList.add(SELECTED_CLASS);
7833
- }
7971
+ });
7972
+ container.addEventListener("wheel", (e2) => {
7973
+ if (e2.ctrlKey || e2.metaKey) {
7974
+ e2.preventDefault();
7975
+ const delta = e2.deltaY > 0 ? -0.1 : 0.1;
7976
+ scale = Math.max(0.2, Math.min(3, scale + delta));
7977
+ applyZoom();
7978
+ }
7979
+ }, { passive: false });
7980
+ async function render(schema, columnsMap) {
7981
+ el.hidden = false;
7982
+ svgWrap.innerHTML = "";
7983
+ scale = 1;
7984
+ applyZoom();
7985
+ const tables = schema.tables.filter((t2) => t2.type === "table");
7986
+ if (tables.length === 0) {
7987
+ svgWrap.textContent = "No tables to display.";
7988
+ return;
7989
+ }
7990
+ const markup = buildErMarkup(schema, columnsMap);
7991
+ lastMarkup = markup;
7992
+ const mermaid = await loadMermaid2();
7993
+ if (!mermaid) {
7994
+ svgWrap.textContent = "Failed to load mermaid.js";
7995
+ return;
7996
+ }
7997
+ const node = document.createElement("div");
7998
+ node.className = "mermaid";
7999
+ node.textContent = markup;
8000
+ svgWrap.appendChild(node);
8001
+ try {
8002
+ await mermaid.run({ nodes: [node], suppressErrors: true });
8003
+ } catch {
8004
+ svgWrap.textContent = "Failed to render ER diagram.";
7834
8005
  }
7835
- deps.pill.show(next.path, start, end);
7836
8006
  }
7837
8007
  function clear() {
7838
- drag = null;
7839
- applySelection(null);
8008
+ el.hidden = true;
8009
+ svgWrap.innerHTML = "";
8010
+ lastMarkup = "";
7840
8011
  }
7841
- const diff = document.querySelector("#diff");
7842
- if (!diff)
7843
- return { clear };
7844
- diff.addEventListener("mousedown", (e2) => {
7845
- const target = e2.target;
7846
- const cell = target.closest("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
7847
- if (!cell)
8012
+ return { el, render, clear };
8013
+ }
8014
+
8015
+ // web-src/views/database/global-search-view.ts
8016
+ function createGlobalSearchView(deps) {
8017
+ const el = document.createElement("div");
8018
+ el.className = "db-global-search";
8019
+ const header = document.createElement("div");
8020
+ header.className = "db-global-search-header";
8021
+ const input = document.createElement("input");
8022
+ input.type = "text";
8023
+ input.className = "db-global-search-input";
8024
+ input.placeholder = "全テーブルを横断検索...";
8025
+ const searchBtn = document.createElement("button");
8026
+ searchBtn.type = "button";
8027
+ searchBtn.className = "db-global-search-btn";
8028
+ searchBtn.textContent = "検索";
8029
+ const cancelBtn = document.createElement("button");
8030
+ cancelBtn.type = "button";
8031
+ cancelBtn.className = "db-global-search-cancel";
8032
+ cancelBtn.textContent = "キャンセル";
8033
+ cancelBtn.hidden = true;
8034
+ const optionsRow = document.createElement("div");
8035
+ optionsRow.className = "db-global-search-options";
8036
+ const nonTextLabel = document.createElement("label");
8037
+ const nonTextCheck = document.createElement("input");
8038
+ nonTextCheck.type = "checkbox";
8039
+ nonTextLabel.append(nonTextCheck, " 数値・日付カラムも検索する");
8040
+ optionsRow.appendChild(nonTextLabel);
8041
+ header.append(input, searchBtn, cancelBtn);
8042
+ const progress = document.createElement("div");
8043
+ progress.className = "db-global-search-progress";
8044
+ progress.hidden = true;
8045
+ const results = document.createElement("div");
8046
+ results.className = "db-global-search-results";
8047
+ el.append(header, optionsRow, progress, results);
8048
+ let currentJobId = null;
8049
+ let pollTimer = null;
8050
+ function stopPolling() {
8051
+ if (pollTimer) {
8052
+ clearInterval(pollTimer);
8053
+ pollTimer = null;
8054
+ }
8055
+ cancelBtn.hidden = true;
8056
+ searchBtn.disabled = false;
8057
+ input.disabled = false;
8058
+ }
8059
+ async function startSearch() {
8060
+ const dbId = deps.getDbId();
8061
+ if (!dbId)
7848
8062
  return;
7849
- const line = afterLineFromCell(cell);
7850
- const path = cardPath(cell);
7851
- if (line === null || !path) {
7852
- if (selection)
7853
- clear();
8063
+ const term = input.value.trim();
8064
+ if (!term)
7854
8065
  return;
8066
+ results.innerHTML = "";
8067
+ progress.hidden = false;
8068
+ progress.textContent = "検索を開始しています...";
8069
+ searchBtn.disabled = true;
8070
+ input.disabled = true;
8071
+ cancelBtn.hidden = false;
8072
+ try {
8073
+ const res = await fetch("/_db/search/start", {
8074
+ method: "POST",
8075
+ headers: {
8076
+ "Content-Type": "application/json",
8077
+ "X-Code-Viewer-Action": "1"
8078
+ },
8079
+ body: JSON.stringify({
8080
+ db: dbId,
8081
+ term,
8082
+ includeNonText: nonTextCheck.checked
8083
+ })
8084
+ });
8085
+ if (!res.ok) {
8086
+ progress.textContent = `エラー: ${await res.text()}`;
8087
+ stopPolling();
8088
+ return;
8089
+ }
8090
+ const data = await res.json();
8091
+ currentJobId = data.jobId;
8092
+ pollTimer = setInterval(() => pollStatus(), 500);
8093
+ } catch (err) {
8094
+ progress.textContent = `エラー: ${err instanceof Error ? err.message : String(err)}`;
8095
+ stopPolling();
7855
8096
  }
7856
- e2.preventDefault();
7857
- drag = { path, start: line };
7858
- applySelection({ path, start: line, end: line });
7859
- });
7860
- diff.addEventListener("mouseover", (e2) => {
7861
- if (!drag)
8097
+ }
8098
+ async function pollStatus() {
8099
+ if (!currentJobId)
7862
8100
  return;
7863
- const target = e2.target;
7864
- const cell = target.closest("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
7865
- if (!cell || cardPath(cell) !== drag.path)
8101
+ try {
8102
+ const res = await fetch(`/_db/search/status?id=${encodeURIComponent(currentJobId)}`);
8103
+ if (!res.ok) {
8104
+ stopPolling();
8105
+ return;
8106
+ }
8107
+ const data = await res.json();
8108
+ if (data.error) {
8109
+ progress.textContent = `エラー: ${data.error}`;
8110
+ stopPolling();
8111
+ return;
8112
+ }
8113
+ const pct = data.totalTables > 0 ? Math.round(data.scannedTables / data.totalTables * 100) : 0;
8114
+ progress.textContent = data.done ? `完了。${data.scannedTables}テーブルから ${data.hits.length}件見つかりました。` : `検索中... ${data.currentTable || "対象テーブルを確認中"} (${pct}% - ${data.scannedTables}/${data.totalTables}テーブル、${data.hits.length}件)`;
8115
+ renderHits(data.hits);
8116
+ if (data.done) {
8117
+ stopPolling();
8118
+ progress.hidden = false;
8119
+ }
8120
+ } catch {}
8121
+ }
8122
+ async function cancelSearch() {
8123
+ if (!currentJobId)
7866
8124
  return;
7867
- const line = afterLineFromCell(cell);
7868
- if (line === null)
8125
+ try {
8126
+ await fetch("/_db/search/cancel", {
8127
+ method: "POST",
8128
+ headers: {
8129
+ "Content-Type": "application/json",
8130
+ "X-Code-Viewer-Action": "1"
8131
+ },
8132
+ body: JSON.stringify({ id: currentJobId })
8133
+ });
8134
+ } catch {}
8135
+ stopPolling();
8136
+ progress.textContent = "検索をキャンセルしました。";
8137
+ }
8138
+ function renderHits(hits) {
8139
+ results.innerHTML = "";
8140
+ if (hits.length === 0)
7869
8141
  return;
7870
- applySelection({ path: drag.path, start: drag.start, end: line });
7871
- });
7872
- document.addEventListener("mouseup", () => {
7873
- drag = null;
7874
- });
7875
- document.addEventListener("keydown", (e2) => {
7876
- if (e2.key === "Escape" && selection && !drag)
7877
- clear();
8142
+ const grouped = new Map;
8143
+ for (const h of hits) {
8144
+ const existing = grouped.get(h.table) || [];
8145
+ existing.push(h);
8146
+ grouped.set(h.table, existing);
8147
+ }
8148
+ for (const [table2, tableHits] of grouped) {
8149
+ const section = document.createElement("div");
8150
+ section.className = "db-search-table-section";
8151
+ const tableHeader = document.createElement("div");
8152
+ tableHeader.className = "db-search-table-header";
8153
+ tableHeader.textContent = `${table2} (${tableHits.length}件)`;
8154
+ section.appendChild(tableHeader);
8155
+ const hitsList = document.createElement("div");
8156
+ hitsList.className = "db-search-hits-list";
8157
+ for (const hit of tableHits.slice(0, 100)) {
8158
+ const row = document.createElement("div");
8159
+ row.className = "db-search-hit-row";
8160
+ const colSpan = document.createElement("span");
8161
+ colSpan.className = "db-search-hit-col";
8162
+ colSpan.textContent = hit.column;
8163
+ const valSpan = document.createElement("span");
8164
+ valSpan.className = "db-search-hit-val";
8165
+ valSpan.textContent = hit.valuePreview;
8166
+ row.append(colSpan, valSpan);
8167
+ hitsList.appendChild(row);
8168
+ }
8169
+ if (tableHits.length > 100) {
8170
+ const more = document.createElement("div");
8171
+ more.className = "db-search-more";
8172
+ more.textContent = `ほか ${tableHits.length - 100}件`;
8173
+ hitsList.appendChild(more);
8174
+ }
8175
+ section.appendChild(hitsList);
8176
+ results.appendChild(section);
8177
+ }
8178
+ }
8179
+ searchBtn.addEventListener("click", startSearch);
8180
+ input.addEventListener("keydown", (e2) => {
8181
+ if (e2.key === "Enter")
8182
+ startSearch();
7878
8183
  });
7879
- return { clear };
7880
- }
7881
-
7882
- // web-src/core/file-path-copy.ts
7883
- function filePathClipboardText(path) {
7884
- return path || "";
7885
- }
7886
- function fileNameClipboardText(path) {
7887
- if (!path)
7888
- return "";
7889
- const parts = path.split("/").filter(Boolean);
7890
- return parts[parts.length - 1] || "";
7891
- }
7892
- function fileReferenceClipboardText(path, start, end) {
7893
- if (!path)
7894
- return "";
7895
- const a2 = Math.max(1, Math.floor(Math.min(start, end)));
7896
- const b2 = Math.max(1, Math.floor(Math.max(start, end)));
7897
- return a2 === b2 ? `@${path}#${a2}` : `@${path}#${a2}-${b2}`;
8184
+ cancelBtn.addEventListener("click", cancelSearch);
8185
+ return { el };
7898
8186
  }
7899
8187
 
7900
- // web-src/core/ws-highlight.ts
7901
- function isWhitespaceOnlyInlineHighlight(text2) {
7902
- return !!text2 && !/\S/.test(text2);
8188
+ // web-src/views/database/query-editor.ts
8189
+ var shikiPromise2 = null;
8190
+ function loadShikiSql() {
8191
+ if (!shikiPromise2) {
8192
+ shikiPromise2 = import("/shiki.js").then((mod) => {
8193
+ const typed = mod;
8194
+ return typed.createHighlighter({
8195
+ themes: ["github-light", "github-dark"],
8196
+ langs: ["sql"]
8197
+ });
8198
+ }).catch(() => null);
8199
+ }
8200
+ return shikiPromise2;
7903
8201
  }
7904
- function suppressWhitespaceOnlyInlineHighlights(root) {
7905
- root.querySelectorAll("ins, del").forEach((el) => {
7906
- if (!isWhitespaceOnlyInlineHighlight(el.textContent))
8202
+ var HISTORY_KEY = "db:query-history";
8203
+ var MAX_HISTORY = 50;
8204
+ function loadHistory() {
8205
+ try {
8206
+ const raw = localStorage.getItem(HISTORY_KEY);
8207
+ if (!raw)
8208
+ return [];
8209
+ const parsed = JSON.parse(raw);
8210
+ return Array.isArray(parsed) ? parsed.filter((s2) => typeof s2 === "string") : [];
8211
+ } catch {
8212
+ return [];
8213
+ }
8214
+ }
8215
+ function saveToHistory(sql) {
8216
+ const history2 = loadHistory();
8217
+ const idx = history2.indexOf(sql);
8218
+ if (idx >= 0)
8219
+ history2.splice(idx, 1);
8220
+ history2.unshift(sql);
8221
+ if (history2.length > MAX_HISTORY)
8222
+ history2.length = MAX_HISTORY;
8223
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(history2));
8224
+ }
8225
+ function createQueryEditor(callbacks) {
8226
+ const el = document.createElement("div");
8227
+ el.className = "db-query-editor";
8228
+ const inputArea = document.createElement("div");
8229
+ inputArea.className = "db-query-input";
8230
+ const editorWrap = document.createElement("div");
8231
+ editorWrap.className = "db-query-editor-wrap";
8232
+ const highlight = document.createElement("pre");
8233
+ highlight.className = "db-query-highlight";
8234
+ highlight.setAttribute("aria-hidden", "true");
8235
+ const textarea = document.createElement("textarea");
8236
+ textarea.className = "db-query-textarea";
8237
+ textarea.placeholder = "SELECT * FROM ...";
8238
+ textarea.spellcheck = false;
8239
+ textarea.rows = 3;
8240
+ editorWrap.append(highlight, textarea);
8241
+ let shiki = null;
8242
+ loadShikiSql().then((h) => {
8243
+ shiki = h;
8244
+ syncHighlight();
8245
+ });
8246
+ function syncEditorHeight() {
8247
+ textarea.style.height = "auto";
8248
+ const h = Math.max(60, Math.min(textarea.scrollHeight, 300));
8249
+ textarea.style.height = `${h}px`;
8250
+ editorWrap.style.height = `${h}px`;
8251
+ }
8252
+ function syncHighlight() {
8253
+ const code2 = textarea.value;
8254
+ if (!code2) {
8255
+ highlight.innerHTML = "";
8256
+ syncEditorHeight();
7907
8257
  return;
7908
- const parent = el.parentNode;
7909
- if (!parent)
8258
+ }
8259
+ if (!shiki) {
8260
+ highlight.textContent = code2;
8261
+ syncEditorHeight();
7910
8262
  return;
7911
- parent.replaceChild(document.createTextNode(el.textContent || ""), el);
7912
- });
7913
- }
7914
-
7915
- // web-src/views/media-embed.ts
7916
- var MEDIA_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico|mp4|webm|mov|mp3|wav|ogg|flac|m4a|aac|opus)(\?.*)?$/i;
7917
- var IMAGE_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)(\?.*)?$/i;
7918
- var VIDEO_RE = /\.(mp4|webm|mov)$/i;
7919
- var AUDIO_RE = /\.(mp3|wav|ogg|flac|m4a|aac|opus)$/i;
7920
- function isMedia(p2) {
7921
- return MEDIA_RE.test(p2);
7922
- }
7923
- function isImage(p2) {
7924
- return IMAGE_RE.test(p2);
7925
- }
7926
- function isVideo(p2) {
7927
- return VIDEO_RE.test(p2);
7928
- }
7929
- function isAudio(p2) {
7930
- return AUDIO_RE.test(p2);
7931
- }
7932
- function fileURL(path, ref) {
7933
- return `/_file?path=${encodeURIComponent(path)}&ref=${ref}`;
7934
- }
7935
- function mediaTag(path, ref) {
7936
- const url = fileURL(path, ref);
7937
- if (isVideo(path)) {
7938
- return `<video src="${url}" controls preload="metadata"></video>`;
8263
+ }
8264
+ const html = shiki.codeToHtml(code2, {
8265
+ lang: "sql",
8266
+ themes: { light: "github-light", dark: "github-dark" },
8267
+ defaultColor: false
8268
+ });
8269
+ const template = document.createElement("template");
8270
+ template.innerHTML = html;
8271
+ const pre = template.content.querySelector("pre");
8272
+ if (pre) {
8273
+ highlight.innerHTML = pre.innerHTML;
8274
+ } else {
8275
+ highlight.textContent = code2;
8276
+ }
8277
+ syncEditorHeight();
7939
8278
  }
7940
- if (isAudio(path)) {
7941
- return `<audio src="${url}" controls preload="metadata"></audio>`;
8279
+ textarea.addEventListener("input", syncHighlight);
8280
+ textarea.addEventListener("scroll", () => {
8281
+ highlight.scrollTop = textarea.scrollTop;
8282
+ highlight.scrollLeft = textarea.scrollLeft;
8283
+ });
8284
+ const toolbar = document.createElement("div");
8285
+ toolbar.className = "db-query-toolbar";
8286
+ const runBtn = document.createElement("button");
8287
+ runBtn.className = "db-btn db-btn-primary db-query-run";
8288
+ runBtn.type = "button";
8289
+ runBtn.textContent = "Run";
8290
+ runBtn.title = "Execute query (Ctrl+Enter)";
8291
+ const explainBtn = document.createElement("button");
8292
+ explainBtn.className = "db-btn db-query-explain";
8293
+ explainBtn.type = "button";
8294
+ explainBtn.textContent = "Explain";
8295
+ explainBtn.title = "Show query execution plan";
8296
+ const historyBtn = document.createElement("button");
8297
+ historyBtn.className = "db-btn db-query-history-btn";
8298
+ historyBtn.type = "button";
8299
+ historyBtn.textContent = "History";
8300
+ historyBtn.title = "Query history";
8301
+ const statusSpan = document.createElement("span");
8302
+ statusSpan.className = "db-query-status";
8303
+ const historyDropdown = document.createElement("div");
8304
+ historyDropdown.className = "db-query-history-dropdown";
8305
+ historyDropdown.hidden = true;
8306
+ toolbar.append(runBtn, explainBtn, historyBtn, statusSpan);
8307
+ inputArea.append(editorWrap, toolbar, historyDropdown);
8308
+ const resultArea = document.createElement("div");
8309
+ resultArea.className = "db-query-result";
8310
+ resultArea.hidden = true;
8311
+ el.append(inputArea, resultArea);
8312
+ async function run() {
8313
+ const sql = textarea.value.trim();
8314
+ if (!sql)
8315
+ return;
8316
+ runBtn.disabled = true;
8317
+ statusSpan.textContent = "Running…";
8318
+ resultArea.hidden = true;
8319
+ try {
8320
+ const result = await callbacks.executeQuery(sql);
8321
+ if (result.error) {
8322
+ statusSpan.textContent = `Error (${result.elapsedMs}ms)`;
8323
+ resultArea.hidden = false;
8324
+ resultArea.innerHTML = "";
8325
+ const errEl = document.createElement("pre");
8326
+ errEl.className = "db-query-error";
8327
+ errEl.textContent = result.error;
8328
+ resultArea.appendChild(errEl);
8329
+ return;
8330
+ }
8331
+ saveToHistory(sql);
8332
+ const suffix = result.truncated ? "+" : "";
8333
+ statusSpan.textContent = `${result.rowCount}${suffix} rows (${result.elapsedMs}ms)`;
8334
+ renderResultTable(result);
8335
+ } catch (err) {
8336
+ statusSpan.textContent = "Failed";
8337
+ resultArea.hidden = false;
8338
+ resultArea.innerHTML = "";
8339
+ const errEl = document.createElement("pre");
8340
+ errEl.className = "db-query-error";
8341
+ errEl.textContent = err instanceof Error ? err.message : String(err);
8342
+ resultArea.appendChild(errEl);
8343
+ } finally {
8344
+ runBtn.disabled = false;
8345
+ }
7942
8346
  }
7943
- return `<img src="${url}" alt="" loading="lazy">`;
7944
- }
8347
+ function renderResultTable(result) {
8348
+ resultArea.hidden = false;
8349
+ resultArea.innerHTML = "";
8350
+ if (result.columns.length === 0) {
8351
+ resultArea.textContent = "Query returned no columns.";
8352
+ return;
8353
+ }
8354
+ const table2 = document.createElement("table");
8355
+ table2.className = "db-query-table";
8356
+ const thead = document.createElement("thead");
8357
+ const headRow = document.createElement("tr");
8358
+ const thNum = document.createElement("th");
8359
+ thNum.textContent = "#";
8360
+ thNum.className = "db-grid-rownum";
8361
+ headRow.appendChild(thNum);
8362
+ for (const col of result.columns) {
8363
+ const th = document.createElement("th");
8364
+ th.textContent = col;
8365
+ headRow.appendChild(th);
8366
+ }
8367
+ thead.appendChild(headRow);
8368
+ const tbody = document.createElement("tbody");
8369
+ for (let i2 = 0;i2 < result.rows.length; i2++) {
8370
+ const row = result.rows[i2];
8371
+ const tr = document.createElement("tr");
8372
+ if (i2 % 2 === 1)
8373
+ tr.classList.add("alt");
8374
+ const tdNum = document.createElement("td");
8375
+ tdNum.className = "db-grid-rownum";
8376
+ tdNum.textContent = String(i2 + 1);
8377
+ tr.appendChild(tdNum);
8378
+ for (const value of row) {
8379
+ const td = document.createElement("td");
8380
+ td.textContent = formatValue(value);
8381
+ if (value === null)
8382
+ td.classList.add("null");
8383
+ tr.appendChild(td);
8384
+ }
8385
+ tbody.appendChild(tr);
8386
+ }
8387
+ table2.append(thead, tbody);
8388
+ const wrapper = document.createElement("div");
8389
+ wrapper.className = "db-query-table-wrap";
8390
+ wrapper.appendChild(table2);
8391
+ resultArea.appendChild(wrapper);
8392
+ }
8393
+ async function runExplain() {
8394
+ const sql = textarea.value.trim();
8395
+ if (!sql)
8396
+ return;
8397
+ explainBtn.disabled = true;
8398
+ runBtn.disabled = true;
8399
+ statusSpan.textContent = "Explaining…";
8400
+ resultArea.hidden = true;
8401
+ try {
8402
+ const result = await callbacks.executeQuery(`EXPLAIN QUERY PLAN ${sql}`);
8403
+ if (result.error) {
8404
+ statusSpan.textContent = `Error (${result.elapsedMs}ms)`;
8405
+ resultArea.hidden = false;
8406
+ resultArea.innerHTML = "";
8407
+ const errEl = document.createElement("pre");
8408
+ errEl.className = "db-query-error";
8409
+ errEl.textContent = result.error;
8410
+ resultArea.appendChild(errEl);
8411
+ return;
8412
+ }
8413
+ statusSpan.textContent = `Explain (${result.elapsedMs}ms)`;
8414
+ renderResultTable(result);
8415
+ } catch (err) {
8416
+ statusSpan.textContent = "Failed";
8417
+ resultArea.hidden = false;
8418
+ resultArea.innerHTML = "";
8419
+ const errEl = document.createElement("pre");
8420
+ errEl.className = "db-query-error";
8421
+ errEl.textContent = err instanceof Error ? err.message : String(err);
8422
+ resultArea.appendChild(errEl);
8423
+ } finally {
8424
+ explainBtn.disabled = false;
8425
+ runBtn.disabled = false;
8426
+ }
8427
+ }
8428
+ runBtn.addEventListener("click", run);
8429
+ explainBtn.addEventListener("click", runExplain);
8430
+ historyBtn.addEventListener("click", () => {
8431
+ if (!historyDropdown.hidden) {
8432
+ historyDropdown.hidden = true;
8433
+ return;
8434
+ }
8435
+ const history2 = loadHistory();
8436
+ historyDropdown.innerHTML = "";
8437
+ if (history2.length === 0) {
8438
+ const empty = document.createElement("div");
8439
+ empty.className = "db-query-history-empty";
8440
+ empty.textContent = "No history";
8441
+ historyDropdown.appendChild(empty);
8442
+ } else {
8443
+ for (const sql of history2) {
8444
+ const item = document.createElement("div");
8445
+ item.className = "db-query-history-item";
8446
+ item.textContent = sql.length > 100 ? `${sql.slice(0, 100)}...` : sql;
8447
+ item.title = sql;
8448
+ item.addEventListener("click", () => {
8449
+ setSql(sql);
8450
+ historyDropdown.hidden = true;
8451
+ });
8452
+ historyDropdown.appendChild(item);
8453
+ }
8454
+ }
8455
+ historyDropdown.hidden = false;
8456
+ });
8457
+ document.addEventListener("click", (e2) => {
8458
+ if (!historyDropdown.hidden && !historyBtn.contains(e2.target) && !historyDropdown.contains(e2.target)) {
8459
+ historyDropdown.hidden = true;
8460
+ }
8461
+ });
8462
+ textarea.addEventListener("keydown", (e2) => {
8463
+ if ((e2.ctrlKey || e2.metaKey) && e2.key === "Enter") {
8464
+ e2.preventDefault();
8465
+ run();
8466
+ return;
8467
+ }
8468
+ if (e2.key === "Tab") {
8469
+ e2.preventDefault();
8470
+ const start = textarea.selectionStart;
8471
+ const end = textarea.selectionEnd;
8472
+ if (e2.shiftKey) {
8473
+ const before = textarea.value.slice(0, start);
8474
+ const lineStart = before.lastIndexOf(`
8475
+ `) + 1;
8476
+ const linePrefix = textarea.value.slice(lineStart, start);
8477
+ const spaces = linePrefix.match(/^ {1,2}/);
8478
+ if (spaces) {
8479
+ textarea.setRangeText("", lineStart, lineStart + spaces[0].length, "end");
8480
+ }
8481
+ } else {
8482
+ textarea.setRangeText(" ", start, end, "end");
8483
+ }
8484
+ syncHighlight();
8485
+ }
8486
+ });
8487
+ function focus() {
8488
+ textarea.focus();
8489
+ }
8490
+ function setSql(sql) {
8491
+ textarea.value = sql;
8492
+ syncHighlight();
8493
+ }
8494
+ return { el, focus, setSql };
8495
+ }
8496
+ function formatValue(value) {
8497
+ if (value === null)
8498
+ return "NULL";
8499
+ if (value instanceof Uint8Array)
8500
+ return `<blob ${value.byteLength} bytes>`;
8501
+ if (typeof value === "boolean")
8502
+ return value ? "true" : "false";
8503
+ if (typeof value === "object")
8504
+ return JSON.stringify(value);
8505
+ return String(value);
8506
+ }
8507
+
8508
+ // web-src/views/database/query-history-view.ts
8509
+ function createQueryHistoryView(callbacks) {
8510
+ const el = document.createElement("div");
8511
+ el.className = "db-query-history";
8512
+ const toolbar = document.createElement("div");
8513
+ toolbar.className = "db-query-history-toolbar";
8514
+ const refreshBtn = document.createElement("button");
8515
+ refreshBtn.className = "db-query-history-action";
8516
+ refreshBtn.type = "button";
8517
+ refreshBtn.textContent = "Refresh";
8518
+ refreshBtn.title = "Refresh history";
8519
+ const clearBtn = document.createElement("button");
8520
+ clearBtn.className = "db-query-history-action db-query-history-danger";
8521
+ clearBtn.type = "button";
8522
+ clearBtn.textContent = "Clear All";
8523
+ clearBtn.title = "Delete all query history";
8524
+ toolbar.append(refreshBtn, clearBtn);
8525
+ const body = document.createElement("div");
8526
+ body.className = "db-query-history-body-split";
8527
+ const listCol = document.createElement("div");
8528
+ listCol.className = "db-query-history-list-col";
8529
+ const listEl = document.createElement("div");
8530
+ listEl.className = "db-query-history-list";
8531
+ listCol.appendChild(listEl);
8532
+ const detailCol = document.createElement("div");
8533
+ detailCol.className = "db-query-history-detail-col";
8534
+ const detailPlaceholder = document.createElement("div");
8535
+ detailPlaceholder.className = "db-query-history-detail-placeholder";
8536
+ detailPlaceholder.textContent = "Select a query to view details";
8537
+ detailCol.appendChild(detailPlaceholder);
8538
+ body.append(listCol, detailCol);
8539
+ el.append(toolbar, body);
8540
+ let entries = [];
8541
+ const expandedIds = new Set;
8542
+ async function refresh() {
8543
+ const dbId = callbacks.getDbId();
8544
+ const params = dbId ? `?db=${encodeURIComponent(dbId)}` : "";
8545
+ try {
8546
+ const res = await fetch(`/_db/history${params}`);
8547
+ if (!res.ok)
8548
+ return;
8549
+ const state = await res.json();
8550
+ entries = state.entries;
8551
+ render();
8552
+ } catch {}
8553
+ }
8554
+ function render() {
8555
+ listEl.innerHTML = "";
8556
+ if (entries.length === 0) {
8557
+ const empty = document.createElement("div");
8558
+ empty.className = "db-query-history-empty";
8559
+ empty.textContent = "No query history";
8560
+ listEl.appendChild(empty);
8561
+ return;
8562
+ }
8563
+ for (const entry of entries) {
8564
+ listEl.appendChild(renderEntry(entry));
8565
+ }
8566
+ }
8567
+ let selectedEntryId = null;
8568
+ function selectEntry(entry) {
8569
+ selectedEntryId = entry.id;
8570
+ listEl.querySelectorAll(".db-query-history-entry").forEach((el2) => {
8571
+ el2.classList.toggle("selected", el2.dataset.id === entry.id);
8572
+ });
8573
+ renderDetail(entry);
8574
+ }
8575
+ function renderDetail(entry) {
8576
+ detailCol.innerHTML = "";
8577
+ const actions = document.createElement("div");
8578
+ actions.className = "db-query-history-detail-actions";
8579
+ const useBtn = document.createElement("button");
8580
+ useBtn.className = "db-btn db-btn-primary";
8581
+ useBtn.type = "button";
8582
+ useBtn.textContent = "Use in Editor";
8583
+ useBtn.addEventListener("click", () => callbacks.copySqlToQuery(entry.sql));
8584
+ const copyBtn = document.createElement("button");
8585
+ copyBtn.className = "db-btn";
8586
+ copyBtn.type = "button";
8587
+ copyBtn.textContent = "Copy SQL";
8588
+ copyBtn.addEventListener("click", () => {
8589
+ navigator.clipboard.writeText(entry.sql).then(() => {
8590
+ copyBtn.textContent = "Copied!";
8591
+ setTimeout(() => {
8592
+ copyBtn.textContent = "Copy SQL";
8593
+ }, 1500);
8594
+ }, () => {});
8595
+ });
8596
+ const deleteBtn = document.createElement("button");
8597
+ deleteBtn.className = "db-btn db-query-history-danger";
8598
+ deleteBtn.type = "button";
8599
+ deleteBtn.textContent = "Delete";
8600
+ deleteBtn.addEventListener("click", () => {
8601
+ deleteEntry(entry.id);
8602
+ });
8603
+ actions.append(useBtn, copyBtn, deleteBtn);
8604
+ const sqlBlock = document.createElement("pre");
8605
+ sqlBlock.className = "db-query-history-sql";
8606
+ sqlBlock.textContent = entry.sql;
8607
+ detailCol.append(actions, sqlBlock);
8608
+ if (entry.body) {
8609
+ const bodyBlock = document.createElement("div");
8610
+ bodyBlock.className = "db-query-history-body";
8611
+ bodyBlock.textContent = entry.body;
8612
+ detailCol.appendChild(bodyBlock);
8613
+ }
8614
+ if (entry.columns.length > 0 && entry.rowsPreview.length > 0) {
8615
+ detailCol.appendChild(renderPreviewTable(entry));
8616
+ }
8617
+ if (entry.savedRows < entry.rowCount) {
8618
+ const note = document.createElement("div");
8619
+ note.className = "db-query-history-truncated";
8620
+ note.textContent = `Showing ${entry.savedRows} of ${entry.rowCount} rows`;
8621
+ detailCol.appendChild(note);
8622
+ }
8623
+ }
8624
+ function renderEntry(entry) {
8625
+ const item = document.createElement("div");
8626
+ item.className = "db-query-history-entry";
8627
+ if (entry.id === selectedEntryId)
8628
+ item.classList.add("selected");
8629
+ item.dataset.id = entry.id;
8630
+ const meta = document.createElement("div");
8631
+ meta.className = "db-query-history-entry-meta";
8632
+ const byIcon = document.createElement("span");
8633
+ byIcon.className = "db-query-history-by";
8634
+ byIcon.textContent = entry.executedBy === "ai" ? "[AI]" : "[User]";
8635
+ const time = document.createElement("span");
8636
+ time.className = "db-query-history-time";
8637
+ time.textContent = formatTime(entry.executedAt);
8638
+ time.title = entry.executedAt;
8639
+ const stats = document.createElement("span");
8640
+ stats.className = "db-query-history-stats";
8641
+ const truncMark = entry.truncated ? "+" : "";
8642
+ stats.textContent = `${entry.rowCount}${truncMark} rows, ${entry.elapsedMs}ms`;
8643
+ meta.append(byIcon, time, stats);
8644
+ const title = document.createElement("div");
8645
+ title.className = "db-query-history-entry-title";
8646
+ title.textContent = entry.title || (entry.sql.length > 80 ? `${entry.sql.slice(0, 80)}...` : entry.sql);
8647
+ item.append(meta, title);
8648
+ item.addEventListener("click", () => selectEntry(entry));
8649
+ return item;
8650
+ }
8651
+ function renderPreviewTable(entry) {
8652
+ const wrapper = document.createElement("div");
8653
+ wrapper.className = "db-query-table-wrap";
8654
+ const table2 = document.createElement("table");
8655
+ table2.className = "db-query-table";
8656
+ const thead = document.createElement("thead");
8657
+ const headRow = document.createElement("tr");
8658
+ const thNum = document.createElement("th");
8659
+ thNum.textContent = "#";
8660
+ thNum.className = "db-grid-rownum";
8661
+ headRow.appendChild(thNum);
8662
+ for (const col of entry.columns) {
8663
+ const th = document.createElement("th");
8664
+ th.textContent = col;
8665
+ headRow.appendChild(th);
8666
+ }
8667
+ thead.appendChild(headRow);
8668
+ const tbody = document.createElement("tbody");
8669
+ for (let i2 = 0;i2 < entry.rowsPreview.length; i2++) {
8670
+ const row = entry.rowsPreview[i2];
8671
+ const tr = document.createElement("tr");
8672
+ if (i2 % 2 === 1)
8673
+ tr.classList.add("alt");
8674
+ const tdNum = document.createElement("td");
8675
+ tdNum.className = "db-grid-rownum";
8676
+ tdNum.textContent = String(i2 + 1);
8677
+ tr.appendChild(tdNum);
8678
+ for (const value of row) {
8679
+ const td = document.createElement("td");
8680
+ td.textContent = formatValue2(value);
8681
+ if (value === null)
8682
+ td.classList.add("null");
8683
+ tr.appendChild(td);
8684
+ }
8685
+ tbody.appendChild(tr);
8686
+ }
8687
+ table2.append(thead, tbody);
8688
+ wrapper.appendChild(table2);
8689
+ if (entry.savedRows < entry.rowCount) {
8690
+ const note = document.createElement("div");
8691
+ note.className = "db-query-history-truncated";
8692
+ note.textContent = `Showing ${entry.savedRows} of ${entry.rowCount} rows`;
8693
+ wrapper.appendChild(note);
8694
+ }
8695
+ return wrapper;
8696
+ }
8697
+ function resetDetailCol() {
8698
+ detailCol.innerHTML = "";
8699
+ detailCol.appendChild(detailPlaceholder);
8700
+ selectedEntryId = null;
8701
+ }
8702
+ async function deleteEntry(id) {
8703
+ try {
8704
+ await fetch("/_db/history/delete", {
8705
+ method: "POST",
8706
+ headers: {
8707
+ "Content-Type": "application/json",
8708
+ "X-Code-Viewer-Action": "1"
8709
+ },
8710
+ body: JSON.stringify({ id })
8711
+ });
8712
+ entries = entries.filter((e2) => e2.id !== id);
8713
+ expandedIds.delete(id);
8714
+ if (selectedEntryId === id)
8715
+ resetDetailCol();
8716
+ const itemEl = listEl.querySelector(`.db-query-history-entry[data-id="${CSS.escape(id)}"]`);
8717
+ if (itemEl) {
8718
+ itemEl.remove();
8719
+ if (entries.length === 0)
8720
+ render();
8721
+ } else {
8722
+ render();
8723
+ }
8724
+ } catch {}
8725
+ }
8726
+ clearBtn.addEventListener("click", async () => {
8727
+ const dbId = callbacks.getDbId();
8728
+ try {
8729
+ await fetch("/_db/history/clear", {
8730
+ method: "POST",
8731
+ headers: {
8732
+ "Content-Type": "application/json",
8733
+ "X-Code-Viewer-Action": "1"
8734
+ },
8735
+ body: JSON.stringify(dbId ? { db: dbId } : {})
8736
+ });
8737
+ entries = [];
8738
+ render();
8739
+ } catch {}
8740
+ });
8741
+ refreshBtn.addEventListener("click", () => {
8742
+ refresh();
8743
+ });
8744
+ return { el, refresh };
8745
+ }
8746
+ function formatTime(iso) {
8747
+ try {
8748
+ const d2 = new Date(iso);
8749
+ const pad = (n2) => String(n2).padStart(2, "0");
8750
+ return `${d2.getFullYear()}-${pad(d2.getMonth() + 1)}-${pad(d2.getDate())} ${pad(d2.getHours())}:${pad(d2.getMinutes())}:${pad(d2.getSeconds())}`;
8751
+ } catch {
8752
+ return iso;
8753
+ }
8754
+ }
8755
+ function formatValue2(value) {
8756
+ if (value === null)
8757
+ return "NULL";
8758
+ if (value instanceof Uint8Array)
8759
+ return `<blob ${value.byteLength} bytes>`;
8760
+ if (typeof value === "boolean")
8761
+ return value ? "true" : "false";
8762
+ if (typeof value === "object")
8763
+ return JSON.stringify(value);
8764
+ return String(value);
8765
+ }
8766
+
8767
+ // web-src/views/database/schema-view.ts
8768
+ function createSchemaView() {
8769
+ const el = document.createElement("div");
8770
+ el.className = "db-schema-view";
8771
+ el.hidden = true;
8772
+ function render(table2, columns, indexes, extra) {
8773
+ el.hidden = false;
8774
+ el.innerHTML = "";
8775
+ const header = document.createElement("div");
8776
+ header.className = "db-schema-header";
8777
+ header.textContent = `Schema: ${table2}`;
8778
+ el.appendChild(header);
8779
+ const colSectionHeader = document.createElement("div");
8780
+ colSectionHeader.className = "db-schema-section-header";
8781
+ colSectionHeader.textContent = "Columns";
8782
+ el.appendChild(colSectionHeader);
8783
+ const colTable = document.createElement("table");
8784
+ colTable.className = "db-schema-table";
8785
+ const thead = document.createElement("thead");
8786
+ const headRow = document.createElement("tr");
8787
+ for (const label of ["Column", "Type", "Nullable", "PK", "Default"]) {
8788
+ const th = document.createElement("th");
8789
+ th.textContent = label;
8790
+ headRow.appendChild(th);
8791
+ }
8792
+ thead.appendChild(headRow);
8793
+ const tbody = document.createElement("tbody");
8794
+ for (const col of columns) {
8795
+ const tr = document.createElement("tr");
8796
+ if (col.primaryKey)
8797
+ tr.classList.add("pk-row");
8798
+ const tdName = document.createElement("td");
8799
+ tdName.textContent = col.name;
8800
+ tdName.className = "db-schema-col-name";
8801
+ const tdType = document.createElement("td");
8802
+ tdType.textContent = col.type;
8803
+ tdType.className = "db-schema-col-type";
8804
+ const tdNull = document.createElement("td");
8805
+ tdNull.textContent = col.nullable ? "YES" : "NO";
8806
+ const tdPk = document.createElement("td");
8807
+ tdPk.textContent = col.primaryKey ? "PK" : "";
8808
+ if (col.primaryKey)
8809
+ tdPk.className = "db-schema-pk";
8810
+ const tdDefault = document.createElement("td");
8811
+ tdDefault.textContent = col.defaultValue ?? "";
8812
+ tdDefault.className = "db-schema-default";
8813
+ tr.append(tdName, tdType, tdNull, tdPk, tdDefault);
8814
+ tbody.appendChild(tr);
8815
+ }
8816
+ colTable.append(thead, tbody);
8817
+ el.appendChild(colTable);
8818
+ const fks = extra?.foreignKeys?.filter((fk) => fk.fromTable === table2);
8819
+ if (fks && fks.length > 0) {
8820
+ const fkHeader = document.createElement("div");
8821
+ fkHeader.className = "db-schema-section-header";
8822
+ fkHeader.textContent = "Foreign Keys";
8823
+ el.appendChild(fkHeader);
8824
+ const fkTable = document.createElement("table");
8825
+ fkTable.className = "db-schema-table";
8826
+ const fkThead = document.createElement("thead");
8827
+ const fkHeadRow = document.createElement("tr");
8828
+ for (const label of ["Column", "References Table", "References Column"]) {
8829
+ const th = document.createElement("th");
8830
+ th.textContent = label;
8831
+ fkHeadRow.appendChild(th);
8832
+ }
8833
+ fkThead.appendChild(fkHeadRow);
8834
+ const fkTbody = document.createElement("tbody");
8835
+ for (const fk of fks) {
8836
+ const tr = document.createElement("tr");
8837
+ const tdFrom = document.createElement("td");
8838
+ tdFrom.textContent = fk.fromColumn;
8839
+ const tdToTable = document.createElement("td");
8840
+ tdToTable.textContent = fk.toTable;
8841
+ const tdToCol = document.createElement("td");
8842
+ tdToCol.textContent = fk.toColumn;
8843
+ tr.append(tdFrom, tdToTable, tdToCol);
8844
+ fkTbody.appendChild(tr);
8845
+ }
8846
+ fkTable.append(fkThead, fkTbody);
8847
+ el.appendChild(fkTable);
8848
+ }
8849
+ const tableIndexes = indexes.filter((idx) => idx.table === table2);
8850
+ if (tableIndexes.length > 0) {
8851
+ const idxHeader = document.createElement("div");
8852
+ idxHeader.className = "db-schema-section-header";
8853
+ idxHeader.textContent = "Indexes";
8854
+ el.appendChild(idxHeader);
8855
+ const idxTable = document.createElement("table");
8856
+ idxTable.className = "db-schema-table";
8857
+ const idxThead = document.createElement("thead");
8858
+ const idxHeadRow = document.createElement("tr");
8859
+ for (const label of ["Name", "Columns", "Unique"]) {
8860
+ const th = document.createElement("th");
8861
+ th.textContent = label;
8862
+ idxHeadRow.appendChild(th);
8863
+ }
8864
+ idxThead.appendChild(idxHeadRow);
8865
+ const idxTbody = document.createElement("tbody");
8866
+ for (const idx of tableIndexes) {
8867
+ const tr = document.createElement("tr");
8868
+ const tdName = document.createElement("td");
8869
+ tdName.textContent = idx.name;
8870
+ const tdCols = document.createElement("td");
8871
+ tdCols.textContent = idx.columns.join(", ");
8872
+ const tdUnique = document.createElement("td");
8873
+ tdUnique.textContent = idx.unique ? "YES" : "NO";
8874
+ tr.append(tdName, tdCols, tdUnique);
8875
+ idxTbody.appendChild(tr);
8876
+ }
8877
+ idxTable.append(idxThead, idxTbody);
8878
+ el.appendChild(idxTable);
8879
+ }
8880
+ const triggers = extra?.triggers;
8881
+ if (triggers && triggers.length > 0) {
8882
+ const trigHeader = document.createElement("div");
8883
+ trigHeader.className = "db-schema-section-header";
8884
+ trigHeader.textContent = "Triggers";
8885
+ el.appendChild(trigHeader);
8886
+ for (const trig of triggers) {
8887
+ const trigBlock = document.createElement("div");
8888
+ trigBlock.className = "db-schema-trigger-block";
8889
+ const trigName = document.createElement("div");
8890
+ trigName.className = "db-schema-trigger-name";
8891
+ trigName.textContent = trig.name;
8892
+ const trigPre = document.createElement("pre");
8893
+ trigPre.className = "db-schema-ddl-pre";
8894
+ trigPre.textContent = trig.sql;
8895
+ trigBlock.append(trigName, trigPre);
8896
+ el.appendChild(trigBlock);
8897
+ }
8898
+ }
8899
+ const ddl = extra?.ddl;
8900
+ if (ddl) {
8901
+ const ddlHeader = document.createElement("div");
8902
+ ddlHeader.className = "db-schema-section-header";
8903
+ ddlHeader.textContent = "DDL";
8904
+ el.appendChild(ddlHeader);
8905
+ const ddlWrap = document.createElement("div");
8906
+ ddlWrap.className = "db-schema-ddl-wrap";
8907
+ const copyBtn = document.createElement("button");
8908
+ copyBtn.type = "button";
8909
+ copyBtn.className = "db-btn db-btn-sm db-schema-copy-btn";
8910
+ copyBtn.textContent = "Copy DDL";
8911
+ copyBtn.addEventListener("click", () => {
8912
+ navigator.clipboard.writeText(ddl).then(() => {
8913
+ copyBtn.textContent = "Copied!";
8914
+ setTimeout(() => {
8915
+ copyBtn.textContent = "Copy DDL";
8916
+ }, 1500);
8917
+ }, () => {});
8918
+ });
8919
+ const ddlPre = document.createElement("pre");
8920
+ ddlPre.className = "db-schema-ddl-pre";
8921
+ ddlPre.textContent = ddl;
8922
+ ddlWrap.append(copyBtn, ddlPre);
8923
+ el.appendChild(ddlWrap);
8924
+ }
8925
+ }
8926
+ function clear() {
8927
+ el.hidden = true;
8928
+ el.innerHTML = "";
8929
+ }
8930
+ return { el, render, clear };
8931
+ }
8932
+
8933
+ // web-src/views/database/snapshot-view.ts
8934
+ function changeTypeLabel(type) {
8935
+ if (type === "inserted")
8936
+ return "追加";
8937
+ if (type === "updated")
8938
+ return "更新";
8939
+ return "削除";
8940
+ }
8941
+ function snapshotLabel(s2) {
8942
+ const date = new Date(s2.createdAt).toLocaleString();
8943
+ const note = s2.note ? ` — ${s2.note}` : "";
8944
+ return `${date} (${s2.tables.length}テーブル)${note}`;
8945
+ }
8946
+ function arraysEqual(a2, b2) {
8947
+ if (a2.length !== b2.length)
8948
+ return false;
8949
+ const sa = [...a2].sort();
8950
+ const sb = [...b2].sort();
8951
+ return sa.every((v, i2) => v === sb[i2]);
8952
+ }
8953
+ function postJson(path, body) {
8954
+ return fetch(path, {
8955
+ method: "POST",
8956
+ headers: {
8957
+ "Content-Type": "application/json",
8958
+ "X-Code-Viewer-Action": "1"
8959
+ },
8960
+ body: JSON.stringify(body)
8961
+ });
8962
+ }
8963
+ function createSnapshotView(deps) {
8964
+ const el = document.createElement("div");
8965
+ el.className = "db-snapshot-view";
8966
+ const guide = document.createElement("div");
8967
+ guide.className = "db-snapshot-guide";
8968
+ guide.innerHTML = '<div class="db-snapshot-guide-title">スナップショット差分</div>' + '<div class="db-snapshot-guide-body">' + "① スナップショット取得 → ② アプリやテストでDB操作 → ③ もう一度取得すると自動で差分表示されます" + "</div>";
8969
+ const toolbar = document.createElement("div");
8970
+ toolbar.className = "db-snapshot-toolbar";
8971
+ const createBtn = document.createElement("button");
8972
+ createBtn.type = "button";
8973
+ createBtn.className = "db-snapshot-create-btn";
8974
+ createBtn.textContent = "スナップショット取得";
8975
+ const refreshBtn = document.createElement("button");
8976
+ refreshBtn.type = "button";
8977
+ refreshBtn.className = "db-snapshot-refresh-btn";
8978
+ refreshBtn.textContent = "更新";
8979
+ toolbar.append(createBtn, refreshBtn);
8980
+ const tableSelector = document.createElement("div");
8981
+ tableSelector.className = "db-snapshot-table-selector";
8982
+ tableSelector.hidden = true;
8983
+ const tableSelectorHeader = document.createElement("div");
8984
+ tableSelectorHeader.className = "db-snapshot-table-selector-header";
8985
+ tableSelectorHeader.textContent = "対象テーブルを選択";
8986
+ const tableCheckboxes = document.createElement("div");
8987
+ tableCheckboxes.className = "db-snapshot-table-checkboxes";
8988
+ const selectActions = document.createElement("div");
8989
+ selectActions.className = "db-snapshot-select-actions";
8990
+ const selectAllBtn = document.createElement("button");
8991
+ selectAllBtn.type = "button";
8992
+ selectAllBtn.textContent = "すべて選択";
8993
+ const deselectAllBtn = document.createElement("button");
8994
+ deselectAllBtn.type = "button";
8995
+ deselectAllBtn.textContent = "選択解除";
8996
+ const noteInput = document.createElement("input");
8997
+ noteInput.type = "text";
8998
+ noteInput.className = "db-snapshot-note-input";
8999
+ noteInput.placeholder = "例: ユーザー登録テスト前";
9000
+ const noteField = document.createElement("label");
9001
+ noteField.className = "db-snapshot-note-field";
9002
+ const noteLabel = document.createElement("span");
9003
+ noteLabel.className = "db-snapshot-note-label";
9004
+ noteLabel.textContent = "メモ";
9005
+ noteField.append(noteLabel, noteInput);
9006
+ const confirmBtn = document.createElement("button");
9007
+ confirmBtn.type = "button";
9008
+ confirmBtn.className = "db-snapshot-confirm-btn";
9009
+ confirmBtn.textContent = "取得開始";
9010
+ const cancelBtn = document.createElement("button");
9011
+ cancelBtn.type = "button";
9012
+ cancelBtn.className = "db-snapshot-cancel-btn";
9013
+ cancelBtn.textContent = "キャンセル";
9014
+ selectActions.append(selectAllBtn, deselectAllBtn, noteField, confirmBtn, cancelBtn);
9015
+ tableSelector.append(tableSelectorHeader, tableCheckboxes, selectActions);
9016
+ const mainArea = document.createElement("div");
9017
+ mainArea.className = "db-snapshot-main-area";
9018
+ el.append(guide, toolbar, tableSelector, mainArea);
9019
+ let snapshots = [];
9020
+ function showTableSelector() {
9021
+ const tables = deps.getTables();
9022
+ const lastTables = getLastTables();
9023
+ tableCheckboxes.innerHTML = "";
9024
+ for (const t2 of tables) {
9025
+ if (t2.type !== "table")
9026
+ continue;
9027
+ const label = document.createElement("label");
9028
+ label.className = "db-snapshot-table-label";
9029
+ const cb = document.createElement("input");
9030
+ cb.type = "checkbox";
9031
+ cb.checked = lastTables.length === 0 || lastTables.includes(t2.name);
9032
+ cb.value = t2.name;
9033
+ const rowInfo = t2.rowCount != null ? ` (${t2.rowCount}件)` : "";
9034
+ label.append(cb, ` ${t2.name}${rowInfo}`);
9035
+ tableCheckboxes.appendChild(label);
9036
+ }
9037
+ tableSelector.hidden = false;
9038
+ noteInput.value = "";
9039
+ }
9040
+ function getLastTables() {
9041
+ const dbId = deps.getDbId();
9042
+ if (!dbId)
9043
+ return [];
9044
+ const done = snapshots.filter((s2) => s2.status === "done");
9045
+ return done.length > 0 ? done[0].tables : [];
9046
+ }
9047
+ function getSelectedTables() {
9048
+ return Array.from(tableCheckboxes.querySelectorAll('input[type="checkbox"]:checked')).map((cb) => cb.value);
9049
+ }
9050
+ selectAllBtn.addEventListener("click", () => {
9051
+ tableCheckboxes.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
9052
+ cb.checked = true;
9053
+ });
9054
+ });
9055
+ deselectAllBtn.addEventListener("click", () => {
9056
+ tableCheckboxes.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
9057
+ cb.checked = false;
9058
+ });
9059
+ });
9060
+ createBtn.addEventListener("click", showTableSelector);
9061
+ cancelBtn.addEventListener("click", () => {
9062
+ tableSelector.hidden = true;
9063
+ });
9064
+ confirmBtn.addEventListener("click", async () => {
9065
+ const dbId = deps.getDbId();
9066
+ if (!dbId)
9067
+ return;
9068
+ const tables = getSelectedTables();
9069
+ if (tables.length === 0)
9070
+ return;
9071
+ confirmBtn.disabled = true;
9072
+ confirmBtn.textContent = "取得中...";
9073
+ try {
9074
+ await postJson("/_db/snapshot/create", {
9075
+ db: dbId,
9076
+ tables,
9077
+ note: noteInput.value.trim()
9078
+ });
9079
+ tableSelector.hidden = true;
9080
+ setTimeout(() => refreshAndAutoDiff(), 3000);
9081
+ } catch {} finally {
9082
+ confirmBtn.disabled = false;
9083
+ confirmBtn.textContent = "取得開始";
9084
+ }
9085
+ });
9086
+ refreshBtn.addEventListener("click", refresh);
9087
+ async function refresh() {
9088
+ const dbId = deps.getDbId();
9089
+ if (!dbId)
9090
+ return;
9091
+ try {
9092
+ const snapRes = await fetch(`/_db/snapshot/list?db=${encodeURIComponent(dbId)}`);
9093
+ if (snapRes.ok) {
9094
+ const data = await snapRes.json();
9095
+ snapshots = data.snapshots;
9096
+ }
9097
+ renderMain();
9098
+ } catch {}
9099
+ }
9100
+ async function refreshAndAutoDiff() {
9101
+ await refresh();
9102
+ const done = snapshots.filter((s2) => s2.status === "done");
9103
+ if (done.length < 2)
9104
+ return;
9105
+ const newest = done[0];
9106
+ const prev = done[1];
9107
+ if (!arraysEqual(newest.tables, prev.tables))
9108
+ return;
9109
+ showDiffInline(prev.id, newest.id);
9110
+ }
9111
+ function renderMain() {
9112
+ mainArea.innerHTML = "";
9113
+ const done = snapshots.filter((s2) => s2.status === "done");
9114
+ if (snapshots.length === 0) {
9115
+ mainArea.innerHTML = '<div class="db-snapshot-empty">まだスナップショットがありません。「スナップショット取得」で開始します。</div>';
9116
+ return;
9117
+ }
9118
+ const snapshotSection = document.createElement("div");
9119
+ snapshotSection.className = "db-snapshot-list-section";
9120
+ const snapTitle = document.createElement("h3");
9121
+ snapTitle.className = "db-snapshot-section-title";
9122
+ snapTitle.textContent = `スナップショット (${done.length}件)`;
9123
+ snapshotSection.appendChild(snapTitle);
9124
+ for (const snap of snapshots) {
9125
+ const item = document.createElement("div");
9126
+ item.className = "db-snapshot-item";
9127
+ const info = document.createElement("div");
9128
+ info.className = "db-snapshot-info";
9129
+ info.innerHTML = `<span class="db-snapshot-date">${new Date(snap.createdAt).toLocaleString()}</span>` + `<span class="db-snapshot-tables-count" title="${snap.tables.join(", ")}">${snap.tables.length}テーブル</span>` + (snap.note ? `<span class="db-snapshot-note">${snap.note}</span>` : "");
9130
+ const actions = document.createElement("div");
9131
+ actions.className = "db-snapshot-actions";
9132
+ const noteBtn = document.createElement("button");
9133
+ noteBtn.type = "button";
9134
+ noteBtn.textContent = "メモ";
9135
+ noteBtn.addEventListener("click", () => editNote(snap.id, snap.note));
9136
+ const deleteBtn = document.createElement("button");
9137
+ deleteBtn.type = "button";
9138
+ deleteBtn.textContent = "削除";
9139
+ deleteBtn.addEventListener("click", () => deleteSnap(snap.id));
9140
+ actions.append(noteBtn, deleteBtn);
9141
+ item.append(info, actions);
9142
+ snapshotSection.appendChild(item);
9143
+ }
9144
+ if (done.length >= 2) {
9145
+ const diffCreate = document.createElement("div");
9146
+ diffCreate.className = "db-snapshot-diff-create";
9147
+ const beforeSelect = document.createElement("select");
9148
+ beforeSelect.className = "db-snapshot-diff-select";
9149
+ const afterSelect = document.createElement("select");
9150
+ afterSelect.className = "db-snapshot-diff-select";
9151
+ const sorted = [...done].sort((a2, b2) => new Date(a2.createdAt).getTime() - new Date(b2.createdAt).getTime());
9152
+ for (const s2 of sorted) {
9153
+ const optB = document.createElement("option");
9154
+ optB.value = s2.id;
9155
+ optB.textContent = snapshotLabel(s2);
9156
+ beforeSelect.appendChild(optB);
9157
+ const optA = document.createElement("option");
9158
+ optA.value = s2.id;
9159
+ optA.textContent = snapshotLabel(s2);
9160
+ afterSelect.appendChild(optA);
9161
+ }
9162
+ if (sorted.length >= 2) {
9163
+ beforeSelect.selectedIndex = sorted.length - 2;
9164
+ afterSelect.selectedIndex = sorted.length - 1;
9165
+ }
9166
+ const diffBtn = document.createElement("button");
9167
+ diffBtn.type = "button";
9168
+ diffBtn.className = "db-snapshot-diff-btn";
9169
+ diffBtn.textContent = "手動で差分チェック";
9170
+ diffBtn.addEventListener("click", async () => {
9171
+ diffBtn.disabled = true;
9172
+ diffBtn.textContent = "比較中...";
9173
+ try {
9174
+ showDiffInline(beforeSelect.value, afterSelect.value);
9175
+ } finally {
9176
+ diffBtn.disabled = false;
9177
+ diffBtn.textContent = "手動で差分チェック";
9178
+ }
9179
+ });
9180
+ diffCreate.append(beforeSelect, document.createTextNode(" → "), afterSelect, diffBtn);
9181
+ snapshotSection.appendChild(diffCreate);
9182
+ }
9183
+ mainArea.appendChild(snapshotSection);
9184
+ }
9185
+ async function showDiffInline(beforeId, afterId) {
9186
+ const existing = mainArea.querySelector(".db-snapshot-diff-inline");
9187
+ if (existing)
9188
+ existing.remove();
9189
+ const loading = document.createElement("div");
9190
+ loading.className = "db-snapshot-diff-inline";
9191
+ loading.innerHTML = '<div class="db-snapshot-loading">差分を計算中...</div>';
9192
+ mainArea.appendChild(loading);
9193
+ try {
9194
+ const res = await fetch(`/_db/snapshot/diff/tables?before=${encodeURIComponent(beforeId)}&after=${encodeURIComponent(afterId)}`);
9195
+ if (!res.ok) {
9196
+ loading.innerHTML = '<div class="db-snapshot-error">差分の計算に失敗しました</div>';
9197
+ return;
9198
+ }
9199
+ const data = await res.json();
9200
+ loading.remove();
9201
+ renderDiffInline(beforeId, afterId, data.tables);
9202
+ } catch {
9203
+ loading.innerHTML = '<div class="db-snapshot-error">差分の計算に失敗しました</div>';
9204
+ }
9205
+ }
9206
+ function renderDiffInline(beforeId, afterId, tables) {
9207
+ const existing = mainArea.querySelector(".db-snapshot-diff-inline");
9208
+ if (existing)
9209
+ existing.remove();
9210
+ const section = document.createElement("div");
9211
+ section.className = "db-snapshot-diff-inline";
9212
+ const changedTables = tables.filter((t2) => t2.insertedCount + t2.updatedCount + t2.deletedCount > 0);
9213
+ const unchangedCount = tables.length - changedTables.length;
9214
+ const summary = document.createElement("div");
9215
+ summary.className = "db-snapshot-diff-summary";
9216
+ if (changedTables.length === 0) {
9217
+ summary.textContent = "変更は検出されませんでした。";
9218
+ section.appendChild(summary);
9219
+ mainArea.appendChild(section);
9220
+ return;
9221
+ }
9222
+ summary.textContent = `${changedTables.length}テーブルに変更あり` + (unchangedCount > 0 ? `、${unchangedCount}テーブルは変更なし` : "");
9223
+ section.appendChild(summary);
9224
+ for (const t2 of changedTables) {
9225
+ const tableEl = document.createElement("div");
9226
+ tableEl.className = "db-snapshot-diff-table";
9227
+ const tableHeader = document.createElement("div");
9228
+ tableHeader.className = "db-snapshot-diff-table-header";
9229
+ const nameSpan = document.createElement("span");
9230
+ nameSpan.className = "db-snapshot-diff-table-name";
9231
+ nameSpan.textContent = t2.tableName;
9232
+ const statsSpan = document.createElement("span");
9233
+ statsSpan.className = "db-snapshot-diff-table-stats";
9234
+ const parts = [];
9235
+ if (t2.insertedCount > 0)
9236
+ parts.push(`+${t2.insertedCount}`);
9237
+ if (t2.updatedCount > 0)
9238
+ parts.push(`~${t2.updatedCount}`);
9239
+ if (t2.deletedCount > 0)
9240
+ parts.push(`-${t2.deletedCount}`);
9241
+ statsSpan.textContent = parts.join(" ");
9242
+ tableHeader.append(nameSpan, statsSpan);
9243
+ tableHeader.style.cursor = "pointer";
9244
+ const rowsContainer = document.createElement("div");
9245
+ rowsContainer.className = "db-snapshot-diff-rows-container";
9246
+ loadDiffRows(beforeId, afterId, t2.tableName, rowsContainer);
9247
+ tableHeader.addEventListener("click", () => {
9248
+ rowsContainer.hidden = !rowsContainer.hidden;
9249
+ });
9250
+ tableEl.append(tableHeader, rowsContainer);
9251
+ section.appendChild(tableEl);
9252
+ }
9253
+ mainArea.appendChild(section);
9254
+ }
9255
+ async function loadDiffRows(beforeId, afterId, table2, container) {
9256
+ container.innerHTML = '<div class="db-snapshot-loading">読み込み中...</div>';
9257
+ try {
9258
+ const res = await fetch(`/_db/snapshot/diff/rows?before=${encodeURIComponent(beforeId)}&after=${encodeURIComponent(afterId)}&table=${encodeURIComponent(table2)}&limit=200`);
9259
+ if (!res.ok) {
9260
+ container.innerHTML = '<div class="db-snapshot-error">読み込みに失敗しました</div>';
9261
+ return;
9262
+ }
9263
+ const data = await res.json();
9264
+ renderDiffRows(container, data.rows, data.total);
9265
+ } catch {
9266
+ container.innerHTML = '<div class="db-snapshot-error">読み込みに失敗しました</div>';
9267
+ }
9268
+ }
9269
+ function renderDiffRows(container, rows, total) {
9270
+ container.innerHTML = "";
9271
+ if (rows.length === 0)
9272
+ return;
9273
+ const allCols = new Set;
9274
+ for (const row of rows) {
9275
+ if (row.beforeValues)
9276
+ for (const k of Object.keys(row.beforeValues))
9277
+ allCols.add(k);
9278
+ if (row.afterValues)
9279
+ for (const k of Object.keys(row.afterValues))
9280
+ allCols.add(k);
9281
+ }
9282
+ const columns = [...allCols];
9283
+ const table2 = document.createElement("table");
9284
+ table2.className = "db-snap-diff-grid";
9285
+ const thead = document.createElement("thead");
9286
+ const headRow = document.createElement("tr");
9287
+ const thType = document.createElement("th");
9288
+ thType.textContent = "";
9289
+ headRow.appendChild(thType);
9290
+ for (const col of columns) {
9291
+ const th = document.createElement("th");
9292
+ th.textContent = col;
9293
+ headRow.appendChild(th);
9294
+ }
9295
+ thead.appendChild(headRow);
9296
+ table2.appendChild(thead);
9297
+ const tbody = document.createElement("tbody");
9298
+ for (const row of rows) {
9299
+ if (row.changeType === "updated" && row.beforeValues && row.afterValues) {
9300
+ const trBefore = document.createElement("tr");
9301
+ trBefore.className = "snap-diff-del";
9302
+ const tdTypeBefore = document.createElement("td");
9303
+ tdTypeBefore.className = "snap-diff-type-cell";
9304
+ tdTypeBefore.textContent = "更新前";
9305
+ tdTypeBefore.rowSpan = 2;
9306
+ trBefore.appendChild(tdTypeBefore);
9307
+ for (const col of columns) {
9308
+ const td = document.createElement("td");
9309
+ const bs = row.beforeValues[col] == null ? "NULL" : String(row.beforeValues[col]);
9310
+ const as_ = row.afterValues[col] == null ? "NULL" : String(row.afterValues[col]);
9311
+ td.textContent = bs;
9312
+ if (bs !== as_)
9313
+ td.classList.add("snap-diff-changed-cell");
9314
+ trBefore.appendChild(td);
9315
+ }
9316
+ tbody.appendChild(trBefore);
9317
+ const trAfter = document.createElement("tr");
9318
+ trAfter.className = "snap-diff-add";
9319
+ for (const col of columns) {
9320
+ const td = document.createElement("td");
9321
+ const bs = row.beforeValues[col] == null ? "NULL" : String(row.beforeValues[col]);
9322
+ const as_ = row.afterValues[col] == null ? "NULL" : String(row.afterValues[col]);
9323
+ td.textContent = as_;
9324
+ if (bs !== as_)
9325
+ td.classList.add("snap-diff-changed-cell");
9326
+ trAfter.appendChild(td);
9327
+ }
9328
+ tbody.appendChild(trAfter);
9329
+ } else {
9330
+ const tr = document.createElement("tr");
9331
+ tr.className = row.changeType === "inserted" ? "snap-diff-add" : "snap-diff-del";
9332
+ const tdType = document.createElement("td");
9333
+ tdType.className = "snap-diff-type-cell";
9334
+ tdType.textContent = changeTypeLabel(row.changeType);
9335
+ tr.appendChild(tdType);
9336
+ const values = row.changeType === "inserted" ? row.afterValues : row.beforeValues;
9337
+ for (const col of columns) {
9338
+ const td = document.createElement("td");
9339
+ const v = values?.[col];
9340
+ td.textContent = v == null ? "NULL" : String(v);
9341
+ tr.appendChild(td);
9342
+ }
9343
+ tbody.appendChild(tr);
9344
+ }
9345
+ }
9346
+ table2.appendChild(tbody);
9347
+ container.appendChild(table2);
9348
+ if (total > rows.length) {
9349
+ const more = document.createElement("div");
9350
+ more.className = "db-snapshot-diff-more";
9351
+ more.textContent = `全${total}件中 ${rows.length}件を表示中`;
9352
+ container.appendChild(more);
9353
+ }
9354
+ }
9355
+ async function editNote(snapshotId, currentNote) {
9356
+ const existing = el.querySelector(".db-snapshot-inline-dialog");
9357
+ if (existing)
9358
+ existing.remove();
9359
+ const dialog = document.createElement("div");
9360
+ dialog.className = "db-snapshot-inline-dialog";
9361
+ const input = document.createElement("input");
9362
+ input.type = "text";
9363
+ input.className = "db-snapshot-note-input";
9364
+ input.value = currentNote || "";
9365
+ input.placeholder = "メモを入力";
9366
+ const saveBtn = document.createElement("button");
9367
+ saveBtn.type = "button";
9368
+ saveBtn.textContent = "保存";
9369
+ saveBtn.className = "db-snapshot-confirm-btn";
9370
+ const cancelDlgBtn = document.createElement("button");
9371
+ cancelDlgBtn.type = "button";
9372
+ cancelDlgBtn.textContent = "キャンセル";
9373
+ cancelDlgBtn.addEventListener("click", () => dialog.remove());
9374
+ saveBtn.addEventListener("click", async () => {
9375
+ await postJson("/_db/snapshot/update-note", {
9376
+ id: snapshotId,
9377
+ note: input.value
9378
+ });
9379
+ dialog.remove();
9380
+ refresh();
9381
+ });
9382
+ input.addEventListener("keydown", (e2) => {
9383
+ if (e2.key === "Enter")
9384
+ saveBtn.click();
9385
+ if (e2.key === "Escape")
9386
+ dialog.remove();
9387
+ });
9388
+ dialog.append(input, saveBtn, cancelDlgBtn);
9389
+ const item = el.querySelector(`.db-snapshot-item [title="${snapshotId}"]`)?.closest(".db-snapshot-item");
9390
+ if (item) {
9391
+ item.after(dialog);
9392
+ } else {
9393
+ mainArea.prepend(dialog);
9394
+ }
9395
+ input.focus();
9396
+ }
9397
+ async function deleteSnap(snapshotId) {
9398
+ await postJson("/_db/snapshot/delete", { id: snapshotId });
9399
+ refresh();
9400
+ }
9401
+ function handleSse(data) {
9402
+ try {
9403
+ const parsed = JSON.parse(data);
9404
+ if (parsed.action === "created" || parsed.action === "error") {
9405
+ refreshAndAutoDiff();
9406
+ }
9407
+ } catch {}
9408
+ }
9409
+ return { el, refresh, handleSse };
9410
+ }
9411
+
9412
+ // web-src/views/database/table-grid.ts
9413
+ var ROW_HEIGHT = 28;
9414
+ var OVERSCAN = 20;
9415
+ var PAGE_SIZE = 200;
9416
+ var FILTER_DEBOUNCE_MS = 300;
9417
+ var DEFAULT_COL_WIDTH = 180;
9418
+ function createTableGrid(callbacks) {
9419
+ const el = document.createElement("div");
9420
+ el.className = "db-grid";
9421
+ const filterBar = document.createElement("div");
9422
+ filterBar.className = "db-grid-filter-bar";
9423
+ const filterIcon = document.createElement("span");
9424
+ filterIcon.className = "db-grid-filter-icon";
9425
+ filterIcon.textContent = "\uD83D\uDD0D";
9426
+ const filterInput = document.createElement("input");
9427
+ filterInput.type = "search";
9428
+ filterInput.className = "db-grid-filter-input";
9429
+ filterInput.placeholder = "Search all columns…";
9430
+ filterInput.autocomplete = "off";
9431
+ const filterClear = document.createElement("button");
9432
+ filterClear.type = "button";
9433
+ filterClear.className = "db-btn db-btn-icon db-grid-filter-clear";
9434
+ filterClear.textContent = "×";
9435
+ filterClear.hidden = true;
9436
+ filterBar.append(filterIcon, filterInput, filterClear);
9437
+ const headerWrap = document.createElement("div");
9438
+ headerWrap.className = "db-grid-header-wrap";
9439
+ const headerRow = document.createElement("div");
9440
+ headerRow.className = "db-grid-header";
9441
+ headerWrap.appendChild(headerRow);
9442
+ const filterRowWrap = document.createElement("div");
9443
+ filterRowWrap.className = "db-grid-filter-row-wrap";
9444
+ const filterRow = document.createElement("div");
9445
+ filterRow.className = "db-grid-filter-row";
9446
+ filterRowWrap.appendChild(filterRow);
9447
+ const viewport = document.createElement("div");
9448
+ viewport.className = "db-grid-viewport";
9449
+ const spacer = document.createElement("div");
9450
+ spacer.className = "db-grid-spacer";
9451
+ const body = document.createElement("div");
9452
+ body.className = "db-grid-body";
9453
+ const detailPanel = document.createElement("div");
9454
+ detailPanel.className = "db-grid-detail-panel";
9455
+ detailPanel.hidden = true;
9456
+ viewport.append(spacer, body);
9457
+ el.append(filterBar, headerWrap, filterRowWrap, viewport, detailPanel);
9458
+ let currentTable = "";
9459
+ let columns = [];
9460
+ let columnNames = [];
9461
+ let totalRows = 0;
9462
+ const columnFilters = new Map;
9463
+ let globalSearchValue = "";
9464
+ let sort = null;
9465
+ let pageCache = new Map;
9466
+ let pendingPages = new Set;
9467
+ let loadGeneration = 0;
9468
+ let rafId = 0;
9469
+ let statusEl = null;
9470
+ let filterTimer = null;
9471
+ let selectedRowIndex = -1;
9472
+ const colWidths = new Map;
9473
+ function storageKey() {
9474
+ const project = callbacks.getProjectName?.() ?? "";
9475
+ if (!currentTable)
9476
+ return null;
9477
+ return `db:col-widths:${project}:${currentTable}`;
9478
+ }
9479
+ function saveColWidths() {
9480
+ const key = storageKey();
9481
+ if (!key)
9482
+ return;
9483
+ const obj = {};
9484
+ for (const [name, w] of colWidths) {
9485
+ obj[name] = w;
9486
+ }
9487
+ try {
9488
+ localStorage.setItem(key, JSON.stringify(obj));
9489
+ } catch {}
9490
+ }
9491
+ function loadColWidths() {
9492
+ colWidths.clear();
9493
+ const key = storageKey();
9494
+ if (!key)
9495
+ return;
9496
+ try {
9497
+ const raw = localStorage.getItem(key);
9498
+ if (raw) {
9499
+ const obj = JSON.parse(raw);
9500
+ for (const [name, w] of Object.entries(obj)) {
9501
+ if (typeof w === "number" && w > 0) {
9502
+ colWidths.set(name, w);
9503
+ }
9504
+ }
9505
+ }
9506
+ } catch {}
9507
+ }
9508
+ function getColWidth(colName) {
9509
+ return colWidths.get(colName) ?? DEFAULT_COL_WIDTH;
9510
+ }
9511
+ function applyColWidth(colName, index) {
9512
+ const w = getColWidth(colName);
9513
+ const headerCells = headerRow.querySelectorAll(".db-grid-header-cell");
9514
+ if (headerCells[index]) {
9515
+ headerCells[index].style.width = `${w}px`;
9516
+ }
9517
+ const filterCells = filterRow.querySelectorAll(".db-grid-filter-cell");
9518
+ if (filterCells[index]) {
9519
+ filterCells[index].style.width = `${w}px`;
9520
+ }
9521
+ }
9522
+ function autoFitColumn(colName, colIndex) {
9523
+ const measure = document.createElement("span");
9524
+ measure.style.cssText = "position:absolute;visibility:hidden;white-space:nowrap;font:inherit;padding:0 8px;";
9525
+ document.body.appendChild(measure);
9526
+ const headerLabel = columns[colIndex]?.name || colName;
9527
+ const typeLabel = columns[colIndex]?.type || "";
9528
+ measure.textContent = `${headerLabel} ${typeLabel} ▲`;
9529
+ let maxW = measure.offsetWidth + 16;
9530
+ const rows = body.querySelectorAll(".db-grid-row");
9531
+ for (const row of rows) {
9532
+ const cells = row.querySelectorAll(".db-grid-cell:not(.db-grid-rownum)");
9533
+ const cell = cells[colIndex];
9534
+ if (cell) {
9535
+ measure.textContent = cell.textContent || "";
9536
+ maxW = Math.max(maxW, measure.offsetWidth);
9537
+ }
9538
+ }
9539
+ document.body.removeChild(measure);
9540
+ const fitted = Math.max(60, Math.min(600, maxW));
9541
+ colWidths.set(colName, fitted);
9542
+ applyColWidth(colName, colIndex);
9543
+ saveColWidths();
9544
+ syncContentWidth();
9545
+ renderViewport();
9546
+ }
9547
+ function collectFilters() {
9548
+ const filters = [];
9549
+ for (const [col, val] of columnFilters) {
9550
+ if (val)
9551
+ filters.push({ column: col, value: val });
9552
+ }
9553
+ if (globalSearchValue && filters.length === 0) {
9554
+ for (const col of columnNames) {
9555
+ filters.push({ column: col, value: globalSearchValue });
9556
+ }
9557
+ }
9558
+ return filters;
9559
+ }
9560
+ function invalidateData() {
9561
+ pageCache = new Map;
9562
+ pendingPages = new Set;
9563
+ loadGeneration++;
9564
+ viewport.scrollTop = 0;
9565
+ ensurePage(0);
9566
+ }
9567
+ function clear() {
9568
+ currentTable = "";
9569
+ columns = [];
9570
+ columnNames = [];
9571
+ totalRows = 0;
9572
+ sort = null;
9573
+ columnFilters.clear();
9574
+ globalSearchValue = "";
9575
+ filterInput.value = "";
9576
+ filterClear.hidden = true;
9577
+ filterRow.innerHTML = "";
9578
+ pageCache = new Map;
9579
+ pendingPages = new Set;
9580
+ loadGeneration++;
9581
+ cancelAnimationFrame(rafId);
9582
+ if (filterTimer)
9583
+ clearTimeout(filterTimer);
9584
+ headerRow.innerHTML = "";
9585
+ body.innerHTML = "";
9586
+ spacer.style.height = "0px";
9587
+ statusEl?.remove();
9588
+ statusEl = null;
9589
+ detailPanel.hidden = true;
9590
+ detailPanel.innerHTML = "";
9591
+ colWidths.clear();
9592
+ }
9593
+ function showCellDetail(colIndex, value) {
9594
+ const colName = columnNames[colIndex];
9595
+ const colType = columns[colIndex]?.type || "";
9596
+ detailPanel.hidden = false;
9597
+ detailPanel.innerHTML = "";
9598
+ const header = document.createElement("div");
9599
+ header.className = "db-grid-detail-header";
9600
+ const title = document.createElement("span");
9601
+ title.className = "db-grid-detail-title";
9602
+ title.textContent = `${colName} (${colType})`;
9603
+ const closeBtn = document.createElement("button");
9604
+ closeBtn.type = "button";
9605
+ closeBtn.className = "db-btn db-btn-icon db-grid-detail-close";
9606
+ closeBtn.textContent = "×";
9607
+ closeBtn.addEventListener("click", () => {
9608
+ detailPanel.hidden = true;
9609
+ });
9610
+ header.append(title, closeBtn);
9611
+ const content = document.createElement("div");
9612
+ content.className = "db-grid-detail-content";
9613
+ if (value === null) {
9614
+ content.textContent = "NULL";
9615
+ content.classList.add("null");
9616
+ } else if (value instanceof Uint8Array) {
9617
+ content.textContent = `BLOB (${value.byteLength} bytes)`;
9618
+ content.classList.add("blob");
9619
+ } else {
9620
+ const str = typeof value === "object" ? JSON.stringify(value) : String(value);
9621
+ if (str.length > 0 && (str[0] === "{" || str[0] === "[")) {
9622
+ try {
9623
+ const parsed = JSON.parse(str);
9624
+ const pre = document.createElement("pre");
9625
+ pre.className = "db-grid-detail-json";
9626
+ pre.textContent = JSON.stringify(parsed, null, 2);
9627
+ content.appendChild(pre);
9628
+ } catch {
9629
+ content.textContent = str;
9630
+ }
9631
+ } else {
9632
+ content.textContent = str;
9633
+ }
9634
+ }
9635
+ detailPanel.append(header, content);
9636
+ }
9637
+ function renderHeader() {
9638
+ headerRow.innerHTML = "";
9639
+ const rowNum = document.createElement("div");
9640
+ rowNum.className = "db-grid-cell db-grid-rownum-header";
9641
+ rowNum.textContent = "#";
9642
+ headerRow.appendChild(rowNum);
9643
+ for (let i2 = 0;i2 < columns.length; i2++) {
9644
+ const col = columns[i2];
9645
+ const cell = document.createElement("div");
9646
+ cell.className = "db-grid-cell db-grid-header-cell";
9647
+ cell.style.width = `${getColWidth(col.name)}px`;
9648
+ if (sort?.column === col.name) {
9649
+ cell.classList.add("sorted");
9650
+ cell.dataset.dir = sort.direction;
9651
+ }
9652
+ const label = document.createElement("span");
9653
+ label.className = "db-grid-header-label";
9654
+ label.textContent = col.name;
9655
+ const typeTag = document.createElement("span");
9656
+ typeTag.className = "db-grid-header-type";
9657
+ typeTag.textContent = col.type;
9658
+ if (col.primaryKey)
9659
+ typeTag.classList.add("pk");
9660
+ const sortIcon = document.createElement("span");
9661
+ sortIcon.className = "db-grid-sort-icon";
9662
+ sortIcon.textContent = sort?.column === col.name ? sort.direction === "asc" ? "▲" : "▼" : "";
9663
+ const resizeHandle = document.createElement("div");
9664
+ resizeHandle.className = "db-grid-resize-handle";
9665
+ const colIndex = i2;
9666
+ resizeHandle.addEventListener("mousedown", (e2) => {
9667
+ e2.preventDefault();
9668
+ e2.stopPropagation();
9669
+ startResize(colIndex, e2);
9670
+ });
9671
+ cell.append(label, typeTag, sortIcon, resizeHandle);
9672
+ cell.addEventListener("click", (e2) => {
9673
+ if (e2.target.classList.contains("db-grid-resize-handle"))
9674
+ return;
9675
+ handleSort(col.name);
9676
+ });
9677
+ cell.addEventListener("dblclick", (e2) => {
9678
+ if (e2.target.classList.contains("db-grid-resize-handle")) {
9679
+ e2.preventDefault();
9680
+ e2.stopPropagation();
9681
+ autoFitColumn(col.name, colIndex);
9682
+ }
9683
+ });
9684
+ headerRow.appendChild(cell);
9685
+ }
9686
+ renderFilterRow();
9687
+ syncContentWidth();
9688
+ }
9689
+ function startResize(colIndex, startEvent) {
9690
+ const colName = columnNames[colIndex];
9691
+ const startX = startEvent.clientX;
9692
+ const startWidth = getColWidth(colName);
9693
+ document.body.classList.add("db-resizing");
9694
+ const onMouseMove = (e2) => {
9695
+ const delta = e2.clientX - startX;
9696
+ const newWidth = Math.max(60, startWidth + delta);
9697
+ colWidths.set(colName, newWidth);
9698
+ applyColWidth(colName, colIndex);
9699
+ syncContentWidth();
9700
+ renderViewport();
9701
+ };
9702
+ const onMouseUp = () => {
9703
+ document.body.classList.remove("db-resizing");
9704
+ document.removeEventListener("mousemove", onMouseMove);
9705
+ document.removeEventListener("mouseup", onMouseUp);
9706
+ saveColWidths();
9707
+ };
9708
+ document.addEventListener("mousemove", onMouseMove);
9709
+ document.addEventListener("mouseup", onMouseUp);
9710
+ }
9711
+ function renderFilterRow() {
9712
+ filterRow.innerHTML = "";
9713
+ const rowNumSpacer = document.createElement("div");
9714
+ rowNumSpacer.className = "db-grid-cell db-grid-rownum-header db-grid-filter-spacer";
9715
+ filterRow.appendChild(rowNumSpacer);
9716
+ for (let i2 = 0;i2 < columns.length; i2++) {
9717
+ const col = columns[i2];
9718
+ const cell = document.createElement("div");
9719
+ cell.className = "db-grid-cell db-grid-filter-cell";
9720
+ cell.style.width = `${getColWidth(col.name)}px`;
9721
+ const input = document.createElement("input");
9722
+ input.type = "search";
9723
+ input.className = "db-grid-col-filter";
9724
+ input.placeholder = `${col.name}…`;
9725
+ input.autocomplete = "off";
9726
+ input.value = columnFilters.get(col.name) || "";
9727
+ input.addEventListener("input", () => {
9728
+ const val = input.value.trim();
9729
+ if (val) {
9730
+ columnFilters.set(col.name, val);
9731
+ } else {
9732
+ columnFilters.delete(col.name);
9733
+ }
9734
+ scheduleFilter();
9735
+ });
9736
+ input.addEventListener("keydown", (e2) => {
9737
+ if (e2.key === "Escape") {
9738
+ input.value = "";
9739
+ columnFilters.delete(col.name);
9740
+ scheduleFilter();
9741
+ }
9742
+ });
9743
+ cell.appendChild(input);
9744
+ filterRow.appendChild(cell);
9745
+ }
9746
+ }
9747
+ function scheduleFilter() {
9748
+ if (filterTimer)
9749
+ clearTimeout(filterTimer);
9750
+ filterTimer = setTimeout(() => {
9751
+ invalidateData();
9752
+ }, FILTER_DEBOUNCE_MS);
9753
+ }
9754
+ function syncContentWidth() {
9755
+ requestAnimationFrame(() => {
9756
+ const w = headerRow.scrollWidth;
9757
+ if (w > 0) {
9758
+ spacer.style.minWidth = `${w}px`;
9759
+ body.style.minWidth = `${w}px`;
9760
+ filterRow.style.minWidth = `${w}px`;
9761
+ }
9762
+ });
9763
+ }
9764
+ function handleSort(column) {
9765
+ if (sort?.column === column) {
9766
+ sort = sort.direction === "asc" ? { column, direction: "desc" } : null;
9767
+ } else {
9768
+ sort = { column, direction: "asc" };
9769
+ }
9770
+ pageCache = new Map;
9771
+ pendingPages = new Set;
9772
+ renderHeader();
9773
+ renderViewport();
9774
+ }
9775
+ function ensurePage(pageStart) {
9776
+ if (pageCache.has(pageStart) || pendingPages.has(pageStart))
9777
+ return;
9778
+ pendingPages.add(pageStart);
9779
+ const gen = loadGeneration;
9780
+ const filters = collectFilters();
9781
+ callbacks.fetchPage(currentTable, pageStart, PAGE_SIZE, sort, filters).then((data) => {
9782
+ if (gen !== loadGeneration)
9783
+ return;
9784
+ pendingPages.delete(pageStart);
9785
+ pageCache.set(pageStart, data.rows);
9786
+ totalRows = data.totalRows;
9787
+ spacer.style.height = `${totalRows * ROW_HEIGHT}px`;
9788
+ updateStatus();
9789
+ renderViewport();
9790
+ }).catch(() => {
9791
+ if (gen === loadGeneration)
9792
+ pendingPages.delete(pageStart);
9793
+ });
9794
+ }
9795
+ function renderViewport() {
9796
+ cancelAnimationFrame(rafId);
9797
+ rafId = requestAnimationFrame(() => {
9798
+ const scrollTop = viewport.scrollTop;
9799
+ const viewHeight = viewport.clientHeight;
9800
+ const startRow = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN);
9801
+ const endRow = Math.min(totalRows, Math.ceil((scrollTop + viewHeight) / ROW_HEIGHT) + OVERSCAN);
9802
+ const neededPageStart = Math.floor(startRow / PAGE_SIZE) * PAGE_SIZE;
9803
+ const neededPageEnd = Math.floor(endRow / PAGE_SIZE) * PAGE_SIZE;
9804
+ for (let p2 = neededPageStart;p2 <= neededPageEnd; p2 += PAGE_SIZE) {
9805
+ ensurePage(p2);
9806
+ }
9807
+ body.innerHTML = "";
9808
+ body.style.transform = `translateY(${startRow * ROW_HEIGHT}px)`;
9809
+ for (let i2 = startRow;i2 < endRow; i2++) {
9810
+ const pageStart = Math.floor(i2 / PAGE_SIZE) * PAGE_SIZE;
9811
+ const pageRows = pageCache.get(pageStart);
9812
+ const rowData = pageRows ? pageRows[i2 - pageStart] : null;
9813
+ const row = document.createElement("div");
9814
+ row.className = "db-grid-row";
9815
+ if (i2 % 2 === 1)
9816
+ row.classList.add("alt");
9817
+ if (i2 === selectedRowIndex)
9818
+ row.classList.add("selected");
9819
+ const rowIndex = i2;
9820
+ row.addEventListener("click", () => {
9821
+ selectedRowIndex = rowIndex;
9822
+ body.querySelectorAll(".db-grid-row.selected").forEach((r2) => {
9823
+ r2.classList.remove("selected");
9824
+ });
9825
+ row.classList.add("selected");
9826
+ });
9827
+ const rowNum = document.createElement("div");
9828
+ rowNum.className = "db-grid-cell db-grid-rownum";
9829
+ rowNum.textContent = String(i2 + 1);
9830
+ row.appendChild(rowNum);
9831
+ if (rowData) {
9832
+ for (let c2 = 0;c2 < columnNames.length; c2++) {
9833
+ const cell = document.createElement("div");
9834
+ cell.className = "db-grid-cell";
9835
+ cell.style.width = `${getColWidth(columnNames[c2])}px`;
9836
+ const val = rowData[c2];
9837
+ cell.textContent = formatValue3(val);
9838
+ if (val === null)
9839
+ cell.classList.add("null");
9840
+ else if (val instanceof Uint8Array)
9841
+ cell.classList.add("blob");
9842
+ else if (typeof val === "string" && val === "")
9843
+ cell.classList.add("empty");
9844
+ cell.style.cursor = "pointer";
9845
+ const cellValue = val;
9846
+ const cellColIndex = c2;
9847
+ cell.addEventListener("click", (e2) => {
9848
+ e2.stopPropagation();
9849
+ selectedRowIndex = rowIndex;
9850
+ body.querySelectorAll(".db-grid-row.selected").forEach((r2) => {
9851
+ r2.classList.remove("selected");
9852
+ });
9853
+ row.classList.add("selected");
9854
+ showCellDetail(cellColIndex, cellValue);
9855
+ const text2 = formatValueForCopy(cellValue);
9856
+ navigator.clipboard.writeText(text2).then(() => {
9857
+ cell.classList.add("copied");
9858
+ setTimeout(() => cell.classList.remove("copied"), 600);
9859
+ }, () => {});
9860
+ });
9861
+ row.appendChild(cell);
9862
+ }
9863
+ } else {
9864
+ for (let c2 = 0;c2 < columnNames.length; c2++) {
9865
+ const cell = document.createElement("div");
9866
+ cell.className = "db-grid-cell loading";
9867
+ cell.style.width = `${getColWidth(columnNames[c2])}px`;
9868
+ cell.textContent = "…";
9869
+ row.appendChild(cell);
9870
+ }
9871
+ }
9872
+ body.appendChild(row);
9873
+ }
9874
+ });
9875
+ }
9876
+ function triggerExport(format2) {
9877
+ const dbId = callbacks.getDbId();
9878
+ if (!dbId || !currentTable)
9879
+ return;
9880
+ const params = new URLSearchParams({
9881
+ db: dbId,
9882
+ table: currentTable,
9883
+ format: format2
9884
+ });
9885
+ if (sort) {
9886
+ params.set("sort", sort.column);
9887
+ params.set("dir", sort.direction);
9888
+ }
9889
+ const filters = collectFilters();
9890
+ if (filters.length > 0) {
9891
+ params.set("filters", JSON.stringify(filters));
9892
+ }
9893
+ const a2 = document.createElement("a");
9894
+ a2.href = `/_db/export?${params}`;
9895
+ a2.download = `${currentTable}.${format2}`;
9896
+ document.body.appendChild(a2);
9897
+ a2.click();
9898
+ a2.remove();
9899
+ }
9900
+ let exportCsvBtn = null;
9901
+ let exportJsonBtn = null;
9902
+ function updateStatus() {
9903
+ if (!statusEl) {
9904
+ statusEl = document.createElement("div");
9905
+ statusEl.className = "db-grid-status";
9906
+ exportCsvBtn = document.createElement("button");
9907
+ exportCsvBtn.type = "button";
9908
+ exportCsvBtn.className = "db-btn db-btn-sm db-grid-export-btn";
9909
+ exportCsvBtn.textContent = "Export CSV";
9910
+ exportCsvBtn.addEventListener("click", () => triggerExport("csv"));
9911
+ exportJsonBtn = document.createElement("button");
9912
+ exportJsonBtn.type = "button";
9913
+ exportJsonBtn.className = "db-btn db-btn-sm db-grid-export-btn";
9914
+ exportJsonBtn.textContent = "Export JSON";
9915
+ exportJsonBtn.addEventListener("click", () => triggerExport("json"));
9916
+ statusEl.append(exportCsvBtn, exportJsonBtn);
9917
+ el.appendChild(statusEl);
9918
+ }
9919
+ const parts = [`${totalRows.toLocaleString()} rows`];
9920
+ if (sort)
9921
+ parts.push(`Sort: ${sort.column} ${sort.direction.toUpperCase()}`);
9922
+ const activeFilterCount = columnFilters.size + (globalSearchValue ? 1 : 0);
9923
+ if (activeFilterCount > 0)
9924
+ parts.push(`${activeFilterCount} filter(s)`);
9925
+ const textNode = statusEl.firstChild;
9926
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
9927
+ textNode.textContent = `${parts.join(" | ")} `;
9928
+ } else {
9929
+ statusEl.insertBefore(document.createTextNode(`${parts.join(" | ")} `), statusEl.firstChild);
9930
+ }
9931
+ }
9932
+ function load(table2, initialData) {
9933
+ clear();
9934
+ currentTable = table2;
9935
+ if (initialData) {
9936
+ columns = initialData.columns;
9937
+ columnNames = columns.map((c2) => c2.name);
9938
+ totalRows = initialData.totalRows;
9939
+ pageCache.set(0, initialData.rows);
9940
+ } else {
9941
+ columns = [];
9942
+ columnNames = [];
9943
+ totalRows = 0;
9944
+ }
9945
+ loadColWidths();
9946
+ spacer.style.height = `${totalRows * ROW_HEIGHT}px`;
9947
+ renderHeader();
9948
+ updateStatus();
9949
+ if (!initialData) {
9950
+ ensurePage(0);
9951
+ } else {
9952
+ renderViewport();
9953
+ }
9954
+ }
9955
+ filterInput.addEventListener("input", () => {
9956
+ globalSearchValue = filterInput.value.trim();
9957
+ filterClear.hidden = !globalSearchValue;
9958
+ scheduleFilter();
9959
+ });
9960
+ filterInput.addEventListener("keydown", (e2) => {
9961
+ if (e2.key === "Escape") {
9962
+ filterInput.value = "";
9963
+ globalSearchValue = "";
9964
+ filterClear.hidden = true;
9965
+ scheduleFilter();
9966
+ }
9967
+ });
9968
+ filterClear.addEventListener("click", () => {
9969
+ filterInput.value = "";
9970
+ globalSearchValue = "";
9971
+ filterClear.hidden = true;
9972
+ invalidateData();
9973
+ });
9974
+ viewport.addEventListener("scroll", () => {
9975
+ headerWrap.scrollLeft = viewport.scrollLeft;
9976
+ filterRowWrap.scrollLeft = viewport.scrollLeft;
9977
+ renderViewport();
9978
+ }, { passive: true });
9979
+ function destroy() {
9980
+ clear();
9981
+ viewport.removeEventListener("scroll", renderViewport);
9982
+ }
9983
+ return { el, load, clear, destroy };
9984
+ }
9985
+ function formatValue3(value) {
9986
+ if (value === null)
9987
+ return "NULL";
9988
+ if (value instanceof Uint8Array)
9989
+ return `<blob ${value.byteLength} bytes>`;
9990
+ if (typeof value === "boolean")
9991
+ return value ? "true" : "false";
9992
+ if (typeof value === "object")
9993
+ return JSON.stringify(value);
9994
+ const s2 = String(value);
9995
+ if (s2 === "")
9996
+ return "<empty>";
9997
+ return s2;
9998
+ }
9999
+ function formatValueForCopy(value) {
10000
+ if (value === null)
10001
+ return "";
10002
+ if (value instanceof Uint8Array)
10003
+ return `<blob ${value.byteLength} bytes>`;
10004
+ if (typeof value === "boolean")
10005
+ return value ? "true" : "false";
10006
+ if (typeof value === "object")
10007
+ return JSON.stringify(value);
10008
+ return String(value);
10009
+ }
10010
+
10011
+ // web-src/views/database/table-list.ts
10012
+ function createTableList(callbacks) {
10013
+ const wrapper = document.createElement("div");
10014
+ wrapper.className = "db-table-list-wrapper";
10015
+ wrapper.style.display = "flex";
10016
+ wrapper.style.flexDirection = "column";
10017
+ wrapper.style.flex = "1";
10018
+ wrapper.style.overflow = "hidden";
10019
+ const filterWrap = document.createElement("div");
10020
+ filterWrap.className = "db-table-filter-wrap";
10021
+ const filterInput = document.createElement("input");
10022
+ filterInput.className = "db-table-filter";
10023
+ filterInput.type = "text";
10024
+ filterInput.placeholder = "Filter tables...";
10025
+ filterWrap.appendChild(filterInput);
10026
+ const el = document.createElement("div");
10027
+ el.className = "db-table-list";
10028
+ wrapper.append(filterWrap, el);
10029
+ let activeTable = null;
10030
+ let allTables = [];
10031
+ const expandedTables = new Set;
10032
+ const columnCache = new Map;
10033
+ let contextMenu = null;
10034
+ function closeContextMenu() {
10035
+ if (contextMenu) {
10036
+ contextMenu.remove();
10037
+ contextMenu = null;
10038
+ }
10039
+ }
10040
+ function showContextMenu(e2, tableName) {
10041
+ e2.preventDefault();
10042
+ closeContextMenu();
10043
+ const menu = document.createElement("div");
10044
+ menu.className = "db-context-menu";
10045
+ menu.style.position = "absolute";
10046
+ menu.style.left = `${e2.pageX}px`;
10047
+ menu.style.top = `${e2.pageY}px`;
10048
+ const items = [
10049
+ {
10050
+ label: "Copy Table Name",
10051
+ action: () => {
10052
+ navigator.clipboard.writeText(tableName).catch(() => {});
10053
+ }
10054
+ },
10055
+ {
10056
+ label: "Copy SELECT Statement",
10057
+ action: () => {
10058
+ const sql = `SELECT * FROM "${tableName}" LIMIT 100`;
10059
+ navigator.clipboard.writeText(sql).catch(() => {});
10060
+ }
10061
+ },
10062
+ {
10063
+ label: "View CREATE TABLE",
10064
+ action: () => {
10065
+ callbacks.onViewCreateTable?.(tableName);
10066
+ }
10067
+ },
10068
+ {
10069
+ label: "View Table Definition",
10070
+ action: () => {
10071
+ callbacks.onViewDefinition?.(tableName);
10072
+ }
10073
+ }
10074
+ ];
10075
+ for (const item of items) {
10076
+ const row = document.createElement("div");
10077
+ row.className = "db-context-menu-item";
10078
+ row.textContent = item.label;
10079
+ row.addEventListener("click", () => {
10080
+ item.action();
10081
+ closeContextMenu();
10082
+ });
10083
+ menu.appendChild(row);
10084
+ }
10085
+ document.body.appendChild(menu);
10086
+ contextMenu = menu;
10087
+ const onDocClick = (ev) => {
10088
+ if (!menu.contains(ev.target)) {
10089
+ closeContextMenu();
10090
+ document.removeEventListener("click", onDocClick, true);
10091
+ document.removeEventListener("keydown", onKeyDown, true);
10092
+ }
10093
+ };
10094
+ const onKeyDown = (ev) => {
10095
+ if (ev.key === "Escape") {
10096
+ closeContextMenu();
10097
+ document.removeEventListener("click", onDocClick, true);
10098
+ document.removeEventListener("keydown", onKeyDown, true);
10099
+ }
10100
+ };
10101
+ setTimeout(() => {
10102
+ document.addEventListener("click", onDocClick, true);
10103
+ document.addEventListener("keydown", onKeyDown, true);
10104
+ }, 0);
10105
+ }
10106
+ async function renderColumns(container, tableName) {
10107
+ let cols = columnCache.get(tableName);
10108
+ if (!cols && callbacks.getColumns) {
10109
+ cols = await callbacks.getColumns(tableName);
10110
+ columnCache.set(tableName, cols);
10111
+ }
10112
+ if (!cols || cols.length === 0)
10113
+ return;
10114
+ container.innerHTML = "";
10115
+ for (const col of cols) {
10116
+ const colRow = document.createElement("div");
10117
+ colRow.className = "db-table-col-item";
10118
+ if (col.primaryKey)
10119
+ colRow.classList.add("pk");
10120
+ const colName = document.createElement("span");
10121
+ colName.className = "db-table-col-name";
10122
+ colName.textContent = col.name;
10123
+ const colType = document.createElement("span");
10124
+ colType.className = "db-table-col-type";
10125
+ colType.textContent = col.type;
10126
+ colRow.append(colName, colType);
10127
+ container.appendChild(colRow);
10128
+ }
10129
+ }
10130
+ function toggleExpand(tableName, _node, arrow, children) {
10131
+ const expanded = expandedTables.has(tableName);
10132
+ if (expanded) {
10133
+ expandedTables.delete(tableName);
10134
+ children.hidden = true;
10135
+ arrow.classList.remove("expanded");
10136
+ } else {
10137
+ expandedTables.add(tableName);
10138
+ children.hidden = false;
10139
+ arrow.classList.add("expanded");
10140
+ if (children.children.length === 0) {
10141
+ renderColumns(children, tableName);
10142
+ }
10143
+ }
10144
+ }
10145
+ function renderFiltered(tables, filter) {
10146
+ el.innerHTML = "";
10147
+ const filtered = filter ? tables.filter((t2) => t2.name.toLowerCase().includes(filter.toLowerCase())) : tables;
10148
+ if (filtered.length === 0) {
10149
+ const empty = document.createElement("div");
10150
+ empty.className = "db-table-list-empty";
10151
+ empty.textContent = filter ? "No matching tables" : "No tables found";
10152
+ el.appendChild(empty);
10153
+ return;
10154
+ }
10155
+ const groups = { table: [], view: [] };
10156
+ for (const t2 of filtered) {
10157
+ (groups[t2.type] || groups.table).push(t2);
10158
+ }
10159
+ for (const [type, items] of Object.entries(groups)) {
10160
+ if (items.length === 0)
10161
+ continue;
10162
+ const header = document.createElement("div");
10163
+ header.className = "db-table-group-header";
10164
+ header.textContent = type === "view" ? "Views" : "Tables";
10165
+ el.appendChild(header);
10166
+ for (const table2 of items) {
10167
+ const node = document.createElement("div");
10168
+ node.className = "db-table-node";
10169
+ node.dataset.table = table2.name;
10170
+ const row = document.createElement("div");
10171
+ row.className = "db-table-item";
10172
+ if (table2.name === activeTable)
10173
+ row.classList.add("active");
10174
+ row.dataset.table = table2.name;
10175
+ const arrow = document.createElement("span");
10176
+ arrow.className = "db-table-arrow";
10177
+ if (expandedTables.has(table2.name))
10178
+ arrow.classList.add("expanded");
10179
+ const icon = document.createElement("span");
10180
+ icon.className = "db-table-icon";
10181
+ icon.textContent = table2.type === "view" ? "V" : "T";
10182
+ icon.title = table2.type === "view" ? "View" : "Table";
10183
+ const name = document.createElement("span");
10184
+ name.className = "db-table-name";
10185
+ name.textContent = table2.name;
10186
+ const count = document.createElement("span");
10187
+ count.className = "db-table-count";
10188
+ count.textContent = table2.rowCount != null ? formatRowCount(table2.rowCount) : "";
10189
+ row.append(arrow, icon, name, count);
10190
+ const children = document.createElement("div");
10191
+ children.className = "db-table-children";
10192
+ children.hidden = !expandedTables.has(table2.name);
10193
+ if (expandedTables.has(table2.name)) {
10194
+ renderColumns(children, table2.name);
10195
+ }
10196
+ arrow.addEventListener("click", (e2) => {
10197
+ e2.stopPropagation();
10198
+ toggleExpand(table2.name, node, arrow, children);
10199
+ });
10200
+ row.addEventListener("click", () => callbacks.onSelectTable(table2.name));
10201
+ row.addEventListener("dblclick", () => callbacks.onSelectSchema(table2.name));
10202
+ row.addEventListener("contextmenu", (e2) => showContextMenu(e2, table2.name));
10203
+ node.append(row, children);
10204
+ el.appendChild(node);
10205
+ }
10206
+ }
10207
+ }
10208
+ function render(tables) {
10209
+ allTables = tables;
10210
+ filterInput.value = "";
10211
+ renderFiltered(tables, "");
10212
+ }
10213
+ filterInput.addEventListener("input", () => {
10214
+ renderFiltered(allTables, filterInput.value);
10215
+ });
10216
+ function setActive(table2) {
10217
+ activeTable = table2;
10218
+ el.querySelectorAll(".db-table-item").forEach((item) => {
10219
+ item.classList.toggle("active", item.dataset.table === table2);
10220
+ });
10221
+ }
10222
+ return { el: wrapper, render, setActive };
10223
+ }
10224
+ function formatRowCount(n2) {
10225
+ if (n2 >= 1e6)
10226
+ return `${(n2 / 1e6).toFixed(1)}M`;
10227
+ if (n2 >= 1000)
10228
+ return `${(n2 / 1000).toFixed(1)}K`;
10229
+ return String(n2);
10230
+ }
10231
+
10232
+ // web-src/views/database/database-view.ts
10233
+ function createDatabaseView(deps) {
10234
+ let mounted = false;
10235
+ let currentDb = null;
10236
+ let schemaCache = null;
10237
+ let lastFiles = [];
10238
+ const dbSelect = document.createElement("select");
10239
+ dbSelect.className = "db-file-select";
10240
+ dbSelect.title = "Select database file";
10241
+ const dbToolbar = document.createElement("div");
10242
+ dbToolbar.className = "db-toolbar";
10243
+ dbToolbar.appendChild(dbSelect);
10244
+ const tabBar = document.createElement("div");
10245
+ tabBar.className = "db-tab-bar";
10246
+ const tabData = createTab("Data", true);
10247
+ const tabSchema = createTab("Schema", false);
10248
+ tabBar.append(tabData, tabSchema);
10249
+ let currentTab = "data";
10250
+ const tableList = createTableList({
10251
+ onSelectTable: (table2) => selectTable(table2),
10252
+ onSelectSchema: (table2) => showSchema(table2),
10253
+ onViewCreateTable: (table2) => showDdl(table2),
10254
+ onViewDefinition: (table2) => showSchema(table2),
10255
+ getColumns: (table2) => fetchColumns(table2)
10256
+ });
10257
+ const sidebar = document.createElement("div");
10258
+ sidebar.className = "db-sidebar";
10259
+ const savedWidth = localStorage.getItem("db:sidebar-width");
10260
+ if (savedWidth)
10261
+ sidebar.style.width = savedWidth;
10262
+ const toolsSection = document.createElement("div");
10263
+ toolsSection.className = "db-tools-section";
10264
+ const queryBtn = document.createElement("button");
10265
+ queryBtn.className = "db-tool-btn";
10266
+ queryBtn.type = "button";
10267
+ queryBtn.textContent = "Query";
10268
+ queryBtn.title = "SQL Query Editor";
10269
+ queryBtn.addEventListener("click", () => {
10270
+ setActiveTab("query");
10271
+ });
10272
+ const erBtn = document.createElement("button");
10273
+ erBtn.className = "db-tool-btn";
10274
+ erBtn.type = "button";
10275
+ erBtn.textContent = "ER Diagram";
10276
+ erBtn.title = "Entity Relationship Diagram";
10277
+ erBtn.addEventListener("click", () => {
10278
+ setActiveTab("er");
10279
+ if (schemaCache)
10280
+ renderErDiagram();
10281
+ });
10282
+ const searchBtn = document.createElement("button");
10283
+ searchBtn.className = "db-tool-btn";
10284
+ searchBtn.type = "button";
10285
+ searchBtn.textContent = "Search";
10286
+ searchBtn.title = "Search all tables";
10287
+ searchBtn.addEventListener("click", () => {
10288
+ setActiveTab("search");
10289
+ });
10290
+ const snapshotBtn = document.createElement("button");
10291
+ snapshotBtn.className = "db-tool-btn";
10292
+ snapshotBtn.type = "button";
10293
+ snapshotBtn.textContent = "Snapshot";
10294
+ snapshotBtn.title = "Snapshot & Diff";
10295
+ snapshotBtn.addEventListener("click", () => {
10296
+ setActiveTab("snapshot");
10297
+ snapshotView.refresh();
10298
+ });
10299
+ toolsSection.append(queryBtn, erBtn, searchBtn, snapshotBtn);
10300
+ sidebar.append(dbToolbar, tableList.el, toolsSection);
10301
+ const resizeHandle = document.createElement("div");
10302
+ resizeHandle.className = "db-sidebar-resize";
10303
+ sidebar.appendChild(resizeHandle);
10304
+ let resizing = false;
10305
+ resizeHandle.addEventListener("mousedown", (e2) => {
10306
+ e2.preventDefault();
10307
+ resizing = true;
10308
+ resizeHandle.classList.add("active");
10309
+ const onMove = (ev) => {
10310
+ if (!resizing)
10311
+ return;
10312
+ const w = Math.max(120, Math.min(600, ev.clientX));
10313
+ sidebar.style.width = `${w}px`;
10314
+ };
10315
+ const onUp = () => {
10316
+ resizing = false;
10317
+ resizeHandle.classList.remove("active");
10318
+ localStorage.setItem("db:sidebar-width", sidebar.style.width);
10319
+ window.removeEventListener("mousemove", onMove);
10320
+ window.removeEventListener("mouseup", onUp);
10321
+ };
10322
+ window.addEventListener("mousemove", onMove);
10323
+ window.addEventListener("mouseup", onUp);
10324
+ });
10325
+ const grid = createTableGrid({
10326
+ fetchPage: (table2, offset, limit, sort, filters) => fetchTablePage(table2, offset, limit, sort, filters),
10327
+ getDbId: () => currentDb?.id || null
10328
+ });
10329
+ const queryEditor = createQueryEditor({
10330
+ executeQuery: (sql) => executeQuery(sql)
10331
+ });
10332
+ const schemaView = createSchemaView();
10333
+ const erDiagram = createErDiagram();
10334
+ const globalSearchView = createGlobalSearchView({
10335
+ getDbId: () => currentDb?.id || null
10336
+ });
10337
+ const snapshotView = createSnapshotView({
10338
+ getDbId: () => currentDb?.id || null,
10339
+ getTables: () => schemaCache?.tables || []
10340
+ });
10341
+ const historyView = createQueryHistoryView({
10342
+ getDbId: () => currentDb?.id || null,
10343
+ copySqlToQuery: (sql) => {
10344
+ queryEditor.setSql(sql);
10345
+ setActiveTab("query");
10346
+ }
10347
+ });
10348
+ const mainContent = document.createElement("div");
10349
+ mainContent.className = "db-main-content";
10350
+ mainContent.append(tabBar, grid.el, queryEditor.el, schemaView.el, erDiagram.el, globalSearchView.el, snapshotView.el);
10351
+ queryEditor.el.hidden = true;
10352
+ globalSearchView.el.hidden = true;
10353
+ snapshotView.el.hidden = true;
10354
+ const upperArea = document.createElement("div");
10355
+ upperArea.className = "db-upper-area";
10356
+ upperArea.append(sidebar, mainContent);
10357
+ const historyResizer = document.createElement("div");
10358
+ historyResizer.className = "db-history-resizer";
10359
+ const historyPane = document.createElement("div");
10360
+ historyPane.className = "db-history-pane";
10361
+ const savedHistoryHeight = localStorage.getItem("db:history-height");
10362
+ if (savedHistoryHeight)
10363
+ historyPane.style.height = savedHistoryHeight;
10364
+ historyPane.appendChild(historyView.el);
10365
+ let historyResizing = false;
10366
+ historyResizer.addEventListener("mousedown", (e2) => {
10367
+ e2.preventDefault();
10368
+ historyResizing = true;
10369
+ historyResizer.classList.add("active");
10370
+ const startY = e2.clientY;
10371
+ const startH = historyPane.offsetHeight;
10372
+ const onMove = (ev) => {
10373
+ if (!historyResizing)
10374
+ return;
10375
+ const maxH = container.offsetHeight - 60;
10376
+ const h = Math.max(60, Math.min(maxH, startH - (ev.clientY - startY)));
10377
+ historyPane.style.height = `${h}px`;
10378
+ };
10379
+ const onUp = () => {
10380
+ historyResizing = false;
10381
+ historyResizer.classList.remove("active");
10382
+ localStorage.setItem("db:history-height", historyPane.style.height);
10383
+ window.removeEventListener("mousemove", onMove);
10384
+ window.removeEventListener("mouseup", onUp);
10385
+ };
10386
+ window.addEventListener("mousemove", onMove);
10387
+ window.addEventListener("mouseup", onUp);
10388
+ });
10389
+ const historyToggle = document.createElement("button");
10390
+ historyToggle.className = "db-history-toggle";
10391
+ historyToggle.type = "button";
10392
+ historyToggle.textContent = "Query History";
10393
+ historyToggle.title = "Toggle query history panel";
10394
+ sidebar.appendChild(historyToggle);
10395
+ let historyOpen = localStorage.getItem("db:history-open") !== "false";
10396
+ function applyHistoryVisibility() {
10397
+ historyResizer.hidden = !historyOpen;
10398
+ historyPane.hidden = !historyOpen;
10399
+ historyToggle.classList.toggle("active", historyOpen);
10400
+ if (historyOpen)
10401
+ historyView.refresh();
10402
+ }
10403
+ applyHistoryVisibility();
10404
+ historyToggle.addEventListener("click", () => {
10405
+ historyOpen = !historyOpen;
10406
+ localStorage.setItem("db:history-open", String(historyOpen));
10407
+ applyHistoryVisibility();
10408
+ });
10409
+ const container = document.createElement("div");
10410
+ container.className = "db-container";
10411
+ container.append(upperArea, historyResizer, historyPane);
10412
+ function createTab(text2, active) {
10413
+ const btn = document.createElement("button");
10414
+ btn.className = `db-tab${active ? " active" : ""}`;
10415
+ btn.type = "button";
10416
+ btn.textContent = text2;
10417
+ return btn;
10418
+ }
10419
+ function clearDockerNotice() {
10420
+ const notice = mainContent.querySelector(".db-docker-notice");
10421
+ if (notice)
10422
+ notice.remove();
10423
+ tabBar.hidden = false;
10424
+ grid.el.hidden = false;
10425
+ }
10426
+ function mount() {
10427
+ if (mounted)
10428
+ return;
10429
+ const content = document.getElementById("content");
10430
+ if (!content)
10431
+ return;
10432
+ const diff = document.getElementById("diff");
10433
+ if (diff)
10434
+ diff.hidden = true;
10435
+ const empty = document.getElementById("empty");
10436
+ if (empty)
10437
+ empty.classList.add("hidden");
10438
+ content.appendChild(container);
10439
+ mounted = true;
10440
+ applyHistoryVisibility();
10441
+ }
10442
+ function unmount() {
10443
+ if (!mounted)
10444
+ return;
10445
+ container.remove();
10446
+ const diff = document.getElementById("diff");
10447
+ if (diff)
10448
+ diff.hidden = false;
10449
+ mounted = false;
10450
+ grid.clear();
10451
+ schemaView.clear();
10452
+ erDiagram.clear();
10453
+ currentDb = null;
10454
+ schemaCache = null;
10455
+ }
10456
+ function setActiveTab(tab, updateUrl = true) {
10457
+ currentTab = tab;
10458
+ tabData.classList.toggle("active", tab === "data");
10459
+ tabSchema.classList.toggle("active", tab === "schema");
10460
+ queryBtn.classList.toggle("active", tab === "query");
10461
+ erBtn.classList.toggle("active", tab === "er");
10462
+ searchBtn.classList.toggle("active", tab === "search");
10463
+ snapshotBtn.classList.toggle("active", tab === "snapshot");
10464
+ tabBar.hidden = tab === "query" || tab === "er" || tab === "search" || tab === "snapshot";
10465
+ grid.el.hidden = tab !== "data";
10466
+ queryEditor.el.hidden = tab !== "query";
10467
+ schemaView.el.hidden = tab !== "schema";
10468
+ erDiagram.el.hidden = tab !== "er";
10469
+ globalSearchView.el.hidden = tab !== "search";
10470
+ snapshotView.el.hidden = tab !== "snapshot";
10471
+ if (tab === "query")
10472
+ queryEditor.focus();
10473
+ if (updateUrl) {
10474
+ const activeTable = tableList.el.querySelector(".db-table-item.active");
10475
+ deps.setRoute({
10476
+ screen: "database",
10477
+ db: currentDb?.id,
10478
+ table: activeTable?.dataset.table,
10479
+ tab: tab === "data" ? undefined : tab,
10480
+ range: deps.currentRange()
10481
+ }, true);
10482
+ }
10483
+ }
10484
+ tabData.addEventListener("click", () => setActiveTab("data"));
10485
+ tabSchema.addEventListener("click", () => {
10486
+ setActiveTab("schema");
10487
+ const active = tableList.el.querySelector(".db-table-item.active");
10488
+ if (active?.dataset.table)
10489
+ showSchema(active.dataset.table);
10490
+ });
10491
+ async function fetchDbFiles() {
10492
+ const res = await deps.trackLoad(fetch("/_db/files"));
10493
+ if (!res.ok)
10494
+ return [];
10495
+ const data = await res.json();
10496
+ return data.files;
10497
+ }
10498
+ async function fetchSchema(dbId) {
10499
+ const res = await deps.trackLoad(fetch(`/_db/schema?db=${encodeURIComponent(dbId)}&includeColumns=1`));
10500
+ if (!res.ok)
10501
+ return null;
10502
+ return await res.json();
10503
+ }
10504
+ async function fetchTablePage(table2, offset, limit, sort, filters) {
10505
+ if (!currentDb)
10506
+ throw new Error("no database selected");
10507
+ const params = new URLSearchParams({
10508
+ db: currentDb.id,
10509
+ table: table2,
10510
+ offset: String(offset),
10511
+ limit: String(limit)
10512
+ });
10513
+ if (sort) {
10514
+ params.set("sort", sort.column);
10515
+ params.set("dir", sort.direction);
10516
+ }
10517
+ if (filters.length > 0) {
10518
+ params.set("filters", JSON.stringify(filters));
10519
+ }
10520
+ const res = await fetch(`/_db/table?${params}`);
10521
+ if (!res.ok)
10522
+ throw new Error(await res.text());
10523
+ return await res.json();
10524
+ }
10525
+ async function executeQuery(sql) {
10526
+ if (!currentDb)
10527
+ throw new Error("no database selected");
10528
+ const res = await fetch("/_db/query", {
10529
+ method: "POST",
10530
+ headers: {
10531
+ "Content-Type": "application/json",
10532
+ "X-Code-Viewer-Action": "1"
10533
+ },
10534
+ body: JSON.stringify({
10535
+ db: currentDb.id,
10536
+ sql,
10537
+ saveHistory: true,
10538
+ source: "browser",
10539
+ executedBy: "user"
10540
+ })
10541
+ });
10542
+ return await res.json();
10543
+ }
10544
+ async function selectDb(dbId) {
10545
+ const schema = await fetchSchema(dbId);
10546
+ if (!schema)
10547
+ return;
10548
+ schemaCache = schema;
10549
+ tableList.render(schema.tables);
10550
+ grid.clear();
10551
+ schemaView.clear();
10552
+ erDiagram.clear();
10553
+ setActiveTab("data");
10554
+ if (schema.tables.length > 0) {
10555
+ selectTable(schema.tables[0].name);
10556
+ }
10557
+ }
10558
+ async function selectTable(table2) {
10559
+ tableList.setActive(table2);
10560
+ if (!currentDb)
10561
+ return;
10562
+ if (currentTab === "query" || currentTab === "er") {
10563
+ setActiveTab("data", false);
10564
+ }
10565
+ deps.setRoute({
10566
+ screen: "database",
10567
+ db: currentDb.id,
10568
+ table: table2,
10569
+ tab: currentTab === "data" ? undefined : currentTab,
10570
+ range: deps.currentRange()
10571
+ }, true);
10572
+ try {
10573
+ const data = await deps.trackLoad(fetchTablePage(table2, 0, 200, null, []));
10574
+ grid.load(table2, data);
10575
+ } catch {
10576
+ grid.load(table2);
10577
+ }
10578
+ if (currentTab === "schema") {
10579
+ const columns = await fetchColumns(table2);
10580
+ schemaView.render(table2, columns, schemaCache?.indexes || []);
10581
+ }
10582
+ }
10583
+ async function fetchColumns(table2) {
10584
+ if (schemaCache?.columnsMap?.[table2]) {
10585
+ return schemaCache.columnsMap[table2];
10586
+ }
10587
+ if (!currentDb)
10588
+ return [];
10589
+ const res = await fetch(`/_db/columns?db=${encodeURIComponent(currentDb.id)}&table=${encodeURIComponent(table2)}`);
10590
+ if (!res.ok)
10591
+ return [];
10592
+ const data = await res.json();
10593
+ return data.columns;
10594
+ }
10595
+ async function showSchema(table2) {
10596
+ setActiveTab("schema");
10597
+ if (!currentDb)
10598
+ return;
10599
+ const columns = await fetchColumns(table2);
10600
+ schemaView.render(table2, columns, schemaCache?.indexes || []);
10601
+ }
10602
+ async function showDdl(table2) {
10603
+ if (!currentDb)
10604
+ return;
10605
+ setActiveTab("schema");
10606
+ try {
10607
+ const res = await fetch(`/_db/ddl?db=${encodeURIComponent(currentDb.id)}&table=${encodeURIComponent(table2)}`);
10608
+ if (!res.ok)
10609
+ return;
10610
+ const data = await res.json();
10611
+ const columns = await fetchColumns(table2);
10612
+ schemaView.render(table2, columns, schemaCache?.indexes || [], {
10613
+ foreignKeys: schemaCache?.foreignKeys,
10614
+ triggers: data.triggers,
10615
+ ddl: data.sql
10616
+ });
10617
+ } catch {}
10618
+ }
10619
+ async function renderErDiagram() {
10620
+ if (!schemaCache || !currentDb)
10621
+ return;
10622
+ const columnsMap = new Map;
10623
+ if (schemaCache.columnsMap) {
10624
+ for (const [name, cols] of Object.entries(schemaCache.columnsMap)) {
10625
+ columnsMap.set(name, cols);
10626
+ }
10627
+ } else {
10628
+ const tables = schemaCache.tables.filter((t2) => t2.type !== "view");
10629
+ const results = await Promise.all(tables.map((t2) => fetchColumns(t2.name).then((cols) => ({ name: t2.name, cols }))));
10630
+ for (const { name, cols } of results) {
10631
+ if (cols.length > 0)
10632
+ columnsMap.set(name, cols);
10633
+ }
10634
+ }
10635
+ erDiagram.render(schemaCache, columnsMap);
10636
+ }
10637
+ dbSelect.addEventListener("change", () => {
10638
+ const dbId = dbSelect.value;
10639
+ if (!dbId)
10640
+ return;
10641
+ const file = lastFiles.find((f2) => f2.id === dbId);
10642
+ const option = dbSelect.selectedOptions[0];
10643
+ currentDb = {
10644
+ id: dbId,
10645
+ path: file?.path || dbId,
10646
+ name: option?.textContent || dbId,
10647
+ sizeBytes: file?.sizeBytes || 0,
10648
+ kind: file?.kind || "sqlite"
10649
+ };
10650
+ clearDockerNotice();
10651
+ deps.setRoute({ screen: "database", db: dbId, range: deps.currentRange() }, true);
10652
+ selectDb(dbId);
10653
+ });
10654
+ async function enter(db, table2, tab) {
10655
+ mount();
10656
+ document.body.classList.add("gdp-database-page");
10657
+ deps.setPageMode();
10658
+ deps.syncHeaderMenu();
10659
+ const files = await fetchDbFiles();
10660
+ lastFiles = files;
10661
+ dbSelect.innerHTML = "";
10662
+ if (files.length === 0) {
10663
+ const opt = document.createElement("option");
10664
+ opt.value = "";
10665
+ opt.textContent = "No database files found";
10666
+ dbSelect.appendChild(opt);
10667
+ dbSelect.disabled = true;
10668
+ return;
10669
+ }
10670
+ dbSelect.disabled = false;
10671
+ for (const f2 of files) {
10672
+ const opt = document.createElement("option");
10673
+ opt.value = f2.id;
10674
+ const isDocker = f2.id.startsWith("docker:");
10675
+ const label = isDocker ? `${f2.name} (Docker)` : `${f2.path} (${formatSize(f2.sizeBytes)})`;
10676
+ opt.textContent = label;
10677
+ dbSelect.appendChild(opt);
10678
+ }
10679
+ const target = db && files.find((f2) => f2.id === db) ? db : files[0].id;
10680
+ dbSelect.value = target;
10681
+ currentDb = files.find((f2) => f2.id === target) || null;
10682
+ await selectDb(target);
10683
+ if (table2) {
10684
+ await selectTable(table2);
10685
+ }
10686
+ if (tab && tab !== "data") {
10687
+ setActiveTab(tab);
10688
+ if (tab === "schema") {
10689
+ const activeTable = tableList.el.querySelector(".db-table-item.active");
10690
+ if (activeTable?.dataset.table)
10691
+ showSchema(activeTable.dataset.table);
10692
+ } else if (tab === "er") {
10693
+ if (schemaCache)
10694
+ renderErDiagram();
10695
+ } else if (tab === "snapshot") {
10696
+ snapshotView.refresh();
10697
+ }
10698
+ }
10699
+ }
10700
+ function leave() {
10701
+ document.body.classList.remove("gdp-database-page");
10702
+ unmount();
10703
+ }
10704
+ function handleSse(event, data) {
10705
+ if (!mounted)
10706
+ return;
10707
+ if (historyOpen)
10708
+ historyView.refresh();
10709
+ if (event === "db-snapshot" && data) {
10710
+ snapshotView.handleSse(data);
10711
+ }
10712
+ }
10713
+ return { enter, leave, handleSse };
10714
+ }
10715
+ function formatSize(bytes) {
10716
+ if (bytes >= 1024 * 1024 * 1024)
10717
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
10718
+ if (bytes >= 1024 * 1024)
10719
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
10720
+ if (bytes >= 1024)
10721
+ return `${(bytes / 1024).toFixed(1)} KB`;
10722
+ return `${bytes} B`;
10723
+ }
10724
+
10725
+ // web-src/views/diff-line-select.ts
10726
+ var SELECTED_CLASS = "gdp-diff-line-selected";
10727
+ function cardPath(el) {
10728
+ return el.closest(".gdp-file-shell[data-path]")?.dataset.path || "";
10729
+ }
10730
+ function afterLineFromCell(cell) {
10731
+ const sideCell = cell.closest("td.d2h-code-side-linenumber");
10732
+ if (sideCell) {
10733
+ const side = sideCell.closest(".d2h-file-side-diff");
10734
+ const wrapper = sideCell.closest(".d2h-file-wrapper");
10735
+ if (!side || !wrapper)
10736
+ return null;
10737
+ const sides = wrapper.querySelectorAll(".d2h-file-side-diff");
10738
+ if (sides.length < 2 || side !== sides[1])
10739
+ return null;
10740
+ const line2 = Number((sideCell.textContent || "").trim());
10741
+ return Number.isInteger(line2) && line2 > 0 ? line2 : null;
10742
+ }
10743
+ const numCell = cell.closest("td.d2h-code-linenumber");
10744
+ if (!numCell)
10745
+ return null;
10746
+ const raw = (numCell.querySelector(".line-num2")?.textContent || "").trim();
10747
+ const line = Number(raw);
10748
+ return Number.isInteger(line) && line > 0 ? line : null;
10749
+ }
10750
+ function rowsWithAfterLines(card) {
10751
+ const out = [];
10752
+ card.querySelectorAll("table.d2h-diff-table tr").forEach((row) => {
10753
+ const cell = row.querySelector("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
10754
+ if (!cell)
10755
+ return;
10756
+ const line = afterLineFromCell(cell);
10757
+ if (line !== null)
10758
+ out.push({ row, line });
10759
+ });
10760
+ return out;
10761
+ }
10762
+ function createDiffLineSelect(deps) {
10763
+ let drag = null;
10764
+ let selection = null;
10765
+ function clearHighlights() {
10766
+ document.querySelectorAll(`.${SELECTED_CLASS}`).forEach((row) => {
10767
+ row.classList.remove(SELECTED_CLASS);
10768
+ });
10769
+ }
10770
+ function applySelection(next) {
10771
+ selection = next;
10772
+ clearHighlights();
10773
+ if (!next) {
10774
+ deps.pill.hide();
10775
+ return;
10776
+ }
10777
+ const start = Math.min(next.start, next.end);
10778
+ const end = Math.max(next.start, next.end);
10779
+ const card = document.querySelector(`.gdp-file-shell[data-path="${CSS.escape(next.path)}"]`);
10780
+ if (card) {
10781
+ for (const item of rowsWithAfterLines(card)) {
10782
+ if (item.line >= start && item.line <= end)
10783
+ item.row.classList.add(SELECTED_CLASS);
10784
+ }
10785
+ }
10786
+ deps.pill.show(next.path, start, end);
10787
+ }
10788
+ function clear() {
10789
+ drag = null;
10790
+ applySelection(null);
10791
+ }
10792
+ const diff = document.querySelector("#diff");
10793
+ if (!diff)
10794
+ return { clear };
10795
+ diff.addEventListener("mousedown", (e2) => {
10796
+ const target = e2.target;
10797
+ const cell = target.closest("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
10798
+ if (!cell)
10799
+ return;
10800
+ const line = afterLineFromCell(cell);
10801
+ const path = cardPath(cell);
10802
+ if (line === null || !path) {
10803
+ if (selection)
10804
+ clear();
10805
+ return;
10806
+ }
10807
+ e2.preventDefault();
10808
+ drag = { path, start: line };
10809
+ applySelection({ path, start: line, end: line });
10810
+ });
10811
+ diff.addEventListener("mouseover", (e2) => {
10812
+ if (!drag)
10813
+ return;
10814
+ const target = e2.target;
10815
+ const cell = target.closest("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
10816
+ if (!cell || cardPath(cell) !== drag.path)
10817
+ return;
10818
+ const line = afterLineFromCell(cell);
10819
+ if (line === null)
10820
+ return;
10821
+ applySelection({ path: drag.path, start: drag.start, end: line });
10822
+ });
10823
+ document.addEventListener("mouseup", () => {
10824
+ drag = null;
10825
+ });
10826
+ document.addEventListener("keydown", (e2) => {
10827
+ if (e2.key === "Escape" && selection && !drag)
10828
+ clear();
10829
+ });
10830
+ return { clear };
10831
+ }
10832
+
10833
+ // web-src/core/file-path-copy.ts
10834
+ function filePathClipboardText(path) {
10835
+ return path || "";
10836
+ }
10837
+ function fileNameClipboardText(path) {
10838
+ if (!path)
10839
+ return "";
10840
+ const parts = path.split("/").filter(Boolean);
10841
+ return parts[parts.length - 1] || "";
10842
+ }
10843
+ function fileReferenceClipboardText(path, start, end) {
10844
+ if (!path)
10845
+ return "";
10846
+ const a2 = Math.max(1, Math.floor(Math.min(start, end)));
10847
+ const b2 = Math.max(1, Math.floor(Math.max(start, end)));
10848
+ return a2 === b2 ? `@${path}#${a2}` : `@${path}#${a2}-${b2}`;
10849
+ }
10850
+
10851
+ // web-src/core/ws-highlight.ts
10852
+ function isWhitespaceOnlyInlineHighlight(text2) {
10853
+ return !!text2 && !/\S/.test(text2);
10854
+ }
10855
+ function suppressWhitespaceOnlyInlineHighlights(root) {
10856
+ root.querySelectorAll("ins, del").forEach((el) => {
10857
+ if (!isWhitespaceOnlyInlineHighlight(el.textContent))
10858
+ return;
10859
+ const parent = el.parentNode;
10860
+ if (!parent)
10861
+ return;
10862
+ parent.replaceChild(document.createTextNode(el.textContent || ""), el);
10863
+ });
10864
+ }
10865
+
10866
+ // web-src/views/media-embed.ts
10867
+ var MEDIA_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico|mp4|webm|mov|mp3|wav|ogg|flac|m4a|aac|opus)(\?.*)?$/i;
10868
+ var IMAGE_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)(\?.*)?$/i;
10869
+ var VIDEO_RE = /\.(mp4|webm|mov)$/i;
10870
+ var AUDIO_RE = /\.(mp3|wav|ogg|flac|m4a|aac|opus)$/i;
10871
+ function isMedia(p2) {
10872
+ return MEDIA_RE.test(p2);
10873
+ }
10874
+ function isImage(p2) {
10875
+ return IMAGE_RE.test(p2);
10876
+ }
10877
+ function isVideo(p2) {
10878
+ return VIDEO_RE.test(p2);
10879
+ }
10880
+ function isAudio(p2) {
10881
+ return AUDIO_RE.test(p2);
10882
+ }
10883
+ function fileURL(path, ref) {
10884
+ return `/_file?path=${encodeURIComponent(path)}&ref=${ref}`;
10885
+ }
10886
+ function mediaTag(path, ref) {
10887
+ const url = fileURL(path, ref);
10888
+ if (isVideo(path)) {
10889
+ return `<video src="${url}" controls preload="metadata"></video>`;
10890
+ }
10891
+ if (isAudio(path)) {
10892
+ return `<audio src="${url}" controls preload="metadata"></audio>`;
10893
+ }
10894
+ return `<img src="${url}" alt="" loading="lazy">`;
10895
+ }
7945
10896
  function enhanceMediaCard(file, card) {
7946
10897
  const path = file.path;
7947
10898
  if (!file.media_kind && !isMedia(path))
@@ -7997,6 +10948,7 @@ ${frontmatter.yaml}
7997
10948
  setProjectName,
7998
10949
  getProjectName,
7999
10950
  createOpenPathButton,
10951
+ persistViewedFiles,
8000
10952
  applyHideTests,
8001
10953
  getServerGeneration,
8002
10954
  setServerGeneration
@@ -8023,9 +10975,6 @@ ${frontmatter.yaml}
8023
10975
  span.title = { M: "modified", A: "added", D: "deleted", R: "renamed" }[ch] || ch;
8024
10976
  return span;
8025
10977
  }
8026
- function persistViewedFiles() {
8027
- localStorage.setItem("gdp:viewed-files", JSON.stringify([...STATE.viewedFiles]));
8028
- }
8029
10978
  function setFileViewed(path, viewed) {
8030
10979
  if (viewed)
8031
10980
  STATE.viewedFiles.add(path);
@@ -8171,21 +11120,112 @@ ${frontmatter.yaml}
8171
11120
  syncViewedCardDisplay(card, viewed);
8172
11121
  });
8173
11122
  }
8174
- let CLIENT_REQ_SEQ = 0;
8175
- const LOAD_QUEUE = [];
8176
- let ACTIVE_LOADS = 0;
8177
- const MAX_PARALLEL = 2;
8178
- let lazyObserver = null;
8179
- function renderShell(meta) {
11123
+ let CLIENT_REQ_SEQ = 0;
11124
+ const LOAD_QUEUE = [];
11125
+ let ACTIVE_LOADS = 0;
11126
+ const MAX_PARALLEL = 2;
11127
+ let lazyObserver = null;
11128
+ let scrollSpyInstalled = false;
11129
+ let prevListSignature = "";
11130
+ let prevCardSignatures = new Map;
11131
+ function fileKey(f2) {
11132
+ return f2.key || f2.path;
11133
+ }
11134
+ function computeListSignature(files) {
11135
+ return files.map((f2) => `${fileKey(f2)}\x00${f2.path}\x00${f2.old_path || ""}\x00${f2.status || "M"}`).join(`
11136
+ `);
11137
+ }
11138
+ function computeCardSignature(f2) {
11139
+ return [
11140
+ fileKey(f2),
11141
+ f2.status || "M",
11142
+ f2.additions || 0,
11143
+ f2.deletions || 0,
11144
+ f2.binary ? 1 : 0,
11145
+ f2.media_kind || "",
11146
+ f2.size_class || "small",
11147
+ f2.force_layout || "",
11148
+ f2.highlight ? 1 : 0,
11149
+ f2.load_url,
11150
+ f2.preview_url || "",
11151
+ f2.estimated_height_px || 0,
11152
+ f2.untracked ? 1 : 0
11153
+ ].join("\x00");
11154
+ }
11155
+ function ensureLazyObserver() {
11156
+ if (!lazyObserver) {
11157
+ lazyObserver = new IntersectionObserver((entries) => {
11158
+ for (const entry of entries) {
11159
+ if (!entry.isIntersecting)
11160
+ continue;
11161
+ const card = entry.target;
11162
+ if (card.classList.contains("loaded") || card.classList.contains("loading"))
11163
+ continue;
11164
+ const f2 = STATE.files.find((x) => x.path === card.dataset.path);
11165
+ if (f2)
11166
+ enqueueLoad(f2, card, 0);
11167
+ }
11168
+ }, { rootMargin: "1200px 0px 1600px 0px" });
11169
+ }
11170
+ return lazyObserver;
11171
+ }
11172
+ function activatePendingCard(card, file) {
11173
+ ensureLazyObserver().observe(card);
11174
+ const rect = card.getBoundingClientRect();
11175
+ if (rect.top <= window.innerHeight + 1600) {
11176
+ enqueueLoad(file, card, 0);
11177
+ }
11178
+ }
11179
+ function invalidateLoadedCard(card, file, changedPaths) {
11180
+ if (!card.classList.contains("loaded"))
11181
+ return false;
11182
+ if (changedPaths && !changedPaths.has(file.path))
11183
+ return false;
11184
+ card.classList.remove("loaded", "error");
11185
+ card.classList.add("pending");
11186
+ card._diffData = null;
11187
+ const body = card.querySelector(".gdp-shell-body");
11188
+ if (body)
11189
+ body.innerHTML = "";
11190
+ const head = card.querySelector(".gdp-shell-header");
11191
+ if (head)
11192
+ head.style.display = "";
11193
+ const indicator = card.querySelector(".loading-indicator");
11194
+ if (indicator)
11195
+ indicator.hidden = false;
11196
+ activatePendingCard(card, file);
11197
+ return true;
11198
+ }
11199
+ function updateSidebarStats(files) {
11200
+ for (const f2 of files) {
11201
+ const li = document.querySelector(`#filelist li[data-path="${CSS.escape(f2.path)}"]`);
11202
+ if (!li)
11203
+ continue;
11204
+ const badge = li.querySelector(".badge");
11205
+ if (badge) {
11206
+ const ch = (f2.status || "M")[0].toUpperCase();
11207
+ if (badge.textContent !== ch) {
11208
+ badge.textContent = ch;
11209
+ badge.className = `badge ${ch}`;
11210
+ }
11211
+ }
11212
+ }
11213
+ }
11214
+ function renderShell(meta, changedPaths) {
8180
11215
  const newFiles = meta.files || [];
11216
+ const newListSig = computeListSignature(newFiles);
11217
+ const listSame = newListSig === prevListSignature && prevListSignature !== "";
8181
11218
  STATE.files = newFiles;
8182
11219
  setServerGeneration(meta.generation || 0);
8183
11220
  window._lastMeta = meta;
8184
11221
  renderMeta(meta);
8185
- renderSidebar(newFiles);
8186
11222
  const target = $("#diff");
8187
11223
  const empty = $("#empty");
8188
11224
  if (!newFiles.length) {
11225
+ prevListSignature = newListSig;
11226
+ prevCardSignatures.clear();
11227
+ if (!listSame)
11228
+ renderSidebar(newFiles);
8189
11229
  if (STATE.route.screen === "file") {
8190
11230
  empty.classList.add("hidden");
8191
11231
  applySourceRouteToShell();
@@ -8194,17 +11234,91 @@ ${frontmatter.yaml}
8194
11234
  target.replaceChildren();
8195
11235
  }
8196
11236
  LOAD_QUEUE.length = 0;
8197
- return;
11237
+ return {
11238
+ structureChanged: !listSame,
11239
+ invalidatedCards: 0,
11240
+ preservedDom: listSame
11241
+ };
8198
11242
  }
8199
11243
  empty.classList.add("hidden");
11244
+ const newCardSigs = new Map;
11245
+ for (const f2 of newFiles) {
11246
+ newCardSigs.set(fileKey(f2), computeCardSignature(f2));
11247
+ }
11248
+ let invalidatedCards = 0;
11249
+ if (listSame) {
11250
+ const pathsUnknown = !changedPaths;
11251
+ let sidebarNeedsStatsUpdate = false;
11252
+ for (const f2 of newFiles) {
11253
+ const key = fileKey(f2);
11254
+ const oldSig = prevCardSignatures.get(key);
11255
+ const newSig = newCardSigs.get(key);
11256
+ const sigChanged = oldSig !== newSig;
11257
+ const pathHint = pathsUnknown || changedPaths.has(f2.path);
11258
+ if (!sigChanged && !pathHint) {
11259
+ continue;
11260
+ }
11261
+ const card = document.querySelector(`.gdp-file-shell[data-key="${CSS.escape(key)}"]`);
11262
+ if (!card)
11263
+ continue;
11264
+ const sizeChanged = card.dataset.sizeClass !== (f2.size_class || "small");
11265
+ const statusChanged = card.dataset.status !== (f2.status || "M");
11266
+ if (sizeChanged || statusChanged) {
11267
+ card.classList.remove("loaded", "error");
11268
+ card.classList.add("pending");
11269
+ card.replaceChildren();
11270
+ const tmp = createPlaceholder(f2);
11271
+ while (tmp.firstChild)
11272
+ card.appendChild(tmp.firstChild);
11273
+ card.dataset.sizeClass = f2.size_class || "small";
11274
+ card.dataset.status = f2.status || "M";
11275
+ delete card.dataset.manualRendered;
11276
+ delete card.dataset.manualLoad;
11277
+ delete card.dataset.manualMode;
11278
+ card.style.minHeight = `${f2.estimated_height_px || 80}px`;
11279
+ card._diffData = null;
11280
+ card._file = null;
11281
+ activatePendingCard(card, f2);
11282
+ invalidatedCards++;
11283
+ sidebarNeedsStatsUpdate = true;
11284
+ } else {
11285
+ const stats = card.querySelector(".gdp-shell-header .stats");
11286
+ if (stats) {
11287
+ stats.innerHTML = '<span class="a">+' + (f2.additions || 0) + "</span>" + '<span class="d">−' + (f2.deletions || 0) + "</span>";
11288
+ }
11289
+ card._file = f2;
11290
+ const didInvalidate = sigChanged ? invalidateLoadedCard(card, f2, null) : invalidateLoadedCard(card, f2, changedPaths);
11291
+ if (didInvalidate)
11292
+ invalidatedCards++;
11293
+ if (sigChanged)
11294
+ sidebarNeedsStatsUpdate = true;
11295
+ }
11296
+ }
11297
+ if (sidebarNeedsStatsUpdate)
11298
+ updateSidebarStats(newFiles);
11299
+ prevCardSignatures = newCardSigs;
11300
+ prevListSignature = newListSig;
11301
+ applySourceRouteToShell();
11302
+ if (!scrollSpyInstalled) {
11303
+ setupScrollSpy();
11304
+ scrollSpyInstalled = true;
11305
+ }
11306
+ return {
11307
+ structureChanged: false,
11308
+ invalidatedCards,
11309
+ preservedDom: invalidatedCards === 0
11310
+ };
11311
+ }
11312
+ if (!listSame)
11313
+ renderSidebar(newFiles);
8200
11314
  const oldByKey = new Map;
8201
11315
  document.querySelectorAll(".gdp-file-shell").forEach((c2) => {
8202
11316
  if (c2.dataset.key)
8203
11317
  oldByKey.set(c2.dataset.key, c2);
8204
11318
  });
8205
11319
  const ordered = [];
8206
- newFiles.forEach((f2) => {
8207
- const key = f2.key || f2.path;
11320
+ for (const f2 of newFiles) {
11321
+ const key = fileKey(f2);
8208
11322
  const old = oldByKey.get(key);
8209
11323
  if (old) {
8210
11324
  oldByKey.delete(key);
@@ -8225,6 +11339,7 @@ ${frontmatter.yaml}
8225
11339
  old.style.minHeight = `${f2.estimated_height_px || 80}px`;
8226
11340
  old._diffData = null;
8227
11341
  old._file = null;
11342
+ invalidatedCards++;
8228
11343
  } else {
8229
11344
  const stats = old.querySelector(".gdp-shell-header .stats");
8230
11345
  if (stats) {
@@ -8235,8 +11350,9 @@ ${frontmatter.yaml}
8235
11350
  ordered.push(old);
8236
11351
  } else {
8237
11352
  ordered.push(createPlaceholder(f2));
11353
+ invalidatedCards++;
8238
11354
  }
8239
- });
11355
+ }
8240
11356
  oldByKey.forEach((c2) => {
8241
11357
  c2.remove();
8242
11358
  });
@@ -8245,14 +11361,18 @@ ${frontmatter.yaml}
8245
11361
  if (!LOAD_QUEUE[i2].card.isConnected)
8246
11362
  LOAD_QUEUE.splice(i2, 1);
8247
11363
  }
11364
+ prevCardSignatures = newCardSigs;
11365
+ prevListSignature = newListSig;
8248
11366
  setupLazyObserver();
8249
11367
  enqueueInitialLoads();
8250
11368
  applySourceRouteToShell();
8251
11369
  setupScrollSpy();
11370
+ scrollSpyInstalled = true;
8252
11371
  if (typeof applyHideTests === "function")
8253
11372
  applyHideTests();
8254
11373
  applyFilter();
8255
11374
  applyViewedState();
11375
+ return { structureChanged: true, invalidatedCards, preservedDom: false };
8256
11376
  }
8257
11377
  function createPlaceholder(f2) {
8258
11378
  const card = document.createElement("div");
@@ -9008,6 +12128,7 @@ ${frontmatter.yaml}
9008
12128
  var HELP_SECTIONS = [
9009
12129
  "overview",
9010
12130
  "annotations",
12131
+ "database",
9011
12132
  "skills",
9012
12133
  "keybindings"
9013
12134
  ];
@@ -9140,6 +12261,93 @@ ${frontmatter.yaml}
9140
12261
  }
9141
12262
  ]
9142
12263
  },
12264
+ database: {
12265
+ nav: "Database",
12266
+ title: "Database Viewer",
12267
+ intro: "Browse SQLite files and Docker-hosted MySQL/PostgreSQL databases. Run queries with syntax highlighting and explore table schemas and ER diagrams.",
12268
+ groups: [
12269
+ {
12270
+ title: "Supported databases",
12271
+ blocks: [
12272
+ {
12273
+ kind: "table",
12274
+ rows: [
12275
+ [
12276
+ "SQLite",
12277
+ "Automatically discovered from .db, .sqlite, .sqlite3 files in the repository."
12278
+ ],
12279
+ [
12280
+ "MySQL / MariaDB",
12281
+ "Detected from docker-compose.yml services. Multiple databases per server are listed."
12282
+ ],
12283
+ [
12284
+ "PostgreSQL",
12285
+ "Detected from docker-compose.yml services. Multiple databases per server are listed."
12286
+ ]
12287
+ ]
12288
+ }
12289
+ ]
12290
+ },
12291
+ {
12292
+ title: "UI layout",
12293
+ blocks: [
12294
+ {
12295
+ kind: "table",
12296
+ rows: [
12297
+ [
12298
+ "Sidebar",
12299
+ "DB selector, table tree (expand to see columns), filter, Query/ER buttons."
12300
+ ],
12301
+ [
12302
+ "Data tab",
12303
+ "Paginated table grid with sort, filter, cell copy, and CSV/JSON export."
12304
+ ],
12305
+ [
12306
+ "Schema tab",
12307
+ "Column definitions, indexes, foreign keys, triggers, and DDL."
12308
+ ],
12309
+ [
12310
+ "Query editor",
12311
+ "SQL syntax highlighting, Tab indent, auto-resize, Ctrl+Enter to run."
12312
+ ],
12313
+ [
12314
+ "ER Diagram",
12315
+ "Mermaid-based entity-relationship diagram with zoom and pan."
12316
+ ],
12317
+ [
12318
+ "Query History",
12319
+ "Bottom panel with master-detail layout. Left: history list, Right: result preview."
12320
+ ]
12321
+ ]
12322
+ }
12323
+ ]
12324
+ },
12325
+ {
12326
+ title: "CLI query (for AI agents)",
12327
+ blocks: [
12328
+ {
12329
+ kind: "paragraph",
12330
+ text: "AI agents can execute read-only queries from the CLI. Results are saved to query history and visible in the browser."
12331
+ },
12332
+ {
12333
+ kind: "command",
12334
+ title: "Run a query",
12335
+ command: 'code-viewer query exec --db data.db --sql "SELECT * FROM users LIMIT 10" --title "Sample data"'
12336
+ },
12337
+ {
12338
+ kind: "command",
12339
+ title: "List databases",
12340
+ command: "code-viewer query list"
12341
+ },
12342
+ {
12343
+ kind: "command",
12344
+ title: "Agent reference",
12345
+ command: "code-viewer query agent-help"
12346
+ }
12347
+ ]
12348
+ }
12349
+ ]
12350
+ },
9143
12351
  skills: {
9144
12352
  nav: "Agent Skill",
9145
12353
  title: "Agent Skill Setup",
@@ -9396,6 +12604,93 @@ ${frontmatter.yaml}
9396
12604
  }
9397
12605
  ]
9398
12606
  },
12607
+ database: {
12608
+ nav: "データベース",
12609
+ title: "データベースビューア",
12610
+ intro: "SQLite ファイルや Docker 上の MySQL/PostgreSQL を閲覧できます。シンタックスハイライト付きクエリ実行、スキーマ表示、ER 図を提供します。",
12611
+ groups: [
12612
+ {
12613
+ title: "対応データベース",
12614
+ blocks: [
12615
+ {
12616
+ kind: "table",
12617
+ rows: [
12618
+ [
12619
+ "SQLite",
12620
+ "リポジトリ内の .db, .sqlite, .sqlite3 ファイルを自動検出します。"
12621
+ ],
12622
+ [
12623
+ "MySQL / MariaDB",
12624
+ "docker-compose.yml のサービスから検出。同一サーバー上の複数データベースを一覧表示します。"
12625
+ ],
12626
+ [
12627
+ "PostgreSQL",
12628
+ "docker-compose.yml のサービスから検出。同一サーバー上の複数データベースを一覧表示します。"
12629
+ ]
12630
+ ]
12631
+ }
12632
+ ]
12633
+ },
12634
+ {
12635
+ title: "UI構成",
12636
+ blocks: [
12637
+ {
12638
+ kind: "table",
12639
+ rows: [
12640
+ [
12641
+ "サイドバー",
12642
+ "DB選択、テーブルツリー(展開でカラム表示)、フィルター、Query/ERボタン。"
12643
+ ],
12644
+ [
12645
+ "Data タブ",
12646
+ "ページネーション付きグリッド。ソート、フィルター、セルコピー、CSV/JSONエクスポート。"
12647
+ ],
12648
+ [
12649
+ "Schema タブ",
12650
+ "カラム定義、インデックス、外部キー、トリガー、DDL。"
12651
+ ],
12652
+ [
12653
+ "クエリエディター",
12654
+ "SQLシンタックスハイライト、Tabインデント、自動リサイズ、Ctrl+Enterで実行。"
12655
+ ],
12656
+ [
12657
+ "ER図",
12658
+ "Mermaidベースのエンティティ関係図。ズーム・パン対応。"
12659
+ ],
12660
+ [
12661
+ "クエリ履歴",
12662
+ "下部パネルにマスター/ディテール表示。左に履歴一覧、右に結果プレビュー。"
12663
+ ]
12664
+ ]
12665
+ }
12666
+ ]
12667
+ },
12668
+ {
12669
+ title: "CLI クエリ(AIエージェント用)",
12670
+ blocks: [
12671
+ {
12672
+ kind: "paragraph",
12673
+ text: "AIエージェントはCLIから読み取り専用クエリを実行できます。結果はクエリ履歴に保存され、ブラウザで確認できます。"
12674
+ },
12675
+ {
12676
+ kind: "command",
12677
+ title: "クエリを実行",
12678
+ command: 'code-viewer query exec --db data.db --sql "SELECT * FROM users LIMIT 10" --title "サンプルデータ"'
12679
+ },
12680
+ {
12681
+ kind: "command",
12682
+ title: "データベース一覧",
12683
+ command: "code-viewer query list"
12684
+ },
12685
+ {
12686
+ kind: "command",
12687
+ title: "エージェント向けリファレンス",
12688
+ command: "code-viewer query agent-help"
12689
+ }
12690
+ ]
12691
+ }
12692
+ ]
12693
+ },
9399
12694
  skills: {
9400
12695
  nav: "スキル登録",
9401
12696
  title: "Agent Skill の登録",
@@ -12819,6 +16114,7 @@ ${frontmatter.yaml}
12819
16114
  fileBadge,
12820
16115
  fileEntryIcon,
12821
16116
  applyViewedState,
16117
+ persistCollapsedDirs,
12822
16118
  appendScopeParams,
12823
16119
  createOpenPathButton,
12824
16120
  normalizeViewerFontSize,
@@ -13068,7 +16364,7 @@ ${frontmatter.yaml}
13068
16364
  STATE.collapsedDirs.add(dir.path);
13069
16365
  else
13070
16366
  STATE.collapsedDirs.delete(dir.path);
13071
- localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
16367
+ persistCollapsedDirs();
13072
16368
  };
13073
16369
  if (!dir.children_omitted) {
13074
16370
  chev.addEventListener("click", toggleDir);
@@ -13275,7 +16571,7 @@ ${frontmatter.yaml}
13275
16571
  STATE.collapsedDirs.add(dir.path);
13276
16572
  else
13277
16573
  STATE.collapsedDirs.delete(dir.path);
13278
- localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
16574
+ persistCollapsedDirs();
13279
16575
  rerenderVirtualSidebar();
13280
16576
  } finally {
13281
16577
  delete li.dataset.toggling;
@@ -13585,7 +16881,7 @@ ${frontmatter.yaml}
13585
16881
  STATE.collapsedDirs.add(row.path);
13586
16882
  }
13587
16883
  }
13588
- localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
16884
+ persistCollapsedDirs();
13589
16885
  rerenderVirtualSidebar();
13590
16886
  return;
13591
16887
  }
@@ -13600,7 +16896,7 @@ ${frontmatter.yaml}
13600
16896
  if (collapsed)
13601
16897
  STATE.collapsedDirs.add(path);
13602
16898
  });
13603
- localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
16899
+ persistCollapsedDirs();
13604
16900
  }
13605
16901
  function sidebarAncestorDirs(path) {
13606
16902
  const parts = path.split("/").filter(Boolean);
@@ -13623,7 +16919,7 @@ ${frontmatter.yaml}
13623
16919
  setFolderIcon(icon, false);
13624
16920
  }
13625
16921
  if (changed)
13626
- localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
16922
+ persistCollapsedDirs();
13627
16923
  rerenderVirtualSidebar();
13628
16924
  }
13629
16925
  function markActive(path, options = {}) {
@@ -13938,7 +17234,7 @@ ${frontmatter.yaml}
13938
17234
  STATE.collapsedDirs.add(row.path);
13939
17235
  else
13940
17236
  STATE.collapsedDirs.delete(row.path);
13941
- localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
17237
+ persistCollapsedDirs();
13942
17238
  rerenderVirtualSidebar();
13943
17239
  scrollVirtualSidebarPathIntoView(row.path);
13944
17240
  return;
@@ -14427,6 +17723,8 @@ ${frontmatter.yaml}
14427
17723
  preview.className = "gdp-html-preview";
14428
17724
  const frame = document.createElement("iframe");
14429
17725
  frame.title = `${target.path} preview`;
17726
+ frame.sandbox.value = "";
17727
+ frame.referrerPolicy = "no-referrer";
14430
17728
  frame.srcdoc = html;
14431
17729
  preview.appendChild(frame);
14432
17730
  return preview;
@@ -15652,10 +18950,25 @@ ${frontmatter.yaml}
15652
18950
  const UNDO_STACK = [];
15653
18951
  let PENDING_G_SCOPE = null;
15654
18952
  let PENDING_G_UNTIL = 0;
18953
+ let PROJECT_NAME = "";
15655
18954
  const SCOPE_OMIT_DIRS_STORAGE_KEY_PREFIX = "gdp:scope-omit-dirs:";
15656
18955
  const SCOPE_EXCLUDE_NAMES_STORAGE_KEY_PREFIX = "gdp:scope-exclude-names:";
15657
18956
  const CODE_FONT_SIZE_STORAGE_KEY = "gdp:code-font-size";
15658
18957
  const VIEWER_LANGUAGE_STORAGE_KEY = "gdp:language";
18958
+ function scopedKey(base2) {
18959
+ return PROJECT_NAME ? `${base2}:${PROJECT_NAME}` : base2;
18960
+ }
18961
+ function readScopedStorage(base2) {
18962
+ if (PROJECT_NAME) {
18963
+ const v = localStorage.getItem(`${base2}:${PROJECT_NAME}`);
18964
+ if (v !== null)
18965
+ return v;
18966
+ }
18967
+ return localStorage.getItem(base2);
18968
+ }
18969
+ function writeScopedStorage(base2, value) {
18970
+ localStorage.setItem(scopedKey(base2), value);
18971
+ }
15659
18972
  const VIEWER_LANGUAGES = ["en", "ja"];
15660
18973
  const CLIENT_SCOPE_OMIT_DIRS_DEFAULT = [
15661
18974
  "node_modules",
@@ -15770,6 +19083,29 @@ ${frontmatter.yaml}
15770
19083
  projectTitle.textContent = project;
15771
19084
  projectTitle.title = project;
15772
19085
  }
19086
+ reloadScopedState();
19087
+ }
19088
+ function reloadScopedState() {
19089
+ const collapsed = readScopedStorage("gdp:collapsed-dirs");
19090
+ if (collapsed !== null) {
19091
+ STATE.collapsedDirs = new Set(JSON.parse(collapsed));
19092
+ }
19093
+ const viewed = readScopedStorage("gdp:viewed-files");
19094
+ if (viewed !== null) {
19095
+ STATE.viewedFiles = new Set(JSON.parse(viewed));
19096
+ }
19097
+ const igRaw = readScopedStorage("gdp:ignore-ws");
19098
+ if (igRaw !== null)
19099
+ STATE.ignoreWs = igRaw === "1";
19100
+ const from = readScopedStorage("gdp:from");
19101
+ const to = readScopedStorage("gdp:to");
19102
+ if (from !== null)
19103
+ STATE.from = from;
19104
+ if (to !== null)
19105
+ STATE.to = to;
19106
+ const ht = readScopedStorage("gdp:hide-tests");
19107
+ if (ht !== null)
19108
+ STATE.hideTests = ht === "1";
15773
19109
  }
15774
19110
  function savedScopeOmitDirs() {
15775
19111
  const raw = localStorage.getItem(scopeOmitDirsStorageKey());
@@ -15857,10 +19193,10 @@ ${frontmatter.yaml}
15857
19193
  }
15858
19194
  }
15859
19195
  const STATE = (() => {
15860
- const igRaw = localStorage.getItem("gdp:ignore-ws");
19196
+ const igRaw = readScopedStorage("gdp:ignore-ws");
15861
19197
  const fallbackRange = {
15862
- from: localStorage.getItem("gdp:from") || DEFAULT_RANGE.from,
15863
- to: localStorage.getItem("gdp:to") || DEFAULT_RANGE.to
19198
+ from: readScopedStorage("gdp:from") || DEFAULT_RANGE.from,
19199
+ to: readScopedStorage("gdp:to") || DEFAULT_RANGE.to
15864
19200
  };
15865
19201
  const savedLanguage = viewerLanguageFromSearch(window.location.search) || savedViewerLanguage();
15866
19202
  const parsedRoute = parseRoute(window.location.pathname, window.location.search, fallbackRange);
@@ -15873,22 +19209,22 @@ ${frontmatter.yaml}
15873
19209
  sbView: localStorage.getItem("gdp:sbview") || "tree",
15874
19210
  sbWidth: parseInt(localStorage.getItem("gdp:sbwidth") ?? "", 10) || 308,
15875
19211
  sidebarHidden: localStorage.getItem("gdp:sidebar-hidden") === "1",
15876
- collapsedDirs: new Set(JSON.parse(localStorage.getItem("gdp:collapsed-dirs") || "[]")),
19212
+ collapsedDirs: new Set(JSON.parse(readScopedStorage("gdp:collapsed-dirs") || "[]")),
15877
19213
  ignoreWs: igRaw === null ? true : igRaw === "1",
15878
19214
  from: route.range.from,
15879
19215
  to: route.range.to,
15880
19216
  collapsed: false,
15881
19217
  files: [],
15882
19218
  activeFile: null,
15883
- hideTests: localStorage.getItem("gdp:hide-tests") === "1",
19219
+ hideTests: readScopedStorage("gdp:hide-tests") === "1",
15884
19220
  syntaxHighlight: localStorage.getItem("gdp:syntax-highlight") !== "0",
15885
- viewedFiles: new Set(JSON.parse(localStorage.getItem("gdp:viewed-files") || "[]")),
19221
+ viewedFiles: new Set(JSON.parse(readScopedStorage("gdp:viewed-files") || "[]")),
15886
19222
  route,
15887
- repoRef: route.screen === "repo" ? route.ref : "worktree"
19223
+ repoRef: route.screen === "repo" ? route.ref : "worktree",
19224
+ autoUpdate: localStorage.getItem("gdp:auto-update") !== "0"
15888
19225
  };
15889
19226
  })();
15890
19227
  let highlightConfigured = false;
15891
- let PROJECT_NAME = "";
15892
19228
  let REPO_SIDEBAR_REF = null;
15893
19229
  const LINE_REF_PILL = createLineRefPill();
15894
19230
  const DIFF_LINE_SELECT = createDiffLineSelect({ pill: LINE_REF_PILL });
@@ -15912,6 +19248,7 @@ ${frontmatter.yaml}
15912
19248
  fileBadge: (status) => DIFF_VIEW.fileBadge(status),
15913
19249
  fileEntryIcon: () => REPO_VIEW.fileEntryIcon(),
15914
19250
  applyViewedState: () => DIFF_VIEW.applyViewedState(),
19251
+ persistCollapsedDirs: () => writeScopedStorage("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs])),
15915
19252
  appendScopeParams,
15916
19253
  createOpenPathButton,
15917
19254
  normalizeViewerFontSize,
@@ -16074,10 +19411,12 @@ ${frontmatter.yaml}
16074
19411
  repo: "Repository",
16075
19412
  diff: "Diff Viewer",
16076
19413
  history: "History",
19414
+ database: "Database",
16077
19415
  help: "Help"
16078
19416
  },
16079
19417
  global: {
16080
19418
  annotations: "code annotations",
19419
+ queryHistory: "query history",
16081
19420
  settings: "viewer settings",
16082
19421
  theme: "toggle theme",
16083
19422
  product: "code viewer"
@@ -16096,7 +19435,18 @@ ${frontmatter.yaml}
16096
19435
  syntaxLoadingTitle: "loading syntax highlighter",
16097
19436
  syntaxErrorTitle: "failed to load syntax highlighter",
16098
19437
  syntaxOffTitle: "syntax highlighting off",
16099
- hideTests: "hide test files (test|spec)"
19438
+ hideTests: "hide test files (test|spec)",
19439
+ autoUpdate: "auto",
19440
+ autoUpdateOnTitle: "auto update on file change",
19441
+ autoUpdateOffTitle: "auto update off — manual reload"
19442
+ },
19443
+ changeBanner: {
19444
+ text: "Files changed",
19445
+ reload: "Reload",
19446
+ justNow: "just now",
19447
+ secondsAgo: (seconds) => `${seconds}s ago`,
19448
+ minutesAgo: (minutes) => `${minutes}m ago`,
19449
+ hoursAgo: (hours) => `${hours}h ago`
16100
19450
  },
16101
19451
  sidebar: {
16102
19452
  files: "Files",
@@ -16120,7 +19470,8 @@ ${frontmatter.yaml}
16120
19470
  close: "close viewer settings",
16121
19471
  display: "Display",
16122
19472
  language: "Language",
16123
- fileListFontSize: "File list font size",
19473
+ fileListFontSize: "UI font size",
19474
+ fileListFontSizeHelp: "Applies to the file sidebar and database UI.",
16124
19475
  codeFontSize: "Code font size",
16125
19476
  sizeSmall: "Small",
16126
19477
  sizeRegular: "Regular",
@@ -16150,10 +19501,12 @@ ${frontmatter.yaml}
16150
19501
  repo: "リポジトリ",
16151
19502
  diff: "Diff ビューア",
16152
19503
  history: "履歴",
19504
+ database: "データベース",
16153
19505
  help: "ヘルプ"
16154
19506
  },
16155
19507
  global: {
16156
19508
  annotations: "コード注釈",
19509
+ queryHistory: "クエリ履歴",
16157
19510
  settings: "ビューア設定",
16158
19511
  theme: "テーマ切り替え",
16159
19512
  product: "code viewer"
@@ -16172,7 +19525,18 @@ ${frontmatter.yaml}
16172
19525
  syntaxLoadingTitle: "シンタックスハイライトを読み込み中",
16173
19526
  syntaxErrorTitle: "シンタックスハイライトの読み込みに失敗",
16174
19527
  syntaxOffTitle: "シンタックスハイライト無効",
16175
- hideTests: "test/spec ファイルを隠す"
19528
+ hideTests: "test/spec ファイルを隠す",
19529
+ autoUpdate: "自動",
19530
+ autoUpdateOnTitle: "ファイル変更時に自動更新",
19531
+ autoUpdateOffTitle: "自動更新オフ — 手動で再読み込み"
19532
+ },
19533
+ changeBanner: {
19534
+ text: "ファイルに変更がありました",
19535
+ reload: "再読み込みする",
19536
+ justNow: "たった今",
19537
+ secondsAgo: (seconds) => `${seconds}秒前`,
19538
+ minutesAgo: (minutes) => `${minutes}分前`,
19539
+ hoursAgo: (hours) => `${hours}時間前`
16176
19540
  },
16177
19541
  sidebar: {
16178
19542
  files: "ファイル",
@@ -16196,8 +19560,9 @@ ${frontmatter.yaml}
16196
19560
  close: "ビューア設定を閉じる",
16197
19561
  display: "表示",
16198
19562
  language: "言語",
16199
- fileListFontSize: "ファイル一覧の文字サイズ",
16200
- codeFontSize: "コードの文字サイズ",
19563
+ fileListFontSize: "UIの文字サイズ",
19564
+ fileListFontSizeHelp: "ファイル一覧とデータベース画面に適用されます。",
19565
+ codeFontSize: "コード表示の文字サイズ",
16201
19566
  sizeSmall: "小",
16202
19567
  sizeRegular: "標準",
16203
19568
  sizeLarge: "大",
@@ -16256,6 +19621,11 @@ ${frontmatter.yaml}
16256
19621
  annotationsToggle.title = text2.global.annotations;
16257
19622
  annotationsToggle.setAttribute("aria-label", text2.global.annotations);
16258
19623
  }
19624
+ const queryHistoryToggle = document.querySelector("#query-history-toggle");
19625
+ if (queryHistoryToggle) {
19626
+ queryHistoryToggle.title = text2.global.queryHistory;
19627
+ queryHistoryToggle.setAttribute("aria-label", text2.global.queryHistory);
19628
+ }
16259
19629
  const viewerSettings = document.querySelector("#viewer-settings");
16260
19630
  if (viewerSettings) {
16261
19631
  viewerSettings.title = text2.global.settings;
@@ -16280,6 +19650,7 @@ ${frontmatter.yaml}
16280
19650
  const hideTests = document.querySelector("#hide-tests");
16281
19651
  if (hideTests)
16282
19652
  hideTests.title = text2.topbar.hideTests;
19653
+ applyAutoUpdateButton();
16283
19654
  setHighlightButton(STATE.syntaxHighlight && getHljs() ? "loaded" : "idle");
16284
19655
  setElementText(".sb-title", text2.sidebar.files);
16285
19656
  const sidebarActions = document.querySelector(".sb-actions");
@@ -16349,6 +19720,7 @@ ${frontmatter.yaml}
16349
19720
  large: text2.settings.sizeLarge,
16350
19721
  xlarge: text2.settings.sizeExtraLarge
16351
19722
  });
19723
+ setElementText("#ui-font-size-help", text2.settings.fileListFontSizeHelp);
16352
19724
  setElementText("#display-settings-source", text2.settings.displaySource);
16353
19725
  setButtonLabel(document.querySelector("#scope-omit-reset"), text2.settings.reset);
16354
19726
  setButtonLabel(document.querySelector("#scope-omit-save"), text2.settings.save);
@@ -16364,6 +19736,8 @@ ${frontmatter.yaml}
16364
19736
  setButtonLabel(document.querySelector("#annotation-clear"), text2.annotations.clear);
16365
19737
  setButtonLabel(document.querySelector("#annotation-panel-close"), text2.annotations.close);
16366
19738
  setElementText(".annotation-list-head strong", text2.annotations.sessions);
19739
+ setElementText(".query-history-panel-head strong", text2.global.queryHistory);
19740
+ setButtonLabel(document.querySelector("#query-history-panel-close"), text2.annotations.close);
16367
19741
  }
16368
19742
  function setViewerLanguage(language, persist = true) {
16369
19743
  const next = normalizeViewerLanguage(language);
@@ -16652,6 +20026,9 @@ ${frontmatter.yaml}
16652
20026
  function repoFileTargetFromRoute() {
16653
20027
  return STATE.route.screen === "file" && STATE.route.view === "blob" ? STATE.route.ref : null;
16654
20028
  }
20029
+ function isRepoBlobRoute(route) {
20030
+ return route.screen === "file" && route.view === "blob";
20031
+ }
16655
20032
  let ANNOTATIONS_UI = null;
16656
20033
  function applyInlineAnnotations() {
16657
20034
  ANNOTATIONS_UI?.applyInlineAnnotations();
@@ -16681,12 +20058,23 @@ ${frontmatter.yaml}
16681
20058
  syncHeaderMenu();
16682
20059
  syncLineRefPill();
16683
20060
  }
20061
+ function setQueryHistoryPanelOpen(open) {
20062
+ const panel = document.getElementById("query-history-panel");
20063
+ if (!panel)
20064
+ return;
20065
+ panel.hidden = !open;
20066
+ document.body.classList.toggle("query-history-panel-open", open);
20067
+ if (open && ANNOTATIONS_UI) {
20068
+ ANNOTATIONS_UI.setAnnotationPanelOpen(false);
20069
+ }
20070
+ }
16684
20071
  function setPageMode() {
16685
20072
  document.body.classList.toggle("gdp-file-detail-page", STATE.route.screen === "file");
16686
20073
  document.body.classList.toggle("gdp-repo-blob-page", STATE.route.screen === "file" && STATE.route.view === "blob");
16687
20074
  document.body.classList.toggle("gdp-repo-page", STATE.route.screen === "repo");
16688
20075
  document.body.classList.toggle("gdp-help-page", STATE.route.screen === "help");
16689
20076
  document.body.classList.toggle("gdp-history-page", STATE.route.screen === "history");
20077
+ document.body.classList.toggle("gdp-database-page", STATE.route.screen === "database");
16690
20078
  placeSidebarToggle();
16691
20079
  syncSidebarHeaderHeight();
16692
20080
  const historyPanel = $("#history-panel");
@@ -16698,6 +20086,19 @@ ${frontmatter.yaml}
16698
20086
  historyRefInput.value = STATE.route.ref || "HEAD";
16699
20087
  }
16700
20088
  syncRepoTargetInput(repoFileTargetFromRoute() || "worktree");
20089
+ const isDatabase = STATE.route.screen === "database";
20090
+ const annotationsToggle = document.querySelector("#annotations-toggle");
20091
+ const qhToggle = document.querySelector("#query-history-toggle");
20092
+ if (annotationsToggle)
20093
+ annotationsToggle.hidden = isDatabase;
20094
+ if (qhToggle)
20095
+ qhToggle.hidden = !isDatabase;
20096
+ if (!isDatabase) {
20097
+ setQueryHistoryPanelOpen(false);
20098
+ }
20099
+ if (isDatabase && ANNOTATIONS_UI) {
20100
+ ANNOTATIONS_UI.setAnnotationPanelOpen(false);
20101
+ }
16701
20102
  }
16702
20103
  function syncHeaderMenu() {
16703
20104
  document.querySelectorAll(".app-menu-item, .global-help-link").forEach((link2) => {
@@ -16726,6 +20127,12 @@ ${frontmatter.yaml}
16726
20127
  range: currentRange()
16727
20128
  });
16728
20129
  }
20130
+ if (link2.dataset.route === "database") {
20131
+ link2.href = buildRoute({
20132
+ screen: "database",
20133
+ range: currentRange()
20134
+ });
20135
+ }
16729
20136
  if (link2.dataset.route === "help") {
16730
20137
  link2.href = buildRoute({
16731
20138
  screen: "help",
@@ -16868,6 +20275,7 @@ ${frontmatter.yaml}
16868
20275
  setProjectName,
16869
20276
  getProjectName: () => PROJECT_NAME,
16870
20277
  createOpenPathButton,
20278
+ persistViewedFiles: () => writeScopedStorage("gdp:viewed-files", JSON.stringify([...STATE.viewedFiles])),
16871
20279
  applyHideTests: () => applyHideTests(),
16872
20280
  getServerGeneration: () => SERVER_GENERATION,
16873
20281
  setServerGeneration: (generation) => {
@@ -17221,10 +20629,24 @@ ${frontmatter.yaml}
17221
20629
  setStatus("live");
17222
20630
  renderHelpPage();
17223
20631
  syncHeaderMenu();
17224
- return Promise.resolve();
20632
+ return Promise.resolve(null);
20633
+ }
20634
+ if (STATE.route.screen === "database") {
20635
+ DATABASE_VIEW.enter(STATE.route.db, STATE.route.table, STATE.route.tab);
20636
+ setStatus("live");
20637
+ return Promise.resolve(null);
20638
+ }
20639
+ if (isRepoBlobRoute(STATE.route)) {
20640
+ setStatus("live");
20641
+ applySourceRouteToShell();
20642
+ return Promise.resolve({
20643
+ structureChanged: false,
20644
+ invalidatedCards: 0,
20645
+ preservedDom: true
20646
+ });
17225
20647
  }
17226
20648
  if (STATE.route.screen === "repo")
17227
- return loadRepo();
20649
+ return loadRepo().then(() => null);
17228
20650
  {
17229
20651
  const empty = $("#empty");
17230
20652
  if (empty) {
@@ -17249,9 +20671,13 @@ ${frontmatter.yaml}
17249
20671
  params.set("nocache", "1");
17250
20672
  const url = `/diff.json${params.toString() ? `?${params.toString()}` : ""}`;
17251
20673
  return trackLoad(fetch(url).then((r2) => r2.json())).then((data) => {
17252
- renderShell(data);
20674
+ const result = renderShell(data, options.changedPaths);
17253
20675
  setStatus("live");
17254
- }).catch(() => setStatus("error"));
20676
+ return result;
20677
+ }).catch(() => {
20678
+ setStatus("error");
20679
+ return null;
20680
+ });
17255
20681
  }
17256
20682
  loadSettings().finally(() => {
17257
20683
  if (STATE.route.screen === "help") {
@@ -17266,6 +20692,9 @@ ${frontmatter.yaml}
17266
20692
  parkRangeForHistory();
17267
20693
  setStatus("live");
17268
20694
  HISTORY_VIEW.enterHistory();
20695
+ } else if (STATE.route.screen === "database") {
20696
+ setStatus("live");
20697
+ DATABASE_VIEW.enter(STATE.route.db, STATE.route.table, STATE.route.tab);
17269
20698
  } else
17270
20699
  load();
17271
20700
  syncLineRefPill();
@@ -17281,8 +20710,8 @@ ${frontmatter.yaml}
17281
20710
  preHistoryRange = null;
17282
20711
  STATE.from = from || "";
17283
20712
  STATE.to = to || "";
17284
- localStorage.setItem("gdp:from", STATE.from);
17285
- localStorage.setItem("gdp:to", STATE.to);
20713
+ writeScopedStorage("gdp:from", STATE.from);
20714
+ writeScopedStorage("gdp:to", STATE.to);
17286
20715
  syncRefInputs();
17287
20716
  const range = currentRange();
17288
20717
  if (STATE.route.screen === "file") {
@@ -17312,7 +20741,7 @@ ${frontmatter.yaml}
17312
20741
  STATE.from = range.from;
17313
20742
  STATE.to = range.to;
17314
20743
  syncRefInputs();
17315
- return load();
20744
+ return load().then(() => {});
17316
20745
  },
17317
20746
  showEmptyDiffPane: () => {
17318
20747
  const diff = $("#diff");
@@ -17332,6 +20761,13 @@ ${frontmatter.yaml}
17332
20761
  },
17333
20762
  trackLoad
17334
20763
  });
20764
+ const DATABASE_VIEW = createDatabaseView({
20765
+ setRoute,
20766
+ setPageMode,
20767
+ currentRange,
20768
+ trackLoad,
20769
+ syncHeaderMenu
20770
+ });
17335
20771
  const REF_PICKER = createRefPicker({
17336
20772
  $,
17337
20773
  escapeHtml: escapeHtml3,
@@ -17356,6 +20792,9 @@ ${frontmatter.yaml}
17356
20792
  if (STATE.route.screen === "history" && window.location.pathname !== "/history") {
17357
20793
  restoreRangeAfterHistory();
17358
20794
  }
20795
+ if (STATE.route.screen === "database" && window.location.pathname !== "/database") {
20796
+ DATABASE_VIEW.leave();
20797
+ }
17359
20798
  const parsedRoute = parseRoute(window.location.pathname, window.location.search, currentRange());
17360
20799
  const routeLanguage = viewerLanguageFromSearch(window.location.search);
17361
20800
  if (routeLanguage && routeLanguage !== STATE.language)
@@ -17392,6 +20831,14 @@ ${frontmatter.yaml}
17392
20831
  HISTORY_VIEW.enterHistory();
17393
20832
  return;
17394
20833
  }
20834
+ if (STATE.route.screen === "database") {
20835
+ cancelActiveSourceLoad("navigation");
20836
+ setPageMode();
20837
+ removeStandaloneSource();
20838
+ DATABASE_VIEW.enter(STATE.route.db, STATE.route.table, STATE.route.tab);
20839
+ setStatus("live");
20840
+ return;
20841
+ }
17395
20842
  if (STATE.route.screen !== "file") {
17396
20843
  cancelActiveSourceLoad("navigation");
17397
20844
  setPageMode();
@@ -17423,7 +20870,7 @@ ${frontmatter.yaml}
17423
20870
  applyIgnoreWs();
17424
20871
  $("#ignore-ws").addEventListener("click", () => {
17425
20872
  STATE.ignoreWs = !STATE.ignoreWs;
17426
- localStorage.setItem("gdp:ignore-ws", STATE.ignoreWs ? "1" : "0");
20873
+ writeScopedStorage("gdp:ignore-ws", STATE.ignoreWs ? "1" : "0");
17427
20874
  applyIgnoreWs();
17428
20875
  load();
17429
20876
  });
@@ -17480,7 +20927,7 @@ ${frontmatter.yaml}
17480
20927
  applyHideTests();
17481
20928
  $("#hide-tests").addEventListener("click", () => {
17482
20929
  STATE.hideTests = !STATE.hideTests;
17483
- localStorage.setItem("gdp:hide-tests", STATE.hideTests ? "1" : "0");
20930
+ writeScopedStorage("gdp:hide-tests", STATE.hideTests ? "1" : "0");
17484
20931
  applyHideTests();
17485
20932
  });
17486
20933
  ANNOTATIONS_UI = createAnnotationsUi({
@@ -17504,8 +20951,8 @@ ${frontmatter.yaml}
17504
20951
  setRange: (from, to) => {
17505
20952
  STATE.from = from;
17506
20953
  STATE.to = to;
17507
- localStorage.setItem("gdp:from", from);
17508
- localStorage.setItem("gdp:to", to);
20954
+ writeScopedStorage("gdp:from", from);
20955
+ writeScopedStorage("gdp:to", to);
17509
20956
  }
17510
20957
  });
17511
20958
  createAnnotationsPlayer({
@@ -17517,35 +20964,240 @@ ${frontmatter.yaml}
17517
20964
  onAnnotationOpened: (cb) => ANNOTATIONS_UI?.onAnnotationOpened(cb),
17518
20965
  getActiveAnnotationId: () => ANNOTATIONS_UI ? ANNOTATIONS_UI.getActiveAnnotationId() : null
17519
20966
  });
20967
+ const qhToggleBtn = document.getElementById("query-history-toggle");
20968
+ if (qhToggleBtn) {
20969
+ qhToggleBtn.addEventListener("click", () => {
20970
+ const panel = document.getElementById("query-history-panel");
20971
+ const opening = panel ? panel.hidden : true;
20972
+ setQueryHistoryPanelOpen(opening);
20973
+ if (opening)
20974
+ DATABASE_VIEW.handleSse();
20975
+ });
20976
+ }
20977
+ const qhCloseBtn = document.getElementById("query-history-panel-close");
20978
+ if (qhCloseBtn) {
20979
+ qhCloseBtn.addEventListener("click", () => {
20980
+ setQueryHistoryPanelOpen(false);
20981
+ });
20982
+ }
20983
+ (function setupQueryHistoryResizer() {
20984
+ const panel = document.getElementById("query-history-panel");
20985
+ const handle = document.getElementById("query-history-resizer");
20986
+ if (!panel || !handle)
20987
+ return;
20988
+ const STORAGE_KEY = "gdp:qh-panel-width";
20989
+ const MIN_W = 280;
20990
+ const MAX_W = 800;
20991
+ const saved = localStorage.getItem(STORAGE_KEY);
20992
+ if (saved) {
20993
+ const w = Math.max(MIN_W, Math.min(MAX_W, Number(saved) || 420));
20994
+ panel.style.width = `${w}px`;
20995
+ }
20996
+ let dragging = false;
20997
+ let startX = 0;
20998
+ let startW = 0;
20999
+ handle.addEventListener("mousedown", (e2) => {
21000
+ dragging = true;
21001
+ startX = e2.clientX;
21002
+ startW = panel.offsetWidth;
21003
+ document.body.classList.add("db-resizing");
21004
+ e2.preventDefault();
21005
+ });
21006
+ window.addEventListener("mousemove", (e2) => {
21007
+ if (!dragging)
21008
+ return;
21009
+ const w = Math.max(MIN_W, Math.min(MAX_W, startW - (e2.clientX - startX)));
21010
+ panel.style.width = `${w}px`;
21011
+ });
21012
+ window.addEventListener("mouseup", () => {
21013
+ if (!dragging)
21014
+ return;
21015
+ dragging = false;
21016
+ document.body.classList.remove("db-resizing");
21017
+ localStorage.setItem(STORAGE_KEY, String(panel.offsetWidth));
21018
+ });
21019
+ })();
21020
+ function applyAutoUpdateButton() {
21021
+ const btn = document.querySelector("#auto-update");
21022
+ if (!btn)
21023
+ return;
21024
+ const text2 = uiText();
21025
+ btn.classList.toggle("active", STATE.autoUpdate);
21026
+ btn.textContent = text2.topbar.autoUpdate;
21027
+ btn.title = STATE.autoUpdate ? text2.topbar.autoUpdateOnTitle : text2.topbar.autoUpdateOffTitle;
21028
+ btn.setAttribute("aria-pressed", STATE.autoUpdate ? "true" : "false");
21029
+ }
21030
+ function setAutoUpdate(on) {
21031
+ STATE.autoUpdate = on;
21032
+ localStorage.setItem("gdp:auto-update", on ? "1" : "0");
21033
+ applyAutoUpdateButton();
21034
+ if (on) {
21035
+ if (bannerPendingPaths) {
21036
+ const paths = bannerPendingPaths;
21037
+ hideChangeBanner();
21038
+ doSseLoad(paths);
21039
+ return;
21040
+ }
21041
+ hideChangeBanner();
21042
+ }
21043
+ }
21044
+ let bannerPendingPaths = null;
21045
+ let changeBannerShownAt = 0;
21046
+ let changeBannerAgeTimer = null;
21047
+ function formatChangeBannerAge(now) {
21048
+ const text2 = uiText().changeBanner;
21049
+ const elapsedSeconds = Math.max(0, Math.floor((now - changeBannerShownAt) / 1000));
21050
+ if (elapsedSeconds < 5)
21051
+ return text2.justNow;
21052
+ if (elapsedSeconds < 60)
21053
+ return text2.secondsAgo(elapsedSeconds);
21054
+ const elapsedMinutes = Math.floor(elapsedSeconds / 60);
21055
+ if (elapsedMinutes < 60)
21056
+ return text2.minutesAgo(elapsedMinutes);
21057
+ return text2.hoursAgo(Math.floor(elapsedMinutes / 60));
21058
+ }
21059
+ function updateChangeBannerAge() {
21060
+ const ageEl = document.getElementById("change-banner-age");
21061
+ if (ageEl)
21062
+ ageEl.textContent = formatChangeBannerAge(Date.now());
21063
+ }
21064
+ function showChangeBanner(paths) {
21065
+ bannerPendingPaths = paths;
21066
+ changeBannerShownAt = Date.now();
21067
+ const banner = document.getElementById("change-banner");
21068
+ if (!banner)
21069
+ return;
21070
+ const text2 = uiText();
21071
+ const textEl = document.getElementById("change-banner-text");
21072
+ if (textEl)
21073
+ textEl.textContent = text2.changeBanner.text;
21074
+ updateChangeBannerAge();
21075
+ if (!changeBannerAgeTimer) {
21076
+ changeBannerAgeTimer = setInterval(updateChangeBannerAge, 1000);
21077
+ }
21078
+ const reloadBtn = document.getElementById("change-banner-reload");
21079
+ if (reloadBtn)
21080
+ reloadBtn.textContent = text2.changeBanner.reload;
21081
+ banner.hidden = false;
21082
+ }
21083
+ function hideChangeBanner() {
21084
+ const banner = document.getElementById("change-banner");
21085
+ if (banner)
21086
+ banner.hidden = true;
21087
+ bannerPendingPaths = null;
21088
+ changeBannerShownAt = 0;
21089
+ if (changeBannerAgeTimer) {
21090
+ clearInterval(changeBannerAgeTimer);
21091
+ changeBannerAgeTimer = null;
21092
+ }
21093
+ }
21094
+ document.getElementById("change-banner-reload")?.addEventListener("click", () => {
21095
+ const paths = bannerPendingPaths;
21096
+ hideChangeBanner();
21097
+ const route = STATE.route;
21098
+ if (isRepoBlobRoute(route)) {
21099
+ renderStandaloneSource({
21100
+ path: route.path,
21101
+ ref: route.ref || "worktree"
21102
+ });
21103
+ return;
21104
+ }
21105
+ doSseLoad(paths);
21106
+ });
21107
+ document.getElementById("change-banner-dismiss")?.addEventListener("click", () => {
21108
+ hideChangeBanner();
21109
+ });
21110
+ document.getElementById("auto-update")?.addEventListener("click", () => {
21111
+ setAutoUpdate(!STATE.autoUpdate);
21112
+ });
21113
+ applyAutoUpdateButton();
21114
+ function doSseLoad(paths) {
21115
+ const route = STATE.route;
21116
+ if (isRepoBlobRoute(route)) {
21117
+ const viewingPath = route.path;
21118
+ if (paths && viewingPath && !paths.has(viewingPath))
21119
+ return;
21120
+ renderStandaloneSource({
21121
+ path: route.path,
21122
+ ref: route.ref || "worktree"
21123
+ });
21124
+ return;
21125
+ }
21126
+ if (route.screen === "repo") {
21127
+ invalidateRepoSidebar();
21128
+ loadRepo();
21129
+ return;
21130
+ }
21131
+ const savedScroll = window.scrollY;
21132
+ const savedActive = STATE.activeFile;
21133
+ load({ changedPaths: paths }).then((result) => {
21134
+ if (result?.preservedDom)
21135
+ return;
21136
+ if (savedActive) {
21137
+ const card = document.querySelector(diffCardSelector(savedActive));
21138
+ if (card) {
21139
+ card.scrollIntoView({ block: "start" });
21140
+ return;
21141
+ }
21142
+ }
21143
+ window.scrollTo(0, savedScroll);
21144
+ });
21145
+ }
17520
21146
  let sseTimer = null;
17521
- function scheduleSseLoad() {
21147
+ let pendingSseChangedPaths = new Set;
21148
+ function scheduleSseLoad(changedPaths) {
21149
+ if (STATE.route.screen === "database" || STATE.route.screen === "help")
21150
+ return;
21151
+ if (changedPaths && pendingSseChangedPaths) {
21152
+ for (const p2 of changedPaths)
21153
+ pendingSseChangedPaths.add(p2);
21154
+ } else {
21155
+ pendingSseChangedPaths = null;
21156
+ }
17522
21157
  if (sseTimer)
17523
21158
  clearTimeout(sseTimer);
17524
21159
  sseTimer = setTimeout(() => {
17525
21160
  sseTimer = null;
17526
- invalidateRepoSidebar();
17527
- const savedScroll = window.scrollY;
17528
- const savedActive = STATE.activeFile;
17529
- load().then(() => {
17530
- if (savedActive) {
17531
- const card = document.querySelector(diffCardSelector(savedActive));
17532
- if (card) {
17533
- card.scrollIntoView({ block: "start" });
17534
- return;
17535
- }
17536
- }
17537
- window.scrollTo(0, savedScroll);
17538
- });
21161
+ const paths = pendingSseChangedPaths;
21162
+ pendingSseChangedPaths = new Set;
21163
+ const route = STATE.route;
21164
+ if (isRepoBlobRoute(route)) {
21165
+ const viewingPath = route.path;
21166
+ if (paths && viewingPath && !paths.has(viewingPath))
21167
+ return;
21168
+ }
21169
+ if (STATE.autoUpdate) {
21170
+ doSseLoad(paths);
21171
+ } else {
21172
+ showChangeBanner(paths);
21173
+ }
17539
21174
  }, 350);
17540
21175
  }
17541
21176
  const es = new EventSource("/events");
17542
21177
  const catchUpGate = createCatchUpGate(() => Date.now(), 1000);
17543
21178
  let openedOnce = false;
17544
- es.addEventListener("update", () => scheduleSseLoad());
21179
+ es.addEventListener("update", (event) => {
21180
+ const raw = event.data;
21181
+ let paths = null;
21182
+ if (raw && raw !== "tick") {
21183
+ try {
21184
+ const parsed = JSON.parse(raw);
21185
+ if (Array.isArray(parsed.paths))
21186
+ paths = parsed.paths;
21187
+ } catch {}
21188
+ }
21189
+ scheduleSseLoad(paths);
21190
+ });
17545
21191
  es.addEventListener("reload", () => location.reload());
17546
21192
  es.addEventListener("annotation", (event) => {
17547
21193
  ANNOTATIONS_UI?.handleSse(event.data);
17548
21194
  });
21195
+ es.addEventListener("db-query", () => {
21196
+ DATABASE_VIEW.handleSse("db-query");
21197
+ });
21198
+ es.addEventListener("db-snapshot", (event) => {
21199
+ DATABASE_VIEW.handleSse("db-snapshot", event.data);
21200
+ });
17549
21201
  es.addEventListener("error", () => setStatus("error"));
17550
21202
  es.addEventListener("open", () => {
17551
21203
  setStatus("live");
@@ -17560,6 +21212,10 @@ ${frontmatter.yaml}
17560
21212
  return;
17561
21213
  if (!catchUpGate())
17562
21214
  return;
21215
+ if (!STATE.autoUpdate) {
21216
+ showChangeBanner(null);
21217
+ return;
21218
+ }
17563
21219
  load({ force: true });
17564
21220
  }
17565
21221
  document.addEventListener("visibilitychange", () => {