sqlite-hub 1.2.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +113 -71
  2. package/bin/sqlite-hub.js +723 -116
  3. package/docs/API.md +152 -0
  4. package/docs/CLI.md +249 -0
  5. package/docs/CLI_API_PARITY.md +61 -0
  6. package/docs/changelog.md +149 -0
  7. package/docs/guidelines/AGENTS.md +32 -0
  8. package/docs/guidelines/DESIGN.md +55 -0
  9. package/docs/shortkeys.md +27 -0
  10. package/docs/todo.md +4 -0
  11. package/examples/api/generate-types.js +52 -0
  12. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  13. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  14. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  16. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  17. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  18. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  19. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  21. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  22. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  23. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  24. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  25. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  26. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  27. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  28. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
  29. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  30. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  31. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  32. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  33. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  34. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  35. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  36. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  37. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  38. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  39. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  40. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  41. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  42. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  43. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  44. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  45. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  46. package/frontend/js/api.js +63 -0
  47. package/frontend/js/app.js +310 -10
  48. package/frontend/js/components/connectionCard.js +4 -2
  49. package/frontend/js/components/emptyState.js +4 -4
  50. package/frontend/js/components/generateTypesDropdown.js +33 -0
  51. package/frontend/js/components/metricCard.js +1 -1
  52. package/frontend/js/components/modal.js +555 -21
  53. package/frontend/js/components/pageHeader.js +1 -1
  54. package/frontend/js/components/queryHistoryDetail.js +3 -3
  55. package/frontend/js/components/queryHistoryHeader.js +1 -1
  56. package/frontend/js/components/queryHistoryPanel.js +1 -1
  57. package/frontend/js/components/rowEditorPanel.js +13 -3
  58. package/frontend/js/components/sidebar.js +41 -7
  59. package/frontend/js/components/topNav.js +2 -14
  60. package/frontend/js/router.js +38 -6
  61. package/frontend/js/store.js +870 -8
  62. package/frontend/js/utils/inputClear.js +36 -0
  63. package/frontend/js/utils/syntheticData.js +240 -0
  64. package/frontend/js/views/backups.js +581 -37
  65. package/frontend/js/views/charts.js +9 -9
  66. package/frontend/js/views/connections.js +2 -2
  67. package/frontend/js/views/data.js +17 -1
  68. package/frontend/js/views/documents.js +3 -3
  69. package/frontend/js/views/editor.js +6 -6
  70. package/frontend/js/views/logs.js +339 -0
  71. package/frontend/js/views/mediaTagging.js +7 -5
  72. package/frontend/js/views/overview.js +3 -3
  73. package/frontend/js/views/settings.js +80 -30
  74. package/frontend/js/views/structure.js +12 -1
  75. package/frontend/js/views/tableAdvisor.js +385 -0
  76. package/frontend/js/views/tableDesigner.js +6 -0
  77. package/frontend/styles/base.css +1 -40
  78. package/frontend/styles/components.css +258 -232
  79. package/frontend/styles/structure-graph.css +32 -68
  80. package/frontend/styles/tailwind.css +8 -4
  81. package/frontend/styles/tailwind.generated.css +1 -1
  82. package/frontend/styles/tokens.css +28 -21
  83. package/frontend/styles/views.css +102 -261
  84. package/package.json +18 -2
  85. package/server/routes/backups.js +18 -2
  86. package/server/routes/data.js +45 -0
  87. package/server/routes/externalApi.js +321 -2
  88. package/server/routes/logs.js +127 -0
  89. package/server/routes/structure.js +22 -0
  90. package/server/server.js +3 -0
  91. package/server/services/databaseCommandService.js +59 -0
  92. package/server/services/sqlite/backupDiff.js +914 -0
  93. package/server/services/sqlite/backupService.js +77 -1
  94. package/server/services/sqlite/dataBrowserService.js +36 -0
  95. package/server/services/sqlite/introspection.js +226 -22
  96. package/server/services/sqlite/structureService.js +7 -0
  97. package/server/services/sqlite/syntheticDataGenerator.js +728 -0
  98. package/server/services/sqlite/tableAdvisor.js +790 -0
  99. package/server/services/storage/appStateStore.js +773 -169
  100. package/server/services/typeGenerationService.js +663 -0
  101. package/tailwind.config.cjs +0 -1
  102. package/.github/funding.yml +0 -2
  103. package/.github/workflows/ci.yml +0 -36
  104. package/database.sqlite +0 -0
  105. package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
  106. package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
  107. package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
  108. package/frontend/assets/mockups/connections_1200.webp +0 -0
  109. package/frontend/assets/mockups/data_1_1200.webp +0 -0
  110. package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
  111. package/frontend/assets/mockups/documents_1200.webp +0 -0
  112. package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
  113. package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
  114. package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
  115. package/frontend/assets/mockups/overview_1200.webp +0 -0
  116. package/frontend/assets/mockups/settings_1200.webp +0 -0
  117. package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
  118. package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
  119. package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
  120. package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
  121. package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
  122. package/frontend/assets/mockups/structure_1_1200.webp +0 -0
  123. package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
  124. package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
  125. package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
  126. package/tests/api-token-auth.test.js +0 -279
  127. package/tests/backup-manager.test.js +0 -140
  128. package/tests/backups-view.test.js +0 -70
  129. package/tests/charts-height-preset-storage.test.js +0 -60
  130. package/tests/charts-route-state.test.js +0 -144
  131. package/tests/check-constraint-options.test.js +0 -90
  132. package/tests/cli-args.test.js +0 -113
  133. package/tests/cli-service-delegation.test.js +0 -140
  134. package/tests/connection-removal.test.js +0 -52
  135. package/tests/connections-file-dialog-route.test.js +0 -89
  136. package/tests/copy-column-modal.test.js +0 -131
  137. package/tests/database-command-service.test.js +0 -174
  138. package/tests/database-documents.test.js +0 -85
  139. package/tests/documents-view.test.js +0 -132
  140. package/tests/dropdown-button.test.js +0 -101
  141. package/tests/email-preview.test.js +0 -89
  142. package/tests/export-blob.test.js +0 -99
  143. package/tests/export-filenames.test.js +0 -38
  144. package/tests/file-path-preview.test.js +0 -165
  145. package/tests/form-controls.test.js +0 -34
  146. package/tests/json-preview.test.js +0 -49
  147. package/tests/local-request-security.test.js +0 -85
  148. package/tests/markdown-documents.test.js +0 -103
  149. package/tests/native-file-dialog.test.js +0 -105
  150. package/tests/query-editor.test.js +0 -28
  151. package/tests/query-history-detail.test.js +0 -37
  152. package/tests/query-history-header.test.js +0 -30
  153. package/tests/query-results-truncation.test.js +0 -20
  154. package/tests/risky-sql.test.js +0 -30
  155. package/tests/row-editor-json.test.js +0 -82
  156. package/tests/row-editor-null-values.test.js +0 -184
  157. package/tests/row-editor-timestamp-preview.test.js +0 -192
  158. package/tests/security-paths.test.js +0 -84
  159. package/tests/settings-api-tokens-route.test.js +0 -97
  160. package/tests/settings-metadata.test.js +0 -114
  161. package/tests/settings-view.test.js +0 -108
  162. package/tests/sql-formatter.test.js +0 -173
  163. package/tests/sql-highlight.test.js +0 -38
  164. package/tests/sql-identifier-safety.test.js +0 -171
  165. package/tests/sql-result-limit.test.js +0 -66
  166. package/tests/structure-view.test.js +0 -60
  167. package/tests/table-designer-v2-unique-constraints.test.js +0 -78
  168. package/tests/table-scroll-state.test.js +0 -70
  169. package/tests/text-cell-stats.test.js +0 -38
