Compare commits

...

7 Commits

Author SHA1 Message Date
Gitea Actions
94f45d9726 ci: Bump version to 0.11.4 [skip ci] 2026-01-18 14:36:55 +05:00
136a9ce3f3 Add ADR-054 for Bugsink to Gitea issue synchronization and frontend testing summary for 2026-01-18
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 17m3s
- Introduced ADR-054 detailing the implementation of an automated sync worker to create Gitea issues from unresolved Bugsink errors.
- Documented architecture, queue configuration, Redis schema, and implementation phases for the sync feature.
- Added frontend testing summary for 2026-01-18, covering multiple sessions of API testing, fixes applied, and Bugsink error tracking status.
- Included detailed API reference and common validation errors encountered during testing.
2026-01-18 01:35:00 -08:00
Gitea Actions
e65151c3df ci: Bump version to 0.11.3 [skip ci] 2026-01-18 10:49:14 +05:00
3d91d59b9c refactor: update API response handling across multiple queries to ensure compliance with ADR-028
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m53s
- Removed direct return of json.data in favor of structured error handling.
- Implemented checks for success and data array in useActivityLogQuery, useBestSalePricesQuery, useBrandsQuery, useCategoriesQuery, useFlyerItemsForFlyersQuery, useFlyerItemsQuery, useFlyersQuery, useLeaderboardQuery, useMasterItemsQuery, usePriceHistoryQuery, useShoppingListsQuery, useSuggestedCorrectionsQuery, and useWatchedItemsQuery.
- Updated unit tests to reflect changes in expected behavior when API response does not conform to the expected structure.
- Updated package.json to use the latest version of @sentry/vite-plugin.
- Adjusted vite.config.ts for local development SSL configuration.
- Added self-signed SSL certificate and key for local development.
2026-01-17 21:45:51 -08:00
Gitea Actions
822d6d1c3c ci: Bump version to 0.11.2 [skip ci] 2026-01-18 06:50:06 +05:00
a24e28f52f update node packages
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m32s
2026-01-17 17:49:09 -08:00
8dbfa62768 add missing plugin
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 11s
2026-01-17 17:36:25 -08:00
25 changed files with 2007 additions and 40 deletions

19
certs/localhost.crt Normal file
View File

@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUHhZUK1vmww2wCepWPuVcU6d27hMwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDExODAyMzM0NFoXDTI3MDEx
ODAyMzM0NFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAuUJGtSZzd+ZpLi+efjrkxJJNfVxVz2VLhknNM2WKeOYx
JTK/VaTYq5hrczy6fEUnMhDAJCgEPUFlOK3vn1gFJKNMN8m7arkLVk6PYtrx8CTw
w78Q06FLITr6hR0vlJNpN4MsmGxYwUoUpn1j5JdfZF7foxNAZRiwoopf7ZJxltDu
PIuFjmVZqdzR8c6vmqIqdawx/V6sL9fizZr+CDH3oTsTUirn2qM+1ibBtPDiBvfX
omUsr6MVOcTtvnMvAdy9NfV88qwF7MEWBGCjXkoT1bKCLD8hjn8l7GjRmPcmMFE2
GqWEvfJiFkBK0CgSHYEUwzo0UtVNeQr0k0qkDRub6QIDAQABo1MwUTAdBgNVHQ4E
FgQU5VeD67yFLV0QNYbHaJ6u9cM6UbkwHwYDVR0jBBgwFoAU5VeD67yFLV0QNYbH
aJ6u9cM6UbkwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEABueA
8ujAD+yjeP5dTgqQH1G0hlriD5LmlJYnktaLarFU+y+EZlRFwjdORF/vLPwSG+y7
CLty/xlmKKQop70QzQ5jtJcsWzUjww8w1sO3AevfZlIF3HNhJmt51ihfvtJ7DVCv
CNyMeYO0pBqRKwOuhbG3EtJgyV7MF8J25UEtO4t+GzX3jcKKU4pWP+kyLBVfeDU3
MQuigd2LBwBQQFxZdpYpcXVKnAJJlHZIt68ycO1oSBEJO9fIF0CiAlC6ITxjtYtz
oCjd6cCLKMJiC6Zg7t1Q17vGl+FdGyQObSsiYsYO9N3CVaeDdpyGCH0Rfa0+oZzu
a5U9/l1FHlvpX980bw==
-----END CERTIFICATE-----

28
certs/localhost.key Normal file
View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Qka1JnN35mku
L55+OuTEkk19XFXPZUuGSc0zZYp45jElMr9VpNirmGtzPLp8RScyEMAkKAQ9QWU4
re+fWAUko0w3ybtquQtWTo9i2vHwJPDDvxDToUshOvqFHS+Uk2k3gyyYbFjBShSm
fWPkl19kXt+jE0BlGLCiil/tknGW0O48i4WOZVmp3NHxzq+aoip1rDH9Xqwv1+LN
mv4IMfehOxNSKufaoz7WJsG08OIG99eiZSyvoxU5xO2+cy8B3L019XzyrAXswRYE
YKNeShPVsoIsPyGOfyXsaNGY9yYwUTYapYS98mIWQErQKBIdgRTDOjRS1U15CvST
SqQNG5vpAgMBAAECggEAAnv0Dw1Mv+rRy4ZyxtObEVPXPRzoxnDDXzHP4E16BTye
Fc/4pSBUIAUn2bPvLz0/X8bMOa4dlDcIv7Eu9Pvns8AY70vMaUReA80fmtHVD2xX
1PCT0X3InnxRAYKstSIUIGs+aHvV5Z+iJ8F82soOStN1MU56h+JLWElL5deCPHq3
tLZT8wM9aOZlNG72kJ71+DlcViahynQj8+VrionOLNjTJ2Jv/ByjM3GMIuSdBrgd
Sl4YAcdn6ontjJGoTgI+e+qkBAPwMZxHarNGQgbS0yNVIJe7Lq4zIKHErU/ZSmpD
GzhdVNzhrjADNIDzS7G+pxtz+aUxGtmRvOyopy8GAQKBgQDEPp2mRM+uZVVT4e1j
pkKO1c3O8j24I5mGKwFqhhNs3qGy051RXZa0+cQNx63GokXQan9DIXzc/Il7Y72E
z9bCFbcSWnlP8dBIpWiJm+UmqLXRyY4N8ecNnzL5x+Tuxm5Ij+ixJwXgdz/TLNeO
MBzu+Qy738/l/cAYxwcF7mR7AQKBgQDxq1F95HzCxBahRU9OGUO4s3naXqc8xKCC
m3vbbI8V0Exse2cuiwtlPPQWzTPabLCJVvCGXNru98sdeOu9FO9yicwZX0knOABK
QfPyDeITsh2u0C63+T9DNn6ixI/T68bTs7DHawEYbpS7bR50BnbHbQrrOAo6FSXF
yC7+Te+o6QKBgQCXEWSmo/4D0Dn5Usg9l7VQ40GFd3EPmUgLwntal0/I1TFAyiom
gpcLReIogXhCmpSHthO1h8fpDfZ/p+4ymRRHYBQH6uHMKugdpEdu9zVVpzYgArp5
/afSEqVZJwoSzWoELdQA23toqiPV2oUtDdiYFdw5nDccY1RHPp8nb7amAQKBgQDj
f4DhYDxKJMmg21xCiuoDb4DgHoaUYA0xpii8cL9pq4KmBK0nVWFO1kh5Robvsa2m
PB+EfNjkaIPepLxWbOTUEAAASoDU2JT9UoTQcl1GaUAkFnpEWfBB14TyuNMkjinH
lLpvn72SQFbm8VvfoU4jgfTrZP/LmajLPR1v6/IWMQKBgBh9qvOTax/GugBAWNj3
ZvF99rHOx0rfotEdaPcRN66OOiSWILR9yfMsTvwt1V0VEj7OqO9juMRFuIyB57gd
Hs/zgbkuggqjr1dW9r22P/UpzpodAEEN2d52RSX8nkMOkH61JXlH2MyRX65kdExA
VkTDq6KwomuhrU3z0+r/MSOn
-----END PRIVATE KEY-----

271
docs/BUGSINK-SYNC.md Normal file
View File

