@typed-assistant/builder 0.0.56 → 0.0.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typed-assistant/builder",
3
- "version": "0.0.56",
3
+ "version": "0.0.58",
4
4
  "exports": {
5
5
  "./appProcess": "./src/appProcess.tsx",
6
6
  "./bunInstall": "./src/bunInstall.tsx",
@@ -11,6 +11,9 @@ import { getAddonInfo } from "./getAddonInfo"
11
11
  import { addKillListener, killSubprocess } from "./killProcess"
12
12
  import { restartAddon } from "./restartAddon"
13
13
  import { levels } from "@typed-assistant/logger/levels"
14
+ import { getSupervisorAPI } from "@typed-assistant/utils/getHassAPI"
15
+ import { withErrorHandling } from "@typed-assistant/utils/withErrorHandling"
16
+ import { ONE_SECOND } from "@typed-assistant/utils/durations"
14
17
 
15
18
  const indexHtmlFilePath = `${import.meta.dir}/webserver/index.html` as const
16
19
  const cssFile = `${import.meta.dir}/webserver/input.css` as const
@@ -55,6 +58,46 @@ const subscribers = new Map<string, (message: string) => void>()
55
58
  const logSubscribers = new Map<string, () => void>()
56
59
 
57
60
  let lastMessage = ""
61
+ let stats = {
62
+ cpu_percent: null as number | null,
63
+ memory_usage: null as number | null,
64
+ memory_limit: null as number | null,
65
+ memory_percent: null as number | null,
66
+ max_memory_usage: 0,
67
+ }
68
+ const getStats = async () => {
69
+ const { data, error } = await withErrorHandling(
70
+ getSupervisorAPI<{
71
+ error?: never
72
+ cpu_percent: number
73
+ memory_usage: number
74
+ memory_limit: number
75
+ memory_percent: number
76
+ max_memory_usage: number
77
+ }>,
78
+ )("/addons/self/stats")
79
+
80
+ if (error) {
81
+ logger.error(
82
+ { additionalDetails: error.message, emoji: "❌" },
83
+ "Error getting stats",
84
+ )
85
+ } else {
86
+ stats = {
87
+ ...data,
88
+ max_memory_usage:
89
+ data.memory_usage > stats.max_memory_usage
90
+ ? data.memory_usage
91
+ : stats.max_memory_usage,
92
+ }
93
+ logger.info(
94
+ { additionalDetails: JSON.stringify(stats, null, 2), emoji: "📊" },
95
+ "Stats updated",
96
+ )
97
+ }
98
+
99
+ setTimeout(getStats, 30 * ONE_SECOND)
100
+ }
58
101
 
59
102
  export const startWebappServer = async ({
60
103
  basePath,
@@ -141,6 +184,7 @@ export const startWebappServer = async ({
141
184
  translations: "HIDDEN",
142
185
  }
143
186
  })
187
+ .get("/stats", async () => stats)
144
188
  .get(
145
189
  "/log.txt",
146
190
  async ({ query }) => {
@@ -164,17 +208,6 @@ export const startWebappServer = async ({
164
208
  t.Literal("fatal"),
165
209
  ]),
166
210
  }),
167
- response: t.Object({
168
- logs: t.Array(
169
- t.Object({
170
- level: t.Number(),
171
- time: t.Number(),
172
- pid: t.Number(),
173
- hostname: t.String(),
174
- msg: t.String(),
175
- }),
176
- ),
177
- }),
178
211
  async open(ws) {
179
212
  ws.send(await getLogsFromFile(ws.data.query.level, ws.data.query.limit))
180
213
  logSubscribers.set(ws.id, async () => {
@@ -227,6 +260,8 @@ export const startWebappServer = async ({
227
260
  }
228
261
  })
229
262
 
263
+ getStats()
264
+
230
265
  addKillListener(async () => {
231
266
  watcher.close()
232
267
  await server.stop()
@@ -241,7 +276,9 @@ export const startWebappServer = async ({
241
276
  ? { value: undefined }
242
277
  : await stderrReader.read()
243
278
 
244
- const decodedString = decoder.decode(value ?? stderrValue)
279
+ const decodedString = stderrValue
280
+ ? decoder.decode(stderrValue)
281
+ : decoder.decode(value)
245
282
  const convertedMessage = convert.toHtml(decodedString)
246
283
  if (convertedMessage !== "") {
247
284
  lastMessage = convertedMessage
@@ -1,5 +1,11 @@
1
1
  import { Terminal } from "./Terminal"
2
2
  import { Logs } from "./Logs"
3
+ import { useEffect, useState } from "react"
4
+ import { app } from "./api"
5
+ import { Await } from "ts-toolbelt/out/Any/Await"
6
+ import { AppSection } from "./AppSection"
7
+ import { set } from "zod"
8
+ import { ONE_SECOND } from "@typed-assistant/utils/durations"
3
9
 
4
10
  const basePath = process.env.BASE_PATH ?? ""
5
11
 
@@ -10,10 +16,77 @@ const App = () => {
10
16
  <Terminal />
11
17
  </div>
12
18
  <div className="col-span-1">
19
+ <Stats />
13
20
  <Logs basePath={basePath} />
14
21
  </div>
15
22
  </div>
16
23
  )
17
24
  }
18
25
 
26
+ const Stats = () => {
27
+ const [counter, setCounter] = useState(0)
28
+ const [error, setError] = useState<string | null>(null)
29
+ const [lastUpdated, setLastUpdated] = useState<number | null>(null)
30
+ const [stats, setStats] =
31
+ useState<Awaited<ReturnType<typeof app.stats.get>>["data"]>(null)
32
+
33
+ useEffect(() => {
34
+ app.stats.get().then((stats) => {
35
+ setLastUpdated(Date.now())
36
+ if (stats.error) {
37
+ setError(stats.error.message)
38
+ return
39
+ }
40
+ setStats(stats.data)
41
+
42
+ const timeout = setTimeout(() => {
43
+ setCounter((c) => c + 1)
44
+ }, 30 * ONE_SECOND)
45
+ return () => clearTimeout(timeout)
46
+ })
47
+ }, [counter])
48
+
49
+ return (
50
+ <AppSection className="pb-0" fullHeight={false} scrollable={false}>
51
+ {error ? (
52
+ <>Error: {error}</>
53
+ ) : stats ? (
54
+ <div
55
+ title={`Last updated: ${lastUpdated ? new Date(lastUpdated).toLocaleTimeString() : "Never"}`}
56
+ >
57
+ <div className="mb-3">
58
+ <div className="mb-1">
59
+ Memory:{" "}
60
+ {stats.memory_usage
61
+ ? `${bytesToMegaBytes(stats.memory_usage ?? 0)} / ${bytesToMegaBytes(stats.memory_limit ?? 0)}MB`
62
+ : "Loading..."}
63
+ </div>
64
+ <ProgressBar value={stats.memory_percent ?? 0} />
65
+ </div>
66
+ <div className="mb-1">
67
+ CPU: {stats.cpu_percent ? `${stats.cpu_percent}%` : "Loading..."}
68
+ </div>
69
+ <ProgressBar value={stats.cpu_percent ?? 0} />
70
+ </div>
71
+ ) : (
72
+ <div>Loading...</div>
73
+ )}
74
+ </AppSection>
75
+ )
76
+ }
77
+
78
+ const ProgressBar = ({ value }: { value: number }) => {
79
+ return (
80
+ <div className="relative w-full h-2 bg-slate-800 rounded-md overflow-hidden">
81
+ <div
82
+ className="absolute h-full bg-slate-600"
83
+ style={{ width: `${value}%` }}
84
+ ></div>
85
+ </div>
86
+ )
87
+ }
88
+
89
+ const bytesToMegaBytes = (bytes: number) =>
90
+ Math.round((bytes / 1024 / 1024) * 100) / 100
91
+
19
92
  export default App
@@ -1,16 +1,34 @@
1
+ import type { ReactNode } from "react"
2
+ import { twMerge } from "tailwind-merge"
3
+
1
4
  export const AppSection = ({
2
- renderHeader,
3
5
  children,
6
+ className,
7
+ fullHeight = true,
8
+ renderHeader,
9
+ scrollable = true,
4
10
  }: {
5
- renderHeader: () => JSX.Element
6
- children: JSX.Element
11
+ children: ReactNode
12
+ className?: string
13
+ fullHeight?: boolean
14
+ renderHeader?: () => JSX.Element
15
+ scrollable?: boolean
7
16
  }) => {
8
17
  return (
9
- <div className="p-4 text-xs h-full max-h-dvh w-dvw md:w-auto overflow-x-auto flex flex-col">
10
- <div className="flex flex-wrap gap-4 mb-4 items-center justify-between">
11
- {renderHeader()}
12
- </div>
13
- <div className="overflow-x-auto">{children}</div>
18
+ <div
19
+ className={twMerge(
20
+ "p-4 text-xs max-h-dvh w-dvw md:w-auto flex flex-col",
21
+ fullHeight ? "h-full" : "",
22
+ scrollable ? "overflow-x-auto" : "",
23
+ className ?? "",
24
+ )}
25
+ >
26
+ {renderHeader ? (
27
+ <div className="flex flex-wrap gap-4 mb-4 items-center justify-between">
28
+ {renderHeader()}
29
+ </div>
30
+ ) : null}
31
+ <div className={scrollable ? "overflow-x-auto" : ""}>{children}</div>
14
32
  </div>
15
33
  )
16
34
  }
@@ -1,13 +1,12 @@
1
+ import type { LogSchema } from "@typed-assistant/logger"
2
+ import { levels } from "@typed-assistant/logger/levels"
3
+ import { getPrettyTimestamp } from "@typed-assistant/utils/getPrettyTimestamp"
1
4
  import { useCallback, useState } from "react"
2
- import { z } from "zod"
3
5
  import { AppSection } from "./AppSection"
4
6
  import { WSIndicator } from "./WSIndicator"
5
7
  import { app } from "./api"
6
- import { useWS } from "./useWS"
7
- import { getPrettyTimestamp } from "@typed-assistant/utils/getPrettyTimestamp"
8
- import { levels } from "@typed-assistant/logger/levels"
9
- import type { LogSchema } from "@typed-assistant/logger"
10
8
  import { buttonStyle } from "./styles"
9
+ import { useWS } from "./useWS"
11
10
 
12
11
  export const Logs = ({ basePath }: { basePath: string }) => {
13
12
  const [limit, setLimit] = useState(200)