Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
# Required for local development when testing AI-assisted pantry parsing.
# Set this in your local .env.local and in Vercel project environment variables.
GEMINI_API_KEY="YOUR_GEMINI_API_KEY"

# Base URL for pinned catalog images (owned CDN/bucket). Required.
# Example: https://cdn.example.com/rasoi/ingredients
VITE_INGREDIENT_IMAGE_BASE_URL="https://cdn.example.com/rasoi/ingredients"
68 changes: 68 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,76 @@ jobs:
node-version: 20
cache: npm

- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"

- name: Install dependencies
run: npm ci

- name: Run full verification
run: npm run verify:local

detect-firestore-changes:
name: detect-firestore-changes
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
outputs:
firestore_changed: ${{ steps.filter.outputs.firestore_changed }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Detect firestore-related changes
id: filter
uses: dorny/paths-filter@v3
with:
filters: |
firestore_changed:
- firestore.rules
- firebase.json
- firebase-applet-config.json
- scripts/print-firestore-rules-target.mjs
- scripts/smoke-firestore-unknown-queue.mjs

firestore-rules-deploy:
name: firestore-rules-deploy
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-firestore-changes.outputs.firestore_changed == 'true'
needs:
- verify-local
- detect-firestore-changes
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Verify firestore deploy target
run: npm run rules:target:check

- name: Deploy Firestore rules (production)
env:
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
run: npm run rules:deploy:prod

- name: Smoke test unknown ingredient queue access (production)
env:
SMOKE_OWNER_EMAIL: ${{ secrets.SMOKE_OWNER_EMAIL }}
SMOKE_OWNER_PASSWORD: ${{ secrets.SMOKE_OWNER_PASSWORD }}
SMOKE_OWNER_HOUSEHOLD_ID: ${{ secrets.SMOKE_OWNER_HOUSEHOLD_ID }}
SMOKE_NON_MEMBER_EMAIL: ${{ secrets.SMOKE_NON_MEMBER_EMAIL }}
SMOKE_NON_MEMBER_PASSWORD: ${{ secrets.SMOKE_NON_MEMBER_PASSWORD }}
run: npm run rules:smoke:prod
17 changes: 17 additions & 0 deletions ACTIVITY_LOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Activity Log

## 2026-03-25

- Reviewed owner/cook flows, inventory transitions, AI parsing validation, multilingual pantry labels, and activity-log rendering.
- Added `QA_EDGE_CASE_MATRIX.md` with 34 prioritized edge cases covering the requested risk areas.
- Expanded automated coverage in `test/unit/run.ts` and `test/rules/run.ts` for AI parse validation, localized labels, deterministic log construction, and inventory/log write consistency.
- Added deterministic ingredient visual resolver and applied image + fallback rendering across cook, grocery, and pantry surfaces.
- Refined owner-side UX hierarchy (settings grouping, owner tab affordance, responsive meal planner card layout).
- Validation completed: `npm run lint`, `npm run unit:test`, `npm run build`, and `npm run rules:test` (rules test executed successfully outside sandbox due emulator port restrictions in sandbox mode).
- Added 10 follow-up QA edge cases for future ingredient additions, bilingual naming recognition, responsive owner/pantry layouts, and owner-tab keyboard navigation.
- Appended targeted unit coverage in `test/unit/run.ts` for bilingual ingredient matching, catalog fallback behavior for future items, and category search aliases.
- Validation commands/results:
- `npm run lint` -> passed
- `npm run unit:test` -> passed
- `npm run build` -> passed
- `npm run rules:test` -> passed (outside sandbox)
38 changes: 38 additions & 0 deletions QA_EDGE_CASE_MATRIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# QA Edge Case Matrix

| ID | Priority | Area | Edge case | Expected result |
|---|---|---|---|---|
| Q-01 | P0 | Owner flow | Owner signs in with the matching household owner UID. | Owner dashboard loads with settings, meals, grocery, and pantry access. |
| Q-02 | P0 | Cook flow | Cook signs in with the invited household email. | Cook dashboard loads with the cook workspace and pantry check tools. |
| Q-03 | P0 | Access control | Cook signs in after access is removed by the owner. | Access removed screen appears and the cook cannot continue. |
| Q-04 | P1 | Owner flow | Owner changes the cook invite email casing before saving. | Email is normalized to lowercase and persisted consistently. |
| Q-05 | P1 | Multilingual labels | Owner language is switched to Hindi. | Owner-facing labels update to Hindi copy. |
| Q-06 | P1 | Multilingual labels | Cook language is switched to English. | Cook-facing labels update to English copy. |
| Q-07 | P0 | Inventory transition | In-stock item is marked low by the owner. | Pantry item updates, and a matching activity log entry is written. |
| Q-08 | P0 | Inventory transition | In-stock item is marked out within the anomaly window. | Verification warning appears with a clear anomaly reason. |
| Q-09 | P0 | Inventory transition | Low or out item gets a note from the cook. | Requested quantity persists and renders on reopen. |
| Q-10 | P1 | Inventory transition | Owner clears a verification warning after review. | Warning text disappears while item status stays unchanged. |
| Q-11 | P1 | Inventory transition | Owner adds a new pantry item with a custom quantity. | Item is saved with normalized category and default quantity. |
| Q-12 | P1 | Search behavior | Pantry search uses the English alias `ration`. | Staples items match and remain visible. |
| Q-13 | P1 | Search behavior | Pantry search uses the Hindi label `मुख्य`. | Staples items match through localized category copy. |
| Q-14 | P0 | AI parsing | AI returns a valid response with multiple updates and one unlisted item. | All valid updates apply and the unlisted item is created. |
| Q-15 | P0 | AI parsing | AI response sets `understood` to a non-boolean value. | Parsing fails safely with no writes. |
| Q-16 | P0 | AI parsing | AI response has malformed arrays or invalid item shapes. | Parsing fails safely and the cook view stays usable. |
| Q-17 | P1 | AI parsing | AI response contains one valid update and one unknown item. | Valid updates apply and the partial-match warning appears. |
| Q-18 | P1 | AI parsing | AI response includes a quantity like `2kg`. | Requested quantity is preserved on the inventory item and note display. |
| Q-19 | P1 | Multilingual labels | Pantry labels are viewed in English and Hindi for the same category. | English and Hindi names stay aligned for the same pantry category. |
| Q-20 | P1 | Multilingual labels | Cook helper text switches with the selected language. | Assistant hints and button labels match the current language. |
| Q-21 | P0 | Activity logs | Every successful pantry status change writes a log entry. | Logs tab shows the new entry immediately after the change. |
| Q-22 | P0 | Activity logs | Log entry is created for the correct actor and item. | Role, item name, and status text match the write that occurred. |
| Q-23 | P1 | Activity logs | Multiple updates happen in sequence. | Logs are ordered newest-first and no entry is lost. |
| Q-24 | P1 | Activity logs | No pantry writes have occurred yet. | Empty-state copy is shown instead of stale or partial logs. |
| Q-25 | P1 | Future ingredient additions | Owner adds a new pantry item that is not in the ingredient catalog yet, such as `Curry Leaves`. | The item saves successfully with a category fallback icon and no dependency on catalog metadata. |
| Q-26 | P1 | Future ingredient additions | Owner adds a new pantry item with punctuation and casing variation, such as `Red Chilli Powder (Kashmiri)`. | The item name is preserved, the category is normalized, and the best matching visual path is still selected. |
| Q-27 | P1 | Future ingredient additions | Cook submits an unlisted ingredient request for a future inventory item. | The new item is created, stays readable on reopen, and keeps the requested quantity intact. |
| Q-28 | P1 | Bilingual naming recognition | Ingredient matching is attempted with English and Hindi aliases for the same item, such as `Cumin Seeds` and `जीरा`. | Search and visual resolution land on the same pantry item instead of creating a duplicate. |
| Q-29 | P1 | Bilingual naming recognition | Ingredient matching is attempted with Hindi first and an English alias, such as `नमक` and `Salt`. | The same pantry item is recognized correctly in both directions. |
| Q-30 | P1 | Bilingual naming recognition | Pantry search uses a mixed-language query like `atta आटा` or `sabzi सब्ज़ियाँ`. | The expected item or category stays visible and unrelated rows remain hidden. |
| Q-31 | P2 | Responsive behavior | Owner workspace is viewed on a narrow mobile viewport. | The tab row stacks cleanly, labels stay readable, and horizontal overflow does not block section switching. |
| Q-32 | P2 | Responsive behavior | Pantry add form is viewed on a small screen while typing a long ingredient name. | The inputs stack vertically and the add button remains fully usable without clipping. |
| Q-33 | P2 | Owner tab keyboard interactions | Keyboard user tabs to the owner tablist and activates Grocery or Pantry with Enter or Space. | The selected tab changes without a mouse and the visible panel updates immediately. |
| Q-34 | P2 | Owner tab keyboard interactions | Keyboard user navigates the stacked owner tabs on a narrow screen. | Focus remains visible, the active tab state stays clear, and wrapped labels do not trap keyboard navigation. |
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,24 @@ These constraints are enforced in `firestore.rules` and validated by `test/rules
### Firebase
- Firestore security rules source: `firestore.rules`
- Local emulator config: `firebase.json`
- App uses Firestore named database: `ai-studio-3900af62-0bf5-496a-a136-d1c8a0c4b8bd`
- Confirm production Firebase Auth domain setup before release (Google provider and authorized domains)

### Production Firestore Rules Runbook
1. Mainline path (default):
- Merge to `main`; CI deploys `firestore.rules` automatically when Firestore files change.
2. CI deploy preconditions:
- `verify-local` must pass.
- `npm run rules:target:check` must pass (project/database target integrity gate).
3. Post-deploy smoke checks:
- Owner smoke user must read `households/{householdId}/unknownIngredientQueue`.
- Non-member smoke user must receive `PERMISSION_DENIED`.
4. Emergency/manual deploy only:
- `npm run rules:deploy:prod`
- `npm run rules:smoke:prod`
5. Optional deploy diagnostics:
- `npm run rules:deploy:prod:dry`

## GitHub-Vercel Sync Workflow

This project uses GitHub as the deployment source of truth.
Expand All @@ -229,6 +245,11 @@ This project uses GitHub as the deployment source of truth.
- CI command chain:
- `npm ci`
- `npm run verify:local`
- `verify-local` includes `npm run rules:target:check`
- `main` push additional automation:
- Detect Firestore-related file changes.
- Deploy Firestore rules automatically when changed.
- Run production smoke test for owner-allow and non-member-deny unknown queue reads.

### Local push gate (Husky)
- Husky install hook is configured via `npm run prepare`.
Expand All @@ -241,6 +262,13 @@ This project uses GitHub as the deployment source of truth.
- Require status checks to pass before merging.
- Add required status check: `verify-local`.
- Require branches to be up to date before merging.
- Add repository secrets for Firestore deploy/smoke workflow:
- `FIREBASE_TOKEN`
- `SMOKE_OWNER_EMAIL`
- `SMOKE_OWNER_PASSWORD`
- `SMOKE_OWNER_HOUSEHOLD_ID`
- `SMOKE_NON_MEMBER_EMAIL`
- `SMOKE_NON_MEMBER_PASSWORD`

### Required Vercel settings
- Git repository connected to this GitHub repo.
Expand All @@ -261,6 +289,13 @@ This project uses GitHub as the deployment source of truth.
### Firestore rules tests fail with Java/emulator error
- Install Java 17+ and confirm `java -version` resolves correctly in shell.

### `Unknown ingredient queue access denied... [build:<id>]`
- Confirm the visible build id is the latest deployment.
- Validate CI Firestore deploy and smoke test status on latest `main` run.
- For emergency recovery, deploy + smoke manually:
- `npm run rules:deploy:prod`
- `npm run rules:smoke:prod`

### Google sign-in popup fails locally
- Add `localhost` / `127.0.0.1` to Firebase Auth authorized domains.
- Ensure browser popup blocking is disabled for local app.
Expand Down
13 changes: 10 additions & 3 deletions firebase.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
{
"firestore": {
"rules": "firestore.rules"
},
"firestore": [
{
"database": "(default)",
"rules": "firestore.rules"
},
{
"database": "ai-studio-3900af62-0bf5-496a-a136-d1c8a0c4b8bd",
"rules": "firestore.rules"
}
],
"emulators": {
"firestore": {
"host": "127.0.0.1",
Expand Down
63 changes: 63 additions & 0 deletions firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ service cloud.firestore {
// - newStatus: string (required, 'in-stock', 'low', 'out')
// - timestamp: string (required, ISO date)
// - role: string (required, 'owner', 'cook')
//
// Collection: households/{householdId}/unknownIngredientQueue
// Document ID: auto-generated
// Fields:
// - name: string (required, max 100 chars)
// - category: string (required, max 50 chars)
// - status: string (required, 'open', 'resolved')
// - requestedStatus: string (required, 'in-stock', 'low', 'out')
// - createdAt: string (required, ISO date)
// - createdBy: string (required, 'owner', 'cook')
// - requestedQuantity: string (optional, max 50 chars)
// - resolvedAt: string (optional, ISO date)
// - resolvedBy: string (optional, 'owner', 'cook')
// - resolution: string (optional, 'promoted', 'dismissed')
// - promotedInventoryItemId: string (optional, max 100 chars)
// ===============================================================

function isAuthenticated() {
Expand Down Expand Up @@ -129,6 +144,43 @@ service cloud.firestore {
(!('cookLanguage' in data) || data.cookLanguage in ['en', 'hi']);
}

function isValidUnknownIngredientQueueItem(data) {
return data.keys().hasAll(['name', 'category', 'status', 'requestedStatus', 'createdAt', 'createdBy']) &&
isValidString(data.name, 1, 100) &&
isValidString(data.category, 1, 50) &&
data.status in ['open', 'resolved'] &&
data.requestedStatus in ['in-stock', 'low', 'out'] &&
data.createdAt is string &&
data.createdBy in ['owner', 'cook'] &&
(!('requestedQuantity' in data) || isValidOptionalString(data.requestedQuantity, 0, 50)) &&
(!('resolvedAt' in data) || data.resolvedAt is string) &&
(!('resolvedBy' in data) || data.resolvedBy in ['owner', 'cook']) &&
(!('resolution' in data) || data.resolution in ['promoted', 'dismissed']) &&
(!('promotedInventoryItemId' in data) || isValidOptionalString(data.promotedInventoryItemId, 1, 100));
}

function isValidUnknownQueueCreate(householdId, data) {
return data.status == 'open' &&
data.createdBy == effectiveRole(householdId) &&
!('resolvedAt' in data) &&
!('resolvedBy' in data) &&
!('resolution' in data) &&
!('promotedInventoryItemId' in data);
}

function isValidUnknownQueueResolve(householdId, currentData, nextData) {
return currentData.status == 'open' &&
nextData.status == 'resolved' &&
nextData.createdAt == currentData.createdAt &&
nextData.createdBy == currentData.createdBy &&
nextData.name == currentData.name &&
nextData.category == currentData.category &&
nextData.requestedStatus == currentData.requestedStatus &&
nextData.resolvedBy == effectiveRole(householdId) &&
nextData.resolution in ['promoted', 'dismissed'] &&
nextData.resolvedAt is string;
}

function isValidInventoryWrite(householdId, data) {
return !('updatedBy' in data) || data.updatedBy == effectiveRole(householdId);
}
Expand Down Expand Up @@ -198,6 +250,17 @@ service cloud.firestore {
allow update: if false; // Logs are immutable
allow delete: if isOwner(householdId);
}

match /unknownIngredientQueue/{queueId} {
allow read: if isHouseholdMember(householdId);
allow create: if isHouseholdMember(householdId) &&
isValidUnknownIngredientQueueItem(request.resource.data) &&
isValidUnknownQueueCreate(householdId, request.resource.data);
allow update: if isOwner(householdId) &&
isValidUnknownIngredientQueueItem(request.resource.data) &&
isValidUnknownQueueResolve(householdId, resource.data, request.resource.data);
allow delete: if false;
}
}

// Keep legacy users path for migration
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@
"lint": "tsc --noEmit",
"unit:test": "node --import tsx test/unit/run.ts",
"rules:test": "node test/rules/check-java.mjs && firebase emulators:exec --only firestore --project demo-rasoi-planner \"tsx test/rules/run.ts\"",
"rules:target:check": "node scripts/print-firestore-rules-target.mjs",
"rules:deploy:prod": "npx firebase deploy --only firestore:rules --project gen-lang-client-0862152879",
"rules:deploy:prod:dry": "npx firebase deploy --only firestore:rules --project gen-lang-client-0862152879 --debug",
"rules:smoke:prod": "node scripts/smoke-firestore-unknown-queue.mjs",
"e2e": "node test/e2e/run.mjs",
"verify:local": "npm run lint && npm run unit:test && npm run build && npm run rules:test && npm run e2e",
"e2e:headed": "E2E_HEADLESS=false node test/e2e/run.mjs",
"verify:local": "npm run rules:target:check && npm run lint && npm run unit:test && npm run build && npm run rules:test && npm run e2e",
"prepare": "husky"
},
"dependencies": {
Expand Down
Loading
Loading