@@ -48,6 +48,7 @@ import {
48
48
  initializeApp,
49
49
  insertMarkdownIntoLastOpenDocument,
50
50
  loadMoreQueryHistory,
51
+ loadMoreLogs,
51
52
  openModal,
52
53
  openOverviewInFinder,
53
54
  openQueryHistoryInEditor,
@@ -56,11 +57,15 @@ import {
56
57
  openDeleteQueryHistoryModal,
57
58
  openDeleteSettingsApiTokenModal,
58
59
  openCreateBackupModal,
59
- openEditBackupNotesModal,
60
+ openEditBackupModal,
61
+ openBackupDiffDrawer,
62
+ closeBackupDiffDrawer,
63
+ openGenerateTypesModal,
60
64
  openDeleteBackupModal,
61
65
  openRestoreBackupModal,
62
66
  openDeleteQueryChartModal,
63
67
  openDataExportModal,
68
+ openGenerateDataModal,
64
69
  openDeleteDocumentModal,
65
70
  openQueryExportModal,
66
71
  openDataRowByIdentity,
@@ -74,6 +79,7 @@ import {
74
79
  openDataRowUpdatePreview,
75
80
  openEditorRowUpdatePreview,
76
81
  refreshCurrentRoute,
82
+ refreshLogs,
77
83
  refreshBackups,
78
84
  refreshMediaTaggingPreview,
79
85
  removeConnection,
@@ -91,6 +97,9 @@ import {
91
97
  selectQueryHistoryItem,
92
98
  selectStructureEntry,
93
99
  setDocumentsSearchQuery,
100
+ setLogFilter,
101
+ setLogSearchInput,
102
+ applyLogSearch,
94
103
  setTableDesignerSearchQuery,
95
104
  setTableDesignerSqlPreviewVisibility,
96
105
  toggleTableDesignerTablesPanel,
@@ -122,8 +131,10 @@ import {
122
131
  submitDeleteSettingsApiTokenConfirmation,
123
132
  submitBackupSafetyChoice,
124
133
  submitCreateBackupConfirmation,
125
- submitEditBackupNotesConfirmation,
134
+ submitEditBackupConfirmation,
135
+ submitGenerateDataRows,
126
136
  submitRowUpdatePreviewConfirmation,
137
+ previewGenerateDataRows,
127
138
  setQueryHistoryPanelVisibility,
128
139
  sortDataTableByColumn,
129
140
  sortEditorResultsByColumn,
@@ -132,6 +143,7 @@ import {
132
143
  setCopyColumnModalError,
133
144
  setCopyColumnModalEditedText,
134
145
  setCopyColumnModalSubmitting,
146
+ setBackupDiffTab,
135
147
  setRoute,
136
148
  setSettingsSection,
137
149
  saveQueryHistoryNotes,
@@ -166,6 +178,9 @@ import {
166
178
  updateCurrentTableDesignerColumnField,
167
179
  updateCurrentTableDesignerConstraintField,
168
180
  updateCurrentTableDesignerField,
181
+ updateGenerateDataMapping,
182
+ updateGenerateDataModal,
183
+ updateGenerateTypesModal,
169
184
  addCurrentTableDesignerColumn,
170
185
  applyCurrentMediaTaggingSelection,
171
186
  removeCurrentTableDesignerColumn,
@@ -177,10 +192,12 @@ import { renderDataRowEditorPanel, renderDataView } from './views/data.js';
177
192
  import { renderDocumentsView } from './views/documents.js';
178
193
  import { renderEditorView } from './views/editor.js';
179
194
  import { renderLandingView } from './views/landing.js';
195
+ import { renderLogsView, renderLogTable } from './views/logs.js';
180
196
  import { renderMediaTaggingView } from './views/mediaTagging.js';
181
197
  import { renderOverviewView } from './views/overview.js';
182
198
  import { renderSettingsView } from './views/settings.js';
183
199
  import { renderStructureView } from './views/structure.js';
200
+ import { renderTableAdvisorView } from './views/tableAdvisor.js';
184
201
  import { renderTableDesignerView } from './views/tableDesigner.js';
185
202
  import { replaceChildrenFromRenderedMarkup, replaceElementFromRenderedMarkup } from './utils/dom.js';
186
203
  import {
@@ -195,6 +212,7 @@ import {
195
212
  detectFilePathValue,
196
213
  getPathTypeLabel,
197
214
  } from './utils/filePathPreview.js';
215
+ import { clearInputForEscape } from './utils/inputClear.js';
198
216
  import { formatSqlQuery } from './utils/sqlFormatter.js';
199
217
  import {
200
218
  buildDataRowEditorJsonObject,
@@ -258,6 +276,7 @@ const ROUTE_TITLE_SEGMENTS = {
258
276
  backups: 'Backups',
259
277
  overview: 'Overview',
260
278
  data: 'Data',
279
+ tableAdvisor: 'Table Advisor',
261
280
  structure: 'Structure',
262
281
  editor: 'SQL Editor',
263
282
  editorResults: 'SQL Editor',
@@ -267,6 +286,7 @@ const ROUTE_TITLE_SEGMENTS = {
267
286
  mediaTaggingSetup: 'Media Tagging',
268
287
  mediaTaggingQueue: 'Tagging Queue',
269
288
  settings: 'Settings',
289
+ logs: 'Logs',
270
290
  notFound: 'Not Found',
271
291
  };
272
292
 
@@ -968,6 +988,92 @@ function patchChartsDetailUi(state, { preserveCharts = false } = {}) {
968
988
  return true;
969
989
  }
970
990
 
991
+ function getLogsActiveDatabaseId(state) {
992
+ return String(state.connections?.active?.id ?? state.logs?.metadata?.activeDatabase?.id ?? '');
993
+ }
994
+
995
+ function syncLogsFilterControls(state) {
996
+ const filters = state.logs?.filters ?? {};
997
+
998
+ for (const button of shellRefs.view.querySelectorAll('[data-action="set-log-filter"][data-field][data-value]')) {
999
+ if (!(button instanceof HTMLElement)) {
1000
+ continue;
1001
+ }
1002
+
1003
+ const field = button.dataset.field;
1004
+ const value = button.dataset.value;
1005
+ const active = String(filters[field] ?? '') === String(value ?? '');
1006
+
1007
+ button.classList.toggle('is-active', active);
1008
+ button.setAttribute('aria-pressed', active ? 'true' : 'false');
1009
+ }
1010
+ }
1011
+
1012
+ function syncLogsMetaStrip(state) {
1013
+ const logs = state.logs ?? {};
1014
+ const values = {
1015
+ visible: formatNumber((logs.items ?? []).length),
1016
+ matched: formatNumber(logs.total ?? 0),
1017
+ scope: logs.metadata?.activeDatabase?.label ?? 'Active Database',
1018
+ };
1019
+
1020
+ for (const [key, value] of Object.entries(values)) {
1021
+ const node = shellRefs.view.querySelector(`[data-logs-meta="${key}"]`);
1022
+
1023
+ if (node instanceof HTMLElement) {
1024
+ node.textContent = value;
1025
+ node.title = value;
1026
+ }
1027
+ }
1028
+ }
1029
+
1030
+ function patchLogsTableUi(state) {
1031
+ const logsView = shellRefs.view.querySelector('[data-logs-view]');
1032
+
1033
+ if (!(logsView instanceof HTMLElement)) {
1034
+ return false;
1035
+ }
1036
+
1037
+ const activeDatabaseId = getLogsActiveDatabaseId(state);
1038
+
1039
+ if (String(logsView.dataset.logsActiveDatabaseId ?? '') !== activeDatabaseId) {
1040
+ return false;
1041
+ }
1042
+
1043
+ const currentTable = logsView.querySelector('[data-logs-table]');
1044
+
1045
+ if (!(currentTable instanceof HTMLElement)) {
1046
+ return false;
1047
+ }
1048
+
1049
+ const currentScrollNode = currentTable.querySelector('[data-logs-table-scroll]');
1050
+ const scrollState =
1051
+ currentScrollNode instanceof HTMLElement
1052
+ ? {
1053
+ left: currentScrollNode.scrollLeft,
1054
+ top: currentScrollNode.scrollTop,
1055
+ }
1056
+ : null;
1057
+ const patched = replaceElementFromRenderedMarkup(currentTable, renderLogTable(state.logs ?? {}));
1058
+
1059
+ if (!patched) {
1060
+ return false;
1061
+ }
1062
+
1063
+ if (scrollState) {
1064
+ const nextScrollNode = logsView.querySelector('[data-logs-table-scroll]');
1065
+
1066
+ if (nextScrollNode instanceof HTMLElement) {
1067
+ nextScrollNode.scrollLeft = scrollState.left;
1068
+ nextScrollNode.scrollTop = scrollState.top;
1069
+ }
1070
+ }
1071
+
1072
+ syncLogsFilterControls(state);
1073
+ syncLogsMetaStrip(state);
1074
+ return true;
1075
+ }
1076
+
971
1077
  function readFileAsBase64(file) {
972
1078
  return new Promise((resolve, reject) => {
973
1079
  const reader = new FileReader();
@@ -1065,11 +1171,11 @@ function renderNotFoundView() {
1065
1171
  <p class="font-mono text-[10px] uppercase tracking-[0.3em] text-primary-container/40">
1066
1172
  ROUTE_LOST // HASH_NOT_RECOGNIZED
1067
1173
  </p>
1068
- <h1 class="mt-4 font-headline text-6xl font-black uppercase tracking-tight text-primary-container">
1174
+ <h1 class="mt-4 font-body text-6xl font-black uppercase tracking-tight text-primary-container">
1069
1175
  404_SIGNAL
1070
1176
  </h1>
1071
1177
  <button
1072
- class="standard-button mt-8 px-6 font-headline text-sm"
1178
+ class="standard-button mt-8 px-6 font-body text-sm"
1073
1179
  data-action="navigate"
1074
1180
  data-to="/"
1075
1181
  type="button"
@@ -1107,12 +1213,16 @@ function resolveView(state) {
1107
1213
  return renderStructureView(state);
1108
1214
  case 'tableDesigner':
1109
1215
  return renderTableDesignerView(state);
1216
+ case 'tableAdvisor':
1217
+ return renderTableAdvisorView(state);
1110
1218
  case 'mediaTaggingSetup':
1111
1219
  return renderMediaTaggingView(state, { subView: 'setup' });
1112
1220
  case 'mediaTaggingQueue':
1113
1221
  return renderMediaTaggingView(state, { subView: 'queue' });
1114
1222
  case 'settings':
1115
1223
  return renderSettingsView(state);
1224
+ case 'logs':
1225
+ return renderLogsView(state);
1116
1226
  default:
1117
1227
  return renderNotFoundView();
1118
1228
  }
@@ -1398,9 +1508,11 @@ function renderApp(state) {
1398
1508
  'editorResults',
1399
1509
  'data',
1400
1510
  'charts',
1511
+ 'logs',
1401
1512
  'documents',
1402
1513
  'structure',
1403
1514
  'tableDesigner',
1515
+ 'tableAdvisor',
1404
1516
  'mediaTaggingSetup',
1405
1517
  'mediaTaggingQueue',
1406
1518
  ].includes(state.route.name);
@@ -1452,8 +1564,8 @@ function renderApp(state) {
1452
1564
  pendingNewTableDesignerAutofocus = false;
1453
1565
  }
1454
1566
 
1455
- const canPatchChartsMain =
1456
- mainChanged && previousRouteName === 'charts' && state.route.name === 'charts';
1567
+ const canPatchChartsMain = mainChanged && previousRouteName === 'charts' && state.route.name === 'charts';
1568
+ const canPatchLogsMain = mainChanged && previousRouteName === 'logs' && state.route.name === 'logs';
1457
1569
  let mainPatched = false;
1458
1570
  let preservedChartsDom = false;
1459
1571
 
@@ -1479,6 +1591,10 @@ function renderApp(state) {
1479
1591
  }
1480
1592
  }
1481
1593
 
1594
+ if (!mainPatched && canPatchLogsMain) {
1595
+ mainPatched = patchLogsTableUi(state);
1596
+ }
1597
+
1482
1598
  if (mainChanged) {
1483
1599
  if (!mainPatched) {
1484
1600
  teardownStructureGraph();
@@ -2003,6 +2119,68 @@ function downloadTextFile({ text, filename, mimeType }) {
2003
2119
  URL.revokeObjectURL(url);
2004
2120
  }
2005
2121
 
2122
+ async function copyGeneratedTypes() {
2123
+ const modal = getState().modal;
2124
+ const code = modal?.kind === 'generate-types' ? String(modal.result?.code ?? '') : '';
2125
+
2126
+ if (!code) {
2127
+ showToast('No generated code to copy.', 'alert');
2128
+ return;
2129
+ }
2130
+
2131
+ if (!navigator.clipboard?.writeText) {
2132
+ showToast('Clipboard API is not available.', 'alert');
2133
+ return;
2134
+ }
2135
+
2136
+ try {
2137
+ await navigator.clipboard.writeText(code);
2138
+ showToast(
2139
+ modal.result?.files?.length ? 'Type definitions copied to clipboard.' : 'Type definition copied to clipboard.',
2140
+ 'success',
2141
+ );
2142
+ } catch (error) {
2143
+ showToast(
2144
+ modal.result?.files?.length
2145
+ ? 'Type definitions could not be copied.'
2146
+ : 'Type definition could not be copied.',
2147
+ 'alert',
2148
+ );
2149
+ }
2150
+ }
2151
+
2152
+ function downloadGeneratedTypes() {
2153
+ const modal = getState().modal;
2154
+ const result = modal?.kind === 'generate-types' ? modal.result : null;
2155
+ const files = result?.files ?? [];
2156
+
2157
+ if (files.length) {
2158
+ files.forEach((file, index) => {
2159
+ window.setTimeout(() => {
2160
+ downloadTextFile({
2161
+ text: file.code,
2162
+ filename: file.fileName || `${file.tableName || 'types'}.txt`,
2163
+ mimeType: 'text/plain;charset=utf-8',
2164
+ });
2165
+ }, index * 120);
2166
+ });
2167
+ showToast(`${files.length} type definition downloads started.`, 'success');
2168
+ return;
2169
+ }
2170
+
2171
+ if (!result?.code) {
2172
+ showToast('No generated code to download.', 'alert');
2173
+ return;
2174
+ }
2175
+
2176
+ downloadTextFile({
2177
+ text: result.code,
2178
+ filename: result.fileName || 'types.txt',
2179
+ mimeType: 'text/plain;charset=utf-8',
2180
+ });
2181
+ showToast('Type definition download started.', 'success');
2182
+ }
2183
+
2006
2184
  function getSelectedDataBrowserRowForJson(state) {
2007
2185
  if (state.dataBrowser.selectedRow) {
2008
2186
  return state.dataBrowser.selectedRow;
@@ -2361,6 +2539,15 @@ async function handleAction(actionNode) {
2361
2539
  case 'refresh-view':
2362
2540
  await refreshCurrentRoute();
2363
2541
  return;
2542
+ case 'refresh-logs':
2543
+ await refreshLogs();
2544
+ return;
2545
+ case 'set-log-filter':
2546
+ await setLogFilter(actionNode.dataset.field, actionNode.dataset.value);
2547
+ return;
2548
+ case 'load-more-logs':
2549
+ await loadMoreLogs();
2550
+ return;
2364
2551
  case 'set-settings-section':
2365
2552
  setSettingsSection(actionNode.dataset.section);
2366
2553
  return;
@@ -2582,11 +2769,29 @@ async function handleAction(actionNode) {
2582
2769
  case 'open-table-in-sql-editor':
2583
2770
  await openTableInSqlEditor(actionNode.dataset.tableName);
2584
2771
  return;
2772
+ case 'open-generate-types-modal':
2773
+ await openGenerateTypesModal(actionNode.dataset.tableName, actionNode.dataset.typeTarget, actionNode.dataset.typeScope);
2774
+ return;
2775
+ case 'copy-generated-types':
2776
+ await copyGeneratedTypes();
2777
+ return;
2778
+ case 'download-generated-types':
2779
+ downloadGeneratedTypes();
2780
+ return;
2585
2781
  case 'open-restore-backup-modal':
2586
2782
  openRestoreBackupModal(actionNode.dataset.backupId);
2587
2783
  return;
2588
- case 'open-edit-backup-notes-modal':
2589
- openEditBackupNotesModal(actionNode.dataset.backupId);
2784
+ case 'open-compare-backup-drawer':
2785
+ await openBackupDiffDrawer(actionNode.dataset.backupId);
2786
+ return;
2787
+ case 'set-backup-diff-tab':
2788
+ setBackupDiffTab(actionNode.dataset.tab);
2789
+ return;
2790
+ case 'close-backup-diff-drawer':
2791
+ closeBackupDiffDrawer();
2792
+ return;
2793
+ case 'open-edit-backup-modal':
2794
+ openEditBackupModal(actionNode.dataset.backupId);
2590
2795
  return;
2591
2796
  case 'open-delete-backup-modal':
2592
2797
  openDeleteBackupModal(actionNode.dataset.backupId);
@@ -2793,6 +2998,12 @@ async function handleAction(actionNode) {
2793
2998
  case 'open-data-export-modal':
2794
2999
  openDataExportModal();
2795
3000
  return;
3001
+ case 'open-generate-data-modal':
3002
+ openGenerateDataModal();
3003
+ return;
3004
+ case 'preview-generate-data':
3005
+ await previewGenerateDataRows();
3006
+ return;
2796
3007
  case 'export-data-format': {
2797
3008
  const format = actionNode.dataset.exportFormat;
2798
3009
  const filename = getExportFilenameFromAction(actionNode);
@@ -2863,6 +3074,30 @@ async function handleAction(actionNode) {
2863
3074
  }
2864
3075
  return;
2865
3076
  }
3077
+ case 'copy-table-advisor-sql': {
3078
+ const issueId = actionNode.dataset.issueId;
3079
+ const advisor = getState().tableAdvisor;
3080
+ const issue = (advisor.result?.issues ?? []).find(item => item.id === issueId);
3081
+ const sql = String(issue?.sql ?? '');
3082
+
3083
+ if (!sql.trim()) {
3084
+ showToast('No advisor SQL to copy.', 'alert');
3085
+ return;
3086
+ }
3087
+
3088
+ if (!navigator.clipboard?.writeText) {
3089
+ showToast('Clipboard API is not available.', 'alert');
3090
+ return;
3091
+ }
3092
+
3093
+ try {
3094
+ await navigator.clipboard.writeText(sql);
3095
+ showToast('Advisor SQL copied.', 'success');
3096
+ } catch (error) {
3097
+ showToast('Advisor SQL could not be copied.', 'alert');
3098
+ }
3099
+ return;
3100
+ }
2866
3101
  case 'toggle-table-designer-sql-preview':
2867
3102
  setTableDesignerSqlPreviewVisibility(
2868
3103
  actionNode.dataset.nextValue ? actionNode.dataset.nextValue === 'true' : undefined,
@@ -3151,6 +3386,11 @@ document.addEventListener('keydown', event => {
3151
3386
  return;
3152
3387
  }
3153
3388
 
3389
+ if (target instanceof HTMLInputElement && clearInputForEscape(target)) {
3390
+ event.preventDefault();
3391
+ return;
3392
+ }
3393
+
3154
3394
  if (state.modal) {
3155
3395
  event.preventDefault();
3156
3396
  closeModal();
@@ -3181,6 +3421,12 @@ document.addEventListener('keydown', event => {
3181
3421
  return;
3182
3422
  }
3183
3423
 
3424
+ if (state.route.name === 'backups' && state.backups.diff?.visible) {
3425
+ event.preventDefault();
3426
+ closeBackupDiffDrawer();
3427
+ return;
3428
+ }
3429
+
3184
3430
  if (state.editor.historySelectedId !== null || state.editor.historyDetail) {
3185
3431
  event.preventDefault();
3186
3432
  clearQueryHistorySelection();
@@ -3227,6 +3473,29 @@ document.addEventListener('input', event => {
3227
3473
  return;
3228
3474
  }
3229
3475
 
3476
+ if (bindNode.dataset.bind === 'type-generation-field') {
3477
+ if (bindNode instanceof HTMLInputElement && bindNode.type === 'checkbox') {
3478
+ return;
3479
+ }
3480
+
3481
+ void updateGenerateTypesModal(bindNode.dataset.typeGenerationField, bindNode.value);
3482
+ return;
3483
+ }
3484
+
3485
+ if (bindNode.dataset.bind === 'generate-data-field') {
3486
+ updateGenerateDataModal(bindNode.dataset.field, bindNode.value, { notify: false });
3487
+ return;
3488
+ }
3489
+
3490
+ if (bindNode.dataset.bind === 'generate-data-mapping') {
3491
+ if (bindNode instanceof HTMLSelectElement) {
3492
+ return;
3493
+ }
3494
+
3495
+ updateGenerateDataMapping(bindNode.dataset.columnName, bindNode.dataset.field, bindNode.value, { notify: false });
3496
+ return;
3497
+ }
3498
+
3230
3499
  if (bindNode.dataset.bind === 'copy-column-format-field') {
3231
3500
  updateCopyColumnModalFormatField(
3232
3501
  bindNode.dataset.field,
@@ -3254,6 +3523,11 @@ document.addEventListener('input', event => {
3254
3523
  return;
3255
3524
  }
3256
3525
 
3526
+ if (bindNode.dataset.bind === 'logs-search') {
3527
+ setLogSearchInput(bindNode.value);
3528
+ return;
3529
+ }
3530
+
3257
3531
  if (bindNode.dataset.bind === 'data-search-query') {
3258
3532
  void setDataSearchQuery(bindNode.value);
3259
3533
  return;
@@ -3398,6 +3672,23 @@ document.addEventListener('change', event => {
3398
3672
  return;
3399
3673
  }
3400
3674
 
3675
+ if (bindNode.dataset.bind === 'type-generation-field') {
3676
+ const nextValue =
3677
+ bindNode instanceof HTMLInputElement && bindNode.type === 'checkbox' ? bindNode.checked : bindNode.value;
3678
+ void updateGenerateTypesModal(bindNode.dataset.typeGenerationField, nextValue);
3679
+ return;
3680
+ }
3681
+
3682
+ if (bindNode.dataset.bind === 'generate-data-field') {
3683
+ updateGenerateDataModal(bindNode.dataset.field, bindNode.value);
3684
+ return;
3685
+ }
3686
+
3687
+ if (bindNode.dataset.bind === 'generate-data-mapping') {
3688
+ updateGenerateDataMapping(bindNode.dataset.columnName, bindNode.dataset.field, bindNode.value);
3689
+ return;
3690
+ }
3691
+
3401
3692
  if (bindNode.dataset.bind === 'data-search-column') {
3402
3693
  void setDataSearchColumn(bindNode.value);
3403
3694
  return;
@@ -3542,6 +3833,9 @@ document.addEventListener('submit', async event => {
3542
3833
  const formData = submitter ? new FormData(form, submitter) : new FormData(form);
3543
3834
 
3544
3835
  switch (form.dataset.form) {
3836
+ case 'logs-search':
3837
+ await applyLogSearch(String(formData.get('search') ?? ''));
3838
+ return;
3545
3839
  case 'create-api-token':
3546
3840
  await createSettingsApiToken(String(formData.get('name') ?? ''));
3547
3841
  return;
@@ -3623,8 +3917,11 @@ document.addEventListener('submit', async event => {
3623
3917
  type: String(formData.get('type') ?? 'manual'),
3624
3918
  });
3625
3919
  return;
3626
- case 'edit-backup-notes':
3627
- await submitEditBackupNotesConfirmation(String(formData.get('notes') ?? ''));
3920
+ case 'edit-backup':
3921
+ await submitEditBackupConfirmation({
3922
+ name: String(formData.get('name') ?? ''),
3923
+ notes: String(formData.get('notes') ?? ''),
3924
+ });
3628
3925
  return;
3629
3926
  case 'delete-backup-confirm':
3630
3927
  await submitDeleteBackupConfirmation();
@@ -3670,6 +3967,9 @@ document.addEventListener('submit', async event => {
3670
3967
  case 'apply-row-update-preview':
3671
3968
  await submitRowUpdatePreviewConfirmation();
3672
3969
  return;
3970
+ case 'generate-data':
3971
+ await submitGenerateDataRows();
3972
+ return;
3673
3973
  case 'create-media-tagging-tag-table':
3674
3974
  await submitCreateMediaTaggingTagTable();
3675
3975
  return;
@@ -36,7 +36,7 @@ export function renderConnectionCard(connection, activeConnectionId) {
36
36
  renderStatusBadge(isActive ? "ACTIVE" : "RECENT", isActive ? "primary" : "muted"),
37
37
  connection.readOnly ? renderStatusBadge("READ_ONLY", "alert") : "",
38
38
  "</div></div>",
39
- '<h3 class="mb-1 font-headline text-xl font-bold uppercase ',
39
+ '<h3 class="mb-1 font-body text-xl font-bold uppercase ',
40
40
  isActive ? "text-[#FCE300]" : "text-on-surface",
41
41
  '">',
42
42
  escapeHtml(connection.label),
@@ -56,7 +56,9 @@ export function renderConnectionCard(connection, activeConnectionId) {
56
56
  '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Last Opened</div><div class="text-xs font-bold text-on-surface">',
57
57
  escapeHtml(formatDateTime(connection.lastOpenedAt)),
58
58
  "</div></div>",
59
- '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Mode</div><div class="text-xs font-bold text-on-surface">',
59
+ '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Mode</div><div class="text-xs font-bold ',
60
+ connection.readOnly ? "text-primary-container" : "text-on-surface",
61
+ '">',
60
62
  connection.readOnly ? "Read only" : "Read / Write",
61
63
  "</div></div></div></div>",
62
64
  '<div class="border-t border-outline-variant/10 bg-surface-container-low px-4 py-3">',
@@ -61,7 +61,7 @@ function renderActiveConnection(activeConnection) {
61
61
  <p class="text-[10px] font-mono uppercase tracking-[0.24em] text-primary-container/70">
62
62
  ACTIVE_DATABASE
63
63
  </p>
64
- <h2 class="mt-2 font-headline text-2xl font-black uppercase tracking-tight text-primary-container">
64
+ <h2 class="mt-2 font-body text-2xl font-black uppercase tracking-tight text-primary-container">
65
65
  ${escapeHtml(activeConnection.label)}
66
66
  </h2>
67
67
  <p class="mt-2 font-mono text-[10px] text-on-surface-variant/55">${escapeHtml(
@@ -93,7 +93,7 @@ export function renderEmptyState({ activeConnection, recentConnections = [] }) {
93
93
  SYSTEM_READY // ${hasActive ? 'ACTIVE_CONTEXT' : 'IDLE_STATE'}
94
94
  </span>
95
95
  </div>
96
- <h1 class="mb-4 font-headline text-7xl font-black tracking-tighter text-primary-container opacity-90 md:text-9xl">
96
+ <h1 class="mb-4 font-body text-7xl font-black tracking-tighter text-primary-container opacity-90 md:text-9xl">
97
97
  SQLite Hub
98
98
  </h1>
99
99
  <p class="font-mono text-primary-container/40">The only fucking SQLite Manager you'll ever need.</p>
@@ -104,7 +104,7 @@ export function renderEmptyState({ activeConnection, recentConnections = [] }) {
104
104
  </div>
105
105
  <div class="mx-auto grid w-full max-w-3xl grid-cols-1 gap-4 px-6 md:grid-cols-2">
106
106
  <button
107
- class="landing-primary-action clipped-btn group flex items-center justify-between bg-primary-container px-8 py-6 font-headline text-lg font-bold transition-all duration-300 hover:shadow-[0_0_20px_rgba(252,227,0,0.3)]"
107
+ class="landing-primary-action clipped-btn group flex items-center justify-between bg-primary-container px-8 py-6 font-body text-lg font-bold transition-all duration-300 hover:shadow-[0_0_20px_rgba(252,227,0,0.3)]"
108
108
  data-action="open-modal"
109
109
  data-modal="open-connection"
110
110
  style="--clip-path: polygon(0 0, 90% 0, 100% 25%, 100% 100%, 0 100%);"
@@ -114,7 +114,7 @@ export function renderEmptyState({ activeConnection, recentConnections = [] }) {
114
114
  <span class="material-symbols-outlined transition-transform group-hover:translate-x-1">add_circle</span>
115
115
  </button>
116
116
  <button
117
- class="flex items-center justify-between border-l-2 border-primary-container bg-surface-container-highest px-8 py-6 font-headline text-lg font-bold text-primary-container transition-colors duration-150 hover:bg-surface-bright"
117
+ class="flex items-center justify-between border-l-2 border-primary-container bg-surface-container-highest px-8 py-6 font-body text-lg font-bold text-primary-container transition-colors duration-150 hover:bg-surface-bright"
118
118
  data-action="open-modal"
119
119
  data-modal="create-connection"
120
120
  type="button"
@@ -0,0 +1,33 @@
1
+ import { renderDropdownButton } from './dropdownButton.js';
2
+
3
+ export function renderGenerateTypesDropdown({ selectedTableName = '', hasTables = false } = {}) {
4
+ const hasSelectedTable = Boolean(String(selectedTableName ?? '').trim());
5
+
6
+ return renderDropdownButton({
7
+ align: 'left',
8
+ icon: 'code',
9
+ label: 'Generate Types',
10
+ title: 'Generate types',
11
+ items: [
12
+ {
13
+ action: 'open-generate-types-modal',
14
+ disabled: !hasSelectedTable,
15
+ icon: 'select_check_box',
16
+ label: 'Selected table',
17
+ dataAttributes: {
18
+ tableName: selectedTableName,
19
+ typeScope: 'selected',
20
+ },
21
+ },
22
+ {
23
+ action: 'open-generate-types-modal',
24
+ disabled: !hasTables,
25
+ icon: 'library_books',
26
+ label: 'All tables',
27
+ dataAttributes: {
28
+ typeScope: 'all',
29
+ },
30
+ },
31
+ ],
32
+ });
33
+ }
@@ -10,7 +10,7 @@ export function renderMetricCard({
10
10
  return `
11
11
  <div class="metric-card ${accent ? "metric-card--accent" : ""}">
12
12
  <span class="text-[10px] font-mono text-on-surface/40 uppercase">${escapeHtml(label)}</span>
13
- <span class="text-3xl font-headline font-bold text-on-surface">${escapeHtml(value)}</span>
13
+ <span class="text-3xl font-body font-bold text-on-surface">${escapeHtml(value)}</span>
14
14
  ${
15
15
  progress
16
16
  ? `