--- phase: 03-dashboard-intake-ui plan: "01" type: execute wave: 1 depends_on: [] files_modified: - web/package.json - web/tsconfig.json - web/tsconfig.node.json - web/vite.config.ts - web/tailwind.config.ts - web/postcss.config.cjs - web/index.html - web/src/main.tsx - web/src/App.tsx - web/src/router.tsx - web/src/lib/queryClient.ts - web/src/store/ui.ts - web/src/styles/globals.css - web/components.json - web/src/components/ui/button.tsx - web/src/components/ui/card.tsx - web/src/components/ui/badge.tsx - Makefile autonomous: true requirements: [UI-06] must_haves: truths: - "Running `npm run dev` inside web/ serves the SPA at http://localhost:5173 with /api proxied to :8080" - "Running `npm run build` inside web/ produces web/dist/index.html and web/dist/assets/ that Go can embed" - "The page background is #000000 with neon volt (#faff69) as the accent color" - "Inter font weight 900 is loaded and applied to display headings" - "shadcn/ui Button, Card, and Badge components are available with ClickHouse tokens" - "TanStack Router renders a /health-check route returning a plain text component" - "TanStack Query QueryClientProvider wraps the app" - "Zustand uiStore is defined and importable" artifacts: - path: "web/package.json" provides: "npm project manifest with all dependencies pinned" - path: "web/tailwind.config.ts" provides: "ClickHouse design tokens as Tailwind CSS custom colors/fonts" - path: "web/src/main.tsx" provides: "React 18 entry point with Router + QueryClient providers" - path: "web/src/router.tsx" provides: "TanStack Router route tree with placeholder routes" - path: "web/src/store/ui.ts" provides: "Zustand UI store (view mode toggle, scanner state)" - path: "web/components.json" provides: "shadcn/ui CLI config" - path: "web/src/components/ui/button.tsx" provides: "shadcn/ui Button primitive" - path: "web/src/components/ui/card.tsx" provides: "shadcn/ui Card primitive" - path: "web/src/components/ui/badge.tsx" provides: "shadcn/ui Badge primitive" key_links: - from: "web/src/main.tsx" to: "web/src/router.tsx" via: "RouterProvider from @tanstack/react-router" - from: "web/tailwind.config.ts" to: "web/src/styles/globals.css" via: "@tailwind directives + CSS custom properties" - from: "web/vite.config.ts" to: "http://localhost:8080" via: "proxy: { '/api': 'http://localhost:8080' }" --- Bootstrap the React+TypeScript frontend with the full toolchain and ClickHouse design system. Purpose: Every subsequent UI plan depends on this scaffold existing. The design tokens, component library, routing shell, and build pipeline must all be correct before feature development begins. Output: A buildable SPA in web/ with Vite 5, React 18, TypeScript 5, Tailwind 3, shadcn/ui, TanStack Router v1, TanStack Query v5, Zustand v4, and the ClickHouse visual identity. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/03-dashboard-intake-ui/03-CONTEXT.md @assets.go @web/dist/index.html From assets.go: ```go //go:embed web/dist var StaticFiles embed.FS ``` From internal/api/router.go (SPA fallback handler): ```go // Serves static files from embedded FS; falls back to index.html for client-side routing r.Handle("/*", spaHandler{staticFS: staticFiles}) ``` Task 1: Scaffold Vite+React project and apply ClickHouse design tokens web/package.json, web/tsconfig.json, web/tsconfig.node.json, web/vite.config.ts, web/tailwind.config.ts, web/postcss.config.cjs, web/index.html, web/src/styles/globals.css, web/components.json Read first: `web/dist/index.html` (current stub), `assets.go`, `internal/api/router.go`. **1. Create web/package.json** with these exact dependencies: ```json { "name": "hwlab", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "preview": "vite preview", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" }, "dependencies": { "@radix-ui/react-slot": "^1.1.0", "@tanstack/react-query": "^5.51.0", "@tanstack/react-router": "^1.48.0", "@tanstack/router-devtools": "^1.48.0", "@zxing/browser": "^0.1.5", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "lucide-react": "^0.417.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", "react-hot-toast": "^2.4.1", "tailwind-merge": "^2.5.2", "zustand": "^4.5.4" }, "devDependencies": { "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^7.15.0", "@typescript-eslint/parser": "^7.15.0", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.19", "eslint": "^9.7.0", "postcss": "^8.4.40", "tailwindcss": "^3.4.7", "typescript": "^5.5.3", "vite": "^5.3.5" } } ``` **2. Create web/vite.config.ts:** ```typescript import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import path from 'path' export default defineConfig({ plugins: [react()], resolve: { alias: { '@': path.resolve(__dirname, './src'), }, }, build: { outDir: 'dist', emptyOutDir: true, }, server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, }, }, }, }) ``` **3. Create web/tsconfig.json** (strict mode, path aliases): ```json { "files": [], "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } ] } ``` Create web/tsconfig.app.json: ```json { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "baseUrl": ".", "paths": { "@/*": ["./src/*"] } }, "include": ["src"] } ``` Create web/tsconfig.node.json: ```json { "compilerOptions": { "target": "ES2022", "lib": ["ES2023"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, "strict": true }, "include": ["vite.config.ts"] } ``` **4. Create web/postcss.config.cjs:** ```js module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ``` **5. Create web/tailwind.config.ts** with ClickHouse design tokens: ```typescript import type { Config } from 'tailwindcss' const config: Config = { darkMode: ['class'], content: ['./index.html', './src/**/*.{ts,tsx}'], theme: { extend: { colors: { // ClickHouse design system tokens 'volt': '#faff69', // neon yellow-green — primary CTA accent 'volt-pale': '#f4f692', // active/pressed state text 'forest': '#166534', // primary action button (Get Started) 'forest-dark':'#14572f', // forest border variant 'olive-dark': '#161600', // subtle brand text 'border-olive':'#4f5100', // ghost button borders // Surfaces 'canvas': '#000000', // pure black background 'near-black': '#141414', // button bg, elevated surfaces 'hover-gray': '#3a3a3a', // button hover bg 'charcoal': '#414141', // primary border (used at 80% opacity in CSS) 'deep-charcoal':'#343434',// subtle dividers }, fontFamily: { display: ['Inter', 'system-ui', 'sans-serif'], body: ['Inter', 'system-ui', 'sans-serif'], code: ['Inconsolata', 'monospace'], }, fontWeight: { black: '900', }, borderRadius: { sharp: '4px', card: '8px', }, letterSpacing: { widest: '1.4px', }, }, }, plugins: [], } export default config ``` **6. Create web/index.html** (replace the stub — this is the Vite entry point, not the final dist): ```html HWLab
``` **7. Create web/src/styles/globals.css** with CSS custom properties and Tailwind directives: ```css @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 0%; --foreground: 0 0% 100%; --card: 0 0% 8%; --card-foreground: 0 0% 100%; --border: 0 0% 25%; --ring: 64 100% 71%; --radius: 0.5rem; } * { border-color: rgba(65, 65, 65, 0.8); } body { background-color: #000000; color: #ffffff; } /* Charcoal border utility */ .border-charcoal-80 { border-color: rgba(65, 65, 65, 0.8); } /* ClickHouse card elevation */ .card-elevated { box-shadow: rgba(0,0,0,0.1) 0px 10px 15px -3px, rgba(0,0,0,0.1) 0px 4px 6px -4px; } /* Neon accent glow (use sparingly) */ .ring-volt { box-shadow: 0 0 0 2px #faff69; } /* Uppercase tracked label */ .label-upper { text-transform: uppercase; letter-spacing: 1.4px; font-weight: 600; font-size: 0.75rem; } } ``` **8. Create web/components.json** (shadcn/ui CLI config — dark mode, no rsc, Tailwind aliases): ```json { "$schema": "https://ui.shadcn.com/schema.json", "style": "default", "rsc": false, "tsx": true, "tailwind": { "config": "tailwind.config.ts", "css": "src/styles/globals.css", "baseColor": "zinc", "cssVariables": false, "prefix": "" }, "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" }, "iconLibrary": "lucide" } ``` After creating config files: `cd web && npm install` to generate package-lock.json.
cd /home/mikkel/homelabby/web && npm run build 2>&1 | tail -5 `npm run build` inside web/ completes without errors and produces web/dist/index.html and web/dist/assets/. The tailwind.config.ts contains all ClickHouse color tokens (volt, forest, canvas, charcoal, near-black). The vite.config.ts proxy routes /api to http://localhost:8080.
Task 2: Wire React app entry point with Router, QueryClient, design primitives, and shadcn/ui components web/src/main.tsx, web/src/App.tsx, web/src/router.tsx, web/src/lib/utils.ts, web/src/lib/queryClient.ts, web/src/store/ui.ts, web/src/components/ui/button.tsx, web/src/components/ui/card.tsx, web/src/components/ui/badge.tsx Read first: `web/src/styles/globals.css`, `web/tailwind.config.ts` (just created in Task 1). **1. Create web/src/lib/utils.ts** (shadcn/ui cn helper): ```typescript import { clsx, type ClassValue } from 'clsx' import { twMerge } from 'tailwind-merge' export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } ``` **2. Create web/src/lib/queryClient.ts:** ```typescript import { QueryClient } from '@tanstack/react-query' export const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 30_000, // 30s — inventory data doesn't change rapidly retry: 2, refetchOnWindowFocus: false, }, }, }) ``` **3. Create web/src/store/ui.ts** (Zustand — UI state only, not server data): ```typescript import { create } from 'zustand' type ViewMode = 'grid' | 'list' interface UIStore { viewMode: ViewMode setViewMode: (mode: ViewMode) => void scannerActive: boolean setScannerActive: (active: boolean) => void intakeStep: number setIntakeStep: (step: number) => void } export const useUIStore = create((set) => ({ viewMode: 'grid', setViewMode: (mode) => set({ viewMode: mode }), scannerActive: false, setScannerActive: (active) => set({ scannerActive: active }), intakeStep: 0, setIntakeStep: (step) => set({ intakeStep: step }), })) ``` **4. Create web/src/router.tsx** — TanStack Router v1 route tree with placeholder routes for all Phase 3 views: ```typescript import { createRouter, createRoute, createRootRoute, Outlet } from '@tanstack/react-router' import { TanStackRouterDevtools } from '@tanstack/router-devtools' // Root layout — wraps all routes with the app shell const rootRoute = createRootRoute({ component: () => ( <> {import.meta.env.DEV && } ), }) // Routes — components are lazy-imported in Plan 03 and 04; stubs for now const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: '/', component: () => (

HWLab — Dashboard loading…

), }) const itemRoute = createRoute({ getParentRoute: () => rootRoute, path: '/item/$id', component: () => (

Item detail loading…

), }) const intakeRoute = createRoute({ getParentRoute: () => rootRoute, path: '/intake', component: () => (

Intake wizard loading…

), }) const scanRoute = createRoute({ getParentRoute: () => rootRoute, path: '/scan', component: () => (

QR Scanner loading…

), }) const routeTree = rootRoute.addChildren([indexRoute, itemRoute, intakeRoute, scanRoute]) export const router = createRouter({ routeTree, defaultPreload: 'intent', }) // TypeScript type augmentation for TanStack Router declare module '@tanstack/react-router' { interface Register { router: typeof router } } ``` **5. Create web/src/App.tsx:** ```typescript import { QueryClientProvider } from '@tanstack/react-query' import { RouterProvider } from '@tanstack/react-router' import { Toaster } from 'react-hot-toast' import { queryClient } from '@/lib/queryClient' import { router } from '@/router' export function App() { return ( ) } ``` **6. Create web/src/main.tsx:** ```typescript import React from 'react' import ReactDOM from 'react-dom/client' import './styles/globals.css' import { App } from './App' ReactDOM.createRoot(document.getElementById('root')!).render( , ) ``` **7. Create shadcn/ui Button component at web/src/components/ui/button.tsx** (ClickHouse-themed variants): ```typescript import * as React from 'react' import { Slot } from '@radix-ui/react-slot' import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '@/lib/utils' const buttonVariants = cva( 'inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-volt disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { // Neon primary — #faff69 background, near-black text default: 'bg-volt text-near-black rounded-sharp border border-volt hover:bg-near-black hover:text-white active:text-volt-pale', // Forest green — primary action forest: 'bg-forest text-white rounded-sharp border border-near-black hover:bg-near-black active:text-volt-pale', // Dark solid secondary: 'bg-near-black text-white rounded-sharp border border-near-black hover:bg-hover-gray active:text-volt-pale', // Ghost / outlined outline: 'bg-transparent text-white rounded-sharp border border-border-olive hover:bg-near-black active:text-volt-pale', // Destructive destructive: 'bg-red-900 text-white rounded-sharp hover:bg-red-800', // Invisible — just text ghost: 'bg-transparent text-white hover:text-volt', link: 'text-volt underline-offset-4 hover:underline', }, size: { default: 'h-9 px-4 py-2', sm: 'h-8 px-3 text-xs', lg: 'h-11 px-8', icon: 'h-9 w-9', }, }, defaultVariants: { variant: 'default', size: 'default', }, }, ) export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : 'button' return ( ) }, ) Button.displayName = 'Button' export { Button, buttonVariants } ``` **8. Create web/src/components/ui/card.tsx** (ClickHouse charcoal card): ```typescript import * as React from 'react' import { cn } from '@/lib/utils' const Card = React.forwardRef>( ({ className, ...props }, ref) => (
), ) Card.displayName = 'Card' const CardHeader = React.forwardRef>( ({ className, ...props }, ref) => (
), ) CardHeader.displayName = 'CardHeader' const CardTitle = React.forwardRef>( ({ className, ...props }, ref) => (

), ) CardTitle.displayName = 'CardTitle' const CardContent = React.forwardRef>( ({ className, ...props }, ref) =>
, ) CardContent.displayName = 'CardContent' const CardFooter = React.forwardRef>( ({ className, ...props }, ref) => (
), ) CardFooter.displayName = 'CardFooter' export { Card, CardHeader, CardTitle, CardContent, CardFooter } ``` **9. Create web/src/components/ui/badge.tsx** (status badges with ClickHouse tokens): ```typescript import * as React from 'react' import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '@/lib/utils' const badgeVariants = cva( 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors', { variants: { variant: { default: 'border-volt/40 bg-volt/10 text-volt', indexed: 'border-green-500/40 bg-green-500/10 text-green-400', draft: 'border-charcoal/80 bg-charcoal/20 text-gray-400', needs_research: 'border-yellow-500/40 bg-yellow-500/10 text-yellow-400', researched: 'border-blue-500/40 bg-blue-500/10 text-blue-400', complete: 'border-forest/40 bg-forest/20 text-green-300', destructive: 'border-red-500/40 bg-red-500/10 text-red-400', }, }, defaultVariants: { variant: 'default', }, }, ) export interface BadgeProps extends React.HTMLAttributes, VariantProps {} function Badge({ className, variant, ...props }: BadgeProps) { return
} export { Badge, badgeVariants } ``` **10. Update Makefile** to add frontend build and dev targets: - Read current Makefile first. - Add these targets after existing ones: ```makefile frontend: cd web && npm run build dev-frontend: cd web && npm run dev dev: air -c .air.toml ``` (Note: `dev` already exists — if it does, keep the existing `dev` and add only `frontend` and `dev-frontend`.) Finally run `cd web && npm run build` to verify the build succeeds and web/dist gets populated. cd /home/mikkel/homelabby/web && npm run build 2>&1 | tail -10 && ls dist/assets/*.js 2>/dev/null | head -3 `npm run build` succeeds (exit 0). `web/dist/assets/` contains at least one .js file. `web/src/store/ui.ts` exports `useUIStore` with `viewMode`, `scannerActive`, `intakeStep`. `web/src/router.tsx` exports `router` with routes for `/`, `/item/$id`, `/intake`, `/scan`. All shadcn/ui primitives (Button, Card, Badge) have ClickHouse variants. ## Trust Boundaries | Boundary | Description | |----------|-------------| | Browser → Go /api | All API requests cross this boundary; frontend must not forge hw_id or bypass intake | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-03-01 | Tampering | vite.config.ts proxy | accept | Dev proxy is localhost-only; no external exposure; production uses embedded SPA with no proxy | | T-03-02 | Info Disclosure | google fonts CDN | accept | Only font metrics exposed; no auth tokens; acceptable for homelab context | | T-03-03 | Spoofing | TanStack Router client-side routes | accept | No auth in Phase 3 — all routes are public; server enforces data access via NetBox token | From project root: 1. `cd web && npm run build` — exits 0, produces web/dist/index.html 2. `ls web/dist/assets/*.js` — at least one JS bundle present 3. `go build ./...` — Go binary still compiles (embed path unchanged) 4. `grep -r "volt" web/tailwind.config.ts` — confirms ClickHouse tokens present 5. `grep "proxy" web/vite.config.ts` — confirms /api proxy to :8080 - Vite 5 + React 18 + TypeScript 5 + Tailwind 3 project exists in web/ - `npm run build` produces web/dist/ ready for Go embed - ClickHouse design tokens (volt, forest, canvas, charcoal, near-black) available as Tailwind classes - Inter 900 weight loaded from Google Fonts - TanStack Router renders placeholder routes for /, /item/$id, /intake, /scan - TanStack Query QueryClientProvider wraps the app - Zustand uiStore tracks viewMode, scannerActive, intakeStep - shadcn/ui Button (neon/forest/secondary/outline variants), Card, Badge primitives exist - Makefile has `frontend` and `dev-frontend` targets After completion, create `.planning/phases/03-dashboard-intake-ui/03-01-SUMMARY.md` following the summary template.