-
- {log.filename}:{log.lineno}
-
-
+ {date}
+
+
+ {time}
+
+
+
- {log.function}()
+ {log.level}
+
+
+
+ {log.name}
+
+
+
+
+ {log.filename}:{log.lineno}
+
+
+ {log.function}()
+
-
-
-
-
- {log.message}
-
-
-
- ))
+
+
+
+
+
+ );
+ })
) : (
-
+
No logs found matching your filters.
@@ -381,13 +459,6 @@ export function LogViewer() {
)}
-
- {/* Log Detail Dialog */}
-
- You can left click into a log for a detailed view.
+ Click a log to copy it. Hover over a long message to see all of it.
),
diff --git a/frontend/src/lib/util/logs.ts b/frontend/src/lib/util/logs.ts
new file mode 100644
index 00000000..6bf5ea00
--- /dev/null
+++ b/frontend/src/lib/util/logs.ts
@@ -0,0 +1,109 @@
+import { components } from "@/schemas/dwe_os_2";
+
+type Log = components["schemas"]["LogSchema"];
+
+// "2026-10-15 05:14:50.293" -> ["2026-10-15", "05:14:50.293"]. The backend
+// uses "." before the milliseconds; Python's logging default is ",".
+export function splitTimestamp(timestamp: string): [string, string] {
+ const match = timestamp.match(
+ /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})(?:[.,](\d+))?/,
+ );
+ if (!match) return [timestamp, ""];
+ return [match[1], match[3] ? `${match[2]}.${match[3]}` : match[2]];
+}
+
+/**
+ * One log for the clipboard, like the backend's log file but with the message
+ * on its own line:
+ * "