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.
- package/.npmingnore +4 -0
- package/README.md +46 -0
- package/assets/images/logo.webp +0 -0
- package/assets/images/logo_extrasmall.webp +0 -0
- package/assets/images/logo_raw.png +0 -0
- package/assets/images/logo_small.webp +0 -0
- package/assets/mockups/connections.png +0 -0
- package/assets/mockups/data.png +0 -0
- package/assets/mockups/data_edit.png +0 -0
- package/assets/mockups/home.png +0 -0
- package/assets/mockups/overview.png +0 -0
- package/assets/mockups/sql_editor.png +0 -0
- package/assets/mockups/structure.png +0 -0
- package/bin/sqlite-hub.js +116 -0
- package/changelog.md +3 -0
- package/data/.gitkeep +0 -0
- package/index.html +100 -0
- package/js/api.js +193 -0
- package/js/app.js +520 -0
- package/js/components/actionBar.js +8 -0
- package/js/components/appShell.js +17 -0
- package/js/components/badges.js +5 -0
- package/js/components/bottomTabs.js +37 -0
- package/js/components/connectionCard.js +106 -0
- package/js/components/dataGrid.js +47 -0
- package/js/components/emptyState.js +159 -0
- package/js/components/metricCard.js +32 -0
- package/js/components/modal.js +317 -0
- package/js/components/pageHeader.js +33 -0
- package/js/components/queryEditor.js +121 -0
- package/js/components/queryResults.js +107 -0
- package/js/components/rowEditorPanel.js +164 -0
- package/js/components/sidebar.js +57 -0
- package/js/components/statusBar.js +39 -0
- package/js/components/toast.js +39 -0
- package/js/components/topNav.js +27 -0
- package/js/router.js +66 -0
- package/js/store.js +1092 -0
- package/js/utils/format.js +179 -0
- package/js/views/connections.js +133 -0
- package/js/views/data.js +400 -0
- package/js/views/editor.js +259 -0
- package/js/views/landing.js +11 -0
- package/js/views/overview.js +220 -0
- package/js/views/settings.js +109 -0
- package/js/views/structure.js +242 -0
- package/package.json +18 -0
- package/publish_brew.sh +444 -0
- package/publish_npm.sh +241 -0
- package/server/routes/connections.js +146 -0
- package/server/routes/data.js +59 -0
- package/server/routes/export.js +25 -0
- package/server/routes/overview.js +39 -0
- package/server/routes/settings.js +50 -0
- package/server/routes/sql.js +50 -0
- package/server/routes/structure.js +38 -0
- package/server/server.js +136 -0
- package/server/services/sqlite/connectionManager.js +306 -0
- package/server/services/sqlite/dataBrowserService.js +255 -0
- package/server/services/sqlite/exportService.js +34 -0
- package/server/services/sqlite/importService.js +111 -0
- package/server/services/sqlite/introspection.js +302 -0
- package/server/services/sqlite/overviewService.js +109 -0
- package/server/services/sqlite/sqlExecutor.js +434 -0
- package/server/services/sqlite/structureService.js +60 -0
- package/server/services/storage/appStateStore.js +530 -0
- package/server/utils/csv.js +34 -0
- package/server/utils/errors.js +175 -0
- package/server/utils/fileValidation.js +135 -0
- package/server/utils/identifier.js +38 -0
- package/server/utils/sqliteTypes.js +112 -0
- package/styles/base.css +176 -0
- package/styles/components.css +323 -0
- package/styles/layout.css +101 -0
- package/styles/tokens.css +49 -0
- 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
|
+
}
|