sqlite-hub 0.17.2 → 1.1.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 (85) hide show
  1. package/README.md +143 -47
  2. package/bin/sqlite-hub.js +206 -444
  3. package/examples/api/queries.js +31 -0
  4. package/examples/api/rows.js +27 -0
  5. package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
  6. package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
  7. package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
  8. package/frontend/assets/mockups/connections_1200.webp +0 -0
  9. package/frontend/assets/mockups/data_1_1200.webp +0 -0
  10. package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
  11. package/frontend/assets/mockups/documents_1200.webp +0 -0
  12. package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
  13. package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
  14. package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
  15. package/frontend/assets/mockups/overview_1200.webp +0 -0
  16. package/frontend/assets/mockups/settings_1200.webp +0 -0
  17. package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
  18. package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
  19. package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
  20. package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
  21. package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
  22. package/frontend/assets/mockups/structure_1_1200.webp +0 -0
  23. package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
  24. package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
  25. package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
  26. package/frontend/js/api.js +25 -0
  27. package/frontend/js/app.js +263 -34
  28. package/frontend/js/components/formControls.js +38 -0
  29. package/frontend/js/components/modal.js +77 -21
  30. package/frontend/js/components/queryChartRenderer.js +28 -3
  31. package/frontend/js/components/queryEditor.js +20 -24
  32. package/frontend/js/components/queryHistoryDetail.js +1 -1
  33. package/frontend/js/components/queryHistoryHeader.js +48 -0
  34. package/frontend/js/components/queryHistoryList.js +132 -0
  35. package/frontend/js/components/queryHistoryPanel.js +72 -136
  36. package/frontend/js/components/structureGraph.js +699 -89
  37. package/frontend/js/components/tableDesignerEditor.js +3 -5
  38. package/frontend/js/store.js +188 -7
  39. package/frontend/js/utils/exportFilenames.js +3 -1
  40. package/frontend/js/views/charts.js +320 -169
  41. package/frontend/js/views/connections.js +59 -64
  42. package/frontend/js/views/data.js +12 -20
  43. package/frontend/js/views/editor.js +0 -2
  44. package/frontend/js/views/settings.js +219 -5
  45. package/frontend/js/views/structure.js +27 -13
  46. package/frontend/styles/components.css +155 -0
  47. package/frontend/styles/structure-graph.css +140 -35
  48. package/frontend/styles/tailwind.generated.css +2 -2
  49. package/frontend/styles/views.css +12 -6
  50. package/package.json +7 -6
  51. package/server/middleware/apiTokenAuth.js +30 -0
  52. package/server/routes/connections.js +17 -0
  53. package/server/routes/export.js +89 -22
  54. package/server/routes/externalApi.js +304 -0
  55. package/server/routes/settings.js +90 -21
  56. package/server/server.js +22 -1
  57. package/server/services/apiTokenService.js +101 -0
  58. package/server/services/appInfoService.js +215 -0
  59. package/server/services/databaseCommandService.js +443 -0
  60. package/server/services/nativeFileDialogService.js +93 -1
  61. package/server/services/sqlite/exportService.js +307 -22
  62. package/server/services/storage/appStateStore.js +113 -0
  63. package/server/utils/errors.js +7 -0
  64. package/tests/api-token-auth.test.js +236 -0
  65. package/tests/cli-args.test.js +16 -3
  66. package/tests/cli-service-delegation.test.js +43 -0
  67. package/tests/connections-file-dialog-route.test.js +43 -0
  68. package/tests/copy-column-modal.test.js +34 -0
  69. package/tests/database-command-service.test.js +139 -0
  70. package/tests/export-blob.test.js +54 -1
  71. package/tests/export-filenames.test.js +4 -0
  72. package/tests/form-controls.test.js +34 -0
  73. package/tests/native-file-dialog.test.js +27 -0
  74. package/tests/settings-api-tokens-route.test.js +97 -0
  75. package/tests/settings-metadata.test.js +99 -1
  76. package/tests/settings-view.test.js +75 -0
  77. package/frontend/assets/mockups/connections.png +0 -0
  78. package/frontend/assets/mockups/data.png +0 -0
  79. package/frontend/assets/mockups/data_row_editor.png +0 -0
  80. package/frontend/assets/mockups/home.png +0 -0
  81. package/frontend/assets/mockups/sql_editor.png +0 -0
  82. package/frontend/assets/mockups/sql_editor_croped.png +0 -0
  83. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  84. package/frontend/assets/mockups/structure.png +0 -0
  85. package/frontend/assets/mockups/structure_inspector.png +0 -0
