원클릭으로
bedrud-fe-platform
Frontend foundation — routing, API client, build/dev ports, auth hooks, types.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Frontend foundation — routing, API client, build/dev ports, auth hooks, types.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Admin endpoints — users, rooms, queue, settings, invite tokens, webhooks, overview, recordings.
Auth endpoints — JWT flow, local/OAuth/passkey/verify/password-reset/avatar/preferences.
Room endpoints — CRUD, join, moderation, presence, chat upload, stage stubs, recording.
Complete Bedrud API endpoint reference. Dense index of every route, middleware, status code; deep detail in leaf skills.
All DTO definitions, source file index, Swagger reference.
Auth service + middleware — JWT, passkeys, OAuth, session store, rate limiting, recordings gate.
| name | bedrud-fe-platform |
| description | Frontend foundation — routing, API client, build/dev ports, auth hooks, types. |
| license | Apache License |
React 19 SPA. apps/web/. TanStack Router (file-based) + TanStack Query 5 + TailwindCSS v4 + Bun + Biome.
Path aliases: #/* → ./src/*, @/* → ./src/*. Prefer #/*. Never relative ../src/*.
Owns: routes map, lib/api.ts, platform hooks, Vite/tsconfig/biome, vendor resolve aliases.
Does not own: Zustand store internals → bedrud-fe-state; meeting runtime/whiteboard → bedrud-fe-meeting; admin UI → bedrud-fe-admin; shadcn/UI chrome → bedrud-fe-ui-foundation.
src/router.tsxgetRouter() — TanStack Router.
| Option | Value |
|---|---|
scrollRestoration | true |
defaultPreload | 'intent' |
defaultPreloadStaleTime | 0 |
defaultNotFoundComponent | <ErrorPage variant="not-found" /> |
defaultErrorComponent | msg starts with 404 → not-found; else <ErrorPage variant="server" /> |
Registers router type on @tanstack/react-router.
src/routes/__root.tsx — App shellcreateRootRoute + shellComponent: RootDocumentviewport-fit=cover), title Bedrud, styles.css, favicons, manifest.json, inline theme script (reads localStorage key theme before paint)QueryClient: retry: 1, staleTime: 30_000ErrorBoundary → IntlProvider (en + #/locales/en.json) → QueryClientProvider + Sonner Toaster#main-contentapplyTheme on store change + OS prefers-color-scheme listeneruseAuthStore.getState().initialize() (non-blocking; protected routes await in beforeLoad)src/routeTree.gen.tsAuto-generated by TanStack Router. Never edit. ~32 route modules (1 root + 5 layouts + leaves).
scripts/embed.mjsProd embed bridge: Vite build → SSR shell → copy client assets → server/frontend/ (index.html + shell.html). Invoked via bun run build:embed.
vite.config.ts)| Constant | Port | Role |
|---|---|---|
DEV_PORT_WEB | 7070 | Vite / TanStack Start |
DEV_PORT_API | 7071 | Go API (make dev) |
DEV_PORT_LIVEKIT | 7072 | Embedded LiveKit (local only) |
DEV_PORT_DEVTOOLS | 7074 | TanStack devtools event bus |
| Path | Target | Notes |
|---|---|---|
/api | http://localhost:7071 | API |
/uploads | http://localhost:7071 | Chat/media uploads via API |
/livekit | http://127.0.0.1:7072 | Direct to LiveKit — ws: true, changeOrigin: true, rewrite strips /livekit prefix |
Do not chain /livekit through the Go API — double WS proxy breaks /rtc/v1/validate.
Remote debug (BEDRUD_ALLOWED_HOSTS non-empty): HMR disabled (prevents LiveKit PC teardown). Bind host: BEDRUD_DEV_BIND_HOST. Allowed hosts: localhost, 127.0.0.1, + env list.
resolve.alias:
/^@\/(.*)$/ → src/$1
/^#\/(.*)$/ → src/$1
...excalidrawAliases(appRoot) // see Vendor aliases
optimizeDeps.include:
@livekit/components-react, @livekit/krisp-noise-filter, livekit-client,
roughjs, jotai, perfect-freehand, yjs, y-protocols/sync, lib0/encoding, lib0/decoding
| Chunk | Match |
|---|---|
meeting-context | /components/meeting/MeetingContext |
react-vendor | react / react-dom |
tanstack-vendor | @tanstack/ |
livekit-client-vendor | livekit-client |
livekit-components-vendor | @livekit/components-react |
charts-vendor | recharts / d3- |
ui-vendor | @radix-ui/ |
markdown-vendor | react-markdown, remark, unified, rehype, hast, mdast, micromark, vfile |
excalidraw-vendor | /src/vendor/excalidraw/ |
vendor | remaining node_modules except krisp, rnnoise-wasm, mermaid / @excalidraw/mermaid-* / markdown-to-text (kept lazy) |
chunkSizeWarningLimit: 6000.
Plugins: @tanstack/devtools-vite, @tailwindcss/vite, @tanstack/react-start/plugin/vite, @vitejs/plugin-react.
Vitest: jsdom, src/**/*.test.ts(x).
Source: src/vendor/excalidraw/aliases.ts — platform owns Vite/tsconfig wiring. Whiteboard UI/runtime stays in meeting skill.
excalidrawAliases(appRoot) maps workspace packages under src/vendor/excalidraw/packages/:
| Import | Resolves to |
|---|---|
@excalidraw/common (+ /*) | packages/common/src |
@excalidraw/element (+ /*) | packages/element/src |
@excalidraw/math (+ /*) | packages/math/src |
@excalidraw/utils (+ /*) | packages/utils/src |
@excalidraw/fractional-indexing | packages/fractional-indexing/src |
@excalidraw/laser-pointer | packages/laser-pointer/src |
@excalidraw/excalidraw | packages/excalidraw/index.tsx |
@excalidraw/excalidraw/* | packages/excalidraw/* |
@excalidraw/excalidraw/types | packages/excalidraw/types.ts |
@excalidraw/excalidraw/element/types | packages/element/src/types.ts |
Mirrored in tsconfig.json paths. Biome excludes src/vendor.
__root__ QueryClient, theme, Intl, auth init
├── / landing + guest join (no forced auth redirect)
├── /$ splat → ErrorPage not-found
├── /new auth → POST /api/room/create → /m/$meetId
├── /auth layout (split-screen); redirect /dashboard if tokens
│ ├── /auth/ guest join form
│ ├── /auth/login email/pass + passkey + OAuth; ?redirect=
│ ├── /auth/register name/email/pass + invite token
│ ├── /auth/callback OAuth cookie → GET /api/auth/me
│ ├── /auth/verify email verify (?token|status|reason)
│ ├── /auth/forgot-password POST /api/auth/forgot-password
│ └── /auth/reset-password ?token → POST reset
├── /dashboard layout sidebar+topbar; require tokens
│ ├── /dashboard/ My Rooms + Recent
│ ├── /dashboard/archived/$roomId archived room detail (placeholder/TODO)
│ ├── /dashboard/settings settings layout tabs
│ │ ├── /dashboard/settings/ profile
│ │ ├── /dashboard/settings/security
│ │ ├── /dashboard/settings/audio
│ │ └── /dashboard/settings/video
│ └── /dashboard/admin admin layout; admin|superadmin|moderator
│ ├── /dashboard/admin/ overview KPIs
│ ├── /dashboard/admin/queue
│ ├── /dashboard/admin/rooms
│ ├── /dashboard/admin/rooms/$roomId
│ ├── /dashboard/admin/rooms/events
│ ├── /dashboard/admin/users
│ ├── /dashboard/admin/users/$userId
│ ├── /dashboard/admin/users/recent-signups
│ ├── /dashboard/admin/recordings placeholder ("future release")
│ └── /dashboard/admin/settings blocks moderator; admin/superadmin only
└── /m/$meetId live meeting (meeting skill)
| File | Path | Type | Guard / loader | Purpose |
|---|---|---|---|---|
__root.tsx | (root) | Layout | — | Shell, providers, theme, auth init |
index.tsx | / | Leaf | beforeLoad: init auth only; loader: me if tokens | Landing + guest join |
$.tsx | /$ | Leaf | — | Catch-all not-found |
new.tsx | /new | Leaf | auth required; loader creates room | Instant meeting |
auth.tsx | /auth | Layout | tokens → /dashboard | Auth chrome + <Outlet /> |
auth.index.tsx | /auth/ | Leaf | — | Guest join |
auth.login.tsx | /auth/login | Leaf | search redirect? | Login |
auth.register.tsx | /auth/register | Leaf | — | Register |
auth.callback.tsx | /auth/callback | Leaf | — | OAuth completion |
auth.verify.tsx | /auth/verify | Leaf | search token/status/reason | Email verification |
auth.forgot-password.tsx | /auth/forgot-password | Leaf | — | Request reset email |
auth.reset-password.tsx | /auth/reset-password | Leaf | search token | Set new password |
dashboard.tsx | /dashboard | Layout | no tokens → /auth; loader /api/auth/me | App chrome |
dashboard.index.tsx | /dashboard/ | Leaf | — | Rooms list + recent |
dashboard/archived_.$roomId.tsx | /dashboard/archived/$roomId | Leaf | — | Archived room (stub UI) |
dashboard/settings.tsx | /dashboard/settings | Layout | — | Settings tabs |
dashboard/settings/index.tsx | /dashboard/settings/ | Leaf | — | Profile |
dashboard/settings/security.tsx | /dashboard/settings/security | Leaf | — | Password |
dashboard/settings/audio.tsx | /dashboard/settings/audio | Leaf | — | Audio prefs |
dashboard/settings/video.tsx | /dashboard/settings/video | Leaf | — | Video prefs |
dashboard/admin.tsx | /dashboard/admin | Layout | loader: me + allow admin/superadmin/moderator; else /dashboard | Admin shell + AdminContext (read-only for mods) |
dashboard/admin/index.tsx | /dashboard/admin/ | Leaf | — | Overview |
dashboard/admin/queue.tsx | /dashboard/admin/queue | Leaf | — | Queue stats |
dashboard/admin/rooms.tsx | /dashboard/admin/rooms | Leaf | — | Rooms table |
dashboard/admin/rooms_.$roomId.tsx | /dashboard/admin/rooms/$roomId | Leaf | — | Room detail |
dashboard/admin/rooms_.events.tsx | /dashboard/admin/rooms/events | Leaf | — | Room events |
dashboard/admin/users.tsx | /dashboard/admin/users | Leaf | — | Users table |
dashboard/admin/users_.$userId.tsx | /dashboard/admin/users/$userId | Leaf | — | User detail |
dashboard/admin/users_.recent-signups.tsx | /dashboard/admin/users/recent-signups | Leaf | — | Recent signups |
dashboard/admin/recordings.tsx | /dashboard/admin/recordings | Leaf | — | Placeholder |
dashboard/admin/settings.tsx | /dashboard/admin/settings | Leaf | beforeLoad: block moderator / non-admin | System settings tabs |
m.$meetId.tsx | /m/$meetId | Leaf | join flow in-page | Live meeting |
Counts: 32 route modules · 5 layouts (__root, auth, dashboard, settings, admin) · rest leaves.
| Area | Rule |
|---|---|
/auth/* layout | Tokens present → /dashboard |
/ | Does not redirect authenticated users (landing stays public) |
/dashboard | No tokens → /auth; loader loads user |
/new | No tokens → /auth/login?redirect=/new |
/dashboard/admin | Need isSuperAdmin or access admin or moderator |
/dashboard/admin/settings | Moderator blocked; need admin or superadmin |
src/lib/api.tsapi.get<T>(path, opts?)
api.post<T>(path, body?, opts?)
api.put<T>(path, body?, opts?)
api.delete<T>(path, opts?)
export const API_URL // same as BASE_URL
export class ApiError extends Error // status, body, parsedBody, message
| Concern | Behavior |
|---|---|
| Base URL | VITE_API_URL or '' so dev hits relative /api → Vite proxy → :7071 |
| Auth | Authorization: Bearer <accessToken> when present |
| CSRF fallback | No bearer → X-CSRF-Token from <meta name="csrf-token"> or csrf_token cookie |
| Credentials | include always (HTTP-only refresh cookie) |
| Body | JSON stringify unless FormData (then no forced Content-Type) |
| 401 refresh | If request had access token: singleton POST /api/auth/refresh → update tokens → retry once. Fail → clear() + location.replace('/auth'). Skip on refresh path / unauthenticated 401 / already-retried |
| Errors | Non-OK → ApiError with user message from JSON error or message |
Refresh response keys: snake_case access_token, refresh_token.
| File | Export | Notes |
|---|---|---|
handle-auth-success.ts | useHandleAuthSuccess(), AuthResponse | setTokens + setUser (isSuperAdmin/isAdmin from accesses) + navigate (redirectTo or /dashboard) |
webauthn.ts | base64ToBuffer, bufferToBase64, request/creation option types | Passkey encode helpers |
jwt-user.ts | decodeBedrudJwt(token) | { userId, accesses } via jwt-decode |
use-public-settings.ts | getPublicSettings, refreshPublicSettings, usePublicSettings | Module cache of GET /api/auth/settings |
| File | Export | Notes |
|---|---|---|
use-admin-overview.ts | useAdminOverview(), AdminOverview + nested types | GET /api/admin/overview, poll 60s |
use-queue-stats.ts | useQueueStats(), QueueStats, FailedJobSummary | GET /api/admin/queue, poll 10s |
| File | Export | Notes |
|---|---|---|
utils.ts | cn(...) | clsx + twMerge |
errors.ts | getErrorMessage(error, fallback) | Strip status prefix; parse JSON message/error/detail |
i18n.ts | useI18n, FormattedMessage, messages, defaultLocale | Thin react-intl wrap (en) |
user-preferences.ts | parseUserPreferences, patchUserPreferences | Server prefs blob merge → PUT-style preferences API |
useLongPress.ts | useLongPress(cb, ms=500) | Touch long-press; cancel if drift >10px |
text-direction.ts | textStartsRtl, textDirectionFor | RTL heuristics |
Not platform: meeting-only modules in lib/ (livekit-publish, meeting-sounds, rnnoise-processor, audio-processor.service, pin/PTT helpers, etc.) and components/meeting/chat/useChatPersistence → meeting skill. Stores → bedrud-fe-state.
| Type | File | Shape |
|---|---|---|
AuthTokens | lib/auth.store.ts | { accessToken: string, refreshToken: string | null } |
User | lib/user.store.ts | { id, email, name, provider, isSuperAdmin, isAdmin, accesses: string[] | null, avatarUrl? } |
Theme | lib/theme.store.ts | 'light' | 'dark' | 'system' |
AudioPreferences | lib/audio-preferences.store.ts | NS mode, echo, AGC, gain, gate, muted beep + interval, pushToTalkEnabled, pushToTalkKey |
NoiseSuppressionMode | same | 'none' | 'browser' | 'rnnoise' | 'krisp' |
VideoPreferences | lib/video-preferences.store.ts | { mirrorWebcam: boolean } |
RecentRoom | lib/recent-rooms.store.ts | { name, joinedAt } |
AuthResponse | lib/handle-auth-success.ts | { user, tokens } (login/passkey payloads) |
// lib/use-public-settings.ts
PublicSettings: {
serverName: string
registrationEnabled: boolean
tokenRegistrationOnly: boolean
guestLoginEnabled: boolean
passkeysEnabled: boolean
oauthProviders: string[]
requireEmailVerification: boolean
chatMaxMessageCount: number
chatMessageTTLHours: number
recordingsEnabled: boolean // TODO feature flag surface
}
src/types/admin.ts| Export | Purpose |
|---|---|
AdminUser | id, email, name, provider, isActive, accesses, createdAt |
AdminRoom | room DTO + optional settings / owner / activity fields |
ROLE_OPTS | superadmin · admin · moderator · user · guest |
ROLE_ACCESS_MAP | role → accesses[] |
detectRole, getRoleLabel | map accesses → role label |
See use-admin-overview.ts (AdminOverview, health, KPIs, activity, composition, attention items, signups, events, instance) and use-queue-stats.ts (pending/active/done/failed rates + email queue fields).
Chat/meeting message types live under meeting components — not this skill.
tsconfig.jsonreact-jsx, ESNext, moduleResolution: bundler#/*, @/*, full @excalidraw/* mirrornoUnusedLocals, noUnusedParameters, noFallthroughCasesInSwitch, noUncheckedSideEffectImportsverbatimModuleSyntax, allowImportingTsExtensions, noEmitvite/client, @types/reactbiome.jsondist, node_modules, src/routeTree.gen.ts, src/vendor, **/*.csscomponents.jsonshadcn: default style, CSS vars, slate base, Lucide. Aliases: @/components, @/lib/utils, @/components/ui.
package.json scripts| Script | Command |
|---|---|
dev | vite dev |
build | bun --bun vite build |
build:embed | node scripts/embed.mjs |
check | biome + tsc --noEmit |
lint / lint:fix / format | Biome |
typecheck | tsc --noEmit |
test | vitest run |
Key runtime deps: react 19, livekit-client 2.x, @livekit/components-react, zustand 5, TanStack router/start/query, zod 4, tailwindcss 4, recharts, yjs, sonner, react-intl.
router.tsx → routeTree.gen.ts
__root.tsx → QueryClient, theme.store, auth.store.initialize, Intl, ErrorBoundary
routes/* → lib/api.ts → auth.store (Bearer + refresh)
→ handle-auth-success / webauthn / use-public-settings
→ use-admin-overview / use-queue-stats (admin leaves)
→ types/admin.ts (admin tables)
vite.config → ports 7070/7071/7072, proxy, excalidrawAliases, manualChunks