Skip to main content

KA26 Changelog

All notable fixes, features, and lessons learned are documented here. Every entry includes: what broke, why it broke, how it was fixed, and the test that prevents it from breaking again.


[2026-05-05] Consumer location — header pill + auto-detect + content filtering

User-requested. The consumer app now treats location as a first-class input — modeled after Swiggy / Zomato. Top-right pill on every consumer tab; tap to switch between current GPS / search / map.

The flow:

  • App opens → LocationContext hydrates from cache → server (if logged in) → GPS auto-detect → fallback to Gadag default
  • Pill shows current locality + city; tap → bottom sheet
  • Bottom sheet: use current GPS / search address (Google Places autocomplete) / pick on map (placeholder, coming next iteration)
  • Every change persists to AsyncStorage + pushes to /api/consumer/location PUT
  • Shop / Requests fetches now pass lat+lng to backend → results filtered to the active location

Schema additions (additive, deployed): Consumer.addressLine, Consumer.locality, Consumer.pincode, Consumer.locationSource, Consumer.locationUpdatedAt.

Precision upgrade: GPS calls upgraded from Accuracy.Low/BalancedAccuracy.Highest. Was returning generic street-level (e.g. "Wartenberger Straße"); now ≤5 m precision (e.g. "Wartenberger Straße 42B, 13053 Berlin").

Tests: 23 new in tests/consumer-location.test.ts. Full project: 2,825 / 2,825 passing.

See Consumer Location for the full feature page.


[2026-05-03] Seller onboarding — 4-screen tour + 6-step guided wizard

Replaces the v1 stub at mobile-seller/(onboarding) with the full guided onboarding promised in the seller-mobile-app spec §4.1. Takes a non-technical Gadag shopkeeper from "just registered" → "store fully configured" in under 5 minutes.

Why now: onboarding friction was the #1 blocker on seller acquisition. The old screen sent sellers to ka26.shop manually; most closed the app instead.

The flow:

  • 4 swipeable welcome cards (~30s) — Welcome / Orders / Bids / Earnings
  • 6-step wizard (~3-5 min, one question per screen) — photos / name / hours / service / delivery details / confirmation
  • Rich done screen with animated check + "what happens next" cards
  • Pickup-only sellers skip the delivery-details step automatically

Schema: added Seller.tourCompletedAt, Seller.onboardingCompletedAt, Store.sameDayOrderCutoff (additive, deployed).

Routing matrix in mobile-seller/app/index.tsx — first match wins: no seller → login · no tour → tour · no onboarding → wizard · pending → pending screen · active → main app.

API: new POST/GET/PATCH /api/seller/onboarding, idempotent on retry, rate-limited via PRODUCT_CREATE bucket. Single-shot ingestion of full wizard payload → creates/updates Store + StoreImage rows + flips the seller flag.

i18n: 61 new keys × 7 locales (en+kn proper translations, others fallback).

Tests: 33 regression-pinning assertions in tests/seller-onboarding.test.ts. Full project: 2,783 / 2,783 passing.

Live verification against prod with a Mahalaxmi Furniture Mart test seller. Every validation rule fires correctly; valid payload creates store + flips flag; idempotent re-submission returns same storeId.

Same-day follow-ups:

  • Web parity/seller/onboarding rebuilt to match the mobile flow exactly (same 4 + 6 step UX, same backend endpoint). New /seller/pending page for the post-onboarding waiting state. Layout routing fixed to avoid infinite loop after wizard completion.
  • Admin verification queue fix/admin/verification was hiding sellers who finished the new wizard (queue filtered only on legacy submittedForReviewAt). Now accepts both submittedForReviewAt and onboardingCompletedAt. Side-nav badge + dashboard pending count both work for new-flow sellers.

Tests grew to 50; full project at 2,799 / 2,799 passing. APK mobile-seller/ka26-seller-v4.apk (1.0.3 / versionCode 4) built locally.

See Seller Onboarding for the full feature page.


[2026-05-02] Account Deletion + production red-team / blue-team audit

A long single-day pre-launch security + Play-Store-readiness pass.

A) In-app Account Deletion

Required by Google Play Store policy (Aug 2024), Apple guideline 5.1.1(v), and India's DPDP Act 2023. KA26 had none, which would have auto-rejected every Play Store submission.

  • New DELETE /api/consumer/account (anonymise + soft-delete; preserves order rows for 7-yr Indian tax retention)
  • New mobile screen mobile/app/delete-account.tsx (2-step destructive confirmation; full i18n in 6 locales; Grievance Officer footer per DPDP Act)
  • Email + phone tomb-mangled to deleted-{id}-{nonce}@deleted.ka26.local so unique constraint frees up — same-email re-registration works
  • tokenInvalidatedAt = now() force-logs-out every device
  • Admin /admin/consumers now surfaces the deleted status with its own filter chip + count

Live-verified end-to-end against prod 2026-05-02. PII fields wiped, child rows (push tokens, addresses, notifications) cleaned, re-registration with the same email succeeded as a new consumer ID.

See Account Deletion for the full feature page.

B) Red-team / blue-team production audit

24 attack vectors tested against https://ka26.shop. 6 holes found, 6 closed. 51 regression tests added.

Critical (consumer): errors/report flood (no auth, no rate limit), saved-searches/notify exposed publicly (could trigger user WhatsApp spam), analytics/pwa-install + pwa-launch flood. High (seller): withdrawals TOCTOU race — concurrent requests could drain 100× balance. Fixed with prisma.$transaction({ isolationLevel: "Serializable" }). Medium: OTP brute-force botnet bypass (added per-userId secondary lock); trust-score scraping (rate-limited + binned totalSalesRange).

Follow-up: rate-limited 5 reels write endpoints + 2 doctor endpoints; new SecurityEvent Prisma model + logSecurity() DB-write integration; 4 high-signal sites wired.

See Security & Hardening for the full breakdown.

C) Play-Store-readiness work

  • Permissions audit on all 3 mobile apps (dropped RECEIVE_BOOT_COMPLETED × 3, VIBRATE × 1)
  • Privacy policy drafted (DPDP-compliant, names every real third-party, includes Grievance Officer)
  • Listing copy + feature graphic + 512px icon prepared
  • Target SDK 35 (Android 15) verified

Tests

2,749 / 2,749 passing. 4 new security suites with 51 assertions.

Strategy

Direction-set agreed: photo-to-catalog seller onboarding is the first agentic-AI feature to build (Tier-3 onboarding moat); read-only customer-care agent second; ads + finance agents deferred.


[2026-04-28] Pre-launch hardening sweep

CTO-mode autonomous pass through the launch blockers list (money ops deferred until bank account opens).

Fixes shipped:

  • critical_pages health WARN: dropped /eats from probe list (Eats archived 2026-04-17, was returning 404).
  • Privacy Policy + Terms of Service: published at /legal/privacy + /legal/terms. Both public, no auth.
  • Doctor app push tokens at zero (silent push failures): root cause was registerForPushNotifications only firing on login. Added register call to (tabs)/_layout.tsx mount, mirroring consumer + seller apps.
  • Bidding write endpoints unguarded: BID_CREATE (10/5min per buyer) on offer creation, BID_MESSAGE (30/5min per actor per offer) on bid messages.
  • Eats fully decommissioned (per user ask): deleted seller/restaurant/, api/seller/restaurant/, api/restaurants/, mobile/app/restaurant/. Cleaned cross-cutting refs in seller orders, consumer reels, mobile reels. Schema preserved for historical Order data (legal retention).
  • Order email → in-app notification (per user ask): POST /api/store-orders no longer fires sendOrderConfirmation. Writes a Notification row instead. Email reserved for invoices + refunds when money ops launches.
  • GDPR delete-my-account flow: new POST /api/auth/delete-account (requires typing DELETE). Soft-deletes (anonymises) the consumer row, bumps tokenInvalidatedAt, cascades content-hide. Order rows preserved (Indian tax law). UI button at bottom of /settings.

tests/pre-launch-2026-04-28.test.ts (26 cases) pins every fix.


[2026-04-27] Admin Panel v2.3 — Unified entity action vocabulary

Pre-v2.3 each admin list page had its own action verbs and button positions: Sellers said "Disable", Consumers said "Suspend"+"Ban", Doctors said "Deactivate", Delivery Partners said "Suspend"+"Delete", Creators said "Suspend". 5 dialects, no muscle-memory transfer.

Unified everything via one shared component + one set of verbs:

  • <AdminEntityActions> renders the same dropdown on every list page (sellers / consumers / doctors / delivery-partners / creators).
  • Standard verbs across all types: View / Edit / Suspend / Reactivate / Ban / Unban / Delete / Impersonate.
  • High-stakes verbs (Suspend, Ban, Delete, Reject) require typing the verb verbatim in ConfirmModal + show a cascade preview.
  • Routes through the single chokepoint POST /api/admin/entities/[type]/[id]/actions. Extended that endpoint to handle consumer, doctor, delivery_partner.
  • Every destructive action: writes AdminAction + bumps tokenInvalidatedAt where applicable + calls notifyAffected() for the affected user.

tests/admin-entity-actions.test.ts (45 cases) pins component existence, wiring on all 5 list pages, unified API coverage per entity, and the audit + notification + token-revocation contract.

Full doc: Admin Panel §11.


[2026-04-27] Broadcast — timezone fix + explicit Send-now / Schedule toggle

User picked 10:20 PM IST. Send History showed 12:21 AM next day. Root cause: <input type="datetime-local"> returns a naked "YYYY-MM-DDTHH:mm" string. Server's new Date(string) on Cloud Run UTC interpreted it as UTC. Fix: client converts via new Date(scheduledFor).toISOString() before POST.

UX rebuild: replaced "leave empty to send immediately" hint with explicit two-card toggle (⚡ Send now / ⏰ Schedule for later). Datetime picker only appears when "Schedule" is selected.

Why our static-grep tests didn't catch it: the bug lives at the runtime boundary between browser input and server parse — static analysis can't see across. Added tests/broadcast-timezone.test.ts (7 cases) pinning the fix.


[2026-04-27] System Health v2 — 12 checks, Test Coverage panel, Feature Health panel

/api/health route + /admin/observability dashboard rebuilt to cover everything shipped after Jan 2026. The previous version was checking the legacy Restaurant table (Eats archived 2026-04-17) and silent on every system shipped post-Jan 2026.

Health checks (12 total, was 7): added store_orders (replaces legacy order_system), bidding_system, admin_audit_log, push_tokens, moderation_queue, broadcast_system. Removed checkOrderSystem (Restaurant-based, dead code).

Dashboard new sections: Test Coverage (per-app file + case counts: backend / mobile-consumer / mobile-seller / mobile-doctor) + Feature Health (bidding state breakdown, moderation depth + staleness, push tokens per app, broadcast delivery counts, admin actions by category + impersonation count). Replaced legacy Order metric → StoreOrder.

