diff --git a/app/components/Attribute.tsx b/app/components/Attribute.tsx
index 604a6c9..8b89f0f 100644
--- a/app/components/Attribute.tsx
+++ b/app/components/Attribute.tsx
@@ -10,8 +10,8 @@ type Properties = {
export default function Attribute({ name, value, isCopyable }: Properties) {
const canCopy = isCopyable ?? false
return (
-
- -
+
+ -
{name}
diff --git a/app/routes/_data.tsx b/app/routes/_data.tsx
index aa671cd..6df72b2 100644
--- a/app/routes/_data.tsx
+++ b/app/routes/_data.tsx
@@ -1,4 +1,4 @@
-import { CpuChipIcon, ServerStackIcon } from '@heroicons/react/24/outline'
+import { CpuChipIcon, ServerStackIcon, UsersIcon } from '@heroicons/react/24/outline'
import { type LoaderFunctionArgs, redirect } from '@remix-run/node'
import { Outlet } from '@remix-run/react'
@@ -46,6 +46,7 @@ export default function Layout() {
}/>
+ }/>
diff --git a/app/routes/_data.users._index.tsx b/app/routes/_data.users._index.tsx
new file mode 100644
index 0000000..7850898
--- /dev/null
+++ b/app/routes/_data.users._index.tsx
@@ -0,0 +1,66 @@
+/* eslint-disable unicorn/filename-case */
+import { ClipboardIcon, UserIcon } from '@heroicons/react/24/outline'
+import { type LoaderFunctionArgs } from '@remix-run/node'
+import { useLoaderData } from '@remix-run/react'
+import { toast } from 'react-hot-toast/headless'
+
+import Attribute from '~/components/Attribute'
+import StatusCircle from '~/components/StatusCircle'
+import { type Machine, type User } from '~/types'
+import { pull } from '~/utils/headscale'
+import { getSession } from '~/utils/sessions'
+import { useLiveData } from '~/utils/useLiveData'
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ const session = await getSession(request.headers.get('Cookie'))
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const data = await pull<{ nodes: Machine[] }>('v1/node', session.get('hsApiKey')!)
+
+ const users = new Map()
+ for (const machine of data.nodes) {
+ const { user } = machine
+ if (!users.has(user.id)) {
+ users.set(user.id, [])
+ }
+
+ users.get(user.id)?.push(machine)
+ }
+
+ return [...users.values()].map(machines => {
+ const { user } = machines[0]
+
+ return {
+ ...user,
+ machines
+ }
+ })
+}
+
+export default function Page() {
+ const data = useLoaderData()
+ useLiveData({ interval: 3000 })
+
+ return (
+
+ {data.map(user => (
+
+
+
+
+ {user.name}
+
+
+
+ {user.machines.map(machine => (
+
+ ))}
+
+
+ ))}
+
+ )
+}