@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 +1 -1
- package/src/setupWebserver.tsx +49 -12
- package/src/webserver/App.tsx +73 -0
- package/src/webserver/AppSection.tsx +26 -8
- package/src/webserver/Logs.tsx +4 -5
package/package.json
CHANGED
package/src/setupWebserver.tsx
CHANGED
|
@@ -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 =
|
|
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
|
package/src/webserver/App.tsx
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
11
|
+
children: ReactNode
|
|
12
|
+
className?: string
|
|
13
|
+
fullHeight?: boolean
|
|
14
|
+
renderHeader?: () => JSX.Element
|
|
15
|
+
scrollable?: boolean
|
|
7
16
|
}) => {
|
|
8
17
|
return (
|
|
9
|
-
<div
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
}
|
package/src/webserver/Logs.tsx
CHANGED
|
@@ -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)
|