Compare commits

...

4 Commits

Author SHA1 Message Date
Gitea Actions
1fcb9fd5c7 ci: Bump version to 0.12.6 [skip ci] 2026-01-22 03:41:25 +05:00
8bd4e081ea e2e fixin, frontend + home page work
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 19m0s
2026-01-21 14:40:19 -08:00
Gitea Actions
6e13570deb ci: Bump version to 0.12.5 [skip ci] 2026-01-22 01:36:01 +05:00
2eba66fb71 make e2e actually e2e - sigh
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 19m9s
2026-01-21 12:34:46 -08:00
25 changed files with 810 additions and 115 deletions

154
CLAUDE.md
View File

@@ -424,11 +424,24 @@ psql -d "flyer-crawler-test" -c "\dn+ public"
The dev container runs its own **local Bugsink instance** - it does NOT connect to the production Bugsink server:
- **Local Bugsink**: Runs at `http://localhost:8000` inside the container
- **Pre-configured DSNs**: Set in `compose.dev.yml`, pointing to local instance
- **Local Bugsink UI**: Accessible at `https://localhost:8443` (proxied from `http://localhost:8000` by nginx)
- **Admin credentials**: `admin@localhost` / `admin`
- **Bugsink Projects**: Backend (Dev) - Project ID 1, Frontend (Dev) - Project ID 2
- **Configuration Files**:
- `compose.dev.yml` - Sets default DSNs using `127.0.0.1:8000` protocol (for initial container setup)
- `.env.local` - **OVERRIDES** compose.dev.yml with `localhost:8000` protocol (this is what the app actually uses)
- **CRITICAL**: `.env.local` takes precedence over `compose.dev.yml` environment variables
- **DSN Configuration**:
- **Backend DSN** (Node.js/Express): Configured in `.env.local` as `SENTRY_DSN=http://<key>@localhost:8000/1`
- **Frontend DSN** (React/Browser): Configured in `.env.local` as `VITE_SENTRY_DSN=http://<key>@localhost:8000/2`
- **Why localhost instead of 127.0.0.1?** The `.env.local` file was created separately and uses `localhost` which works fine in practice
- **HTTPS Setup**: Self-signed certificates auto-generated with mkcert on container startup (for UI access only, not for Sentry SDK)
- **CSRF Protection**: Django configured with `SECURE_PROXY_SSL_HEADER` to trust `X-Forwarded-Proto` from nginx
- **Isolated**: Dev errors stay local, don't pollute production/test dashboards
- **No Gitea secrets needed**: Everything is self-contained in the container
- **Accessing Errors**:
- **Via Browser**: Open `https://localhost:8443` and login to view issues
- **Via MCP**: Configure a second Bugsink MCP server pointing to `http://localhost:8000` (see MCP Servers section below)
---
@@ -436,64 +449,105 @@ The dev container runs its own **local Bugsink instance** - it does NOT connect
The following MCP servers are configured for this project:
| Server | Purpose |
| --------------------- | ------------------------------------------- |
| gitea-projectium | Gitea API for gitea.projectium.com |
| gitea-torbonium | Gitea API for gitea.torbonium.com |
| podman | Container management |
| filesystem | File system access |
| fetch | Web fetching |
| markitdown | Convert documents to markdown |
| sequential-thinking | Step-by-step reasoning |
| memory | Knowledge graph persistence |
| postgres | Direct database queries (localhost:5432) |
| playwright | Browser automation and testing |
| redis | Redis cache inspection (localhost:6379) |
| sentry-selfhosted-mcp | Error tracking via Bugsink (localhost:8000) |
| Server | Purpose |
| ------------------- | ---------------------------------------------------------------------------- |
| gitea-projectium | Gitea API for gitea.projectium.com |
| gitea-torbonium | Gitea API for gitea.torbonium.com |
| podman | Container management |
| filesystem | File system access |
| fetch | Web fetching |
| markitdown | Convert documents to markdown |
| sequential-thinking | Step-by-step reasoning |
| memory | Knowledge graph persistence |
| postgres | Direct database queries (localhost:5432) |
| playwright | Browser automation and testing |
| redis | Redis cache inspection (localhost:6379) |
| bugsink | Error tracking - production Bugsink (bugsink.projectium.com) - **PROD/TEST** |
| bugsink-dev | Error tracking - dev container Bugsink (localhost:8000) - **DEV CONTAINER** |
**Note:** MCP servers work in both **Claude CLI** and **Claude Code VS Code extension** (as of January 2026).
### Sentry/Bugsink MCP Server Setup (ADR-015)
**CRITICAL**: There are **TWO separate Bugsink MCP servers**:
To enable Claude Code to query and analyze application errors from Bugsink:
- **bugsink**: Connects to production Bugsink at `https://bugsink.projectium.com` for production and test server errors
- **bugsink-dev**: Connects to local dev container Bugsink at `http://localhost:8000` for local development errors
1. **Install the MCP server**:
### Bugsink MCP Server Setup (ADR-015)
```bash
# Clone the sentry-selfhosted-mcp repository
git clone https://github.com/ddfourtwo/sentry-selfhosted-mcp.git
cd sentry-selfhosted-mcp
npm install
```
**IMPORTANT**: You need to configure **TWO separate MCP servers** - one for production/test, one for local dev.
2. **Configure Claude Code** (add to `.claude/mcp.json`):
#### Installation (shared for both servers)
```json
{
"sentry-selfhosted-mcp": {
"command": "node",
"args": ["/path/to/sentry-selfhosted-mcp/dist/index.js"],
"env": {
"SENTRY_URL": "http://localhost:8000",
"SENTRY_AUTH_TOKEN": "<get-from-bugsink-ui>",
"SENTRY_ORG_SLUG": "flyer-crawler"
}
}
}
```
```bash
# Clone the bugsink-mcp repository (NOT sentry-selfhosted-mcp)
git clone https://github.com/j-shelfwood/bugsink-mcp.git
cd bugsink-mcp
npm install
npm run build
```
3. **Get the auth token**:
- Navigate to Bugsink UI at `http://localhost:8000`
- Log in with admin credentials
- Go to Settings > API Keys
- Create a new API key with read access
#### Production/Test Bugsink MCP (bugsink)
4. **Available capabilities**:
- List projects and issues
- View detailed error events
- Search by error message or stack trace
- Update issue status (resolve, ignore)
- Add comments to issues
Add to `.claude/mcp.json`:
```json
{
"bugsink": {
"command": "node",
"args": ["d:\\gitea\\bugsink-mcp\\dist\\index.js"],
"env": {
"BUGSINK_URL": "https://bugsink.projectium.com",
"BUGSINK_API_TOKEN": "<get-from-production-bugsink>",
"BUGSINK_ORG_SLUG": "sentry"
}
}
}
```
**Get the auth token**:
- Navigate to https://bugsink.projectium.com
- Log in with production credentials
- Go to Settings > API Keys
- Create a new API key with read access
#### Dev Container Bugsink MCP (bugsink-dev)
Add to `.claude/mcp.json`:
```json
{
"bugsink-dev": {
"command": "node",
"args": ["d:\\gitea\\bugsink-mcp\\dist\\index.js"],
"env": {
"BUGSINK_URL": "http://localhost:8000",
"BUGSINK_API_TOKEN": "<get-from-local-bugsink>",
"BUGSINK_ORG_SLUG": "sentry"
}
}
}
```
**Get the auth token**:
- Navigate to http://localhost:8000 (or https://localhost:8443)
- Log in with `admin@localhost` / `admin`
- Go to Settings > API Keys
- Create a new API key with read access
#### MCP Tool Usage
When using Bugsink MCP tools, remember:
- `mcp__bugsink__*` tools connect to **production/test** Bugsink
- `mcp__bugsink-dev__*` tools connect to **dev container** Bugsink
- Available capabilities for both:
- List projects and issues
- View detailed error events and stacktraces
- Search by error message or stack trace
- Update issue status (resolve, ignore)
- Create releases
### SSH Server Access

