Skip to content

Add React web UI embedded in Go binary#411

Merged
wilke merged 3 commits intomasterfrom
feature/UI
Mar 5, 2026
Merged

Add React web UI embedded in Go binary#411
wilke merged 3 commits intomasterfrom
feature/UI

Conversation

@wilke
Copy link
Member

@wilke wilke commented Mar 4, 2026

Summary

  • Adds a React SPA (Vite + React 18 + TypeScript + TailwindCSS v4) served at /ui/ via go:embed
  • Extends shock-ts client with basic auth support, admin methods, and trace download/analysis hooks
  • Adds three new Go endpoints: GET /trace/download, /trace/summary, /trace/events for remote trace analysis

User Features

  • Login with basic auth (username/password)
  • Node browser with pagination, attribute filters, sort controls
  • Node detail with tabbed view: file info, attribute editor, ACL management, index management, storage locations
  • File upload with drag-and-drop, progress bar, optional JSON attributes
  • File download with direct download and pre-authenticated URL support

Admin Features

  • Dashboard showing server info, locked resource counts, auth/anon config
  • Storage location inspector with missing/present node counts
  • Locker view showing locked nodes, files, and indexes
  • Trace controls with start/stop, raw file download, footprint summary table, and parsed event viewer

Infrastructure

  • shock-server/ui/ui.gogo:embed with SPA fallback for client-side routing
  • build-ui.sh — builds shock-ts + shock-ui and copies dist to embed directory
  • Multi-stage Dockerfile with Node.js build step
  • npm workspace linking shock-ts and shock-ui

Test plan

  • cd clients/shock-ui && npm run dev — login as admin:secret, browse nodes, upload file, manage ACLs
  • ./build-ui.sh && ./compile-server.sh — verify embedded UI at http://localhost:7445/ui/
  • docker compose -f docker-compose.local.yml up --build — verify Docker build includes UI
  • Test admin dashboard, trace start/stop/download/summary, location inspector
  • Test non-admin user (user1:secret) — verify admin sidebar links are hidden

🤖 Generated with Claude Code

wilke and others added 2 commits March 3, 2026 20:34
- React 18 SPA with Vite, TypeScript, TailwindCSS v4, React Router v6
- Served at /ui/ via go:embed with SPA fallback routing
- Auth: basic auth login, admin detection via /locker probe
- Pages: node browser with pagination/filters/sort, node detail with
  tabbed view (file info, attributes editor, ACL management, indexes,
  locations), file upload with drag-and-drop and progress, download
  with pre-auth URL support
- Admin pages: server dashboard, storage location inspector, locker
  view, execution trace controls with download/summary/events analysis