@@ -0,0 +1,271 @@
# Bugsink to Gitea Issue Synchronization
This document describes the automated workflow for syncing Bugsink error tracking issues to Gitea tickets.
## Overview
The sync system automatically creates Gitea issues from unresolved Bugsink errors, ensuring all application errors are tracked and assignable.
**Key Points:**
- Runs **only on test/staging server** (not production)
- Syncs **all 6 Bugsink projects** (including production errors)
- Creates Gitea issues with full error context
- Marks synced issues as resolved in Bugsink
- Uses Redis db 15 for sync state tracking
## Architecture
```
TEST/STAGING SERVER
┌─────────────────────────────────────────────────┐
│ │
│ BullMQ Queue ──▶ Sync Worker ──▶ Redis DB 15 │
│ (bugsink-sync) (15min) (sync state) │
│ │ │
└──────────────────────┼───────────────────────────┘
┌─────────────┴─────────────┐
▼ ▼
┌─────────┐ ┌─────────┐
│ Bugsink │ │ Gitea │
│ (read) │ │ (write) │
└─────────┘ └─────────┘
```
## Bugsink Projects
| Project Slug | Type | Environment | Label Mapping |
| --------------------------------- | -------- | ----------- | ----------------------------------- |
| flyer-crawler-backend | Backend | Production | bug:backend + env:production |
| flyer-crawler-backend-test | Backend | Test | bug:backend + env:test |
| flyer-crawler-frontend | Frontend | Production | bug:frontend + env:production |
| flyer-crawler-frontend-test | Frontend | Test | bug:frontend + env:test |
| flyer-crawler-infrastructure | Infra | Production | bug:infrastructure + env:production |
| flyer-crawler-test-infrastructure | Infra | Test | bug:infrastructure + env:test |
## Gitea Labels
| Label | Color | ID |
| ------------------ | ------------------ | --- |
| bug:frontend | #e11d48 (Red) | 8 |
| bug:backend | #ea580c (Orange) | 9 |
| bug:infrastructure | #7c3aed (Purple) | 10 |
| env:production | #dc2626 (Dark Red) | 11 |
| env:test | #2563eb (Blue) | 12 |
| env:development | #6b7280 (Gray) | 13 |
| source:bugsink | #10b981 (Green) | 14 |
## Environment Variables
Add these to **test environment only** (`deploy-to-test.yml`):
```bash
# Bugsink API
BUGSINK_URL=https://bugsink.projectium.com
BUGSINK_API_TOKEN=<from Bugsink Settings > API Keys>
# Gitea API
GITEA_URL=https://gitea.projectium.com
GITEA_API_TOKEN=<personal access token with repo scope>
GITEA_OWNER=torbo
GITEA_REPO=flyer-crawler.projectium.com
# Sync Control
BUGSINK_SYNC_ENABLED=true # Only set true in test env
BUGSINK_SYNC_INTERVAL=15 # Minutes between sync runs
```
## Gitea Secrets to Add
Add these secrets in Gitea repository settings (Settings > Secrets):
| Secret Name | Value | Environment |
| ---------------------- | ---------------------- | ----------- |
| `BUGSINK_API_TOKEN` | API token from Bugsink | Test only |
| `GITEA_SYNC_TOKEN` | Personal access token | Test only |
| `BUGSINK_SYNC_ENABLED` | `true` | Test only |
## Redis Configuration
| Database | Purpose |
| -------- | ------------------------ |
| 0 | BullMQ production queues |
| 1 | BullMQ test queues |
| 15 | Bugsink sync state |
**Key Pattern:**
```
bugsink:synced:{issue_uuid}
```
**Value (JSON):**
```json
{
"gitea_issue_number": 42,
"synced_at": "2026-01-17T10:30:00Z",
"project": "flyer-crawler-frontend-test",
"title": "[TypeError] t.map is not a function"
}
```
## Sync Workflow
1. **Trigger**: Every 15 minutes (or manual via admin API)
2. **Fetch**: List unresolved issues from all 6 Bugsink projects
3. **Check**: Skip issues already in Redis sync state
4. **Create**: Create Gitea issue with labels and full context
5. **Record**: Store sync mapping in Redis db 15
6. **Resolve**: Mark issue as resolved in Bugsink
## Issue Template
Created Gitea issues follow this format:
```markdown
## Error Details
| Field | Value |
| ------------ | ----------------------- |
| **Type** | TypeError |
| **Message** | t.map is not a function |
| **Platform** | javascript |
| **Level** | error |
## Occurrence Statistics
- **First Seen**: 2026-01-13 18:24:22 UTC
- **Last Seen**: 2026-01-16 05:03:02 UTC
- **Total Occurrences**: 4
## Request Context
- **URL**: GET https://flyer-crawler-test.projectium.com/
## Stacktrace
<details>
<summary>Click to expand</summary>
[Full stacktrace]
</details>
---
**Bugsink Issue**: https://bugsink.projectium.com/issues/{id}
**Project**: flyer-crawler-frontend-test
```
## Admin Endpoints
### Manual Sync Trigger
```bash
POST /api/admin/bugsink/sync
Authorization: Bearer <admin_jwt>
# Response
{
"success": true,
"data": {
"synced": 3,
"skipped": 12,
"failed": 0,
"duration_ms": 2340
}
}
```
### Sync Status
```bash
GET /api/admin/bugsink/sync/status
Authorization: Bearer <admin_jwt>
# Response
{
"success": true,
"data": {
"enabled": true,
"last_run": "2026-01-17T10:30:00Z",
"next_run": "2026-01-17T10:45:00Z",
"total_synced": 47
}
}
```
## Files to Create
| File | Purpose |
| -------------------------------------- | --------------------- |
| `src/services/bugsinkSync.server.ts` | Core sync logic |
| `src/services/bugsinkClient.server.ts` | Bugsink HTTP client |
| `src/services/giteaClient.server.ts` | Gitea HTTP client |
| `src/types/bugsink.ts` | TypeScript interfaces |
| `src/routes/admin/bugsink-sync.ts` | Admin endpoints |
## Files to Modify
| File | Changes |
| ------------------------------------- | ------------------------- |
| `src/services/queues.server.ts` | Add `bugsinkSyncQueue` |
| `src/services/workers.server.ts` | Add sync worker |
| `src/config/env.ts` | Add bugsink config schema |
| `.env.example` | Document new variables |
| `.gitea/workflows/deploy-to-test.yml` | Pass secrets |
## Implementation Phases
### Phase 1: Core Infrastructure
- [ ] Add env vars to `env.ts` schema
- [ ] Create BugsinkClient service
- [ ] Create GiteaClient service
- [ ] Add Redis db 15 connection
### Phase 2: Sync Logic
- [ ] Create BugsinkSyncService
- [ ] Add bugsink-sync queue
- [ ] Add sync worker
- [ ] Create TypeScript types
### Phase 3: Integration
- [ ] Add admin endpoints
- [ ] Update deploy-to-test.yml
- [ ] Add Gitea secrets
- [ ] End-to-end testing
## Troubleshooting
### Sync not running
1. Check `BUGSINK_SYNC_ENABLED` is `true`
2. Verify worker is running: `GET /api/admin/workers/status`
3. Check Bull Board: `/api/admin/jobs`
### Duplicate issues created
1. Check Redis db 15 connectivity
2. Verify sync state keys exist: `redis-cli -n 15 KEYS "bugsink:*"`
### Issues not resolving in Bugsink
1. Verify `BUGSINK_API_TOKEN` has write permissions
2. Check worker logs for API errors
### Missing stacktrace in Gitea issue
1. Source maps may not be uploaded
2. Bugsink API may have returned partial data
3. Check worker logs for fetch errors
## Related Documentation
- [ADR-054: Bugsink-Gitea Sync](./adr/0054-bugsink-gitea-issue-sync.md)
- [ADR-006: Background Job Processing](./adr/0006-background-job-processing-and-task-queues.md)
- [ADR-015: Error Tracking](./adr/0015-application-performance-monitoring-and-error-tracking.md)

View File

