diff --git a/deno.json b/deno.json
index 64744bf..28ed70f 100644
--- a/deno.json
+++ b/deno.json
@@ -20,7 +20,8 @@
"@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.2.0",
"@std/assert": "jsr:@std/assert@^1.0.0",
"marked": "npm:marked@^15.0.6",
- "simple-icons": "npm:simple-icons@^15.17.0"
+ "simple-icons": "npm:simple-icons@^15.17.0",
+ "highlight.js": "npm:highlight.js@^11.11.1"
},
"tasks": {
"dev": "APP_BASE_PATH=./dist/dev PARSER_HOST=localhost PARSER_PORT=5000 deno run -A npm:vite dev",
diff --git a/deno.lock b/deno.lock
index 5c0f5ee..c4c8abf 100644
--- a/deno.lock
+++ b/deno.lock
@@ -1947,6 +1947,7 @@
"dependencies": [
"jsr:@soapbox/kysely-deno-sqlite@^2.2.0",
"jsr:@std/assert@1",
+ "npm:highlight.js@^11.11.1",
"npm:marked@^15.0.6",
"npm:simple-icons@^15.17.0"
],
@@ -1965,6 +1966,7 @@
"npm:eslint-plugin-svelte@^3.12.4",
"npm:eslint@^9.36.0",
"npm:globals@^16.4.0",
+ "npm:highlight.js@^11.11.1",
"npm:kysely@0.27.6",
"npm:lucide-svelte@0.546",
"npm:marked@^15.0.6",
diff --git a/package-lock.json b/package-lock.json
index 90dbdaf..7ba1372 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"@deno/vite-plugin": "^1.0.5",
"@jsr/db__sqlite": "^0.12.0",
+ "highlight.js": "^11.11.1",
"kysely": "0.27.6",
"lucide-svelte": "^0.546.0",
"marked": "^15.0.6",
@@ -2586,6 +2587,15 @@
"node": ">=8"
}
},
+ "node_modules/highlight.js": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
diff --git a/package.json b/package.json
index 1ed757b..bcbf0ab 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"dependencies": {
"@deno/vite-plugin": "^1.0.5",
"@jsr/db__sqlite": "^0.12.0",
+ "highlight.js": "^11.11.1",
"kysely": "0.27.6",
"lucide-svelte": "^0.546.0",
"marked": "^15.0.6",
diff --git a/src/app.css b/src/app.css
index 8eb4676..aa814b7 100644
--- a/src/app.css
+++ b/src/app.css
@@ -2,6 +2,73 @@
@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&display=swap');
@import 'tailwindcss';
+/* Highlight.js: atom-one-light for light mode */
+@import 'highlight.js/styles/atom-one-light.css';
+
+/* Highlight.js: atom-one-dark for dark mode */
+.dark .hljs {
+ color: #abb2bf;
+}
+.dark .hljs-comment,
+.dark .hljs-quote {
+ color: #5c6370;
+ font-style: italic;
+}
+.dark .hljs-doctag,
+.dark .hljs-keyword,
+.dark .hljs-formula {
+ color: #c678dd;
+}
+.dark .hljs-section,
+.dark .hljs-name,
+.dark .hljs-selector-tag,
+.dark .hljs-deletion,
+.dark .hljs-subst {
+ color: #e06c75;
+}
+.dark .hljs-literal {
+ color: #56b6c2;
+}
+.dark .hljs-string,
+.dark .hljs-regexp,
+.dark .hljs-addition,
+.dark .hljs-attribute,
+.dark .hljs-meta .hljs-string {
+ color: #98c379;
+}
+.dark .hljs-attr,
+.dark .hljs-variable,
+.dark .hljs-template-variable,
+.dark .hljs-type,
+.dark .hljs-selector-class,
+.dark .hljs-selector-attr,
+.dark .hljs-selector-pseudo,
+.dark .hljs-number {
+ color: #d19a66;
+}
+.dark .hljs-symbol,
+.dark .hljs-bullet,
+.dark .hljs-link,
+.dark .hljs-meta,
+.dark .hljs-selector-id,
+.dark .hljs-title {
+ color: #61aeee;
+}
+.dark .hljs-built_in,
+.dark .hljs-title.class_,
+.dark .hljs-class .hljs-title {
+ color: #e6c07b;
+}
+.dark .hljs-emphasis {
+ font-style: italic;
+}
+.dark .hljs-strong {
+ font-weight: bold;
+}
+.dark .hljs-link {
+ text-decoration: underline;
+}
+
@custom-variant dark (&:where(.dark, .dark *));
@theme {
diff --git a/src/lib/client/ui/meta/JsonView.svelte b/src/lib/client/ui/meta/JsonView.svelte
new file mode 100644
index 0000000..955bf92
--- /dev/null
+++ b/src/lib/client/ui/meta/JsonView.svelte
@@ -0,0 +1,57 @@
+
+
+
+
+
{@html highlightedJson}
+
+
+ {#if queries.length > 0}
+
+
+ Queries ({queries.length})
+
+
+ {#each queries as query, i}
+
+
{@html highlightSql(query)}
+
+ {/each}
+
+
+ {/if}
+
+
+
diff --git a/src/lib/client/ui/modal/Modal.svelte b/src/lib/client/ui/modal/Modal.svelte
index 77e7f99..85fc784 100644
--- a/src/lib/client/ui/modal/Modal.svelte
+++ b/src/lib/client/ui/modal/Modal.svelte
@@ -9,6 +9,15 @@
export let confirmText = 'Confirm';
export let cancelText = 'Cancel';
export let confirmDanger = false; // If true, confirm button is styled as danger (red)
+ export let size: 'sm' | 'md' | 'lg' | 'xl' | '2xl' = 'md';
+
+ const sizeClasses = {
+ sm: 'max-w-sm',
+ md: 'max-w-md',
+ lg: 'max-w-2xl',
+ xl: 'max-w-4xl',
+ '2xl': 'max-w-6xl'
+ };
const dispatch = createEventDispatcher();
@@ -53,7 +62,7 @@
>
diff --git a/src/lib/client/ui/table/Table.svelte b/src/lib/client/ui/table/Table.svelte
index 5e2cf72..35327c6 100644
--- a/src/lib/client/ui/table/Table.svelte
+++ b/src/lib/client/ui/table/Table.svelte
@@ -105,7 +105,8 @@
return sortDirection === 'desc' ? sorted.reverse() : sorted;
}
- $: sortedData = sortData(data);
+ $: sortedData = sortKey ? sortData(data) : data;
+ $: sortKey, sortDirection, sortedData = sortData(data);
diff --git a/src/routes/settings/logs/+page.svelte b/src/routes/settings/logs/+page.svelte
index 0d33c23..a6cca5f 100644
--- a/src/routes/settings/logs/+page.svelte
+++ b/src/routes/settings/logs/+page.svelte
@@ -2,6 +2,9 @@
import { Eye, Copy } from 'lucide-svelte';
import { alertStore } from '$alerts/store';
import Modal from '$ui/modal/Modal.svelte';
+ import JsonView from '$ui/meta/JsonView.svelte';
+ import Table from '$ui/table/Table.svelte';
+ import type { Column } from '$ui/table/types';
import LogsActionsBar from './components/LogsActionsBar.svelte';
import { createSearchStore } from '$lib/client/stores/search';
import { invalidateAll } from '$app/navigation';
@@ -45,6 +48,38 @@
ERROR: 'text-red-600 dark:text-red-400'
};
+ // Table columns
+ const columns: Column[] = [
+ {
+ key: 'timestamp',
+ header: 'Timestamp',
+ sortable: true,
+ sortAccessor: (row) => new Date(row.timestamp).getTime(),
+ cell: (row) => ({
+ html: `${new Date(row.timestamp).toLocaleString()}`
+ })
+ },
+ {
+ key: 'level',
+ header: 'Level',
+ sortable: true,
+ cell: (row) => ({
+ html: `${row.level}`
+ })
+ },
+ {
+ key: 'source',
+ header: 'Source',
+ sortable: true,
+ cell: (row) => row.source || '-'
+ },
+ {
+ key: 'message',
+ header: 'Message',
+ cell: (row) => row.message
+ }
+ ];
+
// Meta modal state
let showMetaModal = false;
let selectedMeta: unknown = null;
@@ -155,98 +190,31 @@
{/if}
-
-
-
-
-
-
+
+
+
+
+ {#if row.meta}
+
-
+
+
+ {/if}
+
+
+ |
@@ -256,14 +224,14 @@
bodyMessage=""
confirmText="Close"
cancelText="Close"
+ size="2xl"
on:confirm={closeMetaModal}
on:cancel={closeMetaModal}
>
-
{JSON.stringify(
- selectedMeta,
- null,
- 2
- )}
+ class="max-h-[70vh] overflow-auto rounded-lg bg-neutral-50 p-4 font-mono text-sm dark:bg-neutral-800"
+ >
+
+