- shock-ts client updates: authType (basic/oauth) support, admin
  methods (locker, locked/*, trace, location info), deleteIndex,
  trace download/summary/events, corresponding React hooks
- New Go endpoints: GET /trace/download, /trace/summary, /trace/events
- Multi-stage Dockerfile with Node.js build step
- build-ui.sh script for local development

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- API Token section: copyable Authorization header, curl examples for
  list/upload/download
- Preferences: theme toggle (dark/light), default page size persisted
  to localStorage
- Account info: username and admin/user role badge
- Server Configuration (admin only): version, uptime, auth methods,
  anonymous permissions, attribute indexes
- Sidebar: added Settings link for all users
- NodesPage: reads default page size from settings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an embedded React single-page UI to Shock (served at /ui/ from the Go binary), extends the shock-ts client to support basic auth plus new admin/trace APIs, and wires new server endpoints to support trace download/analysis.

Changes:

  • Embed and serve a Vite-built React SPA under /ui/ via go:embed with SPA fallback routing.
  • Add trace analysis endpoints (/trace/download, /trace/summary, /trace/events) backed by go tool trace, plus client/hooks to consume them.
  • Introduce a clients/ npm workspace (shock-ts + shock-ui), a build script, and Docker multi-stage build for UI artifacts.

Reviewed changes

Copilot reviewed 64 out of 67 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
shock-server/ui/ui.go Embedded SPA file server with SPA fallback routing
shock-server/ui/dist/.gitkeep Placeholder to keep embed directory in repo
shock-server/trace.go Adds helpers to locate latest trace and run go tool trace
shock-server/main.go Adds /trace/* endpoints and mounts embedded UI at /ui/
scratchpad.md Updates scratchpad notes for UI branch and local dev info
clients/shock-ui/vite.config.ts Vite config (base /ui/, aliases, dev proxy)
clients/shock-ui/tsconfig.tsbuildinfo TypeScript incremental build artifact (currently committed)
clients/shock-ui/tsconfig.json TS compiler configuration for the SPA
clients/shock-ui/src/vite-env.d.ts Vite type references
clients/shock-ui/src/providers/ShockUIProvider.tsx Composes React Query + shock-client provider + auth
clients/shock-ui/src/providers/AuthProvider.tsx Basic-auth login state + sessionStorage persistence
clients/shock-ui/src/pages/UploadPage.tsx File upload UI with optional JSON attributes
clients/shock-ui/src/pages/SettingsPage.tsx Settings (theme, page size, token copy helpers)
clients/shock-ui/src/pages/NodesPage.tsx Node browser with filters/sort/pagination
clients/shock-ui/src/pages/NodeDetailPage.tsx Node detail view (download, attributes, ACL, indexes, locations)
clients/shock-ui/src/pages/LoginPage.tsx Login route + redirect when already authenticated
clients/shock-ui/src/pages/AdminTracePage.tsx Admin trace page wrapper
clients/shock-ui/src/pages/AdminLockerPage.tsx Admin locker page wrapper
clients/shock-ui/src/pages/AdminLocationsPage.tsx Admin locations page wrapper
clients/shock-ui/src/pages/AdminDashboardPage.tsx Admin dashboard page wrapper
clients/shock-ui/src/main.tsx React entrypoint
clients/shock-ui/src/lib/utils.ts UI helpers (classnames merge, formatting, basic auth encoding)
clients/shock-ui/src/lib/auth.ts Session storage helpers for auth state
clients/shock-ui/src/lib/api-url.ts Dev vs embedded API base URL selection
clients/shock-ui/src/index.css Tailwind v4 theme tokens and base styles
clients/shock-ui/src/hooks/use-is-admin.ts Convenience hook for admin status
clients/shock-ui/src/hooks/use-auth.ts Auth context + hook
clients/shock-ui/src/components/upload/UploadZone.tsx Drag/drop file selector component
clients/shock-ui/src/components/upload/UploadProgress.tsx Upload progress bar display
clients/shock-ui/src/components/ui/tabs.tsx Custom tabs components
clients/shock-ui/src/components/ui/skeleton.tsx Skeleton loader component
clients/shock-ui/src/components/ui/input.tsx Input component wrapper
clients/shock-ui/src/components/ui/card.tsx Card UI primitives
clients/shock-ui/src/components/ui/button.tsx Button variants via CVA
clients/shock-ui/src/components/ui/badge.tsx Badge variants via CVA
clients/shock-ui/src/components/nodes/Pagination.tsx Pagination controls + page size selector
clients/shock-ui/src/components/nodes/NodeTable.tsx Node list table with actions
clients/shock-ui/src/components/nodes/NodeFilters.tsx Filter parsing + sort controls
clients/shock-ui/src/components/nodes/AttributeEditor.tsx JSON attribute editor with save/reset
clients/shock-ui/src/components/layout/Sidebar.tsx App navigation sidebar (admin links gated)
clients/shock-ui/src/components/layout/Header.tsx Header (server info, theme toggle, logout)
clients/shock-ui/src/components/layout/AppShell.tsx Layout shell for protected routes
clients/shock-ui/src/components/indexes/IndexList.tsx Index CRUD UI
clients/shock-ui/src/components/download/PreAuthButton.tsx Pre-authenticated URL generator + copy
clients/shock-ui/src/components/download/DownloadButton.tsx Direct download via shock client
clients/shock-ui/src/components/auth/ProtectedRoute.tsx Auth/admin route guards
clients/shock-ui/src/components/auth/LoginForm.tsx Login form UI
clients/shock-ui/src/components/admin/TraceControls.tsx Trace start/stop/download + analysis tabs
clients/shock-ui/src/components/admin/LockerView.tsx Locked nodes/files/indexes view
clients/shock-ui/src/components/admin/LocationBrowser.tsx Location inspector with missing/present lists
clients/shock-ui/src/components/admin/Dashboard.tsx Admin dashboard summary cards
clients/shock-ui/src/components/acl/AclPanel.tsx ACL management UI
clients/shock-ui/src/components/ErrorBoundary.tsx Top-level UI error boundary
clients/shock-ui/src/App.tsx Router + code-splitting + protected/admin routing
clients/shock-ui/package.json SPA package manifest (deps + build scripts)
clients/shock-ui/index.html SPA HTML entry
clients/shock-ui/.gitignore Ignores node_modules and dist
clients/shock-ts/src/types.ts Adds authType + admin/location/locker types
clients/shock-ts/src/react/provider.tsx Propagates authType through ShockProvider
clients/shock-ts/src/react/index.ts Exports new admin/index/trace hooks
clients/shock-ts/src/react/hooks.ts Adds admin/index/trace hooks and query keys
clients/shock-ts/src/index.ts Re-exports new types
clients/shock-ts/src/client.ts Adds authType header support + admin APIs + deleteIndex
clients/package.json NPM workspace root for shock-ts + shock-ui
build-ui.sh Builds shock-ts + shock-ui and copies dist for embedding
Dockerfile Adds Node build stage and copies UI dist into embed dir
.gitignore Ignores UI build outputs and node_modules

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +34 to +38
<button
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2",
ctx.value === value && "bg-background text-foreground shadow",
className
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TabsTrigger should set type="button" on the underlying <button>; otherwise, if tabs are used inside a <form>, clicking a tab will submit the form by default.

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +71
{node.file.size > 0 && (
<Link to={`/ui/nodes/${node.id}`}>
<Button variant="ghost" size="icon" title="Download">
<Download className="h-3.5 w-3.5" />
</Button>
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This action is labeled "Download" but it only navigates to the node detail route (/ui/nodes/:id) and doesn’t trigger a download. Either wire this button to the actual download behavior (e.g., call downloadFile / link to the ?download endpoint) or rename it to something like "View" to match the navigation.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +56
cmd := exec.Command(goPath, "tool", "trace", "-d="+mode, traceFilePath)
out, err := cmd.CombinedOutput()
if err != nil {
return out, fmt.Errorf("go tool trace failed: %w\n%s", err, string(out))
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CombinedOutput() buffers the entire go tool trace output in memory and the command can run for a long time on large traces. Consider using exec.CommandContext with a reasonable timeout/cancellation and streaming/limiting output size to reduce memory/CPU risk for this admin endpoint.

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +34
if strings.HasPrefix(e.Name(), "trace.") && strings.HasSuffix(e.Name(), ".log") {
// Skip the currently-active trace file
if traceOn && traceFile != nil && e.Name() == filepath.Base(traceFile.Name()) {
continue
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check for the currently-active trace file reads traceOn/traceFile without synchronization while /trace/start and /trace/stop can update them concurrently, which can cause data races. Protect these globals with a mutex/atomic (and use the same lock in start/stop and latestTraceFile).

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +40
// Fall back to index.html for SPA routing
r.URL.Path = "/"
fileServer.ServeHTTP(w, r)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If dist/index.html is missing (e.g., fresh checkout where only .gitkeep exists), falling back to r.URL.Path = "/" can cause http.FileServer to serve a directory listing. Consider checking for index.html and returning 404/500 with a helpful message and/or disabling directory listings.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/components/errorboundary.tsx","./src/components/acl/aclpanel.tsx","./src/components/admin/dashboard.tsx","./src/components/admin/locationbrowser.tsx","./src/components/admin/lockerview.tsx","./src/components/admin/tracecontrols.tsx","./src/components/auth/loginform.tsx","./src/components/auth/protectedroute.tsx","./src/components/download/downloadbutton.tsx","./src/components/download/preauthbutton.tsx","./src/components/indexes/indexlist.tsx","./src/components/layout/appshell.tsx","./src/components/layout/header.tsx","./src/components/layout/sidebar.tsx","./src/components/nodes/attributeeditor.tsx","./src/components/nodes/nodefilters.tsx","./src/components/nodes/nodetable.tsx","./src/components/nodes/pagination.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/input.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/upload/uploadprogress.tsx","./src/components/upload/uploadzone.tsx","./src/hooks/use-auth.ts","./src/hooks/use-is-admin.ts","./src/lib/auth.ts","./src/lib/utils.ts","./src/pages/admindashboardpage.tsx","./src/pages/adminlocationspage.tsx","./src/pages/adminlockerpage.tsx","./src/pages/admintracepage.tsx","./src/pages/loginpage.tsx","./src/pages/nodedetailpage.tsx","./src/pages/nodespage.tsx","./src/pages/uploadpage.tsx","./src/providers/authprovider.tsx","./src/providers/shockuiprovider.tsx"],"version":"5.9.3"} No newline at end of file
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is a TypeScript incremental build artifact produced by tsc -b and is environment-specific. It shouldn’t be committed; remove it from the repo and add tsconfig.tsbuildinfo (or *.tsbuildinfo) to the appropriate .gitignore.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +29
try {
attributes = JSON.parse(attrsText);
} catch {
return; // Don't upload if JSON is invalid
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the attributes JSON is invalid, this returns early with no user-visible feedback, which can look like the upload button does nothing. Consider storing a parse error in state and rendering an error message near the textarea.

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +27
function TabsList({ children, className }: { children: ReactNode; className?: string }) {
return (
<div className={cn("inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground", className)}>
{children}
</div>
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom tabs UI is missing ARIA roles/attributes and keyboard interaction (e.g., role="tablist"/"tab", aria-selected, roving tabIndex, arrow-key navigation), which hurts accessibility for keyboard and screen-reader users. Consider adding these semantics or using an accessible tabs primitive.

Copilot uses AI. Check for mistakes.
Comment on lines 170 to 171
responder.RespondWithData(w, r, "trace stoped")
})
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Response string has a typo ("trace stoped"). Since this text is user-visible (UI uses it), please correct it to "trace stopped" for consistency and clearer UX.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +15
export function getStoredPageSize(): number {
const stored = localStorage.getItem(PAGE_SIZE_KEY);
return stored ? Number(stored) : 25;
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getStoredPageSize() can return NaN if localStorage contains a non-numeric value (or empty string), which then propagates into query limit values. Consider validating with Number.isFinite and falling back to a sane default (and possibly clamping to allowed options).

Copilot uses AI. Check for mistakes.
Fix issues raised in PR #411 review: add type="button" and ARIA roles
to tabs, rename misleading Download button to View, add mutex for trace
globals, add 60s timeout to go tool trace, guard against missing
index.html in SPA handler, remove committed tsbuildinfo, show JSON
parse errors on upload, fix "stoped" typo, validate stored page size.
Make SHOCK_PORT configurable in docker-compose.local.yml.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@wilke wilke merged commit 5d05778 into master Mar 5, 2026
@wilke wilke deleted the feature/UI branch March 5, 2026 19:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants