+
+
-
+
+
diff --git a/ui/app/utils/logs.ts b/ui/app/utils/logs.ts
new file mode 100644
index 00000000..586fec0a
--- /dev/null
+++ b/ui/app/utils/logs.ts
@@ -0,0 +1,59 @@
+const formatPathValue = (value: unknown): string => {
+ if (typeof value === 'string') {
+ return value;
+ }
+
+ if (typeof value === 'number' || typeof value === 'boolean' || null === value) {
+ return String(value);
+ }
+
+ return JSON.stringify(value);
+};
+
+const flattenJsonPaths = (value: unknown, prefix = ''): Array => {
+ if (Array.isArray(value)) {
+ if (0 === value.length) {
+ return prefix ? [`${prefix}: []`] : [];
+ }
+
+ return value.flatMap((item, index) =>
+ flattenJsonPaths(item, prefix ? `${prefix}.${index}` : String(index)),
+ );
+ }
+
+ if (value && typeof value === 'object') {
+ const entries = Object.entries(value as Record);
+ if (0 === entries.length) {
+ return prefix ? [`${prefix}: {}`] : [];
+ }
+
+ return entries.flatMap(([key, item]) =>
+ flattenJsonPaths(item, prefix ? `${prefix}.${key}` : key),
+ );
+ }
+
+ return prefix ? [`${prefix}: ${formatPathValue(value)}`] : [];
+};
+
+export const filterLogTextLines = (value: string, filter: string): Array => {
+ const query = filter.trim();
+ if (!query) {
+ return value.split('\n');
+ }
+
+ const needle = query.toLowerCase();
+
+ if (query.includes('.')) {
+ try {
+ const flattened = flattenJsonPaths(JSON.parse(value) as unknown);
+ const matches = flattened.filter((line) => line.toLowerCase().includes(needle));
+ if (matches.length > 0) {
+ return matches;
+ }
+ } catch {
+ // Fall back to plain line filtering for non-JSON content.
+ }
+ }
+
+ return value.split('\n').filter((line) => line.toLowerCase().includes(needle));
+};
diff --git a/ui/tests/composables/useMode.test.ts b/ui/tests/composables/useMode.test.ts
new file mode 100644
index 00000000..e3344e31
--- /dev/null
+++ b/ui/tests/composables/useMode.test.ts
@@ -0,0 +1,44 @@
+import { describe, expect, it } from 'bun:test';
+
+import {
+ SIMPLE_PATH,
+ isSimple,
+ modeFromPref,
+ parseMode,
+ prefFromMode,
+ routeTarget,
+} from '~/composables/useMode';
+
+describe('useMode', () => {
+ it('parse', () => {
+ expect(parseMode('true')).toBe(true);
+ expect(parseMode('false')).toBe(false);
+ expect(parseMode(true)).toBe(true);
+ expect(parseMode(false)).toBe(false);
+ expect(parseMode('null')).toBeNull();
+ expect(parseMode('')).toBeNull();
+ });
+
+ it('resolve', () => {
+ expect(isSimple(null, true)).toBe(true);
+ expect(isSimple(null, false)).toBe(false);
+ expect(isSimple(false, true)).toBe(false);
+ expect(isSimple(true, false)).toBe(true);
+ });
+
+ it('map', () => {
+ expect(modeFromPref(null)).toBe('default');
+ expect(modeFromPref(true)).toBe('simple');
+ expect(modeFromPref(false)).toBe('regular');
+ expect(prefFromMode('default')).toBeNull();
+ expect(prefFromMode('simple')).toBe(true);
+ expect(prefFromMode('regular')).toBe(false);
+ });
+
+ it('route', () => {
+ expect(routeTarget('/', null, true)).toBe(SIMPLE_PATH);
+ expect(routeTarget('/', false, true)).toBeNull();
+ expect(routeTarget('/history', null, true)).toBeNull();
+ expect(routeTarget(SIMPLE_PATH, null, false)).toBeNull();
+ });
+});