Skip to content

Commit d07242c

Browse files
Merge pull request #6 from shadowdevcode/codex/final-ui-release-candidate
feat: pantry workflow polish with stronger coverage
2 parents 9429b62 + 826b237 commit d07242c

34 files changed

+3856
-343
lines changed

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22
# Required for local development when testing AI-assisted pantry parsing.
33
# Set this in your local .env.local and in Vercel project environment variables.
44
GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
5+
6+
# Base URL for pinned catalog images (owned CDN/bucket). Required.
7+
# Example: https://cdn.example.com/rasoi/ingredients
8+
VITE_INGREDIENT_IMAGE_BASE_URL="https://cdn.example.com/rasoi/ingredients"

.github/workflows/ci.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,76 @@ jobs:
2626
node-version: 20
2727
cache: npm
2828

29+
- name: Setup Java
30+
uses: actions/setup-java@v4
31+
with:
32+
distribution: temurin
33+
java-version: "21"
34+
2935
- name: Install dependencies
3036
run: npm ci
3137

3238
- name: Run full verification
3339
run: npm run verify:local
40+
41+
detect-firestore-changes:
42+
name: detect-firestore-changes
43+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
44+
runs-on: ubuntu-latest
45+
outputs:
46+
firestore_changed: ${{ steps.filter.outputs.firestore_changed }}
47+
48+
steps:
49+
- name: Checkout
50+
uses: actions/checkout@v4
51+
52+
- name: Detect firestore-related changes
53+
id: filter
54+
uses: dorny/paths-filter@v3
55+
with:
56+
filters: |
57+
firestore_changed:
58+
- firestore.rules
59+
- firebase.json
60+
- firebase-applet-config.json
61+
- scripts/print-firestore-rules-target.mjs
62+
- scripts/smoke-firestore-unknown-queue.mjs
63+
64+
firestore-rules-deploy:
65+
name: firestore-rules-deploy
66+
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-firestore-changes.outputs.firestore_changed == 'true'
67+
needs:
68+
- verify-local
69+
- detect-firestore-changes
70+
runs-on: ubuntu-latest
71+
timeout-minutes: 20
72+
73+
steps:
74+
- name: Checkout
75+
uses: actions/checkout@v4
76+
77+
- name: Setup Node.js
78+
uses: actions/setup-node@v4
79+
with:
80+
node-version: 20
81+
cache: npm
82+
83+
- name: Install dependencies
84+
run: npm ci
85+
86+
- name: Verify firestore deploy target
87+
run: npm run rules:target:check
88+
89+
- name: Deploy Firestore rules (production)
90+
env:
91+
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
92+
run: npm run rules:deploy:prod
93+
94+
- name: Smoke test unknown ingredient queue access (production)
95+
env:
96+
SMOKE_OWNER_EMAIL: ${{ secrets.SMOKE_OWNER_EMAIL }}
97+
SMOKE_OWNER_PASSWORD: ${{ secrets.SMOKE_OWNER_PASSWORD }}
98+
SMOKE_OWNER_HOUSEHOLD_ID: ${{ secrets.SMOKE_OWNER_HOUSEHOLD_ID }}
99+
SMOKE_NON_MEMBER_EMAIL: ${{ secrets.SMOKE_NON_MEMBER_EMAIL }}
100+
SMOKE_NON_MEMBER_PASSWORD: ${{ secrets.SMOKE_NON_MEMBER_PASSWORD }}
101+
run: npm run rules:smoke:prod

