forked from unom/punktfunk
**Recent activity.** The console could describe the present — a status snapshot — but never the recent past. A client that connected and left while you were on another page left no trace anywhere you could look, and the host's own log is a developer artifact rather than a narrative. The event stream was already open for cache invalidation, so a feed costs one ring buffer next to it: every frame is recorded, labelled per kind, and rendered newest-first on the dashboard. Deliberately in-memory and bounded to 200. It starts empty on a page load and fills as things happen, which is the honest shape for a live tail — an audit trail would need the host to keep one, and pretending otherwise would be worse than not having it. **Connect a device.** The console knew the host's address and identity all along and never offered either in a form you could hand to a phone: pairing meant reading an IP off the Host page and retyping it on a couch. There is a card now with the address and a `punktfunk://connect/<uniqueid>` deep link, both copyable — the link is the shipped client grammar (clients/shared/deeplink-vectors.json), so an installed client opens straight onto this host. No QR: rendering one needs an encoder we do not bundle, and a wrong QR is worse than none. **Installable.** A web manifest and the theme/apple meta tags, so the console can live on a phone's home screen — which is where it is used from as often as from a desk. No service worker on purpose: an offline shell for a console whose every screen is live host state would only ever show stale numbers convincingly. The manifest is reachable without a session (install needs it, and it says nothing the login page doesn't); /api stays gated, verified. Verified in a browser: three host-emitted events appear in the feed with the right labels, the deep link renders and copies as `punktfunk://connect/abc123`, and the manifest serves 200 as application/manifest+json while /api/v1/host still answers 401. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
86 lines
2.9 KiB
TypeScript
86 lines
2.9 KiB
TypeScript
/// <reference types="vite/client" />
|
|
|
|
import type { QueryClient } from "@tanstack/react-query";
|
|
import {
|
|
createRootRouteWithContext,
|
|
HeadContent,
|
|
Outlet,
|
|
Scripts,
|
|
useRouterState,
|
|
} from "@tanstack/react-router";
|
|
import "@fontsource-variable/geist";
|
|
import { Toaster } from "@unom/ui/toast";
|
|
import { MotionConfig } from "motion/react";
|
|
import { useEffect } from "react";
|
|
import { AppShell } from "@/components/app-shell";
|
|
import { adoptStoredLocale, useLocale } from "@/lib/i18n";
|
|
import appCss from "@/styles.css?url";
|
|
|
|
export interface RouterContext {
|
|
queryClient: QueryClient;
|
|
}
|
|
|
|
export const Route = createRootRouteWithContext<RouterContext>()({
|
|
head: () => ({
|
|
meta: [
|
|
{ charSet: "utf-8" },
|
|
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
|
{ name: "color-scheme", content: "dark light" },
|
|
{ name: "theme-color", content: "#6c5bf3" },
|
|
{ name: "apple-mobile-web-app-capable", content: "yes" },
|
|
{ name: "apple-mobile-web-app-title", content: "Punktfunk" },
|
|
{ title: "Punktfunk" },
|
|
],
|
|
links: [
|
|
{ rel: "stylesheet", href: appCss },
|
|
{ rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
|
|
// Installable on a phone — this console is used from a couch as often as from a desk,
|
|
// and a home-screen launcher beats retyping a LAN IP. Standalone display, no service
|
|
// worker: an offline shell for a console whose every screen is live host state would
|
|
// only ever show stale numbers convincingly.
|
|
{ rel: "manifest", href: "/manifest.webmanifest" },
|
|
],
|
|
}),
|
|
component: RootComponent,
|
|
});
|
|
|
|
function RootComponent() {
|
|
// Adopt the persisted/browser locale AFTER hydration — the initial render stays at the base
|
|
// locale to match SSR (see lib/i18n.ts), so this is the single, mismatch-free locale switch.
|
|
useEffect(() => {
|
|
adoptStoredLocale();
|
|
}, []);
|
|
// `lang` must track the locale the page is actually rendered in — it is what tells a screen
|
|
// reader which pronunciation to use, and it was pinned to "en" while the app switched to German
|
|
// underneath it. `adoptStoredLocale` also sets it on the live document; this keeps SSR honest.
|
|
const locale = useLocale();
|
|
// The login screen renders bare (no sidebar); everything else gets the app shell.
|
|
const isLogin = useRouterState({
|
|
select: (s) => s.location.pathname === "/login",
|
|
});
|
|
return (
|
|
<html lang={locale} className="dark">
|
|
<head>
|
|
<HeadContent />
|
|
</head>
|
|
<body className="min-h-screen">
|
|
{/* Motion defaults to `reducedMotion: "never"`, so every card, nav item and button
|
|
animated at full strength even for someone whose OS asks for less. "user" honours
|
|
the OS setting. */}
|
|
<MotionConfig reducedMotion="user">
|
|
{isLogin ? (
|
|
<Outlet />
|
|
) : (
|
|
<AppShell>
|
|
<Outlet />
|
|
</AppShell>
|
|
)}
|
|
</MotionConfig>
|
|
{/* Sonner toaster (lazy client-side) — success feedback for auto-saved settings. */}
|
|
<Toaster />
|
|
<Scripts />
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|