diff --git a/src/client/components/LogsNext.tsx b/src/client/components/LogsNext.tsx
new file mode 100644
index 00000000..7be8dd4f
--- /dev/null
+++ b/src/client/components/LogsNext.tsx
@@ -0,0 +1,88 @@
+import React, {PropsWithChildren, useState, useEffect} from 'react';
+import * as AnsiImport from "ansi-to-react";
+import { Text, Box } from '@chakra-ui/react';
+import {FixedSizeList} from "fixed-size-list";
+import {useSSE} from "@flamefrontend/sse-runtime-react";
+import { useQueryClient, QueryFunctionContext, useQuery } from '@tanstack/react-query'
+import { LogOutputConfig } from '../../core/Atomic';
+import ky from 'ky';
+import { baseUrl } from '../utils';
+
+// @ts-expect-error Ansi export is built incorrectly
+const Ansi = AnsiImport.default.default as typeof AnsiImport.default;
+
+interface LogLineProps {
+ message: string
+}
+
+export const LogLine = (props: LogLineProps) => {
+ return (
+ {props.message}
+ )
+};
+
+interface MinLogInfo {
+ message: string,
+ id: string,
+ level: number
+ levelLabel: string
+}
+
+const createFixedList = (size, initialList: MinLogInfo[] = []): FixedSizeList => {
+ return new FixedSizeList(size, initialList);
+}
+
+let list = createFixedList(50);
+
+export const Logs = (props: {logs: Readonly}) => {
+ return
+ {props.logs.map(x => )}
+
+}
+
+export const LogsFetchable = (props: {settings?: LogOutputConfig, streamable?: boolean}) => {
+ const {
+ settings: {
+ limit = 50,
+ level = 'trace'
+ } = {},
+ streamable = true
+ } = props;
+
+ const [logLevel, setLogLevel] = useState(level);
+ const [logList, setLogList] = useState([]);
+
+ const { isPending, isError, data, error } = useQuery({
+ queryKey: ['logs', { level: logLevel }],
+ queryFn: queryFn
+ });
+
+ const connection = useSSE<{stream: MinLogInfo}>({
+ key: ['stream'],
+ url: "/api/logs/stream",
+ enabled: streamable,
+ events: {
+ stream: (data) => {
+ if(!isPending) {
+ list.add(data);
+ setLogList(Array.from(list.data));
+ }
+ }
+ }
+ });
+
+ useEffect(() => {
+ if(data !== undefined) {
+ list = createFixedList(limit, data.data.map((x, index) => ({...x, message: x.line, id: x.time.toString(), levelLabel: x.levelLabel, level: x.level})));
+ setLogList(Array.from(list.data));
+ }
+ }, [data, limit,setLogList]);
+
+ return
+
+}
+
+type LogsQueryKey = ['logs', {level: string}];
+const queryFn = async (context: QueryFunctionContext) => {
+ return await ky.get(`logs`, { baseUrl: baseUrl }).json() as {data: {line: string, time: number, levelLabel: string, level: number}[]};
+}
\ No newline at end of file
diff --git a/src/client/components/Provider.tsx b/src/client/components/Provider.tsx
index 0bc77db9..02aee388 100644
--- a/src/client/components/Provider.tsx
+++ b/src/client/components/Provider.tsx
@@ -13,13 +13,42 @@ import {
QueryClientProvider,
} from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
+import { createSystem, defaultConfig, defineConfig } from "@chakra-ui/react"
const queryClient = new QueryClient()
+const customConfig = defineConfig({
+ globalCss: {
+ ".ansi-blue-fg": {
+ color: "blue.400"
+ },
+ ".ansi-red-fg": {
+ color: "red.400"
+ },
+ ".ansi-green-fg": {
+ color: "green.400"
+ },
+ ".ansi-magenta-fg": {
+ color: "pink.400"
+ },
+ ".ansi-yellow-fg": {
+ color: "yellow.400"
+ },
+ ".ansi-cyan-fg": {
+ color: "white"
+ },
+ ".ansi-bright-black-fg": {
+ color: "gray.500"
+ }
+ },
+})
+
+export const system = createSystem(defaultConfig, customConfig)
+
export function Provider(props: ColorModeProviderProps) {
return (
-
+
{/* */}
diff --git a/src/stories/Logs.stories.tsx b/src/stories/Logs.stories.tsx
new file mode 100644
index 00000000..4e3b4557
--- /dev/null
+++ b/src/stories/Logs.stories.tsx
@@ -0,0 +1,41 @@
+import preview from "../../.storybook/preview.js";
+import React from 'react';
+
+import { Container, Box } from '@chakra-ui/react';
+import { Logs } from "../client/components/LogsNext";
+import {Provider} from "../client/components/Provider";
+import { generateClientApiJson, generateSourceApiJson, generateSourcePlayerJson } from "../core/tests/utils/apiFixtures.js";
+
+const traceMessage = "[2026-06-16 13:02:42.674 -0400] \u001b[90mTRACE\u001b[39m : \u001b[36m\u001b[90m[App]\u001b[36m \u001b[90m[Scrobblers]\u001b[36m \u001b[90m[Koito - koito]\u001b[36m \u001b[90m[Now Playing]\u001b[36m Not updating, previous matches current update --BUT-- time since last update (194s) is less than max threshold 238.121s\u001b[39m";
+const debugMessage = "[2026-06-16 13:03:20.150 -0400] \u001b[34mDEBUG\u001b[39m : \u001b[36m\u001b[90m[App]\u001b[36m \u001b[90m[Sources]\u001b[36m \u001b[90m[Spotify - default]\u001b[36m Temporarily decreasing polling interval to 1.00s due to Player c98a8fb80e-foxx-arch-SingleUser reporting track duration remaining (2.01s) less than normal interval (10.00s)\u001b[39m";
+const verboseMessage = "[2026-06-16 13:03:23.568 -0400] \u001b[35mVERBOSE\u001b[39m: \u001b[36m\u001b[90m[App]\u001b[36m \u001b[90m[Sources]\u001b[36m \u001b[90m[Spotify - default]\u001b[36m \u001b[90m[Player c98a8fb80e-foxx-arch-SingleUser]\u001b[36m New Play: (2KcQh1rHrJ23eaxax1L1PG) Strutman Lane - One of a Kind\u001b[39m";
+const infoMessage = "[2026-06-16 13:03:24.129 -0400] \u001b[32mINFO\u001b[39m : \u001b[36m\u001b[90m[App]\u001b[36m \u001b[90m[Scrobblers]\u001b[36m \u001b[90m[Koito - koito]\u001b[36m Scrobbled (New) => (Spotify) Couch - Jessie @ 2026-06-16T13:03:23-04:00 (C)\u001b[39m";
+const errorMessage = "[2026-06-16 10:41:13.948 -0400] \u001b[31mERROR\u001b[39m : \u001b[36m\u001b[90m[App]\u001b[36m \u001b[90m[Scrobblers]\u001b[36m \u001b[90m[Lastfm - mylfm]\u001b[36m Scrobble Error (New)\u001b[39m\n playInfo: \"Alice Auer - Unknown @ 2026-06-16T10:06:07-04:00 (C)\"\n payload: {\n \"artist\": \"Alice Auer\",\n \"track\": \"Unknown\",\n \"album\": \"Unknown\",\n \"timestamp\": 1781618767,\n \"mbid\": \"9957324d-3c86-47cd-b844-0c92cf374ec1\",\n \"duration\": 214\n }";
+
+const messages = [traceMessage, debugMessage, verboseMessage, infoMessage, errorMessage];
+
+// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
+const meta = preview.meta({
+ title: 'Examples/Logs',
+ component: Logs,
+ parameters: {
+ // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
+ layout: 'padded',
+ },
+ // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
+ tags: ['autodocs'],
+ // More on argTypes: https://storybook.js.org/docs/api/argtypes
+ args: {
+ logs: messages.map(x => ({message: x}))
+ },
+ render: function Render(args) {
+ return ()
+ },
+decorators: [
+ (Story) => (),
+ ]
+ // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#story-args
+});
+
+export const LogLevels = meta.story({
+});
\ No newline at end of file