sqlite-hub 0.9.1 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.github/workflows/ci.yml +36 -0
  2. package/README.md +2 -2
  3. package/bin/sqlite-hub.js +1 -1
  4. package/frontend/index.html +3 -158
  5. package/frontend/js/app.js +231 -5
  6. package/frontend/js/components/connectionCard.js +62 -87
  7. package/frontend/js/components/emptyState.js +20 -23
  8. package/frontend/js/components/modal.js +158 -199
  9. package/frontend/js/components/pageHeader.js +1 -1
  10. package/frontend/js/components/queryEditor.js +16 -30
  11. package/frontend/js/components/queryHistoryDetail.js +93 -164
  12. package/frontend/js/components/queryHistoryPanel.js +90 -100
  13. package/frontend/js/components/queryResults.js +3 -1
  14. package/frontend/js/components/rowEditorPanel.js +76 -56
  15. package/frontend/js/components/tableDesignerEditor.js +91 -116
  16. package/frontend/js/lib/queryChartOptions.js +5 -1
  17. package/frontend/js/lib/queryCharts.js +50 -2
  18. package/frontend/js/store.js +161 -23
  19. package/frontend/js/utils/tableDesigner.js +8 -2
  20. package/frontend/js/views/charts.js +126 -73
  21. package/frontend/js/views/data.js +147 -133
  22. package/frontend/js/views/editor.js +1 -0
  23. package/frontend/js/views/mediaTagging.js +131 -164
  24. package/frontend/js/views/structure.js +52 -48
  25. package/frontend/styles/base.css +57 -13
  26. package/frontend/styles/components.css +166 -96
  27. package/frontend/styles/layout.css +1 -1
  28. package/frontend/styles/structure-graph.css +11 -11
  29. package/frontend/styles/tailwind.css +81 -0
  30. package/frontend/styles/tailwind.generated.css +1 -0
  31. package/frontend/styles/tokens.css +50 -7
  32. package/frontend/styles/utilities.css +21 -0
  33. package/frontend/styles/views.css +94 -86
  34. package/package.json +16 -3
  35. package/server/routes/mediaTagging.js +2 -10
  36. package/server/routes/sql.js +35 -10
  37. package/server/server.js +24 -0
  38. package/server/services/sqlite/dataBrowserService.js +25 -5
  39. package/server/services/sqlite/exportService.js +4 -2
  40. package/server/services/sqlite/introspection.js +2 -2
  41. package/server/services/sqlite/mediaTaggingService.js +166 -53
  42. package/server/services/sqlite/structureService.js +2 -2
  43. package/server/services/sqlite/tableDesigner/sql.js +19 -3
  44. package/server/services/storage/appStateStore.js +227 -87
  45. package/server/services/storage/queryHistoryChartUtils.js +19 -2
  46. package/server/utils/appPaths.js +55 -19
  47. package/server/utils/fileValidation.js +94 -8
  48. package/tailwind.config.cjs +73 -0
  49. package/tests/security-paths.test.js +84 -0
  50. package/tests/sql-identifier-safety.test.js +66 -0
  51. package/.npmingnore +0 -4
  52. package/changelog.md +0 -77
  53. package/docs/DESIGN_GUIDELINES.md +0 -36
  54. package/scripts/publish_brew.sh +0 -466
  55. package/scripts/publish_npm.sh +0 -241
  56. package/shortkeys.md +0 -5
@@ -0,0 +1,36 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ build-test-audit:
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - name: Checkout
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Setup Node
21
+ uses: actions/setup-node@v4
22
+ with:
23
+ node-version: 26
24
+ cache: npm
25
+
26
+ - name: Install dependencies
27
+ run: npm ci
28
+
29
+ - name: Build
30
+ run: npm run build
31
+
32
+ - name: Test
33
+ run: npm test
34
+
35
+ - name: Dependency audit
36
+ run: npm run audit
package/README.md CHANGED
@@ -66,13 +66,13 @@ brew install sqlite-hub
66
66
  ### NPM
