# Yellow Stone Traders & General Supplies — Website, Admin Dashboard & AI Chat

A full self-hosted platform for **Yellow Stone Traders & General Supplies**
(Mhongolo, Kahama District, Shinyanga Region, Tanzania), founded by
**Matemla Michael Lameck**.

```
yst-platform/
├── public/     the marketing site (home, about, services, projects, insights, contact)
├── admin/      the admin dashboard (login, messages, subscribers, content, media, settings)
└── server/     Express + node:sqlite backend — serves both of the above, plus /api/*
```

Only **one Node process** runs everything: it serves `public/` at `/`, `admin/` at
`/admin`, and the JSON API at `/api/*`.

## What's included

- **Public site** — light/dark themeable design with a distinct Three.js hero
  animation on every page (see `public/assets/`), a floating AI chat widget
  (bottom-left), a Contact/quote-request form and newsletter signup wired to
  the real backend, and a first-party, cookie-free pageview beacon
  (`public/assets/analytics.js`) on every page. The **Projects** and
  **Insights** pages pull in anything you publish through the admin
  dashboard automatically.
  - Home — `scene-gold.js`: a cluster of low-poly gold ore nuggets
  - Services — `scene-house.js`: a gold house under construction + crane
  - About — `scene-ingots.js`: a small stacked vault of gold ingots
  - Projects — `scene-pins.js`: gold map-pin markers scattered on a site plan
  - Insights — `scene-plates.js`: a fanned stack of gold-edged document panels
  - Contact — `scene-envelope.js`: a gold envelope with a wax-seal medallion, plus floating nodes for phone/email/visit
- **Admin dashboard** (`/admin`) — session-login protected:
  - **Dashboard** — stat cards, a 14-day traffic sparkline, and a live recent-activity feed
  - **Messages** — quote-request / contact leads, mark read/unread, delete
  - **Subscribers** — newsletter list, CSV export
  - **Posts** (`content.html`) — CRUD for **Projects** (categories: roads,
    government, buildings, mining), **Insights** (guides / company updates),
    **Press**, and **Events** (site visits, tender openings)
  - **Media** — dedicated photo/video gallery manager with drag-and-drop upload,
    feeding the public Insights page's gallery
  - **Analytics** — traffic overview: daily views chart, top pages, top referrers, devices
  - **Security** — account lockout status, login-attempt history, and the full audit
    log of every add/edit/delete/publish action, with a one-click unlock
  - **Settings** — change the admin password, with a live strength meter
- **Chatbot** — floating widget on every public page, backed by `/api/chat`,
  answering questions about road contracting, government/private construction,
  building construction, railways/utility projects, mining equipment supply,
  ore-processing chemicals, and blueprint/consultancy services. Calls Groq (or
  NVIDIA) if configured; otherwise falls back to canned keyword-based replies
  so nothing breaks before you add an API key.

## Company details already wired in

- Founder: **Matemla Michael Lameck** (photo at `public/assets/founder.jpg`)
- Phone: **+255 787 094 468**
- Address: Mhongolo Ward, Kahama District, Shinyanga Region — P.O. Box 472,
  Postal Code 37310, Tanzania
- Primary Mining Licence **PML04335MBG** (Ministry of Minerals — Mining
  Commission), referenced on the About/Contact pages as a trust signal
- Email is currently a placeholder — **update `info@yst.co.tz`**
  throughout `public/`, `server/src/llm/provider.js`, and this README once you
  have a live company inbox and/or domain.

## Local setup

Requires **Node 22.5+** (built-in `node:sqlite`).

```bash
cd server
cp .env.example .env      # then edit .env — see below
npm install
npm run dev                # http://localhost:3000
```

Visit:
- `http://localhost:3000/index.html` — public site
- `http://localhost:3000/admin/login.html` — admin dashboard

**First-run login:** username/password come from `ADMIN_INITIAL_USERNAME` /
`ADMIN_INITIAL_PASSWORD` in `.env` (defaults: `admin` / `ChangeMe123!`). This is
only used the very first time the database is created — **log in and change the
password immediately** via Settings.

Sample "Sample entry: …" rows are seeded into Projects, Insights, Press, Media
and Events on first run purely so the dashboard and public pages aren't empty —
edit or delete them from `/admin` and replace with real content.

## Configuring the chatbot

In `.env`:

```
LLM_PROVIDER=groq
GROQ_API_KEY=your-key-here
GROQ_MODEL=llama-3.1-8b-instant
```

Get a free Groq API key at https://console.groq.com. To switch to NVIDIA instead,
set `LLM_PROVIDER=nvidia` and fill in `NVIDIA_API_KEY` / `NVIDIA_MODEL`. If neither
key is set, the widget still works but replies with a simple canned/offline message
tuned to construction/mining questions (see `server/src/llm/provider.js`).

## Deploying to cPanel (or similar Node hosting)

1. In cPanel → **Setup Node.js App**, create an app:
   - Node version: **22.x**
   - Application root: the `server/` folder (upload `public/` and `admin/` one level
     above it, i.e. as siblings — the app expects `../public` and `../admin` relative
     to `server/`)
   - Application startup file: `server.js`
   - Application URL: your domain
2. Open the app's terminal / "Run NPM Install" and set the **start command** to:
   ```
   node --experimental-sqlite server.js
   ```
   (or use the `NODE_OPTIONS` workaround below if your cPanel version doesn't
   expose a custom start command.)