View File

@@ -147,6 +147,9 @@ ALLOWED_HOSTS = deduce_allowed_hosts(BUGSINK["BASE_URL"])\n\
\n\
# Console email backend for dev\n\
EMAIL_BACKEND = "bugsink.email_backends.QuietConsoleEmailBackend"\n\
\n\
# HTTPS proxy support (nginx reverse proxy on port 8443)\n\
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")\n\
' > /opt/bugsink/conf/bugsink_conf.py
# Create Bugsink startup script

View File

@@ -59,7 +59,11 @@ See [INSTALL.md](INSTALL.md) for detailed setup instructions.
## Environment Variables
This project uses environment variables for configuration (no `.env` files). Key variables:
**Production/Test**: Uses Gitea CI/CD secrets injected during deployment (no local `.env` files)
**Dev Container**: Uses `.env.local` file which **overrides** the default DSNs in `compose.dev.yml`
Key variables:
| Variable | Description |
| -------------------------------------------- | -------------------------------- |

View File

@@ -50,7 +50,8 @@ services:
- '80:80' # HTTP redirect to HTTPS (matches production)
- '443:443' # Frontend HTTPS (nginx proxies Vite 5173 → 443)
- '3001:3001' # Backend API
- '8000:8000' # Bugsink error tracking (ADR-015)
- '8000:8000' # Bugsink error tracking HTTP (ADR-015)
- '8443:8443' # Bugsink error tracking HTTPS (ADR-015)
environment:
# Core settings
- NODE_ENV=development
@@ -77,13 +78,16 @@ services:
- BUGSINK_DB_USER=bugsink
- BUGSINK_DB_PASSWORD=bugsink_dev_password
- BUGSINK_PORT=8000
- BUGSINK_BASE_URL=http://localhost:8000
- BUGSINK_BASE_URL=https://localhost:8443
- BUGSINK_ADMIN_EMAIL=admin@localhost
- BUGSINK_ADMIN_PASSWORD=admin
- BUGSINK_SECRET_KEY=dev-bugsink-secret-key-minimum-50-characters-for-security
# Sentry SDK configuration (points to local Bugsink)
- SENTRY_DSN=http://59a58583-e869-7697-f94a-cfa0337676a8@localhost:8000/1
- VITE_SENTRY_DSN=http://d5fc5221-4266-ff2f-9af8-5689696072f3@localhost:8000/2
# Sentry SDK configuration (points to local Bugsink HTTP)
# Note: Using HTTP with 127.0.0.1 instead of localhost because Sentry SDK
# doesn't accept 'localhost' as a valid hostname in DSN validation
# The browser accesses Bugsink at http://localhost:8000 (nginx proxies to HTTPS for the app)
- SENTRY_DSN=http://cea01396-c562-46ad-b587-8fa5ee6b1d22@127.0.0.1:8000/1
- VITE_SENTRY_DSN=http://d92663cb-73cf-4145-b677-b84029e4b762@127.0.0.1:8000/2
- SENTRY_ENVIRONMENT=development
- VITE_SENTRY_ENVIRONMENT=development
- SENTRY_ENABLED=true

