Sri Maha Jewels · Block B
B S I II III IV A M G R L
Block B · App Manager · Design Brief

The Control
Room.

Hero slides, featured pieces, push broadcasts, booking inbox — the four levers that turn the Sri Maha Jewels app from a static catalogue into a living conversation with every customer who installs it.

Sessions4
Sub-pages4
New endpoints9
New tables1
The brief

An app without a curator is just a brochure.

The Android app shipped. Customers are walking around Perambalur with it in their pockets. What they see, today, is exactly whatever inventory happens to be in the DB sorted newest-first. We can't surface the piece that arrived yesterday. We can't tell them about Akshaya Tritiya. We can't push a rate drop. And every booking they send goes into a table the owner has no way to see.

Block B is the four levers that change that: a hero carousel for whatever deserves the front page this week, a featured shelf for pieces the shop wants to sell now, a push composer for the moments worth interrupting a customer for, and a bookings inbox for closing the loop on every visit request. All of it lives in bill.srimahajewels.com — one new sidebar entry, four tabs, owner-only.

"When the owner logs in Monday morning and sees a new booking request sitting there, a rate drop ready to push, and the kasulaperu haram that arrived Friday moved to the hero — the app feels staffed. That's the goal."
The runway

Four sessions, in order.

Each session ships one tab, owner-reviewed, on prod. Sessions 1 and 2 are pure OTA — no new APK. Session 3 depends on Firebase credentials being set up first (the same fix that unblocks APK 12's push-token check). Session 4 closes the loop on bookings — builds on Monday's booking fix.

Session 1
Session 2
Session 3
Session 4
01 Hero Carousel
Foundation · Hero
02 Featured Pieces
Featured Pins
03 Push (blocked)
FCM
Push Composer
04 Bookings Inbox
Inbox · Launch
Session 1 — Foundation + Hero Session 2 — Featured Session 3 — Push Session 4 — Bookings Blocked — Firebase setup required
I
Chapter I·Session 1·Ships OTA

The hero that changes every Friday.

Upload a photo, write a line in Playfair italic, set a date range — and that's what every customer sees on the Home tab next time they open the app.

The hero is the one thing the shop wants every customer to notice this week. Akshaya Tritiya. A new kasulaperu haram that just landed. A rate drop good for the weekend. The ladies' ring collection the owner wants to push before Diwali.

Admin side: a drag-to-reorder list of slides. Each slide gets a photo, a title in italic serif, an optional subtitle, an optional CTA label pointing to a product code, and a date range. A slide goes live only if today falls inside its range AND the active toggle is on. Outside its range it shows a soft queued or expired chip so the owner can see at a glance what's in flight.

App side: the Home screen's top hero area auto-advances between live slides every 4.5s, respects prefers-reduced-motion, falls back gracefully to the static "Welcome to Sri Maha Jewels" panel if zero slides are live. Max 5 active at once.

Storage
settings.appHeroSlides
JSON array, lives in existing settings table. Photos on S3.
Android reads
GET /shop-info
Existing endpoint extended. One extra field: hero_slides.
Limits
5 live
Max 5 active slides · title ≤ 64c · subtitle ≤ 140c.
  • Photo upload — reuses existing S3 presign flow from product photos. 2 MB max, JPG/PNG. Stored under hero/<uuid>.jpg.
  • Date range — validation: end ≥ start ≥ today−7. Blocks typos that accidentally run an expired slide.
  • CTA link — autocompletes against inventory. Tapping in-app deep-links to the product detail screen.
  • Zero-state — when no slide is live the app shows the existing brand block. No "empty hero area" gap.
  • Preview — live phone-frame preview beside the editor, updates as you type. Owner sees exactly what the customer will.
Admin side · bill.srimahajewels.com
bill.srimahajewels.com/app-manager
Hero
Featured
Push
Bookings
⋮⋮
Akshaya Tritiya · 10% off APR 29 — MAY 05 · 22K · CTA: "Shop now"
Live
⋮⋮
Kasulaperu haram · just arrived MAY 10 — MAY 25 · no CTA
Queued
⋮⋮
Ladies' ring collection APR 01 — APR 20 · expired
Off
II
Chapter II·Session 2·Ships OTA

The shelf up front.

Tick the boxes. Drag the order. Every customer's Shop tab now opens with those pieces, in that sequence.
Admin side · Featured grid
bill.srimahajewels.com/app-manager · Featured
Hero
Featured
Push
Bookings
Pinned · 3 of 12
Kasulaperu HaramP0042 · ₹3,12,000
Mango Bangle SetP0017 · ₹84,200
Diamond StudP0038 · ₹46,800
Men's ChainP0011 · ₹72,400
Ladies RingP0023 · ₹18,900
PendantP0045 · ₹12,400

Newest-first is fine when there's no editorial hand. With a curator, it's wrong. The owner wants this kasulaperu haram at the front, that bangle set after it. A checkbox grid with drag-reorder gives them that power without teaching them a new concept — it's exactly how email clients move messages, which they already understand.

App side: the Shop tab renders the pinned list first as a distinct Featured row (gold-star corner badge), then the rest in the existing newest-first order. Pinned items keep their star even when scrolled past. Tap any star → "This is a piece the shop is highlighting" tooltip — no mystery.

Backend is tiny: settings.featuredProductCodes is a JSON array of product codes in display order. Nothing fancy. Max 12 picks — enough for a nice Featured row, not enough to turn the Shop into noise.

Storage
featuredProductCodes
JSON array of codes. Order = display order.
Endpoint
GET PUT
/api/app/featured — owner-only.
Cap
12 pinned
Soft-warn above 8, hard-block at 12.
  • Sold-out pruning — if a pinned piece gets sold, the app renders a gentle "just sold — see similar" card instead of a 404. Curator gets nudged next login to repick.
  • Search inside picker — the inventory grows past 200 items. A search box (name, code, category) inside the picker keeps the UI fast.
  • Live preview — tiny phone frame on the right shows the Shop top-of-grid as you pick. Order changes update instantly.
III
Chapter III·Session 3·Blocked on FCM

The moment worth interrupting for.

A broadcast that reaches every customer on the app — but only after a four-gate safety ritual that makes "sent to 4,218 phones by accident" impossible.

Push is the most powerful lever in Block B — a customer's phone vibrates, their name is on it, and a sentence from the shop is on the lock screen. Used well, it closes the distance. Used carelessly, it costs a customer forever. So the composer is built around one principle: make it slow to send, fast to unsend-in-your-head.

The composer has three fields: title (64 chars), body (240 chars), optional deep-link to a product code. Under that, a segment picker — radio list, never checkbox, so there's exactly one audience per broadcast. Under that, a live notification preview rendered exactly as the customer will see it. Under that, the safety gate: recipient count, first three token previews, dry-run-to-owner button, and the "type SEND to confirm" field that only unlocks the big red button after a 5-second cooldown.

History lives in a new push_campaigns table — every send gets a row with status + delivery counts. So two weeks from now the owner can look at the list and see which message got read vs which fell flat.

Why blocked: push needs Firebase Cloud Messaging credentials uploaded to EAS. That's the same prerequisite as the APK 12 push-token check — ~10 min of Firebase Console work + eas credentials upload. Session 3 can't land until that's done.

New table
push_campaigns
id, title, body, segment, recipient_count, sent, delivered, failed, sent_at, status, sent_by
Rate limits
1/30m · 3/day
Owner can override with discount PIN. Hard stop at 10/day.
Delivery
Expo Push API
Server-to-server. No frontend ever sees tokens.
  • Segments — All (token'd) · Last 30d buyers · Active scheme holders · Pending booking · Birthday this week · Custom phone list.
  • Templates — 5 pre-filled drafts: new arrival · rate drop · custom order ready · scheme payment due · festive wish. Edit before send, never blank-slate.
  • Dry-run — sends only to the owner's phone (7010742905). Owner sees the real notification before any customer does.
  • Audit trail — every send writes to activity_logs with recipient count, segment, sender. Non-destructive history.
  • Throttling — Expo Push API batches 100 tokens per call. Server chunks + backs off on 429. Failed tokens clear expo_push_token (stale-token cleanup).
Push composer · safety-first
bill.srimahajewels.com/app-manager · Push
Active scheme42 customers
All phones187 customers
Last 60d buyers93 customers
Pending bookings8 customers
SRI MAHA JEWELS · NOW
Gold rate dropped ₹80 today Today only — 22K at ₹6,640/g. Good window for a chain or a thalikkodi.
42 phones
Recipients
Type SEND to confirm Cooldown unlocks the button in 5s. Dry-run available.
IV
Chapter IV·Session 4·Ships OTA

Every request, accounted for.

Monday morning view: who asked, which piece, what they said, status. One tap opens WhatsApp with their number pre-loaded.
Bookings inbox · filter · act · archive
bill.srimahajewels.com/app-manager · Bookings
Hero
Featured
Push
Bookings
All 14 Awaiting 6 In convo 3 Confirmed 5
V
VIMAL VISHAL M · Kasulaperu Haram Requested 25 APR · "this weekend if possible"
Awaiting
K
KAVIN BALAJI · Diamond Stud Requested 24 APR · no note
In convo
A
ABINAYA R · Ladies Ring Requested 22 APR · "for engagement"
Confirmed
S
SURYA · Men's Chain Requested 20 APR · "after 11 AM weekdays"
Awaiting
R
RAJESH · Mango Bangle Set Requested 18 APR · "wife's birthday"
Confirmed

Bookings are already landing — Monday's fix made sure of that. What they're missing is a home on the shop side. The customer asked to see a piece; right now the owner finds out by running a SQL query. That's not a workflow.

The inbox is an email client pattern, not a dashboard. Four filter pills across the top (All / Awaiting / In convo / Confirmed) with live counts. Rows beneath show who, what, when, a notes excerpt, status, and — the key move — a one-tap WhatsApp button that opens the customer's number with a pre-filled template: "Hi {name}, this is Sri Maha Jewels — about your request to see the {product}...". No copying numbers. No retyping greetings.

Status dropdown on each row cycles Pending → Contacted → Confirmed → Cancelled. Customer's Android app picks up the change on next refresh — their My Bookings list pill flips from "Awaiting reply" to "Owner contacted you" automatically. No second message needed; the status change is the handshake.

Uses existing
bookings table
No schema change. Status enum already in place.
Endpoints
GET · PATCH
/api/app/bookings · /api/app/bookings/:id/status
Aging
Auto-archive 30d
Anything not touched in 30 days moves to an Archive filter.
  • Desktop unread count — the sidebar entry shows a live "6" badge for anything in Awaiting. Clears on first visit to the tab.
  • WhatsApp deep-link — uses the shop's existing messaging template infrastructure. Tap → opens wa.me/91{phone}?text={prefilled}.
  • Notes preview — max 2 lines, full text on hover. Keeps the row scannable.
  • Bulk actions — select multiple rows → mark all Contacted (common case: owner just did the morning WhatsApp loop).
  • Empty state — "No requests yet — promote a hero slide to start driving them" with a link to Chapter I.
Architecture map

Three surfaces, one source of truth.

The Android app only ever reads from /api/public/*. App Manager only writes through /api/app/*. Every byte the customer sees lives in the DB the owner controls. No second store, no sync lag, no drift.

Admin surface

App Manager

New sidebar entry inside bill.srimahajewels.com. Four tabs. Owner-only. Writes go out through /api/app/*.

GET /api/app/hero-slides PUT /api/app/hero-slides GET /api/app/featured PUT /api/app/featured POST /api/app/push/send GET /api/app/push/history GET /api/app/bookings PATCH /api/app/bookings/:id/status POST /api/app/photo-upload-url
Data layer

Existing DB

Same smjdb. One new table (push_campaigns). One new settings key per feature. Bookings already shipped.

settings.appHeroSlides settings.featuredProductCodes push_campaigns (new) bookings (existing) customers.expo_push_token S3: hero/<uuid>.jpg
Consumer

Android app

Only reads. No client-side Admin surface. Contract extensions are additive — old APKs keep working if they're on fields they don't know.

GET /api/public/shop-info
+ hero_slides
GET /api/public/products
+ featured_codes
GET /api/public/bookings
existing · status pill
POST /api/public/push-token
existing · receives broadcasts
Owner taps "Save" in App Manager ↓ (JWT, owner role) PUT /api/app/hero-slidessettings.appHeroSlides = [...] ← S3 photo keys live here ↓ (writes activity_logs row) ╭─ customer opens app ↓ GET /api/public/shop-infoSELECT settings WHERE key IN (...) ↓ presign hero photo URLs ↓ Android renders carousel
Data model

JSON where we can, tables where we must.

Small, owner-edited state (hero slides, featured picks) lives as JSON in the existing settings table — zero new migrations, no schema drift, fast iteration. Anything with volume, history, or aggregates (push history) gets its own table. Bookings already exists.

ItemShapeWhy
appHeroSlides JSON [{id, photoKey, title, subtitle, ctaLabel, ctaProductCode, startDate, endDate, active, sortOrder}]
Max 20 slides total (5 active). Stored in settings.appHeroSlides.
Tiny, infrequently written, rendered on every shop-info call. Migrating to a table later is a 10-line backfill if volume justifies.
featuredProductCodes JSON ["P0042", "P0017", "P0038", ...]
Max 12. Codes must exist in products table.
Order-significant, tiny, pure join key — storing it as an array preserves order naturally without an ORDER BY sortOrder table column.
push_campaigns TABLE id, title, body, segment, recipient_count, sent_count, delivered_count, failed_count, sent_at, status, sent_by, deep_link
New table. One row per broadcast. Indexed on sent_at DESC.
Aggregates over time (delivery rates), paginated history view, audit-critical — all pointing to a proper table. Not a JSON blob.
bookings EXISTING id, customer_id, product_code, product_name, notes, status, created_at
Already shipped. Status enum: Pending · Contacted · Confirmed · Cancelled.
Zero schema change. The inbox is pure UI over what's already there — no new column, no migration.
customers.expo_push_token EXISTING VARCHAR(200) NULL
Already migrated. One per customer. Stale tokens cleared by push-send failure path.
The single column that makes push possible. Chapter III cleans stale tokens automatically as it discovers them.
Guardrails

Expensive to send. Cheap to think twice.

Every lever in App Manager has an undo. Most actions are saved drafts until you explicitly publish. Push is the one exception — you can't un-vibrate a phone — so push gets its own four-gate ritual.

#
Recipient count shown

Every push shows the exact number of devices about to receive it, computed server-side from the segment, right above the confirm button. "42 phones" never becomes a surprise.

Dry-run to owner

One tap sends the exact notification only to the owner's phone (7010742905). See it land, read it on your lock screen, then decide whether to broadcast.

T
Type SEND to confirm

Red button stays disabled until "SEND" is typed in caps. Five-second cooldown after that before it unlocks. Muscle-memory click can't fire a broadcast.

!
Rate limits

Soft: one every 30 min, three per day. Override: discount PIN. Hard ceiling: 10 per day, no override. Stops a bad mood turning into 50 push messages.

Hero drafts live

Slides save as drafts. A toggle flips one to "active". No slide reaches a customer before the owner explicitly flips that switch. Queued / Expired states shown in the list.

👁
Live preview

Every tab has a phone-frame preview beside the editor. Hero slide, push notification, featured grid — the owner sees exactly what the customer will, before any save.

📝
Activity log

Every App Manager write — hero save, push send, booking status change — writes an activity_logs row. Full audit trail for free.

Stale token cleanup

When Expo reports a dead token, the backend clears customers.expo_push_token so next push skips that phone. No "sent to 42, delivered to 18" without an explanation.

Rules · load-bearing

Every rule, honoured.

Block B is owner-curated work that lands on a customer's phone. Every LOAD-BEARING feedback rule from this project's memory applies to something in these four sessions. Here's the full list and how each one is respected.

Ship bigger · not minimal

Session 1 lands the full Hero surface — editor, drag-reorder, photo upload, CTA autocomplete, live phone-frame preview, zero-state fallback, Android consumption — in the first commit. No "v1 minimal, polish later."

🎨
Demos are the floor

The mockups embedded in this plan (hero editor, featured grid, push composer, bookings inbox) are the quality floor. Direction of travel is always up — richer motion, smoother transitions, better assets. Never down.

📱
Low-end optimized

Hero carousel animation on Android uses transform+opacity only (useNativeDriver:true). Featured row uses FlatList patterns. No blur, one shadow per hero element. 60 fps holds on the 2 GB Android 6 floor.

🔌
EAS channel linked

Before the first OTA of Block B, eas channel:edit preview --branch preview runs once to link the channel. Without this, publishes silently void. Verified via eas channel:view.

🔁
Force-close twice

Every OTA instruction to the owner says: open, wait 30s for bundle download, force-close, reopen. Updates apply on the next launch, not the current one. Documented in every session handoff.

🌐
EAS env persisted

Any new EXPO_PUBLIC_* var a session adds is written both into eas.json.build.env (for APKs) AND via eas env:create (for OTAs). Block B doesn't add new client env vars, but this rule armed the day one does.

Keyboard + nav-bar safe

App Manager runs in the desktop billing app — no Android keyboard concerns in Block B itself. The rule still applies to the Android side of every consumption contract: the carousel, featured grid, and bookings list never overlap system nav, and no input auto-focuses.

💬
Messaging · WhatsApp-style

Bookings Inbox borrows the WhatsApp-quick-reply pattern — one tap opens the customer's number with a pre-filled template greeting. Owner keeps their existing messaging muscle memory; no new UX to learn.

Customer copy · warm

Hero titles, push body text, and featured-piece blurbs are read by customers, not staff. Tone guideline from feedback_customer_ux.md applies: friendly, trendy, never corporate. Push preview reminds the owner of this tone before sending.

Don't cancel builds

Block B ships almost entirely as OTAs. APK 13 only gets rebuilt when Session 3's Firebase credentials land. If any gap is found mid-build, it waits for the NEXT build — never cancel to add a fix.

Throttle respected

Push broadcasts share Expo Push infra, not Meta WhatsApp, so 131049-style retry rules don't apply. But rate-limits (1/30m · 3/day) serve the same purpose: don't hammer a recipient who just got one.

🔍
Rule-check first

This page itself is the output of a rule-check pass. Every LOAD-BEARING feedback_*.md was re-read in full (not just the index description) before Session 1 code.

Session 1 · Ready to start

The first session: foundation + hero.

When you tap "start", here's the concrete delivery list for the first working session — enough to put a live hero slide in front of every Android customer by end of day. OTA ships the app-side changes automatically.

LayerFileWhat it does
Prereq eas channel:edit preview --branch preview
One-shot per channel. Verify with eas channel:view preview — must show a branch linked. Without this, OTAs void silently (see feedback_eas_update_flow.md).
Locks in that every Block B OTA actually reaches customers. Runs once; never again for this channel.
Prereq OTA delivery note
Every OTA handoff tells the owner: open app → wait 30 s for silent bundle download → force-close → reopen. Updates apply on next launch, not current.
Kills the "I shipped but nothing changed on my phone" class of bugs. Documented per feedback_eas_update_flow.md §2.
Frontend index.html
New section#app-manager with four tabs, sidebar entry added, owner-only nav gate.
Entry point. Sidebar shows "📱 App Manager" below Settings. Tabs: Hero (enabled), Featured/Push/Bookings (coming soon).
Frontend js/app-manager.js
New. Nav + hero-slide CRUD + drag-reorder + photo upload + live preview.
Tab state, draft list, save-on-blur, drag handlers, inventory-code autocomplete for CTA links.
Frontend css/app-manager.css
New. Scoped styles for App Manager surface.
Matches Heritage Cream tokens. Phone-frame preview layout. Does not leak into billing pages.
Backend routes/app.js
New. Owner-only. GET/PUT /hero-slides.
Validates shape, date ranges, caps at 5 active. Writes to settings.appHeroSlides. Logs to activity_logs.
Backend routes/public.js
Extend /shop-info response with hero_slides.
Filters to only active + in-date slides. Presigns S3 URLs (24h TTL). Additive — old apps ignore the new field.
Backend routes/uploads.js
Reuse existing presign pattern. New key prefix hero/.
No new endpoint — the existing product photo presign flow takes a prefix arg. Zero new code, zero new auth.
Android src/api/shop.ts · src/hooks/useShopInfo.ts
Consume hero_slides field. Render in Home hero area.
Auto-advance carousel (4.5s), reduced-motion respected, zero-slide fallback to existing brand panel. Ships as OTA.
Tests app-manager.test.js · hero-slides.test.ts
Backend shape validation + Android carousel render.
Covers cap enforcement, date-range math, stale slide filtering, zero-state fallback.
The horizon

When all four tabs are live.

The shop becomes reachable through the app in a way that isn't possible through WhatsApp alone. Monday's "what should we push this week?" question has a home. Friday's new arrival has a front-page slot. Every booking gets a human reply with a WhatsApp tap. The app earns its place on the home screen.

4
Sessions
9
New endpoints
0
Android refactor
1
New DB table
Block B · App Manager · drafted 2026-04-25
← Session 28 plan · top ↑