Self-pinning contract: tests/system-health-coverage.test.ts (25 cases) asserts every shipped system has a named check + is registered. Legacy checkOrderSystem is verified gone. Test inventory walks all 4 apps with sanity floors. Dashboard cannot drift silently again.

Full doc: System Health.


[2026-04-27] Admin Panel v2.2 — recipient counter fix + Live Orders Monitor + 63 tests

After v2 ship, two reported bugs:

  • Compose page recipient summary showed gibberish digits ("265118167...") because /api/admin/broadcasts/audience-count returned the resolved user-ID arrays from resolveAudience instead of just the counts; the UI rendered count.consumers (an array of ints) where it expected a number. Fix: endpoint now returns {consumers, sellers, doctors, total} as scalar numbers. Side benefit: doesn't leak user IDs to the browser.
  • /admin/orders-monitor 404'd. Side nav linked to it but the page never shipped. Built the missing page + API: KPI strip + filter chips + 100-row most-recent table that auto-refreshes every 10s.

Compose page also redesigned: 5 numbered cards (Audience → Type → Channels → Message → When), sticky right-side Review pane with live recipient count + per-app pill breakdown + push preview rendered in each app's brand color. Send button label tells you exactly what's missing when disabled.

Tests: tests/admin-v2.test.ts (63 cases) pinning schema, API contracts, UI sections, helpers, security, notification-type contract.


[2026-04-27] Admin Panel v2.1 — token revocation, real impersonation, notify-on-action, ConfirmModal, ReportButton

CTO call after v2 ship: closing security/trust gaps before they bite us.

Token revocation: Added Seller/Consumer/Doctor.tokenInvalidatedAt. Auth helpers reject any JWT whose iat predates that timestamp. Suspend cascade + reject-application bump it — existing seller cookies die at the moment the admin acts.

Impersonation that actually works: /api/admin/impersonate/[token] redeems a single-use ImpersonationSession, issues a 30-min cookie for the target app with impersonatedBy + impersonationMode claims. Drawer opens it in a new tab. Root-layout ImpersonationBanner shows a sticky red ribbon with "End session" button.

notifyAffected(): Suspend / reactivate / approve / reject seller and hide / delete product all push + write a notification to the affected user with the admin's reason. Trust + legal compliance.

ConfirmModal replaces window.prompt: Reason textarea (≥3 chars), optional cascade preview, optional "type SUSPEND to confirm" verb gate for high-stakes actions.

ReportButton (consumer): Drop-in component on consumer product detail. POSTs to /api/admin/moderation. Queue has a real user-input source now.

Polish: Operations homepage auto-refreshes every 15s. Moderation queue: j/k cursor + Enter opens drawer. Audit log: keyed array elements (kills React dev warning).


[2026-04-27] Admin Panel v2 — side nav, ops home, comms studio, T&S, audit log, Cmd-K

Comprehensive rebuild from a top-nav stats dashboard into a true operations command center. Phase A-D shipped together.

Schema additions (live in prod via prisma db push): AdminAction, BroadcastTemplate, ScheduledBroadcast, BroadcastDelivery, ModerationTicket, SavedFilter, ImpersonationSession.

Architectural moves: side nav (AdminShell) replaces top nav. ⌘K command palette mounted globally. Universal EntityDrawer slide-in panel. Single chokepoint /api/admin/entities/[type]/[id]/actions for ALL destructive actions.