@@ -0,0 +1,337 @@
# ADR-054: Bugsink to Gitea Issue Synchronization
**Date**: 2026-01-17
**Status**: Proposed
## Context
The application uses Bugsink (Sentry-compatible self-hosted error tracking) to capture runtime errors across 6 projects:
| Project | Type | Environment |
| --------------------------------- | -------------- | ------------ |
| flyer-crawler-backend | Backend | Production |
| flyer-crawler-backend-test | Backend | Test/Staging |
| flyer-crawler-frontend | Frontend | Production |
| flyer-crawler-frontend-test | Frontend | Test/Staging |
| flyer-crawler-infrastructure | Infrastructure | Production |
| flyer-crawler-test-infrastructure | Infrastructure | Test/Staging |
Currently, errors remain in Bugsink until manually reviewed. There is no automated workflow to:
1. Create trackable tickets for errors
2. Assign errors to developers
3. Track resolution progress
4. Prevent errors from being forgotten
## Decision
Implement an automated background worker that synchronizes unresolved Bugsink issues to Gitea as trackable tickets. The sync worker will:
1. **Run only on the test/staging server** (not production, not dev container)
2. **Poll all 6 Bugsink projects** for unresolved issues
3. **Create Gitea issues** with full error context
4. **Mark synced issues as resolved** in Bugsink (to prevent re-polling)
5. **Track sync state in Redis** to ensure idempotency
### Why Test/Staging Only?
- The sync worker is a background service that needs API tokens for both Bugsink and Gitea
- Running on test/staging provides a single sync point without duplicating infrastructure
- All 6 Bugsink projects (including production) are synced from this one worker
- Production server stays focused on serving users, not running sync jobs
## Architecture
### Component Overview
```
┌─────────────────────────────────────────────────────────────────────┐
│ TEST/STAGING SERVER │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌───────────────┐ │
│ │ BullMQ Queue │───▶│ Sync Worker │───▶│ Redis DB 15 │ │
│ │ bugsink-sync │ │ (15min repeat) │ │ Sync State │ │
│ └──────────────────┘ └────────┬─────────┘ └───────────────┘ │
│ │ │
└───────────────────────────────────┼──────────────────────────────────┘
┌───────────────┴───────────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Bugsink │ │ Gitea │
│ (6 projects) │ │ (1 repo) │
└──────────────┘ └──────────────┘
```
### Queue Configuration
| Setting | Value | Rationale |
| --------------- | ---------------------- | -------------------------------------------- |
| Queue Name | `bugsink-sync` | Follows existing naming pattern |
| Repeat Interval | 15 minutes | Balances responsiveness with API rate limits |
| Retry Attempts | 3 | Standard retry policy |
| Backoff | Exponential (30s base) | Handles temporary API failures |
| Concurrency | 1 | Serial processing prevents race conditions |
### Redis Database Allocation
| Database | Usage | Owner |
| -------- | ------------------- | --------------- |
| 0 | BullMQ (Production) | Existing queues |
| 1 | BullMQ (Test) | Existing queues |
| 2-14 | Reserved | Future use |
| 15 | Bugsink Sync State | This feature |
### Redis Key Schema
```
bugsink:synced:{bugsink_issue_id}
└─ Value: JSON {
gitea_issue_number: number,
synced_at: ISO timestamp,
project: string,
title: string
}
```
### Gitea Labels
The following labels have been created in `torbo/flyer-crawler.projectium.com`:
| Label | ID | Color | Purpose |
| -------------------- | --- | ------------------ | ---------------------------------- |
| `bug:frontend` | 8 | #e11d48 (Red) | Frontend JavaScript/React errors |
| `bug:backend` | 9 | #ea580c (Orange) | Backend Node.js/API errors |
| `bug:infrastructure` | 10 | #7c3aed (Purple) | Infrastructure errors (Redis, PM2) |
| `env:production` | 11 | #dc2626 (Dark Red) | Production environment |
| `env:test` | 12 | #2563eb (Blue) | Test/staging environment |
| `env:development` | 13 | #6b7280 (Gray) | Development environment |
| `source:bugsink` | 14 | #10b981 (Green) | Auto-synced from Bugsink |
### Label Mapping
| Bugsink Project | Bug Label | Env Label |
| --------------------------------- | ------------------ | -------------- |
| flyer-crawler-backend | bug:backend | env:production |
| flyer-crawler-backend-test | bug:backend | env:test |
| flyer-crawler-frontend | bug:frontend | env:production |
| flyer-crawler-frontend-test | bug:frontend | env:test |
| flyer-crawler-infrastructure | bug:infrastructure | env:production |
| flyer-crawler-test-infrastructure | bug:infrastructure | env:test |
All synced issues also receive the `source:bugsink` label.
## Implementation Details
### New Files
| File | Purpose |
| -------------------------------------- | ------------------------------------------- |
| `src/services/bugsinkSync.server.ts` | Core synchronization logic |
| `src/services/bugsinkClient.server.ts` | HTTP client for Bugsink API |
| `src/services/giteaClient.server.ts` | HTTP client for Gitea API |
| `src/types/bugsink.ts` | TypeScript interfaces for Bugsink responses |
| `src/routes/admin/bugsink-sync.ts` | Admin endpoints for manual trigger |
### Modified Files
| File | Changes |
| ------------------------------------- | ------------------------------------- |
| `src/services/queues.server.ts` | Add `bugsinkSyncQueue` definition |
| `src/services/workers.server.ts` | Add sync worker implementation |
| `src/config/env.ts` | Add bugsink sync configuration schema |
| `.env.example` | Document new environment variables |
| `.gitea/workflows/deploy-to-test.yml` | Pass sync-related secrets |
### Environment Variables
```bash
# Bugsink Configuration
BUGSINK_URL=https://bugsink.projectium.com
BUGSINK_API_TOKEN=77deaa5e... # From Bugsink Settings > API Keys
# Gitea Configuration
GITEA_URL=https://gitea.projectium.com
GITEA_API_TOKEN=... # Personal access token with repo scope
GITEA_OWNER=torbo
GITEA_REPO=flyer-crawler.projectium.com
# Sync Control
BUGSINK_SYNC_ENABLED=false # Set true only in test environment
BUGSINK_SYNC_INTERVAL=15 # Minutes between sync runs
```
### Gitea Issue Template
```markdown
## Error Details
| Field | Value |
| ------------ | --------------- |
| **Type** | {error_type} |
| **Message** | {error_message} |
| **Platform** | {platform} |
| **Level** | {level} |
## Occurrence Statistics
- **First Seen**: {first_seen}
- **Last Seen**: {last_seen}
- **Total Occurrences**: {count}
## Request Context
- **URL**: {request_url}
- **Additional Context**: {context}
## Stacktrace
<details>
<summary>Click to expand</summary>
{stacktrace}
</details>
---
**Bugsink Issue**: {bugsink_url}
**Project**: {project_slug}
**Trace ID**: {trace_id}
```
### Sync Workflow
```
1. Worker triggered (every 15 min or manual)
2. For each of 6 Bugsink projects:
a. List issues with status='unresolved'
b. For each issue:
i. Check Redis for existing sync record
ii. If already synced → skip
iii. Fetch issue details + stacktrace
iv. Create Gitea issue with labels
v. Store sync record in Redis
vi. Mark issue as 'resolved' in Bugsink
3. Log summary (synced: N, skipped: N, failed: N)
```
### Idempotency Guarantees
1. **Redis check before creation**: Prevents duplicate Gitea issues
2. **Atomic Redis write after Gitea create**: Ensures state consistency
3. **Query only unresolved issues**: Resolved issues won't appear in polls
4. **No TTL on Redis keys**: Permanent sync history
## Consequences
### Positive
1. **Visibility**: All application errors become trackable tickets
2. **Accountability**: Errors can be assigned to developers
3. **History**: Complete audit trail of when errors were discovered and resolved
4. **Integration**: Errors appear alongside feature work in Gitea
5. **Automation**: No manual error triage required
### Negative
1. **API Dependencies**: Requires both Bugsink and Gitea APIs to be available
2. **Token Management**: Additional secrets to manage in CI/CD
3. **Potential Noise**: High-frequency errors could create many tickets (mitigated by Bugsink's issue grouping)
4. **Single Point**: Sync only runs on test server (if test server is down, no sync occurs)
### Risks & Mitigations
| Risk | Mitigation |
| ----------------------- | ------------------------------------------------- |
| Bugsink API rate limits | 15-minute polling interval |
| Gitea API rate limits | Sequential processing with delays |
| Redis connection issues | Reuse existing connection patterns |
| Duplicate issues | Redis tracking + idempotent checks |
| Missing stacktrace | Graceful degradation (create issue without trace) |
## Admin Interface
### Manual Sync Endpoint
```
POST /api/admin/bugsink/sync
Authorization: Bearer {admin_jwt}
Response:
{
"success": true,
"data": {
"synced": 3,
"skipped": 12,
"failed": 0,
"duration_ms": 2340
}
}
```
### Sync Status Endpoint
```
GET /api/admin/bugsink/sync/status
Authorization: Bearer {admin_jwt}
Response:
{
"success": true,
"data": {
"enabled": true,
"last_run": "2026-01-17T10:30:00Z",
"next_run": "2026-01-17T10:45:00Z",
"total_synced": 47,
"projects": [
{ "slug": "flyer-crawler-backend", "synced_count": 12 },
...
]
}
}
```
## Implementation Phases
### Phase 1: Core Infrastructure
- Add environment variables to `env.ts` schema
- Create `BugsinkClient` service (HTTP client)
- Create `GiteaClient` service (HTTP client)
- Add Redis db 15 connection for sync tracking
### Phase 2: Sync Logic
- Create `BugsinkSyncService` with sync logic
- Add `bugsink-sync` queue to `queues.server.ts`
- Add sync worker to `workers.server.ts`
- Create TypeScript types for API responses
### Phase 3: Integration
- Add admin endpoints for manual sync trigger
- Update `deploy-to-test.yml` with new secrets
- Add secrets to Gitea repository settings
- Test end-to-end in staging environment
### Phase 4: Documentation
- Update CLAUDE.md with sync information
- Create operational runbook for sync issues
## Future Enhancements
1. **Bi-directional sync**: Update Bugsink when Gitea issue is closed
2. **Smart deduplication**: Detect similar errors across projects
3. **Priority mapping**: High occurrence count → high priority label
4. **Slack/Discord notifications**: Alert on new critical errors
5. **Metrics dashboard**: Track error trends over time
## References
- [ADR-006: Background Job Processing](./0006-background-job-processing-and-task-queues.md)
- [ADR-015: Application Performance Monitoring](./0015-application-performance-monitoring-and-error-tracking.md)
- [Bugsink API Documentation](https://bugsink.com/docs/api/)
- [Gitea API Documentation](https://docs.gitea.io/en-us/api-usage/)

View File

@@ -0,0 +1,782 @@
# Frontend Testing Summary - 2026-01-18
## Session 1: Initial Frontend Testing
**Environment:** Dev container (`flyer-crawler-dev`)
**Date:** 2026-01-18
### Tests Completed
| Area | Status | Notes |
| ---------------- | ------ | --------------------------------------------------- |
| Authentication | Pass | Register, login, profile retrieval all work |
| Flyer Upload | Pass | Upload with checksum, job processing, mock AI works |
| Pantry/Inventory | Pass | Add items, list items with master_item linking |
| Shopping Lists | Pass | Create lists, add items, retrieve items |
| Navigation | Pass | All SPA routes return 200 |
| Error Handling | Pass | Proper error responses for auth, validation, 404s |
### Code Changes Made
1. `src/services/aiService.server.ts` - Added `development` to mock AI environments
2. `src/utils/rateLimit.ts` - Added `development` and `staging` to rate limit skip list
### Bugsink Status
- Frontend (dev): No new issues
- Backend (dev): No new issues during testing
- Test environment: 1 existing `t.map is not a function` issue (already fixed, needs deployment)
---
## Session 2: Extended API Testing
**Date:** 2026-01-18
**Tester:** Claude Code
### Budget API Testing - PASSED
| Test | Status | Notes |
| ---------------------------------- | ------ | ----------------------------------------- |
| GET /api/budgets (empty) | Pass | Returns empty array for new user |
| POST /api/budgets (create) | Pass | Creates budget with all fields |
| GET /api/budgets (list) | Pass | Returns all user budgets |
| PUT /api/budgets/:id (update) | Pass | Updates amount correctly |
| DELETE /api/budgets/:id | Pass | Returns 204, budget removed |
| GET /api/budgets/spending-analysis | Pass | Returns spending by category |
| Validation: invalid period | Pass | Rejects "yearly", requires weekly/monthly |
| Validation: negative amount | Pass | Rejects negative values |
| Validation: invalid date | Pass | Requires YYYY-MM-DD format |
| Validation: missing name | Pass | Proper error message |
| Error: update non-existent | Pass | Returns 404 |
| Error: delete non-existent | Pass | Returns 404 |
| Error: no auth | Pass | Returns "Unauthorized" |
**Example API Calls:**
```bash
# Create budget
curl -X POST http://localhost:3001/api/budgets \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Weekly Groceries", "amount_cents": 15000, "period": "weekly", "start_date": "2025-01-01"}'
# Response:
{"success":true,"data":{"budget_id":1,"user_id":"...","name":"Weekly Groceries","amount_cents":15000,"period":"weekly","start_date":"2025-01-01T00:00:00.000Z","created_at":"...","updated_at":"..."}}
```
### Deals API Testing - NOT MOUNTED
**Finding:** The `/api/deals` routes are defined in `src/routes/deals.routes.ts` but are NOT mounted in `server.ts`.
Routes that exist but are NOT mounted:
- `deals.routes.ts` - `/api/deals/best-watched-prices`
- `reactions.routes.ts` - Social reactions feature
### Routes Currently Mounted (from server.ts)
| Route | Path | Status |
| --------------------- | -------------------- | ------- |
| authRouter | /api/auth | Mounted |
| healthRouter | /api/health | Mounted |
| systemRouter | /api/system | Mounted |
| userRouter | /api/users | Mounted |
| aiRouter | /api/ai | Mounted |
| adminRouter | /api/admin | Mounted |
| budgetRouter | /api/budgets | Mounted |
| gamificationRouter | /api/achievements | Mounted |
| flyerRouter | /api/flyers | Mounted |
| recipeRouter | /api/recipes | Mounted |
| personalizationRouter | /api/personalization | Mounted |
| priceRouter | /api/price-history | Mounted |
| statsRouter | /api/stats | Mounted |
| upcRouter | /api/upc | Mounted |
| inventoryRouter | /api/inventory | Mounted |
| receiptRouter | /api/receipts | Mounted |
### Gamification API Testing - PASSED
| Test | Status | Notes |
| ----------------------------------------- | ------ | ----------------------------------------- |
| GET /api/achievements (public) | Pass | Returns 8 achievements with icons, points |
| GET /api/achievements/leaderboard | Pass | Returns ranked users by points |
| GET /api/achievements/leaderboard?limit=5 | Pass | Respects limit parameter |
| GET /api/achievements/me (auth) | Pass | Returns user's earned achievements |
| GET /api/achievements/me (no auth) | Pass | Returns "Unauthorized" |
| Validation: limit > 50 | Pass | Returns validation error |
| Validation: limit < 0 | Pass | Returns validation error |
| Validation: non-numeric limit | Pass | Returns validation error |
**Note:** New users automatically receive "Welcome Aboard" achievement (5 points) on registration.
### Recipe API Testing - PASSED (with notes)
| Test | Status | Notes |
| -------------------------------------------------------------------- | --------- | --------------------------------------------------------------- |
| GET /api/recipes/by-sale-percentage | Pass | Returns empty (no sale data in dev) |
| GET /api/recipes/by-sale-percentage?minPercentage=25 | Pass | Respects parameter |
| GET /api/recipes/by-sale-ingredients | Pass | Returns empty (no sale data) |
| GET /api/recipes/by-ingredient-and-tag (missing params) | Pass | Validation error for both params |
| GET /api/recipes/by-ingredient-and-tag?ingredient=chicken&tag=dinner | Pass | Works, returns empty |
| GET /api/recipes/1 | Pass | Returns full recipe with ingredients, tags |
| GET /api/recipes/99999 | Pass | Returns 404 "Recipe not found" |
| GET /api/recipes/1/comments | Pass | Returns empty initially |
| POST /api/recipes/1/comments | Pass | Adds comment successfully |
| POST /api/recipes/suggest | Pass | Returns AI mock suggestion |
| POST /api/recipes/1/fork | **Issue** | "A required field was left null" - seed recipe has null user_id |
**Known Issue:** Recipe forking fails for seed recipes that have `user_id: null`. This may be expected behavior - only user-owned recipes can be forked.
### Receipt Processing API Testing - PASSED
| Test | Status | Notes |
| --------------------------------- | ------ | -------------------------------------------------------- |
| GET /api/receipts (empty) | Pass | Returns `{"receipts":[],"total":0}` |
| GET /api/receipts (no auth) | Pass | Returns "Unauthorized" |
| GET /api/receipts with filters | Pass | Accepts status, limit, store_id, dates |
| POST /api/receipts (upload) | Pass | Creates receipt, queues for processing |
| POST /api/receipts (no file) | Pass | Validation: "A file for the 'receipt' field is required" |
| POST /api/receipts (invalid date) | Pass | Validation: YYYY-MM-DD format required |
**Note:** Receipt processing uses mock AI in development, correctly reports status as "processing".
### UPC Lookup API Testing - PASSED
| Test | Status | Notes |
| --------------------------------- | ------ | -------------------------------------------------- |
| GET /api/upc/history (empty) | Pass | Returns `{"scans":[],"total":0}` |
| POST /api/upc/scan (manual) | Pass | Records scan, looks up OpenFoodFacts |
| GET /api/upc/lookup | Pass | Returns cached product data |
| GET /api/upc/history (after scan) | Pass | Shows scan history |
| Validation: short UPC | Pass | "UPC code must be 8-14 digits" |
| Validation: invalid source | Pass | Enum validation for scan_source |
| Validation: missing data | Pass | "Either upc_code or image_base64 must be provided" |
**Note:** External lookup via OpenFoodFacts API is working and returning product data.
### Price History API Testing - PASSED
| Test | Status | Notes |
| ------------------------------------- | ------ | -------------------------------------- |
| POST /api/price-history (valid) | Pass | Returns empty (no price data in dev) |
| POST /api/price-history (empty array) | Pass | Validation: "non-empty array" required |
| POST /api/price-history (no auth) | Pass | Returns "Unauthorized" |
### Personalization API Testing - PASSED
| Test | Status | Notes |
| --------------------------------------------- | ------ | ------------------------------------------ |
| GET /api/personalization/master-items | Pass | Returns 100+ grocery items with categories |
| GET /api/personalization/dietary-restrictions | Pass | Returns 12 items (diets + allergies) |
| GET /api/personalization/appliances | Pass | Returns 12 kitchen appliances |
**Note:** All personalization endpoints are public (no auth required).
### Admin Routes - PASSED
**Admin credentials:** `admin@example.com` / `adminpass` (from seed script)
| Test | Status | Notes |
| ---------------------------- | ------ | --------------------------------------------- |
| GET /api/admin/stats | Pass | Returns flyer count, user count, recipe count |
| GET /api/admin/users | Pass | Returns all users with profiles |
| GET /api/admin/corrections | Pass | Returns empty list (no corrections in dev) |
| GET /api/admin/review/flyers | Pass | Returns empty list (no pending reviews) |
| GET /api/admin/brands | Pass | Returns 2 brands from seed data |
| GET /api/admin/stats/daily | Pass | Returns 30-day daily statistics |
| Role check: regular user | Pass | Returns 403 Forbidden for non-admin |
**Note:** Admin user is created by `src/db/seed_admin_account.ts` which runs during dev container setup.
---
## Session 3: Route Fixes and Admin Testing
**Date:** 2026-01-18
### Fixes Applied
1. **Mounted deals.routes.ts** - Added import and `app.use('/api/deals', dealsRouter)` to server.ts
2. **Mounted reactions.routes.ts** - Added import and `app.use('/api/reactions', reactionsRouter)` to server.ts
### Deals API Testing - PASSED
| Test | Status | Notes |
| ---------------------------------- | ------ | --------------------------------------- |
| GET /api/deals/best-watched-prices | Pass | Returns empty (no watched items in dev) |
| No auth check | Pass | Returns "Unauthorized" |
### Reactions API Testing - PASSED
| Test | Status | Notes |
| ------------------------------------------------ | ------ | -------------------------------- |
| GET /api/reactions/summary/:targetType/:targetId | Pass | Returns reaction counts |
| POST /api/reactions/toggle | Pass | Toggles reaction (requires auth) |
| No auth check | Pass | Returns "Unauthorized" |
---
## Testing Summary
| API Area | Status | Endpoints Tested |
| --------------- | -------- | ------------------------- |
| Budget | **PASS** | 6 endpoints |
| Deals | **PASS** | 1 endpoint (now mounted) |
| Reactions | **PASS** | 2 endpoints (now mounted) |
| Gamification | **PASS** | 4 endpoints |
| Recipe | **PASS** | 7 endpoints |
| Receipt | **PASS** | 2 endpoints |
| UPC | **PASS** | 3 endpoints |
| Price History | **PASS** | 1 endpoint |
| Personalization | **PASS** | 3 endpoints |
| Admin | **PASS** | 6 endpoints |
**Total: 35+ endpoints tested, all passing**
### Issues Found (and Fixed)
1. ~~**Unmounted Routes:** `deals.routes.ts` and `reactions.routes.ts` are defined but not mounted in server.ts~~ **FIXED** - Routes now mounted in server.ts
2. **Recipe Fork Issue:** Seed recipes with `user_id: null` cannot be forked (database constraint) - Expected behavior
3. **UPC Validation:** Short UPC code validation happens at service layer, not Zod (minor)
---
## Bugsink Error Tracking
**Projects configured:**
- flyer-crawler-backend (ID: 1)
- flyer-crawler-backend-test (ID: 3)
- flyer-crawler-frontend (ID: 2)
- flyer-crawler-frontend-test (ID: 4)
- flyer-crawler-infrastructure (ID: 5)
- flyer-crawler-test-infrastructure (ID: 6)
**Current Issues:**
- Backend (ID: 1): 1 test message from setup (not a real error)
- All other projects: No issues
---
## Session 4: Extended Integration Testing
**Date:** 2026-01-18
**Tester:** Claude Code
**Objective:** Deep testing of edge cases, user flows, queue behavior, and system resilience
### Test Areas Planned
| # | Area | Status | Description |
| --- | --------------------------- | -------- | ------------------------------------------------ |
| 1 | End-to-End User Flows | **PASS** | Complete user journeys across multiple endpoints |
| 2 | Edge Cases & Error Recovery | PENDING | File limits, corrupt files, timeouts |
| 3 | Queue/Worker Behavior | PENDING | Job processing, retries, cleanup |
| 4 | Authentication Edge Cases | PENDING | Token expiry, sessions, OAuth |
| 5 | Performance Under Load | PENDING | Concurrent requests, pagination |
| 6 | WebSocket/Real-time | PENDING | Live updates, notifications |
| 7 | Data Integrity | PENDING | Cascade deletes, FK constraints |
---
### Area 1: End-to-End User Flows
**Status:** PASSED ✓
| Test | Status | Notes |
| ----------------------------------------------------- | -------- | --------------------------------------------------- |
| Register → Upload flyer → View items → Add to list | **Pass** | Full flow works; job completes in ~1s with mock AI |
| Recipe: Browse → Comment → React → Fork | **Pass** | Comments work; reactions need `entity_id` as STRING |
| Inventory: Scan UPC → Add to inventory → Track expiry | **Pass** | Requires `master_item_id` (NOT NULL in DB) |
#### E2E Flow 1: Flyer to Shopping List
```bash
# 1. Register user
POST /api/auth/register
# 2. Upload flyer
POST /api/ai/upload-and-process (flyerFile + checksum)
# 3. Poll job status
GET /api/ai/jobs/{jobId}/status → returnValue.flyerId
# 4. Get flyer items
GET /api/flyers/{flyerId}/items
# 5. Create shopping list
POST /api/users/shopping-lists
# 6. Add item (use shopping_list_id, not list_id)
POST /api/users/shopping-lists/{shopping_list_id}/items
```
#### E2E Flow 2: Recipe Interaction
```bash
# 1. Get recipe
GET /api/recipes/{id}
# 2. Add comment
POST /api/recipes/{id}/comments {"content": "..."}
# 3. Toggle reaction (entity_id must be STRING!)
POST /api/reactions/toggle {"entity_type":"recipe","entity_id":"1","reaction_type":"like"}
# 4. Fork (only works on user-owned recipes, not seed data)
POST /api/recipes/{id}/fork
```
#### E2E Flow 3: Inventory Management
```bash
# 1. Scan UPC
POST /api/upc/scan {"upc_code":"...", "scan_source":"manual_entry"}
# 2. Get master items (to find valid master_item_id)
GET /api/personalization/master-items
# 3. Add to inventory (master_item_id REQUIRED - NOT NULL)
POST /api/inventory {
"item_name": "...",
"master_item_id": 105, # REQUIRED
"quantity": 2,
"source": "upc_scan", # REQUIRED: manual|receipt_scan|upc_scan
"location": "pantry", # fridge|freezer|pantry|room_temp
"expiry_date": "2026-03-15",
"unit": "box"
}
# 4. Get inventory
GET /api/inventory
# 5. Get expiry summary
GET /api/inventory/expiring/summary
```
#### API Gotchas Discovered in E2E Testing
| Issue | Correct Usage |
| ------------------------ | ----------------------------------------------------------------- |
| Shopping list ID field | Use `shopping_list_id`, not `list_id` |
| Reaction entity_id | Must be STRING, not number: `"entity_id":"1"` |
| Inventory master_item_id | REQUIRED (NOT NULL in pantry_items table) |
| Inventory source | REQUIRED: `manual`, `receipt_scan`, or `upc_scan` |
| Recipe forking | Only works on user-owned recipes (seed recipes have null user_id) |
| Item name in inventory | Resolved from master_grocery_items, not stored directly |
---
### Area 2: Edge Cases & Error Recovery
**Status:** PENDING
| Test | Status | Notes |
| --------------------------------- | ------ | ----- |
| File upload at size limits | | |
| Corrupt/invalid image files | | |
| Concurrent uploads from same user | | |
| Network timeout simulation | | |
---
### Area 3: Queue/Worker Behavior
**Status:** PENDING
| Test | Status | Notes |
| --------------------------- | ------ | ----- |
| Job retry on AI failure | | |
| Cleanup queue file deletion | | |
| Analytics queue execution | | |
| Token cleanup queue | | |
---
### Area 4: Authentication Edge Cases
**Status:** PENDING
| Test | Status | Notes |
| ------------------------------ | ------ | ----- |
| Token expiration behavior | | |
| Multiple simultaneous sessions | | |
| Invalid/malformed tokens | | |
| Refresh token flow | | |
---
### Area 5: Performance Under Load
**Status:** PENDING
| Test | Status | Notes |
| ------------------------------ | ------ | ----- |
| Concurrent API requests | | |
| Pagination with large datasets | | |
| Cache hit/miss behavior | | |
---
### Area 6: WebSocket/Real-time Features
**Status:** PENDING
| Test | Status | Notes |
| ----------------------- | ------ | ----- |
| Real-time notifications | | |
| Job status updates | | |
---
### Area 7: Data Integrity
**Status:** PENDING
| Test | Status | Notes |
| ----------------------- | ------ | ----- |
| User deletion cascade | | |
| Foreign key constraints | | |
| Transaction rollback | | |
---
## API Reference: Correct Endpoint Calls
This section documents the **correct** API calls, field names, and common gotchas discovered during testing.
### Container Execution Pattern
All curl commands should be run inside the dev container:
```bash
podman exec flyer-crawler-dev bash -c "
# Your curl command here
"
```
**Gotcha:** When using special characters (like `!` or `$`), use single quotes for the outer bash command and escape JSON properly.
---
### Authentication
#### Register User
```bash
# Password must be strong (zxcvbn validation)
curl -s -X POST http://localhost:3001/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"SecurePassword2026xyz","name":"Test User"}'
# Response includes token:
# {"success":true,"data":{"message":"User registered successfully!","userprofile":{...},"token":"eyJ..."}}
```
**Gotchas:**
- Password validation uses zxcvbn - simple passwords like `testpass123` are rejected
- New users automatically get "Welcome Aboard" achievement (5 points)
#### Login
```bash
curl -s -X POST http://localhost:3001/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"SecurePassword2026xyz"}'
```
#### Admin Login
```bash
# Admin user from seed: admin@example.com / adminpass
curl -s -X POST http://localhost:3001/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"adminpass"}'
```
---
### Flyer Upload & Processing
**IMPORTANT:** Flyer upload is via `/api/ai/upload-and-process`, NOT `/api/flyers`
#### Upload Flyer
```bash
# Calculate checksum first
CHECKSUM=$(sha256sum /path/to/flyer.png | cut -d" " -f1)
curl -s -X POST http://localhost:3001/api/ai/upload-and-process \
-H "Authorization: Bearer $TOKEN" \
-F "flyerFile=@/path/to/flyer.png" \
-F "checksum=$CHECKSUM"
# Response:
# {"success":true,"data":{"message":"Flyer accepted for processing.","jobId":"1"}}
```
**Gotchas:**
- Field name is `flyerFile`, not `flyer` or `file`
- Checksum is required (SHA-256)
- Returns jobId for status polling
#### Check Job Status
```bash
curl -s http://localhost:3001/api/ai/jobs/{jobId}/status \
-H "Authorization: Bearer $TOKEN"
# Response when complete:
# {"success":true,"data":{"id":"1","state":"completed","progress":{...},"returnValue":{"flyerId":2}}}
```
**Gotchas:**
- Endpoint is `/api/ai/jobs/{jobId}/status`, NOT `/api/ai/job-status/{jobId}`
- `returnValue.flyerId` contains the created flyer ID
#### Get Flyer Details & Items
```bash
# Get flyer metadata
curl -s http://localhost:3001/api/flyers/{flyerId}
# Get extracted items
curl -s http://localhost:3001/api/flyers/{flyerId}/items
```
---
### Shopping Lists
**IMPORTANT:** Shopping list endpoints are under `/api/users/shopping-lists`, NOT `/api/users/me/shopping-lists`
#### Create Shopping List
```bash
curl -s -X POST http://localhost:3001/api/users/shopping-lists \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"My Shopping List"}'
```
#### Add Item to List
```bash
# Use customItemName (camelCase), NOT custom_name
curl -s -X POST http://localhost:3001/api/users/shopping-lists/{listId}/items \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"customItemName":"Product Name","quantity":2}'
# OR with master item:
# -d '{"masterItemId":123,"quantity":1}'
```
**Gotchas:**
- Field is `customItemName` not `custom_name`
- Must provide either `masterItemId` OR `customItemName`, not both
- `quantity` is optional, defaults to 1
#### Get Shopping List with Items
```bash
curl -s http://localhost:3001/api/users/shopping-lists/{listId} \
-H "Authorization: Bearer $TOKEN"
```
---
### Recipes
#### Get Recipe by ID
```bash
curl -s http://localhost:3001/api/recipes/{recipeId}
# Public endpoint - no auth required
```
#### Add Comment
```bash
curl -s -X POST http://localhost:3001/api/recipes/{recipeId}/comments \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"content":"Great recipe!"}'
```
#### Fork Recipe
```bash
curl -s -X POST http://localhost:3001/api/recipes/{recipeId}/fork \
-H "Authorization: Bearer $TOKEN"
# No request body needed
```
**Gotchas:**
- Forking fails for seed recipes (user_id: null) - this is expected
- Only user-owned recipes can be forked
#### AI Recipe Suggestion
```bash
curl -s -X POST http://localhost:3001/api/recipes/suggest \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"ingredients":["chicken","rice","broccoli"]}'
```
---
### UPC Scanning
#### Scan UPC Code
```bash
curl -s -X POST http://localhost:3001/api/upc/scan \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"upc_code":"076808533842","scan_source":"manual_entry"}'
```
**Gotchas:**
- `scan_source` must be one of: `image_upload`, `manual_entry`, `phone_app`, `camera_scan`
- NOT `manual` - use `manual_entry`
- UPC must be 8-14 digits
#### Get Scan History
```bash
curl -s http://localhost:3001/api/upc/history \
-H "Authorization: Bearer $TOKEN"
```
---
### Inventory/Pantry
#### Add Item to Pantry
```bash
curl -s -X POST http://localhost:3001/api/inventory/pantry \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"master_item_id":1,"quantity":2,"expiry_date":"2026-02-15"}'
```
#### Get Pantry Items
```bash
curl -s http://localhost:3001/api/inventory/pantry \
-H "Authorization: Bearer $TOKEN"
```
---
### Budgets
#### Create Budget
```bash
curl -s -X POST http://localhost:3001/api/budgets \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Weekly Groceries","amount_cents":15000,"period":"weekly","start_date":"2025-01-01"}'
```
**Gotchas:**
- `period` must be `weekly` or `monthly` (not `yearly`)
- `amount_cents` must be positive
- `start_date` format: `YYYY-MM-DD`
---
### Receipts
#### Upload Receipt
```bash
curl -s -X POST http://localhost:3001/api/receipts \
-H "Authorization: Bearer $TOKEN" \
-F "receipt=@/path/to/receipt.jpg" \
-F "purchase_date=2026-01-18"
```
**Gotchas:**
- Field name is `receipt`
- `purchase_date` format: `YYYY-MM-DD`
---
### Reactions
#### Toggle Reaction
```bash
curl -s -X POST http://localhost:3001/api/reactions/toggle \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"target_type":"recipe","target_id":1,"reaction_type":"like"}'
```
#### Get Reaction Summary
```bash
curl -s http://localhost:3001/api/reactions/summary/{targetType}/{targetId}
# Public endpoint
```
---
### Admin Routes
All admin routes require admin role (403 for regular users).
```bash
# Stats
curl -s http://localhost:3001/api/admin/stats -H "Authorization: Bearer $ADMIN_TOKEN"
# Users list
curl -s http://localhost:3001/api/admin/users -H "Authorization: Bearer $ADMIN_TOKEN"
# Corrections
curl -s http://localhost:3001/api/admin/corrections -H "Authorization: Bearer $ADMIN_TOKEN"
# Brands
curl -s http://localhost:3001/api/admin/brands -H "Authorization: Bearer $ADMIN_TOKEN"
# Daily stats
curl -s http://localhost:3001/api/admin/stats/daily -H "Authorization: Bearer $ADMIN_TOKEN"
```
---
### Common Validation Errors
| Error | Cause | Fix |
| --------------------------------------- | ------------------------------- | --------------------------------------------------------------- |
| `Password is too weak` | zxcvbn rejects simple passwords | Use complex password with mixed case, numbers |
| `Either masterItemId or customItemName` | Shopping list item missing both | Provide one of them |
| `Invalid option` for scan_source | Wrong enum value | Use: `manual_entry`, `image_upload`, `phone_app`, `camera_scan` |
| `A flyer file is required` | Missing flyerFile in upload | Check field name is `flyerFile` |
| `A required field was left null` | Forking seed recipe | Seed recipes have null user_id, cannot fork |
| `non-empty array required` | Empty masterItemIds | Provide at least one ID |
---
### Response Format
All API responses follow this format:
```json
// Success
{"success":true,"data":{...}}
// Error
{"success":false,"error":{"code":"ERROR_CODE","message":"Description","details":[...]}}
```
Common error codes:
- `VALIDATION_ERROR` - Request validation failed (check `details` array)
- `BAD_REQUEST` - Invalid request format
- `UNAUTHORIZED` - Missing or invalid token
- `FORBIDDEN` - User lacks permission (e.g., non-admin accessing admin route)
- `NOT_FOUND` - Resource not found

458
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "flyer-crawler",
"version": "0.11.1",
"version": "0.11.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "flyer-crawler",
"version": "0.11.1",
"version": "0.11.4",
"dependencies": {
"@bull-board/api": "^6.14.2",
"@bull-board/express": "^6.14.2",
@@ -55,6 +55,7 @@
"zxing-wasm": "^2.2.4"
},
"devDependencies": {
"@sentry/vite-plugin": "^4.6.2",
"@tailwindcss/postcss": "4.1.17",
"@tanstack/react-query-devtools": "^5.91.2",
"@testcontainers/postgresql": "^11.8.1",
@@ -4634,6 +4635,16 @@
"node": ">=18"
}
},
"node_modules/@sentry/babel-plugin-component-annotate": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.6.2.tgz",
"integrity": "sha512-6VTjLJXtIHKwxMmThtZKwi1+hdklLNzlbYH98NhbH22/Vzb/c6BlSD2b5A0NGN9vFB807rD4x4tuP+Su7BxQXQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/@sentry/browser": {
"version": "10.32.1",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.32.1.tgz",
@@ -4650,6 +4661,258 @@
"node": ">=18"
}
},
"node_modules/@sentry/bundler-plugin-core": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.6.2.tgz",
"integrity": "sha512-JkOc3JkVzi/fbXsFp8R9uxNKmBrPRaU4Yu4y1i3ihWfugqymsIYaN0ixLENZbGk2j4xGHIk20PAJzBJqBMTHew==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.18.5",
"@sentry/babel-plugin-component-annotate": "4.6.2",
"@sentry/cli": "^2.57.0",
"dotenv": "^16.3.1",
"find-up": "^5.0.0",
"glob": "^10.5.0",
"magic-string": "0.30.8",
"unplugin": "1.0.1"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@sentry/bundler-plugin-core/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@sentry/bundler-plugin-core/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC"
},
"node_modules/@sentry/bundler-plugin-core/node_modules/magic-string": {
"version": "0.30.8",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
"integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@sentry/bundler-plugin-core/node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@sentry/cli": {
"version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.4.tgz",
"integrity": "sha512-ArDrpuS8JtDYEvwGleVE+FgR+qHaOp77IgdGSacz6SZy6Lv90uX0Nu4UrHCQJz8/xwIcNxSqnN22lq0dH4IqTg==",
"dev": true,
"hasInstallScript": true,
"license": "FSL-1.1-MIT",
"dependencies": {
"https-proxy-agent": "^5.0.0",
"node-fetch": "^2.6.7",
"progress": "^2.0.3",
"proxy-from-env": "^1.1.0",
"which": "^2.0.2"
},
"bin": {
"sentry-cli": "bin/sentry-cli"
},
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@sentry/cli-darwin": "2.58.4",
"@sentry/cli-linux-arm": "2.58.4",
"@sentry/cli-linux-arm64": "2.58.4",
"@sentry/cli-linux-i686": "2.58.4",
"@sentry/cli-linux-x64": "2.58.4",
"@sentry/cli-win32-arm64": "2.58.4",
"@sentry/cli-win32-i686": "2.58.4",
"@sentry/cli-win32-x64": "2.58.4"
}
},
"node_modules/@sentry/cli-darwin": {
"version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.4.tgz",
"integrity": "sha512-kbTD+P4X8O+nsNwPxCywtj3q22ecyRHWff98rdcmtRrvwz8CKi/T4Jxn/fnn2i4VEchy08OWBuZAqaA5Kh2hRQ==",
"dev": true,
"license": "FSL-1.1-MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-arm": {
"version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.4.tgz",
"integrity": "sha512-rdQ8beTwnN48hv7iV7e7ZKucPec5NJkRdrrycMJMZlzGBPi56LqnclgsHySJ6Kfq506A2MNuQnKGaf/sBC9REA==",
"cpu": [
"arm"
],
"dev": true,
"license": "FSL-1.1-MIT",
"optional": true,
"os": [
"linux",
"freebsd",
"android"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-arm64": {
"version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.4.tgz",
"integrity": "sha512-0g0KwsOozkLtzN8/0+oMZoOuQ0o7W6O+hx+ydVU1bktaMGKEJLMAWxOQNjsh1TcBbNIXVOKM/I8l0ROhaAb8Ig==",
"cpu": [
"arm64"
],
"dev": true,
"license": "FSL-1.1-MIT",
"optional": true,
"os": [
"linux",
"freebsd",
"android"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-i686": {
"version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.4.tgz",
"integrity": "sha512-NseoIQAFtkziHyjZNPTu1Gm1opeQHt7Wm1LbLrGWVIRvUOzlslO9/8i6wETUZ6TjlQxBVRgd3Q0lRBG2A8rFYA==",
"cpu": [
"x86",
"ia32"
],
"dev": true,
"license": "FSL-1.1-MIT",
"optional": true,
"os": [
"linux",
"freebsd",
"android"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-x64": {
"version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.4.tgz",
"integrity": "sha512-d3Arz+OO/wJYTqCYlSN3Ktm+W8rynQ/IMtSZLK8nu0ryh5mJOh+9XlXY6oDXw4YlsM8qCRrNquR8iEI1Y/IH+Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "FSL-1.1-MIT",
"optional": true,
"os": [
"linux",
"freebsd",
"android"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-win32-arm64": {
"version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.4.tgz",
"integrity": "sha512-bqYrF43+jXdDBh0f8HIJU3tbvlOFtGyRjHB8AoRuMQv9TEDUfENZyCelhdjA+KwDKYl48R1Yasb4EHNzsoO83w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "FSL-1.1-MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-win32-i686": {
"version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.4.tgz",
"integrity": "sha512-3triFD6jyvhVcXOmGyttf+deKZcC1tURdhnmDUIBkiDPJKGT/N5xa4qAtHJlAB/h8L9jgYih9bvJnvvFVM7yug==",
"cpu": [
"x86",
"ia32"
],
"dev": true,
"license": "FSL-1.1-MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-win32-x64": {
"version": "2.58.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.4.tgz",
"integrity": "sha512-cSzN4PjM1RsCZ4pxMjI0VI7yNCkxiJ5jmWncyiwHXGiXrV1eXYdQ3n1LhUYLZ91CafyprR0OhDcE+RVZ26Qb5w==",
"cpu": [
"x64"
],
"dev": true,
"license": "FSL-1.1-MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/core": {
"version": "10.32.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.32.1.tgz",
@@ -4765,6 +5028,20 @@
"react": "^16.14.0 || 17.x || 18.x || 19.x"
}
},
"node_modules/@sentry/vite-plugin": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-4.6.2.tgz",
"integrity": "sha512-hK9N50LlTaPlb2P1r87CFupU7MJjvtrp+Js96a2KDdiP8ViWnw4Gsa/OvA0pkj2wAFXFeBQMLS6g/SktTKG54w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sentry/bundler-plugin-core": "4.6.2",
"unplugin": "1.0.1"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@smithy/abort-controller": {
"version": "4.2.7",
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.7.tgz",
@@ -7036,6 +7313,33 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/anymatch/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
@@ -7691,6 +7995,19 @@
"node": "*"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@@ -8153,6 +8470,44 @@
"node": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chokidar/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
@@ -9216,6 +9571,19 @@
"license": "MIT",
"peer": true
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -11615,6 +11983,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-boolean-object": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
@@ -15197,6 +15578,16 @@
],
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -15303,6 +15694,13 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true,
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
@@ -15567,6 +15965,32 @@
"node": ">=10"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/readdirp/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
@@ -17782,6 +18206,19 @@
"node": ">= 0.8"
}
},
"node_modules/unplugin": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz",
"integrity": "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.8.1",
"chokidar": "^3.5.3",
"webpack-sources": "^3.2.3",
"webpack-virtual-modules": "^0.5.0"
}
},
"node_modules/until-async": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz",
@@ -18110,6 +18547,23 @@
"node": ">=20"
}
},
"node_modules/webpack-sources": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/webpack-virtual-modules": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
"integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
"dev": true,
"license": "MIT"
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "flyer-crawler",
"private": true,
"version": "0.11.1",
"version": "0.11.4",
"type": "module",
"scripts": {
"dev": "concurrently \"npm:start:dev\" \"vite\"",
@@ -75,6 +75,7 @@
"zxing-wasm": "^2.2.4"
},
"devDependencies": {
"@sentry/vite-plugin": "^4.6.2",
"@tailwindcss/postcss": "4.1.17",
"@tanstack/react-query-devtools": "^5.91.2",
"@testcontainers/postgresql": "^11.8.1",

View File

@@ -35,6 +35,8 @@ import healthRouter from './src/routes/health.routes';
import upcRouter from './src/routes/upc.routes';
import inventoryRouter from './src/routes/inventory.routes';
import receiptRouter from './src/routes/receipt.routes';
import dealsRouter from './src/routes/deals.routes';
import reactionsRouter from './src/routes/reactions.routes';
import { errorHandler } from './src/middleware/errorHandler';
import { backgroundJobService, startBackgroundJobs } from './src/services/backgroundJobService';
import type { UserProfile } from './src/types';
@@ -278,6 +280,10 @@ app.use('/api/upc', upcRouter);
app.use('/api/inventory', inventoryRouter);
// 13. Receipt scanning routes.
app.use('/api/receipts', receiptRouter);
// 14. Deals and best prices routes.
app.use('/api/deals', dealsRouter);
// 15. Reactions/social features routes.
app.use('/api/reactions', reactionsRouter);
// --- Error Handling and Server Startup ---

View File

@@ -34,8 +34,12 @@ export const useActivityLogQuery = (limit: number = 20, offset: number = 0) => {
}
const json = await response.json();
// API returns { success: true, data: [...] }, extract the data array
return json.data ?? json;
// ADR-028: API returns { success: true, data: [...] }
// If success is false or data is not an array, return empty array to prevent .map() errors
if (!json.success || !Array.isArray(json.data)) {
return [];
}
return json.data;
},
// Activity log changes frequently, keep stale time short
staleTime: 1000 * 30, // 30 seconds