@@ -1,39 +1,36 @@
1
- import { renderConnectionCard } from "../components/connectionCard.js";
2
- import { renderPageHeader } from "../components/pageHeader.js";
3
- import { escapeHtml } from "../utils/format.js";
1
+ import { renderConnectionCard } from '../components/connectionCard.js';
2
+ import { renderPageHeader } from '../components/pageHeader.js';
3
+ import { escapeHtml } from '../utils/format.js';
4
4
 
5
5
  function renderConnectionsActionButton({
6
- label,
7
- icon,
8
- action = "open-modal",
9
- modal,
10
- tone = "secondary",
11
- disabled = false,
6
+ label,
7
+ icon,
8
+ action = 'open-modal',
9
+ modal,
10
+ tone = 'secondary',
11
+ disabled = false,
12
12
  }) {
13
- const toneClassName =
14
- tone === "primary"
15
- ? "signature-button"
16
- : "standard-button";
17
- const iconClassName = "";
18
- const modalAttribute = modal ? `data-modal="${modal}"` : "";
13
+ const toneClassName = tone === 'primary' ? 'signature-button' : 'standard-button';
14
+ const iconClassName = '';
15
+ const modalAttribute = modal ? `data-modal="${modal}"` : '';
19
16
 
20
- return `
17
+ return `
21
18
  <button
22
19
  class="${toneClassName}"
23
20
  data-action="${escapeHtml(action)}"
24
21
  ${modalAttribute}
25
22
  type="button"
26
- ${disabled ? "disabled" : ""}
23
+ ${disabled ? 'disabled' : ''}
27
24
  >
28
25
  <span>${label}</span>
29
- <span class="material-symbols-outlined${iconClassName ? ` ${iconClassName}` : ""}">${icon}</span>
26
+ <span class="material-symbols-outlined${iconClassName ? ` ${iconClassName}` : ''}">${icon}</span>
30
27
  </button>
31
28
  `;
32
29
  }
33
30
 
34
31
  function renderConnectionsBody(state) {
35
- if (state.connections.loading && !state.connections.recent.length) {
36
- return `
32
+ if (state.connections.loading && !state.connections.recent.length) {
33
+ return `
37
34
  <div class="flex min-h-[280px] items-center justify-center border border-outline-variant/10 bg-surface-container-low">
38
35
  <div class="text-center text-on-surface-variant/40">
39
36
  <span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
@@ -41,10 +38,10 @@ function renderConnectionsBody(state) {
41
38
  </div>
42
39
  </div>
43
40
  `;
44
- }
41
+ }
45
42
 