ACTIVITY_LOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Activity Log
2+
3+
## 2026-03-25
4+
5+
- Reviewed owner/cook flows, inventory transitions, AI parsing validation, multilingual pantry labels, and activity-log rendering.
6+
- Added `QA_EDGE_CASE_MATRIX.md` with 34 prioritized edge cases covering the requested risk areas.
7+
- 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.
8+
- Added deterministic ingredient visual resolver and applied image + fallback rendering across cook, grocery, and pantry surfaces.
9+
- Refined owner-side UX hierarchy (settings grouping, owner tab affordance, responsive meal planner card layout).
10+
- 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).
11+
- Added 10 follow-up QA edge cases for future ingredient additions, bilingual naming recognition, responsive owner/pantry layouts, and owner-tab keyboard navigation.
12+
- Appended targeted unit coverage in `test/unit/run.ts` for bilingual ingredient matching, catalog fallback behavior for future items, and category search aliases.
13+
- Validation commands/results:
14+
- `npm run lint` -> passed
15+
- `npm run unit:test` -> passed
16+
- `npm run build` -> passed
17+
- `npm run rules:test` -> passed (outside sandbox)

QA_EDGE_CASE_MATRIX.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# QA Edge Case Matrix
2+
3+
| ID | Priority | Area | Edge case | Expected result |
4+
|---|---|---|---|---|
5+
| Q-01 | P0 | Owner flow | Owner signs in with the matching household owner UID. | Owner dashboard loads with settings, meals, grocery, and pantry access. |
6+
| Q-02 | P0 | Cook flow | Cook signs in with the invited household email. | Cook dashboard loads with the cook workspace and pantry check tools. |
7+
| Q-03 | P0 | Access control | Cook signs in after access is removed by the owner. | Access removed screen appears and the cook cannot continue. |
8+
| Q-04 | P1 | Owner flow | Owner changes the cook invite email casing before saving. | Email is normalized to lowercase and persisted consistently. |
9+
| Q-05 | P1 | Multilingual labels | Owner language is switched to Hindi. | Owner-facing labels update to Hindi copy. |
10+
| Q-06 | P1 | Multilingual labels | Cook language is switched to English. | Cook-facing labels update to English copy. |
11+
| 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. |
12+
| Q-08 | P0 | Inventory transition | In-stock item is marked out within the anomaly window. | Verification warning appears with a clear anomaly reason. |
13+
| Q-09 | P0 | Inventory transition | Low or out item gets a note from the cook. | Requested quantity persists and renders on reopen. |
14+
| Q-10 | P1 | Inventory transition | Owner clears a verification warning after review. | Warning text disappears while item status stays unchanged. |
15+
| Q-11 | P1 | Inventory transition | Owner adds a new pantry item with a custom quantity. | Item is saved with normalized category and default quantity. |
16+
| Q-12 | P1 | Search behavior | Pantry search uses the English alias `ration`. | Staples items match and remain visible. |
17+
| Q-13 | P1 | Search behavior | Pantry search uses the Hindi label `मुख्य`. | Staples items match through localized category copy. |
18+
| 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. |
19+
| Q-15 | P0 | AI parsing | AI response sets `understood` to a non-boolean value. | Parsing fails safely with no writes. |
20+
| Q-16 | P0 | AI parsing | AI response has malformed arrays or invalid item shapes. | Parsing fails safely and the cook view stays usable. |
21+
| Q-17 | P1 | AI parsing | AI response contains one valid update and one unknown item. | Valid updates apply and the partial-match warning appears. |
22+
| Q-18 | P1 | AI parsing | AI response includes a quantity like `2kg`. | Requested quantity is preserved on the inventory item and note display. |
23+
| 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. |
24+
| Q-20 | P1 | Multilingual labels | Cook helper text switches with the selected language. | Assistant hints and button labels match the current language. |
25+
| Q-21 | P0 | Activity logs | Every successful pantry status change writes a log entry. | Logs tab shows the new entry immediately after the change. |
26+
| 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. |
27+
| Q-23 | P1 | Activity logs | Multiple updates happen in sequence. | Logs are ordered newest-first and no entry is lost. |
28+
| Q-24 | P1 | Activity logs | No pantry writes have occurred yet. | Empty-state copy is shown instead of stale or partial logs. |
29+
| 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. |
30+
| 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. |
31+
| 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. |
32+
| 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. |
33+
| 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. |
34+
| 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. |
35+
| 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. |
36+
| 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. |
37+
| 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. |
38+
| 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. |

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,24 @@ These constraints are enforced in `firestore.rules` and validated by `test/rules
206206
### Firebase
207207
- Firestore security rules source: `firestore.rules`
208208
- Local emulator config: `firebase.json`
209+
- App uses Firestore named database: `ai-studio-3900af62-0bf5-496a-a136-d1c8a0c4b8bd`
209210
- Confirm production Firebase Auth domain setup before release (Google provider and authorized domains)
210211

212+
### Production Firestore Rules Runbook
213+
1. Mainline path (default):
214+
- Merge to `main`; CI deploys `firestore.rules` automatically when Firestore files change.
215+
2. CI deploy preconditions:
216+
- `verify-local` must pass.
217+
- `npm run rules:target:check` must pass (project/database target integrity gate).
218+
3. Post-deploy smoke checks:
219+
- Owner smoke user must read `households/{householdId}/unknownIngredientQueue`.
220+
- Non-member smoke user must receive `PERMISSION_DENIED`.
221+
4. Emergency/manual deploy only:
222+
- `npm run rules:deploy:prod`
223+
- `npm run rules:smoke:prod`
224+
5. Optional deploy diagnostics:
225+
- `npm run rules:deploy:prod:dry`
226+
211227
## GitHub-Vercel Sync Workflow
212228

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

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

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

292+
### `Unknown ingredient queue access denied... [build:<id>]`
293+
- Confirm the visible build id is the latest deployment.
294+
- Validate CI Firestore deploy and smoke test status on latest `main` run.
295+
- For emergency recovery, deploy + smoke manually:
296+
- `npm run rules:deploy:prod`
297+
- `npm run rules:smoke:prod`
298+
264299
### Google sign-in popup fails locally
265300
- Add `localhost` / `127.0.0.1` to Firebase Auth authorized domains.
266301
- Ensure browser popup blocking is disabled for local app.

firebase.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
{
2-
"firestore": {
3-
"rules": "firestore.rules"
4-
},
2+
"firestore": [
3+
{
4+
"database": "(default)",
5+
"rules": "firestore.rules"
6+
},
7+
{
8+
"database": "ai-studio-3900af62-0bf5-496a-a136-d1c8a0c4b8bd",
9+
"rules": "firestore.rules"
10+
}
11+
],
512
"emulators": {
613
"firestore": {
714
"host": "127.0.0.1",

firestore.rules

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ service cloud.firestore {
4545
// - newStatus: string (required, 'in-stock', 'low', 'out')
4646
// - timestamp: string (required, ISO date)
4747
// - role: string (required, 'owner', 'cook')
48+
//
49+
// Collection: households/{householdId}/unknownIngredientQueue
50+
// Document ID: auto-generated
51+
// Fields:
52+
// - name: string (required, max 100 chars)
53+
// - category: string (required, max 50 chars)
54+
// - status: string (required, 'open', 'resolved')
55+
// - requestedStatus: string (required, 'in-stock', 'low', 'out')
56+
// - createdAt: string (required, ISO date)
57+
// - createdBy: string (required, 'owner', 'cook')
58+
// - requestedQuantity: string (optional, max 50 chars)
59+
// - resolvedAt: string (optional, ISO date)
60+
// - resolvedBy: string (optional, 'owner', 'cook')
61+
// - resolution: string (optional, 'promoted', 'dismissed')
62+
// - promotedInventoryItemId: string (optional, max 100 chars)
4863
// ===============================================================
4964

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

147+
function isValidUnknownIngredientQueueItem(data) {
148+
return data.keys().hasAll(['name', 'category', 'status', 'requestedStatus', 'createdAt', 'createdBy']) &&
149+
isValidString(data.name, 1, 100) &&
150+
isValidString(data.category, 1, 50) &&
151+
data.status in ['open', 'resolved'] &&
152+
data.requestedStatus in ['in-stock', 'low', 'out'] &&
153+
data.createdAt is string &&
154+
data.createdBy in ['owner', 'cook'] &&
155+
(!('requestedQuantity' in data) || isValidOptionalString(data.requestedQuantity, 0, 50)) &&
156+
(!('resolvedAt' in data) || data.resolvedAt is string) &&
157+
(!('resolvedBy' in data) || data.resolvedBy in ['owner', 'cook']) &&
158+
(!('resolution' in data) || data.resolution in ['promoted', 'dismissed']) &&
159+
(!('promotedInventoryItemId' in data) || isValidOptionalString(data.promotedInventoryItemId, 1, 100));
160+
}
161+
162+
function isValidUnknownQueueCreate(householdId, data) {
163+
return data.status == 'open' &&
164+
data.createdBy == effectiveRole(householdId) &&
165+
!('resolvedAt' in data) &&
166+
!('resolvedBy' in data) &&
167+
!('resolution' in data) &&
168+
!('promotedInventoryItemId' in data);
169+
}
170+
171+
function isValidUnknownQueueResolve(householdId, currentData, nextData) {
172+
return currentData.status == 'open' &&
173+
nextData.status == 'resolved' &&
174+
nextData.createdAt == currentData.createdAt &&
175+
nextData.createdBy == currentData.createdBy &&
176+
nextData.name == currentData.name &&
177+
nextData.category == currentData.category &&
178+
nextData.requestedStatus == currentData.requestedStatus &&
179+
nextData.resolvedBy == effectiveRole(householdId) &&
180+
nextData.resolution in ['promoted', 'dismissed'] &&
181+
nextData.resolvedAt is string;
182+
}
183+
132184
function isValidInventoryWrite(householdId, data) {
133185
return !('updatedBy' in data) || data.updatedBy == effectiveRole(householdId);
134186
}
@@ -198,6 +250,17 @@ service cloud.firestore {
198250
allow update: if false; // Logs are immutable
199251
allow delete: if isOwner(householdId);
200252
}
253+
254+
match /unknownIngredientQueue/{queueId} {
255+
allow read: if isHouseholdMember(householdId);
256+
allow create: if isHouseholdMember(householdId) &&
257+
isValidUnknownIngredientQueueItem(request.resource.data) &&
258+
isValidUnknownQueueCreate(householdId, request.resource.data);
259+
allow update: if isOwner(householdId) &&
260+
isValidUnknownIngredientQueueItem(request.resource.data) &&
261+
isValidUnknownQueueResolve(householdId, resource.data, request.resource.data);
262+
allow delete: if false;
263+
}
201264
}
202265

203266
// Keep legacy users path for migration

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@
1212
"lint": "tsc --noEmit",
1313
"unit:test": "node --import tsx test/unit/run.ts",
1414
"rules:test": "node test/rules/check-java.mjs && firebase emulators:exec --only firestore --project demo-rasoi-planner \"tsx test/rules/run.ts\"",
15+
"rules:target:check": "node scripts/print-firestore-rules-target.mjs",
16+
"rules:deploy:prod": "npx firebase deploy --only firestore:rules --project gen-lang-client-0862152879",
17+
"rules:deploy:prod:dry": "npx firebase deploy --only firestore:rules --project gen-lang-client-0862152879 --debug",
18+
"rules:smoke:prod": "node scripts/smoke-firestore-unknown-queue.mjs",
1519
"e2e": "node test/e2e/run.mjs",
16-
"verify:local": "npm run lint && npm run unit:test && npm run build && npm run rules:test && npm run e2e",
20+
"e2e:headed": "E2E_HEADLESS=false node test/e2e/run.mjs",
21+
"verify:local": "npm run rules:target:check && npm run lint && npm run unit:test && npm run build && npm run rules:test && npm run e2e",
1722
"prepare": "husky"
1823
},
1924
"dependencies": {

0 commit comments

Comments
 (0)