feature: support serving from subpath (#33)

Co-authored-by: Karthik T <karthikt.holmes+github@gmail.com>
This commit is contained in:
Henry Dollman
2025-02-04 21:22:40 -05:00
parent ce171cf375
commit 58085bf300
17 changed files with 118 additions and 54 deletions

View File

@@ -12,6 +12,7 @@ import (
"crypto/ed25519" "crypto/ed25519"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"io/fs"
"log" "log"
"net" "net"
"net/http" "net/http"
@@ -40,15 +41,18 @@ type Hub struct {
rm *records.RecordManager rm *records.RecordManager
systemStats *core.Collection systemStats *core.Collection
containerStats *core.Collection containerStats *core.Collection
appURL string
} }
func NewHub(app *pocketbase.PocketBase) *Hub { func NewHub(app *pocketbase.PocketBase) *Hub {
return &Hub{ hub := &Hub{
app: app, app: app,
am: alerts.NewAlertManager(app), am: alerts.NewAlertManager(app),
um: users.NewUserManager(app), um: users.NewUserManager(app),
rm: records.NewRecordManager(app), rm: records.NewRecordManager(app),
} }
hub.appURL, _ = GetEnv("APP_URL")
return hub
} }
// GetEnv retrieves an environment variable with a "BESZEL_HUB_" prefix, or falls back to the unprefixed key. // GetEnv retrieves an environment variable with a "BESZEL_HUB_" prefix, or falls back to the unprefixed key.
@@ -82,6 +86,10 @@ func (h *Hub) Run() {
settings := h.app.Settings() settings := h.app.Settings()
// batch requests (for global alerts) // batch requests (for global alerts)
settings.Batch.Enabled = true settings.Batch.Enabled = true
// set URL if BASE_URL env is set
if h.appURL != "" {
settings.Meta.AppURL = h.appURL
}
// set auth settings // set auth settings
usersCollection, err := h.app.FindCollectionByNameOrId("users") usersCollection, err := h.app.FindCollectionByNameOrId("users")
if err != nil { if err != nil {
@@ -118,19 +126,39 @@ func (h *Hub) Run() {
Scheme: "http", Scheme: "http",
Host: "localhost:5173", Host: "localhost:5173",
}) })
se.Router.Any("/", func(e *core.RequestEvent) error { se.Router.Any("/{path...}", func(e *core.RequestEvent) error {
proxy.ServeHTTP(e.Response, e.Request) proxy.ServeHTTP(e.Response, e.Request)
return nil return nil
}) })
default: default:
// parse app url
parsedURL, err := url.Parse(h.appURL)
if err != nil {
return err
}
// fix base paths in html if using subpath
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
indexFile, _ := fs.ReadFile(site.DistDirFS, "index.html")
indexContent := strings.ReplaceAll(string(indexFile), "./", basePath)
// set up static asset serving
staticPaths := [2]string{"/static/", "/assets/"}
serveStatic := apis.Static(site.DistDirFS, false)
// get CSP configuration
csp, cspExists := GetEnv("CSP") csp, cspExists := GetEnv("CSP")
s := apis.Static(site.DistDirFS, true) // add route
se.Router.Any("/{path...}", func(e *core.RequestEvent) error { se.Router.Any("/{path...}", func(e *core.RequestEvent) error {
// serve static assets if path is in staticPaths
for i := range staticPaths {
if strings.Contains(e.Request.URL.Path, staticPaths[i]) {
e.Response.Header().Set("Cache-Control", "public, max-age=2592000")
return serveStatic(e)
}
}
if cspExists { if cspExists {
e.Response.Header().Del("X-Frame-Options") e.Response.Header().Del("X-Frame-Options")
e.Response.Header().Set("Content-Security-Policy", csp) e.Response.Header().Set("Content-Security-Policy", csp)
} }
return s(e) return e.HTML(http.StatusOK, indexContent)
}) })
} }
return se.Next() return se.Next()

View File

