@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typed-assistant/builder",
3
- "version": "0.0.37",
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"
@@ -67,7 +67,7 @@ async function buildAndStartAppProcess(
67
67
  }
68
68
 
69
69
  async function startApp(appSourceFile: string) {
70
- log("🚀 Starting app...")
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
- log("♻️ Restarting app...")
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
- log(message)
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
- log(message)
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
- log("🔍 Getting addon info...")
139
+ logger.info("🔍 Getting addon info...")
142
140
 
143
141
  const { data, error } = await getAddonInfoAPI()
144
142
 
145
- if (error) log(`🚨 Failed to get addon info: ${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
- log(
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
- log("⚠️ No HASS_EXTERNAL_URL found. Setting up git poller...")
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
- log("👀 Watching directory:", directoryToWatch)
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
- log(`⚠️ Change to ${filename} detected.`)
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
- log("👋 Exiting...")
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
- log("👋 Exiting...")
223
+ logger.fatal("👋 Exiting...")
226
224
  await callSoftKillListeners()
227
225
  await callKillListeners()
228
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(
@@ -177,10 +177,10 @@ export const startWebappServer = async ({
177
177
  })
178
178
 
179
179
  server.listen(8099)
180
- log("🌐 Web server listening on port 8099")
180
+ logger.info("🌐 Web server listening on port 8099")
181
181
 
182
182
  const directory = join(process.cwd(), ".")
183
- log("👀 Watching log.txt")
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 ? lines.slice(0, Number(limit)) : lines
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"] }
@@ -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"