sqlite-hub 0.1.3

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 (76) hide show
  1. package/.npmingnore +4 -0
  2. package/README.md +46 -0
  3. package/assets/images/logo.webp +0 -0
  4. package/assets/images/logo_extrasmall.webp +0 -0
  5. package/assets/images/logo_raw.png +0 -0
  6. package/assets/images/logo_small.webp +0 -0
  7. package/assets/mockups/connections.png +0 -0
  8. package/assets/mockups/data.png +0 -0
  9. package/assets/mockups/data_edit.png +0 -0
  10. package/assets/mockups/home.png +0 -0
  11. package/assets/mockups/overview.png +0 -0
  12. package/assets/mockups/sql_editor.png +0 -0
  13. package/assets/mockups/structure.png +0 -0
  14. package/bin/sqlite-hub.js +116 -0
  15. package/changelog.md +3 -0
  16. package/data/.gitkeep +0 -0
  17. package/index.html +100 -0
  18. package/js/api.js +193 -0
  19. package/js/app.js +520 -0
  20. package/js/components/actionBar.js +8 -0
  21. package/js/components/appShell.js +17 -0
  22. package/js/components/badges.js +5 -0
  23. package/js/components/bottomTabs.js +37 -0
  24. package/js/components/connectionCard.js +106 -0
  25. package/js/components/dataGrid.js +47 -0
  26. package/js/components/emptyState.js +159 -0
  27. package/js/components/metricCard.js +32 -0
  28. package/js/components/modal.js +317 -0
  29. package/js/components/pageHeader.js +33 -0
  30. package/js/components/queryEditor.js +121 -0
  31. package/js/components/queryResults.js +107 -0
  32. package/js/components/rowEditorPanel.js +164 -0
  33. package/js/components/sidebar.js +57 -0
  34. package/js/components/statusBar.js +39 -0
  35. package/js/components/toast.js +39 -0
  36. package/js/components/topNav.js +27 -0
  37. package/js/router.js +66 -0
  38. package/js/store.js +1092 -0
  39. package/js/utils/format.js +179 -0
  40. package/js/views/connections.js +133 -0
  41. package/js/views/data.js +400 -0
  42. package/js/views/editor.js +259 -0
  43. package/js/views/landing.js +11 -0
  44. package/js/views/overview.js +220 -0
  45. package/js/views/settings.js +109 -0
  46. package/js/views/structure.js +242 -0
  47. package/package.json +18 -0
  48. package/publish_brew.sh +444 -0
  49. package/publish_npm.sh +241 -0
  50. package/server/routes/connections.js +146 -0
  51. package/server/routes/data.js +59 -0
  52. package/server/routes/export.js +25 -0
  53. package/server/routes/overview.js +39 -0
  54. package/server/routes/settings.js +50 -0
  55. package/server/routes/sql.js +50 -0
  56. package/server/routes/structure.js +38 -0
  57. package/server/server.js +136 -0
  58. package/server/services/sqlite/connectionManager.js +306 -0
  59. package/server/services/sqlite/dataBrowserService.js +255 -0
  60. package/server/services/sqlite/exportService.js +34 -0
  61. package/server/services/sqlite/importService.js +111 -0
  62. package/server/services/sqlite/introspection.js +302 -0
  63. package/server/services/sqlite/overviewService.js +109 -0
  64. package/server/services/sqlite/sqlExecutor.js +434 -0
  65. package/server/services/sqlite/structureService.js +60 -0
  66. package/server/services/storage/appStateStore.js +530 -0
  67. package/server/utils/csv.js +34 -0
  68. package/server/utils/errors.js +175 -0
  69. package/server/utils/fileValidation.js +135 -0
  70. package/server/utils/identifier.js +38 -0
  71. package/server/utils/sqliteTypes.js +112 -0
  72. package/styles/base.css +176 -0
  73. package/styles/components.css +323 -0
  74. package/styles/layout.css +101 -0
  75. package/styles/tokens.css +49 -0
  76. package/styles/views.css +84 -0
