sqlite-hub 1.2.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -71
- package/bin/sqlite-hub.js +723 -116
- package/docs/API.md +152 -0
- package/docs/CLI.md +249 -0
- package/docs/CLI_API_PARITY.md +61 -0
- package/docs/changelog.md +149 -0
- package/docs/guidelines/AGENTS.md +32 -0
- package/docs/guidelines/DESIGN.md +55 -0
- package/docs/shortkeys.md +27 -0
- package/docs/todo.md +4 -0
- package/examples/api/generate-types.js +52 -0
- package/frontend/assets/mockups/backups_1_1920.webp +0 -0
- package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_1_1920.webp +0 -0
- package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_1_1920.webp +0 -0
- package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_1_1920.webp +0 -0
- package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
- package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_1_1920.webp +0 -0
- package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/overview_1_1920.webp +0 -0
- package/frontend/assets/mockups/settings_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_1_1920.webp +0 -0
- package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
- package/frontend/js/api.js +63 -0
- package/frontend/js/app.js +310 -10
- package/frontend/js/components/connectionCard.js +4 -2
- package/frontend/js/components/emptyState.js +4 -4
- package/frontend/js/components/generateTypesDropdown.js +33 -0
- package/frontend/js/components/metricCard.js +1 -1
- package/frontend/js/components/modal.js +555 -21
- package/frontend/js/components/pageHeader.js +1 -1
- package/frontend/js/components/queryHistoryDetail.js +3 -3
- package/frontend/js/components/queryHistoryHeader.js +1 -1
- package/frontend/js/components/queryHistoryPanel.js +1 -1
- package/frontend/js/components/rowEditorPanel.js +13 -3
- package/frontend/js/components/sidebar.js +41 -7
- package/frontend/js/components/topNav.js +2 -14
- package/frontend/js/router.js +38 -6
- package/frontend/js/store.js +870 -8
- package/frontend/js/utils/inputClear.js +36 -0
- package/frontend/js/utils/syntheticData.js +240 -0
- package/frontend/js/views/backups.js +581 -37
- package/frontend/js/views/charts.js +9 -9
- package/frontend/js/views/connections.js +2 -2
- package/frontend/js/views/data.js +17 -1
- package/frontend/js/views/documents.js +3 -3
- package/frontend/js/views/editor.js +6 -6
- package/frontend/js/views/logs.js +339 -0
- package/frontend/js/views/mediaTagging.js +7 -5
- package/frontend/js/views/overview.js +3 -3
- package/frontend/js/views/settings.js +80 -30
- package/frontend/js/views/structure.js +12 -1
- package/frontend/js/views/tableAdvisor.js +385 -0
- package/frontend/js/views/tableDesigner.js +6 -0
- package/frontend/styles/base.css +1 -40
- package/frontend/styles/components.css +258 -232
- package/frontend/styles/structure-graph.css +32 -68
- package/frontend/styles/tailwind.css +8 -4
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +28 -21
- package/frontend/styles/views.css +102 -261
- package/package.json +18 -2
- package/server/routes/backups.js +18 -2
- package/server/routes/data.js +45 -0
- package/server/routes/externalApi.js +321 -2
- package/server/routes/logs.js +127 -0
- package/server/routes/structure.js +22 -0
- package/server/server.js +3 -0
- package/server/services/databaseCommandService.js +59 -0
- package/server/services/sqlite/backupDiff.js +914 -0
- package/server/services/sqlite/backupService.js +77 -1
- package/server/services/sqlite/dataBrowserService.js +36 -0
- package/server/services/sqlite/introspection.js +226 -22
- package/server/services/sqlite/structureService.js +7 -0
- package/server/services/sqlite/syntheticDataGenerator.js +728 -0
- package/server/services/sqlite/tableAdvisor.js +790 -0
- package/server/services/storage/appStateStore.js +773 -169
- package/server/services/typeGenerationService.js +663 -0
- package/tailwind.config.cjs +0 -1
- package/.github/funding.yml +0 -2
- package/.github/workflows/ci.yml +0 -36
- package/database.sqlite +0 -0
- package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
- package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
- package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
- package/frontend/assets/mockups/connections_1200.webp +0 -0
- package/frontend/assets/mockups/data_1_1200.webp +0 -0
- package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
- package/frontend/assets/mockups/documents_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
- package/frontend/assets/mockups/overview_1200.webp +0 -0
- package/frontend/assets/mockups/settings_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
- package/frontend/assets/mockups/structure_1_1200.webp +0 -0
- package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
- package/tests/api-token-auth.test.js +0 -279
- package/tests/backup-manager.test.js +0 -140
- package/tests/backups-view.test.js +0 -70
- package/tests/charts-height-preset-storage.test.js +0 -60
- package/tests/charts-route-state.test.js +0 -144
- package/tests/check-constraint-options.test.js +0 -90
- package/tests/cli-args.test.js +0 -113
- package/tests/cli-service-delegation.test.js +0 -140
- package/tests/connection-removal.test.js +0 -52
- package/tests/connections-file-dialog-route.test.js +0 -89
- package/tests/copy-column-modal.test.js +0 -131
- package/tests/database-command-service.test.js +0 -174
- package/tests/database-documents.test.js +0 -85
- package/tests/documents-view.test.js +0 -132
- package/tests/dropdown-button.test.js +0 -101
- package/tests/email-preview.test.js +0 -89
- package/tests/export-blob.test.js +0 -99
- package/tests/export-filenames.test.js +0 -38
- package/tests/file-path-preview.test.js +0 -165
- package/tests/form-controls.test.js +0 -34
- package/tests/json-preview.test.js +0 -49
- package/tests/local-request-security.test.js +0 -85
- package/tests/markdown-documents.test.js +0 -103
- package/tests/native-file-dialog.test.js +0 -105
- package/tests/query-editor.test.js +0 -28
- package/tests/query-history-detail.test.js +0 -37
- package/tests/query-history-header.test.js +0 -30
- package/tests/query-results-truncation.test.js +0 -20
- package/tests/risky-sql.test.js +0 -30
- package/tests/row-editor-json.test.js +0 -82
- package/tests/row-editor-null-values.test.js +0 -184
- package/tests/row-editor-timestamp-preview.test.js +0 -192
- package/tests/security-paths.test.js +0 -84
- package/tests/settings-api-tokens-route.test.js +0 -97
- package/tests/settings-metadata.test.js +0 -114
- package/tests/settings-view.test.js +0 -108
- package/tests/sql-formatter.test.js +0 -173
- package/tests/sql-highlight.test.js +0 -38
- package/tests/sql-identifier-safety.test.js +0 -171
- package/tests/sql-result-limit.test.js +0 -66
- package/tests/structure-view.test.js +0 -60
- package/tests/table-designer-v2-unique-constraints.test.js +0 -78
- package/tests/table-scroll-state.test.js +0 -70
- package/tests/text-cell-stats.test.js +0 -38
|
@@ -16,7 +16,7 @@ function renderMissingDatabase() {
|
|
|
16
16
|
<section class="flex flex-1 items-center justify-center px-8 text-center">
|
|
17
17
|
<div class="max-w-xl">
|
|
18
18
|
<span class="material-symbols-outlined mb-4 text-5xl text-on-surface-variant/20">database_off</span>
|
|
19
|
-
<h1 class="font-
|
|
19
|
+
<h1 class="font-body text-3xl font-black uppercase tracking-tight text-primary-container">
|
|
20
20
|
No Active SQLite Database
|
|
21
21
|
</h1>
|
|
22
22
|
<p class="mt-3 text-sm leading-7 text-on-surface-variant/65">
|
|
@@ -154,7 +154,7 @@ function renderChartsList(state) {
|
|
|
154
154
|
<span class="material-symbols-outlined mb-3 text-4xl text-on-surface-variant/25">${
|
|
155
155
|
hasSearch ? 'manage_search' : isSavedTab ? 'bookmark' : isUnsavedTab ? 'bookmark_border' : 'query_stats'
|
|
156
156
|
}</span>
|
|
157
|
-
<p class="font-
|
|
157
|
+
<p class="font-body text-lg font-black uppercase tracking-tight text-on-surface">
|
|
158
158
|
${
|
|
159
159
|
hasSearch
|
|
160
160
|
? 'No Matching Queries'
|
|
@@ -220,7 +220,7 @@ function renderEmptyChartDetail() {
|
|
|
220
220
|
<div class="flex flex-1 items-center justify-center px-8 py-10 text-center">
|
|
221
221
|
<div class="max-w-lg">
|
|
222
222
|
<span class="material-symbols-outlined mb-4 text-5xl text-on-surface-variant/20">bar_chart</span>
|
|
223
|
-
<h2 class="font-
|
|
223
|
+
<h2 class="font-body text-2xl font-black uppercase tracking-tight text-primary-container">
|
|
224
224
|
Select A Query
|
|
225
225
|
</h2>
|
|
226
226
|
<p class="mt-3 text-sm leading-7 text-on-surface-variant/65">
|
|
@@ -339,7 +339,7 @@ export function renderChartsDetailDrawer(state) {
|
|
|
339
339
|
<section class="flex h-full min-h-0 flex-col bg-surface-low">
|
|
340
340
|
<div class="border-b border-outline-variant/10 px-5 py-4">
|
|
341
341
|
<div class="flex items-center justify-between gap-3">
|
|
342
|
-
<span class="font-
|
|
342
|
+
<span class="font-body text-sm font-black uppercase tracking-[0.18em] text-primary-container">
|
|
343
343
|
Chart Detail
|
|
344
344
|
</span>
|
|
345
345
|
<button class="query-history-icon-button" data-action="close-charts-query-detail" type="button">
|
|
@@ -359,7 +359,7 @@ export function renderChartsDetailDrawer(state) {
|
|
|
359
359
|
<section class="flex h-full min-h-0 flex-col bg-surface-low">
|
|
360
360
|
<div class="border-b border-outline-variant/10 px-5 py-4">
|
|
361
361
|
<div class="flex items-center justify-between gap-3">
|
|
362
|
-
<span class="font-
|
|
362
|
+
<span class="font-body text-sm font-black uppercase tracking-[0.18em] text-primary-container">
|
|
363
363
|
Chart Detail
|
|
364
364
|
</span>
|
|
365
365
|
<button class="query-history-icon-button" data-action="close-charts-query-detail" type="button">
|
|
@@ -415,7 +415,7 @@ export function renderChartsDetailDrawer(state) {
|
|
|
415
415
|
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/70">
|
|
416
416
|
Chart Detail
|
|
417
417
|
</div>
|
|
418
|
-
<h2 class="mt-1 truncate font-
|
|
418
|
+
<h2 class="mt-1 truncate font-body text-lg font-black uppercase tracking-tight text-on-surface">
|
|
419
419
|
${escapeHtml(detail.item.displayTitle)}
|
|
420
420
|
</h2>
|
|
421
421
|
</div>
|
|
@@ -547,7 +547,7 @@ function renderChartCard(chart, state, analysis) {
|
|
|
547
547
|
sizeClass,
|
|
548
548
|
'"><header class="query-chart-card__header"><div class="min-w-0">',
|
|
549
549
|
'<div class="flex flex-wrap items-center gap-2">',
|
|
550
|
-
'<h3 class="truncate font-
|
|
550
|
+
'<h3 class="truncate font-body text-xl font-black uppercase tracking-tight text-on-surface">',
|
|
551
551
|
escapeHtml(chart.name),
|
|
552
552
|
'</h3>',
|
|
553
553
|
renderStatusBadge(getQueryChartTypeLabel(chart.chartType), 'primary'),
|
|
@@ -609,7 +609,7 @@ export function renderChartsDetail(state) {
|
|
|
609
609
|
<div class="charts-detail-shell">
|
|
610
610
|
<header class="charts-detail-shell__header" data-charts-detail-header>
|
|
611
611
|
<div class="charts-detail-shell__title">
|
|
612
|
-
<h1 class="mt-2 truncate font-
|
|
612
|
+
<h1 class="mt-2 truncate font-body text-4xl font-black uppercase tracking-tight text-primary-container">
|
|
613
613
|
${escapeHtml(detail.item.displayTitle)}
|
|
614
614
|
</h1>
|
|
615
615
|
</div>
|
|
@@ -659,7 +659,7 @@ export function renderChartsDetail(state) {
|
|
|
659
659
|
<div class="flex min-h-[240px] items-center justify-center border border-dashed border-outline-variant/20 bg-surface-container-low px-8 text-center">
|
|
660
660
|
<div>
|
|
661
661
|
<span class="material-symbols-outlined mb-3 text-4xl text-on-surface-variant/25">add_chart</span>
|
|
662
|
-
<p class="font-
|
|
662
|
+
<p class="font-body text-lg font-black uppercase tracking-tight text-on-surface">
|
|
663
663
|
No Charts Yet
|
|
664
664
|
</p>
|
|
665
665
|
<p class="mt-2 max-w-md text-sm leading-6 text-on-surface-variant/60">
|
|
@@ -43,7 +43,7 @@ function renderConnectionsBody(state) {
|
|
|
43
43
|
if (state.connections.error) {
|
|
44
44
|
return `
|
|
45
45
|
<div class="border border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
|
|
46
|
-
<div class="font-
|
|
46
|
+
<div class="font-body text-xs font-bold uppercase tracking-[0.18em] text-error">
|
|
47
47
|
${escapeHtml(state.connections.error.code)}
|
|
48
48
|
</div>
|
|
49
49
|
<div class="mt-2">${escapeHtml(state.connections.error.message)}</div>
|
|
@@ -55,7 +55,7 @@ function renderConnectionsBody(state) {
|
|
|
55
55
|
return `
|
|
56
56
|
<div class="border border-dashed border-outline-variant/20 bg-surface-container-low px-8 py-10 text-center">
|
|
57
57
|
<span class="material-symbols-outlined mb-3 text-5xl text-on-surface-variant/25">database_off</span>
|
|
58
|
-
<p class="font-
|
|
58
|
+
<p class="font-body text-xl font-black uppercase tracking-tight text-primary-container">
|
|
59
59
|
No Recorded SQLite Connections
|
|
60
60
|
</p>
|
|
61
61
|
<p class="mx-auto mt-3 max-w-xl text-sm leading-7 text-on-surface-variant/65">
|
|
@@ -137,6 +137,16 @@ function renderWorkspaceHeader(state) {
|
|
|
137
137
|
}
|
|
138
138
|
</div>
|
|
139
139
|
<div class="flex flex-wrap items-center justify-end gap-3">
|
|
140
|
+
<button
|
|
141
|
+
class="standard-button"
|
|
142
|
+
data-action="open-generate-data-modal"
|
|
143
|
+
title="${table ? 'Generate synthetic test rows for this table' : 'Select a table before generating rows'}"
|
|
144
|
+
type="button"
|
|
145
|
+
${table ? '' : 'disabled aria-disabled="true"'}
|
|
146
|
+
>
|
|
147
|
+
<span class="material-symbols-outlined text-sm">auto_awesome</span>
|
|
148
|
+
Generate
|
|
149
|
+
</button>
|
|
140
150
|
${
|
|
141
151
|
table
|
|
142
152
|
? renderWorkspaceOpenDropdown({
|
|
@@ -154,6 +164,12 @@ function renderWorkspaceHeader(state) {
|
|
|
154
164
|
label: 'Table Designer',
|
|
155
165
|
target: tableName => `/table-designer/${encodeURIComponent(tableName)}`,
|
|
156
166
|
},
|
|
167
|
+
{
|
|
168
|
+
icon: 'troubleshoot',
|
|
169
|
+
key: 'table-advisor',
|
|
170
|
+
label: 'Table Advisor',
|
|
171
|
+
target: tableName => `/table-advisor/${encodeURIComponent(tableName)}`,
|
|
172
|
+
},
|
|
157
173
|
{
|
|
158
174
|
key: 'sql-editor',
|
|
159
175
|
},
|
|
@@ -193,7 +209,7 @@ function renderWorkspaceError(state) {
|
|
|
193
209
|
|
|
194
210
|
return `
|
|
195
211
|
<div class="border-b border-error/20 bg-error-container/10 px-6 py-4 text-sm text-on-surface">
|
|
196
|
-
<div class="font-
|
|
212
|
+
<div class="font-body text-xs font-bold uppercase tracking-[0.18em] text-error">
|
|
197
213
|
${escapeHtml(state.dataBrowser.error.code)}
|
|
198
214
|
</div>
|
|
199
215
|
<div class="mt-2">${escapeHtml(state.dataBrowser.error.message)}</div>
|
|
@@ -8,7 +8,7 @@ function renderMissingDatabase() {
|
|
|
8
8
|
<div class="view-frame flex min-h-full items-center justify-center">
|
|
9
9
|
<div class="text-center">
|
|
10
10
|
<span class="material-symbols-outlined mb-3 text-5xl text-on-surface-variant/25">database_off</span>
|
|
11
|
-
<p class="font-
|
|
11
|
+
<p class="font-body text-xl font-black uppercase tracking-tight text-primary-container">
|
|
12
12
|
No Active SQLite Database
|
|
13
13
|
</p>
|
|
14
14
|
<p class="mt-3 max-w-xl text-sm leading-7 text-on-surface-variant/65">
|
|
@@ -294,7 +294,7 @@ function renderEmptyDocumentsState(documents) {
|
|
|
294
294
|
return `
|
|
295
295
|
<div class="documents-empty-state">
|
|
296
296
|
<span class="material-symbols-outlined">description</span>
|
|
297
|
-
<p class="font-
|
|
297
|
+
<p class="font-body text-2xl font-black uppercase tracking-tight text-primary-container">
|
|
298
298
|
No Documents
|
|
299
299
|
</p>
|
|
300
300
|
${renderNewDocumentDropdown(documents, 'mt-4')}
|
|
@@ -309,7 +309,7 @@ function renderDocumentDetail(documents) {
|
|
|
309
309
|
${renderDocumentsTitlebar(documents, { showFilename: false })}
|
|
310
310
|
<div class="documents-empty-state">
|
|
311
311
|
<span class="material-symbols-outlined">sync</span>
|
|
312
|
-
<p class="font-
|
|
312
|
+
<p class="font-body text-2xl font-black uppercase tracking-tight text-primary-container">
|
|
313
313
|
Loading Documents
|
|
314
314
|
</p>
|
|
315
315
|
</div>
|
|
@@ -11,7 +11,7 @@ function renderMissingDatabase() {
|
|
|
11
11
|
return `
|
|
12
12
|
<div class="flex flex-1 flex-col items-center justify-center bg-surface-container-lowest px-8 text-center">
|
|
13
13
|
<span class="material-symbols-outlined mb-3 text-5xl text-on-surface-variant/25">database_off</span>
|
|
14
|
-
<p class="font-
|
|
14
|
+
<p class="font-body text-xl font-black uppercase tracking-tight text-primary-container">
|
|
15
15
|
No Active SQLite Database
|
|
16
16
|
</p>
|
|
17
17
|
<p class="mt-3 max-w-xl text-sm leading-7 text-on-surface-variant/65">
|
|
@@ -60,35 +60,35 @@ function renderPerformancePane(state) {
|
|
|
60
60
|
<div class="performance-metrics-grid grid flex-1 gap-4 bg-surface-container-lowest p-6">
|
|
61
61
|
<div class="metric-card">
|
|
62
62
|
<span class="text-[10px] font-mono uppercase text-on-surface/40">Exec_Time</span>
|
|
63
|
-
<span class="font-
|
|
63
|
+
<span class="font-body text-3xl font-bold text-on-surface">${escapeHtml(
|
|
64
64
|
formatExecutionTimeMs(metrics.timingMs ?? 0),
|
|
65
65
|
)}</span>
|
|
66
66
|
<span class="text-[10px] text-primary-container">Measured backend execution time</span>
|
|
67
67
|
</div>
|
|
68
68
|
<div class="metric-card">
|
|
69
69
|
<span class="text-[10px] font-mono uppercase text-on-surface/40">Statements</span>
|
|
70
|
-
<span class="font-
|
|
70
|
+
<span class="font-body text-3xl font-bold text-on-surface">${escapeHtml(
|
|
71
71
|
formatNumber(metrics.statementCount),
|
|
72
72
|
)}</span>
|
|
73
73
|
<span class="text-[10px] text-on-surface/40">Split and executed by SQLite</span>
|
|
74
74
|
</div>
|
|
75
75
|
<div class="metric-card">
|
|
76
76
|
<span class="text-[10px] font-mono uppercase text-on-surface/40">Rows_Returned</span>
|
|
77
|
-
<span class="font-
|
|
77
|
+
<span class="font-body text-3xl font-bold text-on-surface">${escapeHtml(
|
|
78
78
|
formatNumber(metrics.rowCount),
|
|
79
79
|
)}</span>
|
|
80
80
|
<span class="text-[10px] text-on-surface/40">Visible result set size</span>
|
|
81
81
|
</div>
|
|
82
82
|
<div class="metric-card">
|
|
83
83
|
<span class="text-[10px] font-mono uppercase text-on-surface/40">Memory_Size</span>
|
|
84
|
-
<span class="font-
|
|
84
|
+
<span class="font-body text-3xl font-bold text-on-surface">${escapeHtml(
|
|
85
85
|
formatBytes(metrics.memoryBytes),
|
|
86
86
|
)}</span>
|
|
87
87
|
<span class="text-[10px] text-on-surface/40">Serialized result payload</span>
|
|
88
88
|
</div>
|
|
89
89
|
<div class="metric-card metric-card--accent">
|
|
90
90
|
<span class="text-[10px] font-mono uppercase text-on-surface/40">Rows_Affected</span>
|
|
91
|
-
<span class="font-
|
|
91
|
+
<span class="font-body text-3xl font-bold text-on-surface">${escapeHtml(
|
|
92
92
|
formatNumber(metrics.affectedRowCount),
|
|
93
93
|
)}</span>
|
|
94
94
|
<span class="text-[10px] text-primary-container">INSERT / UPDATE / DELETE impact</span>
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { renderStatusBadge } from '../components/badges.js';
|
|
2
|
+
import { renderTextInput } from '../components/formControls.js';
|
|
3
|
+
import { escapeHtml, formatCompactDateTime, formatDurationMs, formatNumber, truncateMiddle } from '../utils/format.js';
|
|
4
|
+
|
|
5
|
+
const FILTERS = {
|
|
6
|
+
range: [
|
|
7
|
+
['1h', '1H'],
|
|
8
|
+
['24h', '24H'],
|
|
9
|
+
['7d', '7D'],
|
|
10
|
+
['30d', '30D'],
|
|
11
|
+
['all', 'ALL'],
|
|
12
|
+
],
|
|
13
|
+
kind: [
|
|
14
|
+
['all', 'All'],
|
|
15
|
+
['query', 'Query'],
|
|
16
|
+
['access', 'Access'],
|
|
17
|
+
],
|
|
18
|
+
actor: [
|
|
19
|
+
['all', 'All'],
|
|
20
|
+
['user', 'User'],
|
|
21
|
+
['cli', 'CLI'],
|
|
22
|
+
['api', 'API'],
|
|
23
|
+
['mcp', 'MCP'],
|
|
24
|
+
],
|
|
25
|
+
status: [
|
|
26
|
+
['all', 'All'],
|
|
27
|
+
['success', 'Success'],
|
|
28
|
+
['error', 'Error'],
|
|
29
|
+
],
|
|
30
|
+
queryType: [
|
|
31
|
+
['all', 'All'],
|
|
32
|
+
['select', 'Select'],
|
|
33
|
+
['insert', 'Insert'],
|
|
34
|
+
['update', 'Update'],
|
|
35
|
+
['delete', 'Delete'],
|
|
36
|
+
['pragma', 'Pragma'],
|
|
37
|
+
['create', 'Create'],
|
|
38
|
+
['alter', 'Alter'],
|
|
39
|
+
['drop', 'Drop'],
|
|
40
|
+
['other', 'Other'],
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function renderFilterGroup({ label, field, value, items, className = '' }) {
|
|
45
|
+
return `
|
|
46
|
+
<div class="${escapeHtml(['min-w-0', className].filter(Boolean).join(' '))}">
|
|
47
|
+
<div class="mb-2 font-mono text-[9px] font-bold uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
48
|
+
${escapeHtml(label)}
|
|
49
|
+
</div>
|
|
50
|
+
<div class="custom-scrollbar max-w-full overflow-x-auto pb-1">
|
|
51
|
+
<div class="charts-height-toggle" role="group" aria-label="${escapeHtml(label)}">
|
|
52
|
+
${items
|
|
53
|
+
.map(
|
|
54
|
+
([itemValue, itemLabel]) => `
|
|
55
|
+
<button
|
|
56
|
+
class="standard-button charts-height-toggle__button ${value === itemValue ? 'is-active' : ''}"
|
|
57
|
+
aria-pressed="${value === itemValue ? 'true' : 'false'}"
|
|
58
|
+
data-action="set-log-filter"
|
|
59
|
+
data-field="${escapeHtml(field)}"
|
|
60
|
+
data-value="${escapeHtml(itemValue)}"
|
|
61
|
+
type="button"
|
|
62
|
+
>
|
|
63
|
+
${escapeHtml(itemLabel)}
|
|
64
|
+
</button>
|
|
65
|
+
`,
|
|
66
|
+
)
|
|
67
|
+
.join('')}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderLogMetaStrip(logs) {
|
|
75
|
+
const total = formatNumber(logs.total ?? 0);
|
|
76
|
+
const visible = formatNumber((logs.items ?? []).length);
|
|
77
|
+
const activeDatabase = logs.metadata?.activeDatabase?.label ?? null;
|
|
78
|
+
const scope = activeDatabase || 'Active Database';
|
|
79
|
+
const items = [
|
|
80
|
+
['Visible', visible],
|
|
81
|
+
['Matched', total],
|
|
82
|
+
['Scope', scope],
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
return `
|
|
86
|
+
<div class="flex flex-wrap items-center gap-x-8 gap-y-3 border-t border-outline-variant/10 pt-4">
|
|
87
|
+
${items
|
|
88
|
+
.map(
|
|
89
|
+
([label, value]) => `
|
|
90
|
+
<div class="min-w-0">
|
|
91
|
+
<div class="font-mono text-[9px] font-bold uppercase tracking-[0.18em] text-on-surface-variant/50">
|
|
92
|
+
${escapeHtml(label)}
|
|
93
|
+
</div>
|
|
94
|
+
<div class="mt-1 max-w-[18rem] truncate font-mono text-[11px] font-bold uppercase tracking-[0.12em] text-on-surface" data-logs-meta="${escapeHtml(
|
|
95
|
+
label.toLowerCase(),
|
|
96
|
+
)}" title="${escapeHtml(value)}">
|
|
97
|
+
${escapeHtml(value)}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
`,
|
|
101
|
+
)
|
|
102
|
+
.join('')}
|
|
103
|
+
</div>
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderLogFilters(logs) {
|
|
108
|
+
const filters = logs.filters ?? {};
|
|
109
|
+
|
|
110
|
+
return `
|
|
111
|
+
<section class="shell-section p-5">
|
|
112
|
+
<div class="flex flex-col gap-5">
|
|
113
|
+
<form class="grid grid-cols-1 gap-3 md:grid-cols-[minmax(0,1fr)_auto]" data-form="logs-search">
|
|
114
|
+
${renderTextInput({
|
|
115
|
+
dataAttributes: { bind: 'logs-search' },
|
|
116
|
+
name: 'search',
|
|
117
|
+
placeholder: 'Search action, target, SQL, table, error...',
|
|
118
|
+
value: filters.searchInput ?? '',
|
|
119
|
+
})}
|
|
120
|
+
<button class="standard-button justify-center" type="submit">
|
|
121
|
+
<span class="material-symbols-outlined text-sm">search</span>
|
|
122
|
+
Apply
|
|
123
|
+
</button>
|
|
124
|
+
</form>
|
|
125
|
+
${renderLogMetaStrip(logs)}
|
|
126
|
+
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3">
|
|
127
|
+
${renderFilterGroup({
|
|
128
|
+
label: 'Time Window',
|
|
129
|
+
field: 'range',
|
|
130
|
+
value: filters.range ?? 'all',
|
|
131
|
+
items: FILTERS.range,
|
|
132
|
+
})}
|
|
133
|
+
${renderFilterGroup({
|
|
134
|
+
label: 'Log Type',
|
|
135
|
+
field: 'kind',
|
|
136
|
+
value: filters.kind ?? 'all',
|
|
137
|
+
items: FILTERS.kind,
|
|
138
|
+
})}
|
|
139
|
+
${renderFilterGroup({
|
|
140
|
+
label: 'Executed By',
|
|
141
|
+
field: 'actor',
|
|
142
|
+
value: filters.actor ?? 'all',
|
|
143
|
+
items: FILTERS.actor,
|
|
144
|
+
})}
|
|
145
|
+
${renderFilterGroup({
|
|
146
|
+
label: 'Status',
|
|
147
|
+
field: 'status',
|
|
148
|
+
value: filters.status ?? 'all',
|
|
149
|
+
items: FILTERS.status,
|
|
150
|
+
})}
|
|
151
|
+
${renderFilterGroup({
|
|
152
|
+
label: 'Query Type',
|
|
153
|
+
field: 'queryType',
|
|
154
|
+
value: filters.queryType ?? 'all',
|
|
155
|
+
items: FILTERS.queryType,
|
|
156
|
+
className: 'xl:col-span-2',
|
|
157
|
+
})}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</section>
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function renderLogPreview(item) {
|
|
165
|
+
if (item.kind === 'query') {
|
|
166
|
+
return `
|
|
167
|
+
<div class="mt-2 max-w-2xl truncate font-mono text-[11px] text-on-surface-variant/60" title="${escapeHtml(
|
|
168
|
+
item.rawSql ?? '',
|
|
169
|
+
)}">
|
|
170
|
+
${escapeHtml(item.preview || item.rawSql || 'SQL query')}
|
|
171
|
+
</div>
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const metadata = item.metadata ?? {};
|
|
176
|
+
const method = metadata.method ? `${metadata.method} ` : '';
|
|
177
|
+
const path = metadata.path ?? '';
|
|
178
|
+
|
|
179
|
+
return `
|
|
180
|
+
<div class="mt-2 max-w-4xl truncate font-mono text-[11px] text-on-surface-variant/60" title="${escapeHtml(path)}">
|
|
181
|
+
${escapeHtml(`${method}${path || item.action || 'access'}`)}
|
|
182
|
+
</div>
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function getActorTextClass(actor) {
|
|
187
|
+
return ['api', 'cli', 'mcp'].includes(
|
|
188
|
+
String(actor ?? '')
|
|
189
|
+
.trim()
|
|
190
|
+
.toLowerCase(),
|
|
191
|
+
)
|
|
192
|
+
? 'text-primary-container'
|
|
193
|
+
: 'text-on-surface-variant/70';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function renderLogRow(item) {
|
|
197
|
+
const actor = item.executedBy || item.source || 'n/a';
|
|
198
|
+
const source = item.kind || 'log';
|
|
199
|
+
const status = item.status || 'unknown';
|
|
200
|
+
const target = [item.targetType, item.targetName].filter(Boolean).join(' // ') || 'n/a';
|
|
201
|
+
const detailBadges = [
|
|
202
|
+
item.queryType && renderStatusBadge(item.queryType, 'muted'),
|
|
203
|
+
item.destructive === true && renderStatusBadge('destructive', 'alert'),
|
|
204
|
+
item.kind === 'query' && item.rowCount !== null && item.rowCount !== undefined
|
|
205
|
+
? renderStatusBadge(`${formatNumber(item.rowCount)} rows`, 'muted')
|
|
206
|
+
: '',
|
|
207
|
+
item.kind === 'query' && item.affectedRows
|
|
208
|
+
? renderStatusBadge(`${formatNumber(item.affectedRows)} affected`, 'muted')
|
|
209
|
+
: '',
|
|
210
|
+
]
|
|
211
|
+
.filter(Boolean)
|
|
212
|
+
.join('');
|
|
213
|
+
|
|
214
|
+
return `
|
|
215
|
+
<tr class="border-b border-outline-variant/5 bg-surface-container-lowest/60 align-top">
|
|
216
|
+
<td class="px-4 py-4 font-mono text-[11px] text-on-surface-variant/75">
|
|
217
|
+
${escapeHtml(formatCompactDateTime(item.occurredAt))}
|
|
218
|
+
</td>
|
|
219
|
+
<td class="px-4 py-4 font-mono text-[11px] font-bold uppercase tracking-[0.12em] text-on-surface-variant/70">
|
|
220
|
+
${escapeHtml(source)}
|
|
221
|
+
</td>
|
|
222
|
+
<td class="px-4 py-4 font-mono text-[11px] font-bold uppercase tracking-[0.12em] ${getActorTextClass(actor)}">
|
|
223
|
+
${escapeHtml(actor)}
|
|
224
|
+
</td>
|
|
225
|
+
<td class="px-4 py-4">
|
|
226
|
+
<div class="font-body text-sm font-black uppercase text-on-surface">
|
|
227
|
+
${escapeHtml(truncateMiddle(item.action ?? 'log', 44))}
|
|
228
|
+
</div>
|
|
229
|
+
${renderLogPreview(item)}
|
|
230
|
+
${
|
|
231
|
+
detailBadges
|
|
232
|
+
? `<div class="query-history-badge-row query-history-badge-row--compact logs-detail-badge-row mt-3">${detailBadges}</div>`
|
|
233
|
+
: ''
|
|
234
|
+
}
|
|
235
|
+
${item.errorMessage ? `<div class="mt-2 text-xs text-error">${escapeHtml(item.errorMessage)}</div>` : ''}
|
|
236
|
+
</td>
|
|
237
|
+
<td class="px-4 py-4 font-mono text-[11px] text-on-surface-variant/70" title="${escapeHtml(target)}">
|
|
238
|
+
${escapeHtml(truncateMiddle(target, 42))}
|
|
239
|
+
</td>
|
|
240
|
+
<td class="px-4 py-4 font-mono text-[11px] font-bold uppercase tracking-[0.12em] ${
|
|
241
|
+
status === 'error' ? 'text-error' : 'text-on-surface-variant/70'
|
|
242
|
+
}">
|
|
243
|
+
${escapeHtml(status)}
|
|
244
|
+
</td>
|
|
245
|
+
<td class="px-4 py-4 font-mono text-[11px] text-on-surface-variant/70">
|
|
246
|
+
${escapeHtml(formatDurationMs(item.durationMs))}
|
|
247
|
+
</td>
|
|
248
|
+
</tr>
|
|
249
|
+
`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function renderLogTable(logs) {
|
|
253
|
+
if (logs.loading && !(logs.items ?? []).length) {
|
|
254
|
+
return `
|
|
255
|
+
<section class="shell-section flex min-h-0 flex-1 items-center justify-center overflow-hidden" data-logs-table>
|
|
256
|
+
<div class="text-center text-on-surface-variant/45">
|
|
257
|
+
<span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
|
|
258
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.2em]">Loading Logs</div>
|
|
259
|
+
</div>
|
|
260
|
+
</section>
|
|
261
|
+
`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (logs.error) {
|
|
265
|
+
return `
|
|
266
|
+
<section class="shell-section custom-scrollbar min-h-0 flex-1 overflow-auto border-error/20 bg-error-container/10 px-6 py-5" data-logs-table>
|
|
267
|
+
<div class="font-body text-xs font-bold uppercase tracking-[0.18em] text-error">
|
|
268
|
+
${escapeHtml(logs.error.code)}
|
|
269
|
+
</div>
|
|
270
|
+
<div class="mt-2 text-sm text-on-surface">${escapeHtml(logs.error.message)}</div>
|
|
271
|
+
</section>
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!(logs.items ?? []).length) {
|
|
276
|
+
return `
|
|
277
|
+
<section class="shell-section flex min-h-0 flex-1 items-center justify-center overflow-hidden" data-logs-table>
|
|
278
|
+
<div class="text-center text-on-surface-variant/45">
|
|
279
|
+
<span class="material-symbols-outlined mb-3 text-5xl">receipt_long</span>
|
|
280
|
+
<div class="font-body text-xl font-black uppercase text-on-surface">No Logs Found</div>
|
|
281
|
+
<div class="mt-2 font-mono text-[10px] uppercase tracking-[0.18em]">Adjust filters or time window</div>
|
|
282
|
+
</div>
|
|
283
|
+
</section>
|
|
284
|
+
`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return `
|
|
288
|
+
<section class="shell-section flex min-h-0 flex-1 flex-col overflow-hidden" data-logs-table>
|
|
289
|
+
<div class="custom-scrollbar min-h-0 flex-1 overflow-auto" data-logs-table-scroll>
|
|
290
|
+
<table class="w-full min-w-[1080px] text-left">
|
|
291
|
+
<thead class="sticky top-0 z-10 bg-surface-container-highest">
|
|
292
|
+
<tr class="border-b border-outline-variant/10 font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/60">
|
|
293
|
+
<th class="px-4 py-3 font-normal">Time</th>
|
|
294
|
+
<th class="px-4 py-3 font-normal">Source</th>
|
|
295
|
+
<th class="px-4 py-3 font-normal">Executed By</th>
|
|
296
|
+
<th class="px-4 py-3 font-normal">Action</th>
|
|
297
|
+
<th class="px-4 py-3 font-normal">Target</th>
|
|
298
|
+
<th class="px-4 py-3 font-normal">Status</th>
|
|
299
|
+
<th class="px-4 py-3 font-normal">Duration</th>
|
|
300
|
+
</tr>
|
|
301
|
+
</thead>
|
|
302
|
+
<tbody>
|
|
303
|
+
${(logs.items ?? []).map(renderLogRow).join('')}
|
|
304
|
+
</tbody>
|
|
305
|
+
</table>
|
|
306
|
+
</div>
|
|
307
|
+
${
|
|
308
|
+
logs.hasMore
|
|
309
|
+
? `
|
|
310
|
+
<div class="border-t border-outline-variant/10 px-4 py-3">
|
|
311
|
+
<button class="standard-button" data-action="load-more-logs" type="button" ${
|
|
312
|
+
logs.loadingMore ? 'disabled' : ''
|
|
313
|
+
}>
|
|
314
|
+
${logs.loadingMore ? 'Loading...' : 'Load More'}
|
|
315
|
+
</button>
|
|
316
|
+
</div>
|
|
317
|
+
`
|
|
318
|
+
: ''
|
|
319
|
+
}
|
|
320
|
+
</section>
|
|
321
|
+
`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function renderLogsView(state) {
|
|
325
|
+
const logs = state.logs ?? {};
|
|
326
|
+
const activeDatabaseId = state.connections?.active?.id ?? logs.metadata?.activeDatabase?.id ?? '';
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
main: `
|
|
330
|
+
<main class="view-surface flex h-full min-h-0 flex-col" data-logs-view data-logs-active-database-id="${escapeHtml(activeDatabaseId)}">
|
|
331
|
+
<div class="view-frame flex h-full min-h-0 flex-col gap-6">
|
|
332
|
+
${renderLogFilters(logs)}
|
|
333
|
+
${renderLogTable(logs)}
|
|
334
|
+
</div>
|
|
335
|
+
</main>
|
|
336
|
+
`,
|
|
337
|
+
panel: '',
|
|
338
|
+
};
|
|
339
|
+
}
|
|
@@ -301,9 +301,11 @@ function renderCreatedTagList(tags = [], { canRemove = false, removingTagKey = n
|
|
|
301
301
|
escapeHtml(tag.key),
|
|
302
302
|
'" type="button" title="Remove tag" ',
|
|
303
303
|
canRemove && removingTagKey !== tag.key ? '' : 'disabled',
|
|
304
|
-
'><span class="material-symbols-outlined text-
|
|
305
|
-
|
|
306
|
-
'</span
|
|
304
|
+
'><span class="material-symbols-outlined text-sm">',
|
|
305
|
+
'delete',
|
|
306
|
+
'</span>',
|
|
307
|
+
removingTagKey === tag.key ? 'Removing...' : 'Delete',
|
|
308
|
+
'</button></article>',
|
|
307
309
|
].join('');
|
|
308
310
|
})
|
|
309
311
|
.join('')}
|
|
@@ -319,7 +321,7 @@ function renderPreviewMedia(
|
|
|
319
321
|
return `
|
|
320
322
|
<div class="media-tagging-preview__empty">
|
|
321
323
|
<span class="material-symbols-outlined text-4xl">photo_size_select_large</span>
|
|
322
|
-
<div class="mt-3 font-
|
|
324
|
+
<div class="mt-3 font-body text-lg uppercase tracking-[0.06em] text-primary-container">
|
|
323
325
|
Queue Empty
|
|
324
326
|
</div>
|
|
325
327
|
<div class="mt-2 text-sm text-on-surface-variant/55">
|
|
@@ -715,7 +717,7 @@ function renderMappingSection(state) {
|
|
|
715
717
|
</div>
|
|
716
718
|
<div class="media-tagging-card__body media-tagging-card__body--mapping">
|
|
717
719
|
<article class="media-tagging-mapping-card ${mappingCandidate ? 'is-selected' : ''}">
|
|
718
|
-
<div class="font-
|
|
720
|
+
<div class="font-body text-sm uppercase tracking-[0.08em] text-primary-container">
|
|
719
721
|
${escapeHtml(MEDIA_TAGGING_DEFAULT_MAPPING_TABLE)}
|
|
720
722
|
</div>
|
|
721
723
|
${
|
|
@@ -7,7 +7,7 @@ function renderMissingDatabase() {
|
|
|
7
7
|
return `
|
|
8
8
|
<div class="border border-dashed border-outline-variant/20 bg-surface-container-low px-8 py-12 text-center">
|
|
9
9
|
<span class="material-symbols-outlined mb-3 text-5xl text-on-surface-variant/25">database_off</span>
|
|
10
|
-
<p class="font-
|
|
10
|
+
<p class="font-body text-xl font-black uppercase tracking-tight text-primary-container">
|
|
11
11
|
No Active SQLite Database
|
|
12
12
|
</p>
|
|
13
13
|
<p class="mx-auto mt-3 max-w-xl text-sm leading-7 text-on-surface-variant/65">
|
|
@@ -210,7 +210,7 @@ function renderSchemaMapPreview(overview) {
|
|
|
210
210
|
<div class="space-y-5 p-4">
|
|
211
211
|
<div class="flex flex-col gap-5 xl:flex-row xl:items-end xl:justify-between">
|
|
212
212
|
<div>
|
|
213
|
-
<div class="font-
|
|
213
|
+
<div class="font-body text-xl font-black uppercase tracking-tight text-primary-container">
|
|
214
214
|
${escapeHtml(narrative.title)}
|
|
215
215
|
</div>
|
|
216
216
|
<p class="mt-2 max-w-3xl text-sm leading-6 text-on-surface-variant/70">
|
|
@@ -237,7 +237,7 @@ function renderSchemaMapPreview(overview) {
|
|
|
237
237
|
<div class="text-[9px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
238
238
|
${escapeHtml(stat.label)}
|
|
239
239
|
</div>
|
|
240
|
-
<div class="mt-2 font-
|
|
240
|
+
<div class="mt-2 font-body text-2xl font-black uppercase tracking-tight text-on-surface">
|
|
241
241
|
${escapeHtml(stat.value)}
|
|
242
242
|
</div>
|
|
243
243
|
</div>
|