View File

@@ -24,7 +24,29 @@ server {
# Allow large file uploads (matches production)
client_max_body_size 100M;
# Proxy all requests to Vite dev server on port 5173
# Proxy API requests to Express server on port 3001
location /api/ {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Proxy WebSocket connections for real-time notifications
location /ws {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Proxy all other requests to Vite dev server on port 5173
location / {
proxy_pass http://localhost:5173;
proxy_http_version 1.1;

View File

@@ -0,0 +1,534 @@
# Testing Session - UI/UX Improvements
**Date**: 2026-01-21
**Tester**: [Your Name]
**Session Start**: [Time]
**Environment**: Dev Container
---
## 🎯 Session Objective
Test all 4 critical UI/UX improvements:
1. Brand Colors (visual verification)
2. Button Component (functional testing)
3. Onboarding Tour (flow testing)
4. Mobile Navigation (responsive testing)
---
## ✅ Pre-Test Setup Checklist
### 1. Dev Server Status
- [ ] Dev server running at `http://localhost:5173`
- [ ] Browser open (Chrome/Edge recommended)
- [ ] DevTools open (F12)
**Command to start**:
```bash
podman exec -it flyer-crawler-dev npm run dev:container
```
**Server Status**: [ ] Running [ ] Not Running
---
### 2. Browser Setup
- [ ] Clear cache (Ctrl+Shift+Delete)
- [ ] Clear localStorage for localhost
- [ ] Enable responsive design mode (Ctrl+Shift+M)
**Browser Version**: ********\_********
---
## 🧪 Test Execution
### TEST 1: Onboarding Tour ⭐ CRITICAL
**Priority**: 🔴 Must Pass
**Time**: 5 minutes
#### Steps:
1. Open DevTools → Application → Local Storage
2. Delete key: `flyer_crawler_onboarding_completed`
3. Refresh page (F5)
4. Observe if tour appears
#### Expected:
- ✅ Tour modal appears within 2 seconds
- ✅ Shows "Step 1 of 6"
- ✅ Points to Flyer Uploader section
- ✅ Skip button visible
- ✅ Next button visible
#### Actual Result:
```
[Record what you see here]
```
**Status**: [ ] ✅ PASS [ ] ❌ FAIL [ ] ⚠️ PARTIAL
**Screenshots**: [Attach if needed]
---
### TEST 2: Tour Navigation
**Time**: 5 minutes
#### Steps:
Click "Next" button 6 times, observe each step
#### Verification Table:
| Step | Target | Visible? | Correct Text? | Notes |
| ---- | -------------- | -------- | ------------- | ----- |
| 1 | Flyer Uploader | [ ] | [ ] | |
| 2 | Data Table | [ ] | [ ] | |
| 3 | Watch Button | [ ] | [ ] | |
| 4 | Watchlist | [ ] | [ ] | |
| 5 | Price Chart | [ ] | [ ] | |
| 6 | Shopping List | [ ] | [ ] | |
#### Additional Checks:
- [ ] Progress indicator updates (1/6 → 6/6)
- [ ] Can click "Previous" button
- [ ] Tour closes after step 6
- [ ] localStorage key saved
**Status**: [ ] ✅ PASS [ ] ❌ FAIL
---
### TEST 3: Mobile Tab Bar ⭐ CRITICAL
**Priority**: 🔴 Must Pass
**Time**: 8 minutes
#### Part A: Mobile View (375px)
**Setup**: Toggle device toolbar → iPhone SE
#### Checks:
- [ ] Bottom tab bar visible
- [ ] 4 tabs present: Home, Deals, Lists, Profile
- [ ] Left sidebar (flyer list) HIDDEN
- [ ] Right sidebar (widgets) HIDDEN
- [ ] Main content uses full width
**Visual Check**:
```
Tab Bar Position: [ ] Bottom [ ] Other: _______
Number of Tabs: _______
Tab Bar Height: ~64px? [ ] Yes [ ] No
```
#### Part B: Tab Navigation
Click each tab and verify:
| Tab | URL | Page Loads? | Highlights? | Content Correct? |
| ------- | ---------- | ----------- | ----------- | ---------------- |
| Home | `/` | [ ] | [ ] | [ ] |
| Deals | `/deals` | [ ] | [ ] | [ ] |
| Lists | `/lists` | [ ] | [ ] | [ ] |
| Profile | `/profile` | [ ] | [ ] | [ ] |
#### Part C: Desktop View (1440px)
**Setup**: Exit device mode, maximize window
#### Checks:
- [ ] Tab bar HIDDEN (not visible)
- [ ] Left sidebar VISIBLE
- [ ] Right sidebar VISIBLE
- [ ] 3-column layout intact
- [ ] No layout regressions
**Status**: [ ] ✅ PASS [ ] ❌ FAIL
---
### TEST 4: Dark Mode ⭐ CRITICAL
**Priority**: 🔴 Must Pass
**Time**: 5 minutes
#### Steps:
1. Click dark mode toggle in header
2. Navigate: Home → Deals → Lists → Profile
3. Observe colors and contrast
#### Visual Verification:
**Mobile Tab Bar**:
- [ ] Dark background (#111827 or similar)
- [ ] Dark border color
- [ ] Active tab: teal (#14b8a6)
- [ ] Inactive tabs: gray
**New Pages**:
- [ ] DealsPage: dark background, light text
- [ ] ShoppingListsPage: dark cards
- [ ] FlyersPage: dark theme
- [ ] No white boxes visible
**Button Component**:
- [ ] Primary buttons: teal background
- [ ] Secondary buttons: gray background
- [ ] Danger buttons: red background
- [ ] All text readable
#### Toggle Back:
- [ ] Light mode restores correctly
- [ ] No stuck dark elements
**Status**: [ ] ✅ PASS [ ] ❌ FAIL
---
### TEST 5: Brand Colors Visual Check
**Time**: 3 minutes
#### Verification:
Navigate through app and check teal color consistency:
- [ ] Active tab: teal
- [ ] Primary buttons: teal
- [ ] Links on hover: teal
- [ ] Focus rings: teal
- [ ] All teal shades match (#14b8a6)
**Color Picker Check** (optional):
Use DevTools color picker on active tab:
- Expected: `#14b8a6` or `rgb(20, 184, 166)`
- Actual: ********\_\_\_********
**Status**: [ ] ✅ PASS [ ] ❌ FAIL
---
### TEST 6: Button Component
**Time**: 5 minutes
#### Find and Test Buttons:
**FlyerUploader Page**:
- [ ] "Upload Another Flyer" button (primary, teal)
- [ ] Button clickable
- [ ] Hover effect works
- [ ] Loading state (if applicable)
**ShoppingList Page** (navigate to /lists):
- [ ] "New List" button (secondary, gray)
- [ ] "Delete List" button (danger, red)
- [ ] Buttons functional
- [ ] Hover states work
**In Dark Mode**:
- [ ] All button variants visible
- [ ] Good contrast
- [ ] No white backgrounds
**Status**: [ ] ✅ PASS [ ] ❌ FAIL
---
### TEST 7: Responsive Breakpoints
**Time**: 5 minutes
#### Test at each width:
**375px (Mobile)**:
```
Tab bar: [ ] Visible [ ] Hidden
Sidebars: [ ] Visible [ ] Hidden
Layout: [ ] Single column [ ] Multi-column
```
**768px (Tablet)**:
```
Tab bar: [ ] Visible [ ] Hidden
Sidebars: [ ] Visible [ ] Hidden
Layout: [ ] Single column [ ] Multi-column
```
**1024px (Desktop)**:
```
Tab bar: [ ] Visible [ ] Hidden
Sidebars: [ ] Visible [ ] Hidden
Layout: [ ] Single column [ ] Multi-column
```
**1440px (Large Desktop)**:
```
Layout: [ ] Unchanged [ ] Broken
All elements: [ ] Visible [ ] Hidden/Cut off
```
**Status**: [ ] ✅ PASS [ ] ❌ FAIL
---
### TEST 8: Admin Routes (If Admin User)
**Time**: 3 minutes
**Skip if**: [ ] Not admin user
#### Steps:
1. Log in as admin
2. Navigate to `/admin`
3. Check for tab bar
#### Checks:
- [ ] Admin dashboard loads
- [ ] Tab bar NOT visible
- [ ] Layout looks correct
- [ ] Can navigate to subpages
- [ ] Subpages work in mobile view
**Status**: [ ] ✅ PASS [ ] ❌ FAIL [ ] ⏭️ SKIPPED
---
### TEST 9: Console Errors
**Time**: 2 minutes
#### Steps:
1. Open Console tab in DevTools
2. Clear console
3. Navigate through app: Home → Deals → Lists → Profile → Home
4. Check for red error messages
#### Results:
```
Errors Found: [ ] None [ ] Some (list below)
```
**React 19 warnings are OK** (peer dependencies)
**Status**: [ ] ✅ PASS (no errors) [ ] ❌ FAIL (errors present)
---
### TEST 10: Integration Flow
**Time**: 5 minutes
#### User Journey:
1. Start on Home page (mobile view)
2. Navigate to Deals tab
3. Navigate to Lists tab
4. Navigate to Profile tab
5. Navigate back to Home
6. Toggle dark mode
7. Navigate through tabs again
#### Checks:
- [ ] All navigation smooth
- [ ] No data loss
- [ ] Active tab always correct
- [ ] Browser back button works
- [ ] Dark mode persists across routes
- [ ] No JavaScript errors
- [ ] No layout shifting
**Status**: [ ] ✅ PASS [ ] ❌ FAIL
---
## 📊 Test Results Summary
### Critical Tests Status
| Test | Status | Priority | Notes |
| ------------------- | ------ | ----------- | ----- |
| 1. Onboarding Tour | [ ] | 🔴 Critical | |
| 2. Tour Navigation | [ ] | 🟡 High | |
| 3. Mobile Tab Bar | [ ] | 🔴 Critical | |
| 4. Dark Mode | [ ] | 🔴 Critical | |
| 5. Brand Colors | [ ] | 🟡 High | |
| 6. Button Component | [ ] | 🟢 Medium | |
| 7. Responsive | [ ] | 🔴 Critical | |
| 8. Admin Routes | [ ] | 🟢 Medium | |
| 9. Console Errors | [ ] | 🔴 Critical | |
| 10. Integration | [ ] | 🟡 High | |
**Pass Rate**: **\_** / 10 tests passed
---
## 🐛 Issues Found
### Critical Issues (Blockers)
1. ***
2. ***
3. ***
### High Priority Issues
1. ***
2. ***
3. ***
### Medium/Low Priority Issues
1. ***
2. ***
3. ***
---
## 📸 Screenshots
Attach screenshots for:
- [ ] Onboarding tour (step 1)
- [ ] Mobile tab bar (375px)
- [ ] Desktop layout (1440px)
- [ ] Dark mode (tab bar)
- [ ] Any bugs/issues found
---
## 🎯 Final Decision
### Must-Pass Criteria
**Critical tests** (all must pass):
- [ ] Test 1: Onboarding Tour
- [ ] Test 3: Mobile Tab Bar
- [ ] Test 4: Dark Mode
- [ ] Test 7: Responsive
- [ ] Test 9: No Console Errors
**Result**: [ ] ALL CRITICAL PASS [ ] SOME FAIL
---
### Production Readiness
**Overall Assessment**:
[ ] ✅ READY FOR PRODUCTION
[ ] ⚠️ READY WITH MINOR ISSUES
[ ] ❌ NOT READY (critical issues)
**Blocking Issues** (must fix before deploy):
1. ***
2. ***
3. ***
**Recommended Fixes** (can deploy, fix later):
1. ***
2. ***
3. ***
---
## 🔐 Sign-Off
**Tester Name**: ****************\_\_\_****************
**Date/Time Completed**: ************\_\_\_************
**Total Testing Time**: **\_\_** minutes
**Recommended Action**:
[ ] Deploy to production
[ ] Deploy to staging first
[ ] Fix issues, re-test
[ ] Hold deployment
**Additional Notes**:
---
---
---
---
---
## 📋 Next Steps
**If PASS**:
1. [ ] Create commit with test results
2. [ ] Update CHANGELOG.md
3. [ ] Tag release (v0.12.4)
4. [ ] Deploy to staging
5. [ ] Monitor for 24 hours
6. [ ] Deploy to production
**If FAIL**:
1. [ ] Log issues in GitHub/Gitea
2. [ ] Assign to developer
3. [ ] Schedule re-test
4. [ ] Update test plan if needed
---
**Session End**: [Time]
**Session Duration**: **\_\_** minutes

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "flyer-crawler",
"version": "0.12.4",
"version": "0.12.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "flyer-crawler",
"version": "0.12.4",
"version": "0.12.6",
"dependencies": {
"@bull-board/api": "^6.14.2",
"@bull-board/express": "^6.14.2",

View File

@@ -1,7 +1,7 @@
{
"name": "flyer-crawler",
"private": true,
"version": "0.12.4",
"version": "0.12.6",
"type": "module",
"scripts": {
"dev": "concurrently \"npm:start:dev\" \"vite\"",

View File

@@ -17,9 +17,48 @@ set -e
echo "🚀 Starting Flyer Crawler Dev Container..."
# Configure Bugsink HTTPS (ADR-015)
echo "🔒 Configuring Bugsink HTTPS..."
mkdir -p /etc/bugsink/ssl
if [ ! -f "/etc/bugsink/ssl/localhost+2.pem" ]; then
cd /etc/bugsink/ssl && mkcert localhost 127.0.0.1 ::1 > /dev/null 2>&1
fi
# Create nginx config for Bugsink HTTPS
cat > /etc/nginx/sites-available/bugsink <<'NGINX_EOF'
server {
listen 8443 ssl http2;
listen [::]:8443 ssl http2;
server_name localhost;
ssl_certificate /etc/bugsink/ssl/localhost+2.pem;
ssl_certificate_key /etc/bugsink/ssl/localhost+2-key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering off;
client_max_body_size 20M;
}
}
NGINX_EOF
ln -sf /etc/nginx/sites-available/bugsink /etc/nginx/sites-enabled/bugsink
# Copy the dev nginx config from mounted volume to nginx sites-available
echo "📋 Copying nginx dev config..."
cp /app/docker/nginx/dev.conf /etc/nginx/sites-available/default
# Start nginx in background (if installed)
if command -v nginx &> /dev/null; then
echo "🌐 Starting nginx (HTTPS proxy: Vite 5173 → port 443)..."
echo "🌐 Starting nginx (HTTPS: Vite 5173 → 443, Bugsink 8000 → 8443, API 3001 → /api/)..."
nginx &
fi
@@ -27,6 +66,22 @@ fi
echo "📊 Starting Bugsink error tracking..."
/usr/local/bin/start-bugsink.sh > /var/log/bugsink/server.log 2>&1 &
# Wait for Bugsink to initialize, then run snappea migrations
echo "⏳ Waiting for Bugsink to initialize..."
sleep 5
echo "🔧 Running Bugsink snappea database migrations..."
cd /opt/bugsink/conf && \
export DATABASE_URL="postgresql://bugsink:bugsink_dev_password@postgres:5432/bugsink" && \
export SECRET_KEY="dev-bugsink-secret-key-minimum-50-characters-for-security" && \
/opt/bugsink/bin/bugsink-manage migrate --database=snappea > /dev/null 2>&1
# Start Snappea task worker
echo "🔄 Starting Snappea task worker..."
cd /opt/bugsink/conf && \
export DATABASE_URL="postgresql://bugsink:bugsink_dev_password@postgres:5432/bugsink" && \
export SECRET_KEY="dev-bugsink-secret-key-minimum-50-characters-for-security" && \
/opt/bugsink/bin/bugsink-manage runsnappea > /var/log/bugsink/snappea.log 2>&1 &
# Start Logstash in background
echo "📝 Starting Logstash..."
/usr/share/logstash/bin/logstash -f /etc/logstash/conf.d/bugsink.conf > /var/log/logstash/logstash.log 2>&1 &
@@ -41,8 +96,8 @@ cd /app
echo "💻 Starting development server..."
echo " - Frontend: https://localhost (nginx HTTPS → Vite on 5173)"
echo " - Backend API: http://localhost:3001"
echo " - Bugsink: http://localhost:8000"
echo " - Note: Accept the self-signed certificate warning in your browser"
echo " - Bugsink: https://localhost:8443 (nginx HTTPS → Bugsink on 8000)"
echo " - Note: Accept the self-signed certificate warnings in your browser"
echo ""
# Run npm dev server (this will block and keep container alive)

View File

@@ -15,7 +15,7 @@ export const Dashboard: React.FC = () => {
<RecipeSuggester />
{/* Other Dashboard Widgets */}
<div className="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<div className="bg-white dark:bg-gray-800 shadow rounded-lg p-6 transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/80">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">Your Flyers</h2>
<FlyerCountDisplay />
</div>

View File

@@ -31,7 +31,7 @@ export const Header: React.FC<HeaderProps> = ({
// The state and handlers for the old AuthModal and SignUpModal have been removed.
return (
<>
<header className="bg-white dark:bg-gray-900 shadow-md sticky top-0 z-20">
<header className="bg-white dark:bg-gray-900 shadow-md sticky top-0 z-20 border-b-2 border-brand-primary dark:border-brand-secondary">
<div className="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center">

View File

@@ -43,7 +43,7 @@ export const Leaderboard: React.FC = () => {
}
return (
<div className="bg-white dark:bg-gray-800 shadow-lg rounded-lg p-6">
<div className="bg-white dark:bg-gray-800 shadow-lg rounded-lg p-6 transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/80">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4 flex items-center">
<Award className="w-6 h-6 mr-2 text-blue-500" />
Top Users
@@ -57,7 +57,7 @@ export const Leaderboard: React.FC = () => {
{leaderboard.map((user) => (
<li
key={user.user_id}
className="flex items-center space-x-4 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg transition hover:bg-gray-100 dark:hover:bg-gray-600"
className="flex items-center space-x-4 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg transition-colors hover:bg-brand-light/30 dark:hover:bg-brand-dark/20"
>
<div className="shrink-0 w-8 text-center">{getRankIcon(user.rank)}</div>
<img

View File

@@ -48,7 +48,7 @@ export const RecipeSuggester: React.FC = () => {
);
return (
<div className="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<div className="bg-white dark:bg-gray-800 shadow rounded-lg p-6 transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/80">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
Get a Recipe Suggestion
</h2>

View File

@@ -155,9 +155,15 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
unitSystem={'imperial'} // This can be passed down or sourced from a context
user={user}
/>
<PriceHistoryChart />
<Leaderboard />
<ActivityLog userProfile={userProfile} onLogClick={handleActivityLogClick} />
{user && (
<>
<PriceHistoryChart />
<Leaderboard />
{userProfile && (
<ActivityLog userProfile={userProfile} onLogClick={handleActivityLogClick} />
)}
</>
)}
</>
</div>
</div>

View File

@@ -4,14 +4,14 @@ import supertest from 'supertest';
import { cleanupDb } from '../utils/cleanup';
import { createAndLoginUser } from '../utils/testHelpers';
import type { UserProfile } from '../../types';
import { app } from '../setup/e2e-global-setup';
import { getServerUrl } from '../setup/e2e-global-setup';
/**
* @vitest-environment node
*/
describe('Admin Route Authorization', () => {
// Create a getter function that returns supertest instance with the app
const getRequest = () => supertest(app);
const getRequest = () => supertest(getServerUrl());
let regularUser: UserProfile;
let regularUserAuthToken: string;
@@ -38,27 +38,27 @@ describe('Admin Route Authorization', () => {
const adminEndpoints = [
{
method: 'GET',
path: '/admin/stats',
path: '/api/admin/stats',
},
{
method: 'GET',
path: '/admin/users',
path: '/api/admin/users',
},
{
method: 'GET',
path: '/admin/corrections',
path: '/api/admin/corrections',
},
{
method: 'POST',
path: '/admin/corrections/1/approve',
path: '/api/admin/corrections/1/approve',
},
{
method: 'POST',
path: '/admin/trigger/daily-deal-check',
path: '/api/admin/trigger/daily-deal-check',
},
{
method: 'GET',
path: '/admin/queues/status',
path: '/api/admin/queues/status',
},
];

View File

@@ -3,7 +3,7 @@ import { describe, it, expect, afterAll } from 'vitest';
import supertest from 'supertest';
import { getPool } from '../../services/db/connection.db';
import { cleanupDb } from '../utils/cleanup';
import { app } from '../setup/e2e-global-setup';
import { getServerUrl } from '../setup/e2e-global-setup';
/**
* @vitest-environment node
@@ -11,7 +11,7 @@ import { app } from '../setup/e2e-global-setup';
describe('E2E Admin Dashboard Flow', () => {
// Create a getter function that returns supertest instance with the app
const getRequest = () => supertest(app);
const getRequest = () => supertest(getServerUrl());
// Use a unique email for every run to avoid collisions
const uniqueId = Date.now();
@@ -62,7 +62,7 @@ describe('E2E Admin Dashboard Flow', () => {
// 4. Fetch System Stats (Protected Admin Route)
const statsResponse = await getRequest()
.get('/admin/stats')
.get('/api/admin/stats')
.set('Authorization', `Bearer ${authToken}`);
expect(statsResponse.status).toBe(200);
@@ -71,7 +71,7 @@ describe('E2E Admin Dashboard Flow', () => {
// 5. Fetch User List (Protected Admin Route)
const usersResponse = await getRequest()
.get('/admin/users')
.get('/api/admin/users')
.set('Authorization', `Bearer ${authToken}`);
expect(usersResponse.status).toBe(200);
@@ -84,7 +84,7 @@ describe('E2E Admin Dashboard Flow', () => {
// 6. Check Queue Status (Protected Admin Route)
const queueResponse = await getRequest()
.get('/admin/queues/status')
.get('/api/admin/queues/status')
.set('Authorization', `Bearer ${authToken}`);
expect(queueResponse.status).toBe(200);

View File

@@ -4,7 +4,7 @@ import supertest from 'supertest';
import { cleanupDb } from '../utils/cleanup';
import { createAndLoginUser, TEST_PASSWORD } from '../utils/testHelpers';
import type { UserProfile } from '../../types';
import { app } from '../setup/e2e-global-setup';
import { getServerUrl } from '../setup/e2e-global-setup';
/**
* @vitest-environment node
@@ -12,7 +12,7 @@ import { app } from '../setup/e2e-global-setup';
describe('Authentication E2E Flow', () => {
// Create a getter function that returns supertest instance with the app
const getRequest = () => supertest(app);
const getRequest = () => supertest(getServerUrl());
let testUser: UserProfile;
let testUserAuthToken: string;
@@ -142,7 +142,7 @@ describe('Authentication E2E Flow', () => {
// Act: Use the token to access a protected route
const response = await getRequest()
.get('/api/users/me')
.get('/api/users/profile')
.set('Authorization', `Bearer ${token}`);
// Assert
@@ -165,7 +165,7 @@ describe('Authentication E2E Flow', () => {
// Act: Call the update endpoint
const updateResponse = await getRequest()
.put('/api/users/me')
.put('/api/users/profile')
.set('Authorization', `Bearer ${token}`)
.send(profileUpdates);
@@ -176,7 +176,7 @@ describe('Authentication E2E Flow', () => {
// Act 2: Fetch the profile again to verify persistence
const verifyResponse = await getRequest()
.get('/api/users/me')
.get('/api/users/profile')
.set('Authorization', `Bearer ${token}`);
// Assert 2: Check the fetched data
@@ -295,7 +295,7 @@ describe('Authentication E2E Flow', () => {
// 5. Use the new access token to access a protected route.
const profileResponse = await getRequest()
.get('/api/users/me')
.get('/api/users/profile')
.set('Authorization', `Bearer ${newAccessToken}`);
expect(profileResponse.status).toBe(200);
expect(profileResponse.body.data.user.user_id).toBe(testUser.user.user_id);

View File

@@ -13,7 +13,7 @@ import {
cleanupStoreLocations,
type CreatedStoreLocation,
} from '../utils/storeHelpers';
import { app } from '../setup/e2e-global-setup';
import { getServerUrl } from '../setup/e2e-global-setup';
/**
* @vitest-environment node
@@ -21,7 +21,7 @@ import { app } from '../setup/e2e-global-setup';
describe('E2E Budget Management Journey', () => {
// Create a getter function that returns supertest instance with the app
const getRequest = () => supertest(app);
const getRequest = () => supertest(getServerUrl());
const uniqueId = Date.now();
const userEmail = `budget-e2e-${uniqueId}@example.com`;
@@ -310,7 +310,7 @@ describe('E2E Budget Management Journey', () => {
// Step 15: Delete account
const deleteAccountResponse = await getRequest()
.delete('/api/user/account')
.delete('/api/users/account')
.set('Authorization', `Bearer ${authToken}`)
.send({ password: userPassword });

View File

@@ -13,7 +13,7 @@ import {
cleanupStoreLocations,
type CreatedStoreLocation,
} from '../utils/storeHelpers';
import { app } from '../setup/e2e-global-setup';
import { getServerUrl } from '../setup/e2e-global-setup';
/**
* @vitest-environment node
@@ -21,7 +21,7 @@ import { app } from '../setup/e2e-global-setup';
describe('E2E Deals and Price Tracking Journey', () => {
// Create a getter function that returns supertest instance with the app
const getRequest = () => supertest(app);
const getRequest = () => supertest(getServerUrl());
const uniqueId = Date.now();
const userEmail = `deals-e2e-${uniqueId}@example.com`;
@@ -373,7 +373,7 @@ describe('E2E Deals and Price Tracking Journey', () => {
// Step 11: Delete account
const deleteAccountResponse = await getRequest()
.delete('/api/user/account')
.delete('/api/users/account')
.set('Authorization', `Bearer ${authToken}`)
.send({ password: userPassword });

View File

@@ -7,7 +7,7 @@ import path from 'path';
import fs from 'fs';
import { cleanupDb } from '../utils/cleanup';
import { poll } from '../utils/poll';
import { app } from '../setup/e2e-global-setup';
import { getServerUrl } from '../setup/e2e-global-setup';
/**
* @vitest-environment node
@@ -15,7 +15,7 @@ import { app } from '../setup/e2e-global-setup';
describe('E2E Flyer Upload and Processing Workflow', () => {
// Create a getter function that returns supertest instance with the app
const getRequest = () => supertest(app);
const getRequest = () => supertest(getServerUrl());
const uniqueId = Date.now();
const userEmail = `e2e-uploader-${uniqueId}@example.com`;

View File

@@ -8,7 +8,7 @@ import supertest from 'supertest';
import { cleanupDb } from '../utils/cleanup';
import { poll } from '../utils/poll';
import { getPool } from '../../services/db/connection.db';
import { app } from '../setup/e2e-global-setup';
import { getServerUrl } from '../setup/e2e-global-setup';
/**
* @vitest-environment node
@@ -16,7 +16,7 @@ import { app } from '../setup/e2e-global-setup';
describe('E2E Inventory/Expiry Management Journey', () => {
// Create a getter function that returns supertest instance with the app
const getRequest = () => supertest(app);
const getRequest = () => supertest(getServerUrl());
const uniqueId = Date.now();
const userEmail = `inventory-e2e-${uniqueId}@example.com`;
@@ -408,7 +408,7 @@ describe('E2E Inventory/Expiry Management Journey', () => {
// Step 20: Delete account
const deleteAccountResponse = await getRequest()
.delete('/api/user/account')
.delete('/api/users/account')
.set('Authorization', `Bearer ${authToken}`)
.send({ password: userPassword });

View File

@@ -13,7 +13,7 @@ import {
cleanupStoreLocations,
type CreatedStoreLocation,
} from '../utils/storeHelpers';
import { app } from '../setup/e2e-global-setup';
import { getServerUrl } from '../setup/e2e-global-setup';
/**
* @vitest-environment node
@@ -21,7 +21,7 @@ import { app } from '../setup/e2e-global-setup';
describe('E2E Receipt Processing Journey', () => {
// Create a getter function that returns supertest instance with the app
const getRequest = () => supertest(app);
const getRequest = () => supertest(getServerUrl());
const uniqueId = Date.now();
const userEmail = `receipt-e2e-${uniqueId}@example.com`;
@@ -318,7 +318,7 @@ describe('E2E Receipt Processing Journey', () => {
// Step 19: Delete account
const deleteAccountResponse = await getRequest()
.delete('/api/user/account')
.delete('/api/users/account')
.set('Authorization', `Bearer ${authToken}`)
.send({ password: userPassword });

View File

@@ -8,7 +8,7 @@ import supertest from 'supertest';
import { cleanupDb } from '../utils/cleanup';
import { poll } from '../utils/poll';
import { getPool } from '../../services/db/connection.db';
import { app } from '../setup/e2e-global-setup';
import { getServerUrl } from '../setup/e2e-global-setup';
/**
* @vitest-environment node
@@ -16,7 +16,7 @@ import { app } from '../setup/e2e-global-setup';
describe('E2E UPC Scanning Journey', () => {
// Create a getter function that returns supertest instance with the app
const getRequest = () => supertest(app);
const getRequest = () => supertest(getServerUrl());
const uniqueId = Date.now();
const userEmail = `upc-e2e-${uniqueId}@example.com`;
@@ -219,7 +219,7 @@ describe('E2E UPC Scanning Journey', () => {
// Step 12: Delete account (self-service)
const deleteAccountResponse = await getRequest()
.delete('/api/user/account')
.delete('/api/users/account')
.set('Authorization', `Bearer ${authToken}`)
.send({ password: userPassword });

View File

@@ -2,7 +2,7 @@
import { describe, it, expect, afterAll } from 'vitest';
import supertest from 'supertest';
import { cleanupDb } from '../utils/cleanup';
import { app } from '../setup/e2e-global-setup';
import { getServerUrl } from '../setup/e2e-global-setup';
/**
* @vitest-environment node
@@ -10,7 +10,7 @@ import { app } from '../setup/e2e-global-setup';
describe('E2E User Journey', () => {
// Create a getter function that returns supertest instance with the app
const getRequest = () => supertest(app);
const getRequest = () => supertest(getServerUrl());
// Use a unique email for every run to avoid collisions
const uniqueId = Date.now();
@@ -62,7 +62,7 @@ describe('E2E User Journey', () => {
// 3. Create a Shopping List
const createListResponse = await getRequest()
.post('/api/users/me/shopping-lists')
.post('/api/users/shopping-lists')
.set('Authorization', `Bearer ${authToken}`)
.send({ name: 'E2E Party List' });
@@ -72,7 +72,7 @@ describe('E2E User Journey', () => {
// 4. Add an item to the list
const addItemResponse = await getRequest()
.post(`/api/users/me/shopping-lists/${shoppingListId}/items`)
.post(`/api/users/shopping-lists/${shoppingListId}/items`)
.set('Authorization', `Bearer ${authToken}`)
.send({ customItemName: 'Chips' });
@@ -81,7 +81,7 @@ describe('E2E User Journey', () => {
// 5. Verify the list and item exist via GET
const getListsResponse = await getRequest()
.get('/api/users/me/shopping-lists')
.get('/api/users/shopping-lists')
.set('Authorization', `Bearer ${authToken}`);
expect(getListsResponse.status).toBe(200);
@@ -94,7 +94,7 @@ describe('E2E User Journey', () => {
// 6. Delete the User Account (Self-Service)
const deleteAccountResponse = await getRequest()
.delete('/api/users/me')
.delete('/api/users/account')
.set('Authorization', `Bearer ${authToken}`)
.send({ password: userPassword });

View File

@@ -21,8 +21,21 @@ let server: Server;
let globalPool: ReturnType<typeof getPool> | null = null;
// Temporary directory for test file storage (to avoid modifying committed fixtures)
let tempStorageDir: string | null = null;
// Export the Express app for use with supertest in e2e tests
export let app: Express.Application;
// Internal app variable - only used within the globalSetup process
let app: Express.Application;
/**
* Gets the base URL for the E2E test server.
* Tests should make HTTP requests to this URL instead of accessing the app directly.
*
* NOTE: Due to Vitest's architecture, globalSetup runs in a separate Node.js process
* from test files. This means the Express app instance cannot be shared directly.
* Instead, tests should connect via HTTP to the server started by globalSetup.
*/
export function getServerUrl(): string {
const port = process.env.TEST_PORT || 3098;
return `http://localhost:${port}`;
}
/**
* Cleans all BullMQ queues to ensure no stale jobs from previous test runs.