@@ -0,0 +1,220 @@
1
+ import { renderMetricCard } from "../components/metricCard.js";
2
+ import { renderPageHeader } from "../components/pageHeader.js";
3
+ import { renderStatusBadge } from "../components/badges.js";
4
+ import { escapeHtml, formatBytes, formatDateTime, formatNumber } from "../utils/format.js";
5
+
6
+ function renderMissingDatabase() {
7
+ return `
8
+ <div class="border border-dashed border-outline-variant/20 bg-surface-container-low px-8 py-12 text-center">
9
+ <span class="material-symbols-outlined mb-3 text-5xl text-on-surface-variant/25">database_off</span>
10
+ <p class="font-headline text-xl font-black uppercase tracking-tight text-primary-container">
11
+ No Active SQLite Database
12
+ </p>
13
+ <p class="mx-auto mt-3 max-w-xl text-sm leading-7 text-on-surface-variant/65">
14
+ Open a local SQLite file to load real overview metrics, object counts, and storage details.
15
+ </p>
16
+ </div>
17
+ `;
18
+ }
19
+
20
+ function renderOverviewMetrics(overview) {
21
+ const estimatedSizeBytes =
22
+ overview.estimatedSizeBytes || overview.file?.sizeBytes || 0;
23
+
24
+ const metrics = [
25
+ {
26
+ label: "Database Size",
27
+ value: formatBytes(overview.file?.sizeBytes ?? estimatedSizeBytes),
28
+ subtext: `Estimated pages: ${formatNumber(overview.sqlite?.pageCount ?? 0)}`,
29
+ accent: true,
30
+ },
31
+ {
32
+ label: "Tables",
33
+ value: formatNumber(overview.counts?.tables ?? 0),
34
+ subtext: `${formatNumber(overview.counts?.views ?? 0)} views`,
35
+ },
36
+ {
37
+ label: "Indexes",
38
+ value: formatNumber(overview.counts?.indexes ?? 0),
39
+ subtext: `${formatNumber(overview.counts?.triggers ?? 0)} triggers`,
40
+ },
41
+ {
42
+ label: "Journal Mode",
43
+ value: String(overview.sqlite?.journalMode ?? "n/a").toUpperCase(),
44
+ subtext: overview.sqlite?.foreignKeys ? "FK enabled" : "FK disabled",
45
+ },
46
+ ];
47
+
48
+ return `
49
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
50
+ ${metrics.map((metric) => renderMetricCard(metric)).join("")}
51
+ </div>
52
+ `;
53
+ }
54
+
55
+ function renderTopTables(overview) {
56
+ const sizeMap = new Map(
57
+ (overview.topTablesByEstimatedSize ?? []).map((entry) => [entry.name, entry.sizeBytes])
58
+ );
59
+
60
+ return `
61
+ <section class="shell-section xl:col-span-2">
62
+ <div class="flex items-center justify-between bg-surface-container-highest px-4 py-2">
63
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Top Tables</span>
64
+ <span class="material-symbols-outlined text-xs text-on-surface-variant">table_rows</span>
65
+ </div>
66
+ <div class="custom-scrollbar overflow-auto">
67
+ <table class="w-full text-left font-mono text-xs">
68
+ <thead>
69
+ <tr class="border-b border-outline-variant/10 text-on-surface/40">
70
+ <th class="p-4 font-normal">TABLE_NAME</th>
71
+ <th class="p-4 font-normal">ROWS</th>
72
+ <th class="p-4 font-normal">INDEXES</th>
73
+ <th class="p-4 font-normal">ESTIMATED_SIZE</th>
74
+ </tr>
75
+ </thead>
76
+ <tbody>
77
+ ${(overview.topTablesByRowCount ?? [])
78
+ .map(
79
+ (table, index) => `
80
+ <tr
81
+ class="${
82
+ index % 2 === 0 ? "bg-surface-container" : "bg-surface-container-lowest/30"
83
+ } border-b border-outline-variant/5"
84
+ >
85
+ <td class="p-4 text-primary-container">${escapeHtml(table.name)}</td>
86
+ <td class="p-4">${escapeHtml(formatNumber(table.rowCount ?? 0))}</td>
87
+ <td class="p-4">${escapeHtml(formatNumber(table.indexCount ?? 0))}</td>
88
+ <td class="p-4">${escapeHtml(
89
+ sizeMap.has(table.name) ? formatBytes(sizeMap.get(table.name)) : "n/a"
90
+ )}</td>
91
+ </tr>
92
+ `
93
+ )
94
+ .join("")}
95
+ </tbody>
96
+ </table>
97
+ </div>
98
+ </section>
99
+ `;
100
+ }
101
+
102
+ function renderOperationalSurface(overview) {
103
+ const lines = [
104
+ `PATH: ${overview.file?.path ?? "n/a"}`,
105
+ `LAST_MODIFIED: ${formatDateTime(overview.file?.lastModifiedAt)}`,
106
+ `SQLITE_VERSION: ${overview.sqlite?.version ?? "n/a"}`,
107
+ `PAGE_SIZE: ${formatNumber(overview.sqlite?.pageSize ?? 0)} bytes`,
108
+ `FREELIST_COUNT: ${formatNumber(overview.sqlite?.freelistCount ?? 0)}`,
109
+ `ENCODING: ${overview.sqlite?.encoding ?? "n/a"}`,
110
+ ];
111
+
112
+ return `
113
+ <section class="shell-section overflow-hidden">
114
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest px-4 py-2">
115
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Storage Telemetry</span>
116
+ <span class="text-[8px] text-primary-container">LIVE</span>
117
+ </div>
118
+ <div class="custom-scrollbar h-56 space-y-2 overflow-y-auto p-4 font-mono text-[10px] leading-relaxed">
119
+ ${lines
120
+ .map(
121
+ (line) => `
122
+ <div class="text-on-surface/40">
123
+ <span class="text-[#00dce1]">INFO:</span> ${escapeHtml(line)}
124
+ </div>
125
+ `
126
+ )
127
+ .join("")}
128
+ </div>
129
+ </section>
130
+ `;
131
+ }
132
+
133
+ function renderIntegrityCard(overview, readOnly) {
134
+ return `
135
+ <section class="relative overflow-hidden border border-outline-variant/10 bg-[radial-gradient(circle_at_top_right,rgba(252,227,0,0.14),transparent_30%),linear-gradient(135deg,#1c1b1b,#0e0e0e)]">
136
+ <div class="pointer-events-none absolute inset-0 bg-[linear-gradient(to_top,rgba(32,31,31,0.95),rgba(32,31,31,0.2))]"></div>
137
+ <div class="relative flex min-h-[220px] flex-col justify-end p-6">
138
+ <div class="mb-6 flex h-16 w-16 items-center justify-center border border-primary-container/20 bg-primary-container/10">
139
+ <span class="material-symbols-outlined text-4xl text-primary-container">security</span>
140
+ </div>
141
+ <div class="text-[10px] font-black uppercase tracking-[0.25em] text-primary-container">
142
+ INTEGRITY_STATUS
143
+ </div>
144
+ <div class="mt-2 flex flex-wrap items-center gap-2">
145
+ ${renderStatusBadge(overview.sqlite?.integrityCheck ?? "unknown", "success")}
146
+ ${renderStatusBadge(
147
+ readOnly ? "READ_ONLY" : "READ_WRITE",
148
+ readOnly ? "alert" : "primary"
149
+ )}
150
+ </div>
151
+ <div class="mt-3 font-mono text-xs text-on-surface">
152
+ QUICK_CHECK: ${escapeHtml(String(overview.sqlite?.quickCheck ?? "n/a"))}
153
+ </div>
154
+ <div class="mt-2 text-[10px] uppercase tracking-[0.2em] text-on-surface-variant/60">
155
+ USER_VERSION ${escapeHtml(String(overview.sqlite?.userVersion ?? 0))} //
156
+ SCHEMA_VERSION ${escapeHtml(String(overview.sqlite?.schemaVersion ?? 0))}
157
+ </div>
158
+ </div>
159
+ </section>
160
+ `;
161
+ }
162
+
163
+ export function renderOverviewView(state) {
164
+ const overview = state.overview.data;
165
+ const readOnly = state.connections.active?.readOnly;
166
+
167
+ return {
168
+ main: `
169
+ <section class="view-surface min-h-full bg-surface-container">
170
+ <div class="view-frame mx-auto max-w-7xl space-y-8">
171
+ ${renderPageHeader({
172
+ title: "DATABASE_OVERVIEW",
173
+ subtitle: overview
174
+ ? `System Registry: ${overview.connection?.label ?? "ACTIVE_SQLITE_DB"}`
175
+ : "System Registry: NO_ACTIVE_DATABASE",
176
+ actions: `
177
+ <button
178
+ class="toolbar-button toolbar-button--primary bg-primary-container px-4 py-2 font-headline text-xs font-bold uppercase tracking-widest text-on-primary clipped-corner"
179
+ data-action="navigate"
180
+ data-to="/editor"
181
+ style="--clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);"
182
+ type="button"
183
+ >
184
+ <span class="material-symbols-outlined text-sm">terminal</span>
185
+ Run Query
186
+ </button>
187
+ `,
188
+ })}
189
+
190
+ ${
191
+ state.overview.loading && !overview
192
+ ? `
193
+ <div class="flex min-h-[280px] items-center justify-center border border-outline-variant/10 bg-surface-container-low">
194
+ <div class="text-center text-on-surface-variant/40">
195
+ <span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
196
+ <p class="font-mono text-[10px] uppercase tracking-[0.22em]">LOADING_OVERVIEW</p>
197
+ </div>
198
+ </div>
199
+ `
200
+ : state.overview.error
201
+ ? renderMissingDatabase()
202
+ : overview
203
+ ? `
204
+ ${renderOverviewMetrics(overview)}
205
+ <div class="grid grid-cols-1 gap-6 xl:grid-cols-3">
206
+ ${renderTopTables(overview)}
207
+ <div class="space-y-6">
208
+ ${renderOperationalSurface(overview)}
209
+ ${renderIntegrityCard(overview, readOnly)}
210
+ </div>
211
+ </div>
212
+ `
213
+ : renderMissingDatabase()
214
+ }
215
+ </div>
216
+ </section>
217
+ `,
218
+ panel: "",
219
+ };
220
+ }
@@ -0,0 +1,109 @@
1
+ import { renderPageHeader } from '../components/pageHeader.js';
2
+ import { escapeHtml } from '../utils/format.js';
3
+
4
+ function renderSettingsContent(state) {
5
+ if (state.settings.loading && !state.settings.appVersion) {
6
+ return `
7
+ <div class="flex min-h-[280px] items-center justify-center border border-outline-variant/10 bg-surface-container-low">
8
+ <div class="text-center text-on-surface-variant/40">
9
+ <span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
10
+ <p class="font-mono text-[10px] uppercase tracking-[0.22em]">LOADING_SETTINGS</p>
11
+ </div>
12
+ </div>
13
+ `;
14
+ }
15
+
16
+ if (state.settings.error) {
17
+ return `
18
+ <div class="border border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
19
+ <div class="font-headline text-xs font-bold uppercase tracking-[0.18em] text-error">
20
+ ${escapeHtml(state.settings.error.code)}
21
+ </div>
22
+ <div class="mt-2">${escapeHtml(state.settings.error.message)}</div>
23
+ </div>
24
+ `;
25
+ }
26
+
27
+ return `
28
+ <div class="grid grid-cols-1 gap-6 xl:grid-cols-2">
29
+ <section class="shell-section overflow-hidden">
30
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest px-4 py-2">
31
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Application</span>
32
+ <span class="material-symbols-outlined text-xs text-primary-container">deployed_code</span>
33
+ </div>
34
+ <div class="space-y-5 p-6">
35
+ <div>
36
+ <div class="text-[10px] font-mono uppercase tracking-[0.2em] text-on-surface-variant/60">
37
+ Current Version
38
+ </div>
39
+ <div class="mt-2 font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
40
+ v${escapeHtml(state.settings.appVersion ?? '0.0.0')}
41
+ </div>
42
+ <div class="mt-2 text-sm text-on-surface-variant/70">
43
+ Read directly from <code>package.json</code>.
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </section>
48
+
49
+ <section class="shell-section overflow-hidden">
50
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest px-4 py-2">
51
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Copyright</span>
52
+ <span class="material-symbols-outlined text-xs text-primary-container">badge</span>
53
+ </div>
54
+ <div class="space-y-4 p-6">
55
+ <div class="text-sm leading-7 text-on-surface">
56
+ Copyright Oliver Jessner
57
+ </div>
58
+ <a
59
+ class="inline-flex items-center gap-2 border border-outline-variant/20 bg-surface-container-low px-4 py-3 text-xs font-bold uppercase tracking-[0.2em] text-primary-container transition-colors hover:bg-surface-container-highest"
60
+ href="https://oliverjessner.at"
61
+ rel="noreferrer"
62
+ target="_blank"
63
+ >
64
+ <span class="material-symbols-outlined text-sm">open_in_new</span>
65
+ Open Website
66
+ </a>
67
+ </div>
68
+ </section>
69
+
70
+ <section class="shell-section overflow-hidden">
71
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest px-4 py-2">
72
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Source Code</span>
73
+ <span class="material-symbols-outlined text-xs text-primary-container">badge</span>
74
+ </div>
75
+ <div class="space-y-4 p-6">
76
+ <div class="text-sm leading-7 text-on-surface">
77
+ Open Source Code
78
+ </div>
79
+ <a
80
+ class="inline-flex items-center gap-2 border border-outline-variant/20 bg-surface-container-low px-4 py-3 text-xs font-bold uppercase tracking-[0.2em] text-primary-container transition-colors hover:bg-surface-container-highest"
81
+ href="https://github.com/oliverjessner/sqlite-hub"
82
+ rel="noreferrer"
83
+ target="_blank"
84
+ >
85
+ <span class="material-symbols-outlined text-sm">open_in_new</span>
86
+ Open Github
87
+ </a>
88
+ </div>
89
+ </section>
90
+ </div>
91
+ `;
92
+ }
93
+
94
+ export function renderSettingsView(state) {
95
+ return {
96
+ main: `
97
+ <section class="view-surface min-h-full bg-surface-container">
98
+ <div class="view-frame mx-auto max-w-6xl space-y-8">
99
+ ${renderPageHeader({
100
+ title: 'Settings',
101
+ subtitle: 'Application // Build + Credits',
102
+ })}
103
+ ${renderSettingsContent(state)}
104
+ </div>
105
+ </section>
106
+ `,
107
+ panel: '',
108
+ };
109
+ }
@@ -0,0 +1,242 @@
1
+ import { renderMetricCard } from "../components/metricCard.js";
2
+ import { renderPageHeader } from "../components/pageHeader.js";
3
+ import { getCurrentStructureEntryDetail } from "../store.js";
4
+ import { escapeHtml, formatNumber } from "../utils/format.js";
5
+
6
+ function renderEntryGroup(title, entries, activeName) {
7
+ return `
8
+ <section class="shell-section p-5">
9
+ <div class="mb-4 text-[10px] font-bold uppercase tracking-[0.25em] text-primary-container">
10
+ ${escapeHtml(title)}
11
+ </div>
12
+ ${
13
+ entries.length
14
+ ? `
15
+ <div class="space-y-2">
16
+ ${entries
17
+ .map(
18
+ (entry) => `
19
+ <button
20
+ class="w-full border px-3 py-3 text-left transition-colors ${
21
+ entry.name === activeName
22
+ ? "border-primary-container/30 bg-surface-container-high"
23
+ : "border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high"
24
+ }"
25
+ data-action="select-structure-entry"
26
+ data-entry-name="${escapeHtml(entry.name)}"
27
+ type="button"
28
+ >
29
+ <div class="font-mono text-xs ${
30
+ entry.name === activeName
31
+ ? "text-primary-container"
32
+ : "text-on-surface"
33
+ }">
34
+ ${escapeHtml(entry.name)}
35
+ </div>
36
+ <div class="mt-1 text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
37
+ ${escapeHtml(entry.tableName || entry.type)}
38
+ </div>
39
+ </button>
40
+ `
41
+ )
42
+ .join("")}
43
+ </div>
44
+ `
45
+ : `<div class="text-sm text-on-surface-variant/45">No ${escapeHtml(
46
+ title.toLowerCase()
47
+ )} found.</div>`
48
+ }
49
+ </section>
50
+ `;
51
+ }
52
+
53
+ function renderDetail(detail) {
54
+ if (!detail) {
55
+ return `
56
+ <div class="shell-section p-8">
57
+ <p class="text-sm text-on-surface-variant/55">Select a structure object to inspect metadata and relational detail.</p>
58
+ </div>
59
+ `;
60
+ }
61
+
62
+ return `
63
+ <section class="shell-section p-8">
64
+ <div class="mb-6 flex items-center justify-between border-b border-outline-variant/10 pb-4">
65
+ <div>
66
+ <h2 class="font-headline text-2xl font-black uppercase tracking-tight text-primary-container">
67
+ ${escapeHtml(detail.name)}
68
+ </h2>
69
+ <p class="mt-2 font-mono text-[10px] uppercase tracking-[0.2em] text-on-surface-variant/50">
70
+ ${escapeHtml(detail.type ?? "table")}
71
+ </p>
72
+ </div>
73
+ <div class="flex items-center gap-3">
74
+ ${
75
+ detail.type === "table"
76
+ ? `
77
+ <button
78
+ class="border border-outline-variant/20 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface hover:bg-surface-container-highest"
79
+ data-action="navigate"
80
+ data-to="/data/${encodeURIComponent(detail.name)}"
81
+ type="button"
82
+ >
83
+ Open Data
84
+ </button>
85
+ `
86
+ : ""
87
+ }
88
+ <div class="flex h-12 w-12 items-center justify-center bg-primary-container/10">
89
+ <span class="material-symbols-outlined text-2xl text-primary-container">account_tree</span>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ <div class="grid grid-cols-1 gap-6 xl:grid-cols-[0.95fr_1.05fr]">
94
+ <div class="space-y-6">
95
+ <div class="bg-surface-container-lowest p-4">
96
+ <div class="mb-3 text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">Columns</div>
97
+ ${
98
+ detail.columns?.length
99
+ ? `
100
+ <div class="space-y-2 font-mono text-[11px] text-on-surface/65">
101
+ ${detail.columns
102
+ .map(
103
+ (column) => `
104
+ <div class="flex justify-between gap-3">
105
+ <span>${escapeHtml(column.name)}</span>
106
+ <span class="text-primary-container">${escapeHtml(
107
+ column.declaredType || column.affinity
108
+ )}</span>
109
+ </div>
110
+ `
111
+ )
112
+ .join("")}
113
+ </div>
114
+ `
115
+ : '<div class="text-sm text-on-surface-variant/45">No column metadata for this object.</div>'
116
+ }
117
+ </div>
118
+ <div class="bg-surface-container-lowest p-4">
119
+ <div class="mb-3 text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">Relational Detail</div>
120
+ <div class="space-y-2 font-mono text-[11px] text-on-surface/65">
121
+ <div class="flex justify-between">
122
+ <span>Foreign Keys</span>
123
+ <span class="text-primary-container">${escapeHtml(
124
+ formatNumber(detail.foreignKeys?.length ?? 0)
125
+ )}</span>
126
+ </div>
127
+ <div class="flex justify-between">
128
+ <span>Indexes</span>
129
+ <span class="text-primary-container">${escapeHtml(
130
+ formatNumber(detail.indexes?.length ?? 0)
131
+ )}</span>
132
+ </div>
133
+ <div class="flex justify-between">
134
+ <span>Triggers</span>
135
+ <span class="text-primary-container">${escapeHtml(
136
+ formatNumber(detail.triggers?.length ?? 0)
137
+ )}</span>
138
+ </div>
139
+ <div class="flex justify-between">
140
+ <span>Identity</span>
141
+ <span class="text-primary-container">${escapeHtml(
142
+ detail.identityStrategy?.type ?? "n/a"
143
+ )}</span>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ <div class="bg-surface-container-lowest p-4">
149
+ <div class="mb-3 text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">DDL</div>
150
+ <pre class="custom-scrollbar max-h-[520px] overflow-auto whitespace-pre-wrap font-mono text-[11px] leading-6 text-on-surface-variant/75">${escapeHtml(
151
+ detail.ddl || "No DDL available."
152
+ )}</pre>
153
+ </div>
154
+ </div>
155
+ </section>
156
+ `;
157
+ }
158
+
159
+ export function renderStructureView(state) {
160
+ const structure = state.structure.data;
161
+ const detail = getCurrentStructureEntryDetail(state);
162
+ const counts = structure
163
+ ? [
164
+ { label: "Tables", value: formatNumber(structure.grouped.tables.length) },
165
+ { label: "Views", value: formatNumber(structure.grouped.views.length) },
166
+ { label: "Indexes", value: formatNumber(structure.grouped.indexes.length) },
167
+ {
168
+ label: "Triggers",
169
+ value: formatNumber(structure.grouped.triggers.length),
170
+ accent: true,
171
+ },
172
+ ]
173
+ : [];
174
+
175
+ return {
176
+ main: `
177
+ <section class="view-surface min-h-full bg-surface-container">
178
+ <div class="view-frame mx-auto max-w-7xl space-y-8">
179
+ ${renderPageHeader({
180
+ eyebrow: "Structure // sqlite_master + PRAGMA",
181
+ title: "Structure",
182
+ subtitle: "Raw DDL, table identity, foreign keys, and index metadata",
183
+ })}
184
+
185
+ ${
186
+ state.structure.loading && !structure
187
+ ? `
188
+ <div class="flex min-h-[280px] items-center justify-center border border-outline-variant/10 bg-surface-container-low">
189
+ <div class="text-center text-on-surface-variant/40">
190
+ <span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
191
+ <p class="font-mono text-[10px] uppercase tracking-[0.22em]">LOADING_STRUCTURE</p>
192
+ </div>
193
+ </div>
194
+ `
195
+ : state.structure.error
196
+ ? `
197
+ <div class="border border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
198
+ <div class="font-headline text-xs font-bold uppercase tracking-[0.18em] text-error">
199
+ ${escapeHtml(state.structure.error.code)}
200
+ </div>
201
+ <div class="mt-2">${escapeHtml(state.structure.error.message)}</div>
202
+ </div>
203
+ `
204
+ : structure
205
+ ? `
206
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
207
+ ${counts.map((metric) => renderMetricCard(metric)).join("")}
208
+ </div>
209
+ <section class="grid grid-cols-1 gap-6 xl:grid-cols-[0.45fr_1.55fr]">
210
+ <div class="space-y-6">
211
+ ${renderEntryGroup(
212
+ "Tables",
213
+ structure.grouped.tables,
214
+ state.structure.selectedName
215
+ )}
216
+ ${renderEntryGroup(
217
+ "Views",
218
+ structure.grouped.views,
219
+ state.structure.selectedName
220
+ )}
221
+ ${renderEntryGroup(
222
+ "Indexes",
223
+ structure.grouped.indexes,
224
+ state.structure.selectedName
225
+ )}
226
+ ${renderEntryGroup(
227
+ "Triggers",
228
+ structure.grouped.triggers,
229
+ state.structure.selectedName
230
+ )}
231
+ </div>
232
+ ${renderDetail(detail)}
233
+ </section>
234
+ `
235
+ : ""
236
+ }
237
+ </div>
238
+ </section>
239
+ `,
240
+ panel: "",
241
+ };
242
+ }
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "sqlite-hub",
3
+ "version": "0.1.3",
4
+ "description": "SQLite-only local management app backend and SPA shell",
5
+ "main": "server/server.js",
6
+ "bin": {
7
+ "sqlite-hub": "bin/sqlite-hub.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/sqlite-hub.js",
11
+ "dev": "node --watch server/server.js",
12
+ "publish": "bash publish_brew.sh && bash publish_npm.sh"
13
+ },
14
+ "dependencies": {
15
+ "better-sqlite3": "^11.8.1",
16
+ "express": "^4.21.2"
17
+ }
18
+ }