sqlite-hub 1.1.0 → 1.1.2

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 (58) hide show
  1. package/README.md +17 -196
  2. package/bin/sqlite-hub.js +7 -2
  3. package/frontend/js/api.js +60 -0
  4. package/frontend/js/app.js +140 -10
  5. package/frontend/js/components/dropdownButton.js +92 -0
  6. package/frontend/js/components/modal.js +991 -876
  7. package/frontend/js/components/queryEditor.js +29 -18
  8. package/frontend/js/components/queryHistoryDetail.js +20 -1
  9. package/frontend/js/components/queryHistoryHeader.js +3 -2
  10. package/frontend/js/components/rowEditorPanel.js +1 -0
  11. package/frontend/js/components/sidebar.js +1 -0
  12. package/frontend/js/components/structureGraph.js +1 -0
  13. package/frontend/js/components/tableDesignerEditor.js +23 -42
  14. package/frontend/js/components/tableDesignerSidebar.js +7 -31
  15. package/frontend/js/components/tableDesignerSqlPreview.js +2 -1
  16. package/frontend/js/router.js +2 -0
  17. package/frontend/js/store.js +407 -38
  18. package/frontend/js/utils/riskySql.js +165 -0
  19. package/frontend/js/views/backups.js +176 -0
  20. package/frontend/js/views/charts.js +12 -11
  21. package/frontend/js/views/data.js +37 -35
  22. package/frontend/js/views/documents.js +231 -162
  23. package/frontend/js/views/mediaTagging.js +6 -5
  24. package/frontend/js/views/structure.js +47 -43
  25. package/frontend/js/views/tableDesigner.js +45 -1
  26. package/frontend/styles/components.css +237 -59
  27. package/frontend/styles/structure-graph.css +10 -2
  28. package/frontend/styles/tailwind.generated.css +1 -1
  29. package/frontend/styles/tokens.css +2 -0
  30. package/frontend/styles/views.css +84 -41
  31. package/package.json +2 -1
  32. package/server/routes/backups.js +120 -0
  33. package/server/routes/connections.js +26 -3
  34. package/server/routes/externalApi.js +6 -2
  35. package/server/server.js +3 -1
  36. package/server/services/databaseCommandService.js +4 -3
  37. package/server/services/sqlite/backupService.js +497 -66
  38. package/server/services/sqlite/connectionManager.js +2 -2
  39. package/server/services/sqlite/dataBrowserService.js +11 -3
  40. package/server/services/sqlite/importService.js +25 -0
  41. package/server/services/sqlite/sqlExecutor.js +2 -0
  42. package/server/services/storage/appStateStore.js +379 -88
  43. package/tests/api-token-auth.test.js +45 -2
  44. package/tests/backup-manager.test.js +140 -0
  45. package/tests/backups-view.test.js +64 -0
  46. package/tests/charts-height-preset-storage.test.js +60 -0
  47. package/tests/charts-route-state.test.js +144 -0
  48. package/tests/cli-service-delegation.test.js +97 -0
  49. package/tests/connection-removal.test.js +52 -0
  50. package/tests/database-command-service.test.js +38 -3
  51. package/tests/documents-view.test.js +132 -0
  52. package/tests/dropdown-button.test.js +75 -0
  53. package/tests/query-editor.test.js +28 -0
  54. package/tests/query-history-detail.test.js +37 -0
  55. package/tests/query-history-header.test.js +30 -0
  56. package/tests/risky-sql.test.js +30 -0
  57. package/tests/row-editor-null-values.test.js +24 -0
  58. package/tests/structure-view.test.js +56 -0
@@ -1,3 +1,4 @@
1
+ import { renderDropdownButton } from "./dropdownButton.js";
1
2
  import { escapeHtml, highlightSql } from "../utils/format.js";
2
3
 
