@typed-assistant/builder 0.0.57 → 0.0.59

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.57",
3
+ "version": "0.0.59",
4
4
  "exports": {
5
5
  "./appProcess": "./src/appProcess.tsx",
6
6
  "./bunInstall": "./src/bunInstall.tsx",
@@ -7,8 +7,8 @@ export const pullChanges = async () => {
7
7
  const { stderr, stdout } = await $`git pull`.quiet()
8
8
  if (stderr.length > 0) {
9
9
  logger.error(
10
- { emoji: "⬇️🚨" },
11
- `Failed to pull changes.\n (${stderr.toString().trim()})`,
10
+ { additionalDetails: stderr.toString().trim(), emoji: "⬇️🚨" },
11
+ `Failed to pull changes.`,
12
12
  )
13
13
  return
14
14
  }
@@ -13,6 +13,7 @@ import { restartAddon } from "./restartAddon"
13
13
  import { levels } from "@typed-assistant/logger/levels"
14
14
  import { getSupervisorAPI } from "@typed-assistant/utils/getHassAPI"
15
15
  import { withErrorHandling } from "@typed-assistant/utils/withErrorHandling"
16
+ import { ONE_SECOND } from "@typed-assistant/utils/durations"
16
17
 
17
18
  const indexHtmlFilePath = `${import.meta.dir}/webserver/index.html` as const
18
19
  const cssFile = `${import.meta.dir}/webserver/input.css` as const
@@ -57,6 +58,48 @@ const subscribers = new Map<string, (message: string) => void>()
57
58
  const logSubscribers = new Map<string, () => void>()
58
59
 
59
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
+ data: {
72
+ error?: never
73
+ cpu_percent: number
74
+ memory_usage: number
75
+ memory_limit: number
76
+ memory_percent: number
77
+ max_memory_usage: number
78
+ }
79
+ }>,
80
+ )("/addons/self/stats")
81
+
82
+ if (error) {
83
+ logger.error(
84
+ { additionalDetails: error.message, emoji: "❌" },
85
+ "Error getting stats",
86
+ )
87
+ } else {
88
+ stats = {
89
+ ...data.data,
90
+ max_memory_usage:
91
+ data.data.memory_usage > stats.max_memory_usage
92
+ ? data.data.memory_usage
93
+ : stats.max_memory_usage,
94
+ }
95
+ logger.info(
96
+ { additionalDetails: JSON.stringify(stats, null, 2), emoji: "📊" },
97
+ "Stats updated",
98
+ )
99
+ }
100
+
101
+ setTimeout(getStats, 30 * ONE_SECOND)
102
+ }
60
103
 
61
104
  export const startWebappServer = async ({
62
105
  basePath,
@@ -143,20 +186,7 @@ export const startWebappServer = async ({
143
186
  translations: "HIDDEN",
144
187
  }
145
188
  })
146
- .get("/stats", async () => {
147
- const { data, error } = await withErrorHandling(
148
- getSupervisorAPI<{
149
- error?: never
150
- cpu_percent: number
151
- memory_usage: number
152
- memory_limit: number
153
- memory_percent: number
154
- }>,
155
- )("/addons/self/stats")
156
-
157
- if (error) return { error: error.message }
158
- return data
159
- })
189
+ .get("/stats", async () => stats)
160
190
  .get(
161
191
  "/log.txt",
162
192
  async ({ query }) => {
@@ -232,6 +262,8 @@ export const startWebappServer = async ({
232
262
  }
233
263
  })
234
264
 
265
+ getStats()
266
+
235
267
  addKillListener(async () => {
236
268
  watcher.close()
237
269
  await server.stop()
@@ -246,7 +278,9 @@ export const startWebappServer = async ({
246
278
  ? { value: undefined }
247
279
  : await stderrReader.read()
248
280
 
249
- const decodedString = decoder.decode(value ?? stderrValue)
281
+ const decodedString = stderrValue
282
+ ? decoder.decode(stderrValue)
283
+ : decoder.decode(value)
250
284
  const convertedMessage = convert.toHtml(decodedString)
251
285
  if (convertedMessage !== "") {
252
286
  lastMessage = convertedMessage
@@ -3,6 +3,9 @@ import { Logs } from "./Logs"
3
3
  import { useEffect, useState } from "react"
4
4
  import { app } from "./api"
5
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"
6
9
 
7
10
  const basePath = process.env.BASE_PATH ?? ""
8
11
 
@@ -13,43 +16,77 @@ const App = () => {
13
16
  <Terminal />
14
17
  </div>
15
18
  <div className="col-span-1">
16
- <div className="p-4 text-xs h-full max-h-dvh w-dvw md:w-auto overflow-x-auto flex flex-col">
17
- <Logs basePath={basePath} />
18
- <Logs basePath={basePath} />
19
- </div>
19
+ <Stats />
20
+ <Logs basePath={basePath} />
20
21
  </div>
21
22
  </div>
22
23
  )
23
24
  }
24
25
 
25
26
  const Stats = () => {
27
+ const [counter, setCounter] = useState(0)
26
28
  const [error, setError] = useState<string | null>(null)
29
+ const [lastUpdated, setLastUpdated] = useState<number | null>(null)
27
30
  const [stats, setStats] =
28
- useState<Awaited<ReturnType<typeof app.stats.get>>["data"]>()
31
+ useState<Awaited<ReturnType<typeof app.stats.get>>["data"]>(null)
29
32
 
30
33
  useEffect(() => {
31
34
  app.stats.get().then((stats) => {
32
- if (stats.error || stats.data.error) {
33
- setError(stats.error?.message ?? stats.data?.error ?? "Unknown error")
35
+ setLastUpdated(Date.now())
36
+ if (stats.error) {
37
+ setError(stats.error.message)
34
38
  return
35
39
  }
36
-
37
40
  setStats(stats.data)
41
+
42
+ const timeout = setTimeout(() => {
43
+ setCounter((c) => c + 1)
44
+ }, 30 * ONE_SECOND)
45
+ return () => clearTimeout(timeout)
38
46
  })
39
- }, [])
47
+ }, [counter])
40
48
 
41
- if (error || stats?.error) {
42
- return <div>Error: {error}</div>
43
- }
44
- if (stats?.error) {
45
- return <div>Error: {error}</div>
46
- }
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
+ }
47
77
 
78
+ const ProgressBar = ({ value }: { value: number }) => {
48
79
  return (
49
- <div className="">
50
- <div>Memory: </div>
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>
51
85
  </div>
52
86
  )
53
87
  }
54
88
 
89
+ const bytesToMegaBytes = (bytes: number) =>
90
+ Math.round((bytes / 1024 / 1024) * 100) / 100
91
+
55
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
  }