@@ -1,10 +1,11 @@
<!DOCTYPE html> <!doctype html>
<html lang="en" dir="ltr"> <html lang="en" dir="ltr">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Beszel</title> <title>Beszel</title>
<script>window.BASE_PATH = "%BASE_URL%"</script>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@@ -18,7 +18,7 @@ import { Copy, PlusIcon } from "lucide-react"
import { useState, useRef, MutableRefObject } from "react" import { useState, useRef, MutableRefObject } from "react"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { cn, copyToClipboard, isReadOnlyUser } from "@/lib/utils" import { cn, copyToClipboard, isReadOnlyUser } from "@/lib/utils"
import { navigate } from "./router" import { basePath, navigate } from "./router"
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { i18n } from "@lingui/core" import { i18n } from "@lingui/core"
@@ -60,7 +60,7 @@ export function AddSystemButton({ className }: { className?: string }) {
try { try {
setOpen(false) setOpen(false)
await pb.collection("systems").create(data) await pb.collection("systems").create(data)
navigate("/") navigate(basePath)
// console.log(record) // console.log(record)
} catch (e) { } catch (e) {
console.log(e) console.log(e)

View File

@@ -23,8 +23,9 @@ import { useEffect } from "react"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { $systems } from "@/lib/stores" import { $systems } from "@/lib/stores"
import { isAdmin } from "@/lib/utils" import { isAdmin } from "@/lib/utils"
import { navigate } from "./router" import { $router, basePath, navigate } from "./router"
import { Trans, t } from "@lingui/macro" import { Trans, t } from "@lingui/macro"
import { getPagePath } from "@nanostores/router"
export default function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) { export default function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) {
const systems = useStore($systems) const systems = useStore($systems)
@@ -55,7 +56,7 @@ export default function CommandPalette({ open, setOpen }: { open: boolean; setOp
<CommandItem <CommandItem
key={system.id} key={system.id}
onSelect={() => { onSelect={() => {
navigate(`/system/${encodeURIComponent(system.name)}`) navigate(getPagePath($router, "system", { name: system.name }))
setOpen(false) setOpen(false)
}} }}
> >
@@ -72,7 +73,7 @@ export default function CommandPalette({ open, setOpen }: { open: boolean; setOp
<CommandItem <CommandItem
keywords={["home"]} keywords={["home"]}
onSelect={() => { onSelect={() => {
navigate("/") navigate(basePath)
setOpen(false) setOpen(false)
}} }}
> >
@@ -86,7 +87,7 @@ export default function CommandPalette({ open, setOpen }: { open: boolean; setOp
</CommandItem> </CommandItem>
<CommandItem <CommandItem
onSelect={() => { onSelect={() => {
navigate("/settings/general") navigate(getPagePath($router, "settings", { name: "general" }))
setOpen(false) setOpen(false)
}} }}
> >
@@ -101,7 +102,7 @@ export default function CommandPalette({ open, setOpen }: { open: boolean; setOp
<CommandItem <CommandItem
keywords={["alerts"]} keywords={["alerts"]}
onSelect={() => { onSelect={() => {
navigate("/settings/notifications") navigate(getPagePath($router, "settings", { name: "notifications" }))
setOpen(false) setOpen(false)
}} }}
> >

View File

@@ -9,8 +9,9 @@ import { toast } from "../ui/use-toast"
import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { useCallback, useState } from "react" import { useCallback, useState } from "react"
import { AuthMethodsList, OAuth2AuthConfig } from "pocketbase" import { AuthMethodsList, OAuth2AuthConfig } from "pocketbase"
import { Link } from "../router" import { $router, Link, prependBasePath } from "../router"
import { Trans, t } from "@lingui/macro" import { Trans, t } from "@lingui/macro"
import { getPagePath } from "@nanostores/router"
const honeypot = v.literal("") const honeypot = v.literal("")
const emailSchema = v.pipe(v.string(), v.email(t`Invalid email address.`)) const emailSchema = v.pipe(v.string(), v.email(t`Invalid email address.`))
@@ -260,11 +261,11 @@ export function UserAuthForm({
) : ( ) : (
<img <img
className="me-2 h-4 w-4 dark:brightness-0 dark:invert" className="me-2 h-4 w-4 dark:brightness-0 dark:invert"
src={`/_/images/oauth2/${provider.name}.svg`} src={prependBasePath(`/_/images/oauth2/${provider.name}.svg`)}
alt="" alt=""
onError={(e) => { // onError={(e) => {
e.currentTarget.src = "/static/lock.svg" // e.currentTarget.src = "/static/lock.svg"
}} // }}
/> />
)} )}
<span className="translate-y-[1px]">{provider.displayName}</span> <span className="translate-y-[1px]">{provider.displayName}</span>
@@ -278,7 +279,7 @@ export function UserAuthForm({
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<button type="button" className={cn(buttonVariants({ variant: "outline" }))}> <button type="button" className={cn(buttonVariants({ variant: "outline" }))}>
<img className="me-2 h-4 w-4 dark:invert" src="/_/images/oauth2/github.svg" alt="" /> <img className="me-2 h-4 w-4 dark:invert" src={prependBasePath("/_/images/oauth2/github.svg")} alt="" />
<span className="translate-y-[1px]">GitHub</span> <span className="translate-y-[1px]">GitHub</span>
</button> </button>
</DialogTrigger> </DialogTrigger>
@@ -311,7 +312,7 @@ export function UserAuthForm({
{passwordEnabled && !isFirstRun && ( {passwordEnabled && !isFirstRun && (
<Link <Link
href="/forgot-password" href={getPagePath($router, "forgot_password")}
className="text-sm mx-auto hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity" className="text-sm mx-auto hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity"
> >
<Trans>Forgot password?</Trans> <Trans>Forgot password?</Trans>

View File

@@ -34,7 +34,7 @@ export default function () {
const subtitle = useMemo(() => { const subtitle = useMemo(() => {
if (isFirstRun) { if (isFirstRun) {
return t`Please create an admin account` return t`Please create an admin account`
} else if (page?.path === "/forgot-password") { } else if (page?.route === "forgot_password") {
return t`Enter email address to reset password` return t`Enter email address to reset password`
} else { } else {
return t`Please sign in to your account` return t`Please sign in to your account`
@@ -59,7 +59,7 @@ export default function () {
</h1> </h1>
<p className="text-sm text-muted-foreground">{subtitle}</p> <p className="text-sm text-muted-foreground">{subtitle}</p>
</div> </div>
{page?.path === "/forgot-password" ? ( {page?.route === "forgot_password" ? (
<ForgotPassword /> <ForgotPassword />
) : ( ) : (
<UserAuthForm isFirstRun={isFirstRun} authMethods={authMethods} /> <UserAuthForm isFirstRun={isFirstRun} authMethods={authMethods} />

View File

@@ -10,7 +10,7 @@ import {
UserIcon, UserIcon,
UsersIcon, UsersIcon,
} from "lucide-react" } from "lucide-react"
import { Link } from "./router" import { $router, basePath, Link, prependBasePath } from "./router"
import { LangToggle } from "./lang-toggle" import { LangToggle } from "./lang-toggle"
import { ModeToggle } from "./mode-toggle" import { ModeToggle } from "./mode-toggle"
import { Logo } from "./logo" import { Logo } from "./logo"
@@ -27,6 +27,7 @@ import {
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { AddSystemButton } from "./add-system" import { AddSystemButton } from "./add-system"
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { getPagePath } from "@nanostores/router"
const CommandPalette = lazy(() => import("./command-palette")) const CommandPalette = lazy(() => import("./command-palette"))
@@ -35,7 +36,7 @@ const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
export default function Navbar() { export default function Navbar() {
return ( return (
<div className="flex items-center h-14 md:h-16 bg-card px-4 pe-3 sm:px-6 border border-border/60 bt-0 rounded-md my-4"> <div className="flex items-center h-14 md:h-16 bg-card px-4 pe-3 sm:px-6 border border-border/60 bt-0 rounded-md my-4">
<Link href="/" aria-label="Home" className="p-2 ps-0 me-3"> <Link href={basePath} aria-label="Home" className="p-2 ps-0 me-3">
<Logo className="h-[1.1rem] md:h-5 fill-foreground" /> <Logo className="h-[1.1rem] md:h-5 fill-foreground" />
</Link> </Link>
<SearchButton /> <SearchButton />
@@ -44,7 +45,7 @@ export default function Navbar() {
<LangToggle /> <LangToggle />
<ModeToggle /> <ModeToggle />
<Link <Link
href="/settings/general" href={getPagePath($router, "settings", { name: "general" })}
aria-label="Settings" aria-label="Settings"
className={cn("", buttonVariants({ variant: "ghost", size: "icon" }))} className={cn("", buttonVariants({ variant: "ghost", size: "icon" }))}
> >
@@ -63,7 +64,7 @@ export default function Navbar() {
{isAdmin() && ( {isAdmin() && (
<> <>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<a href="/_/" target="_blank"> <a href={prependBasePath("/_/")} target="_blank">
<UsersIcon className="me-2.5 h-4 w-4" /> <UsersIcon className="me-2.5 h-4 w-4" />
<span> <span>
<Trans>Users</Trans> <Trans>Users</Trans>
@@ -71,7 +72,7 @@ export default function Navbar() {
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<a href="/_/#/collections?collection=systems" target="_blank"> <a href={prependBasePath("/_/#/collections?collection=systems")} target="_blank">
<ServerIcon className="me-2.5 h-4 w-4" /> <ServerIcon className="me-2.5 h-4 w-4" />
<span> <span>
<Trans>Systems</Trans> <Trans>Systems</Trans>
@@ -79,7 +80,7 @@ export default function Navbar() {
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<a href="/_/#/logs" target="_blank"> <a href={prependBasePath("/_/#/logs")} target="_blank">
<LogsIcon className="me-2.5 h-4 w-4" /> <LogsIcon className="me-2.5 h-4 w-4" />
<span> <span>
<Trans>Logs</Trans> <Trans>Logs</Trans>
@@ -87,7 +88,7 @@ export default function Navbar() {
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<a href="/_/#/settings/backups" target="_blank"> <a href={prependBasePath("/_/#/settings/backups")} target="_blank">
<DatabaseBackupIcon className="me-2.5 h-4 w-4" /> <DatabaseBackupIcon className="me-2.5 h-4 w-4" />
<span> <span>
<Trans>Backups</Trans> <Trans>Backups</Trans>

View File

@@ -1,16 +1,36 @@
import { createRouter } from "@nanostores/router" import { createRouter } from "@nanostores/router"
export const $router = createRouter( const routes = {
{ home: "/",
home: "/", system: `/system/:name`,
server: "/system/:name", settings: `/settings/:name?`,
settings: "/settings/:name?", forgot_password: `/forgot-password`,
forgot_password: "/forgot-password", } as const
},
{ links: false }
)
/** Navigate to url using router */ /**
* The base path of the application.
* This is used to prepend the base path to all routes.
*/
export const basePath = window.BASE_PATH || ""
/**
* Prepends the base path to the given path.
* @param path The path to prepend the base path to.
* @returns The path with the base path prepended.
*/
export const prependBasePath = (path: string) => (basePath + path).replaceAll("//", "/")
// prepend base path to routes
for (const route in routes) {
// @ts-ignore need as const above to get nanostores to parse types properly
routes[route] = prependBasePath(routes[route])
}
export const $router = createRouter(routes, { links: false })
/** Navigate to url using router
* Base path is automatically prepended if serving from subpath
*/
export const navigate = (urlString: string) => { export const navigate = (urlString: string) => {
$router.open(urlString) $router.open(urlString)
} }

View File

@@ -7,8 +7,9 @@ import { Separator } from "../ui/separator"
import { alertInfo, updateRecordList, updateSystemList } from "@/lib/utils" import { alertInfo, updateRecordList, updateSystemList } from "@/lib/utils"
import { AlertRecord, SystemRecord } from "@/types" import { AlertRecord, SystemRecord } from "@/types"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Link } from "../router" import { $router, Link } from "../router"
import { Plural, t, Trans } from "@lingui/macro" import { Plural, t, Trans } from "@lingui/macro"
import { getPagePath } from "@nanostores/router"
const SystemsTable = lazy(() => import("../systems-table/systems-table")) const SystemsTable = lazy(() => import("../systems-table/systems-table"))
@@ -83,7 +84,7 @@ export default function Home() {
</Trans> </Trans>
</AlertDescription> </AlertDescription>
<Link <Link
href={`/system/${encodeURIComponent(alert.sysname!)}`} href={getPagePath($router, "system", { name: alert.sysname! })}
className="absolute inset-0 w-full h-full" className="absolute inset-0 w-full h-full"
aria-label="View system" aria-label="View system"
></Link> ></Link>

View File

@@ -4,7 +4,7 @@ import { SidebarNav } from "./sidebar-nav.tsx"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { $router } from "@/components/router.tsx" import { $router } from "@/components/router.tsx"
import { redirectPage } from "@nanostores/router" import { getPagePath, redirectPage } from "@nanostores/router"
import { BellIcon, FileSlidersIcon, SettingsIcon } from "lucide-react" import { BellIcon, FileSlidersIcon, SettingsIcon } from "lucide-react"
import { $userSettings, pb } from "@/lib/stores.ts" import { $userSettings, pb } from "@/lib/stores.ts"
import { toast } from "@/components/ui/use-toast.ts" import { toast } from "@/components/ui/use-toast.ts"
@@ -49,17 +49,17 @@ export default function SettingsLayout() {
const sidebarNavItems = [ const sidebarNavItems = [
{ {
title: _(t({ message: `General`, comment: "Context: General settings" })), title: _(t({ message: `General`, comment: "Context: General settings" })),
href: "/settings/general", href: getPagePath($router, "settings", { name: "general" }),
icon: SettingsIcon, icon: SettingsIcon,
}, },
{ {
title: t`Notifications`, title: t`Notifications`,
href: "/settings/notifications", href: getPagePath($router, "settings", { name: "notifications" }),
icon: BellIcon, icon: BellIcon,
}, },
{ {
title: t`YAML Config`, title: t`YAML Config`,
href: "/settings/config", href: getPagePath($router, "settings", { name: "config" }),
icon: FileSlidersIcon, icon: FileSlidersIcon,
admin: true, admin: true,
}, },
@@ -69,8 +69,8 @@ export default function SettingsLayout() {
useEffect(() => { useEffect(() => {
document.title = t`Settings` + " / Beszel" document.title = t`Settings` + " / Beszel"
// redirect to account page if no page is specified // @ts-ignore redirect to account page if no page is specified
if (page?.path === "/settings") { if (!page?.params?.name) {
redirectPage($router, "settings", { name: "general" }) redirectPage($router, "settings", { name: "general" })
} }
}, []) }, [])

View File

@@ -13,6 +13,7 @@ import { saveSettings } from "./layout"
import * as v from "valibot" import * as v from "valibot"
import { isAdmin } from "@/lib/utils" import { isAdmin } from "@/lib/utils"
import { Trans, t } from "@lingui/macro" import { Trans, t } from "@lingui/macro"
import { prependBasePath } from "@/components/router"
interface ShoutrrrUrlCardProps { interface ShoutrrrUrlCardProps {
url: string url: string
@@ -94,7 +95,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
<p className="text-sm text-muted-foreground leading-relaxed"> <p className="text-sm text-muted-foreground leading-relaxed">
<Trans> <Trans>
Please{" "} Please{" "}
<a href="/_/#/settings/mail" className="link" target="_blank"> <a href={prependBasePath("/_/#/settings/mail")} className="link" target="_blank">
configure an SMTP server configure an SMTP server
</a>{" "} </a>{" "}
to ensure alerts are delivered. to ensure alerts are delivered.

View File

@@ -22,7 +22,7 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
<> <>
{/* Mobile View */} {/* Mobile View */}
<div className="md:hidden"> <div className="md:hidden">
<Select onValueChange={(value: string) => navigate(value)} value={page?.path}> <Select onValueChange={navigate} value={page?.path}>
<SelectTrigger className="w-full my-3.5"> <SelectTrigger className="w-full my-3.5">
<SelectValue placeholder="Select page" /> <SelectValue placeholder="Select page" />
</SelectTrigger> </SelectTrigger>

View File

@@ -65,13 +65,14 @@ import { $hubVersion, $systems, pb } from "@/lib/stores"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { cn, copyToClipboard, decimalString, isReadOnlyUser, useLocalStorage } from "@/lib/utils" import { cn, copyToClipboard, decimalString, isReadOnlyUser, useLocalStorage } from "@/lib/utils"
import AlertsButton from "../alerts/alert-button" import AlertsButton from "../alerts/alert-button"
import { Link, navigate } from "../router" import { $router, Link, navigate } from "../router"
import { EthernetIcon } from "../ui/icons" import { EthernetIcon } from "../ui/icons"
import { Trans, t } from "@lingui/macro" import { Trans, t } from "@lingui/macro"
import { useLingui } from "@lingui/react" import { useLingui } from "@lingui/react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
import { Input } from "../ui/input" import { Input } from "../ui/input"
import { ClassValue } from "clsx" import { ClassValue } from "clsx"
import { getPagePath } from "@nanostores/router"
type ViewMode = "table" | "grid" type ViewMode = "table" | "grid"
@@ -406,7 +407,7 @@ export default function SystemsTable() {
onClick={(e) => { onClick={(e) => {
const target = e.target as HTMLElement const target = e.target as HTMLElement
if (!target.closest("[data-nolink]") && e.currentTarget.contains(target)) { if (!target.closest("[data-nolink]") && e.currentTarget.contains(target)) {
navigate(`/system/${encodeURIComponent(row.original.name)}`) navigate(getPagePath($router, "system", { name: row.original.name }))
} }
}} }}
> >
@@ -489,7 +490,7 @@ export default function SystemsTable() {
})} })}
</CardContent> </CardContent>
<Link <Link
href={`/system/${encodeURIComponent(row.original.name)}`} href={getPagePath($router, "system", { name: row.original.name })}
className="inset-0 absolute w-full h-full" className="inset-0 absolute w-full h-full"
> >
<span className="sr-only">{row.original.name}</span> <span className="sr-only">{row.original.name}</span>

View File

@@ -1,9 +1,10 @@
import PocketBase from "pocketbase" import PocketBase from "pocketbase"
import { atom, map, WritableAtom } from "nanostores" import { atom, map, WritableAtom } from "nanostores"
import { AlertRecord, ChartTimes, SystemRecord, UserSettings } from "@/types" import { AlertRecord, ChartTimes, SystemRecord, UserSettings } from "@/types"
import { basePath } from "@/components/router"
/** PocketBase JS Client */ /** PocketBase JS Client */
export const pb = new PocketBase("/") export const pb = new PocketBase(basePath)
/** Store if user is authenticated */ /** Store if user is authenticated */
export const $authenticated = atom(pb.authStore.isValid) export const $authenticated = atom(pb.authStore.isValid)

View File

@@ -63,9 +63,9 @@ const App = () => {
if (!page) { if (!page) {
return <h1 className="text-3xl text-center my-14">404</h1> return <h1 className="text-3xl text-center my-14">404</h1>
} else if (page.path === "/") { } else if (page.route === "home") {
return <Home /> return <Home />
} else if (page.route === "server") { } else if (page.route === "system") {
return <SystemDetail name={page.params.name} /> return <SystemDetail name={page.params.name} />
} else if (page.route === "settings") { } else if (page.route === "settings") {
return ( return (

View File

@@ -1,5 +1,12 @@
import { RecordModel } from "pocketbase" import { RecordModel } from "pocketbase"
// global window properties
declare global {
interface Window {
BASE_PATH: string
}
}
export interface SystemRecord extends RecordModel { export interface SystemRecord extends RecordModel {
name: string name: string
host: string host: string

View File

@@ -4,6 +4,7 @@ import react from "@vitejs/plugin-react-swc"
import { lingui } from "@lingui/vite-plugin" import { lingui } from "@lingui/vite-plugin"
export default defineConfig({ export default defineConfig({
base: "./",
plugins: [ plugins: [
react({ react({
plugins: [["@lingui/swc-plugin", {}]], plugins: [["@lingui/swc-plugin", {}]],