Suspend cascade (the user's headline ask): "Suspend seller" flips status=disabled + hides all stores + hides all products in one transaction, with full audit log entry containing required reason.

Notification types: ephemeral (push only, NO Notification row), persistent (push + Notification row per recipient), ack_required (schema-ready, UI disabled).

Full doc: Admin Panel.


[2026-04-27] Bidding Phase 2.6 — Post-device-test bug bash (8 critical fixes)

After Phase 2.5 shipped (per-product toggle + buyer's My Bids screen), device-testing the live APKs surfaced 5 issues that broke the negotiation flow. Audit + live E2E found 3 more:

  1. Counter modal Send button hidden (KeyboardAvoidingView overlap)
  2. Order Summary showed listed price not negotiated price (cart-line client-display bug)
  3. qty × negotiated price math wrong (same root cause as #2)
  4. Lock-expired UX dead-end (no action button)
  5. Round counter buried (small grey caption → colored urgency pill)
  6. Product save returned "Request Failed" (/api/products/[id] only exported PUT/GET/DELETE; mobile-seller calls api.patch)
  7. Stale accepted offers blocked new bids server-side (lazy-expire fix)
  8. Variable-measurement (kg) products ignored lockedPrice in cart math

Static guards added: tests/no-cart-line-without-locked-price.test.ts, tests/bidding-lazy-expire.test.ts, tests/api-method-coverage.test.ts. Live E2E tests/bidding-live-e2e.test.ts (4/4) verified end-to-end against production with 3 real accounts.

Full doc: Bidding §14.


[2026-04-26] Bidding (Price Negotiation) — Phase 1 → 2 → 2.5

Three phases shipped same day:

Phase 1 — Foundation. Schema (PriceOffer + Store/Product flags), state machine with 38 unit-tested branches, FIFO seller queue (10-cap, invisible queued offers), per-buyer price isolation invariant, locked-price checkout, push notifications, mobile UIs (per-store toggle, Offers inbox, NegotiationCard). 26 live E2E scenarios passing on production with real seller + 2 buyers.

Phase 2 — Messaging + UX gaps. New BidMessage table + thread APIs + system-event auto-injection on every transition. Take-this-deal explicit CTA on accepted state with quantity stepper + lock countdown. Notification deep links to right screen on tap. Per-product PUT support. Fixed Phase 1 bug where buyer pushes only fired native push without creating in-app Notification rows. 20 live E2E scenarios.

Phase 2.5 — Daily-use surfaces. Per-product bidding quick-toggle on seller's product list (one-tap Bidding/No-bid chip; was buried inside Edit Product full-form). Buyer's My Bids screen at Profile tab → "My Bids" with Active + Recent (30-day) sections + status reasons. GET /api/offers?include=history extension. 11 live E2E scenarios.

Critical lesson learned: API tests don't catch component-mount crashes. SellerOffersScreen shipped Phase 1 with useMemo after early if (loading) return … and crashed on every device despite 38 unit + 26 E2E tests passing. Fix + lessons:

  • Static guard tests/no-hooks-after-early-return.test.ts walks all TSX and flags hooks after early-returns at component-body indent level.
  • jest-expo + RNTL render-test mandate documented in ARCHITECTURE.md §28: every NEW or MODIFIED RN screen gets a render test BEFORE APK build.
  • 23 render tests added across 5 new screens.

Full feature documentation: Bidding (Price Negotiation).


[2026-04-18] Cloudflare Migration — DNS, Docs Hosting, and Repo Privacy

Why

The internal docs portal source repo (sidgk/ka26-docs) needed to go private (it documents architecture, secret names, incident playbooks). GitHub Pages free tier requires public repos. We chose to move hosting to Cloudflare Pages (free, supports private repos) rather than pay GitHub Pro forever. Required side-effect: move DNS authority for ka-26.com from Hostinger to Cloudflare.

What we did

  1. DNS: Hostinger → Cloudflare. All 11 records imported, 6 forced to "DNS only" (proxy breaks GitHub Pages SSL + SMTP). Nameservers swapped to anahi/elmo.ns.cloudflare.com. Email continuity verified end-to-end.
  2. Docs hosting: GitHub Pages → Cloudflare Workers Static Assets project ka26-docs. Custom domain docs.ka-26.com attached, SSL auto-provisioned (cert valid through 2026-07-16).
  3. Repo: sidgk/ka26-docs flipped to private. GitHub Pages disabled. Old deploy workflow removed (commit b51ab38).
  4. Sentry status doc corrected: Both web (ka26-marketplace project) and mobile (ka26-mobile project) Sentry SDKs are receiving production events — the monitoring doc's "no-op until DSN set" status was stale and was updated.

Critical gotcha discovered

Cloudflare Workers Static Assets auto-config sets the deploy command to npx wrangler versions upload, which creates versions but doesn't promote them to production traffic. New project appears empty. Fix: change deploy command to npx wrangler deploy in Worker Settings → Build. Now documented in Cloudflare ops doc.

Cost

$0/mo. See the full Cloudflare doc for the complete record (DNS table, build pipeline, disaster recovery, SSL details).


[2026-04-17] Web ↔ Mobile Parity Fix — Public Discussion on Web Requests Page

Bug

User reported: "Requests page on web and mobile look different. I don't see the public discussion option on a few posts on web. Was it intentional?"

Mobile already had full Public Discussion UI (PUBLIC badge, "Open Discussion" CTA, public/private toggle in post form) added in the 2026-04-15 mobile bug fix session. Web had zero of it — RequestData interface didn't even include discussionType. Result: web users saw old "Chat (X/3)" private-supporter UI on every request, including public ones, with no way to create or visually distinguish them.

Fix (src/app/(consumer)/requests/page.tsx)

  • Added discussionType?: "public" | "private" to RequestData interface
  • isPublic helper in both card variants (grid card + detail modal)
  • PUBLIC badge: compact pill on grid, "Public Discussion" with icon on detail
  • CTA label flips to "Open Discussion" + emerald color when public
  • Hides X/3 supporters badge on public requests (only meaningful for private MAX_THREADS_PER_REQUEST=3)
  • Post form: new emerald toggle "Public discussion" placed above anonymity toggle, sends discussionType in POST body

What I deliberately did NOT add (Tier 2, post-launch)

  • Filter pills Public/Private/Mine/Helped (mobile has them, web has only Newest/All/My Requests/I Helped + categories)
  • Member count display on public threads
  • Edit/delete messages within 15-min window
  • Dedicated public chat view component (web reuses existing ThreadChat since the API unifies behavior — see [2026-04-15] entry)

Tests

tests/web-mobile-parity-requests.test.tsnew, 9 file-shape tests. Locks the parity. If a future refactor drops the public-discussion UI, the suite fails. Total web tests: 1874.

Process lesson

Third mobile↔web mismatch caught today (after My Ads data shape + Orders endpoint). Root cause is consistent: two codebases sharing one API but no programmatic check that their feature surfaces match. Worth building a "mobile↔web feature parity registry" post-launch once we know which features must stay at parity.


[2026-04-17] Mobile Bug — Shop → "My Ads" Filter Showed Empty Even With Ads

Bug

User had 8 ads visible in Profile → My Ads, but Shop → Ads → My Ads filter showed "You haven't created any ads".

Root cause (1 line)

/api/ads returns { ads: Product[] } (per src/app/api/ads/route.ts). Mobile (tabs)/shop.tsx was annotated as <{ products: Product[] }> and read data.products — undefined → empty array. TypeScript didn't catch it because the wrong type annotation matched the wrong unwrap; both ends were a coordinated lie.

Fix

  • mobile/app/(tabs)/shop.tsx line 399 — corrected to data.ads
  • 2 new file-shape tests in mobile/tests/mobile-code-integrity.test.ts lock both consumers to the correct field
  • tests/api-shape-contracts.test.ts (new, 7 tests) — generic contract checker for the broader bug class. For each registered API contract, validates: (a) server route actually returns the documented top-level key, (b) every listed client consumer references that key. Easy to extend — add an entry for each new endpoint and you get the contract guard for free.

Why our 1858 existing tests didn't catch it

File-shape tests verified the endpoint string was present ('/api/ads'), not the response unwrapping. TypeScript happily compiled the lie. Smoke tests don't log in as a user with ads + click the filter. This is exactly the gap I called out in the testing audit — code-shape tests catch regressions, not runtime correctness. Sentry will catch this whole bug class once DSN is set.

APK rebuild

  • v14 (89 MB) built with the fix and deployed to https://ka-26.com/downloads/ka26-latest.apk
  • v13 was the first APK with today's earlier changes (rebrand, eats archive, offline flags, email migration); v14 added this fix on top

[2026-04-17] Production Monitoring — Sentry + Hourly Health Cron + Runbook

Three defense-in-depth layers, designed to catch issues before users report them. Full setup guide in docs/MONITORING-SETUP.md.

Layer 1 — Sentry error/crash tracking (no-op safe)

  • Web: @sentry/nextjs installed; sentry.{client,server,edge}.config.ts + instrumentation.ts wired. Boots only when NEXT_PUBLIC_SENTRY_DSN/SENTRY_DSN is set — silently disabled otherwise so pre-Sentry deploys keep working unchanged. Strips PII (request body, cookies, query strings) before send. Source-map upload enabled when SENTRY_AUTH_TOKEN is present in CI.
  • Mobile: @sentry/react-native installed; init in mobile/app/_layout.tsx reads EXPO_PUBLIC_SENTRY_DSN. ErrorBoundary forwards caught errors to Sentry. Native crash reporting catches Java/ObjC crashes.
  • next.config.ts wraps with withSentryConfig only when DSN is present (no Sentry build hook overhead pre-DSN).

Layer 2 — Hourly production health cron

  • .github/workflows/health-check.yml runs at :17 past every hour (avoids busy :00/:30 minute marks)
  • Probes /api/health (3 attempts, 10 s backoff)
  • Runs full tests/e2e-smoke.test.ts against production
  • Emails on failure via Gmail SMTP (uses existing SMTP_USER/SMTP_PASS secrets — set ALERT_TO secret to enable)

Layer 3 — UptimeRobot synthetic monitoring (user setup, free tier)

Docs only — actual setup is a 5-minute task on uptimerobot.com (full instructions in docs/MONITORING-SETUP.md). Pings ka26.shop/api/health + ka-26.com every 5 min, SSL cert expiry alerts, public status page option.

.env.example documented

8 new Sentry env vars (SENTRY_DSN, NEXT_PUBLIC_SENTRY_DSN, SENTRY_ENV, SENTRY_RELEASE, SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT, plus mobile EXPO_PUBLIC_SENTRY_DSN) — all optional, all no-op safe.

Deferred bug fixes during integration

  • Sentry SDK v10 renamed onRequestErrorcaptureRequestError; instrumentation.ts re-exports under the old name
  • Sentry SDK v10 nests sourcemap controls under sourcemaps object; replaced deprecated top-level hideSourceMaps

[2026-04-17] Mobile Distribution — APK Direct Download via ka-26.com

While the Play Store submission queues, the Android APK ships directly from https://ka-26.com/download so early users can install Day 1 without waiting on Google's review cycle.

What's live

  • /download page (/tmp/ka26-website/src/app/download/page.tsx in landing repo) — 4-step install guide, FAQ addressing Chrome's "may harm your device" warning, trust strip explaining we sign the APK ourselves
  • APK at stable URL https://ka-26.com/downloads/ka26-latest.apk — filename intentionally version-less so we can swap to a Play Store redirect later without breaking shared download links
  • Hero CTA on landing home — "Download Android App" (primary, emerald) + "Open in Browser" (secondary, links to ka26.shop)
  • Build pipeline verified locally: npx expo prebuild --platform android --clean && cd android && ./gradlew assembleRelease (2m52s, ~84 MB output). Java 17 + Android SDK already on dev machine.

APK history (today)

VersionDateIncludes
v122026-04-15Pre-launch state — old brand, Eats tab
v132026-04-17First with rebrand to KA26, Eats archived, offline-only flags, email migration, About link, Sentry SDK
v142026-04-17+ Shop My Ads filter fix

Play Store submission (deferred to user, Phase 2)

Full playbook in user-facing message of the launch session. Requires:

  1. Google Play Console account ($25 one-time)
  2. Aadhaar/PAN verification (1-2 days)
  3. AAB build (./gradlew bundleRelease) — not APK
  4. Store listing assets (icon, feature graphic, screenshots, descriptions, content rating, data safety form)
  5. Privacy Policy URL: https://ka-26.com/privacy ✓ already live

Once approved, swap /download page CTA from APK direct download → Play Store redirect.


[2026-04-17] Offline-Only Launch (Feature Flags) — COD + Pay-At-Pickup Only

Bank account in the company name takes 2-3 weeks; without it Razorpay/PhonePe/etc. won't onboard us, so online payments can't go live. Rather than delay launch, we ship offline-only commerce now and flip the switch when the bank account opens.

Architecture decision: feature flags, not code removal

Two env-var flags gate the entire feature surface. Default = false (safe / offline-only). Flip to true on Cloud Run when the bank account opens — no code changes, no redeploy required.

FlagDefaultWhat it gates
PAYMENTS_ONLINE_ENABLEDfalse"Pay via UPI" buttons (web cart, web stores, mobile store), /api/payments/initiate* routes (503), /api/store-orders rejects paymentMethod=online
REELS_COMMERCE_ENABLEDfalseProduct/restaurant tag carousel in web viewer, "Tag products" entry in mobile post flow, tagged-product overlay in mobile viewer, /api/reels/[id]/tags PUT (503)

Why feature flags over rip-and-stitch:

  • 1 day to ship vs 1 week to build twice (now + when re-enabling)
  • Existing payment + reels-commerce code stays maintained (same tests, same paths)
  • Flip-back is a 30-second env var change in Cloud Run

Pickup auto-defaults to "Pay at Pickup"

Per spec: when fulfillmentType is pickup_immediate or pickup_scheduled, the server FORCES paymentMethod=pay_at_pickup regardless of what the client sent. This is enforced in:

  • /api/store-orders POST — server-side single source of truth (resolvedPaymentMethod)
  • Web stores/[id] page — UI auto-snaps payment back to COD when user picks pickup
  • Web stores/[id] page — payment label dynamically shows "Pay at Pickup" vs "Cash on Delivery"

pay_at_pickup is a NEW value alongside cod and online in the paymentMethod and paymentStatus columns. No enum change needed (those columns are String).

Seller verification flow (anti-spam / fake-order defense)

Per spec, the offline launch leans on sellers calling customers to verify:

  • Seller order card shows the customer phone as a tap-to-call tel: link with a "Call to confirm" hint badge (only on pending orders)
  • "Decline" action prompts for an optional cancellation reason (free text, max 500 chars)
  • The reason persists to the new StoreOrder.cancellationReason column so it's visible later for audit

New schema field

  • StoreOrder.cancellationReason String? — captured at cancel time, optional
  • Migration: prisma/migrations/20260417_store_order_cancellation_reason/migration.sql (idempotent ALTER TABLE, applied to production DB)

Files touched

New library:

  • src/lib/feature-flags.tsisOnlinePaymentsEnabled(), isReelsCommerceEnabled(), getPublicFeatureFlags(). Generic env var reader supporting true/1/yes/on (case-insensitive).

Server (gates + validation):

  • src/app/api/payments/upi-config/route.tspaymentEnabled is now providerConfigured && isOnlinePaymentsEnabled() (was just providerConfigured); spreads getPublicFeatureFlags() so client gets both flags in one fetch.
  • src/app/api/payments/initiate/route.ts + initiate-store/route.ts — return 503 when flag off, before any other work.
  • src/app/api/store-orders/route.tsresolvedPaymentMethod logic: pickup → pay_at_pickup, online + flag off → 400 with helpful message, online + flag on → still routes to /api/payments/initiate-store, default → cod.
  • src/app/api/reels/[id]/tags/route.ts — PUT returns 503 when reels commerce is off. Existing tags stay in DB so when flag flips back on, history is intact.
  • src/app/api/seller/stores/[id]/orders/route.ts — PATCH accepts optional cancellationReason, persists to new column.

Web UI:

  • src/app/(consumer)/cart/page.tsx — fetches paymentEnabled flag; auto-snaps online → cod when flag flips off; renders single COD card with explanatory note when online is unavailable.
  • src/app/(consumer)/stores/[id]/page.tsx — same fetch + two effects: (1) snap to cod when flag off, (2) snap to cod when fulfillmentType becomes pickup. Single-card "Pay at Pickup" / "Cash on Delivery" with explanation when online is hidden.
  • src/app/(consumer)/reels/page.tsxreelsCommerceEnabled state in ReelCard; gates ProductTagCarousel + legacy product/restaurant cards.
  • src/app/seller/stores/[id]/page.tsx — customer phone is now a tel: link with "Call to confirm" badge on pending orders; cancel action prompts for optional reason via window.prompt.

Mobile UI:

  • mobile/app/store/[id].tsx — already gated by existing paymentEnabled (now driven by the flag); no changes needed.
  • mobile/app/(tabs)/reels.tsxreelsCommerceEnabled fetched on mount; gates "Tag products" entry, tagged items inline, viewer tag display, and (via filter to empty) all tag-related side-effects.

Tests:

  • tests/offline-only-launch.test.tsnew, 31 regression tests across 9 describe blocks. Locks in: feature-flags lib + defaults, config endpoint AND-logic, server validation + pickup auto-default, payment initiate route gates, web cart/stores fetch + auto-snap, reels server gate + web viewer gate + mobile gate, schema cancellationReason field + migration, seller UI prompts + tel link.
  • tests/build-integrity.test.ts — 2 existing tests updated to match the refactored regex patterns (rejects-online + dynamic-COD-label).

Total tests passing: 1858 (web) + 321 (mobile vitest) + 103 (mobile Jest) = 2282 total.

How to flip back to online when bank account opens

# 1. Open bank account, complete Razorpay/PhonePe onboarding, get keys.
# 2. Set keys + flag on Cloud Run:
gcloud run services update ka26-marketplace --region us-central1 \
--update-env-vars PAYMENTS_ONLINE_ENABLED=true \
--update-secrets PHONEPE_MERCHANT_ID=...,PHONEPE_SALT_KEY=...
# 3. (Optional) Also turn reels commerce back on:
gcloud run services update ka26-marketplace --region us-central1 \
--update-env-vars REELS_COMMERCE_ENABLED=true
# 4. Done — no redeploy. Next request reads new env values.

Anti-patterns avoided

  • ❌ Removing the online payment code (would have to be rebuilt in 3 weeks)
  • ❌ Frontend-only gating (server must also reject — defense in depth)
  • ❌ Hard-coding the flag in NEXT_PUBLIC_* (those bake at build time; can't be flipped without redeploy)
  • ❌ Letting client decide pickup → COD (client could lie; server is single source of truth via resolvedPaymentMethod)
  • ❌ Deleting historical reel tags from DB when flag flips off (would lose data; we just hide the UI + reject new writes)

[2026-04-17] Eats Vertical Archived — Launch Scope Reduction

Removed the Eats (food delivery) vertical from launch scope so we ship 4 strong verticals (Shop, Reels, Requests, Profile) instead of 5 with one unanchored. No restaurant partners committed yet, and a half-empty Eats tab on Day 1 hurts perceived product quality more than a missing feature does.

What was removed (user-facing entry points)

  • Mobile: mobile/app/(tabs)/eats.tsx → archived to _archived-mobile/eats/eats.tsx. Removed the <Tabs.Screen name="eats"> entry from _layout.tsx (was already href: null but kept the screen file). Tab count went 5 → 4.
  • Web — public restaurant page: src/app/(public)/public/restaurant/[id]/ → archived to src/_archived/eats-public-restaurant/. This was the QR-code-scannable restaurant view.
  • Dead links: 3 Link href="/eats" "Order Food" CTAs replaced with "Browse Shops" pointing to /shop:
    • src/app/(consumer)/profile/page.tsx:1226 (empty orders state)
    • src/app/(consumer)/orders/page.tsx:204 (empty orders state)
    • src/app/(public)/public/restaurant/[id]/page.tsx:71 (already archived above)
  • Feedback PAGE_OPTIONS: removed "Eats" from web (src/app/(consumer)/settings/page.tsx) and mobile (mobile/app/feedback.tsx).

What was preserved (deliberately not removed)

  • Backend models: Restaurant, MenuItem, Order Prisma models stay — 30 historical eats orders exist in production DB; deleting would orphan that data and break order render code.
  • Backend API routes: /api/restaurants, /api/orders/* remain functional. They're idle in production but enable the existing 30-order data to display correctly in any consumer's order history.
  • Order rendering code: (consumer)/orders/page.tsx still has type === 'eats' rendering paths so historical eats orders display correctly.
  • seller/restaurant/: Restaurant sellers can still manage their menu in seller dashboard (just no consumer-facing surface).

Test alignment (13 tests had to be updated)

  • tests/app-isolation.test.ts (web): 6 tests about public restaurant page converted to expect(fileExists(...)).toBe(false) regression guards — they now lock in the archive state and will alert if someone accidentally restores eats without doing the full restoration steps.
  • mobile/tests/screen-completeness.test.ts: removed eats from required screens, removed Restaurant Screen Features describe block, added archived regression guard. Tab count test 5 → 4.
  • mobile/tests/mobile-code-integrity.test.ts: removed eats theme test, eats fetch test, eats card enhancements describe block.
  • mobile/tests/ui-consistency.test.ts: removed 3 eats cuisine chip visibility tests.

Documentation updates (each gets an "Archived 2026-04-17" notice with restoration instructions)

  • README.md — Eats section header marked archived
  • ARCHITECTURE.md — Eats section header marked archived
  • KNOWLEDGE_BASE.md — Eats vertical row in feature table struck-through
  • mobile/FEATURES.md — Eats screen description marked archived

Restoration playbook (when restaurant partners are committed)

  1. cp -r src/_archived/eats/ src/app/eats/
  2. cp -r src/_archived/eats-public-restaurant/restaurant src/app/(public)/public/
  3. cp _archived-mobile/eats/eats.tsx mobile/app/(tabs)/eats.tsx
  4. Re-add <Tabs.Screen name="eats"> in mobile/app/(tabs)/_layout.tsx
  5. Restore the 3 /eats Link CTAs (or keep /shop if better UX)
  6. Add "Eats" back to feedback PAGE_OPTIONS (web + mobile)
  7. Update tab count test 4 → 5
  8. Restore the screen-completeness/ui-consistency/mobile-code-integrity test blocks

Test results

  • Main suite: 1827 passing (was 1833 — one shared describe lost some tests when public-restaurant tests collapsed)
  • Mobile vitest: 321 passing (was 340 — some Restaurant Screen Features blocks removed)
  • Mobile Jest: 103 passing (unchanged)
  • Total: 2251 passing

[2026-04-17] Email Infrastructure Migration — noreply@ka-26.com

Replaced personal Gmail (siddugkattimani@gmail.com) with production-grade transactional email infrastructure using noreply@ka-26.com via Google Workspace SMTP. ISO certification + going-live readiness.

Why

  • Personal Gmail account isn't suitable for company-grade communication.
  • We're now a registered company; all outbound mail must reflect a professional identity.
  • Need reliable deliverability (SPF/DKIM/DMARC), retry on transient failures, and ops visibility.

Architecture decisions

  1. Generic SMTP transport instead of service: "gmail" so we can swap providers (Resend/SES/SendGrid) later without code changes.
  2. Backwards-compatible env vars — new SMTP_USER/SMTP_PASS take precedence, but GMAIL_USER/GMAIL_APP_PASSWORD still work as fallback during migration.
  3. 3-attempt retry with exponential backoff (500ms → 1s → 2s) — protects against transient SMTP failures.
  4. Connection pooling (3 connections, max 100 messages each) — reduces overhead under load.
  5. Every send logged to EmailLog table — gives ops/admin visibility without ESP webhook complexity.
  6. Admin recipient is env-driven (EMAIL_ADMIN_ADDRESS) — no more hardcoded personal email in source code.
  7. Branded HTML shell with consistent header/footer + "automated email, please do not reply" — reduces support load.
  8. Auto-Submitted: auto-generated header + X-Auto-Response-Suppress: All — suppresses vacation responders.

OTP improvements

  • Per-user resend cooldown: 60 seconds (OTP_RESEND_COOLDOWN_MS) — prevents spam-resend abuse.
  • IP-level rate limit unchanged: 3 sends per 5 min.
  • Verify endpoint now returns precise reason field: invalid | used | expired — UI can show actionable messages instead of generic "wrong or expired".
  • 10-minute expiry preserved (OTP_EXPIRY_MS).

New email templates

  • Email verification (existing, polished) — 6-digit OTP, 10-min expiry callout.
  • Password reset (new) — 15-min link with prominent CTA.
  • Order confirmation (new) — order number, total, "Track Order" CTA.
  • All other transactional templates updated to the new branded shell.

Files touched

Core library:

  • src/lib/email.tsfully rewritten. Generic SMTP, retry, logging, all templates, central sendAdminNotification().

Routes:

  • src/app/api/auth/send-verification/route.ts — 60s per-user resend cooldown via OTP_RESEND_COOLDOWN_MS.
  • src/app/api/auth/verify-email/route.ts — precise error reason field (invalid/used/expired).
  • src/app/api/website/contact/route.ts — uses central sendAdminNotification(), removed inline nodemailer.
  • src/app/api/website/apply/route.ts — same treatment, removed hardcoded email.

Schema:

  • prisma/schema.prisma — added EmailLog model.
  • prisma/migrations/20260417_add_email_log/migration.sqlnew, idempotent CREATE TABLE.

Config:

  • src/lib/constants.ts — added OTP_RESEND_COOLDOWN_MS.
  • .env.example — documented 8 new SMTP_* / EMAIL_* env vars + retained legacy fallback.

Docs:

  • docs/EMAIL-INFRASTRUCTURE.mdnew. Architecture diagram, env vars, App Password generation, SPF/DKIM/DMARC DNS setup for Hostinger, monitoring SQL, troubleshooting matrix.

Tests:

  • tests/email-infrastructure.test.tsnew, 27 tests across 7 describe blocks. Locks in: SMTP transport choice, env-var fallback, FROM_ADDRESS default, retry, logging, OTP cooldown, precise error reasons, EmailLog model + migration, removal of hardcoded gmail addresses.

Anti-patterns avoided

  • ❌ Switching transports without backwards compatibility — would have broken every running instance.
  • ❌ Hardcoding the new admin recipient — same problem we just solved with the old one.
  • ❌ Logging email content (PII) — EmailLog records subject + to only, not body.
  • ❌ Synchronous logging blocking the send — writeLog is best-effort, never throws into the caller.
  • ❌ Tight coupling to nodemailer in route files — every send now goes through src/lib/email.ts.
  • ❌ Touching the admin auth check (seller.email !== "siddugkattimani@gmail.com" in website/jobs routes) — that's admin identity, not email infrastructure. Will be migrated separately when we configure proper admin RBAC.

NOT done (intentional, flagged for follow-up)

  • The two website/jobs admin auth checks still hardcode siddugkattimani@gmail.com — this is admin identity, not email infrastructure. Changing it without an admin RBAC story would lock you out of the admin panel. Will be revisited when we move to a proper admin role system.
  • Phone OTP / SMS still a TODO (no provider integrated).
  • DMARC starts at p=quarantine (safer for new domain) — upgrade to p=reject after 2-4 clean weeks.

Test results

  • 1823 passed, 19 skipped (24 suites). Up from 1796 thanks to 27 new email-infrastructure tests.
  • Pre-existing public-discussion.test.ts failure is unrelated (missing GIPHY route, tracked separately).

[2026-04-17] GCP Cost Optimization — Phases 1 & 2

Reduced monthly GCP bill from ~€45 to ~€20 (55% reduction) with zero impact to ka26.shop or mobile app.

Phase 1: Artifact Registry & Build Cache Cleanup

  • Deleted 47/52 old Docker images from ka26 Artifact Registry (31.3GB → ~3GB)
  • Set auto-cleanup policy: keep latest 5 images, delete anything older than 7 days
  • Cleared Cloud Build cache
  • Savings: ~€3-5/month

Phase 2: Migrate ka-26.com from GCP to GitHub Pages

Problem: ka-26.com (a purely static landing page) was running on Cloud Run behind a GCP Load Balancer costing ~€18/month with zero dynamic content to justify it.

Solution:

  • Converted sidgk/ka26-website Next.js app to static export (output: "export")
  • Deployed to GitHub Pages with auto-deploy GitHub Actions workflow
  • Updated DNS on Hostinger: 4 A records → GitHub Pages IPs + CNAME www → sidgk.github.io
  • Verified ka-26.com serves correctly (HTTP + HTTPS) from GitHub Pages

GCP resources deleted (all ka26-website-* prefixed):

  • 2 forwarding rules (HTTP + HTTPS)
  • 2 target proxies (HTTP + HTTPS)
  • 2 URL maps (main + HTTP redirect)
  • 1 SSL certificate
  • 1 backend service
  • 1 network endpoint group (NEG)
  • 1 static IP (34.8.8.73)
  • 1 Cloud Run service (ka26-website)

Savings: ~€20-22/month

ka26.shop resources untouched: ka26-marketplace Cloud Run, ka26-marketplace-ip (34.8.146.193), ka26-marketplace LB, ka26-cert-main — all verified working after cleanup.

Phase 3: Replace ka26.shop Load Balancer with Cloud Run Domain Mapping (DEFERRED)

What: Remove the remaining GCP Load Balancer for ka26.shop and use Cloud Run's built-in domain mapping instead. Would save ~€18/month.

Why deferred: Going live in ~1 week. DNS changes carry small risk of propagation delays or SSL provisioning issues. €18/month is not worth risking launch stability.

Revisit: May 2026, after launch is stable.

How to do it when ready:

  1. gcloud beta run domain-mappings create --service ka26-marketplace --domain ka26.shop --region us-central1
  2. Update DNS A records for ka26.shop from 34.8.146.193 to the Cloud Run-provided IP
  3. Wait for SSL provisioning + DNS propagation
  4. Verify ka26.shop + mobile app work
  5. Delete: forwarding rules, proxies, URL maps, backend service, NEG, SSL cert, static IP (all ka26-marketplace-* prefixed)

Files touched

  • /tmp/ka26-website/next.config.ts — added output: "export", images.unoptimized
  • /tmp/ka26-website/.github/workflows/deploy.ymlnew, GitHub Pages auto-deploy
  • /tmp/ka26-website/public/CNAMEnew, custom domain file

[2026-04-15] Mobile App — Four Critical Bug Fixes

Four bugs reported from production mobile app with screenshots. All root-caused and fixed in one pass with a new regression test file (tests/mobile-bugs-2026-04-15.test.ts, 16 tests) that will go red if any fix is removed.

Bug 1 — "Failed to join discussion" on Open Discussion

Symptom: Tapping "Open Discussion" on a request owned by the current user surfaced Failed to join discussion. Root cause: POST /api/requests/[id]/threads was written for the original private 1-to-1 supporter flow and unconditionally rejected the request owner with "You cannot support your own request" (HTTP 400). There was no branch for the newer discussionType === "public" mode, so the public-discussion button couldn't create or join the shared thread when the caller was the owner. The messages route had the same blind spot — it returned 403 for any non-owner non-supporter reading or posting in a public thread. Fix:

  • src/app/api/requests/[id]/threads/route.ts — detects request.discussionType === "public" and takes an early branch: idempotent lookup-or-create of a single shared thread, no owner rejection, no MAX_THREADS_PER_REQUEST check. To avoid a schema migration the shared row's supporterId is set to the request owner; the public UI never surfaces that field so the compromise is invisible.
  • src/app/api/requests/[id]/threads/[threadId]/messages/route.ts — GET and POST both pull request.discussionType and gate the 403 on !isPublic. Public threads skip the 1-to-1 notifyThreadReply call since every participant would otherwise spam every other.
  • File-level JSDoc added to the threads route explaining both modes and the schema compromise.

Bug 2 — Android hardware back from request detail pops to Shop tab

Symptom: Inside Requests → request detail / threads / public chat, the Android back button exited straight to the Shop tab instead of going back to the list. Root cause: The four sub-screens (detail, threads, chat, publicChat) are rendered via an internal view.screen state machine inside mobile/app/(tabs)/requests.tsx, not as real Expo Router routes. The hardware back button bypassed the internal goBack() helper and Expo Router popped the tab stack back to the initial tab (shop). Fix: Added BackHandler + useFocusEffect wiring that intercepts hardwareBackPress when view.screen !== "list" and forwards to goBack(). A viewRef + goBackRef pair keep the handler stable so the focus effect doesn't need to re-subscribe on every navigation.

Bug 3 — Requests tab state not reset on re-focus

Symptom: Leave the Requests tab while inside a post detail, return later — the previously-open detail view is still displayed instead of the list. Root cause: The tab component stays mounted across focus changes, so the view state persisted. There was no cleanup on blur. Fix: Same useFocusEffect from bug 2 now also clears state on blur: setView({ screen: "list" }), clears public-chat scratch state (messages, draft text, reply target, editing target, GIF picker). Next focus always starts clean.

Bug 4 — Recurring "Request failed. Retry" on My Orders → order detail

Symptom: Third occurrence of the same bug. User quote: "this is the 3rd time we are fixing the 'Request Failed' 'Retry' error on the same page". Root cause: mobile/app/order/[id].tsx calls GET /api/store-orders/${id} to load the invoice/tracking screen, but only src/app/api/store-orders/route.ts (the collection POST+GET) existed. There was no [id]/route.ts dynamic segment, so Next.js returned a 404 JSON blob that mobile's api.ts helper surfaces as the generic "Request failed" message. Fix: Created src/app/api/store-orders/[id]/route.ts with a GET handler that:

  • Resolves the consumer via resolveConsumerId (JWT or cookie).
  • Verifies order ownership; returns 404 (not 403) for non-owners to avoid id-enumeration leaks.
  • Serializes Prisma Decimal fields (subtotal, total, deliveryFee, platformFee, sellerAmount, items[].price) to strings so JSON.stringify doesn't choke and the mobile client can parseFloat them consistently.
  • Shape matches { order: Order } — exactly what mobile/app/order/[id].tsx expects.

This is a permanent fix — the route now exists as a real file in git, with a regression test asserting its existence, handlers, auth, 404 behavior, and Decimal serialization. Removing the file will break 6 tests immediately.

Regression tests

tests/mobile-bugs-2026-04-15.test.ts — 16 file-based tests across all 4 bugs. Run: npm test — 1747 passed (21 suites), 19 skipped.

Files touched

  • src/app/api/store-orders/[id]/route.tsnew
  • src/app/api/requests/[id]/threads/route.ts — public-mode branch + JSDoc
  • src/app/api/requests/[id]/threads/[threadId]/messages/route.ts — public-mode access
  • mobile/app/(tabs)/requests.tsx — BackHandler + useFocusEffect
  • tests/mobile-bugs-2026-04-15.test.tsnew, 16 tests

Preventing recurrence — Mobile ↔ Backend API Contract Test

Bug 4 recurred three times because nothing in CI noticed that the route file was missing — mobile called it, Next.js 404'd, mobile surfaced the generic "Request failed" error. Per-bug regression tests only protect the routes we already know about today; they don't prevent the same mistake for a new endpoint tomorrow.

Added tests/mobile-api-contract.test.ts — a static contract test that:

  1. Walks every .ts / .tsx file in mobile/app/ and mobile/src/, greps out every /api/... URL literal appearing inside an api.get/post/put/patch/delete(...) or fetch(...) call, normalises ${x}[x] and strips query strings.
  2. Walks src/app/api/**/route.ts to discover every real backend route (including dynamic segments and catch-alls).
  3. Fails if any mobile call site has no matching route file.

The test immediately found 7 more latent Bug-4-class gaps — routes the mobile app calls that have never existed on the backend:

  • POST /api/requests/[id]/pin
  • PATCH|DELETE /api/requests/[id]/threads/[threadId]/messages/[messageId] (edit/delete messages)
  • GET /api/requests/[id]/threads/[threadId]/members (public thread members)
  • POST /api/requests/[id]/report
  • GET /api/places/autocomplete, /api/places/details, /api/places/reverse-geocode

These are tracked in the test's ALLOWLIST with TODO markers and spawned as a follow-up task. Each entry must be removed as the matching route is created. The test is now the canonical source of truth: you cannot land mobile code that calls a non-existent backend route without either fixing it or explicitly justifying an allowlist entry.

Layered defenses now guarding these bugs (and their whole classes)

  1. Inline JSDoc on every modified route explaining why the code looks the way it does, so the next developer doesn't undo the fix thinking it's dead code.
  2. Per-bug regression tests (tests/mobile-bugs-2026-04-15.test.ts, 16 tests) assert every file, every handler, every access check, and every isPublic branch is present.
  3. Class-level contract test (tests/mobile-api-contract.test.ts) prevents any future mobile URL from drifting out of sync with the backend.
  4. CI gate — GitHub Actions runs npm test on every push to main, predeploy hook runs it before deploy scripts. A red test blocks merge and blocks deploy.
  5. Production smoke tests (tests/e2e-smoke.test.ts) hit the live endpoints after deploy to catch anything that squeaked through.
  6. Documentation Standard (MEMORY.md, added earlier today) — every change must update CHANGELOG + README + ARCHITECTURE + tests before it's considered done. This is now enforced by habit, not tooling, but the CHANGELOG pattern makes regressions visible at review time.

If any single layer is bypassed, the layer above it catches the miss. Bug 4 slipped through three times because none of these layers existed for this class of bug. They do now.


[2026-04-15] Seller Onboarding Cleanup + Verification & Compliance

Problem

Pre-launch audit surfaced 5 blockers:

  1. Registration still offered "Store / Restaurant / Both" even though the Eats vertical was decommissioned — created orphan Restaurant rows for sellerType="both".
  2. No verification layer — anyone with a Gmail could sign up and immediately add products.
  3. No approval gating between OTP and product creation.
  4. Hardcoded global category dropdown — inadequate for specialty shops (watch shop, flower shop, photo studio, etc.) which were all being forced into "Other".
  5. No document upload infrastructure for admin to review sellers.

Decision (locked in with user)

  • Manual verification — a KA26 rep physically visits the shop. Documents are uploaded digitally as a record, the actual trust signal is the on-site visit. Matches our hyperlocal positioning in Gadag (~250k population, launching with 50–200 sellers).
  • Tiered documents by store type — a flower shop doesn't need a business license, a pharmacy absolutely needs a drug license. 14 store types each with their own mandatory/optional doc requirements.
  • Per-store custom categories — each seller defines their own product categories during onboarding. No global master list.
  • Approval-gated product creation/api/products POST already blocked non-active sellers, so no backend change needed for that rule.

Schema (additive, zero breaking changes)

  • New SellerDocument modeltype, url, publicId, status, notes, sellerId, uploadedAt, reviewedAt.
  • New StoreCategory modelstoreId + name unique, ordered per store, cascades on store delete.
  • New Seller fieldsverifiedAt, verifiedBy, verificationNotes, rejectionReason, submittedForReviewAt, documents[].
  • New Product fieldstoreCategoryId Int? (nullable). Legacy products continue to use categoryId.

New library: src/lib/storeVerification.ts

Central registry mapping 14 store types → { mandatory, optional, suggestedCategories }. Baseline (owner ID + store exterior photo) applies to all types. Pharmacy gets drug_license, bakery gets fssai, jewelry gets bis_hallmark, etc.

New API routes

  • POST/GET/DELETE /api/seller/documents — seller uploads verification docs (deletes old GCS image on replace).
  • POST /api/seller/submit-verification — validates mandatory docs, stamps submittedForReviewAt.
  • GET/POST/PATCH/DELETE /api/seller/store-categories — CRUD for per-store custom categories with batch create. Ownership verified on every mutation.
  • GET /api/seller/me — lightweight profile endpoint that works for ANY seller status (unlike /api/auth/verify which hard-filters to active). Used by the onboarding wizard and the seller layout's fallback routing.
  • GET /api/admin/verification-queue — oldest-first queue with stats (pending / approved today / rejected today).
  • GET /api/admin/users/[id]/documents — admin view of a seller's docs with requirement checklist.

Modified API routes

  • PATCH /api/admin/users/[id] — accepts verificationNotes/rejectionReason. On approve: stamps verifiedAt/verifiedBy, auto-promotes all the seller's coming_soon stores to live + isActive, creates verification_approved notification. On reject: records reason + verification_rejected notification.
  • POST /api/auth/register — removed sellerType and restaurantName from destructuring and submit body. Hard-codes sellerType: "product". Restaurant-creation branch gone.
  • POST /api/auth/verify-email — issues a seller_token httpOnly cookie when a seller verifies their email, so they can proceed to onboarding while still in pending status. Scoped strictly to the seller branch — consumer/doctor behaviour unchanged.
  • POST /api/products — accepts new storeCategoryId field (nullable). Legacy categoryId path preserved. Pending-seller error message clarified to mention the 48–72 hour visit timeline.

New pages

  • /seller/onboarding — 5-step wizard (pick type → store details → categories → docs → submitted). Resumes mid-flow. Added to PUBLIC_PAGES so pending sellers can reach it without being bounced to login.
  • /admin/verification — queue UI with stats cards, search, filter, expandable rows showing store + doc grid + required notes on approve / required reason on reject.

Modified pages

  • /seller/register — removed 3-button "Store/Restaurant/Both" picker, removed conditional restaurant-name input, removed sellerType/restaurantName from form state and submit body. Redirects to /seller/onboarding after OTP verify.
  • /seller/stores/[id] — fetches per-store custom categories from /api/seller/store-categories?storeId=, uses them in the "Add Product" dropdown when available, falls back to the legacy static list for legacy stores. Passes storeCategoryId alongside categoryId.
  • /seller/layout.tsx — added /seller/onboarding to PUBLIC_PAGES. If /api/auth/verify says not authenticated, falls back to /api/seller/me; if that returns a pending seller, routes them to /seller/onboarding instead of /seller/login. Active-seller behaviour untouched.
  • /admin/layout.tsx — "Verification" nav link added to desktop + mobile nav.

Testing (+63 new tests, 0 regressions)

tests/seller-verification.test.ts — 63 tests across 10 groups: schema integrity, library correctness (all 14 store types, tiered docs, baseline invariants, fallback behaviour), API route existence + auth + ownership checks, register cleanup, verify-email cookie scoping, onboarding wizard structure, admin verification UI + nav, per-store category plumbing, pending seller routing, and regression guards (product creation still blocks non-active sellers, /api/auth/verify invariant preserved, admin delete cascade intact, consumer/doctor OTP behaviour unchanged).

Test run: Tests 6 failed | 1743 passed (1749). 6 failures are pre-existing from earlier entries. No new regressions. npm run build succeeds.

Anti-patterns avoided

  • Did not widen /api/auth/verify to return pending sellers — 13 files consume that endpoint and rely on the status === "active" invariant. Instead, created /api/seller/me as a dedicated "any-status" endpoint.
  • Did not add a pending-seller banner in the seller layout — pending sellers can only reach /seller/onboarding, so the banner would never render elsewhere, and the onboarding page itself is the "banner". Leaves the active-seller layout untouched.
  • Did not backfill storeCategoryId on existing products — the field is nullable and legacy products work fine with their old categoryId. Dual-path support means zero risk to live data.

[2026-04-12] Seller App Enhancements + Dashboard Redesign + Admin Fixes

Feature: Discount & Offer Management

  • Per-product discounts: Product.originalPrice field — sellers set original price + selling price, consumer sees strikethrough + green "X% OFF" badge on product cards, store pages, and product detail pages
  • Store-wide discounts: Store.discountPercent + Store.discountLabel — toggle in store Settings, shows green gradient banner on consumer store page
  • Design decision: product.price remains the source of truth for orders/commissions — zero changes to order pipeline

Feature: Campaign & Festival Promotions

  • New StoreCampaign model: title, description, bannerUrl, startDate, endDate, isActive
  • Campaign API: GET/POST/PATCH/DELETE /api/seller/stores/[id]/campaigns — max 10 per store
  • Seller UI: Campaign management in Store Settings with LIVE/UPCOMING/EXPIRED status badges
  • Consumer UI: Horizontally-scrollable campaign banner cards on store detail page (active campaigns within date range only)

Feature: Seller Dashboard Redesign

  • 4-tab nav: Removed Home tab, now Stores | Orders | Earnings | Profile
  • /seller redirects to /seller/stores — Stores is the landing page
  • Stores page: Gradient stats header (Orders, Products, Stores), redesigned store cards with banner images, colored stat icons, active discount badges
  • Store products: "Mark Sold" replaced with "Out of Stock" / "Back in Stock"
  • Product edit: Hides resale-specific fields (Product Type, Condition, Pickup Address) for store products

Bug Fix: Seller notification-settings logout

  • Symptom: Clicking "Notification Settings" in Profile → app signs out
  • Root Cause: notification-prefs API used hardcoded cookie name ka26_seller_token instead of correct seller_token
  • Fix: Replaced with getAuthenticatedSeller() from shared auth library
  • Lesson: Always use shared auth helpers, never hardcode cookie names in individual APIs

Bug Fix: Admin seller delete silently failing

  • Symptom: "Delete Forever" button did nothing — no error, no feedback
  • Root Cause: Delete transaction only handled products/images/interests/invites but missed stores, restaurants, orders, notifications, push tokens, campaigns (15+ tables). Foreign key constraint error was silently swallowed.
  • Fix: Complete cascade covering all relations in correct order + error alert in UI
  • Test: tests/admin-dashboard.test.ts — 12 tests verify delete cascade covers every relation

Bug Fix: Infinite redirect loop for restaurant sellers

  • Symptom: Non-admin sellers with sellerType: 'restaurant' saw continuous page vibration and "page not available" error with no way to log out
  • Root Cause: isRouteBlocked() still had active blocking logic for /seller/stores when seller type was 'restaurant'. Combined with new Home → Stores redirect, this created: /seller/seller/stores → blocked → /seller → repeat
  • Fix: Made isRouteBlocked() always return false (Eats vertical is hidden, all sellers use store UI)
  • Lesson: When disabling a feature, disable ALL related code paths — not just the nav items

Testing: Admin Dashboard Tests (139 test cases)

  • New test file: tests/admin-dashboard.test.ts
  • Covers all admin pages (Sellers, Consumers, Creators, Delivery, Doctors, Invites, Observability)
  • Verifies every button, action, modal, filter, API endpoint, auth guard, and data display
  • Specific coverage: delete cascade completeness, error feedback, discount system, campaign CRUD, seller navigation

Files Changed (18 files, 6 commits)

  • prisma/schema.prisma — +originalPrice, +discountPercent/Label, +StoreCampaign model (39 models total)
  • src/app/seller/layout.tsx — 4-tab nav, isRouteBlocked always false
  • src/app/seller/page.tsx — Redirect to /seller/stores
  • src/app/seller/stores/page.tsx — Redesigned landing page with stats
  • src/app/seller/stores/[id]/page.tsx — Discount forms + campaign management + Out of Stock
  • src/app/seller/products/[id]/edit/page.tsx — Discount toggle + hide resale fields
  • src/app/api/seller/stores/[id]/campaigns/route.ts — New campaign CRUD
  • src/app/api/seller/stores/[id]/route.ts — Accept discount fields
  • src/app/api/products/route.ts + [id]/route.ts — Accept/return originalPrice
  • src/app/api/stores/[id]/route.ts — Include active campaigns for consumer
  • src/app/api/seller/notification-prefs/route.ts — Use shared auth
  • src/app/api/admin/users/[id]/route.ts — Complete cascade delete
  • src/app/(consumer)/stores/[id]/page.tsx — Discount display + campaign banners
  • src/app/(consumer)/products/[id]/page.tsx — Strikethrough price
  • src/components/storefront/ProductCard.tsx — Discount badge
  • src/app/admin/page.tsx — Delete error feedback
  • tests/admin-dashboard.test.ts — 139 new test cases

[2026-04-09] Influence-First Profile Redesign

Context

The reels profile page was a copy of Instagram's vanity-metric layout (followers/likes/views → tabs). In KA26, creators are local business partners — their influence drives real purchases. The profile must reflect business impact, not social clout. This redesign makes "Products I Influence" the hero section and treats earnings as contextual data woven throughout.

Design Philosophy: "Warm Luxury"

  • Dark hero header (gray-900 gradient) with amber/gold accent for earnings — premium banking app feel
  • Products-first layout: no more tabs, vertical scroll with Products → Reels → Earnings → Community
  • Glass cards in hero (white/10 + backdrop-blur) for impact summary
  • Collapsible sections for Earnings Detail and Community (keeps page focused)
  • Generous spacing, large bold numbers, uppercase tracking-wider labels

API Enhancement (src/app/api/consumer/reels-profile/route.ts)

  • Added impactSummary top-level field: totalProductsInfluenced, totalEarnings, totalClicks, totalConversions, avgConversionRate
  • Added per-product earnings (paise), conversions (count), conversionRate (%), reelIds (array) to each productInfluence item
  • Groups ReelEarning by productId/menuItemId for per-product earnings
  • All additions are backward-compatible (new fields only, no breaking changes)

Web Redesign (src/app/(consumer)/profile/reels/page.tsx)

  • Removed 4-tab system (Reels | Influence | Earnings | Followers)
  • Dark hero: gradient bg, gold-ringed avatar, "Influencing X products" tagline, 3 glass impact cards (Earned/Products/Clicks), amber wallet banner
  • Products I Influence: large cards with product image, name, price, per-product earnings/reels/clicks, conversion rate progress bar
  • My Reels: 3-column grid (unchanged internals, new section header with count badge)
  • Earnings Detail: collapsible section with chevron toggle (contains existing earnings breakdown)
  • Community: collapsible section showing follower/following counts, expands to full list
  • Responsive: full-width dark hero, max-w-2xl content, px-3/sm:px-4 padding

Mobile Redesign (mobile/app/reels-profile.tsx)

  • Same structural redesign adapted for React Native
  • LinearGradient dark hero with amber accent colors
  • Product cards with conversion bars using View percentage width
  • Collapsible sections using TouchableOpacity + conditional render
  • All management features preserved (hide/delete/earned badges on grid)

Test Fix

  • Updated creator-economy.test.ts: changed key: "earnings" (old tab key) to earningsExpanded (new collapsible state)

Results

  • 1340 web tests pass, 445 mobile tests pass
  • No breaking API changes
  • Both platforms share identical information architecture

[2026-04-08] Mobile Bug Fixes, Edit Ad, Custom Categories, Push Notifications

Bug Fixes

  • Ad images not showing (Shop + My Ads): create-ad.tsx uploaded to /api/upload (seller-only auth). Consumer token got 401, images array stayed empty. Fixed: use /api/consumer/upload?type=ad. Same fix applied to reel photo uploads.
  • Keyboard covers Pickup Location (Create Ad): Android KeyboardAvoidingView had behavior={undefined}. Fixed: behavior="height" + keyboardVerticalOffset={20} + 160px bottom spacer.
  • Push notifications not received on mobile: registerForPushNotifications() only called in Profile tab. If user never opened Profile, no Expo push token registered. Fixed: moved to (tabs)/_layout.tsx (runs on every app launch).
  • CI failing: Mobile Vitest resolved root postcss.config.mjs (needs @tailwindcss/postcss). Fixed: inline empty PostCSS config in mobile/vitest.config.ts.
  • Next.js build failing: tsconfig.json included mobile/**/*.tsx. Fixed: added "mobile" to exclude list.

New Features

  • Edit Ad screen (edit-ad.tsx): Full form with image management (existing + new images), category picker, all fields pre-populated from API. Edit button added to My Ads cards.
  • Custom service categories: Users can create their own categories in Edit Profile → Service Picker. Custom categories appear in Post Request form's category dropdown. "Create custom" option when search doesn't match.
  • 7 new preset categories: Photographer, Videographer, DJ, Event Planner, Mehendi Artist, Interior Designer, Delivery.
  • Consumer upload endpoint upgraded: Accepts ?type=ad for 1200x1200 webp (10MB) vs default 400x400 profile photos.

Documentation

  • Added 12 mobile-specific rules to bugs_fixed.md anti-pattern registry.

[2026-04-08] Instagram-Style Post Reel + Shop Page Fix + UI Lock

Feature: Instagram-Style Post Reel Flow (LOCKED UI)

  • Redesigned post reel from half-screen bottom sheet to full-screen modal (presentationStyle="fullScreen")
  • Step 1 — Select Video: Full-screen video playback immediately after selection (expo-av <Video>), Close (✕) top-left, duration badge top-right, "Change" + "Next →" bottom bar, drafts banner overlay
  • Step 2 — New Reel (Instagram-style details page): "← New reel" header, centered video thumbnail (55% width, 9:16 ratio, 16px radius) with "Edit cover" overlay, borderless caption (500 chars), # Hashtags toggle + emoji chips, hashtag suggestions (#KA26, #Gadag, etc.), clean action rows (Tag products >, Add location >, Visibility toggle, Trim video >), location quick-pick chips, earnings banner, "Save Draft" + "Post Reel" bottom bar
  • Simplified flow: Removed separate "review" step — Post Reel button is directly on details page
  • Draft system: Save/load/resume/delete drafts via AsyncStorage (max 5), discard dialog offers "Save Draft"
  • UI LOCKED: 20+ code integrity tests prevent any modification to the approved layout
  • Files Changed: mobile/app/(tabs)/reels.tsx

Fix: App crashes on video upload (VideoThumbnails — 5 iterations)

  • Root Cause: VideoThumbnails.getThumbnailAsync() is a native FFmpeg call. When it crashes, it kills the native thread — JavaScript try/catch CANNOT catch it. This caused 5 separate crash iterations.
  • Fix: Completely removed all VideoThumbnails calls from the post flow. Video preview uses expo-av <Video> component (safe, different native module). Background feed thumbnail generation pauses during post flow (postStep !== null guard).
  • Architecture Decision: Client-side FFmpeg thumbnail extraction is inherently unsafe on Android (limited hardware decoder instances). Future "Edit Cover" will use video scrubbing (safe <Video>) instead.
  • Lesson: Native thread crashes bypass JavaScript error boundaries. The only safe fix is to not call the crashing API at all.
  • Files Changed: mobile/app/(tabs)/reels.tsx

Fix: Shop page ad filter chips clipped/unreadable

  • Problem: ScrollView with flexGrow: 0 caused Android to miscalculate chip height — text was cut off
  • Fix: Wrapped chips in parent View for stable height, increased paddingVertical, added alignItems: "center"
  • Files Changed: mobile/app/(tabs)/shop.tsx

Tests: Post Reel UI Lock (25+ tests)

  • Added POST REEL UI LOCK test suite that will FAIL if anyone modifies the approved post reel layout
  • Tests lock: full-screen modal, video playback, "New reel" header, thumbnail dimensions, "Edit cover" overlay, caption input, hashtag/emoji chips, action rows (tag/location/visibility/trim), earnings banner, Save Draft + Post Reel buttons, crash prevention (no VideoThumbnails in post flow), draft system, redirect tracking
  • Files Changed: mobile/tests/mobile-code-integrity.test.ts

[2026-04-08] Branding Update + Mobile UX Fixes + Test Suite Expansion

Feature: New Multi-Color Brand Identity

  • Logo: KA (red #DC2626) + 26 (black #1A1A1A)
  • Updated everywhere: App icon (1024px), adaptive icon, splash screen, favicon, web header, web footer, auth page, email templates, PWA manifest, page metadata (title/OG/Twitter), mobile header
  • Files Changed: mobile/assets/icon.png, mobile/assets/adaptive-icon.png, mobile/assets/splash-icon.png, mobile/assets/favicon.png, mobile/app.json, mobile/android/.../colors.xml, mobile/app/(tabs)/shop.tsx, src/components/storefront/Header.tsx, src/components/storefront/Footer.tsx, src/app/layout.tsx, src/app/(auth)/consumer-auth/page.tsx, src/lib/email.ts

Fix: Restaurant category tabs non-functional and mispositioned

  • Problem: Category filter chips (Popular, Main Course, South Indian) were above restaurant info and didn't do anything when tapped
  • Fix: Moved tabs below restaurant info (inside ListHeaderComponent). Tapping a tab now scrolls the SectionList to that section via scrollToLocation() with sectionListRef
  • Files Changed: mobile/app/restaurant/[id].tsx
  • Problem: Items like "Dosa" appeared in both "🔥 Popular" AND "Main Course" sections with shared quantity — confusing for users
  • Fix: Popular items now appear only in "🔥 Popular" section. A popularIds Set excludes them from their regular category section. Matches how Swiggy/Zomato handle bestseller items.
  • Files Changed: mobile/app/restaurant/[id].tsx

Fix: Keyboard covers UTR input in UPI payment

  • Problem: Tapping the UTR/UPI Reference input opened keyboard that covered the field — user couldn't see what they typed
  • Fix: Wrapped all UPI and checkout modals with KeyboardAvoidingView (behavior: "padding" iOS, "height" Android) + keyboardShouldPersistTaps="handled" + bottom padding. Applied to both restaurant and store screens.
  • Files Changed: mobile/app/restaurant/[id].tsx, mobile/app/store/[id].tsx

Fix: "I Can Help" button was a giant full-screen blue rectangle

  • Problem: helpButton style had flex: 1 causing it to expand vertically to fill the entire empty state container
  • Fix: Removed flex: 1, changed to compact pill button (borderRadius: 24, alignSelf: "center", fixed padding). Card row usage retains flex: 1 via inline override. Text changed to "I Can Help" matching web.
  • Files Changed: mobile/app/(tabs)/requests.tsx

Fix: Ads filter chips not clearly visible

  • Problem: Single cramped row mixing category + sort chips with small text and minimal padding
  • Fix: Split into two distinct rows matching web: Row 1 = "All Ads" / "My Ads" toggle (authenticated only), Row 2 = Category chips (All, Electronics, Household, Books, Vehicles, Property Buy/Rent). Larger text (13px), more padding, clear borders. "My Ads" fetches from /api/ads.
  • Files Changed: mobile/app/(tabs)/shop.tsx

Feature: Mobile test suite expanded to 482 tests

  • New files: __tests__/integration/store-flow.test.ts (12 tests — store ordering, UPI, subscriptions), tests/post-deploy-smoke.test.ts (34 tests — production health verification)
  • Updated: tests/mobile-code-integrity.test.ts (fixed language.tsx tests), tests/screen-completeness.test.ts (added deduplication, scroll, keyboard tests)
  • CI/CD: Added test-mobile job to .github/workflows/deploy.yml — mobile tests must pass before deployment
  • Total: 395 Vitest + 87 Jest = 482 mobile tests

Fix: Tab bar active tint was still pink

  • Problem: tabBarActiveTintColor: "#EC4899" in _layout.tsx — inconsistent with uniform blue CTA
  • Fix: Changed to COLORS.primary (blue #2563EB)
  • Files Changed: mobile/app/(tabs)/_layout.tsx

Fix: Android notification visibility

  • Problem: Notifications appeared as tiny dot in status bar, not as heads-up banner overlay like WhatsApp
  • Fix: Added urgency: "high" and TTL: 3600 to web-push sendNotification() options. Updated SW to use unique tags per notification (notifType_timestamp) instead of shared "general" tag — same-tag notifications silently replace each other without alerting.
  • Known Limitation: Chrome on Android still controls notification channel importance. Heads-up banners require native app (planned: React Native with Expo).
  • Files Changed: src/lib/push.ts, public/sw.js, src/lib/notifications.ts

Fix: Notification tap redirects to wrong page

  • Problem: Tapping a thread reply notification opened /requests (generic list), not the specific thread
  • Fix: All notification paths now deep-link to specific content:
    • Thread reply → /requests?openThread={threadId}&requestId={requestId} (auto-opens specific thread chat)
    • Order update → /orders/{orderId} (uses existing dynamic route)
    • Prescription → /profile?tab=health&prescriptionId={id}
    • Request match → /requests?requestId={id}
  • Added deep-link handler in requests page: reads URL params, auto-opens the correct request and thread
  • Files Changed: src/lib/notifications.ts, src/app/(consumer)/requests/page.tsx, public/sw.js

Fix: iOS PWA not receiving push notifications

  • Root Cause: PushSubscriber component auto-called Notification.requestPermission() without a user gesture on page load. iOS silently denies this and can permanently block the permission, preventing PostInstallSetup's tap-triggered flow from working later.
  • Fix: PushSubscriber now only auto-subscribes if permission is already "granted". First-time permission left to PostInstallSetup (user tap required).
  • Additional: Bumped PostInstallSetup key to ka26_pwa_setup_v2 so users who had permission burned get a fresh chance. Fixed manifest icon purpose: "any maskable" → separate entries (iOS compatibility).
  • Files Changed: src/components/PushSubscriber.tsx, src/components/PostInstallSetup.tsx, public/manifest.json, src/app/layout.tsx

Fix: Doctor broadcast not pushing to patients

  • Problem: doctorBroadcastToPatients() created DB notifications for patients but never sent push notifications — only notified the doctor
  • Fix: Added pushToConsumers() call to actually push to all patients
  • Files Changed: src/lib/notifications.ts

Feature: Notification settings in consumer Settings page

  • What: New "Notifications" section in /settings showing current push status
  • States: Enabled (green checkmark), Blocked (instructions to fix in device settings), Not yet enabled (tap to enable button)
  • Why: Gives users a way to enable notifications anytime, not just during first-time PWA setup
  • Files Changed: src/app/(consumer)/settings/page.tsx

Infra: SW cache bump to v10

  • Forces all user devices to re-fetch fresh service worker and manifest
  • Files Changed: public/sw.js, src/app/layout.tsx

[2026-04-05] Simplified Ordering System + Location Gating + Order Timestamps

Feature: Simplified Delivery Model

  • Change: All restaurants now operate with selfDelivery = true. Auto-dispatch to delivery partners is disabled (code retained for v2).
  • Why: The full delivery dispatch system (rider scoring, batching, acceptance timeouts) was too complex for the current rural market. Restaurants handle their own delivery.
  • Migration: UPDATE "Restaurant" SET "selfDelivery" = true;
  • Files Changed: prisma/schema.prisma, prisma/migrations/20260405_delivery_radius_self_delivery/

Feature: Delivery Area (Radius + Pincodes)

  • What: Restaurants define delivery area via deliveryRadiusKm (1-50km) and/or deliveryPincodes (comma-separated 6-digit Indian pincodes)
  • Radius: Orders beyond the radius are rejected with a clear error showing actual vs max distance
  • Pincodes: Orders from matching pincodes bypass distance checks — useful for small cities where one restaurant can serve the entire city
  • Intelligence feed: Restaurants filtered by radius (15km floor for pickup visibility) and pincode match
  • Files Changed: prisma/schema.prisma, src/app/api/seller/restaurant/route.ts, src/app/seller/restaurant/page.tsx, src/app/api/orders/route.ts, src/lib/intelligence.ts, src/app/api/intelligence/feed/route.ts

Feature: Location Gate on Eats Page

  • What: Consumers must set a delivery location before browsing restaurants
  • Options: GPS detection, saved addresses (from /api/consumer/addresses/), city quick-picks (Gadag, Hubli, Dharwad)
  • How: Location indicator in header ("Gadag | Change"), gate UI replaces restaurant grid when no location set
  • Files Changed: src/app/(consumer)/eats/page.tsx

Feature: Standalone Orders Page with Timestamps

  • Problem: Orders were an in-page tab on Profile — pull-to-refresh redirected back to profile page instead of refreshing orders
  • Fix: Created standalone /orders route with own header and back button
  • Timestamps: Each order card shows "Ordered: Today 6:28 pm", "Delivered: Today 6:40 pm" (or "Picked up"), and duration ("12 min", "1h 15m")
  • Files Changed: src/app/(consumer)/orders/page.tsx (new), src/app/(consumer)/profile/page.tsx, src/app/api/orders/consumer/route.ts

Bug Fix: Stale name at checkout after profile update

  • Symptom: User changes name from "Siddu" to "Shiva" in profile, but checkout still shows "Siddu"
  • Root Cause: JWT bakes name at login time and is never refreshed when profile updates
  • Fix: Profile update API (/api/auth/consumer-profile PUT) now issues a fresh JWT with updated name/phone. Frontend saves the new token to localStorage immediately.
  • Files Changed: src/app/api/auth/consumer-profile/route.ts, src/app/(consumer)/profile/page.tsx

Bug Fix: Orders not showing in My Orders

  • Symptom: Order KA26-MNLRP116 didn't appear in consumer's order history
  • Root Cause: Orders had consumerId: 18 instead of 6 due to browser having wrong consumer token (seller ID used as consumer ID)
  • Fix: Fixed affected orders in DB. Root cause prevented by resolveConsumerId() which always resolves from JWT, never trusts frontend-supplied consumerId.

[2026-04-04] Application Isolation + Delivery Engine + QA Fixes

Bug Fix: "My Requests" filter showed empty on Requests page

  • Symptom: User clicks "My Requests" tab on /requests → shows "No Requests Yet" even though they have 6 requests
  • Root Cause: Client-side filtering (requests.filter(r => r.consumer.id === user?.id)) on an array fetched WITHOUT auth. The /api/requests call didn't include mine=true, so the user's own requests weren't in the dataset.
  • Fix: Added fetchMyRequests() with mine=true param + auth headers (same pattern as fetchHelpedRequests()). Lazy-loads when "My Requests" tab is selected.
  • Files Changed: src/app/(consumer)/requests/page.tsx
  • Test: Manual QA verified on production. Profile > My Requests count matches Requests page > My Requests filter.
  • Lesson: Never filter client-side on data that wasn't fetched with the right scope. Always match the API call to the filter intent.

Bug Fix: Delivery partner never assigned to orders

  • Symptom: Order KA26-MNKMWCIF stuck in "accepted" status, no delivery partner assigned
  • Root Cause: Restaurant "Mane ಊಟಾ" (ID: 4) had latitude: null, longitude: null. The getOrderLocation() function silently returned null, causing the entire assignment pipeline to skip.
  • Fix:
    1. Added GPS location button to seller restaurant settings page
    2. Added selfDelivery toggle for restaurants that handle their own delivery
    3. Delivery engine now skips geo-filtering when restaurant has no GPS (finds all online riders)
    4. Added null-safety checks throughout delivery assignment pipeline
  • Files Changed: src/app/seller/restaurant/page.tsx, src/app/api/seller/restaurant/route.ts, src/lib/delivery-engine.ts, src/app/api/orders/[orderId]/status/route.ts, prisma/schema.prisma
  • Test: tests/delivery-system.test.ts (747 lines), tests/delivery-app-redesign.test.ts (761 lines)
  • Lesson: Never silently swallow null GPS data. Always have fallback behavior + warn the user.

Feature: Application Isolation (Consumer / Seller / Delivery / Admin)

  • Problem: Root layout wrapped ALL apps with consumer-specific components (AuthProvider, CartProvider, BottomNav). Seller clicking "View Public Page" loaded consumer app with consumer navigation.
  • Fix:
    1. Route groups: (consumer)/, (auth)/, (public)/ with isolated layouts
    2. Root layout stripped to: LanguageProvider, ToastProvider, ErrorBoundary only
    3. Consumer layout: AuthProvider + CartProvider + BottomNav + NotificationPopup
    4. Auth layout: AuthProvider only (no nav)
    5. Public layout: empty (just children)
    6. "View Public Page" → /public/restaurant/:id (not consumer /eats/restaurant/:id)
    7. Middleware route guards for /seller/* and /admin/*
  • Files Changed: src/app/layout.tsx, src/app/(consumer)/layout.tsx, src/app/(auth)/layout.tsx, src/app/(public)/layout.tsx, src/middleware.ts, 29 route files moved
  • Test: tests/app-isolation.test.ts (103 tests, 642 lines)
  • Lesson: Every app must be isolated at the layout level. Shared root = shared bugs.

Config Change: Referral threshold 3 → 2

  • Context: User felt 3 referrals was too high a bar to unlock reel creation
  • Fix: Changed threshold from 3 to 2 in all 6 locations (frontend display + API logic)
  • Files Changed: src/app/(consumer)/reels/page.tsx, src/app/(consumer)/profile/page.tsx, src/app/api/referral/route.ts, src/app/api/referral/can-upload/route.ts, src/app/api/auth/consumer-register/route.ts
  • Lesson: Business constants scattered across files = maintenance nightmare. Consider centralizing in a config file.

Known Patterns That Cause Bugs (Anti-Pattern Registry)

1. Client-side filtering on incomplete data

Pattern: Fetch all items → filter locally by user ID Why it breaks: The "all items" fetch may not include the user's own items (different API scope) Fix: Always use server-side filtering with proper auth headers Affected: Requests page "My Requests" filter

2. Silent null handling in geo/location code

Pattern: if (!location) return; with no error/warning Why it breaks: Downstream code silently skips critical operations Fix: Always log warnings, show UI indicators, have fallback behavior Affected: Delivery assignment pipeline

3. Consumer components in shared layouts

Pattern: Putting app-specific providers/nav in root layout Why it breaks: Every app inherits consumer UI (BottomNav, AuthProvider) Fix: Use Next.js route groups with isolated layouts Affected: Seller app, delivery app, admin app

4. Hardcoded business constants

Pattern: >= 3 scattered across 6 files Why it breaks: Change one, miss another → inconsistent behavior Fix: Centralize constants (TODO: create src/lib/constants.ts) Affected: Referral threshold

5. Stale JWT after profile changes

Pattern: JWT bakes user data (name, phone) at login time; profile updates don't refresh the token Why it breaks: Checkout, headers, and other pages show stale name/phone until user re-logs in Fix: Issue fresh JWT from profile update API, save to localStorage on frontend Affected: Checkout page, order confirmation, any page reading name from auth context

6. In-page tabs instead of standalone routes

Pattern: Orders/settings as tab state within a parent page (e.g., ?tab=orders on profile) Why it breaks: Pull-to-refresh reloads the parent page and resets tab state; deep links don't work; browser back button confusion Fix: Create standalone routes (/orders, /settings) with their own pages Affected: My Orders section in profile

7. Missing auth headers on filtered API calls

Pattern: Fetch with filter params but no Authorization header Why it breaks: API can't identify the user → returns wrong/empty data Fix: Always include getAuthHeaders() when fetching user-specific data Affected: My Requests, My Orders, any "mine" filter


[2026-04-27] GCP Phase 3: Load Balancer removed → Cloud Run domain mapping

Replaced the global HTTPS Load Balancer fronting ka26.shop with native Cloud Run domain mappings (gcloud beta run domain-mappings create). DNS swapped in GoDaddy: deleted A 34.8.146.193; added 4 Google A IPs (216.239.32–38.21) + 4 IPv6 AAAA records on @; updated existing www CNAME to ghs.googlehosted.com.. Both certs auto-provisioned by Google (issuer Google Trust Services CN=WR3, valid through 2026-07-25). Tore down 6 LB resources: 2 forwarding rules, 2 target proxies, 2 URL maps, 1 backend service, 1 NEG, 1 static IP. Saves ~€18/month effective from June billing.

Architecture change: Browser → static IP → forwarding rule → HTTPS proxy → URL map → backend service → NEG → Cloud Run becomes Browser → Google's anycast frontend → Cloud Run. Two hops less, faster TTFB.