67
67
 
68
68
  ```bash
69
- npm install -g sqlit-hub
69
+ npm install -g sqlite-hub
70
70
  ```
71
71
 
72
72
  ## alternative port
73
73
 
74
74
  ```bash
75
- sqlit-hub --port:4174
75
+ sqlite-hub --port:4174
76
76
  ```
77
77
 
78
78
  ## CLI Interface
package/bin/sqlite-hub.js CHANGED
@@ -334,7 +334,7 @@ async function main() {
334
334
  } else if (stmt.kind === 'mutation') {
335
335
  console.log(`Changes: ${stmt.changes}`);
336
336
  if (stmt.lastInsertRowid) {
337
- console.log(`Last insert rowid: ${stmt.lastInsertRowid}`);
337
+ console.log('Last insert rowid:', stmt.lastInsertRowid);
338
338
  }
339
339
  }
340
340
  });
@@ -6,166 +6,11 @@
6
6
  <title>SQLite Hub</title>
7
7
  <link href="/assets/images/logo_raw.png" rel="icon" type="image/png" />
8
8
  <link href="/assets/images/logo_raw.png" rel="shortcut icon" type="image/png" />
9
- <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
10
- <link
11
- href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700;800;900&family=Inter:wght@300;400;500;600;700;800&family=Roboto+Mono:wght@400;500;700&display=swap"
12
- rel="stylesheet"
13
- />
14
- <link
15
- href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
16
- rel="stylesheet"
17
- />
18
- <script id="tailwind-config">
19
- tailwind.config = {
20
- darkMode: "class",
21
- theme: {
22
- extend: {
23
- colors: {
24
- "on-secondary-fixed": "#1c1b1b",
25
- background: "#131313",
26
- "inverse-primary": "#6a5f00",
27
- "primary-fixed": "#fde403",
28
- tertiary: "#fbfffe",
29
- outline: "#979177",
30
- "on-secondary": "#313030",
31
- "error-container": "#93000a",
32
- "on-primary-fixed-variant": "#504700",
33
- "on-tertiary-container": "#006f72",
34
- "on-tertiary-fixed-variant": "#004f51",
35
- "secondary-fixed-dim": "#c8c6c5",
36
- "tertiary-fixed-dim": "#00dce1",
37
- "on-background": "#e5e2e1",
38
- "surface-tint": "#dec800",
39
- "primary-fixed-dim": "#dec800",
40
- "on-surface": "#e5e2e1",
41
- "surface-container-low": "#1c1b1b",
42
- "on-tertiary": "#003738",
43
- "surface-container": "#201f1f",
44
- "on-surface-variant": "#cdc7ab",
45
- "on-primary-container": "#706400",
46
- "on-error": "#690005",
47
- "surface-container-high": "#2a2a2a",
48
- "surface-container-highest": "#353534",
49
- error: "#ffb4ab",
50
- primary: "#fffeff",
51
- "outline-variant": "#4b4732",
52
- "on-error-container": "#ffdad6",
53
- "on-secondary-fixed-variant": "#474746",
54
- "surface-container-lowest": "#0e0e0e",
55
- "inverse-surface": "#e5e2e1",
56
- surface: "#131313",
57
- "tertiary-container": "#04faff",
58
- "on-tertiary-fixed": "#002021",
59
- "tertiary-fixed": "#2dfaff",
60
- "surface-variant": "#353534",
61
- "on-primary": "#373100",
62
- "primary-container": "#fce300",
63
- "inverse-on-surface": "#313030",
64
- secondary: "#c8c6c5",
65
- "on-secondary-container": "#b7b5b4",
66
- "on-primary-fixed": "#201c00",
67
- "secondary-container": "#474746",
68
- "surface-dim": "#131313",
69
- "surface-bright": "#3a3939",
70
- "secondary-fixed": "#e5e2e1",
71
- },
72
- fontFamily: {
73
- headline: ["Space Grotesk"],
74
- body: ["Inter"],
75
- label: ["Inter"],
76
- mono: ["Roboto Mono"],
77
- },
78
- borderRadius: {
79
- DEFAULT: "0px",
80
- lg: "0px",
81
- xl: "0px",
82
- full: "9999px",
83
- },
84
- },
85
- },
86
- };
87
- </script>
88
- <style type="text/tailwindcss">
89
- @layer components {
90
- .signature-button {
91
- @apply inline-flex items-center justify-center gap-2 h-[var(--control-height)] min-h-[var(--control-height)] border border-primary-container bg-primary-container px-[var(--control-padding-inline)] font-headline text-xs font-bold uppercase tracking-[0.16em] text-on-primary transition-all;
92
- box-shadow: 0 0 18px -10px rgba(252, 227, 0, 0.7);
93
- clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);
94
- }
95
-
96
- .signature-button:hover {
97
- @apply -translate-y-px bg-primary-fixed text-on-primary;
98
- border-color: rgba(252, 227, 0, 0.55);
99
- box-shadow: 0 0 24px -8px rgba(252, 227, 0, 0.92);
100
- filter: brightness(1.03);
101
- }
102
-
103
- .signature-button:disabled,
104
- .signature-button[aria-disabled="true"] {
105
- @apply cursor-not-allowed opacity-45;
106
- box-shadow: none;
107
- filter: none;
108
- transform: none;
109
- }
110
-
111
- .signature-button .material-symbols-outlined {
112
- @apply text-base;
113
- }
114
-
115
- .standard-button {
116
- @apply inline-flex items-center justify-center gap-2 h-[var(--control-height)] min-h-[var(--control-height)] border border-outline-variant/20 bg-surface-container px-[var(--control-padding-inline)] font-mono text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface transition-colors;
117
- }
118
-
119
- .standard-button:hover {
120
- @apply bg-surface-container-highest text-primary-container;
121
- border-color: rgba(252, 227, 0, 0.24);
122
- }
123
-
124
- .standard-button.is-active {
125
- @apply bg-surface-container-high text-primary-container;
126
- border-color: rgba(252, 227, 0, 0.3);
127
- }
128
-
129
- .standard-button:disabled,
130
- .standard-button[aria-disabled="true"] {
131
- @apply cursor-not-allowed opacity-40;
132
- }
133
-
134
- .standard-button .material-symbols-outlined {
135
- @apply text-base;
136
- }
137
-
138
- .standard-checkbox {
139
- @apply inline-flex w-full items-center gap-3 min-h-[var(--control-height)] border border-outline-variant/10 bg-surface-container-lowest px-[var(--control-padding-inline)] text-sm text-on-surface;
140
- }
141
-
142
- .standard-checkbox input[type="checkbox"] {
143
- @apply m-0 rounded-none border-outline bg-surface-container-lowest text-primary-container focus:ring-primary-container;
144
- accent-color: var(--color-primary-container);
145
- }
146
-
147
- .standard-checkbox.is-disabled,
148
- .standard-checkbox:has(input:disabled) {
149
- @apply opacity-45;
150
- }
151
-
152
- .delete-button {
153
- @apply inline-flex items-center justify-center gap-2 h-[var(--control-height)] min-h-[var(--control-height)] border border-error/25 bg-error-container/10 px-[var(--control-padding-inline)] font-mono text-[10px] font-bold uppercase tracking-[0.16em] text-error transition-all;
154
- }
155
-
156
- .delete-button:hover {
157
- @apply -translate-y-px border-error bg-error-container/20 text-error;
158
- }
159
-
160
- .delete-button:disabled,
161
- .delete-button[aria-disabled="true"] {
162
- @apply cursor-not-allowed opacity-45;
163
- transform: none;
164
- }
165
- }
166
- </style>
9
+ <link href="/vendor/material-symbols/outlined.css" rel="stylesheet" />
10
+ <link href="styles/tailwind.generated.css" rel="stylesheet" />
167
11
  <link href="styles/tokens.css" rel="stylesheet" />