46
- if (state.connections.error) {
47
- return `
43
+ if (state.connections.error) {
44
+ return `
48
45
  <div class="border border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
49
46
  <div class="font-headline text-xs font-bold uppercase tracking-[0.18em] text-error">
50
47
  ${escapeHtml(state.connections.error.code)}
@@ -52,10 +49,10 @@ function renderConnectionsBody(state) {
52
49
  <div class="mt-2">${escapeHtml(state.connections.error.message)}</div>
53
50
  </div>
54
51
  `;
55
- }
52
+ }
56
53
 
57
- if (!state.connections.recent.length) {
58
- return `
54
+ if (!state.connections.recent.length) {
55
+ return `
59
56
  <div class="border border-dashed border-outline-variant/20 bg-surface-container-low px-8 py-10 text-center">
60
57
  <span class="material-symbols-outlined mb-3 text-5xl text-on-surface-variant/25">database_off</span>
61
58
  <p class="font-headline text-xl font-black uppercase tracking-tight text-primary-container">
@@ -66,71 +63,69 @@ function renderConnectionsBody(state) {
66
63
  </p>
67
64
  <div class="mt-6 flex flex-wrap items-center justify-center gap-3">
68
65
  ${renderConnectionsActionButton({
69
- label: "Open Database",
70
- icon: "folder_open",
71
- modal: "open-connection",
72
- tone: "primary",
66
+ label: 'Create Database',
67
+ icon: 'note_add',
68
+ modal: 'create-connection',
73
69
  })}
74
70
  ${renderConnectionsActionButton({
75
- label: "Create Database",
76
- icon: "note_add",
77
- modal: "create-connection",
71
+ label: 'Open Database',
72
+ icon: 'folder_open',
73
+ modal: 'open-connection',
74
+ tone: 'primary',
78
75
  })}
79
76
  </div>
80
77
  </div>
81
78
  `;
82
- }
79
+ }
83
80
 
84
- return `
81
+ return `
85
82
  <div class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
86
83
  ${state.connections.recent
87
- .map((connection) =>
88
- renderConnectionCard(connection, state.connections.active?.id)
89
- )
90
- .join("")}
84
+ .map(connection => renderConnectionCard(connection, state.connections.active?.id))
85
+ .join('')}
91
86
  </div>
92
87
  `;
93
88
  }
94
89
 
95
90
  export function renderConnectionsView(state) {
96
- const actions = `
91
+ const actions = `
92
+ ${
93
+ state.connections.active
94
+ ? renderConnectionsActionButton({
95
+ label: state.connections.backupLoading ? 'Creating Backup...' : 'Create Backup',
96
+ icon: 'inventory_2',
97
+ action: 'create-backup',
98
+ disabled: state.connections.backupLoading,
99
+ })
100
+ : ''
101
+ }
97
102
  ${renderConnectionsActionButton({
98
- label: "Open Database",
99
- icon: "folder_open",
100
- modal: "open-connection",
101
- tone: "primary",
103
+ label: 'Create Database',
104
+ icon: 'note_add',
105
+ modal: 'create-connection',
102
106
  })}
103
107
  ${renderConnectionsActionButton({
104
- label: "Create Database",
105
- icon: "note_add",
106
- modal: "create-connection",
108
+ label: 'Open Database',
109
+ icon: 'folder_open',
110
+ modal: 'open-connection',
111
+ tone: 'primary',
107
112
  })}
108
- ${
109
- state.connections.active
110
- ? renderConnectionsActionButton({
111
- label: state.connections.backupLoading ? "Creating Backup..." : "Create Backup",
112
- icon: "inventory_2",
113
- action: "create-backup",
114
- disabled: state.connections.backupLoading,
115
- })
116
- : ""
117
- }
118
113
  `;
119
114
 
