@typed-assistant/builder 0.0.36 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typed-assistant/builder",
3
- "version": "0.0.36",
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.12",
28
+ "@typed-assistant/logger": "0.0.13",
29
29
  "@typed-assistant/typescript-config": "0.0.8",
30
- "@typed-assistant/utils": "0.0.13"
30
+ "@typed-assistant/utils": "0.0.14"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "home-assistant-js-websocket": "^8.2.0"
@@ -1,4 +1,4 @@
1
- import { log } from "@typed-assistant/logger"
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"
@@ -18,6 +18,44 @@ import { restartAddon } from "./restartAddon"
18
18
  import { getAddonInfo as getAddonInfoAPI } from "./getAddonInfo"
19
19
  import debounce from "debounce"
20
20
 
21
+ export async function setup({
22
+ entryFile,
23
+ mdiPaths,
24
+ onProcessError,
25
+ }: {
26
+ entryFile: string
27
+ } & Parameters<typeof generateTypes>[0] &
28
+ Pick<Parameters<typeof checkProcesses>[1], "onProcessError">) {
29
+ const addonInfo = await getAddonInfo()
30
+ const basePath = addonInfo?.data.ingress_entry ?? ""
31
+ const slug = addonInfo?.data.slug ?? ""
32
+ const directoryToWatch = join(process.cwd(), "./src")
33
+ const addonUrl = `${slug}/ingress`
34
+
35
+ checkProcesses(entryFile, { addonUrl, onProcessError })
36
+ await setupGitSync()
37
+
38
+ let subprocesses = await buildAndStartAppProcess(entryFile, {
39
+ mdiPaths: mdiPaths,
40
+ })
41
+
42
+ startWebappServer({
43
+ basePath,
44
+ getSubprocesses: () => subprocesses,
45
+ })
46
+ setupWatcher({
47
+ directoryToWatch,
48
+ entryFile,
49
+ mdiPaths,
50
+ onSubprocessChange: (newSubprocesses) => {
51
+ subprocesses = newSubprocesses
52
+ },
53
+ getSubprocesses: () => subprocesses,
54
+ })
55
+
56
+ return subprocesses
57
+ }
58
+
21
59
  type Processes = Awaited<ReturnType<typeof buildAndStartAppProcess>>
22
60
 
