# dashboard

Admin/CRM dashboard for agents and tenant owners.

Stack: Next.js 14 (App Router), TailwindCSS, hand-written shadcn-style UI
primitives (no Radix dependency), Socket.io-client.

## Status

**Implemented**:

- Login (`/login`) against `POST /api/v1/auth/login`, session stored in
  `localStorage` (`dashboard_token` / `dashboard_user`) via `lib/auth.ts`.
- Authenticated layout (`app/(dashboard)/layout.tsx`) — redirects to `/login`
  when there's no token; renders the Sidebar (Chats / Widget Customizer /
  Analytics / Settings).
- **Chats** (`/chats`) — 3-pane live CRM:
  - left: active conversations list, loaded via REST then kept live over
    Socket.io (`conversation:new` / `conversation:updated`), with unread badges;
  - middle: message thread for the selected conversation (`agent:join_conversation`
    / `agent:send_message`, `chat:typing` in both directions, auto-join/leave
    on selection change), plus a paperclip button to attach a file
    (uploads directly to S3/R2 via a presigned URL from
    `POST /api/v1/uploads/presign`, then sends only the resulting URL);
  - right: visitor info panel (email/name if known, IP, domain, browser,
    session duration — refreshes every 30s).
- **Widget Customizer** (`/widget-customizer`) — edits `WidgetSettings`
  (primary/secondary color, logo, welcome message, launcher position) against
  `GET`/`PATCH /api/v1/widget-settings`, with a live preview panel that mirrors
  the actual widget's CSS. Logo upload is a client-side `FileReader` data URL
  (500KB cap) — a demo-stage stand-in for real object storage (S3/Cloudinary).
- **Settings** (`/settings`) — shows the signed-in user (name, email, role,
  tenant id), an **AI ასისტენტი** section (on/off, provider, reply mode,
  system prompt, knowledge base/FAQ text — against `GET`/`PATCH /api/v1/ai-config`),
  and a logout button.
- **Analytics** (`/analytics`) — placeholder; no metrics wired up yet.
- `lib/use-socket.ts` — `useSocket()` hook: one shared Socket.io connection
  per browser tab (`auth: { type: 'agent', token }`), reused across page
  navigations instead of reconnecting on every mount.

**Not implemented yet**: tenant/agent management, billing, real analytics,
password reset, real object storage for logos, per-tenant AI provider API keys
(the AI provider credentials are platform-level env vars, not per-tenant).

## Structure

```
dashboard/
├── app/
│   ├── layout.tsx, globals.css
│   ├── page.tsx                     # redirects to /chats or /login
│   ├── login/page.tsx
│   └── (dashboard)/
│       ├── layout.tsx                # auth guard + Sidebar
│       ├── chats/page.tsx
│       ├── widget-customizer/page.tsx
│       ├── settings/page.tsx
│       └── analytics/page.tsx
├── components/
│   ├── ui/                           # button, input, textarea, label, badge, separator, switch
│   ├── layout/sidebar.tsx
│   ├── chats/
│   │   ├── conversation-list.tsx
│   │   ├── chat-window.tsx            # message thread + typing + file attachment upload
│   │   ├── message-bubble.tsx         # renders text + image/file attachments
│   │   └── visitor-info-panel.tsx
│   └── widget-customizer/widget-preview.tsx
├── lib/
│   ├── auth.ts                       # DashboardUser + localStorage session helpers
│   ├── api-client.ts                 # REST client (adds Bearer header, typed `api.*` methods)
│   ├── use-socket.ts                 # useSocket() — shared Socket.io connection
│   ├── use-conversations.ts          # conversation list + live updates
│   ├── use-conversation-thread.ts     # message thread + typing + attachments for one conversation
│   ├── use-conversation-detail.ts     # visitor detail fetch for the info panel
│   └── utils.ts                       # cn()
├── next.config.mjs, tailwind.config.ts, tsconfig.json, postcss.config.mjs
├── components.json
└── .env.local.example
```

## Running it locally

The backend must be running first (see `backend/README.md`) with at least
one agent seeded (`pnpm --filter backend db:seed` prints demo login
credentials — by default `agent@demo.com` / `password123`).

```bash
cp dashboard/.env.local.example dashboard/.env.local
# NEXT_PUBLIC_API_URL and NEXT_PUBLIC_SOCKET_URL should point at the backend, e.g. http://localhost:4000

pnpm install                                   # from repo root
pnpm --filter @support-system/shared-types build

pnpm --filter dashboard dev                    # starts on :3000
```

Open `http://localhost:3000`, log in with the seeded agent credentials, and
you should land on `/chats`. Open the widget (`widget/index.html`, see
`widget/README.md`) against the same backend and send a message — a new
conversation should appear in the dashboard's conversation list in real time,
and replies sent from the dashboard should arrive in the widget.

## Verification

```bash
pnpm --filter dashboard exec tsc --noEmit
pnpm --filter dashboard build     # next build — production compile + static page generation
```

Both were run clean against this codebase (Next.js 14.2.5 / TypeScript) as of
the last update to this file — including the AI Settings section, file
attachment upload flow, and multi-language (i18n) support.

## Allowed Domains, Install Widget & Direct Chat Page

- **Widget Customizer** now has an "allowed domains" chip-list editor for
  `WidgetSettings.allowedDomains` — actual enforcement lives in the backend
  (see `backend/README.md`).
- **Install Widget** (`/install-widget`) shows a copyable HTML `<script>`
  embed snippet and a copyable Direct Chat Link, both built from the
  tenant's real `widgetKey` plus `NEXT_PUBLIC_WIDGET_SCRIPT_URL` /
  `NEXT_PUBLIC_DIRECT_CHAT_BASE_URL`.
- **Direct Chat Page** (`app/c/[widgetKey]/page.tsx`, public/no-auth) is a
  standalone full-screen chat UI for the Direct Chat Link — see
  `components/direct-chat/direct-chat-client.tsx`. It talks to the same
  public REST + Socket.io surface as the embedded widget, so behavior stays
  in sync, but it's a separate React implementation rather than mounting
  `widget/dist/widget.js`'s Shadow DOM element (see `CLAUDE.md` §16 for why).
  A `?session=<visitor_token>` query param (set by the widget's "open in a
  new tab" header button) lets the same conversation continue here instead
  of starting a new one.

## Internationalization (i18n)

The dashboard is localized client-side with `i18next` + `react-i18next` —
see `lib/i18n.ts`, `components/i18n-provider.tsx`, and
`components/layout/language-switcher.tsx`. Translation strings live in
`locales/{ka,en,es}.json`; add a new language by adding a new JSON file
there and to `SUPPORTED_LANGUAGES` in `lib/i18n.ts`. The active language is
persisted to `localStorage` and re-applied on every load, browser language
as a fallback, hardcoded `ka` as the ultimate default. See `CLAUDE.md` §15
for the full picture, including the Widget Customizer's separate
per-tenant widget-language settings (`defaultLanguage`/`autoDetectLanguage`/
`customTranslations` on `WidgetSettings`) — those control what *visitors*
see in the embedded widget, independent of what language an agent has the
CRM dashboard set to.