120
- return {
121
- main: `
115
+ return {
116
+ main: `
122
117
  <section class="view-surface relative min-h-full overflow-hidden">
123
118
  <div class="data-grid-texture pointer-events-none absolute inset-0"></div>
124
119
  <div class="view-frame relative z-10">
125
120
  ${renderPageHeader({
126
- title: "Connections",
127
- subtitle: `Registry // Recent_Targets: ${String(state.connections.recent.length).padStart(2, "0")}`,
128
- actions,
121
+ title: 'Connections',
122
+ subtitle: `Registry // Recent_Targets: ${String(state.connections.recent.length).padStart(2, '0')}`,
123
+ actions,
129
124
  })}
130
125
  ${renderConnectionsBody(state)}
131
126
  </div>
132
127
  </section>
133
128
  `,
134
- panel: "",
135
- };
129
+ panel: '',
130
+ };
136
131
  }
@@ -112,25 +112,8 @@ function renderWorkspaceHeader(state) {
112
112
  const tablesVisible = state.dataBrowser.tablesVisible !== false;
113
113
 
114
114
  return `
115
- <header class="border-b border-outline-variant/10 bg-surface-container px-6 py-5">
116
- <div class="flex flex-wrap items-end justify-between gap-4">
117
- <div class="data-headline-container">
118
- <div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
119
- Data Browser
120
- </div>
121
- <h1 class="mt-2 font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
122
- ${escapeHtml(table?.name ?? 'Table Data')}
123
- </h1>
124
- <div class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
125
- ${
126
- table
127
- ? `rows ${escapeHtml(formatNumber(table.rowCount ?? 0))} // columns ${escapeHtml(
128
- formatNumber(table.columns?.length ?? 0),
129
- )}`
130
- : `tables ${escapeHtml(formatNumber(state.dataBrowser.tables.length))}`
131
- }
132
- </div>
133
- </div>
115
+ <header class="border-b border-outline-variant/10 bg-surface-container px-6 py-3">
116
+ <div class="flex flex-wrap items-center justify-between gap-4">
134
117
  <div class="flex items-center gap-3">
135
118
  ${
136
119
  table
@@ -145,7 +128,14 @@ function renderWorkspaceHeader(state) {
145
128
  : '<span class="material-symbols-outlined text-sm">visibility</span> Show Tables'
146
129
  }
147
130
  </button>
148
- <button
131
+ `
132
+ : ''
133
+ }
134
+ </div>
135
+ <div class="flex flex-wrap items-center justify-end gap-3">
136
+ ${
137
+ table
138
+ ? `<button
149
139
  class="standard-button"
150
140
  data-action="open-data-export-modal"
151
141
  type="button"
@@ -476,6 +466,8 @@ function renderTableSurface(state) {
476
466
  escapeHtml(formatNumber(totalRows)),
477
467
  filteredRowsText,
478
468
  ' rows',
469
+ ' // columns ',
470
+ escapeHtml(formatNumber(table.columns?.length ?? 0)),
479
471
  '</div>',
480
472
  '<div class="flex flex-wrap items-center gap-4"><div class="flex items-center gap-2">',
481
473
  '<span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">rows</span>',
@@ -307,8 +307,6 @@ export function renderEditorView(state, { isResultsRoute = false } = {}) {
307
307
  query: state.editor.sqlText,
308
308
  executing: state.editor.executing,
309
309
  exporting: state.editor.exportLoading,
310
- historyLoading: state.editor.historyLoading,
311
- historyTotal: state.editor.historyTotal,
312
310
  editorVisible: state.editor.editorPanelVisible,
313
311
  historyVisible: state.editor.historyPanelVisible,
314
312
  })}
@@ -1,6 +1,95 @@
1
1
  import { renderPageHeader } from '../components/pageHeader.js';
2
+ import { renderTextInput } from '../components/formControls.js';
2
3
  import { escapeHtml } from '../utils/format.js';
3
4
 
5
+ function renderSettingsNavigation(activeSection) {
6
+ const items = [
7
+ { id: 'information', icon: 'info', label: 'Information' },
8
+ { id: 'api-tokens', icon: 'key', label: 'API Tokens' },
9
+ ];
10
+
11
+ return `
12
+ <nav class="flex flex-wrap gap-2 border-b border-outline-variant/10 pb-4" aria-label="Settings sections">
13
+ ${items
14
+ .map(
15
+ item => `
16
+ <button
17
+ class="query-history-tab ${activeSection === item.id ? 'is-active' : ''}"
18
+ aria-current="${activeSection === item.id ? 'page' : 'false'}"
19
+ data-action="set-settings-section"
20
+ data-section="${item.id}"
21
+ type="button"
22
+ >
23
+ <span class="material-symbols-outlined text-sm mr-[5px]">${item.icon}</span>
24
+ ${item.label}
25
+ </button>
26
+ `,
27
+ )
28
+ .join('')}
29
+ </nav>
30
+ `;
31
+ }
32
+
33
+ function renderVersionCheckStatus(settings) {
34
+ const result = settings.versionCheck;
35
+ const error = settings.versionCheckError;
36
+
37
+ if (error) {
38
+ return `
39
+ <div class="border border-error/20 bg-error-container/10 px-4 py-3 text-sm text-on-surface">
40
+ <div class="font-mono text-[10px] font-bold uppercase tracking-[0.18em] text-error">
41
+ ${escapeHtml(error.code ?? 'VERSION_CHECK_FAILED')}
42
+ </div>
43
+ <div class="mt-1 text-on-surface-variant">${escapeHtml(error.message ?? 'Version check failed.')}</div>
44
+ </div>
45
+ `;
46
+ }
47
+
48
+ if (!result) {
49
+ return `
50
+ <div class="border border-outline-variant/10 bg-surface-container-high px-4 py-3 text-sm text-on-surface-variant">
51
+ No version check has been run yet.
52
+ </div>
53
+ `;
54
+ }
55
+
56
+ const updateAvailable = Boolean(result.updateAvailable);
57
+ const releaseLink =
58
+ updateAvailable && result.releaseUrl
59
+ ? `
60
+ <a
61
+ class="standard-button"
62
+ href="${escapeHtml(result.releaseUrl)}"
63
+ rel="noreferrer"
64
+ target="_blank"
65
+ >
66
+ <span class="material-symbols-outlined text-sm">open_in_new</span>
67
+ Open Release
68
+ </a>
69
+ `
70
+ : '';
71
+
72
+ return `
73
+ <div class="border ${
74
+ updateAvailable ? 'border-primary-container/30 bg-primary-container/5' : 'border-outline-variant/10 bg-surface-container-high'
75
+ } px-4 py-3">
76
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
77
+ <div class="min-w-0">
78
+ <div class="font-mono text-[10px] font-bold uppercase tracking-[0.18em] ${
79
+ updateAvailable ? 'text-primary-container' : 'text-on-surface-variant/70'
80
+ }">
81
+ ${updateAvailable ? 'New Version Available' : 'Up To Date'}
82
+ </div>
83
+ <div class="mt-1 text-sm text-on-surface-variant">
84
+ Current v${escapeHtml(result.currentVersion ?? '0.0.0')} · Latest v${escapeHtml(result.latestVersion ?? 'unknown')}
85
+ </div>
86
+ </div>
87
+ ${releaseLink}
88
+ </div>
89
+ </div>
90
+ `;
91
+ }
92
+
4
93
  function renderSettingsContent(state) {
5
94
  if (state.settings.loading && !state.settings.appVersion) {
6
95
  return `
@@ -24,11 +113,98 @@ function renderSettingsContent(state) {
24
113
  `;
25
114
  }
26
115
 
116
+ const activeSection = state.settings.section === 'api-tokens' ? 'api-tokens' : 'information';
27
117
  const appVersion = escapeHtml(state.settings.appVersion ?? '0.0.0');
28
118
  const sqliteVersion = escapeHtml(state.settings.sqliteVersion ?? 'unknown');
119
+ const versionCheckLoading = Boolean(state.settings.versionCheckLoading);
120
+ const tokenDatabase = state.settings.tokenDatabase;
121
+ const apiTokens = state.settings.apiTokens ?? [];
122
+ const createdApiToken = state.settings.createdApiToken;
123
+ const tokenSaving = Boolean(state.settings.tokenSaving);
124
+ const tokenItems = apiTokens.length
125
+ ? apiTokens
126
+ .map(
127
+ token => `
128
+ <div class="flex flex-col gap-3 border border-outline-variant/10 bg-surface-container-high px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
129
+ <div class="min-w-0">
130
+ <div class="truncate text-sm font-semibold text-on-surface">${escapeHtml(token.name)}</div>
131
+ <div class="mt-1 font-mono text-[10px] text-on-surface-variant/60">
132
+ ${escapeHtml(token.tokenPrefix)}... · Created ${escapeHtml(token.createdAt ?? 'unknown')}
133
+ </div>
134
+ </div>
135
+ <button
136
+ class="delete-button"
137
+ data-action="open-delete-api-token-modal"
138
+ data-token-id="${escapeHtml(token.id)}"
139
+ type="button"
140
+ ${tokenSaving ? 'disabled' : ''}
141
+ >
142
+ Delete
143
+ </button>
144
+ </div>
145
+ `,
146
+ )
147
+ .join('')
148
+ : '<div class="text-sm text-on-surface-variant">No API tokens exist for this database.</div>';
149
+ const tokenSection = tokenDatabase
150
+ ? `
151
+ <div class="space-y-5">
152
+ <div class="space-y-3">
153
+ <div class="text-sm font-semibold text-on-surface">${escapeHtml(tokenDatabase.label)}</div>
154
+ <div>
155
+ <div class="text-[10px] font-mono uppercase tracking-[0.2em] text-on-surface-variant/60">
156
+ Database ID
157
+ </div>
158
+ <div class="mt-2 grid grid-cols-1 gap-3 sm:grid-cols-[minmax(0,1fr)_9rem]">
159
+ <div
160
+ class="min-w-0 flex-1 break-all border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 font-mono text-sm text-primary-container"
161
+ data-database-id
162
+ >${escapeHtml(tokenDatabase.id)}</div>
163
+ <button class="standard-button w-full self-center justify-center" data-action="copy-database-id" type="button">
164
+ <span class="material-symbols-outlined text-sm">content_copy</span>
165
+ Copy ID
166
+ </button>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ <form class="grid grid-cols-1 gap-3 sm:grid-cols-[minmax(0,1fr)_9rem]" data-form="create-api-token">
171
+ ${renderTextInput({
172
+ className: 'min-w-0 flex-1',
173
+ dataAttributes: { apiTokenName: true },
174
+ maxlength: 80,
175
+ name: 'name',
176
+ placeholder: 'Token name',
177
+ })}
178
+ <button class="standard-button w-full self-center justify-center" type="submit" ${tokenSaving ? 'disabled' : ''}>
179
+ Create Token
180
+ </button>
181
+ </form>
182
+ ${
183
+ createdApiToken?.token
184
+ ? `
185
+ <div class="border border-primary-container/20 bg-primary-container/5 p-4">
186
+ <div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">Shown once</div>
187
+ <div class="mt-2 text-sm text-on-surface-variant">Store this token now. SQLite Hub only keeps its hash.</div>
188
+ <div class="mt-3 flex flex-col gap-2 sm:flex-row">
189
+ ${renderTextInput({
190
+ className: 'min-w-0 flex-1 font-mono',
191
+ dataAttributes: { createdApiToken: true },
192
+ readonly: true,
193
+ value: createdApiToken.token,
194
+ })}
195
+ <button class="standard-button" data-action="copy-created-api-token" type="button">Copy</button>
196
+ </div>
197
+ </div>
198
+ `
199
+ : ''
200
+ }
201
+ <div class="space-y-2">${tokenItems}</div>
202
+ </div>
203
+ `
204
+ : '<div class="text-sm leading-6 text-on-surface-variant">Select a database before creating database-specific API tokens.</div>';
29
205
 
30
- return `
31
- <div class="grid grid-cols-1 gap-6 xl:grid-cols-2">
206
+ const informationSection = `
207
+ <div class="grid grid-cols-1 gap-6 xl:grid-cols-2">
32
208
  <section class="shell-section overflow-hidden">
33
209
  <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest px-4 py-2">
34
210
  <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Application</span>
@@ -56,6 +232,23 @@ function renderSettingsContent(state) {
56
232
  <div class="border-t border-outline-variant/10 py-4 text-sm leading-6 text-on-surface-variant">
57
233
  SQL functions and syntax are evaluated with this SQLite runtime.
58
234
  </div>
235
+ <div class="border-t border-outline-variant/10 pt-4">
236
+ <div class="mb-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
237
+ <div class="text-[10px] font-mono uppercase tracking-[0.2em] text-on-surface-variant/60">
238
+ Version_Check
239
+ </div>
240
+ <button
241
+ class="standard-button"
242
+ data-action="check-app-version"
243
+ type="button"
244
+ ${versionCheckLoading ? 'disabled aria-disabled="true"' : ''}
245
+ >
246
+ <span class="material-symbols-outlined text-sm">${versionCheckLoading ? 'progress_activity' : 'sync'}</span>
247
+ ${versionCheckLoading ? 'Checking...' : 'Check Updates'}
248
+ </button>
249
+ </div>
250
+ ${renderVersionCheckStatus(state.settings)}
251
+ </div>
59
252
  </div>
60
253
  </section>
61
254
 
@@ -120,8 +313,26 @@ function renderSettingsContent(state) {
120
313
  </a>
121
314
  </div>
122
315
  </section>
123
- </div>
124
- `;
316
+ </div>
317
+ `;
318
+ const apiTokensSection = `
319
+ <section class="shell-section overflow-hidden">
320
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest px-4 py-2">
321
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em] mr-px">API Tokens</span>
322
+ <span class="material-symbols-outlined text-xs text-primary-container">key</span>
323
+ </div>
324
+ <div class="p-6">
325
+ ${tokenSection}
326
+ </div>
327
+ </section>
328
+ `;
329
+
330
+ return `
331
+ <div class="space-y-6">
332
+ ${renderSettingsNavigation(activeSection)}
333
+ ${activeSection === 'api-tokens' ? apiTokensSection : informationSection}
334
+ </div>
335
+ `;
125
336
  }
126
337
 
127
338
  export function renderSettingsView(state) {
@@ -131,7 +342,10 @@ export function renderSettingsView(state) {
131
342
  <div class="view-frame mx-auto max-w-6xl space-y-8">
132
343
  ${renderPageHeader({
133
344
  title: 'Settings',
134
- subtitle: 'Application // Build + Credits',
345
+ subtitle:
346
+ state.settings.section === 'api-tokens'
347
+ ? 'Security // Database API access'
348
+ : 'Application // Build + Credits',
135
349
  })}
136
350
  ${renderSettingsContent(state)}
137
351
  </div>
@@ -115,20 +115,27 @@ function renderObjectInspector(detail) {
115
115
  return `
116
116
  <div class="structure-graph__panel">
117
117
  <div class="space-y-3">
118
- <div class="structure-graph__eyebrow">Object Inspector</div>
119
- <div class="structure-graph__title">${escapeHtml(detail.name)}</div>
120
- <div class="flex flex-wrap items-center gap-3">
121
- <div class="structure-graph__subtitle">${escapeHtml(detail.type ?? 'object')}</div>
122
- ${
123
- detail.tableName
124
- ? `
125
- <div class="structure-graph__subtitle">
126
- TABLE ${escapeHtml(detail.tableName)}
127
- </div>
128
- `
129
- : ''
130
- }
118
+ <div class="structure-graph__panel-heading">
119
+ <div class="structure-graph__eyebrow">Object Inspector</div>
120
+ <button
121
+ class="query-history-icon-button"
122
+ data-structure-graph-action="toggle-inspector"
123
+ type="button"
124
+ aria-label="Hide inspector"
125
+ >
126
+ <span class="material-symbols-outlined text-[18px]">close</span>
127
+ </button>
131
128
  </div>
129
+ <div class="structure-graph__title">${escapeHtml(detail.name)}</div>
130
+ ${
131
+ detail.tableName
132
+ ? `
133
+ <div class="structure-graph__subtitle">
134
+ TABLE ${escapeHtml(detail.tableName)}
135
+ </div>
136
+ `
137
+ : ''
138
+ }
132
139
  </div>
133
140
 
134
141
  <div class="structure-graph__summary">
@@ -255,6 +262,8 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading, tabl
255
262
  <span class="material-symbols-outlined text-sm">table_rows</span>
256
263
  Open Data
257
264
  </button>
265
+ </div>
266
+ <div class="structure-graph__toolbar-inspector">
258
267
  <button
259
268
  class="${toolbarButtonClass}"
260
269
  data-structure-graph-action="toggle-inspector"
@@ -280,6 +289,11 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading, tabl
280
289
  The active database does not expose table metadata for the schema graph.
281
290
  </p>
282
291
  </div>
292
+ <div
293
+ class="structure-graph__edge-readout"
294
+ data-structure-graph-edge-readout
295
+ hidden
296
+ ></div>
283
297
  </div>
284
298
 
285
299
  <aside