abhishek 8537b09f33 Wire the Buddy composer to the assistant
The panel has been complete except for the one part that answers. Its
composer was deliberately disabled — it used to accept text, light up the
send button, and swallow the submit, because no assistant endpoint
existed. This connects it to the one that does.

It is enabled only when a model is configured AND the page has an agent,
checked at runtime via /assistant/status rather than assumed at build
time. The placeholder says which of the two is missing:

  "Not connected yet"              no model on this deployment
  "No assistant for this page yet" no agent for this route
  "Ask about this page"            live

Two different facts, two different sentences. A person on Inventory can
move to Sales and get an answer today; a person whose deployment has no
model can do nothing from the browser. Flattening both into "not
connected" would be true and useless.

The old rule is kept: never accept a message nothing will read.

An answer shows its working — the tools it ran, their row counts, and a
link to the page holding the same rows. Buddy states things with the
confidence of a sentence, and the only honest way to present that is
beside the evidence, so a person can disagree with it. Refused calls are
shown too: an answer that quietly dropped one would look like Buddy chose
not to look.

A partial answer says so in amber, not as a grey hint. An answer cut off
mid-sentence reads as a complete one otherwise, which is the failure the
flag exists to prevent.

The thread clears on navigation. An answer about Sales sitting above the
Inventory page reads as being about what is on screen; losing the history
is the smaller cost.

Phase 2 ships one agent, covering Console and Sales. The rest arrive as
backend config, not as changes here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 13:14:18 +05:30
2026-08-24 20:35:18 +05:30
2026-09-10 17:57:00 +05:30
2026-09-10 17:57:00 +05:30
2026-08-28 11:03:56 +05:30
2026-08-27 17:08:21 +05:30
2026-08-24 20:35:18 +05:30
2026-09-10 17:57:00 +05:30
2026-08-24 20:35:18 +05:30
2026-08-24 20:35:18 +05:30
2026-08-28 18:18:50 +05:30
2026-09-10 17:57:00 +05:30
2026-09-10 17:57:00 +05:30
2026-09-16 17:12:30 +05:30
2026-08-24 20:35:18 +05:30
2026-08-27 13:58:19 +05:30

Nearle Console

The new Nearle Daily retail operations console. Built against the existing Fiesta backend — the API is a fixed constraint, not something this repo changes.

Running it

npm install
npm run dev        # http://localhost:3100
npm run typecheck  # tsc --noEmit
npm run build      # typecheck + production build

Port 3100, not 3000. The old console (daily_merchant_web) runs its dev server on 3000. strictPort is on, so if 3100 is taken this fails loudly rather than silently moving — which is the failure that makes you think your changes did not land when you are actually looking at a different app.

In development Vite proxies /fiesta → https://fiesta.nearle.app. For a deployed build set VITE_API_BASE (see .env.example).

Stack

Layer Choice
Build Vite 8
Language TypeScript 7, strict, noUncheckedIndexedAccess. No .js/.jsx anywhere.
UI React 19 + @astryxdesign/core
Theme src/theme/nearle.ts, compiled to nearle.css
Routing React Router 7, split per page
Server state TanStack Query 5
Icons lucide-react
Spreadsheets xlsx, loaded on demand

What's built

All three workspaces. Which one opens is decided by the account, not chosen — see src/auth/roles.ts.

Nearle Admin (issuperadmin) — the platform operator:

  • /nearle/stores — every tenant, branch counts, per-tenant performance
  • /nearle/stores/:tenantId — one tenant's branches, orders and revenue
  • /nearle/onboard/tenant — provision a merchant group and its first outlet
  • /nearle/catalogue — the global catalogue, plus both product-import paths
  • /nearle/partners — rider partners, and the riders under each
  • /nearle/dispatch — every partner's live work and shifts
  • /nearle/uploads — spreadsheets sent to the catalogue service

Store Admin (roleid 1 and 3) — one merchant, every branch:

  • /admin/console — online and counter sales side by side, per branch
  • /admin/sales · /admin/dispatch · /admin/inventory · /admin/reports
  • /admin/branches/new — commission an outlet with its delivery thresholds
  • /admin/users — back-office people and till accounts
  • /admin/terminals · /admin/uploads · /admin/profile · /admin/onboarding

Store user (everything else) — one branch, scoped to it:

  • /store/console · /store/products · /store/sales · /store/dispatch
  • /store/reports · /store/customers · /store/terminals · /store/staff
  • /store/uploads · /store/account · /store/setup

Two routes are redirects rather than pages, and deliberately: /nearle/onboard/branch → /nearle/stores (a branch is commissioned from the tenant that will own it), and /nearle/fleet → /nearle/dispatch.

Things about the backend that shape this code

These are not bugs in this repo. They are the API's behaviour, and each one is worked around deliberately.

  1. No web authentication. The login endpoints return the user record and no token; only /v1/pos/* has middleware. So the session is that record, held in sessionStorage. src/auth/session.ts is the only file that changes when tokens arrive.
  2. Every list call needs a scoping id or it 400s. The IDOR pass added controller-level guards. Query hooks are therefore enabled-gated on the id and the id is part of the query key.
  3. Roles are derived, not asserted. issuperadmin is checked before roleid, because the flag is server-derived and a roleid is not. Roleids 7 and 8 are till roles and never reach an admin workspace.
  4. POST /products/create takes one product and returns no id. The controller passes the struct to the service by value, so GORM writes the generated id into a copy that is then discarded. The sheet importer therefore creates, then re-queries by SKU to resolve ids, then batches the location and stock writes. See importSheetProducts in src/api/products.ts.
  5. The two import paths are not symmetric. Catalogue import is one idempotent batch call. Sheet import is N creates with no dedupe on SKU, so re-uploading a file duplicates its products — the UI says so before the button.
  6. The global catalogue has a price range, not a price, and no mapping to a tenant's own categories. Category, subcategory, retail price, cost and tax are collected before an import can be enabled.
  7. POS endpoints take one required locationid. There is no tenant-wide counter-sales call, so a multi-branch POS view fans out per branch.

The 30-second cadence

Online orders land when an order is placed; counter sales reach the console on a 30-second refetch; anything still sitting on an offline till has not arrived at all. So any figure blending the two is eventually consistent, and the <Freshness> component exists to say when a number was last true and how many bills are still stranded. Polling is applied per query in src/queries/hooks.ts rather than globally — a provisioning form has no business re-polling.

Design

The visual system is ported from KROW (which is built on Astryx too, so the tokens are role-for-role comparable), with Nearle purple #662582 as the accent. The ambient canvas in index.css — a fixed, viewport-wide horizontal gradient at z-index: -1 — is the one piece of KROW that carries most of the character.

Radius, type scale and control heights are matched to KROW in src/theme/nearle.ts, with the reasoning kept in comments.

Open: the dark-mode accent #B57FD0 was chosen for 5.41:1 contrast on Astryx's dark surface and still needs brand-owner sign-off before dark mode ships.

Description
No description provided
Readme 5.2 MiB
Languages
TypeScript 88%
CSS 9%
JavaScript 2.7%
Dockerfile 0.3%