23
61
  async function buildAndStartAppProcess(
@@ -29,7 +67,7 @@ async function buildAndStartAppProcess(
29
67
  }
30
68
 
31
69
  async function startApp(appSourceFile: string) {
32
- log("🚀 Starting app...")
70
+ logger.info("🚀 Starting app...")
33
71
  const path = join(process.cwd(), appSourceFile)
34
72
  return Bun.spawn(["bun", path], {
35
73
  stderr: "pipe",
@@ -44,7 +82,7 @@ async function killAndRestartApp(
44
82
  subprocesses: Processes,
45
83
  ) {
46
84
  if (settingUp.current) return subprocesses
47
- log("♻️ Restarting app...")
85
+ logger.fatal("♻️ Restarting app...")
48
86
  settingUp.current = true
49
87
  if (subprocesses.app) await killSubprocess(subprocesses.app)
50
88
  const newSubprocesses = await buildAndStartAppProcess(entryFile, options)
@@ -59,29 +97,20 @@ const checkProcesses = async (
59
97
  {
60
98
  addonUrl,
61
99
  onProcessError,
62
- restartAddonUrl,
63
100
  }: {
64
101
  addonUrl: string
65
- onProcessError?: (
66
- message: string,
67
- addonUrl: string,
68
- restartAddonUrl?: string,
69
- ) => void
70
- restartAddonUrl: string
102
+ onProcessError?: (message: string, addonUrl: string) => void
71
103
  },
72
104
  ) => {
73
105
  const ps = await $`ps -f`.text()
74
106
  const matches = ps.match(new RegExp(`bun .+${entryFile}`, "gmi")) ?? []
75
- onProcessError?.("message", addonUrl, restartAddonUrl)
76
107
 
77
108
  if (matches.length > 1) {
78
109
  multipleProcessesErrorCount++
79
110
  if (multipleProcessesErrorCount > 5) {
80
- const message = `🚨 Multiple processes detected. Restarting TypedAssistant addon...`
81
- log(message)
82
- log(ps)
83
- onProcessError?.(message, addonUrl, restartAddonUrl)
84
- // restartAddon()
111
+ const message = `🚨 Multiple processes detected. Check the logs...`
112
+ logger.fatal(message, ps)
113
+ onProcessError?.(message, addonUrl)
85
114
  return
86
115
  }
87
116
  } else {
@@ -91,11 +120,9 @@ const checkProcesses = async (
91
120
  if (matches.length === 0) {
92
121
  noProcessesErrorCount++
93
122
  if (noProcessesErrorCount > 5) {
94
- const message = `🚨 No processes detected. Restarting TypedAssistant addon...`
95
- log(message)
96
- log(ps)
97
- onProcessError?.(message, addonUrl, restartAddonUrl)
98
- // restartAddon()
123
+ const message = `🚨 No processes detected. Check the logs...`
124
+ logger.fatal(message, ps)
125
+ onProcessError?.(message, addonUrl)
99
126
  return
100
127
  }
101
128
  } else {
@@ -103,56 +130,17 @@ const checkProcesses = async (
103
130
  }
104
131
 
105
132
  setTimeout(
106
- () =>
107
- checkProcesses(entryFile, { addonUrl, onProcessError, restartAddonUrl }),
133
+ () => checkProcesses(entryFile, { addonUrl, onProcessError }),
108
134
  5000,
109
135
  )
110
136
  }
111
137
 
112
- export async function setup({
113
- entryFile,
114
- mdiPaths,
115
- onProcessError,
116
- }: {
117
- entryFile: string
118
- } & Parameters<typeof generateTypes>[0] &
119
- Pick<Parameters<typeof checkProcesses>[1], "onProcessError">) {
120
- const addonInfo = await getAddonInfo()
121
- const basePath = addonInfo?.data.ingress_entry ?? ""
122
- const directoryToWatch = join(process.cwd(), "./src")
123
- const addonUrl = basePath
124
- const restartAddonUrl = `${basePath}/restart-addon`
125
-
126
- checkProcesses(entryFile, { addonUrl, onProcessError, restartAddonUrl })
127
- await setupGitSync()
128
-
129
- let subprocesses = await buildAndStartAppProcess(entryFile, {
130
- mdiPaths: mdiPaths,
131
- })
132
-
133
- startWebappServer({
134
- basePath,
135
- getSubprocesses: () => subprocesses,
136
- })
137
- setupWatcher({
138
- directoryToWatch,
139
- entryFile,
140
- mdiPaths,
141
- onSubprocessChange: (newSubprocesses) => {
142
- subprocesses = newSubprocesses
143
- },
144
- getSubprocesses: () => subprocesses,
145
- })
146
-
147
- return subprocesses
148
- }
149
-
150
138
  const getAddonInfo = async () => {
151
- log("🔍 Getting addon info...")
139
+ logger.info("🔍 Getting addon info...")
152
140
 
153
141
  const { data, error } = await getAddonInfoAPI()
154
142
 
155
- if (error) log(`🚨 Failed to get addon info: ${error}`)
143
+ if (error) logger.error(`🚨 Failed to get addon info: ${error}`)
156
144
 
157
145
  return data
158
146
  }
@@ -163,7 +151,7 @@ const setupGitSync = async () => {
163
151
  !process.env.GITHUB_USERNAME ||
164
152
  !process.env.GITHUB_REPO
165
153
  ) {
166
- log(
154
+ logger.warn(
167
155
  "⚠️ Cannot sync with Github without Github token, username, and repo details. Add these in the add-on configuration.",
168
156
  )
169
157
  return { error: {} }
@@ -172,7 +160,7 @@ const setupGitSync = async () => {
172
160
  await setupWebhook()
173
161
  return
174
162
  }
175
- log("⚠️ No HASS_EXTERNAL_URL found. Setting up git poller...")
163
+ logger.warn("⚠️ No HASS_EXTERNAL_URL found. Setting up git poller...")
176
164
  await setupGitPoller()
177
165
  }
178
166
 
@@ -199,15 +187,14 @@ function setupWatcher({
199
187
  app: Subprocess<"ignore", "pipe", "pipe">
200
188
  }
201
189
  }) {
202
- log("👀 Watching directory:", directoryToWatch)
190
+ logger.info("👀 Watching directory:", directoryToWatch)
203
191
  const watcher = watch(
204
192
  directoryToWatch,
205
193
  { recursive: true },
206
194
  debounce(async function onFileChange(event, filename) {
207
- console.log("😅😅😅 ~ event:", event)
208
195
  if (!filename) return
209
196
  if (shouldIgnoreFileOrFolder(filename)) return
210
- log(`⚠️ Change to ${filename} detected.`)
197
+ logger.info(`⚠️ Change to ${filename} detected.`)
211
198
  if (filename.endsWith("process.tsx")) {
212
199
  await killSubprocess(getSubprocesses().app)
213
200
  await restartAddon()
@@ -227,13 +214,13 @@ function setupWatcher({
227
214
  }
228
215
 
229
216
  process.on("SIGINT", async () => {
230
- log("👋 Exiting...")
217
+ logger.fatal("👋 Exiting...")
231
218
  await callSoftKillListeners()
232
219
  await callKillListeners()
233
220
  process.exit(0)
234
221
  })
235
222
  process.on("SIGTERM", async () => {
236
- log("👋 Exiting...")
223
+ logger.fatal("👋 Exiting...")
237
224
  await callSoftKillListeners()
238
225
  await callKillListeners()
239
226
  process.exit(0)
@@ -1,11 +1,11 @@
1
- import { log } from "@typed-assistant/logger"
1
+ import { logger } from "@typed-assistant/logger"
2
2
  import { $ } from "bun"
3
3
 
4
4
  export async function bunInstall() {
5
- log("🏗️ Running bun install...")
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
- log(`🚨 Failed to run bun install: ${error}`)
9
+ logger.error(`🚨 Failed to run bun install: ${error}`)
10
10
  })
11
11
  }
@@ -1,4 +1,3 @@
1
- import { log } from "@typed-assistant/logger"
2
1
  import { getSupervisorAPI } from "@typed-assistant/utils/getHassAPI"
3
2
  import { withErrorHandling } from "@typed-assistant/utils/withErrorHandling"
4
3
 
@@ -1,4 +1,4 @@
1
- import { log } from "@typed-assistant/logger"
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
- log("👋 Called all kill listeners!")
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
- log("👋 Called all soft kill listeners!")
22
+ logger.info("👋 Called all soft kill listeners!")
25
23
  }
26
24
 
27
25
  export async function killSubprocess(subprocess: Subprocess) {
28
- log(`💀 Killing process: ${subprocess.pid}`)
26
+ logger.fatal(`💀 Killing process: ${subprocess.pid}`)
29
27
  await callSoftKillListeners()
30
28
  subprocess.kill()
31
29
  await subprocess.exited
@@ -1,20 +1,20 @@
1
- import { log } from "@typed-assistant/logger"
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
- log("⬇️ Pulling changes...")
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
- log(" 👌 No new changes.")
11
+ logger.info(" 👌 No new changes.")
12
12
  return {}
13
13
  } else {
14
- log(" 👍 Changes pulled.")
14
+ logger.info(" 👍 Changes pulled.")
15
15
  }
16
16
  if (packageJSONUpdated) {
17
- log(" 📦 package.json updated.")
17
+ logger.info(" 📦 package.json updated.")
18
18
  await bunInstall()
19
19
  }
20
20
  return {}
@@ -1,12 +1,12 @@
1
- import { log } from "@typed-assistant/logger"
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
- log("♻️ Can't restart addon. Exiting...")
6
+ logger.fatal("♻️ Can't restart addon. Exiting...")
7
7
  process.exit(1)
8
8
  return
9
9
  }
10
- log("♻️ Restarting addon...")
10
+ logger.fatal("♻️ Restarting addon...")
11
11
  await getSupervisorAPI(`/addons/self/restart`, { method: "POST" })
12
12
  }
@@ -1,6 +1,7 @@
1
- import { log } from "@typed-assistant/logger"
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
- log(` ⏳ Pulling changes again in ${duration} seconds...`)
15
+ logger.info(` ⏳ Pulling changes again in ${duration} seconds...`)
15
16
 
16
17
  setTimeout(() => {
17
18
  setupGitPoller({ gitPullPollDuration })
@@ -1,4 +1,4 @@
1
- import { log } from "@typed-assistant/logger"
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
- log("🚨 Failed fetching webhooks", error.message)
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
- log("🚮 Webhook deleted: ", webhook.config.url)
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
- log(
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
- log("🚨 Failed fetching webhooks. Giving up.", error.message)
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
- log("🪝 Webhook already set up")
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
- log(
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
- log("🚨 Failed creating webhook. Giving up.", createError.message)
139
+ logger.error("🚨 Failed creating webhook. Giving up.", createError.message)
140
140
  return
141
141
  }
142
142
 
143
- log("🪝 Webhook created: ", webhook.config.url)
143
+ logger.info("🪝 Webhook created: ", webhook.config.url)
144
144
  }
145
-
146
- // await setupWebhook()
147
- // await deleteAllRepoWebhooks()
@@ -1,4 +1,4 @@
1
- import { log } from "@typed-assistant/logger"
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
- log("🛠️ Web server built successfully")
80
+ logger.info("🛠️ Web server built successfully")
81
81
 
82
82
  await $`bunx tailwindcss -c ${tailwindConfig} -i ${cssFile} -o ${cssOutputFile}`.quiet()
83
- log("💄 Tailwind built successfully")
83
+ logger.info("💄 Tailwind built successfully")
84
84
 
85
85
  const indexHtml = (await Bun.file(indexHtmlFilePath).text())
86
86
  .replace(
@@ -108,6 +108,7 @@ export const startWebappServer = async ({
108
108
  .get("/restart-addon", async () => {
109
109
  await killSubprocess(getSubprocesses().app)
110
110
  restartAddon()
111
+ return { message: "Restarting addon..." }
111
112
  })
112
113
  .get("/addon-info", async () => {
113
114
  const { data, error } = await getAddonInfo()
@@ -176,10 +177,10 @@ export const startWebappServer = async ({
176
177
  })
177
178
 
178
179
  server.listen(8099)
179
- log("🌐 Web server listening on port 8099")
180
+ logger.info("🌐 Web server listening on port 8099")
180
181
 
181
182
  const directory = join(process.cwd(), ".")
182
- log("👀 Watching log.txt")
183
+ logger.info("👀 Watching log.txt")
183
184
  const watcher = watch(directory, function onFileChange(_event, filename) {
184
185
  if (filename === "log.txt") {
185
186
  logSubscribers.forEach((send) => send())
@@ -226,7 +227,9 @@ export type WebServer = Awaited<ReturnType<typeof startWebappServer>>
226
227
  const getLogsFromFile = async (limit?: string) => {
227
228
  try {
228
229
  const lines = (await Bun.file("./log.txt").text()).split("\n")
229
- const logFile = limit ? lines.slice(0, Number(limit)) : lines
230
+ const logFile = limit
231
+ ? lines.slice(lines.length - Number(limit), lines.length - 1)
232
+ : lines
230
233
  return { logs: logFile }
231
234
  } catch (e) {
232
235
  return { logs: ["Error reading log.txt file"] }
@@ -1,15 +1,13 @@
1
- import { useCallback, useState } from "react"
2
- import { AppSection } from "./AppSection"
3
1
  import { Terminal } from "./Terminal"
4
- import { WSIndicator } from "./WSIndicator"
5
- import { app } from "./api"
6
- import { useWS } from "./useWS"
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
- <h1 className="mb-2 text-2xl flex items-baseline gap-3">
18
- TypedAssistant <WSIndicator ws={ws.ws} />
19
- </h1>
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"