3. Set environment variables in the cPanel Node app UI (or upload a `.env` file):
   `SESSION_SECRET`, `ADMIN_INITIAL_USERNAME`, `ADMIN_INITIAL_PASSWORD`,
   `LLM_PROVIDER`, `GROQ_API_KEY`, `NODE_ENV=production`.
4. Restart the app. The `data/` folder (SQLite file) and `public/assets/uploads/`
   (media uploads) are both created automatically on first boot — make sure the
   app's working directory is writable.
5. Once confirmed working, log into `/admin`, change the seeded password, and
   rotate `ADMIN_INITIAL_PASSWORD` out of your env if you like.
6. Update `sitemap.xml` and `robots.txt` in `public/` with your real domain.

**Note on `node:sqlite`:** it's still an experimental Node API. If your hosting
Node/Passenger setup doesn't let you pass `--experimental-sqlite` directly, set
`NODE_OPTIONS=--experimental-sqlite` as an environment variable instead.

## Security notes

- Passwords are hashed with `scrypt` (Node's built-in `crypto`) — no external
  dependency needed. New passwords must be 10+ characters and mix character
  classes (enforced both client- and server-side).
- **Login lockout** — after 3 failed sign-ins the account locks for 30s; this
  escalates to 2 min (5 fails), 10 min (8 fails), and 1 hour (12+ fails). A
  successful sign-in resets the counter. Admins can review and manually clear
  a lockout on the **Security** page.
- **Audit log** — every create, update, delete, publish/unpublish, login,
  failed login and logout is recorded with who, what, and from which IP —
  visible on the **Security** page and summarized on the **Dashboard**.
- Sessions use `express-session` with the default in-memory store (fine for a
  single low/medium-traffic instance) and auto-expire after 30 minutes of
  inactivity on top of the 8-hour absolute session length.
- Contact, newsletter, chat, login and analytics-beacon endpoints are all
  rate-limited per IP. Baseline security headers (X-Frame-Options, nosniff,
  Referrer-Policy, HSTS in production) are set on every response.
- Media uploads accept JPG/PNG/WEBP/GIF up to 4MB, are re-encoded to a random
  filename, and are only reachable behind `requireAdmin`.
- The analytics beacon never stores a raw IP address — only a salted,
  one-way hash — and skips anything under `/admin`.
- Change `SESSION_SECRET` and the seeded admin password before going live.

## Light/dark mode

The public site now defaults to a full **light theme** (off-white background,
dark charcoal text) site-wide, with a sun/moon toggle button in the header
(next to "Request a Quote") that switches to the **original full dark theme**
— the same charcoal/gold look from the first build. This is a genuine global
toggle, not per-section: hero, header, footer, capabilities, everything flips
together.

- The choice is saved to `localStorage` and applied by a small blocking
  script in every page's `<head>` (see `theme-init` inline script), so a
  returning visitor who picked dark mode never sees a flash of light mode
  before it switches.
- Implemented as one CSS custom-property block per theme in
  `public/assets/style.css` — `:root` holds the light defaults,
  `html[data-theme="dark"]` overrides them. Every component already reads
  these tokens, so no component markup had to change to support the toggle.
- A few things are intentionally **not** theme-dependent, because they need
  to stay a fixed dark color regardless of site theme: button text on gold
  backgrounds, the story-badge chip, and the chat widget's own light panel
  (see the `--true-dark` token). The admin dashboard is fully decoupled from
  this toggle too — it keeps its own fixed light-content/dark-sidebar look no
  matter what a visitor has chosen on the public site.
- Gold is still reserved for buttons, active tab states, hover reveals, and
  the hero's emphasized headline word — everything else (eyebrows, card
  numbers, icons, footer headings) uses a neutral accent token that adapts
  per theme.

## Accessibility (WCAG 2.1 AA)

Every text/background color pairing in the design system — public site, admin
dashboard, and chat widget, across **both** the light default theme and the
dark toggle theme (see above) — was run through a WCAG contrast audit
(relative luminance ratio, not hue):

- All body text, labels, and links meet **4.5:1** (normal text) or **3:1**
  (large text / graphical UI elements) against every background they actually
  appear on, in both themes.
- Several colors were adjusted because they *looked* fine but measured under
  4.5:1: the muted gray label color, the admin danger/warning/success badge
  colors, the chat-widget chip hover color, and form success/error text all
  got small, deliberate shade changes. Status/success/error messages are also
  conveyed through **text content**, not color alone (WCAG 1.4.1).
- **Text over the 3D hero graphic** sits behind a scrim, a text-shadow safety
  net, and solid backing pills behind the small corner labels and eyebrow tag
  — so headline/subtext stay legible regardless of what's rendered behind
  them on the canvas at any given moment, and regardless of which theme is
  active.

This was checked mathematically against the fixed palette, not visually — if
you add new color combinations later, re-run a contrast check before
shipping them.

## What's not real yet — check before launch

- The mining-licence scan you shared names co-licensees (Daniel Bundala
  Shinyonga, Rafael Sayi Felisiani) and an issuing officer's signature — the
  site only references the licence **number and issuing authority**, not the
  full scanned document or ownership split. Add/remove detail as you see fit.
- `info@yst.co.tz` and the social links in the footer are
  placeholders — swap in your real inbox and profiles.
- The "Sample entry" Projects/Insights/Press/Media/Events rows are placeholders
  for dashboard demo purposes — replace with real project write-ups and photos.
