Skip to content

02 — admin_console_R stack

The modern repo. If the rest of the platform feels stuck in 2018, this one feels like 2025.


At a glance

Concern Choice Version
Framework React ^18.3.1
Language TypeScript ^5.5.3
Bundler Vite ^5.4.1
React plugin @vitejs/plugin-react-swc ^3.5.0
Module system ESM ("type": "module" in package.json)
Routing react-router-dom ^6.26.2
Server-state cache @tanstack/react-query ^5.56.2
Forms react-hook-form + @hookform/resolvers + zod latest
CSS Tailwind 3 + tailwindcss-animate + class-variance-authority ^3.4.11
UI primitives shadcn/ui (50+ Radix wrappers in src/components/ui/) varies
Icons lucide-react ^0.462.0
Toasts sonner + shadcn toast ^1.5.0
Charts recharts ^2.12.7
Date date-fns ^3.6.0
Linter ESLint 9 (flat config) + eslint-plugin-react-hooks + eslint-plugin-react-refresh + typescript-eslint latest
Prettier / pre-commit Not configured
Tests None configured

TypeScript, not JavaScript — and the consequences

Aspect Notes
tsconfig.app.json Application TS config. Verify strict: true — many Vite-React templates ship with strict: false.
Type errors Caught at build time (npm run build), not at runtime
Path alias @src/ (configured in both tsconfig and vite.config.ts)
vite-env.d.ts Vite type augmentation

Junior win: TypeScript catches the field-name typos that bite you elsewhere on the platform. Take advantage — don't use as any.


React 18 + Vite

Concern Where
Entry src/main.tsxcreateRoot(...).render(<App />)
Root app src/App.tsx — provider stack + Routes
HMR Vite + @vitejs/plugin-react-swc — fast
Dev port 8080 (set in vite.config.ts)
Build output dist/

vite.config.ts highlights

export default defineConfig(({ mode }) => ({
  server: {
    host: "::",                        // IPv6 + IPv4 dual-stack
    port: 8080,
    allowedHosts: ["admin.someli.ai"], // only relevant if reverse-proxied
    fs: { strict: true, deny: ['.env', '.git'] },
    hmr: { overlay: false },           // overlay disabled — errors land in terminal
  },
  plugins: [
    react(),
    mode === 'development' && componentTagger(),   // lovable-tagger, dev only
  ].filter(Boolean),
  resolve: {
    alias: { "@": path.resolve(__dirname, "./src") },
  },
}));

componentTagger() is from lovable-tagger. The repo was bootstrapped with Lovable.dev (an AI app-builder). The runtime script cdn.gpteng.co/gptengineer.js is loaded in index.html even in production — without SRI. Known finding. See ../../audit/admin_console_R/security.md.


Routing — react-router-dom v6

All routes declared in src/App.tsx as JSX:

<Routes>
  <Route path="/" element={<Navigate to="/login" replace />} />
  <Route path="/login" element={<Login />} />
  <Route path="/accounts" element={<ProtectedRoute><AppLayout><Accounts /></AppLayout></ProtectedRoute>} />
  {/* ...repeated for ~10 routes */}
</Routes>

Every protected route is wrapped in ProtectedRoute → AppLayout → <Page />. Verbose but explicit. A refactor to a single <ProtectedRoutes> layout-route would tighten this without changing behaviour — but don't do it as your first PR.


Auth — hand-rolled AuthContext

Not @nuxtjs/auth style. Not OAuth. Just:

Piece Where
Context src/context/AuthContext.tsx (~150 lines)
Hook src/hooks/useAuth.tsx
Provider Wrapped in App.tsx
Guard src/components/auth/ProtectedRoute.tsx
Token storage localStorage (key TBD — read AuthContext.tsx)
Login form src/components/auth/LoginForm.tsx
Login page src/pages/Login.tsx (mounts LoginForm)

Token in localStorage is more vulnerable to XSS than cookies. Known platform-wide trade-off. Don't dangerouslySetInnerHTML user content.


API consumption — src/services/api.ts

A single 184-line file. Fetch wrapper + ~25 named API methods.

