@typed-assistant/builder 0.0.37 → 0.0.38
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 +3 -3
- package/src/appProcess.tsx +13 -15
- package/src/bunInstall.tsx +3 -3
- package/src/getAddonInfo.tsx +0 -1
- package/src/killProcess.tsx +4 -6
- package/src/pullChanges.tsx +5 -5
- package/src/restartAddon.tsx +3 -3
- package/src/setupGitPoller.tsx +4 -3
- package/src/setupWebhook.tsx +9 -12
- package/src/setupWebserver.tsx +8 -6
- package/src/webserver/App.tsx +4 -90
- package/src/webserver/Logs.tsx +120 -0
- package/src/webserver/Terminal.tsx +16 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typed-assistant/builder",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.38",
|
|
4
4
|
"exports": {
|
|
5
5
|
"./appProcess": "./src/appProcess.tsx",
|
|
6
6
|
"./bunInstall": "./src/bunInstall.tsx",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"ts-toolbelt": "^9.6.0",
|
|
26
26
|
"typescript": "^5.3.3",
|
|
27
27
|
"@typed-assistant/eslint-config": "0.0.8",
|
|
28
|
-
"@typed-assistant/logger": "0.0.
|
|
28
|
+
"@typed-assistant/logger": "0.0.13",
|
|
29
29
|
"@typed-assistant/typescript-config": "0.0.8",
|
|
30
|
-
"@typed-assistant/utils": "0.0.
|
|
30
|
+
"@typed-assistant/utils": "0.0.14"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"home-assistant-js-websocket": "^8.2.0"
|
package/src/appProcess.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from "@typed-assistant/logger"
|
|
2
2
|
import { generateTypes } from "@typed-assistant/types/generateTypes"
|
|
3
3
|
import type { Subprocess } from "bun"
|
|
4
4
|
import { $ } from "bun"
|
|
@@ -67,7 +67,7 @@ async function buildAndStartAppProcess(
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
async function startApp(appSourceFile: string) {
|
|
70
|
-
|
|
70
|
+
logger.info("🚀 Starting app...")
|
|
71
71
|
const path = join(process.cwd(), appSourceFile)
|
|
72
72
|
return Bun.spawn(["bun", path], {
|
|
73
73
|
stderr: "pipe",
|
|
@@ -82,7 +82,7 @@ async function killAndRestartApp(
|
|
|
82
82
|
subprocesses: Processes,
|
|
83
83
|
) {
|
|
84
84
|
if (settingUp.current) return subprocesses
|
|
85
|
-
|
|
85
|
+
logger.fatal("♻️ Restarting app...")
|
|
86
86
|
settingUp.current = true
|
|
87
87
|
if (subprocesses.app) await killSubprocess(subprocesses.app)
|
|
88
88
|
const newSubprocesses = await buildAndStartAppProcess(entryFile, options)
|
|
@@ -109,8 +109,7 @@ const checkProcesses = async (
|
|
|
109
109
|
multipleProcessesErrorCount++
|
|
110
110
|
if (multipleProcessesErrorCount > 5) {
|
|
111
111
|
const message = `🚨 Multiple processes detected. Check the logs...`
|
|
112
|
-
|
|
113
|
-
log(ps)
|
|
112
|
+
logger.fatal(message, ps)
|
|
114
113
|
onProcessError?.(message, addonUrl)
|
|
115
114
|
return
|
|
116
115
|
}
|
|
@@ -122,8 +121,7 @@ const checkProcesses = async (
|
|
|
122
121
|
noProcessesErrorCount++
|
|
123
122
|
if (noProcessesErrorCount > 5) {
|
|
124
123
|
const message = `🚨 No processes detected. Check the logs...`
|
|
125
|
-
|
|
126
|
-
log(ps)
|
|
124
|
+
logger.fatal(message, ps)
|
|
127
125
|
onProcessError?.(message, addonUrl)
|
|
128
126
|
return
|
|
129
127
|
}
|
|
@@ -138,11 +136,11 @@ const checkProcesses = async (
|
|
|
138
136
|
}
|
|
139
137
|
|
|
140
138
|
const getAddonInfo = async () => {
|
|
141
|
-
|
|
139
|
+
logger.info("🔍 Getting addon info...")
|
|
142
140
|
|
|
143
141
|
const { data, error } = await getAddonInfoAPI()
|
|
144
142
|
|
|
145
|
-
if (error)
|
|
143
|
+
if (error) logger.error(`🚨 Failed to get addon info: ${error}`)
|
|
146
144
|
|
|
147
145
|
return data
|
|
148
146
|
}
|
|
@@ -153,7 +151,7 @@ const setupGitSync = async () => {
|
|
|
153
151
|
!process.env.GITHUB_USERNAME ||
|
|
154
152
|
!process.env.GITHUB_REPO
|
|
155
153
|
) {
|
|
156
|
-
|
|
154
|
+
logger.warn(
|
|
157
155
|
"⚠️ Cannot sync with Github without Github token, username, and repo details. Add these in the add-on configuration.",
|
|
158
156
|
)
|
|
159
157
|
return { error: {} }
|
|
@@ -162,7 +160,7 @@ const setupGitSync = async () => {
|
|
|
162
160
|
await setupWebhook()
|
|
163
161
|
return
|
|
164
162
|
}
|
|
165
|
-
|
|
163
|
+
logger.warn("⚠️ No HASS_EXTERNAL_URL found. Setting up git poller...")
|
|
166
164
|
await setupGitPoller()
|
|
167
165
|
}
|
|
168
166
|
|
|
@@ -189,14 +187,14 @@ function setupWatcher({
|
|
|
189
187
|
app: Subprocess<"ignore", "pipe", "pipe">
|
|
190
188
|
}
|
|
191
189
|
}) {
|
|
192
|
-
|
|
190
|
+
logger.info("👀 Watching directory:", directoryToWatch)
|
|
193
191
|
const watcher = watch(
|
|
194
192
|
directoryToWatch,
|
|
195
193
|
{ recursive: true },
|
|
196
194
|
debounce(async function onFileChange(event, filename) {
|
|
197
195
|
if (!filename) return
|
|
198
196
|
if (shouldIgnoreFileOrFolder(filename)) return
|
|
199
|
-
|
|
197
|
+
logger.info(`⚠️ Change to ${filename} detected.`)
|
|
200
198
|
if (filename.endsWith("process.tsx")) {
|
|
201
199
|
await killSubprocess(getSubprocesses().app)
|
|
202
200
|
await restartAddon()
|
|
@@ -216,13 +214,13 @@ function setupWatcher({
|
|
|
216
214
|
}
|
|
217
215
|
|
|
218
216
|
process.on("SIGINT", async () => {
|
|
219
|
-
|
|
217
|
+
logger.fatal("👋 Exiting...")
|
|
220
218
|
await callSoftKillListeners()
|
|
221
219
|
await callKillListeners()
|
|
222
220
|
process.exit(0)
|
|
223
221
|
})
|
|
224
222
|
process.on("SIGTERM", async () => {
|
|
225
|
-
|
|
223
|
+
logger.fatal("👋 Exiting...")
|
|
226
224
|
await callSoftKillListeners()
|
|
227
225
|
await callKillListeners()
|
|
228
226
|
process.exit(0)
|
package/src/bunInstall.tsx
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from "@typed-assistant/logger"
|
|
2
2
|
import { $ } from "bun"
|
|
3
3
|
|
|
4
4
|
export async function bunInstall() {
|
|
5
|
-
|
|
5
|
+
logger.info("🏗️ Running bun install...")
|
|
6
6
|
return $`bun install --frozen-lockfile --cache-dir=.bun-cache`
|
|
7
7
|
.text()
|
|
8
8
|
.catch((error) => {
|
|
9
|
-
|
|
9
|
+
logger.error(`🚨 Failed to run bun install: ${error}`)
|
|
10
10
|
})
|
|
11
11
|
}
|
package/src/getAddonInfo.tsx
CHANGED
package/src/killProcess.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from "@typed-assistant/logger"
|
|
2
2
|
import type { Subprocess } from "bun"
|
|
3
3
|
import { $ } from "bun"
|
|
4
4
|
|
|
@@ -13,19 +13,17 @@ export const addSoftKillListener = (listener: () => void | Promise<void>) => {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export async function callKillListeners() {
|
|
16
|
-
log("👋 Calling kill listeners!")
|
|
17
16
|
await Promise.all(killListeners.map((listener) => listener()))
|
|
18
|
-
|
|
17
|
+
logger.info("👋 Called all kill listeners!")
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
export async function callSoftKillListeners() {
|
|
22
|
-
log("👋 Calling soft kill listeners!")
|
|
23
21
|
await Promise.all(softKillListeners.map((listener) => listener()))
|
|
24
|
-
|
|
22
|
+
logger.info("👋 Called all soft kill listeners!")
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
export async function killSubprocess(subprocess: Subprocess) {
|
|
28
|
-
|
|
26
|
+
logger.fatal(`💀 Killing process: ${subprocess.pid}`)
|
|
29
27
|
await callSoftKillListeners()
|
|
30
28
|
subprocess.kill()
|
|
31
29
|
await subprocess.exited
|
package/src/pullChanges.tsx
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from "@typed-assistant/logger"
|
|
2
2
|
import { $ } from "bun"
|
|
3
3
|
import { bunInstall } from "./bunInstall"
|
|
4
4
|
|
|
5
5
|
export const pullChanges = async () => {
|
|
6
|
-
|
|
6
|
+
logger.info("⬇️ Pulling changes...")
|
|
7
7
|
const gitPullText = await $`git pull`.text()
|
|
8
8
|
const packageJSONUpdated = /package.json/.test(gitPullText)
|
|
9
9
|
const nothingNew = /Already up to date./.test(gitPullText)
|
|
10
10
|
if (nothingNew) {
|
|
11
|
-
|
|
11
|
+
logger.info(" 👌 No new changes.")
|
|
12
12
|
return {}
|
|
13
13
|
} else {
|
|
14
|
-
|
|
14
|
+
logger.info(" 👍 Changes pulled.")
|
|
15
15
|
}
|
|
16
16
|
if (packageJSONUpdated) {
|
|
17
|
-
|
|
17
|
+
logger.info(" 📦 package.json updated.")
|
|
18
18
|
await bunInstall()
|
|
19
19
|
}
|
|
20
20
|
return {}
|
package/src/restartAddon.tsx
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from "@typed-assistant/logger"
|
|
2
2
|
import { getSupervisorAPI } from "@typed-assistant/utils/getHassAPI"
|
|
3
3
|
|
|
4
4
|
export const restartAddon = async () => {
|
|
5
5
|
if (!process.env.SUPERVISOR_TOKEN) {
|
|
6
|
-
|
|
6
|
+
logger.fatal("♻️ Can't restart addon. Exiting...")
|
|
7
7
|
process.exit(1)
|
|
8
8
|
return
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
logger.fatal("♻️ Restarting addon...")
|
|
11
11
|
await getSupervisorAPI(`/addons/self/restart`, { method: "POST" })
|
|
12
12
|
}
|
package/src/setupGitPoller.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from "@typed-assistant/logger"
|
|
2
2
|
import { ONE_SECOND } from "@typed-assistant/utils/durations"
|
|
3
3
|
import { pullChanges } from "./pullChanges"
|
|
4
|
+
import { withErrorHandling } from "@typed-assistant/utils/withErrorHandling"
|
|
4
5
|
|
|
5
6
|
export const setupGitPoller = async ({
|
|
6
7
|
gitPullPollDuration,
|
|
@@ -9,9 +10,9 @@ export const setupGitPoller = async ({
|
|
|
9
10
|
gitPullPollDuration?: number
|
|
10
11
|
} = {}) => {
|
|
11
12
|
const duration = gitPullPollDuration ?? 30
|
|
12
|
-
const { error } = await pullChanges()
|
|
13
|
+
const { error } = await withErrorHandling(pullChanges)()
|
|
13
14
|
if (error) return
|
|
14
|
-
|
|
15
|
+
logger.info(` ⏳ Pulling changes again in ${duration} seconds...`)
|
|
15
16
|
|
|
16
17
|
setTimeout(() => {
|
|
17
18
|
setupGitPoller({ gitPullPollDuration })
|
package/src/setupWebhook.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from "@typed-assistant/logger"
|
|
2
2
|
import { handleFetchError } from "@typed-assistant/utils/getHassAPI"
|
|
3
3
|
import { withErrorHandling } from "@typed-assistant/utils/withErrorHandling"
|
|
4
4
|
import { z } from "zod"
|
|
@@ -36,14 +36,14 @@ const deleteAllRepoWebhooks = async () => {
|
|
|
36
36
|
const { data: webhooks, error } = await listRepoWebhooks()
|
|
37
37
|
|
|
38
38
|
if (error) {
|
|
39
|
-
|
|
39
|
+
logger.error("🚨 Failed fetching webhooks", error.message)
|
|
40
40
|
return
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
await Promise.all(
|
|
44
44
|
webhooks.map(async (webhook) => {
|
|
45
45
|
await deleteRepoWebhook(webhook.id)
|
|
46
|
-
|
|
46
|
+
logger.info("🚮 Webhook deleted: ", webhook.config.url)
|
|
47
47
|
}),
|
|
48
48
|
)
|
|
49
49
|
}
|
|
@@ -106,13 +106,13 @@ export const setupWebhook = async (): Promise<void> => {
|
|
|
106
106
|
if (error) {
|
|
107
107
|
if (retries < 5) {
|
|
108
108
|
retries++
|
|
109
|
-
|
|
109
|
+
logger.error(
|
|
110
110
|
`🔁 Failed fetching webhooks. Retrying setup in ${retryTimeout / 1000}s...`,
|
|
111
111
|
)
|
|
112
112
|
setTimeout(setupWebhook, retryTimeout)
|
|
113
113
|
return
|
|
114
114
|
}
|
|
115
|
-
|
|
115
|
+
logger.error("🚨 Failed fetching webhooks. Giving up.", error.message)
|
|
116
116
|
return
|
|
117
117
|
}
|
|
118
118
|
|
|
@@ -121,7 +121,7 @@ export const setupWebhook = async (): Promise<void> => {
|
|
|
121
121
|
)
|
|
122
122
|
|
|
123
123
|
if (webhookAlreadyExists) {
|
|
124
|
-
|
|
124
|
+
logger.info("🪝 Webhook already set up")
|
|
125
125
|
return
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -130,18 +130,15 @@ export const setupWebhook = async (): Promise<void> => {
|
|
|
130
130
|
if (createError) {
|
|
131
131
|
if (retries < 5) {
|
|
132
132
|
retries++
|
|
133
|
-
|
|
133
|
+
logger.error(
|
|
134
134
|
`🔁 Failed creating webhook. Retrying setup in ${retryTimeout / 1000}s...`,
|
|
135
135
|
)
|
|
136
136
|
setTimeout(setupWebhook, retryTimeout)
|
|
137
137
|
return
|
|
138
138
|
}
|
|
139
|
-
|
|
139
|
+
logger.error("🚨 Failed creating webhook. Giving up.", createError.message)
|
|
140
140
|
return
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
logger.info("🪝 Webhook created: ", webhook.config.url)
|
|
144
144
|
}
|
|
145
|
-
|
|
146
|
-
// await setupWebhook()
|
|
147
|
-
// await deleteAllRepoWebhooks()
|
package/src/setupWebserver.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from "@typed-assistant/logger"
|
|
2
2
|
import Convert from "ansi-to-html"
|
|
3
3
|
import type { Subprocess } from "bun"
|
|
4
4
|
import { $ } from "bun"
|
|
@@ -77,10 +77,10 @@ export const startWebappServer = async ({
|
|
|
77
77
|
}
|
|
78
78
|
throw new Error("Build failed")
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
logger.info("🛠️ Web server built successfully")
|
|
81
81
|
|
|
82
82
|
await $`bunx tailwindcss -c ${tailwindConfig} -i ${cssFile} -o ${cssOutputFile}`.quiet()
|
|
83
|
-
|
|
83
|
+
logger.info("💄 Tailwind built successfully")
|
|
84
84
|
|
|
85
85
|
const indexHtml = (await Bun.file(indexHtmlFilePath).text())
|
|
86
86
|
.replace(
|
|
@@ -177,10 +177,10 @@ export const startWebappServer = async ({
|
|
|
177
177
|
})
|
|
178
178
|
|
|
179
179
|
server.listen(8099)
|
|
180
|
-
|
|
180
|
+
logger.info("🌐 Web server listening on port 8099")
|
|
181
181
|
|
|
182
182
|
const directory = join(process.cwd(), ".")
|
|
183
|
-
|
|
183
|
+
logger.info("👀 Watching log.txt")
|
|
184
184
|
const watcher = watch(directory, function onFileChange(_event, filename) {
|
|
185
185
|
if (filename === "log.txt") {
|
|
186
186
|
logSubscribers.forEach((send) => send())
|
|
@@ -227,7 +227,9 @@ export type WebServer = Awaited<ReturnType<typeof startWebappServer>>
|
|
|
227
227
|
const getLogsFromFile = async (limit?: string) => {
|
|
228
228
|
try {
|
|
229
229
|
const lines = (await Bun.file("./log.txt").text()).split("\n")
|
|
230
|
-
const logFile = limit
|
|
230
|
+
const logFile = limit
|
|
231
|
+
? lines.slice(lines.length - Number(limit), lines.length - 1)
|
|
232
|
+
: lines
|
|
231
233
|
return { logs: logFile }
|
|
232
234
|
} catch (e) {
|
|
233
235
|
return { logs: ["Error reading log.txt file"] }
|
package/src/webserver/App.tsx
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import { useCallback, useState } from "react"
|
|
2
|
-
import { AppSection } from "./AppSection"
|
|
3
1
|
import { Terminal } from "./Terminal"
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { Logs } from "./Logs"
|
|
3
|
+
|
|
4
|
+
const basePath = process.env.BASE_PATH ?? ""
|
|
7
5
|
|
|
8
6
|
const App = () => {
|
|
9
7
|
return (
|
|
10
8
|
<div className="grid md:grid-cols-3">
|
|
11
9
|
<div className="col-span-2">
|
|
12
|
-
<Terminal />
|
|
10
|
+
<Terminal basePath={basePath} />
|
|
13
11
|
</div>
|
|
14
12
|
<div className="col-span-1">
|
|
15
13
|
<Logs />
|
|
@@ -18,88 +16,4 @@ const App = () => {
|
|
|
18
16
|
)
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
const Logs = () => {
|
|
22
|
-
const [limit, setLimit] = useState(50)
|
|
23
|
-
const [dateTimeVisibility, setDateTimeVisibility] = useState<
|
|
24
|
-
"hidden" | "timeOnly" | "visible"
|
|
25
|
-
>("timeOnly")
|
|
26
|
-
const [logs, setLogs] = useState<
|
|
27
|
-
{ date: string; time: string; message: string }[]
|
|
28
|
-
>([])
|
|
29
|
-
|
|
30
|
-
const ws = useWS({
|
|
31
|
-
subscribe: useCallback(
|
|
32
|
-
() => app.logsws.subscribe({ $query: { limit: limit.toString() } }),
|
|
33
|
-
[limit],
|
|
34
|
-
),
|
|
35
|
-
onMessage: useCallback((event) => {
|
|
36
|
-
setLogs(
|
|
37
|
-
(JSON.parse(event.data).logs as string[]).map((log: string) => {
|
|
38
|
-
const [date, time, message] =
|
|
39
|
-
log.match(/\[(.+), (.+)\] +(.+)/)?.slice(1) ?? []
|
|
40
|
-
return { date: date ?? "", time: time ?? "", message: message ?? log }
|
|
41
|
-
}),
|
|
42
|
-
)
|
|
43
|
-
}, []),
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<AppSection
|
|
48
|
-
renderHeader={() => (
|
|
49
|
-
<>
|
|
50
|
-
<h2 className="mb-2 text-2xl flex items-baseline gap-3">
|
|
51
|
-
Logs <WSIndicator ws={ws.ws} />
|
|
52
|
-
</h2>
|
|
53
|
-
<div className="flex flex-wrap gap-2">
|
|
54
|
-
<div className="flex gap-2">
|
|
55
|
-
<label htmlFor="dateTimeVisibility">Date/Time</label>
|
|
56
|
-
<select
|
|
57
|
-
className="border border-gray-300 rounded-md text-slate-800 px-2"
|
|
58
|
-
id="dateTimeVisibility"
|
|
59
|
-
onChange={(e) =>
|
|
60
|
-
setDateTimeVisibility(
|
|
61
|
-
e.target.value as typeof dateTimeVisibility,
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
value={dateTimeVisibility}
|
|
65
|
-
>
|
|
66
|
-
<option value="hidden">Hidden</option>
|
|
67
|
-
<option value="timeOnly">Time only</option>
|
|
68
|
-
<option value="visible">Visible</option>
|
|
69
|
-
</select>
|
|
70
|
-
</div>
|
|
71
|
-
<div className="flex gap-2">
|
|
72
|
-
<label htmlFor="limit">Limit</label>
|
|
73
|
-
<input
|
|
74
|
-
className="border border-gray-300 rounded-md text-slate-800 px-2"
|
|
75
|
-
id="limit"
|
|
76
|
-
onChange={(e) => setLimit(Number(e.target.value))}
|
|
77
|
-
size={8}
|
|
78
|
-
value={limit}
|
|
79
|
-
/>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
</>
|
|
83
|
-
)}
|
|
84
|
-
>
|
|
85
|
-
<pre>
|
|
86
|
-
<ul>
|
|
87
|
-
{logs.map((log, index) => (
|
|
88
|
-
<li key={(log.date ?? index) + log.time + log.message} className="">
|
|
89
|
-
<span className="text-slate-400 mr-2">
|
|
90
|
-
{dateTimeVisibility === "hidden"
|
|
91
|
-
? null
|
|
92
|
-
: dateTimeVisibility === "timeOnly"
|
|
93
|
-
? log.time
|
|
94
|
-
: `${log.date} ${log.time}`}
|
|
95
|
-
</span>
|
|
96
|
-
{log.message}
|
|
97
|
-
</li>
|
|
98
|
-
))}
|
|
99
|
-
</ul>
|
|
100
|
-
</pre>
|
|
101
|
-
</AppSection>
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
19
|
export default App
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useCallback, useState } from "react"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
import { AppSection } from "./AppSection"
|
|
4
|
+
import { WSIndicator } from "./WSIndicator"
|
|
5
|
+
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
|
+
|
|
10
|
+
const LogSchema = z.object({
|
|
11
|
+
level: z.number(),
|
|
12
|
+
time: z.number(),
|
|
13
|
+
pid: z.number(),
|
|
14
|
+
hostname: z.string(),
|
|
15
|
+
msg: z.string(),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
type LogSchema = z.infer<typeof LogSchema>
|
|
19
|
+
|
|
20
|
+
export const Logs = () => {
|
|
21
|
+
const [limit, setLimit] = useState(50)
|
|
22
|
+
const [level, setLevel] = useState<
|
|
23
|
+
"trace" | "debug" | "info" | "warn" | "error" | "fatal"
|
|
24
|
+
>("trace")
|
|
25
|
+
const [dateTimeVisibility, setDateTimeVisibility] = useState<
|
|
26
|
+
"hidden" | "timeOnly" | "visible"
|
|
27
|
+
>("timeOnly")
|
|
28
|
+
const [logs, setLogs] = useState<LogSchema[]>([])
|
|
29
|
+
|
|
30
|
+
const ws = useWS({
|
|
31
|
+
subscribe: useCallback(
|
|
32
|
+
() => app.logsws.subscribe({ $query: { limit: limit.toString() } }),
|
|
33
|
+
[limit],
|
|
34
|
+
),
|
|
35
|
+
onMessage: useCallback((event) => {
|
|
36
|
+
setLogs(
|
|
37
|
+
(JSON.parse(event.data).logs as string[]).map((log: string) =>
|
|
38
|
+
LogSchema.parse(JSON.parse(log)),
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
}, []),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<AppSection
|
|
46
|
+
renderHeader={() => (
|
|
47
|
+
<>
|
|
48
|
+
<h2 className="mb-2 text-2xl flex items-baseline gap-3">
|
|
49
|
+
Logs <WSIndicator ws={ws.ws} />
|
|
50
|
+
</h2>
|
|
51
|
+
<div className="flex flex-wrap gap-2">
|
|
52
|
+
<div className="flex gap-2">
|
|
53
|
+
<label htmlFor="dateTimeVisibility">Date/Time</label>
|
|
54
|
+
<select
|
|
55
|
+
className="border border-gray-300 rounded-md text-slate-800 px-2"
|
|
56
|
+
id="dateTimeVisibility"
|
|
57
|
+
onChange={(e) =>
|
|
58
|
+
setDateTimeVisibility(
|
|
59
|
+
e.target.value as typeof dateTimeVisibility,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
value={dateTimeVisibility}
|
|
63
|
+
>
|
|
64
|
+
<option value="hidden">Hidden</option>
|
|
65
|
+
<option value="timeOnly">Time only</option>
|
|
66
|
+
<option value="visible">Visible</option>
|
|
67
|
+
</select>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="flex gap-2">
|
|
70
|
+
<label htmlFor="dateTimeVisibility">Level</label>
|
|
71
|
+
<select
|
|
72
|
+
className="border border-gray-300 rounded-md text-slate-800 px-2"
|
|
73
|
+
id="level"
|
|
74
|
+
onChange={(e) => setLevel(e.target.value as typeof level)}
|
|
75
|
+
value={level}
|
|
76
|
+
>
|
|
77
|
+
<option value="trace">Trace</option>
|
|
78
|
+
<option value="debug">Debug</option>
|
|
79
|
+
<option value="info">Info</option>
|
|
80
|
+
<option value="warn">Warn</option>
|
|
81
|
+
<option value="error">Error</option>
|
|
82
|
+
<option value="fatal">Fatal</option>
|
|
83
|
+
</select>
|
|
84
|
+
</div>
|
|
85
|
+
<div className="flex gap-2">
|
|
86
|
+
<label htmlFor="limit">Limit</label>
|
|
87
|
+
<input
|
|
88
|
+
className="border border-gray-300 rounded-md text-slate-800 px-2"
|
|
89
|
+
id="limit"
|
|
90
|
+
onChange={(e) => setLimit(Number(e.target.value))}
|
|
91
|
+
size={8}
|
|
92
|
+
value={limit}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</>
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
99
|
+
<pre>
|
|
100
|
+
<ul>
|
|
101
|
+
{logs
|
|
102
|
+
.filter((log) => log.level >= (levels[level] ?? 0))
|
|
103
|
+
.sort((a, b) => b.time - a.time)
|
|
104
|
+
.map((log, index) => (
|
|
105
|
+
<li key={(log.time ?? index) + log.time + log.msg} className="">
|
|
106
|
+
<span className="text-slate-400 mr-2">
|
|
107
|
+
{dateTimeVisibility === "hidden"
|
|
108
|
+
? null
|
|
109
|
+
: dateTimeVisibility === "timeOnly"
|
|
110
|
+
? new Date(log.time).toLocaleTimeString("en-GB")
|
|
111
|
+
: getPrettyTimestamp(log.time)}
|
|
112
|
+
</span>
|
|
113
|
+
{log.msg}
|
|
114
|
+
</li>
|
|
115
|
+
))}
|
|
116
|
+
</ul>
|
|
117
|
+
</pre>
|
|
118
|
+
</AppSection>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -4,7 +4,7 @@ import { useWS } from "./useWS"
|
|
|
4
4
|
import { WSIndicator } from "./WSIndicator"
|
|
5
5
|
import { AppSection } from "./AppSection"
|
|
6
6
|
|
|
7
|
-
export function Terminal() {
|
|
7
|
+
export function Terminal({ basePath }: { basePath: string }) {
|
|
8
8
|
const [content, setContent] = useState("")
|
|
9
9
|
const ws = useWS({
|
|
10
10
|
subscribe: useCallback(() => app.ws.subscribe(), []),
|
|
@@ -14,12 +14,24 @@ export function Terminal() {
|
|
|
14
14
|
return (
|
|
15
15
|
<AppSection
|
|
16
16
|
renderHeader={() => (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
<>
|
|
18
|
+
<h1 className="mb-2 text-2xl flex items-baseline gap-3">
|
|
19
|
+
TypedAssistant <WSIndicator ws={ws.ws} />
|
|
20
|
+
</h1>
|
|
21
|
+
<div className="flex flex-wrap gap-2">
|
|
22
|
+
<a className={buttonStyle} href={`${basePath}/restart-addon`}>
|
|
23
|
+
Restart addon
|
|
24
|
+
</a>
|
|
25
|
+
<a className={buttonStyle} href={`${basePath}/log.txt?limit=500`}>
|
|
26
|
+
View log.txt
|
|
27
|
+
</a>
|
|
28
|
+
</div>
|
|
29
|
+
</>
|
|
20
30
|
)}
|
|
21
31
|
>
|
|
22
32
|
<pre dangerouslySetInnerHTML={{ __html: content }} />
|
|
23
33
|
</AppSection>
|
|
24
34
|
)
|
|
25
35
|
}
|
|
36
|
+
|
|
37
|
+
const buttonStyle = "bg-slate-800 text-white px-3 py-1 rounded-md"
|