diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 8ce91e0..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "cSpell.words": [ - "glazewm", - "neostandard", - "singlefile", - "tabler", - "vueuse", - "whaleybar", - "zebar" - ] -} diff --git a/client/.env b/client/.env index 81670b7..9baa3a0 100644 --- a/client/.env +++ b/client/.env @@ -1 +1,2 @@ VITE_API_PREFIX=http://localhost:4202 +VITE_GLAZE_INIT_SECRET= diff --git a/client/.env.production b/client/.env.production index c1b6025..e4639f3 100644 --- a/client/.env.production +++ b/client/.env.production @@ -1 +1,2 @@ VITE_API_PREFIX=https://whaleybar.deno.dev +VITE_ZEBAR=false diff --git a/client/.env.production.local b/client/.env.production.local index c1b6025..c025599 100644 --- a/client/.env.production.local +++ b/client/.env.production.local @@ -1 +1,3 @@ -VITE_API_PREFIX=https://whaleybar.deno.dev +VITE_API_PREFIX_switchbacklater=https://whaleybar.deno.dev +VITE_API_PREFIX=http://localhost:4202 +VITE_ZEBAR=true diff --git a/client/.eslintrc-auto-import.json b/client/.eslintrc-auto-import.json deleted file mode 100644 index 5d911fa..0000000 --- a/client/.eslintrc-auto-import.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "globals": { - "Component": true, - "ComponentPublicInstance": true, - "ComputedRef": true, - "DirectiveBinding": true, - "EffectScope": true, - "ExtractDefaultPropTypes": true, - "ExtractPropTypes": true, - "ExtractPublicPropTypes": true, - "InjectionKey": true, - "MaybeRef": true, - "MaybeRefOrGetter": true, - "PropType": true, - "Ref": true, - "VNode": true, - "VariantProps": true, - "WritableComputedRef": true, - "computed": true, - "createApp": true, - "customRef": true, - "cva": true, - "defineAsyncComponent": true, - "defineComponent": true, - "effectScope": true, - "getCurrentInstance": true, - "getCurrentScope": true, - "h": true, - "inject": true, - "isProxy": true, - "isReactive": true, - "isReadonly": true, - "isRef": true, - "markRaw": true, - "nextTick": true, - "onActivated": true, - "onBeforeMount": true, - "onBeforeRouteLeave": true, - "onBeforeRouteUpdate": true, - "onBeforeUnmount": true, - "onBeforeUpdate": true, - "onDeactivated": true, - "onErrorCaptured": true, - "onMounted": true, - "onRenderTracked": true, - "onRenderTriggered": true, - "onScopeDispose": true, - "onServerPrefetch": true, - "onUnmounted": true, - "onUpdated": true, - "onWatcherCleanup": true, - "provide": true, - "reactive": true, - "readonly": true, - "ref": true, - "resolveComponent": true, - "shallowReactive": true, - "shallowReadonly": true, - "shallowRef": true, - "toRaw": true, - "toRef": true, - "toRefs": true, - "toValue": true, - "triggerRef": true, - "unref": true, - "useAttrs": true, - "useCssModule": true, - "useCssVars": true, - "useEmoji": true, - "useId": true, - "useLink": true, - "useModel": true, - "useRoute": true, - "useRouter": true, - "useSlots": true, - "useTemplateRef": true, - "useWeather": true, - "watch": true, - "watchEffect": true, - "watchPostEffect": true, - "watchSyncEffect": true, - "useWith": true, - "useWithFetch": true, - "useWithGet": true, - "useWithPost": true, - "LiveLogs": true, - "TimeDate": true, - "WeatherLocation": true, - "emojiQueries": true, - "weatherQueries": true - } -} diff --git a/client/.vscode/settings.json b/client/.vscode/settings.json index 8c42b87..7dc882f 100644 --- a/client/.vscode/settings.json +++ b/client/.vscode/settings.json @@ -9,5 +9,5 @@ "typescript", "typescriptreact", "vue" - ] + ], } diff --git a/client/eslint.config.js b/client/eslint.config.js index f00dab3..5485f0c 100644 --- a/client/eslint.config.js +++ b/client/eslint.config.js @@ -1,6 +1,6 @@ -import fs from 'node:fs/promises' import pluginJs from '@eslint/js' import stylistic from '@stylistic/eslint-plugin' +import stylisticTs from '@stylistic/eslint-plugin-ts' import importPlugin from 'eslint-plugin-import' import tailwind from 'eslint-plugin-tailwindcss' import unusedImports from 'eslint-plugin-unused-imports' @@ -9,19 +9,11 @@ import globals from 'globals' import neostandard from 'neostandard' import tseslint from 'typescript-eslint' -const autoImport = JSON.parse( - await fs.readFile('./.eslintrc-auto-import.json', 'utf8'), -) - const baseConfig = [ - { files: ['*/.{js,jsx,ts,tsx.vue}'] }, + { files: ['**/*.{js,jsx,ts,tsx.vue}'] }, { languageOptions: { globals: { - // Include globals from auto-generated imports (e.g., Vue components, - // composables) created by unplugin-auto-import during the build process - ...autoImport.globals, - // Add browser environment globals (window, document, etc.) to prevent // ESLint from flagging them as undefined ...globals.browser, @@ -146,19 +138,28 @@ const stylisticConfig = [ }, ] +// Must be last in the configuration order to properly override conflicting rules. +// TypeScript's type system handles many checks more accurately than ESLint, +// including import resolution, type checking, and variable usage. +const typeScriptConfig = [ + ...tseslint.configs.strict, + { + plugins: { + '@stylistic/ts': stylisticTs, + }, + rules: { + // The TypeScript config adds rules for JSX that we want to disable in favor of + // the more accurate TypeScript type checking, which handles JSX syntax correctly + // 'react/jsx-no-undef': 0, + }, + }, +] + export default [ ...baseConfig, ...tailwindConfig, ...vueConfig, ...standardConfig, ...stylisticConfig, - - // Must be last in the configuration order to properly override conflicting rules. - // TypeScript's type system handles many checks more accurately than ESLint, - // including import resolution, type checking, and variable usage. - ...tseslint.configs.recommended, - - // The TypeScript config adds rules for JSX that we want to disable in favor of - // the more accurate TypeScript type checking, which handles JSX syntax correctly - // 'react/jsx-no-undef': 0, + ...typeScriptConfig, ] diff --git a/client/package-lock.json b/client/package-lock.json index 1e0bf7b..91024e7 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -23,6 +23,7 @@ "devDependencies": { "@eslint/js": "^9.13.0", "@stylistic/eslint-plugin": "^2.9.0", + "@stylistic/eslint-plugin-ts": "^2.12.1", "@types/node": "^22.7.9", "@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue-jsx": "^4.0.1", @@ -31,7 +32,6 @@ "eslint-plugin-tailwindcss": "^3.17.5", "eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-vue": "^9.29.1", - "fast-glob": "^3.3.2", "globals": "^15.11.0", "neostandard": "^0.11.7", "postcss": "^8.4.47", @@ -1417,6 +1417,164 @@ "eslint": ">=8.40.0" } }, + "node_modules/@stylistic/eslint-plugin-ts": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-2.12.1.tgz", + "integrity": "sha512-Xx1NIioeW6LLlOfq5L/dLSrUXvi6q80UXDNbn/rXjKCzFT4a8wKwtp1q25kssdr1JEXI9a6tOHwFsh4Em+MoGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.13.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/scope-manager": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.1.tgz", + "integrity": "sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/visitor-keys": "8.18.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/types": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.1.tgz", + "integrity": "sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz", + "integrity": "sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/visitor-keys": "8.18.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/utils": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.1.tgz", + "integrity": "sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.18.1", + "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/typescript-estree": "8.18.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz", + "integrity": "sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.18.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@tabler/icons": { "version": "3.20.0", "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.20.0.tgz", @@ -2063,10 +2221,11 @@ } }, "node_modules/acorn": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", - "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -3409,10 +3568,11 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", - "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -3503,14 +3663,15 @@ } }, "node_modules/espree": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", - "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.12.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.1.0" + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/client/package.json b/client/package.json index 47489d4..03e0124 100644 --- a/client/package.json +++ b/client/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "build": "vite build", - "build-watch": "env BUILD_ENV=local vite build --watch", + "build-watch": "env BUILD_ENV=local vite build --watch --emptyOutDir", "dev": "vite" }, "dependencies": { @@ -23,6 +23,7 @@ "devDependencies": { "@eslint/js": "^9.13.0", "@stylistic/eslint-plugin": "^2.9.0", + "@stylistic/eslint-plugin-ts": "^2.12.1", "@types/node": "^22.7.9", "@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue-jsx": "^4.0.1", @@ -31,7 +32,6 @@ "eslint-plugin-tailwindcss": "^3.17.5", "eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-vue": "^9.29.1", - "fast-glob": "^3.3.2", "globals": "^15.11.0", "neostandard": "^0.11.7", "postcss": "^8.4.47", diff --git a/client/src/App.tsx b/client/src/App.tsx index f95188c..6d6015a 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,21 +1,16 @@ import { VueQueryPlugin } from '@tanstack/vue-query' -import { computed, createApp, defineComponent, onMounted, ref } from 'vue' +import { createApp, defineComponent } from 'vue' import { createRouter, createWebHistory, RouterView } from 'vue-router' -import { createProviderGroup, type GlazeWmOutput } from 'zebar' -import { LiveLogs, MonitorGrid, TimeDate, WeatherLocation, WorkspaceGrid } from '~/components' +import { GlazeGrids, LiveLogs, TimeDate } from '~/components' import './index.css' -declare global { - interface Window { - __TAURI_INTERNALS__?: unknown - } -} - const App = defineComponent({ name: 'App', setup () { return () => ( - + <> + {import.meta.env.VITE_ZEBAR ? : } + ) }, }) @@ -23,40 +18,17 @@ const App = defineComponent({ const Zebar = defineComponent({ name: 'Zebar', setup () { - const glazewm = ref(null) - - const allMonitors = computed(() => glazewm.value?.allMonitors ?? []) - - const currentMonitor = computed(() => glazewm.value?.currentMonitor ?? null) - const monitorWorkspaces = computed(() => currentMonitor.value?.children ?? []) - - onMounted(() => { - if (window.__TAURI_INTERNALS__) { - const providers = createProviderGroup({ - glazewm: { type: 'glazewm' }, - }) - - glazewm.value = providers.outputMap.glazewm - - providers.onOutput(() => { - glazewm.value = providers.outputMap.glazewm - console.log('glazewm', glazewm.value) - }) - } - }) - return () => (
- + {/* - - + /> */} +
) }, @@ -81,6 +53,7 @@ const routes = [{ setup () { return () => (
+
) diff --git a/client/src/components.ts b/client/src/components.ts new file mode 100644 index 0000000..1528bd3 --- /dev/null +++ b/client/src/components.ts @@ -0,0 +1,6 @@ +export { default as GlazeGrids } from './components/GlazeGrids' +export { default as LiveLogs } from './components/LiveLogs' +export { default as MonitorGrid } from './components/MonitorGrid' +export { default as TimeDate } from './components/TimeDate' +export { default as WeatherLocation } from './components/WeatherLocation' +export { default as WorkspaceGrid } from './components/WorkspaceGrid' diff --git a/client/src/components/GlazeGrids.tsx b/client/src/components/GlazeGrids.tsx new file mode 100644 index 0000000..1c74fc5 --- /dev/null +++ b/client/src/components/GlazeGrids.tsx @@ -0,0 +1,76 @@ +import { useQuery } from '@tanstack/vue-query' +import { defineComponent, onMounted } from 'vue' +import { MonitorGrid, WorkspaceGrid } from '~/components' +import { useLogStream } from '~/hooks' +import { useGlazeWM } from '~/hooks/useGlazeWM' +import { logStreamQueries } from '~/io/queries/logStream.queries' + +export default defineComponent({ + name: 'GlazeGrids', + setup () { + const { allMonitors, monitorWorkspaces } = useGlazeWM() + + const { + data: logs, + error: logsError, + refetch: logStreamConnect, + isLoading: isLoadingLogs, + } = useLogStream() + + // const dirtyKey = computed(() => { + // return JSON.stringify({ + // allMonitors: allMonitors.value, + // monitorWorkspaces: monitorWorkspaces.value, + // }) + // }) + + const { data, error, refetch, isLoading } = useQuery({ + enabled: false, + queryKey: ['initConfig'], + queryFn: () => { + return logStreamQueries.sendLog({ + category: ['glaze'], + level: 'info', + message: ['Initialize glaze configuration'], + properties: { + setGlazeConfig: true, + allMonitors: allMonitors.value, + monitorWorkspaces: monitorWorkspaces.value, + }, + }) + }, + }) + + const { initialize } = useGlazeWM({ + initConfig: refetch, + }) + + onMounted(() => { + // LogStream connection is long lived; it won't resolve unless it closes + // so we don't need to await it + logStreamConnect() + + // Initialize the GlazeWM state if we're in a Zebar context + initialize() + }) + + const Grids = () => ( + <> + + + + ) + + return () => ( +
+
+ {logsError.value ?
Error: {logsError.value.message}
: null} + {error.value ?
Error: {error.value.message}
: null} +
+
+ {isLoading.value ?
Loading...
: } +
+
+ ) + }, +}) diff --git a/client/src/components/LiveLogs.tsx b/client/src/components/LiveLogs.tsx index b03f0fb..0c7cfb3 100644 --- a/client/src/components/LiveLogs.tsx +++ b/client/src/components/LiveLogs.tsx @@ -1,8 +1,10 @@ -import { useQuery, useQueryClient } from '@tanstack/vue-query' +// import { useQuery, useQueryClient } from '@tanstack/vue-query' +import { useQuery } from '@tanstack/vue-query' import { cva } from 'class-variance-authority' import { computed, defineComponent, onMounted, ref } from 'vue' -import { logStreamQueries } from '~/io/queries' -import { type MessageSchema } from '~/io/queries/logStreamQueries' +// import { type LogStreamMessageSchema } from '$schemas' +import { useLogStream } from '~/hooks' +import { logStreamQueries } from '~/io/queries/logStream.queries' const statusVariants = cva([ 'rounded px-2 text-black ', @@ -62,23 +64,32 @@ const FormatLogLine = defineComponent({ export default defineComponent({ name: 'LiveLogs', setup () { - const queryClient = useQueryClient() + // const queryClient = useQueryClient() + // + // const { + // data: logs, + // error: logsError, + // refetch: refetchLogs, + // isLoading: isLoadingLogs, + // } = useQuery({ + // enabled: false, + // queryKey: ['logs'], + // retry: false, + // queryFn: () => ( + // logStreamQueries.connectLogs((data) => { + // queryClient.setQueryData(['logs'], (oldData: LogStreamMessageSchema[] = []) => { + // return [...oldData, data] + // }) + // }) + // ), + // }) const { data: logs, error: logsError, refetch: refetchLogs, isLoading: isLoadingLogs, - } = useQuery({ - enabled: false, - queryKey: ['logs'], - retry: false, - queryFn: () => logStreamQueries.connectLogs((data) => { - queryClient.setQueryData(['logs'], (oldData: MessageSchema[] = []) => { - return [...oldData, data] - }) - }), - }) + } = useLogStream() const logText = ref('') diff --git a/client/src/components/WeatherLocation.tsx b/client/src/components/WeatherLocation.tsx index 50b9a9f..fc98a7a 100644 --- a/client/src/components/WeatherLocation.tsx +++ b/client/src/components/WeatherLocation.tsx @@ -2,7 +2,7 @@ import { useQuery, useQueryClient } from '@tanstack/vue-query' import { useNow } from '@vueuse/core' import { computed, defineComponent, onMounted } from 'vue' import { useEmoji } from '~/hooks/useEmoji' -import { weatherQueries } from '~/io/queries/index' +import { weatherQueries } from '~/io/queries/weather.queries' import { getWeatherEmoji } from '~/utils/emoji.util' export default defineComponent({ diff --git a/client/src/components/index.ts b/client/src/components/index.ts deleted file mode 100644 index 7a86a7e..0000000 --- a/client/src/components/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { default as LiveLogs } from './LiveLogs' -export { default as MonitorGrid } from './MonitorGrid' -export { default as TimeDate } from './TimeDate' -export { default as WeatherLocation } from './WeatherLocation' -export { default as WorkspaceGrid } from './WorkspaceGrid' diff --git a/client/src/hooks/index.ts b/client/src/hooks/index.ts index a844457..8f0aaf7 100644 --- a/client/src/hooks/index.ts +++ b/client/src/hooks/index.ts @@ -1 +1,2 @@ export { useEmoji } from './useEmoji' +export { useLogStream } from './useLogStream' diff --git a/client/src/hooks/useGlazeWM.ts b/client/src/hooks/useGlazeWM.ts new file mode 100644 index 0000000..667fe0b --- /dev/null +++ b/client/src/hooks/useGlazeWM.ts @@ -0,0 +1,86 @@ +import { filter, map, Subject } from 'rxjs' +import { computed, ref } from 'vue' +import { createProviderGroup, type GlazeWmOutput } from 'zebar' + +type WMPayload = GlazeWmOutput | null + +type WMEvent = { + type: string + payload: WMPayload +} + +const wm = ref(null) +export const wmEventBus = new Subject() + +wmEventBus.pipe( + filter(event => event.type === 'wm-update'), + map(event => event.payload), +).subscribe((payload) => { + wm.value = payload +}) + +type UseGlazeWMOptions = { + initConfig: () => void +} + +export const useGlazeWM = (options?: UseGlazeWMOptions) => { + const allMonitors = computed(() => wm.value?.allMonitors ?? []) + const currentMonitor = computed(() => wm.value?.currentMonitor ?? null) + const monitorWorkspaces = computed(() => currentMonitor.value?.children ?? []) + + const currentMonitorIndex = computed(() => { + return allMonitors.value.findIndex(monitor => monitor.id === currentMonitor.value?.id) + }) + + const initialize = () => { + if (window.__TAURI_INTERNALS__) { + const providers = createProviderGroup({ + glazewm: { type: 'glazewm' }, + }) + + const glazeNext = () => { + console.log('glaze next should fire when changing monitors') + wmEventBus.next({ + type: 'wm-update', + payload: providers.outputMap.glazewm, + }) + // options?.initConfig() + + console.log(currentMonitor.value, currentMonitorIndex.value) + } + + // When requested, tell the server the current monitor configuration + wmEventBus.subscribe((event) => { + // Todo: Rename to wm-request-init + // Only send events from Zebar on the second monitor (I usually have my + // Zebar dev tools here) + // if (event.type === 'wm-init' && currentMonitorIndex.value === 2) { + if (event.type === 'wm-init' && currentMonitorIndex.value === 2) { + options?.initConfig() + } + // if (event.type === 'wm-update') { // Todo: Rename to wm-request-init + // options?.initConfig() + // } + }) + + // Initialize the wm ref + glazeNext() + + // Update the wm ref on output changes + providers.onOutput(glazeNext) + } + } + + return { + allMonitors, + currentMonitor, + monitorWorkspaces, + initialize, + } +} + +declare global { + interface Window { + __TAURI_INTERNALS__?: unknown + } +} diff --git a/client/src/hooks/useLogStream.ts b/client/src/hooks/useLogStream.ts new file mode 100644 index 0000000..e60b547 --- /dev/null +++ b/client/src/hooks/useLogStream.ts @@ -0,0 +1,59 @@ +import { useQuery, useQueryClient } from '@tanstack/vue-query' +import { type LogStreamMessageSchema } from '$schemas' +import { wmEventBus } from '~/hooks/useGlazeWM' +import { logStreamQueries } from '~/io/queries/logStream.queries' + +export const useLogStream = () => { + const queryClient = useQueryClient() + + const { data, error, refetch, isLoading } = useQuery({ + enabled: false, + queryKey: ['logStream'], + retry: false, + queryFn: () => ( + logStreamQueries.connectLogs((data: LogStreamMessageSchema) => { + console.log('data:', data.category) + + // If the log message requests initialization, send an init event to + // the event bus + + if (data.category.includes('glaze')) { + if (data.properties?.requestInit) { + console.log('hey buddy, the server aint got no glaze config', data.properties) + + wmEventBus.next({ + type: 'wm-init', // Todo: Rename to wm-request-init + payload: null, + }) + } + + if (data.properties?.requestUpdate) { + console.log('xxx', data.properties) + + wmEventBus.next({ + type: 'wm-update', + payload: data.properties.glazeConfig, + }) + } + } + + queryClient.setQueryData(['logStream'], (oldData: LogStreamMessageSchema[] = []) => { + return [...oldData, data].slice(-20) // Keep the last 20 logs + + // Combine the new data with the old data + // return from([...oldData, data]).pipe( + // takeLast(20), + // toArray(), + // ) + }) + }) + ), + }) + + return { + data, + error, + refetch, + isLoading, + } +} diff --git a/client/src/io/operators/stream.operators.ts b/client/src/io/operators/stream.operators.ts index da44934..a775029 100644 --- a/client/src/io/operators/stream.operators.ts +++ b/client/src/io/operators/stream.operators.ts @@ -45,6 +45,13 @@ type LoggingContext = { } export function logging (context: T): T { - console.log(JSON.stringify(context.data, null, 2)) + // console.log(JSON.stringify(context.data, null, 2)) + // console.log(JSON.stringify(context.data)) + console.log( + context.data?.category, + context.data?.level, + context.data?.message, + context.data?.properties, + ) return context } diff --git a/client/src/io/queries/index.ts b/client/src/io/queries/index.ts deleted file mode 100644 index 1f62e68..0000000 --- a/client/src/io/queries/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as logStreamQueries from './logStream.queries' -export * as weatherQueries from './weather.queries' diff --git a/client/src/io/queries/logStream.queries.ts b/client/src/io/queries/logStream.queries.ts index f01bc22..65ca021 100644 --- a/client/src/io/queries/logStream.queries.ts +++ b/client/src/io/queries/logStream.queries.ts @@ -1,10 +1,13 @@ -import { type LogRequestSchema, logResponseSchema, messageSchema } from '$schemas/logStream.schema' +import { + type LogStreamMessageSchema, + logStreamMessageSchema, + type LogStreamRequestSchema, + type LogStreamResponseSchema, + logStreamResponseSchema, +} from '$schemas' import { makeRequest } from '~/io/streams/fetch.streams' import { connect, disconnect } from '~/io/streams/sse.streams' -export type LogResponseSchema = z.infer -export type MessageSchema = z.infer - type NextFn = (data: unknown) => void let lastNext: NextFn | undefined @@ -14,12 +17,12 @@ export const connectLogs = async (next: NextFn) => { const data = await connect({ url: '/stream/logs', - messageSchema, + messageSchema: logStreamMessageSchema, next, error: (err) => { throw err }, }) - return [data] as Promise[] + return [data] as Promise[] } export const disconnectLogs = () => { @@ -29,23 +32,31 @@ export const disconnectLogs = () => { export const reconnectLogs = async (next?: NextFn) => { disconnectLogs() - if (!next && !lastNext) { + const nextFn = next ?? lastNext + if (!nextFn) { throw new Error('Next function is not defined') } - return connectLogs(next ?? lastNext!) + return connectLogs(nextFn) } -export const sendLog = async (params: LogRequestSchema) => { +export const sendLog = async (params: LogStreamRequestSchema) => { const data = await makeRequest({ method: 'POST', url: '/api/log', - responseSchema: logResponseSchema, + responseSchema: logStreamResponseSchema, body: JSON.stringify(params), headers: { 'Content-Type': 'application/json', }, }) - return data as Promise + return data as Promise +} + +export const logStreamQueries = { + connectLogs, + disconnectLogs, + reconnectLogs, + sendLog, } diff --git a/client/src/io/queries/weather.queries.ts b/client/src/io/queries/weather.queries.ts index e19c3a9..58b2990 100644 --- a/client/src/io/queries/weather.queries.ts +++ b/client/src/io/queries/weather.queries.ts @@ -1,21 +1,47 @@ -import { type WeatherRequest, type WeatherResponse, weatherResponseSchema } from '$schemas/weather.schema' -// import { type WeatherRequest, type WeatherResponse, weatherResponseSchema } from '../../../../server/modules/weather/weather.schema.ts' +import { z } from 'zod' import { makeRequest } from '~/io/streams/fetch.streams' +// +// +// + +export const RequestSchema = z.object({ + location: z.string({ + required_error: 'Location is required', + invalid_type_error: 'Location must be a string', + }), +}) + +export const ResponseSchema = z.object({ + condition: z.string(), + temp: z.string(), +}) + +export type Request = z.infer +export type Response = z.infer + +// +// +// + // Type casting with 'as Promise' is safe here because: // 1. The Zod schema (WeatherResponseSchema) validates the data at runtime // 2. If the API returns invalid data, Zod's parse() will throw regardless of the type cast // 3. The cast just helps TypeScript understand the shape of the data after Zod validates it -export const getWeather = async (params: WeatherRequest) => { +export const getWeather = async (params: weather.Request) => { const data = await makeRequest({ method: 'GET', url: '/api/weather', queryString: params, - responseSchema: weatherResponseSchema, + responseSchema: weather.ResponseSchema, headers: { 'Cache-Control': 'public, max-age=3600, immutable', }, }) - return data as Promise + return data as Promise +} + +export const weatherQueries = { + getWeather, } diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json index 0fe8e94..7d1dcac 100644 --- a/client/tsconfig.app.json +++ b/client/tsconfig.app.json @@ -31,7 +31,8 @@ /* Project paths */ "baseUrl": ".", "paths": { - "~/*": ["src/*"] + "~/*": ["src/*"], + "$schemas": ["../server/src/modules/schemas.ts"] }, }, "include": [ @@ -41,6 +42,7 @@ "src/**/*.tsx", "src/**/*.vue", "src/vite-env.d.ts", - "../server/**/*.ts" + "../server/src/modules/schema.ts", + "../server/src/modules/**/*.ts" ] } diff --git a/client/vite.config.ts b/client/vite.config.ts index b5f197f..d5956c3 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,8 +1,7 @@ -import path from 'node:path' +import fs from 'node:fs/promises' import { fileURLToPath, URL } from 'node:url' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' -import glob from 'fast-glob' import { defineConfig } from 'vite' import { viteSingleFile } from 'vite-plugin-singlefile' @@ -10,43 +9,44 @@ const outDir = process.env.BUILD_ENV === 'local' ? '/mnt/c/Users/dustin/.glzr/zebar/whaleybar-build' : undefined -export const createSchemaAliases = async () => { - const files = await glob('../server/modules/**/*.schema.ts') - - return files.reduce((aliases, filePath) => { - const aliasName = path.parse(filePath).name - const url = new URL(filePath, import.meta.url) - - aliases[`$schemas/${aliasName}`] = fileURLToPath(url) - - return aliases - }, {}) -} - -export default defineConfig(async () => { - const schemaAliases = await createSchemaAliases() - - return { - build: { - outDir, - sourcemap: !!process.env.DEV, // Ensure sourcemap is a boolean - }, - plugins: [ - vue(), - vueJsx(), - viteSingleFile(), - ], - resolve: { - alias: { - '~': fileURLToPath(new URL('./src', import.meta.url)), - $: fileURLToPath(new URL('..', import.meta.url)), - ...schemaAliases, +export default defineConfig({ + build: { + outDir, + sourcemap: Boolean(process.env.DEV), + }, + plugins: [ + vue(), + vueJsx(), + viteSingleFile(), + // Plugin to emit whaleybar.zebar.json alongside the bundled app. + // This config file needs to be separate since it's loaded at runtime + // by the Zebar platform to configure the app instance and its features. + { + name: 'zebar-config', + generateBundle: async (_options, bundle) => { + bundle['whaleybar.zebar.json'] = { + type: 'asset', + fileName: 'whaleybar.zebar.json', + name: 'whaleybar.zebar.json', + needsCodeReference: false, + source: await fs.readFile('./whaleybar.zebar.json', 'utf-8'), + names: [], + originalFileName: 'whaleybar.zebar.json', + originalFileNames: ['whaleybar.zebar.json'], + } }, - // Ensure the same instance of zod is used everywhere it's imported - dedupe: ['zod'], }, - server: { - port: 4200, + ], + resolve: { + alias: { + '~': fileURLToPath(new URL('./src', import.meta.url)), + $: fileURLToPath(new URL('..', import.meta.url)), + $schemas: fileURLToPath(new URL('../server/src/modules/schemas.ts', import.meta.url)), }, - } + // Ensure the same instance of zod is used everywhere it's imported + dedupe: ['zod'], + }, + server: { + port: 4200, + }, }) diff --git a/client/whaleybar.zebar.json b/client/whaleybar.zebar.json new file mode 100644 index 0000000..afa40ad --- /dev/null +++ b/client/whaleybar.zebar.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://github.com/glzr-io/zebar/raw/v2.1.0/resources/widget-schema.json", + "htmlPath": "./index.html", + "zOrder": "normal", + "shownInTaskbar": false, + "focused": false, + "resizable": false, + "transparent": true, + "defaultPlacements": [ + { + "anchor": "bottom_left", + "offsetX": "0px", + "offsetY": "0px", + "width": "100%", + "height": "72px", + "monitorSelection": { + "type": "all" + } + } + ] +} diff --git a/deno.json b/deno.json deleted file mode 100644 index 5d9111f..0000000 --- a/deno.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "tasks": { - "meta": "deno run --allow-read --allow-write scripts/build-meta.ts", - "meta:watch": "deno run --allow-read --allow-write --watch scripts/build-meta.ts" - }, - "fmt": { - "lineWidth": 240, - "semiColons": false, - "singleQuote": true - }, - "imports": { - "@std/fs": "jsr:@std/fs@^1.0.7" - } -} diff --git a/deno.lock b/deno.lock deleted file mode 100644 index 72a418e..0000000 --- a/deno.lock +++ /dev/null @@ -1,23 +0,0 @@ -{ - "version": "4", - "specifiers": { - "jsr:@std/fs@^1.0.7": "1.0.7", - "jsr:@std/path@^1.0.8": "1.0.8" - }, - "jsr": { - "@std/fs@1.0.7": { - "integrity": "95ac7326e0e59089c979706e17bb28db03b5fb41c5a37c9c556788198b0662d6", - "dependencies": [ - "jsr:@std/path" - ] - }, - "@std/path@1.0.8": { - "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" - } - }, - "workspace": { - "dependencies": [ - "jsr:@std/fs@^1.0.7" - ] - } -} diff --git a/scripts/build-meta.ts b/scripts/build-meta.ts deleted file mode 100644 index 226955c..0000000 --- a/scripts/build-meta.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { walk } from '@std/fs' - -const SKIP_PATTERNS: RegExp[] = [ - /node_modules/, - /dist/, - /_trash/, - /\.git/, - /\.claude/, -] - -interface MetaTree { - [key: string]: MetaTree | null -} - -const getMetaPaths = async (root: string) => { - const entries = walk(root, { skip: SKIP_PATTERNS, includeDirs: false }) - const paths: string[] = [] - - for await (const entry of entries) { - paths.push(entry.path) - } - - return paths -} - -const buildMetaTree = (paths: string[]): MetaTree => paths.reduce((meta, path) => { - return path.split('/').reduce((node, part, i, parts) => { - if (i === parts.length - 1) return { ...node, [part]: null } - return { ...node, [part]: node[part] || {} } - }, meta as MetaTree) -}, {}) - -const writeMetaFile = async (meta: MetaTree) => { - await Deno.mkdir('.claude', { recursive: true }) - await Deno.writeTextFile( - '.claude/project-meta.json', - JSON.stringify(meta, null, 2) - ) -} - -if (import.meta.main) { - const root = Deno.cwd() - const paths = await getMetaPaths(root) - const meta = buildMetaTree(paths) - await writeMetaFile(meta) -} \ No newline at end of file diff --git a/server/index.ts b/server/index.ts index 4bf1c8a..d61f098 100644 --- a/server/index.ts +++ b/server/index.ts @@ -10,7 +10,7 @@ const logger = getLogger(['app']) app.use(middleware.errorMiddleware) app.use(middleware.loggerMiddleware) -app.use(middleware.rateLimitMiddleware()) +// app.use(middleware.rateLimitMiddleware()) app.use(middleware.corsMiddleware) app.use(middleware.staticFileMiddleware) diff --git a/server/src/logging.ts b/server/src/logging.ts index 795eb52..7d69394 100644 --- a/server/src/logging.ts +++ b/server/src/logging.ts @@ -6,16 +6,24 @@ await configure({ console: getConsoleSink(), logStream: getLogStreamSink(), }, - filters: {}, + filters: { + debugAndAbove: 'debug', + warningAndAbove: 'warning', + }, loggers: [ { category: ['app'], - level: 'debug', + filters: ['debugAndAbove'], + sinks: ['console', 'logStream'], + }, + { + category: ['glaze'], + filters: ['debugAndAbove'], sinks: ['console', 'logStream'], }, { category: ['logtape', 'meta'], - level: 'warning', + filters: ['warningAndAbove'], sinks: ['console'], }, ], diff --git a/server/src/modules/logStream/logStream.controller.ts b/server/src/modules/logStream/logStream.controller.ts index 4d18f8c..dd50744 100644 --- a/server/src/modules/logStream/logStream.controller.ts +++ b/server/src/modules/logStream/logStream.controller.ts @@ -1,13 +1,10 @@ -import { getLogger } from '@logtape/logtape' import { Context, ServerSentEvent, Status } from '@oak/oak' -import { createSaltedHash } from '../../utils/hash.util.ts' -import { logRequestSchema } from './logStream.schema.ts' - -const logger = getLogger(['app']) +import { getLogger } from '@logtape/logtape' +import { incomingEvents, outgoingEvents } from './logStream.model.ts' +import { logStreamRequestSchema } from './logStream.schema.ts' +import { createSaltedHash } from './logStream.util.ts' -// type LogStreamDeps = { -// // Add dependencies here -// } +const log = getLogger(['app']) type Connection = { target: EventTarget @@ -17,7 +14,7 @@ type Connection = { // Maps anonymized client IDs to their SSE connections const activeConnections = new Map() -export const createLogStreamController = (/* deps: LogStreamDeps */) => ({ +export const createLogStreamController = () => ({ connect: async (ctx: Context) => { const { request: req } = ctx ctx.response.status = 200 @@ -26,7 +23,7 @@ export const createLogStreamController = (/* deps: LogStreamDeps */) => ({ const existing = activeConnections.get(clientId) if (existing) { - logger.info(`Closing existing connection for client: ${clientId}`) + log.info(`Closing existing connection for client: ${clientId}`) clearInterval(existing.interval) activeConnections.delete(clientId) } @@ -37,11 +34,18 @@ export const createLogStreamController = (/* deps: LogStreamDeps */) => ({ // Prevents connection timeout by sending periodic heartbeats const interval = setInterval(() => { target.dispatchEvent(heartbeat) - logger.info('heartbeat') + log.info('heartbeat', { heartbeat: true }) }, 30000) activeConnections.set(clientId, { target, interval }) - logger.info(`New connection established for client: ${clientId}`) + log.info(`New connection established for client: ${clientId}`, { + newConnection: true, + }) + + // Outgoing events are broadcasted to the client + outgoingEvents.subscribe((data) => { + target.dispatchEvent(new ServerSentEvent('message', { data })) + }) // Ensures connection resources are properly cleaned up on disconnect target.addEventListener('close', () => { @@ -50,7 +54,7 @@ export const createLogStreamController = (/* deps: LogStreamDeps */) => ({ if (connection) { clearInterval(connection.interval) activeConnections.delete(clientId) - logger.info(`Connection closed for client: ${clientId}`) + log.info(`Connection closed for client: ${clientId}`) } }) }, @@ -58,9 +62,11 @@ export const createLogStreamController = (/* deps: LogStreamDeps */) => ({ const { response: res, request: req } = ctx const body = await req.body.json() - const validated = logRequestSchema.parse(body) + const validated = logStreamRequestSchema.parse(body) - logger[validated.level](validated.message.join(', ')) + // Emit the validated message to the logger + log[validated.level](validated.message.join(', ')) + incomingEvents.next(validated) res.status = Status.OK res.body = { status: 'success' } @@ -68,14 +74,4 @@ export const createLogStreamController = (/* deps: LogStreamDeps */) => ({ // Return the response so we can test the controller return res }, - broadcast: (data: unknown) => { - const event = new ServerSentEvent('message', { data }) - - for (const { target } of activeConnections.values()) { - target.dispatchEvent(event) - } - - // Return the data so we can test the controller - return data - }, }) diff --git a/server/src/modules/logStream/logStream.model.ts b/server/src/modules/logStream/logStream.model.ts new file mode 100644 index 0000000..532fd58 --- /dev/null +++ b/server/src/modules/logStream/logStream.model.ts @@ -0,0 +1,59 @@ +import { getLogger } from '@logtape/logtape' +import { Subject } from 'rxjs' +import { type LogStreamMessageSchema } from './logStream.schema.ts' + +const log = getLogger(['app']) +const glazeLog = getLogger(['glaze']) + +type Message = LogStreamMessageSchema + +export const incomingEvents = new Subject() +export const outgoingEvents = new Subject() + +// global state that holds onto the initialize monitor state +const glazeConfig = { + isInitialized: false, + allMonitors: [], + monitorWorkspaces: [], +} + +setInterval(() => { + console.log('glazeConfig', JSON.stringify(glazeConfig, null, 2)) +}, 5000) + +export const getGlazeConfig = () => glazeConfig + +incomingEvents.subscribe((event) => { + console.log('!!!!! INCOMING EVENT !!!!!!!', event) + + // if (event.properties?.requestInit || event.properties?.requestUpdate) { + if (event.category.includes('glaze') && event.properties.setGlazeConfig) { + log.info('Initializing glaze configuration', { + connectionId: event.properties.connectionId, + glazeConfig: event.properties.glazeConfig, + }) + + // Todo: Fix types and narrow down the properties that are sent from the client + glazeConfig.isInitialized = true + glazeConfig.allMonitors = event.properties.allMonitors + glazeConfig.monitorWorkspaces = event.properties.monitorWorkspaces + } +}) + +outgoingEvents.subscribe((event) => { + // Send Glaze config right when client establishes connection + if (event.properties?.newConnection) { + if (glazeConfig.isInitialized) { + glazeLog.info('Sending glaze configuration to new client', { + requestUpdate: true, + connectionId: event.properties.connectionId, + glazeConfig, + }) + } else { + glazeLog.info('Glaze configuration not initialized yet', { + requestInit: true, + connectionId: event.properties.connectionId, + }) + } + } +}) diff --git a/server/src/modules/logStream/logStream.schema.ts b/server/src/modules/logStream/logStream.schema.ts index 8249ab7..827b756 100644 --- a/server/src/modules/logStream/logStream.schema.ts +++ b/server/src/modules/logStream/logStream.schema.ts @@ -1,26 +1,28 @@ import { z } from 'zod' -export const logRequestSchema = z.object({ +// Todo: Log messages should be the LogRecord type from @logtape/logtape + +export const logStreamRequestSchema = z.object({ category: z.array(z.string()), level: z.enum(['debug', 'info', 'warn', 'error', 'fatal']), message: z.array(z.string()), - properties: z.unknown(), + properties: z.record(z.string(), z.unknown()), }) -export const logResponseSchema = z.object({ +export const logStreamResponseSchema = z.object({ status: z.enum(['success', 'error']), }) -export const messageSchema = z.object({ +export const logStreamMessageSchema = z.object({ category: z.array(z.string()), level: z.enum(['debug', 'info', 'warn', 'error', 'fatal']), timestamp: z.number(), message: z.array(z.string()), rawMessage: z.string(), - properties: z.unknown(), + properties: z.record(z.string(), z.unknown()), }) -export type LogRequestSchema = z.infer -export type LogResponseSchema = z.infer +export type LogStreamRequestSchema = z.infer +export type LogStreamResponseSchema = z.infer -export type MessageSchema = z.infer +export type LogStreamMessageSchema = z.infer diff --git a/server/src/modules/logStream/logStream.sink.ts b/server/src/modules/logStream/logStream.sink.ts index e751c56..0f85a32 100644 --- a/server/src/modules/logStream/logStream.sink.ts +++ b/server/src/modules/logStream/logStream.sink.ts @@ -1,8 +1,6 @@ import { type LogRecord } from '@logtape/logtape' -import { createLogStreamController } from './logStream.controller.ts' - -const logStreamController = createLogStreamController() +import { outgoingEvents } from './logStream.model.ts' export const getLogStreamSink = () => (record: LogRecord) => { - logStreamController.broadcast(record) + outgoingEvents.next(record) } diff --git a/server/src/utils/hash.util.ts b/server/src/modules/logStream/logStream.util.ts similarity index 100% rename from server/src/utils/hash.util.ts rename to server/src/modules/logStream/logStream.util.ts diff --git a/server/src/modules/models.ts b/server/src/modules/models.ts index e6a8fa8..84185ed 100644 --- a/server/src/modules/models.ts +++ b/server/src/modules/models.ts @@ -1 +1,2 @@ export * as weatherModel from './weather/weather.model.ts' +export * as logStream from './logStream/logStream.model.ts' // Naming? diff --git a/server/src/modules/schemas.ts b/server/src/modules/schemas.ts new file mode 100644 index 0000000..74db473 --- /dev/null +++ b/server/src/modules/schemas.ts @@ -0,0 +1,2 @@ +export * from './logStream/logStream.schema.ts' +export * from './weather/weather.schema.ts' diff --git a/whaleybar.code-workspace b/whaleybar.code-workspace index d683c84..17dd7b6 100644 --- a/whaleybar.code-workspace +++ b/whaleybar.code-workspace @@ -1,16 +1,13 @@ { "folders": [ { - "name": "client", "path": "client" }, { - "name": "server", "path": "server" }, { - "name": "whaleybar", - "path": ".", + "path": "." }, { "path": "../../../mnt/c/Users/dustin/.glzr/zebar/whaleybar-bootstrap" @@ -38,13 +35,14 @@ "tabler", "tailwindcss", "tanstack", + "TAURI", "tmuxinator", "tseslint", "unmarshal", "unplugin", "vueuse", "whaleybar", - "Zebar" + "zebar" ] } }