168
12
  <link href="styles/base.css" rel="stylesheet" />
13
+ <link href="styles/utilities.css" rel="stylesheet" />
169
14
  <link href="styles/layout.css" rel="stylesheet" />
170
15
  <link href="styles/components.css" rel="stylesheet" />
171
16
  <link href="styles/views.css" rel="stylesheet" />
@@ -69,6 +69,7 @@ import {
69
69
  toggleDataTablesPanel,
70
70
  setCurrentQuery,
71
71
  setChartsHeightPreset,
72
+ setChartsHistoryTab,
72
73
  setEditorPanelVisibility,
73
74
  setEditorTab,
74
75
  submitDeleteChartConfirmation,
@@ -111,7 +112,7 @@ import {
111
112
  applyCurrentMediaTaggingSelection,
112
113
  removeCurrentTableDesignerColumn,
113
114
  } from './store.js';
114
- import { renderChartsView } from './views/charts.js';
115
+ import { renderChartsDetail, renderChartsView } from './views/charts.js';
115
116
  import { renderConnectionsView } from './views/connections.js';
116
117
  import { renderDataRowEditorPanel, renderDataView } from './views/data.js';
117
118
  import { renderEditorView } from './views/editor.js';
@@ -146,6 +147,7 @@ let lastRenderedMainMarkup = '';
146
147
  let lastRenderedPanelMarkup = '';
147
148
  let lastRenderedModalMarkup = '';
148
149
  let lastRenderedToastMarkup = '';
150
+ let lastRenderedChartsHistorySignature = '';
149
151
  let lastRenderedPanelOpen = false;
150
152
  let lastRenderedLockedRoute = false;
151
153
  let pendingNewTableDesignerAutofocus = false;
@@ -365,7 +367,11 @@ function syncQueryHistoryUi(historyId) {
365
367
 
366
368
  const historyItem = state.editor.history.find(entry => Number(entry.id) === numericId) ?? state.editor.historyDetail ?? null;
367
369
  const listItemNode = shellRefs.view.querySelector(
368
- `[data-action="select-query-history-item"][data-history-id="${String(numericId)}"]`,
370
+ [
371
+ '[data-action="select-query-history-item"][data-history-id="',
372
+ String(numericId),
373
+ '"]',
374
+ ].join(''),
369
375
  )?.closest('.query-history-item');
370
376
 
371
377
  if (historyItem && listItemNode instanceof HTMLElement) {
@@ -417,6 +423,188 @@ function syncQueryHistorySelectionUi(selectedHistoryId = null) {
417
423
  return true;
418
424
  }
419
425
 
426
+ function isEditorRouteName(routeName) {
427
+ return routeName === 'editor' || routeName === 'editorResults';
428
+ }
429
+
430
+ function captureQueryHistoryScrollState() {
431
+ if (!isEditorRouteName(lastRenderedRouteName)) {
432
+ return null;
433
+ }
434
+
435
+ const scrollNode = shellRefs.view.querySelector('[data-query-history-scroll]');
436
+
437
+ if (!(scrollNode instanceof HTMLElement)) {
438
+ return null;
439
+ }
440
+
441
+ return {
442
+ committedSearch: scrollNode.dataset.queryHistoryCommittedSearch ?? '',
443
+ historyTab: scrollNode.dataset.queryHistoryTab ?? '',
444
+ routeName: lastRenderedRouteName,
445
+ renderedItemCount: scrollNode.querySelectorAll('.query-history-item').length,
446
+ renderedLoadingMore: scrollNode.dataset.queryHistoryLoadingMore === 'true',
447
+ searchInput: scrollNode.dataset.queryHistorySearch ?? '',
448
+ scrollLeft: scrollNode.scrollLeft,
449
+ scrollTop: scrollNode.scrollTop,
450
+ };
451
+ }
452
+
453
+ function shouldRestoreQueryHistoryScroll(snapshot, state) {
454
+ if (!snapshot || !isEditorRouteName(state.route.name) || snapshot.routeName !== state.route.name) {
455
+ return false;
456
+ }
457
+
458
+ if (!state.editor.historyPanelVisible) {
459
+ return false;
460
+ }
461
+
462
+ if (
463
+ snapshot.historyTab !== state.editor.historyTab ||
464
+ snapshot.searchInput !== state.editor.historySearchInput ||
465
+ snapshot.committedSearch !== state.editor.historySearch
466
+ ) {
467
+ return false;
468
+ }
469
+
470
+ return (
471
+ state.editor.historyLoadingMore ||
472
+ snapshot.renderedLoadingMore ||
473
+ state.editor.history.length > snapshot.renderedItemCount
474
+ );
475
+ }
476
+
477
+ function restoreQueryHistoryScrollState(snapshot) {
478
+ const scrollNode = shellRefs.view.querySelector('[data-query-history-scroll]');
479
+
480
+ if (!(scrollNode instanceof HTMLElement)) {
481
+ return false;
482
+ }
483
+
484
+ scrollNode.scrollLeft = snapshot.scrollLeft;
485
+ scrollNode.scrollTop = snapshot.scrollTop;
486
+ return true;
487
+ }
488
+
489
+ function buildChartsHistorySignature(state) {
490
+ if (state.route.name !== 'charts') {
491
+ return '';
492
+ }
493
+
494
+ const historyVisible = state.editor.historyPanelVisible !== false || !state.charts.selectedHistoryId;
495
+
496
+ if (!historyVisible) {
497
+ return 'charts-history:hidden';
498
+ }
499
+
500
+ const queries = state.charts.queries ?? [];
501
+
502
+ if (!queries.length) {
503
+ if (state.charts.loading) {
504
+ return 'charts-history:loading-empty';
505
+ }
506
+
507
+ if (state.charts.error) {
508
+ return `charts-history:error:${state.charts.error.code}:${state.charts.error.message}`;
509
+ }
510
+
511
+ return 'charts-history:empty';
512
+ }
513
+
514
+ return JSON.stringify({
515
+ tab: state.charts.historyTab ?? 'recent',
516
+ queries: queries.map(item => [
517
+ item.id,
518
+ item.displayTitle,
519
+ item.previewSql,
520
+ Boolean(item.isSaved),
521
+ Array.isArray(item.chartTypes) ? item.chartTypes.join(',') : '',
522
+ ]),
523
+ });
524
+ }
525
+
526
+ function syncChartsHistorySelectionUi(state) {
527
+ const selectedHistoryId = String(state.charts.selectedHistoryId ?? '');
528
+ const historyButtons = shellRefs.view.querySelectorAll(
529
+ '.charts-view__sidebar [data-action="navigate"][data-history-id]',
530
+ );
531
+
532
+ for (const button of historyButtons) {
533
+ if (!(button instanceof HTMLElement)) {
534
+ continue;
535
+ }
536
+
537
+ const itemNode = button.closest('[data-charts-history-item]');
538
+ const isSelected = button.dataset.historyId === selectedHistoryId;
539
+ const titleNode = button.querySelector('[data-charts-history-title]');
540
+ const selectionNode = itemNode instanceof HTMLElement ? itemNode : button;
541
+
542
+ selectionNode.classList.toggle('border-primary-container/30', isSelected);
543
+ selectionNode.classList.toggle('bg-surface-container-high', isSelected);
544
+ selectionNode.classList.toggle('border-outline-variant/10', !isSelected);
545
+ selectionNode.classList.toggle('bg-surface-container-lowest', !isSelected);
546
+ selectionNode.classList.toggle('hover:bg-surface-container-high', !isSelected);
547
+
548
+ if (titleNode instanceof HTMLElement) {
549
+ titleNode.classList.toggle('text-primary-container', isSelected);
550
+ titleNode.classList.toggle('text-on-surface', !isSelected);
551
+ }
552
+ }
553
+
554
+ return true;
555
+ }
556
+
557
+ function syncChartsSavedToggleUi(actionNode, nextValue) {
558
+ const itemNode = actionNode.closest('[data-charts-history-item]');
559
+
560
+ actionNode.classList.toggle('is-active', nextValue);
561
+ actionNode.dataset.nextValue = nextValue ? 'false' : 'true';
562
+ actionNode.title = nextValue ? 'Remove from saved' : 'Save query';
563
+
564
+ const iconNode = actionNode.querySelector('.material-symbols-outlined');
565
+ if (iconNode instanceof HTMLElement) {
566
+ iconNode.textContent = nextValue ? 'bookmark' : 'bookmark_add';
567
+ }
568
+
569
+ if (itemNode instanceof HTMLElement) {
570
+ itemNode.querySelector('[data-charts-saved-badge]')?.toggleAttribute('hidden', !nextValue);
571
+
572
+ if (!nextValue && getState().charts.historyTab === 'saved') {
573
+ itemNode.remove();
574
+ }
575
+ }
576
+
577
+ const countNode = shellRefs.view.querySelector('[data-charts-history-count]');
578
+ if (countNode instanceof HTMLElement) {
579
+ const state = getState();
580
+ const count =
581
+ state.charts.historyTab === 'saved'
582
+ ? (state.charts.queries ?? []).filter(item => item.isSaved).length
583
+ : (state.charts.queries ?? []).length;
584
+ countNode.textContent = String(count);
585
+ }
586
+ }
587
+
588
+ function patchChartsDetailUi(state) {
589
+ const chartsView = shellRefs.view.querySelector('.charts-view');
590
+ const detailNode = chartsView?.querySelector('.charts-view__detail');
591
+
592
+ if (!(chartsView instanceof HTMLElement) || !(detailNode instanceof HTMLElement)) {
593
+ return false;
594
+ }
595
+
596
+ const historyVisible = state.editor.historyPanelVisible !== false || !state.charts.selectedHistoryId;
597
+ const sidebarNode = chartsView.querySelector('.charts-view__sidebar');
598
+
599
+ if (Boolean(sidebarNode) !== historyVisible) {
600
+ return false;
601
+ }
602
+
603
+ detailNode.innerHTML = renderChartsDetail(state);
604
+ syncChartsHistorySelectionUi(state);
605
+ return true;
606
+ }
607
+
420
608
  function readFileAsBase64(file) {
421
609
  return new Promise((resolve, reject) => {
422
610
  const reader = new FileReader();
@@ -755,6 +943,7 @@ function renderApp(state) {
755
943
  const statusBarMarkup = renderStatusBar(state);
756
944
  const modalMarkup = renderModal(state);
757
945
  const toastMarkup = renderToasts(state.toasts);
946
+ const chartsHistorySignature = buildChartsHistorySignature(state);
758
947
  const isLockedRoute = [
759
948
  'editor',
760
949
  'editorResults',
@@ -772,6 +961,7 @@ function renderApp(state) {
772
961
  const mainChanged = main !== lastRenderedMainMarkup;
773
962
  const panelChanged = panel !== lastRenderedPanelMarkup;
774
963
  const modalChanged = modalMarkup !== lastRenderedModalMarkup;
964
+ const chartsHistoryChanged = chartsHistorySignature !== lastRenderedChartsHistorySignature;
775
965
  const panelOpenChanged = panelOpen !== lastRenderedPanelOpen;
776
966
  const lockedRouteChanged = isLockedRoute !== lastRenderedLockedRoute;
777
967
  const shellMarkupUnchanged =
@@ -796,6 +986,7 @@ function renderApp(state) {
796
986
  }
797
987
 
798
988
  const focusedInput = captureFocusedInputState();
989
+ const queryHistoryScrollState = captureQueryHistoryScrollState();
799
990
  const isEnteringNewTableDesignerRoute =
800
991
  state.route.name === 'tableDesigner' && state.route.params?.isNew && previousRoutePath !== state.route.path;
801
992
 
@@ -805,9 +996,20 @@ function renderApp(state) {
805
996
  pendingNewTableDesignerAutofocus = false;
806
997
  }
807
998
 
808
- if (mainChanged) {
809
- teardownStructureGraph();
999
+ const canPatchChartsMain =
1000
+ mainChanged && previousRouteName === 'charts' && state.route.name === 'charts' && !chartsHistoryChanged;
1001
+ let mainPatched = false;
1002
+
1003
+ if (canPatchChartsMain) {
810
1004
  teardownQueryChartRenderer();
1005
+ mainPatched = patchChartsDetailUi(state);
1006
+ }
1007
+
1008
+ if (mainChanged) {
1009
+ if (!mainPatched) {
1010
+ teardownStructureGraph();
1011
+ teardownQueryChartRenderer();
1012
+ }
811
1013
  }
812
1014
 
813
1015
  if (topNavChanged) {
@@ -829,7 +1031,7 @@ function renderApp(state) {
829
1031
  shellRefs.statusBar.innerHTML = statusBarMarkup;
830
1032
  }
831
1033
 
832
- if (mainChanged) {
1034
+ if (mainChanged && !mainPatched) {
833
1035
  shellRefs.view.innerHTML = main;
834
1036
  }
835
1037
 
@@ -853,6 +1055,10 @@ function renderApp(state) {
853
1055
  shellRefs.shell.classList.toggle('panel-open', panelOpen);
854
1056
  }
855
1057
 
1058
+ if (shouldRestoreQueryHistoryScroll(queryHistoryScrollState, state)) {
1059
+ restoreQueryHistoryScrollState(queryHistoryScrollState);
1060
+ }
1061
+
856
1062
  if (pendingQueryEditorFocus && (state.route.name === 'editor' || state.route.name === 'editorResults')) {
857
1063
  if (focusQueryEditorInput()) {
858
1064
  pendingQueryEditorFocus = false;
@@ -883,6 +1089,7 @@ function renderApp(state) {
883
1089
  lastRenderedPanelMarkup = panel;
884
1090
  lastRenderedModalMarkup = modalMarkup;
885
1091
  lastRenderedToastMarkup = toastMarkup;
1092
+ lastRenderedChartsHistorySignature = chartsHistorySignature;
886
1093
  lastRenderedPanelOpen = panelOpen;
887
1094
  lastRenderedLockedRoute = isLockedRoute;
888
1095
 
@@ -1063,6 +1270,20 @@ async function handleAction(actionNode) {
1063
1270
  );
1064
1271
  }
1065
1272
  return;
1273
+ case 'toggle-charts-query-history-saved':
1274
+ if (actionNode.dataset.historyId) {
1275
+ const nextValue = actionNode.dataset.nextValue === 'true';
1276
+ const updated = await toggleQueryHistorySavedState(actionNode.dataset.historyId, nextValue, {
1277
+ notify: false,
1278
+ refresh: false,
1279
+ toast: false,
1280
+ });
1281
+
1282
+ if (updated) {
1283
+ syncChartsSavedToggleUi(actionNode, nextValue);
1284
+ }
1285
+ }
1286
+ return;
1066
1287
  case 'toggle-charts-sql-panel':
1067
1288
  toggleChartsSqlPanel();
1068
1289
  return;
@@ -1074,6 +1295,11 @@ async function handleAction(actionNode) {
1074
1295
  setChartsHeightPreset(actionNode.dataset.preset);
1075
1296
  }
1076
1297
  return;
1298
+ case 'set-charts-history-tab':
1299
+ if (actionNode.dataset.tab) {
1300
+ setChartsHistoryTab(actionNode.dataset.tab);
1301
+ }
1302
+ return;
1077
1303
  case 'export-query-chart-png':
1078
1304
  if (actionNode.dataset.chartId && !exportQueryChartAsPng(actionNode.dataset.chartId)) {
1079
1305
  showToast('The selected chart is not ready for PNG export.', 'alert');