View File

@@ -32,8 +32,12 @@ export const useBestSalePricesQuery = (enabled: boolean = true) => {
}
const json = await response.json();
// API returns { success: true, data: [...] }, extract the data array
return json.data ?? json;
// ADR-028: API returns { success: true, data: [...] }
// If success is false or data is not an array, return empty array to prevent .map() errors
if (!json.success || !Array.isArray(json.data)) {
return [];
}
return json.data;
},
enabled,
// Prices update when flyers change, keep fresh for 2 minutes

View File

@@ -28,8 +28,12 @@ export const useBrandsQuery = (enabled: boolean = true) => {
}
const json = await response.json();
// API returns { success: true, data: [...] }, extract the data array
return json.data ?? json;
// ADR-028: API returns { success: true, data: [...] }
// If success is false or data is not an array, return empty array to prevent .map() errors
if (!json.success || !Array.isArray(json.data)) {
return [];
}
return json.data;
},
enabled,
staleTime: 1000 * 60 * 5, // 5 minutes - brands don't change frequently

View File

@@ -27,8 +27,12 @@ export const useCategoriesQuery = () => {
}
const json = await response.json();
// API returns { success: true, data: [...] }, extract the data array
return json.data ?? json;
// ADR-028: API returns { success: true, data: [...] }
// If success is false or data is not an array, return empty array to prevent .map() errors
if (!json.success || !Array.isArray(json.data)) {
return [];
}
return json.data;
},
staleTime: 1000 * 60 * 60, // 1 hour - categories rarely change
});