// Typical pattern
export async function getAccounts(): Promise<Account[]> {
  const res = await fetch(`${ENV.API_URL}/accounts`, {
    headers: {
      'Authorization': `Bearer ${getToken()}`,
      'Apptype': btoa('admin-console'),     // HARDCODED — should come from VITE_APP_TYPE
    }
  });
  return res.json();
}

Junior gotcha: the Apptype header is hardcoded as "admin-console" rather than coming from VITE_APP_TYPE. Known finding (F-2 in the security audit). If you fix this, do it carefully — backends may have hardcoded expectations.

See ../../audit/admin_console_R/api-consumption.md.


Server state — TanStack Query

QueryClient is configured in App.tsx. Queries use useQuery, mutations use useMutation.

const { data, isLoading, error } = useQuery({
  queryKey: ['accounts'],
  queryFn: getAccounts,
});

Compared to someli-platform's manual store/api.js cache, this is much cleaner. Use TanStack Query for any backend data — don't add a parallel state mechanism.


shadcn/ui

src/components/ui/ is generated by the shadcn CLI (components.json). 50+ Radix-based primitives: Button, Dialog, Card, Input, Select, etc.

These files are owned by you, not imported from a package. If a primitive needs a tweak, edit the file directly.

Junior gotcha: don't npm install @radix-ui/react-button and replace shadcn primitives — that breaks the conventional structure. If you need a new primitive, run the shadcn CLI to add it (npx shadcn-ui@latest add <component>).


Tailwind

tailwind.config.ts:

  • darkMode: 'class'
  • SF Pro Display font stack
  • Design-token-driven HSL palette via CSS variables (index.css has :root { --primary: 220 90% 56%; ... } etc.)

Use Tailwind classes inline. Use cn(...) (from src/lib/utils.ts) to compose conditional class lists — that's the shadcn convention.


Forms — react-hook-form + zod

const schema = z.object({ email: z.string().email() });
const form = useForm({ resolver: zodResolver(schema) });

Used in LoginForm.tsx. For new forms, follow the same pattern — it gets you validation + accessibility for free via shadcn's form primitives.


Configuration — typed env

src/config/env.ts:

export const ENV = {
  API_URL:           import.meta.env.VITE_API_URL,
  BASE_URL:          import.meta.env.VITE_BASE_URL,
  APP_URL:           import.meta.env.VITE_APP_URL,
  APP_TYPE:          import.meta.env.VITE_APP_TYPE,
  USER_APP_TYPE:     import.meta.env.VITE_USER_APP_TYPE,
  ACCOUNT_MANAGER:   Number(import.meta.env.VITE_ACCOUNT_MANAGER),
  SUPER_ADMIN:       Number(import.meta.env.VITE_SUPER_ADMIN),
  ADMIN:             Number(import.meta.env.VITE_ADMIN),
  DEVELOPER:         Number(import.meta.env.VITE_DEVELOPER),
  DESIGNER:          Number(import.meta.env.VITE_DESIGNER),
  SOMELI_DEV_URL:    import.meta.env.VITE_SOMELI_DEV_URL,
};

Use ENV.API_URL in code, not import.meta.env.VITE_API_URL directly. Centralised, typed.


Build, lint, test

Build npm run build
Dev build npm run build:dev (sourcemaps + unminified)
Lint npm run lint (the only repo with a working lint command)
Tests None
CI None in repo
Deploy Opaque from the repo
Branches dev, uat, main

Run npm run lint before pushing. It's the only automated check available.


The Lovable.dev runtime script

The index.html includes:

<script src="https://cdn.gpteng.co/gptengineer.js"></script>

This loads Lovable.dev's runtime, which monkey-patches React internals for live editing in Lovable's web IDE. It runs in production as well as in dev — known finding.

Decision points for the team:

  1. Is the Lovable.dev integration still in active use, or vestigial?
  2. If unused, delete the script tag (one line in index.html).
  3. If used, add SRI and consider scoping to dev-only loads.

See ../../audit/admin_console_R/security.md.


Next

03-architecture.md.