3
4
  function renderLineNumbers(query) {
@@ -45,7 +46,8 @@ export function renderQueryEditor({
45
46
  const secondaryButtonClass = "standard-button";
46
47
  const left = `
47
48
  <button
48
- class="${secondaryButtonClass}"
49
+ class="${secondaryButtonClass} panel-toggle-button ${editorVisible ? "" : "is-active"}"
50
+ aria-pressed="${editorVisible ? "false" : "true"}"
49
51
  data-action="toggle-editor-panel"
50
52
  data-next-value="${editorVisible ? "false" : "true"}"
51
53
  type="button"
@@ -56,21 +58,29 @@ export function renderQueryEditor({
56
58
  `;
57
59
 
58
60
  const center = `
59
- <button
60
- class="${secondaryButtonClass}"
61
- data-action="format-current-query"
62
- type="button"
63
- >
64
- <span class="material-symbols-outlined text-sm">format_align_left</span>
65
- Format
66
- </button>
67
- <button
68
- class="${secondaryButtonClass}"
69
- data-action="clear-query"
70
- type="button"
71
- >
72
- Clear
73
- </button>
61
+ ${renderDropdownButton({
62
+ icon: "edit_note",
63
+ label: "Editor",
64
+ title: "Editor actions",
65
+ items: [
66
+ {
67
+ action: "format-current-query",
68
+ icon: "format_align_left",
69
+ label: "Format",
70
+ },
71
+ {
72
+ action: "clear-query",
73
+ icon: "delete_sweep",
74
+ label: "Clear",
75
+ danger: true,
76
+ },
77
+ {
78
+ action: "copy-current-query",
79
+ icon: "content_copy",
80
+ label: "Copy to Clipboard",
81
+ },
82
+ ],
83
+ })}
74
84
  <button
75
85
  class="${secondaryButtonClass}"
76
86
  data-action="open-query-export-modal"
@@ -90,7 +100,8 @@ export function renderQueryEditor({
90
100
 
91
101
  const right = `
92
102
  <button
93
- class="${secondaryButtonClass}"
103
+ class="${secondaryButtonClass} panel-toggle-button ${historyVisible ? "" : "is-active"}"
104
+ aria-pressed="${historyVisible ? "false" : "true"}"
94
105
  data-action="toggle-query-history-panel"
95
106
  data-next-value="${historyVisible ? "false" : "true"}"
96
107
  type="button"
@@ -102,7 +113,7 @@ export function renderQueryEditor({
102
113
 
103
114
  return `
104
115
  <div class="flex h-full min-h-0 flex-col">
105
- <div class="bg-surface-container-low px-6 py-3">
116
+ <div class="workspace-header workspace-header--low">
106
117
  <div class="query-editor-toolbar">
107
118
  <div class="query-editor-toolbar__left">${left}</div>
108
119
  <div class="query-editor-toolbar__center">${center}</div>
@@ -35,11 +35,30 @@ function getQueryTypeTone(queryType) {
35
35
  return "muted";
36
36
  }
37
37
 
38
+ function getExecutionSourceTone(source) {
39
+ if (source === "api") {
40
+ return "primary";
41
+ }
42
+
43
+ if (source === "cli") {
44
+ return "warning";
45
+ }
46
+
47
+ if (source === "mcp") {
48
+ return "muted";
49
+ }
50
+
51
+ return "success";
52
+ }
53
+
38
54
  function renderRunItem(run) {
55
+ const executedBy = String(run.executedBy ?? "user").trim().toLowerCase() || "user";
56
+
39
57
  return `
40
58
  <div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
41
59
  <div class="flex flex-wrap items-center gap-2">
42
60
  ${renderStatusBadge(run.status, run.status === "error" ? "alert" : "success")}
61
+ ${renderStatusBadge(executedBy, getExecutionSourceTone(executedBy))}
43
62
  <span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
44
63
  ${escapeHtml(formatCompactDateTime(run.executedAt))}
45
64
  </span>
@@ -199,7 +218,7 @@ export function renderQueryHistoryDetail({
199
218
  "</button>",
200
219
  '<button class="delete-button" data-action="open-delete-query-history-modal" data-history-id="',
201
220
  escapeHtml(item.id),
202
- '" type="button">Delete</button></div>',
221
+ '" type="button"><span class="material-symbols-outlined text-sm">delete</span>Delete</button></div>',
203
222
  '<div class="mt-6"><div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">SQL</div>',
204
223
  '<pre class="query-history-detail-sql custom-scrollbar mt-2 overflow-auto border border-outline-variant/10 bg-surface-container-lowest p-4 font-mono text-sm leading-6 text-on-surface"><code>',
205
224
  highlightSql(item.rawSql),
@@ -34,10 +34,11 @@ export function renderQueryHistorySearch({
34
34
  placeholder = 'Search SQL, titles, notes...',
35
35
  } = {}) {
36
36
  return `
37
- <label class="mt-4 block">
37
+ <label class="query-history-search">
38
38
  <span class="sr-only">Search query history</span>
39
+ <span class="material-symbols-outlined query-history-search__icon" aria-hidden="true">search</span>
39
40
  <input
40
- class="control-input w-full border border-outline-variant/20 bg-surface-container text-sm text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container"
41
+ class="control-input query-history-search__input w-full border border-outline-variant/20 bg-surface-container text-sm text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container"
41
42
  data-bind="${escapeHtml(bind)}"
42
43
  placeholder="${escapeHtml(placeholder)}"
43
44
  type="search"
@@ -521,6 +521,7 @@ export function renderRowEditorPanel({
521
521
  ' type="button" ',
522
522
  saving || deleting ? "disabled" : "",
523
523
  ">",
524
+ '<span class="material-symbols-outlined text-sm">delete</span>',
524
525
  escapeHtml(deleting ? "Deleting..." : deleteLabel),
525
526
  "</button>",
526
527
  ].join("")
@@ -19,6 +19,7 @@ const sidebarItems = [
19
19
  { label: 'TAGGING_QUEUE', href: '#/media-tagging/queue', key: 'mediaTaggingQueue' },
20
20
  ],
21
21
  },
22
+ { label: 'Backups', href: '#/backups', key: 'backups', icon: 'inventory_2' },
22
23
  { label: 'Settings', href: '#/settings', key: 'settings', icon: 'settings' },
23
24
  ];
24
25
 
@@ -966,6 +966,7 @@ function updateInspectorToggleButton() {
966
966
  }
967
967
 
968
968
  currentGraph.inspectorToggleButton.classList.toggle('is-active', currentGraph.inspectorHidden);
969
+ currentGraph.inspectorToggleButton.setAttribute('aria-pressed', currentGraph.inspectorHidden ? 'true' : 'false');
969
970
  const icon = document.createElement('span');
970
971
  const label = currentGraph.inspectorHidden ? 'Show Inspector' : 'Hide Inspector';
971
972
 
@@ -323,7 +323,6 @@ export function renderTableDesignerEditor(state) {
323
323
  }
324
324
 
325
325
  const catalogTables = state.tableDesigner.tables ?? [];
326
- const visibleColumns = draft.columns.filter((column) => !column.deleted);
327
326
  const saveLabel =
328
327
  draft.mode === "create"
329
328
  ? state.tableDesigner.saving
@@ -335,11 +334,12 @@ export function renderTableDesignerEditor(state) {
335
334
 
336
335
  return `
337
336
  <section class="table-designer-main shell-section">
338
- <header class="table-designer-main__header">
339
- <div>
340
- <div class="table-designer-main__eyebrow">
341
- ${draft.mode === "create" ? "New Table Draft" : "Editing Existing Table"}
342
- </div>
337
+ <div data-table-designer-feedback>
338
+ ${renderTableDesignerFeedback(draft, state.tableDesigner.saveError)}
339
+ </div>
340
+
341
+ <section class="table-designer-main__section">
342
+ <div class="table-designer-main__section-header">
343
343
  <div class="table-designer-main__title-row">
344
344
  <input
345
345
  class="table-designer-main__name"
@@ -356,42 +356,6 @@ export function renderTableDesignerEditor(state) {
356
356
  : ""
357
357
  }
358
358
  </div>
359
- <div class="table-designer-main__subtitle">
360
- ${escapeHtml(String(visibleColumns.length))} visible column${
361
- visibleColumns.length === 1 ? "" : "s"
362
- } // Table Designer v2 // SQLite-safe operations only
363
- </div>
364
- </div>
365
- <div class="table-designer-main__actions">
366
- ${renderFillToggle(draft)}
367
- <button
368
- class="standard-button"
369
- data-action="refresh-view"
370
- type="button"
371
- >
372
- Reload Schema
373
- </button>
374
- <button
375
- class="standard-button"
376
- data-action="save-table-designer"
377
- data-table-designer-save-button
378
- ${draft.canSave ? "" : "disabled"}
379
- type="button"
380
- >
381
- ${escapeHtml(saveLabel)}
382
- </button>
383
- </div>
384
- </header>
385
-
386
- <div data-table-designer-feedback>
387
- ${renderTableDesignerFeedback(draft, state.tableDesigner.saveError)}
388
- </div>
389
-
390
- <section class="table-designer-main__section">
391
- <div class="table-designer-main__section-header">
392
- <div>
393
- <div class="table-designer-main__section-title">Columns</div>
394
- </div>
395
359
  <div class="table-designer-main__section-actions">
396
360
  <button
397
361
  class="standard-button"
@@ -400,6 +364,23 @@ export function renderTableDesignerEditor(state) {
400
364
  >
401
365
  + Add Column
402
366
  </button>
367
+ <button
368
+ class="standard-button"
369
+ data-action="refresh-view"
370
+ type="button"
371
+ >
372
+ Reload Schema
373
+ </button>
374
+ <button
375
+ class="standard-button"
376
+ data-action="save-table-designer"
377
+ data-table-designer-save-button
378
+ ${draft.canSave ? "" : "disabled"}
379
+ type="button"
380
+ >
381
+ ${escapeHtml(saveLabel)}
382
+ </button>
383
+ ${renderFillToggle(draft)}
403
384
  </div>
404
385
  </div>
405
386
  ${renderColumnGrid(draft, catalogTables)}
@@ -18,38 +18,14 @@ export function renderTableDesignerSidebar(state) {
18
18
  const isNewDraft = state.tableDesigner.draft?.mode === 'create';
19
19
 
20
20
  return `
21
- <aside class="table-designer-sidebar">
22
- <div class="table-designer-sidebar__header">
21
+ <aside class="table-designer-sidebar subnavi-panel">
22
+ <div class="table-designer-sidebar__header subnavi-header">
23
23
  <div>
24
- <div class="table-designer-sidebar__eyebrow">Table Designer</div>
25
- <div class="table-designer-sidebar__meta">
24
+ <div class="subnavi-header-title">Table Designer</div>
25
+ <div class="subnavi-header-details">
26
26
  ${escapeHtml(formatNumber(tables.length))} table${tables.length === 1 ? '' : 's'}
27
27
  </div>
28
28
  </div>
29
- <div class="table-designer-sidebar__header-actions">
30
- <button
31
- class="standard-button"
32
- data-action="import-table-designer-csv"
33
- type="button"
34
- >
35
- Import CSV
36
- </button>
37
- <input
38
- accept=".csv,text/csv"
39
- class="table-designer-sidebar__file-input"
40
- data-bind="table-designer-import-file"
41
- type="file"
42
- />
43
- <button
44
- class="signature-button"
45
- data-action="navigate"
46
- data-to="/table-designer/new"
47
- type="button"
48
- >
49
- + New Table
50
- </button>
51
-
52
- </div>
53
29
  </div>
54
30
 
55
31
  <label class="table-designer-sidebar__search">
@@ -64,7 +40,7 @@ export function renderTableDesignerSidebar(state) {
64
40
  />
65
41
  </label>
66
42
 
67
- <div class="table-designer-sidebar__list custom-scrollbar">
43
+ <div class="table-designer-sidebar__list subnavi-list custom-scrollbar">
68
44
  ${
69
45
  state.tableDesigner.loading && !tables.length
70
46
  ? `
@@ -76,7 +52,7 @@ export function renderTableDesignerSidebar(state) {
76
52
  : isNewDraft
77
53
  ? `
78
54
  <button
79
- class="table-designer-sidebar__item is-active w-full border border-primary-container/30 bg-surface-container-high px-4 py-3 text-left transition-colors"
55
+ class="table-designer-sidebar__item subnavi-item is-active border border-primary-container/30 bg-surface-container-high px-4 py-3 text-left transition-colors"
80
56
  data-action="navigate"
81
57
  data-to="/table-designer/new"
82
58
  type="button"
@@ -102,7 +78,7 @@ export function renderTableDesignerSidebar(state) {
102
78
  .map(
103
79
  table => `
104
80
  <button
105
- class="table-designer-sidebar__item w-full border px-4 py-3 text-left transition-colors ${
81
+ class="table-designer-sidebar__item subnavi-item border px-4 py-3 text-left transition-colors ${
106
82
  !isNewDraft && table.name === state.tableDesigner.selectedTableName
107
83
  ? 'is-active border-primary-container/30 bg-surface-container-high'
108
84
  : 'border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high'
@@ -23,7 +23,8 @@ function renderPreviewHeader(draft, isVisible) {
23
23
  : ''
24
24
  }
25
25
  <button
26
- class="standard-button"
26
+ class="standard-button panel-toggle-button ${isVisible ? '' : 'is-active'}"
27
+ aria-pressed="${isVisible ? 'false' : 'true'}"
27
28
  data-action="toggle-table-designer-sql-preview"
28
29
  data-next-value="${isVisible ? 'false' : 'true'}"
29
30
  type="button"
@@ -11,6 +11,8 @@ export function parseHash(hash = window.location.hash) {
11
11
  switch (segments[0]) {
12
12
  case 'connections':
13
13
  return { name: 'connections', path: '/connections', params: {} };
14
+ case 'backups':
15
+ return { name: 'backups', path: '/backups', params: {} };
14
16
  case 'overview':
15
17
  return { name: 'overview', path: '/overview', params: {} };
16
18
  case 'charts':