View File

@@ -38,8 +38,12 @@ export const useFlyerItemsForFlyersQuery = (flyerIds: number[], enabled: boolean
}
const json = await response.json();
// API returns { success: true, data: [...] }, extract the data array
return json.data ?? json;
// ADR-028: API returns { success: true, data: [...] }
// If success is false or data is not an array, return empty array to prevent .map() errors
if (!json.success || !Array.isArray(json.data)) {
return [];
}
return json.data;
},
enabled: enabled && flyerIds.length > 0,
// Flyer items don't change frequently once created

View File

@@ -117,9 +117,9 @@ describe('useFlyerItemsQuery', () => {
expect(result.current.data).toEqual([]);
});
it('should handle response without data property (fallback)', async () => {
// Edge case: API returns unexpected format without data property
// The hook falls back to returning the raw json object
it('should return empty array when response lacks success/data structure (ADR-028)', async () => {
// ADR-028: API must return { success: true, data: [...] }
// Non-compliant responses return empty array to prevent .map() errors
const legacyItems = [{ item_id: 1, name: 'Legacy Item' }];
mockedApiClient.fetchFlyerItems.mockResolvedValue({
ok: true,
@@ -130,7 +130,7 @@ describe('useFlyerItemsQuery', () => {
await waitFor(() => expect(result.current.isSuccess).toBe(true));
// Falls back to raw response when .data is undefined
expect(result.current.data).toEqual(legacyItems);
// Returns empty array when response doesn't match ADR-028 format
expect(result.current.data).toEqual([]);
});
});

View File

@@ -36,8 +36,12 @@ export const useFlyerItemsQuery = (flyerId: number | undefined) => {
}
const json = await response.json();
// API returns { success: true, data: [...] }, extract the data array
return json.data ?? json;
// ADR-028: API returns { success: true, data: [...] }
// If success is false or data is not an array, return empty array to prevent .map() errors
if (!json.success || !Array.isArray(json.data)) {
return [];
}
return json.data;
},
// Only run the query if we have a valid flyer ID
enabled: !!flyerId,

View File

@@ -33,8 +33,12 @@ export const useFlyersQuery = (limit: number = 20, offset: number = 0) => {
}
const json = await response.json();
// API returns { success: true, data: [...] }, extract the data array
return json.data ?? json;
// ADR-028: API returns { success: true, data: [...] }
// If success is false or data is not an array, return empty array to prevent .map() errors
if (!json.success || !Array.isArray(json.data)) {
return [];
}
return json.data;
},
// Keep data fresh for 2 minutes since flyers don't change frequently
staleTime: 1000 * 60 * 2,

View File

@@ -30,8 +30,12 @@ export const useLeaderboardQuery = (limit: number = 10, enabled: boolean = true)
}
const json = await response.json();
// API returns { success: true, data: [...] }, extract the data array
return json.data ?? json;
// ADR-028: API returns { success: true, data: [...] }
// If success is false or data is not an array, return empty array to prevent .map() errors
if (!json.success || !Array.isArray(json.data)) {
return [];
}
return json.data;
},
enabled,
staleTime: 1000 * 60 * 2, // 2 minutes - leaderboard can change moderately

