update ui and remove mock data
This commit is contained in:
41
docs/API-GAP-REPORT.md
Normal file
41
docs/API-GAP-REPORT.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# API gap report — after backend integration
|
||||
|
||||
Generated 2026-09-09. Format: `Feature | Existing API | Frontend status | Missing API`
|
||||
|
||||
## Wired to the platform
|
||||
|
||||
| Feature | Existing API | Frontend status | Missing API |
|
||||
|---|---|---|---|
|
||||
| Login | `POST /api/auth/login` | Wired | — |
|
||||
| Session restore | `GET /api/auth/me` | Wired — confirmed on every load | — |
|
||||
| Token refresh | `POST /api/auth/refresh` | Wired — single-flight, persist-before-use | — |
|
||||
| Logout | `POST /api/auth/logout` | Wired — revokes upstream, then clears | — |
|
||||
| Site switcher / Store page | `GET /api/sites` | Wired | — |
|
||||
| Dashboard Visitors | `GET /api/reports/footfall` | Wired — server `total` | — |
|
||||
| Dashboard Purchases / Revenue / Conversion | `GET /api/reports/conversion` | Wired | — |
|
||||
| Footfall chart | `GET /api/reports/footfall?bucket=day` | Wired | — |
|
||||
| Revenue chart · Sales page | `GET /api/reports/conversion?bucket=day` | Wired | — |
|
||||
| Recent arrivals · Activity page | `GET /api/visits` | Wired — cursor echoed, deduped on `visit_id` | — |
|
||||
| Customer photos | `GET /api/faces/…` | Wired — proxied so `<img>` works | — |
|
||||
| Team | `GET /api/team` | Wired | — |
|
||||
| Mobile → dashboard purchases | `POST /api/purchases` | Wired via `POST /api/visits` | `GET /api/purchases` |
|
||||
|
||||
## Cannot be wired — backend required
|
||||
|
||||
| Feature | Existing API | Frontend status | Missing API |
|
||||
|---|---|---|---|
|
||||
| LYT programme | none | Unavailable panel | `GET /api/rewards`, `/api/rewards/redemptions`, `/api/lyts/ledger` |
|
||||
| Engagement activities | none | Unavailable panel | `GET /api/activities`, `/api/activities/impact` |
|
||||
| Campaigns | none | Unavailable panel | `GET /api/campaigns` |
|
||||
| Customer journey | partial (visit/purchase/return only) | Unavailable panel | `GET /api/reports/journey` |
|
||||
| Orders / products / stock | none | Unavailable panel | `GET /api/purchases`, `/api/products` |
|
||||
| Payment split / refunds | none | Unavailable panel | `GET /api/reports/payments`, `/api/reports/refunds` |
|
||||
| Staff attendance & ranking | `/api/team` is console accounts | Unavailable panel | `GET /api/staff`, `/api/staff/attendance`, `/api/reports/staff-sales` |
|
||||
| Business profile | none | Unavailable panel | `GET/PATCH /api/settings/profile` |
|
||||
| Roles matrix · Integrations · API keys · Billing | none | Still local state | `GET /api/settings/{roles,integrations,api-keys,billing}` |
|
||||
| Security → sessions | `GET/DELETE /api/auth/sessions` | **Not yet wired** — endpoint exists | — |
|
||||
| Invitations / join flow | `POST /api/team/invitations`, `GET /api/auth/invitation`, `POST /api/auth/register` | **Not yet wired** — endpoints exist, service written | — |
|
||||
| Loyaly AI chat | none | Still mock replies | `POST /api/ai/chat` |
|
||||
| Live arrivals stream | `GET /api/visits/stream` | **Not yet wired** — polling only | — |
|
||||
| Cameras / site health | `GET /api/cameras`, `/api/sites/{id}/check` | **Not yet wired** — service written | — |
|
||||
| Visitor directory | `GET /api/visitors`, `/history`, `PUT /profile`, `DELETE` | **Not yet wired** — service written | — |
|
||||
279
docs/API-INVENTORY.md
Normal file
279
docs/API-INVENTORY.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# Loyaly Merchant OS — API inventory & backend wiring plan
|
||||
|
||||
Audit date: 2026-09-09 · Branch `main` · Audited against the running app on :3100
|
||||
|
||||
Purpose: establish exactly what data the frontend consumes today, which of it is
|
||||
fake, and what a real backend must expose. Compare this against your API spec and
|
||||
mark each row **match / rename / missing / extra**.
|
||||
|
||||
---
|
||||
|
||||
## 1. How data flows today
|
||||
|
||||
```
|
||||
component → hook (useResource) → repository (owns the URL) → httpClient
|
||||
│
|
||||
NEXT_PUBLIC_API_BASE ────┤
|
||||
│
|
||||
(unset) → Next route handler → fixture generator
|
||||
(set) → YOUR BACKEND
|
||||
```
|
||||
|
||||
**The seam already exists and works.** `src/shared/services/httpClient.ts` line 22:
|
||||
|
||||
```ts
|
||||
const BASE = process.env.NEXT_PUBLIC_API_BASE ?? '';
|
||||
```
|
||||
|
||||
Set that to your API host and every endpoint in §2 bypasses the local route
|
||||
handlers entirely. No component changes. This is the intended cutover.
|
||||
|
||||
**Response envelope** — every endpoint must return this shape (`src/shared/types/api.ts`):
|
||||
|
||||
```jsonc
|
||||
// success
|
||||
{ "data": <T>, "meta": { "generatedAt": "ISO-8601", "range": "30d", "storeId": "all" } }
|
||||
// failure (any non-2xx, or 2xx with error present)
|
||||
{ "error": { "code": "internal|not_found|bad_request|unauthorized", "message": "…" } }
|
||||
```
|
||||
|
||||
`meta.generatedAt` is **required**: the UI measures every relative time
|
||||
("2 hours ago", days-to-expiry, the greeting) against the server clock, never
|
||||
`Date.now()`.
|
||||
|
||||
**Scope query** — every analytics endpoint is filtered by the same two params:
|
||||
|
||||
| param | values |
|
||||
|---|---|
|
||||
| `storeId` | `all` \| a store id |
|
||||
| `range` | `7d` \| `30d` \| `90d` \| `mtd` \| `ytd` \| `custom` |
|
||||
|
||||
**Auth** — session is an httpOnly cookie. `credentials: 'same-origin'` is sent on
|
||||
every request. A cross-origin backend needs CORS + `SameSite=None; Secure`, or
|
||||
keep the Next routes as a thin proxy.
|
||||
|
||||
---
|
||||
|
||||
## 2. Endpoints that EXIST (24) — all fixture-backed
|
||||
|
||||
Legend: **T** = TypeScript response type, defined in the file noted.
|
||||
|
||||
### Auth — `src/features/auth/types/auth.ts`
|
||||
|
||||
| Method | Path | Body / Query | Returns |
|
||||
|---|---|---|---|
|
||||
| POST | `/api/auth/login` | `{email, password, rememberMe}` (JSON **and** form-encoded) | `AuthSession` |
|
||||
| POST | `/api/auth/logout` | `{}` | `{ok: boolean}` |
|
||||
| GET | `/api/auth/session` | — | `AuthSession \| null` |
|
||||
|
||||
`AuthSession = {user: {id, email, name, role: 'owner'|'manager'|'analyst', organisation}, expiresAt}`
|
||||
|
||||
### Dashboard — `src/features/dashboard/types/dashboard.ts` + `intelligence.ts`
|
||||
|
||||
| Method | Path | Extra query | Returns |
|
||||
|---|---|---|---|
|
||||
| GET | `/api/dashboard/kpis` | scope | `Kpi[]` |
|
||||
| GET | `/api/dashboard/timeseries` | scope | `TimePoint[]` |
|
||||
| GET | `/api/dashboard/peak-hours` | scope | `HourCell[]` |
|
||||
| GET | `/api/dashboard/activity` | scope | `ActivityEvent[]` |
|
||||
| GET | `/api/dashboard/store-comparison` | scope | `StoreComparison[]` |
|
||||
| GET | `/api/dashboard/reward-usage` | scope | `RewardUsagePoint[]` |
|
||||
| GET | `/api/dashboard/performance` | scope + `granularity=weekly\|monthly` | `PeriodPoint[]` |
|
||||
| GET | `/api/dashboard/briefing` | scope | `DashboardBriefing` |
|
||||
| GET | `/api/dashboard/activity-metrics` | scope | `ActivityMetric[]` |
|
||||
| GET | `/api/dashboard/journey` | scope | `JourneyStage[]` |
|
||||
| GET | `/api/dashboard/campaigns` | scope | `CampaignSummary[]` |
|
||||
| GET | `/api/dashboard/insights` | scope | `Insight[]` |
|
||||
|
||||
Key shapes:
|
||||
|
||||
```ts
|
||||
Kpi { id:'visitors'|'purchases'|'revenue'|'activeRewards', label, value,
|
||||
unit:'count'|'inr'|'lyt'|'pct', deltaPct, isRiseGood, trend:{t,v}[] }
|
||||
TimePoint { t:'YYYY-MM-DD', visitors, purchases, revenue, conversion }
|
||||
HourCell { day:0-6 (0=Mon), hour:0-23, value }
|
||||
ActivityEvent{ id, at:ISO, kind:'reward_redeemed'|'staff_checked_in'|'purchase'
|
||||
|'reward_expired'|'store_opened', title, detail?, storeId }
|
||||
PeriodPoint { label:'W32'|'Aug', visitors, purchases, revenue }
|
||||
Insight { id, severity:'info'|'success'|'warning'|'error', title, body,
|
||||
action?:{label, href} }
|
||||
```
|
||||
|
||||
**Activity intelligence** (`intelligence.ts`) — the model shared by Dashboard and Lyts:
|
||||
|
||||
```ts
|
||||
ActivityMetric {
|
||||
id: 'walk'|'visit'|'selfie'|'spin'|'scratch'|'brand'|'challenge'|'friend'|'shop'|'event'
|
||||
label, description
|
||||
group: 'engagement'|'growth'|'commerce'
|
||||
accent: 'warm'|'cool' // fixed per activity, travels on the payload
|
||||
count, deltaPct
|
||||
status: 'live'|'paused'|'draft'
|
||||
lytsIssued // MEASURED (ledger). 1 LYT = ₹1
|
||||
isFeatured // the 6 the dashboard summarises
|
||||
impact: {
|
||||
customers // MEASURED
|
||||
rewardClaims? // omitted, never 0, when activity grants none
|
||||
repeatVisits, purchases // ATTRIBUTED
|
||||
attributedRevenueInr // ATTRIBUTED — never name this `revenue`
|
||||
attribution: 'estimated'|'observed'
|
||||
}
|
||||
}
|
||||
JourneyStage { id:'visit'|'engage'|'purchase'|'return'|'refer', label, value, conversionPct? }
|
||||
CampaignSummary { id, name, activityId, accent, status:'live'|'ended'|'scheduled',
|
||||
steps:{label,value}[], attributedRevenueInr, attribution }
|
||||
```
|
||||
|
||||
> **Contract rule.** `attribution` is not decorative. When it is `'estimated'` the
|
||||
> UI shows a disclosure ("Attribution is estimated…") beside every attributed
|
||||
> figure; when your backend can join purchases to activity events, return
|
||||
> `'observed'` and the disclosure disappears with no frontend change.
|
||||
> Invariants the UI assumes: `purchases ≤ repeatVisits ≤ customers ≤ count`.
|
||||
|
||||
### Lyts — `src/features/lyts/types/reward.ts`
|
||||
|
||||
| Method | Path | Returns |
|
||||
|---|---|---|
|
||||
| GET | `/api/lyts/rewards` | `Reward[]` |
|
||||
| GET | `/api/lyts/redemptions` | `TimePoint[]` ⚠️ |
|
||||
| GET | `/api/lyts/activity` | `ActivityEvent[]` |
|
||||
|
||||
`Reward { id, name, costLyt, claimed, used, expiresAt: ISO|null, status: 'active'|'paused'|'expiring'|'expired' }`
|
||||
|
||||
⚠️ **Known contract smell:** redemptions reuses `TimePoint`, and the Lyts charts
|
||||
read `visitors` as "Issued" and `purchases` as "Redeemed". Your backend should
|
||||
return a purpose-built shape — `{t, issued, redeemed}` — and I'll update the two
|
||||
chart call sites.
|
||||
|
||||
### Stores — `src/features/stores/types/store.ts`
|
||||
|
||||
| Method | Path | Returns |
|
||||
|---|---|---|
|
||||
| GET | `/api/stores` | `Store[]` |
|
||||
| GET | `/api/stores/:storeId` | `Store` (404 → `not_found`) |
|
||||
|
||||
`Store { id, name, status:'open'|'closed'|'maintenance', visitors, purchases, revenueInr, conversionPct, staffCount }`
|
||||
|
||||
### Staff — `src/features/staff/types/staff.ts`
|
||||
|
||||
| Method | Path | Returns |
|
||||
|---|---|---|
|
||||
| GET | `/api/staff` | `StaffMember[]` |
|
||||
| GET | `/api/staff/summary` | `StaffSummary` |
|
||||
| GET | `/api/staff/attendance` | `AttendancePoint[]` |
|
||||
|
||||
### Settings — `src/features/settings/types/settings.ts`
|
||||
|
||||
| Method | Path | Returns |
|
||||
|---|---|---|
|
||||
| GET | `/api/settings/profile` | `MerchantProfile` |
|
||||
| PATCH | `/api/settings/profile` | `MerchantProfile` (merged) — **does not persist today** |
|
||||
|
||||
---
|
||||
|
||||
## 3. Endpoints that DO NOT EXIST — must be created
|
||||
|
||||
These screens are live in the UI with **no API, no repository, and no persistence**.
|
||||
Mutations are `useState` only: refresh the page and every change is gone.
|
||||
|
||||
### 3a. Commerce — the worst-wired module
|
||||
|
||||
`/commerce` imports `commerceService.ts` **directly and synchronously** from 10
|
||||
component files. No repository, no route handler, no loading or error state.
|
||||
|
||||
Needed:
|
||||
|
||||
| Method | Path | Returns |
|
||||
|---|---|---|
|
||||
| GET | `/api/commerce/kpis` | revenue, orders, AOV, net sales, refunds, conversion (value + delta each) |
|
||||
| GET | `/api/commerce/revenue-trend` | `{t, revenue, target}[]` |
|
||||
| GET | `/api/commerce/orders-trend` | `{t, orders}[]` |
|
||||
| GET | `/api/commerce/payment-breakdown` | `{method, amount, sharePct}[]` |
|
||||
| GET | `/api/commerce/store-performance` | `{storeId, name, revenueInr}[]` |
|
||||
| GET | `/api/commerce/top-products` | `{id, name, category, unitsSold, revenueInr, growthPct, stockCount, stockStatus}[]` |
|
||||
| GET | `/api/commerce/alerts` | `{id, severity, title, body}[]` |
|
||||
| GET | `/api/commerce/orders` | recent orders feed |
|
||||
|
||||
### 3b. Settings — 7 screens, all local-state fakes
|
||||
|
||||
| Screen | Needed endpoints |
|
||||
|---|---|
|
||||
| Team & Staff | `GET/POST/PATCH/DELETE /api/settings/team` (+ suspend, password reset, invite) |
|
||||
| Stores | `GET/POST/PATCH/DELETE /api/settings/stores` |
|
||||
| Roles & Permissions | `GET/PUT /api/settings/roles` (permission matrix) |
|
||||
| Integrations | `GET /api/settings/integrations`, `POST/DELETE .../:id/connection` |
|
||||
| API & Webhooks | `GET/POST/DELETE /api/settings/api-keys`, `GET/POST/DELETE /api/settings/webhooks`, `GET /api/settings/webhooks/logs` |
|
||||
| Security | `GET /api/settings/sessions`, `DELETE /api/settings/sessions/:id`, `GET /api/settings/audit-log`, `POST /api/auth/password`, `POST/DELETE /api/auth/2fa` |
|
||||
| Billing | `GET /api/settings/billing`, `GET /api/settings/invoices`, `PATCH /api/settings/payout-account` |
|
||||
|
||||
### 3c. Loyaly AI
|
||||
|
||||
The chat panel streams from `services/ai/mockAi.ts` — a local prompt classifier
|
||||
picking canned templates. `loyalyAiRepository` is the only repository in the app
|
||||
that imports a mock directly.
|
||||
|
||||
Needed: `POST /api/ai/chat` (streaming — SSE or chunked), plus
|
||||
`GET/POST/DELETE /api/ai/conversations` if history is to survive reload.
|
||||
|
||||
### 3d. Store switcher — **critical**
|
||||
|
||||
`STORE_OPTIONS` is a **hardcoded array in `src/shared/providers/WorkspaceProvider.tsx`**
|
||||
(line 45), duplicated from the fixture roster. This array scopes *every request in
|
||||
the app*. It must be fed from `GET /api/stores`, or a real merchant sees fixture
|
||||
store names in the global switcher.
|
||||
|
||||
---
|
||||
|
||||
## 4. Hardcoded / dummy data inventory
|
||||
|
||||
| Location | What | Removal |
|
||||
|---|---|---|
|
||||
| `src/features/*/mock/*.ts` (11 files, ~1,970 lines) | all fixture generators | delete at cutover |
|
||||
| `src/shared/mock/rng.ts` | seeded RNG | delete at cutover |
|
||||
| `src/features/commerce/services/commerceService.ts` | KPIs, trends, products, alerts, store multipliers | replace with repository |
|
||||
| `src/features/settings/components/*.tsx` × 7 | `INITIAL_STAFF`, `INITIAL_STORES`, `INITIAL_MATRIX`, `INITIAL_APPS`, `INITIAL_KEYS`, `INITIAL_WEBHOOKS`, `WEBHOOK_LOGS`, `INITIAL_SESSIONS`, `AUDIT_LOGS`, `INVOICES` | replace with hooks |
|
||||
| `src/shared/providers/WorkspaceProvider.tsx` | `STORE_OPTIONS` | feed from `/api/stores` |
|
||||
| `src/features/loyaly-ai/services/ai/**` (~570 lines) | prompt router + reply templates | replace with real AI endpoint |
|
||||
| `src/features/auth/mock/users.mock.ts` | 5 users, **plaintext passwords** | replace `verifyCredentials()` body |
|
||||
| `src/app/api/**/route.ts` (24 files) | fixture wiring + `?_state=` simulation | delete or keep as proxy — see §6 |
|
||||
|
||||
**Auth note.** `verifyCredentials()` is already the single credential seam — its
|
||||
body becomes an HTTP call and nothing else changes. But it currently returns
|
||||
`unknown_email` vs `wrong_password` separately, which is a **user-enumeration
|
||||
oracle**. Collapse both to one message at the route when you go live.
|
||||
`AUTH_SECRET` must be set in production (`sessionToken.ts` throws without it).
|
||||
|
||||
---
|
||||
|
||||
## 5. What I recommend NOT doing yet
|
||||
|
||||
Deleting the fixtures before the real endpoints exist leaves every screen in an
|
||||
error state and removes the only way to verify the wiring. The fixtures are also
|
||||
the **executable spec** — `intelligence.mock.ts` encodes the funnel invariants
|
||||
your backend has to honour.
|
||||
|
||||
Order that keeps the app working at every step:
|
||||
|
||||
1. **Now (backend-independent):** add the missing seams — commerce repository +
|
||||
route, settings hooks + routes, `STORE_OPTIONS` from `/api/stores`, AI
|
||||
repository seam. Fixtures stay behind them. Every module then has one file to
|
||||
swap.
|
||||
2. **You send the API spec MD.** I diff it against §2/§3 and report
|
||||
match / rename / missing / extra per endpoint.
|
||||
3. **Cutover:** point `NEXT_PUBLIC_API_BASE` at the real host, adapt any shape
|
||||
mismatches in the repository layer only, delete `mock/` + `src/app/api/**`.
|
||||
4. **Verify:** typecheck, build, and walk every screen with the network tab.
|
||||
|
||||
---
|
||||
|
||||
## 6. Decision needed from you
|
||||
|
||||
**Do the Next.js route handlers stay?**
|
||||
|
||||
- **A — Direct:** frontend calls your backend. Delete `src/app/api/**`. Needs CORS
|
||||
and a cross-origin-safe session cookie.
|
||||
- **B — Proxy (recommended):** keep the route handlers, replace each fixture call
|
||||
with a `fetch` to your backend. Session cookie stays first-party, your API host
|
||||
is never exposed to the browser, and the `?_state=error|empty|loading` dev
|
||||
harness keeps working.
|
||||
|
||||
277
docs/BEHAVISION-GAP-ANALYSIS.md
Normal file
277
docs/BEHAVISION-GAP-ANALYSIS.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# Behavision API ↔ Loyaly Merchant OS — gap analysis
|
||||
|
||||
Audit date: 2026-09-09 · API: `https://platform.loyaly.ai` · Frontend: this repo
|
||||
|
||||
---
|
||||
|
||||
## Headline finding
|
||||
|
||||
**These are two different products.**
|
||||
|
||||
This frontend was built as a **loyalty & rewards console**: LYT points, reward
|
||||
catalogues, engagement activities (spin / selfie / scratch / challenge /
|
||||
referral), campaigns, redemption liability, commerce orders.
|
||||
|
||||
Behavision is a **camera-based footfall & visitor-recognition platform**: sites,
|
||||
cameras, face templates, arrivals, visitor identity, footfall and conversion
|
||||
reports.
|
||||
|
||||
They overlap on roughly **one third** of the surface — the part that is genuinely
|
||||
about *people arriving at a shop and buying something*. The rest of the UI has no
|
||||
data source in this API and never will until a loyalty backend exists.
|
||||
|
||||
So "remove all hardcodes" has a consequence that needs a decision, not a
|
||||
guess: **removing the fixtures makes about half the current UI go blank.** §4
|
||||
lays out the options.
|
||||
|
||||
The reverse is also true and more interesting: **Behavision exposes a lot of real
|
||||
product this console does not surface at all** — a visitor directory, live camera
|
||||
views, site health checks, an invitation/join flow, device management, GDPR
|
||||
erasure. See §5.
|
||||
|
||||
---
|
||||
|
||||
## 1. What maps — build these for real
|
||||
|
||||
| Screen / panel | Behavision endpoint | Notes |
|
||||
|---|---|---|
|
||||
| Login | `POST /api/auth/login` | different token model — §3 |
|
||||
| Session restore | `GET /api/auth/me` | |
|
||||
| Logout | `POST /api/auth/logout` | |
|
||||
| Dashboard · Visitors KPI | `GET /api/reports/footfall` | use server `total`, never sum buckets |
|
||||
| Dashboard · Purchases KPI | `GET /api/reports/conversion` | |
|
||||
| Dashboard · Revenue KPI | `GET /api/reports/conversion` | |
|
||||
| Dashboard · Footfall chart | `GET /api/reports/footfall?bucket=day` | |
|
||||
| Dashboard · Revenue chart | `GET /api/reports/conversion?bucket=day` | |
|
||||
| Dashboard · Visitors vs purchases | both reports | |
|
||||
| Dashboard · Conversion chart | `GET /api/reports/conversion` | |
|
||||
| Dashboard · Peak hours heatmap | `GET /api/reports/footfall?bucket=hour` | 7×24 grid from hourly buckets |
|
||||
| Dashboard · Period rollup | `…?bucket=week\|month` | |
|
||||
| Dashboard · Store comparison | `GET /api/reports/footfall?site=` per site | |
|
||||
| Dashboard · Recent activity feed | `GET /api/visits` | **arrivals only** — see below |
|
||||
| Stores list | `GET /api/sites` | + `fraction_below_gate`, cameras up |
|
||||
| Store detail | `GET /api/sites/{id}/check` | five-step smoke test |
|
||||
| Settings · Team | `GET /api/team`, `PATCH /api/team/{id}` | replaces `INITIAL_STAFF` |
|
||||
| Settings · Security → sessions | `GET/DELETE /api/auth/sessions`, `POST …/revoke-others` | replaces `INITIAL_SESSIONS` |
|
||||
| Store switcher (`STORE_OPTIONS`) | `GET /api/sites` | **critical — scopes every request** |
|
||||
|
||||
**Customer journey** maps partially and honestly:
|
||||
|
||||
| stage | source |
|
||||
|---|---|
|
||||
| Visit | footfall `total` |
|
||||
| Engage | ✗ no source |
|
||||
| Purchase | conversion |
|
||||
| Return | footfall `returning` |
|
||||
| Refer | ✗ no source |
|
||||
|
||||
**Activity feed caveat.** The current feed renders five event kinds
|
||||
(`reward_redeemed`, `staff_checked_in`, `purchase`, `reward_expired`,
|
||||
`store_opened`). `/api/visits` supplies **arrivals only**. Four of the five kinds
|
||||
have no source. The feed becomes an arrivals feed — which is arguably the better
|
||||
screen, and is what the spec calls "the screen a mobile app is for".
|
||||
|
||||
---
|
||||
|
||||
## 2. What does NOT map — no endpoint exists
|
||||
|
||||
| Area | Frontend surface | Status |
|
||||
|---|---|---|
|
||||
| **LYT programme** | entire `/lyts` page: rewards, claimed/used, redemption rate, outstanding liability, expiry alerts, reward usage chart, LYTs issued | ✗ no rewards concept in the API |
|
||||
| **Activity intelligence** | walk / selfie / spin / scratch / brand / challenge / friend / shop / event, impact chains, attribution, activity grouping | ✗ only *visit* has an analogue |
|
||||
| **Campaign performance** | campaign funnels | ✗ |
|
||||
| **Store insights** | "What needs your attention", AI briefing | ✗ |
|
||||
| **Commerce** | orders, products, categories, stock, payment methods, refunds, AOV, alerts | ✗ except revenue/basket via conversion report; `POST /api/purchases` writes but there is no orders read |
|
||||
| **Staff module** | attendance (present/absent/late/leave), punctuality, sales per head, rewards issued, performance score | ✗ **different concept** — `/api/team` is console *user accounts*, not shop-floor rostering |
|
||||
| **Settings · Profile** | business name, GSTIN, timezone, currency, LYTs per ₹100 | ✗ no business-profile endpoint |
|
||||
| **Settings · Roles matrix** | per-permission grid | ✗ role is a single enum via `PATCH /api/team/{id}` |
|
||||
| **Settings · Integrations** | connected apps | ✗ |
|
||||
| **Settings · API & Webhooks** | keys, endpoints, delivery logs | ✗ |
|
||||
| **Settings · Billing** | plan, invoices, payout account | ✗ |
|
||||
| **Settings · Security → audit log, 2FA** | | ✗ (sessions *do* exist) |
|
||||
| **Loyaly AI** | chat panel | ✗ |
|
||||
|
||||
---
|
||||
|
||||
## 3. Architectural changes required
|
||||
|
||||
These are not cosmetic. Each one has a defined failure mode.
|
||||
|
||||
### 3.1 Auth: cookie+HMAC → Bearer JWT with rotation
|
||||
|
||||
Today: `proxy.ts` gates every route on an httpOnly cookie carrying an
|
||||
HMAC-signed payload minted locally by `sessionToken.ts`. There is no upstream.
|
||||
|
||||
Behavision: `access_token` + `refresh_token`, both rotating.
|
||||
|
||||
**Recommendation — keep the Next routes as a BFF (Backend-For-Frontend).**
|
||||
Tokens live server-side; the browser keeps only the existing httpOnly cookie.
|
||||
This is not conservatism, it buys five specific things:
|
||||
|
||||
1. `proxy.ts` and the whole SSR gate keep working unchanged.
|
||||
2. Tokens never reach JS, so an XSS cannot exfiltrate a refresh token.
|
||||
3. **Web `<img>` cannot send `Authorization`.** A BFF can proxy
|
||||
`/api/faces/…` and the browser just uses a normal `<img src>`. Otherwise
|
||||
every avatar needs fetch + `createObjectURL` + revoke-on-unmount.
|
||||
4. The single-flight refresh lock lives in one server process, not in N tabs.
|
||||
5. CORS and `SameSite=None` disappear as problems.
|
||||
|
||||
**Three client rules the spec calls out — all must be implemented in the BFF:**
|
||||
|
||||
- `401` + `"error": "token_expired"` → refresh **once**, retry, silently. Any
|
||||
other 401 is a real sign-out.
|
||||
- **Serialise refresh behind one lock.** Refresh tokens are single-use; four
|
||||
concurrent panels would each spend it and three would lose. This dashboard
|
||||
fires **9 parallel requests on one page load** — it will hit this on the first
|
||||
expiry, every time, without the lock.
|
||||
- **Persist rotated tokens before using them.** A crash between refresh and
|
||||
persist leaves a token the server has already invalidated.
|
||||
|
||||
Also: marshal the request body before the first attempt — a retry re-sends it,
|
||||
and a stream is spent after the first read.
|
||||
|
||||
### 3.2 Response envelope
|
||||
|
||||
Frontend expects `{data, meta:{generatedAt, range, storeId}}`. Behavision
|
||||
returns bare objects/arrays.
|
||||
|
||||
**`meta.generatedAt` is load-bearing** — every relative timestamp, the greeting,
|
||||
and every days-to-expiry countdown measures against the server clock, never
|
||||
`Date.now()`. The BFF must synthesise it (from `polled_at`, or the response
|
||||
time) or ~20 `useResource` call sites all need rewriting.
|
||||
|
||||
Wrap in the BFF. Cheapest correct option by a wide margin.
|
||||
|
||||
### 3.3 Scope parameters
|
||||
|
||||
`?storeId=all&range=30d` → `?site=<slug>&from=YYYY-MM-DD&to=YYYY-MM-DD&tz=…&bucket=…`
|
||||
|
||||
- `storeId: 'all'` → omit `site` entirely.
|
||||
- `RangeKey` → concrete `from`/`to`. `to` is **inclusive**.
|
||||
- Send `tz`; buckets come back as **local wall time with no offset**.
|
||||
- Use `site` (documented spelling). An unknown query param is *silently ignored*,
|
||||
so `site_id` in the wrong place returns the whole estate instead of an error.
|
||||
|
||||
### 3.4 Report arithmetic — three traps
|
||||
|
||||
- **Do not sum buckets to get the total.** `total` is unique people over the
|
||||
window; someone who came Monday and Thursday is 1 person, 2 bucket-visitors.
|
||||
Show the server's `total`, visits underneath.
|
||||
- **`new + returning` can be less than `total`** — a site sending counts without
|
||||
templates records real footfall by an unidentified person.
|
||||
- **Do not `new Date()` a bucket label.** They are wall-time strings with no
|
||||
offset; parsing shifts every label into the viewer's zone.
|
||||
`formatDayLabel()` currently does `new Date(iso)` — it pins `timeZone:'UTC'`
|
||||
so it survives, but bucket labels should be passed through, not parsed.
|
||||
|
||||
### 3.5 Errors
|
||||
|
||||
Behavision: `{"error": "invalid_code", "message": "…"}` (flat).
|
||||
Frontend: `{error: {code, message}}` (nested), with codes
|
||||
`internal|not_found|bad_request|unauthorized`.
|
||||
|
||||
Map in the BFF. And **show the server's `message`** — it is written for humans;
|
||||
branch on `error`, never on the prose. New codes to handle: `token_expired`,
|
||||
`too_many_attempts` (429), `last_owner` (409), `invalid_code` (404),
|
||||
**`501` = feature off for this deployment, not an error**.
|
||||
|
||||
`404` is also what another tenant's data returns — never surface it as "deleted".
|
||||
|
||||
### 3.6 Cursor pagination — new concept
|
||||
|
||||
`GET /api/visits` is cursor-based and lossless; polling by timestamp
|
||||
**permanently skips rows** when a burst exceeds `limit`. `useResource` has no
|
||||
cursor concept — the arrivals feed needs a cursor-aware hook.
|
||||
|
||||
- Echo the returned `cursor` on every poll.
|
||||
- An empty poll returns **your own cursor**, not `""`.
|
||||
- A cursor that fails to parse → drop it, re-poll without one.
|
||||
- Delivery is at-least-once → de-duplicate on `visit_id`.
|
||||
- `GET /api/visits/stream` (SSE) is the low-latency path; needs an HTTP client
|
||||
that can set `Authorization` (browser `EventSource` cannot).
|
||||
|
||||
### 3.7 Images — new subsystem
|
||||
|
||||
- **Missing photo is data, not an error.** Images are off by default across the
|
||||
product; `available: false` with a `reason` is the normal case. Render
|
||||
initials, never an error state.
|
||||
- `auth: true` → send Bearer. `auth` absent/false → presigned, use directly.
|
||||
**Do not infer from the URL shape.** Treat any relative URL as needing auth.
|
||||
- **Every hand-out is written to the audit log.** Fetch once per screen, not
|
||||
once per component — two components asking for one face puts two rows in
|
||||
"who looked at my customers" for one glance.
|
||||
- Web: object URL + **revoke on unmount**, or a screen left open all afternoon
|
||||
holds hundreds of copies of one photograph. (The BFF proxy in §3.1 avoids
|
||||
this entirely.)
|
||||
|
||||
### 3.8 Roles
|
||||
|
||||
`UserRole = 'owner' | 'manager' | 'analyst'` → **`'staff' | 'manager' | 'owner'`**.
|
||||
`analyst` does not exist. Platform admin = `role === 'admin'` **and** empty
|
||||
`client_id` — the two together, never the role alone.
|
||||
|
||||
### 3.9 References instead of uuids
|
||||
|
||||
`V-42`, `chennai`, `Office1`, `priya@tenext.in` all work in paths and filters,
|
||||
and are **immutable** — safe to put in a URL or a saved report. Display names are
|
||||
not. The store switcher should key on `site_slug`, and deep links should use it.
|
||||
|
||||
Ambiguity resolves to nothing, not a guess. Unknown ref: **404 in a path, 400 in
|
||||
a filter**.
|
||||
|
||||
---
|
||||
|
||||
## 4. The decision: what happens to the unmapped half
|
||||
|
||||
The API gives a clean idiom for this: **`501` — "the feature is off for this
|
||||
deployment, not an error."**
|
||||
|
||||
| | option | result |
|
||||
|---|---|---|
|
||||
| **A** | Delete the unmapped modules | Smallest, most honest app. Lose `/lyts`, `/commerce`, activity intelligence, campaigns, insights, staff attendance, most of settings. Recoverable from git when a loyalty backend ships. |
|
||||
| **B** | Keep the UI, render "not available in this deployment" | Nothing hardcoded, nothing invented, screens stay for when the backend arrives. Costs a small unavailable-state component. |
|
||||
| **C** | Keep fixtures behind an explicit `DEMO_MODE` flag | Sales demos keep working; real deployments show B. Highest complexity. |
|
||||
|
||||
**My recommendation: B**, with A for `/commerce` specifically — commerce is the
|
||||
one module with no seam at all (10 components import a service synchronously),
|
||||
so there is nothing to preserve, and conversion-report revenue can move onto the
|
||||
dashboard where it belongs.
|
||||
|
||||
Either way **no invented number survives**, which is what you asked for.
|
||||
|
||||
---
|
||||
|
||||
## 5. Real product this console is not exposing
|
||||
|
||||
Worth knowing before we decide what to delete — the API supports screens that do
|
||||
not exist here yet:
|
||||
|
||||
- **Visitor directory** — `GET /api/visitors?q=` search by name/phone/`V-42`,
|
||||
`/history`, `PUT /profile` (name, phone, notes), and **`DELETE` erasure**
|
||||
(destroys face template + photo, keeps visits unlinked, irreversible; a `502`
|
||||
means *nothing* was deleted and must be reported as failure, never swallowed).
|
||||
- **Live camera view** — `GET /api/cameras/{id}/live`, SSE relayed from the shop PC.
|
||||
- **Camera management** — `GET /api/cameras` with latest still, `PATCH /api/cameras/{id}`.
|
||||
- **Site health** — `GET /api/sites/{id}/check`, five-step smoke test.
|
||||
- **Invitation / join flow** — mint a code, preview it unauthenticated, redeem it
|
||||
into a full session. Replaces the fake "Add Staff Member" form entirely.
|
||||
- **Device management** — the user's own signed-in devices, with `current` marked.
|
||||
- **Arrivals SSE stream** — the live feed.
|
||||
|
||||
---
|
||||
|
||||
## 6. Proposed sequence
|
||||
|
||||
1. **BFF + auth** — Behavision login/refresh/logout/me behind the existing
|
||||
cookie; single-flight refresh; envelope + error adapters; scope→params
|
||||
mapper. Nothing else can be wired until this exists.
|
||||
2. **Kill the highest-risk hardcode** — `STORE_OPTIONS` → `GET /api/sites`.
|
||||
3. **Reports** — dashboard KPIs, footfall, conversion, peak hours, rollup,
|
||||
comparison.
|
||||
4. **Arrivals** — cursor-aware feed replacing the mock activity timeline.
|
||||
5. **Team + Sessions + Invitations** — replaces four fake settings screens with
|
||||
real ones.
|
||||
6. **Apply the §4 decision** to everything unmapped; delete `src/features/*/mock/**`
|
||||
and `src/shared/mock/`.
|
||||
7. **Verify** — typecheck, build, walk every screen against the live API.
|
||||
|
||||
60
docs/PLATFORM-STATUS.md
Normal file
60
docs/PLATFORM-STATUS.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Live platform status — mcp.loyaly.ai
|
||||
|
||||
Probed 2026-09-09. `LOYALY_API_BASE=https://mcp.loyaly.ai`
|
||||
|
||||
The host is confirmed as the Behavision platform: it answers the documented flat
|
||||
`{"error": "...", "message": "..."}` contract with the documented codes
|
||||
(`bad_credentials`, `unauthorized`, `not_found`, `bad_request`).
|
||||
|
||||
**8 of the 16 endpoints the spec documents are not deployed yet.**
|
||||
A `401 unauthorized` proves the route exists and is gated. A
|
||||
`404 {"error":"not_found","message":"No such endpoint."}` means it is absent.
|
||||
|
||||
## Live
|
||||
|
||||
| Endpoint | Probe | Console feature |
|
||||
|---|---|---|
|
||||
| `POST /api/auth/login` | 401 `bad_credentials` | Sign in — **wired** |
|
||||
| `POST /api/auth/refresh` | 400 `refresh_token is required` | Token rotation — **wired** |
|
||||
| `POST /api/auth/logout` | 401 `unauthorized` | Sign out — **wired** |
|
||||
| `GET /api/auth/me` | 401 `unauthorized` | Session confirmation — **wired** |
|
||||
| `GET /api/sites` | 401 `unauthorized` | Site switcher + Store page — **wired** |
|
||||
| `GET /api/visitors` | 401 `unauthorized` | Customer directory — service written, UI not built |
|
||||
| `GET /api/reports/footfall` | 401 `unauthorized` | Visitors KPI + Footfall chart — **wired** |
|
||||
| `GET /api/reports/conversion` | 401 `unauthorized` | Purchases/Revenue/Conversion + Sales — **wired** |
|
||||
|
||||
## Not deployed
|
||||
|
||||
| Endpoint | Probe | Blocks |
|
||||
|---|---|---|
|
||||
| `GET /api/visits` | 404 | **Recent arrivals feed, Activity page** |
|
||||
| `POST /api/purchases` | 404 | **Mobile → dashboard purchase flow** |
|
||||
| `GET /api/team` | 404 | Leaderboard team table |
|
||||
| `GET /api/team/invitations` | 404 | Invite flow |
|
||||
| `GET /api/auth/invitation` | 404 | Join preview |
|
||||
| `POST /api/auth/register` | 404 | Redeeming an invitation |
|
||||
| `GET /api/auth/sessions` | 404 | Device management |
|
||||
| `GET /api/cameras` | 404 | Camera list / live view |
|
||||
|
||||
## Consequence for the "most important requirement"
|
||||
|
||||
The brief's §7 flow —
|
||||
|
||||
```
|
||||
Mobile → POST /api/visits → DB → Dashboard GET /api/visits → new visit appears
|
||||
Mobile → POST /api/purchases → DB → reports update
|
||||
```
|
||||
|
||||
— cannot run today. **Neither `/api/visits` nor `/api/purchases` is deployed.**
|
||||
The console side is built and pointed at both; they return 404 until the
|
||||
platform ships them.
|
||||
|
||||
What *does* work end to end once there is an account: sign in, site switching,
|
||||
and every KPI and chart on the Dashboard and Sales pages, since those read the
|
||||
two report endpoints that are live.
|
||||
|
||||
## Still needed
|
||||
|
||||
A valid account on `mcp.loyaly.ai`. There is no open registration by design,
|
||||
and `POST /api/auth/register` is not deployed either — so an account has to be
|
||||
created directly on the platform side.
|
||||
Reference in New Issue
Block a user