View File

@@ -32,8 +32,12 @@ export const useMasterItemsQuery = () => {
}
const json = await response.json();
// API returns { success: true, data: [...] }, extract the data array
return json.data ?? json;
// ADR-028: API returns { success: true, data: [...] }
// If success is false or data is not an array, return empty array to prevent .map() errors
if (!json.success || !Array.isArray(json.data)) {
return [];
}
return json.data;
},
// Master items change infrequently, keep data fresh for 10 minutes
staleTime: 1000 * 60 * 10,

View File

@@ -35,8 +35,12 @@ export const usePriceHistoryQuery = (masterItemIds: number[], enabled: boolean =
}
const json = await response.json();
// API returns { success: true, data: [...] }, extract the data array
return json.data ?? json;
// ADR-028: API returns { success: true, data: [...] }
// If success is false or data is not an array, return empty array to prevent .map() errors
if (!json.success || !Array.isArray(json.data)) {
return [];
}
return json.data;
},
enabled: enabled && masterItemIds.length > 0,
staleTime: 1000 * 60 * 10, // 10 minutes - historical data doesn't change frequently

View File

@@ -32,8 +32,12 @@ export const useShoppingListsQuery = (enabled: boolean) => {
}
const json = await response.json();
// API returns { success: true, data: [...] }, extract the data array
return json.data ?? json;
// ADR-028: API returns { success: true, data: [...] }
// If success is false or data is not an array, return empty array to prevent .map() errors
if (!json.success || !Array.isArray(json.data)) {
return [];
}
return json.data;
},
enabled,
// Keep data fresh for 1 minute since users actively manage shopping lists

View File

@@ -27,8 +27,12 @@ export const useSuggestedCorrectionsQuery = () => {
}
const json = await response.json();
// API returns { success: true, data: [...] }, extract the data array
return json.data ?? json;
// ADR-028: API returns { success: true, data: [...] }
// If success is false or data is not an array, return empty array to prevent .map() errors
if (!json.success || !Array.isArray(json.data)) {
return [];
}
return json.data;
},
staleTime: 1000 * 60, // 1 minute - corrections change moderately
});

View File

@@ -32,8 +32,12 @@ export const useWatchedItemsQuery = (enabled: boolean) => {
}
const json = await response.json();
// API returns { success: true, data: [...] }, extract the data array
return json.data ?? json;
// ADR-028: API returns { success: true, data: [...] }
// If success is false or data is not an array, return empty array to prevent .map() errors
if (!json.success || !Array.isArray(json.data)) {
return [];
}
return json.data;
},
enabled,
// Keep data fresh for 1 minute since users actively manage watched items

View File

@@ -160,10 +160,11 @@ export class AIService {
this.logger = logger;
this.logger.info('---------------- [AIService] Constructor Start ----------------');
// Use mock AI in test and staging environments (no real API calls, no GEMINI_API_KEY needed)
// Use mock AI in test, staging, and development environments (no real API calls, no GEMINI_API_KEY needed)
const isTestEnvironment =
process.env.NODE_ENV === 'test' ||
process.env.NODE_ENV === 'staging' ||
process.env.NODE_ENV === 'development' ||
!!process.env.VITEST_POOL_ID;
if (aiClient) {

View File

@@ -1,7 +1,10 @@
// src/utils/rateLimit.ts
import { Request } from 'express';
const isTestEnv = process.env.NODE_ENV === 'test';
const isTestEnv =
process.env.NODE_ENV === 'test' ||
process.env.NODE_ENV === 'development' ||
process.env.NODE_ENV === 'staging';
/**
* Helper to determine if rate limiting should be skipped.
@@ -10,4 +13,4 @@ const isTestEnv = process.env.NODE_ENV === 'test';
export const shouldSkipRateLimit = (req: Request) => {
if (!isTestEnv) return false;
return req.headers['x-test-rate-limit-enable'] !== 'true';
};
};

View File

@@ -1,9 +1,23 @@
// vite.config.ts
import path from 'path';
// import fs from 'fs'; // Unused when nginx handles SSL
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import { sentryVitePlugin } from '@sentry/vite-plugin';
// HTTPS configuration for local development (optional, disabled when using nginx proxy)
// const httpsConfig = (() => {
// const keyPath = '/app/certs/localhost.key';
// const certPath = '/app/certs/localhost.crt';
// if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
// return {
// key: fs.readFileSync(keyPath),
// cert: fs.readFileSync(certPath),
// };
// }
// return undefined;
// })();
// Ensure NODE_ENV is set to 'test' for all Vitest runs.
process.env.NODE_ENV = 'test';
@@ -38,11 +52,9 @@ export default defineConfig({
? [
sentryVitePlugin({
// URL of the Bugsink instance (Sentry-compatible)
// This is read from SENTRY_URL env var or falls back to the DSN's origin
url: process.env.SENTRY_URL,
// Org and project are required by the API but Bugsink ignores them
// when using debug ID matching (Bugsink 1.5+)
org: 'flyer-crawler',
project: 'flyer-crawler-frontend',
@@ -61,8 +73,9 @@ export default defineConfig({
: []),
],
server: {
port: 3000,
port: 5173, // Internal port, nginx proxies on 3000
host: '0.0.0.0',
// https: httpsConfig, // Disabled - nginx handles SSL
},
resolve: {
alias: {