Compare commits

..

5 Commits

Author SHA1 Message Date
Gitea Actions
96251ec2cc ci: Bump version to 0.19.1 [skip ci] 2026-02-19 04:45:22 +05:00
fe79522ea4 pm2 save fix + test work
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 27m44s
2026-02-18 15:43:20 -08:00
Gitea Actions
743216ef1b ci: Bump version to 0.19.0 for production release [skip ci] 2026-02-19 02:54:09 +05:00
Gitea Actions
c53295a371 ci: Bump version to 0.18.1 [skip ci] 2026-02-19 02:03:05 +05:00
c18efb1b60 fix: add PORT=3001 to production PM2 config to prevent EADDRINUSE crashes
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 32m55s
Root cause: server.ts defaults to port 3001 when PORT env var is not set.
PM2 cluster mode shares a single port across all instances, but the PORT
env variable was missing from ecosystem.config.cjs, causing port binding
conflicts.

Fix: Added PORT: '3001' to production API env configuration in
ecosystem.config.cjs. Test environment already had PORT: 3002 set.

Port allocation on projectium.com:
- 3000: Gitea (system)
- 3001: flyer-crawler production
- 3002: flyer-crawler test
- 3003: stock-alert production
- 3004: stock-alert test

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-18 12:55:39 -08:00
17 changed files with 198 additions and 33 deletions

View File

@@ -301,6 +301,10 @@ jobs:
}
"
# Save PM2 process list after cleanup to persist deletions
echo "Saving PM2 process list after cleanup..."
pm2 save
# === POST-CLEANUP VERIFICATION ===
echo "=== POST-CLEANUP VERIFICATION ==="
pm2 jlist | node -e "

View File

@@ -173,6 +173,10 @@ jobs:
}
" || true
# Save PM2 process list after cleanup to persist deletions
echo "Saving PM2 process list after cleanup..."
pm2 save || true
# === POST-CLEANUP VERIFICATION ===
echo "=== POST-CLEANUP VERIFICATION ==="
pm2 jlist 2>/dev/null | node -e "
@@ -732,6 +736,10 @@ jobs:
}
"
# Save PM2 process list after cleanup to persist deletions
echo "Saving PM2 process list after cleanup..."
pm2 save
# === POST-CLEANUP VERIFICATION ===
echo "=== POST-CLEANUP VERIFICATION ==="
pm2 jlist | node -e "

View File

@@ -58,7 +58,8 @@ jobs:
run: |
echo "Stopping PRODUCTION PM2 processes to release database connections..."
pm2 stop flyer-crawler-api flyer-crawler-worker flyer-crawler-analytics-worker || echo "Production PM2 processes were not running."
echo "✅ Production application server stopped."
pm2 save
echo "✅ Production application server stopped and saved."
- name: Step 2 - Drop and Recreate Database
run: |

View File

@@ -205,6 +205,10 @@ jobs:
}
"
# Save PM2 process list after cleanup to persist deletions
echo "Saving PM2 process list after cleanup..."
pm2 save
# === POST-CLEANUP VERIFICATION ===
echo "=== POST-CLEANUP VERIFICATION ==="
pm2 jlist | node -e "

View File

@@ -66,9 +66,9 @@ jobs:
echo "Restarting test PM2 processes to refresh version metadata..."
# Restart with --update-env to pick up new package.json version
pm2 restart flyer-crawler-api-test flyer-crawler-worker-test flyer-crawler-analytics-worker-test --update-env
pm2 restart flyer-crawler-api-test flyer-crawler-worker-test flyer-crawler-analytics-worker-test --update-env && pm2 save
echo "✅ Test PM2 processes restarted"
echo "✅ Test PM2 processes restarted and saved"
# Show current state
echo ""

View File

@@ -89,6 +89,54 @@ if (p.pm2_env.status === 'errored') {
}
```
### PM2 Save Requirement (CRITICAL)
**CRITICAL**: Every `pm2 start`, `pm2 restart`, `pm2 stop`, or `pm2 delete` command MUST be immediately followed by `pm2 save`.
Without `pm2 save`, processes become ephemeral and will disappear on:
- PM2 daemon restarts
- Server reboots
- Internal PM2 reconciliation events
**Pattern:**
```bash
# ✅ CORRECT - Save after every state change
pm2 start ecosystem.config.cjs && pm2 save
pm2 restart my-app && pm2 save
pm2 stop my-app && pm2 save
pm2 delete my-app && pm2 save
# ❌ WRONG - Missing save (processes become ephemeral)
pm2 start ecosystem.config.cjs
pm2 restart my-app
```
**In Cleanup Scripts:**
```javascript
// ✅ CORRECT - Save after cleanup loop completes
targetProcesses.forEach((p) => {
exec('pm2 delete ' + p.pm2_env.pm_id);
});
exec('pm2 save'); // Persist all deletions
// ❌ WRONG - Missing save
targetProcesses.forEach((p) => {
exec('pm2 delete ' + p.pm2_env.pm_id);
});
```
**Why This Matters:**
PM2 maintains an in-memory process list. The `pm2 save` command writes this list to `~/.pm2/dump.pm2`, which PM2 uses to resurrect processes after daemon restarts. Without it, your carefully managed process state is lost.
**See Also:**
- [ADR-014: Containerization and Deployment Strategy](docs/adr/0014-containerization-and-deployment-strategy.md)
- [ADR-061: PM2 Process Isolation Safeguards](docs/adr/0061-pm2-process-isolation-safeguards.md)
### Communication Style
Ask before assuming. Never assume:

View File

@@ -249,26 +249,39 @@ module.exports = {
### PM2 Commands Reference
**CRITICAL**: Every `pm2 start`, `pm2 restart`, `pm2 stop`, or `pm2 delete` command MUST be immediately followed by `pm2 save`. Without this, processes become ephemeral and will disappear on PM2 daemon restarts, server reboots, or internal reconciliation events.
```bash
# Start/reload with environment
# ✅ CORRECT - Start/reload with environment and save
pm2 startOrReload ecosystem.config.cjs --env production --update-env && pm2 save
# ✅ CORRECT - Restart and save
pm2 restart flyer-crawler-api && pm2 save
# ✅ CORRECT - Stop and save
pm2 stop flyer-crawler-api && pm2 save
# ✅ CORRECT - Delete and save
pm2 delete flyer-crawler-api && pm2 save
# ❌ WRONG - Missing save (processes become ephemeral)
pm2 startOrReload ecosystem.config.cjs --env production --update-env
# Save process list for startup
pm2 save
# View logs
# View logs (read-only operation, no save needed)
pm2 logs flyer-crawler-api --lines 50
# Monitor processes
# Monitor processes (read-only operation, no save needed)
pm2 monit
# List all processes
# List all processes (read-only operation, no save needed)
pm2 list
# Describe process details
# Describe process details (read-only operation, no save needed)
pm2 describe flyer-crawler-api
```
**Why This Matters**: PM2 maintains an in-memory process list. The `pm2 save` command writes this list to `~/.pm2/dump.pm2`, which PM2 uses to resurrect processes after daemon restarts. Without it, your carefully managed process state is lost.
### Resource Limits
| Process | Memory Limit | Restart Delay | Kill Timeout |

View File

@@ -115,6 +115,31 @@ echo "=== END POST-CLEANUP VERIFICATION ==="
**Purpose**: Immediately identifies cross-environment contamination.
#### Layer 6: PM2 Process List Persistence
**CRITICAL**: Save the PM2 process list after every state-changing operation:
```bash
# After any pm2 start/stop/restart/delete operation
pm2 save
# Example: After cleanup loop completes
targetProcesses.forEach(p => {
exec('pm2 delete ' + p.pm2_env.pm_id);
});
exec('pm2 save'); // Persist all deletions
```
**Purpose**: Ensures PM2 process state persists across daemon restarts, server reboots, and internal reconciliation events.
**Why This Matters**: PM2 maintains an in-memory process list. Without `pm2 save`, processes become ephemeral:
- Daemon restart → All unsaved processes disappear
- Server reboot → Process list reverts to last saved state
- PM2 internal reconciliation → Unsaved processes may be lost
**Pattern**: Every `pm2 start`, `pm2 restart`, `pm2 stop`, or `pm2 delete` MUST be followed by `pm2 save`.
## Consequences
### Positive

View File

@@ -17,15 +17,15 @@ This guide covers deploying Flyer Crawler to a production server.
### Command Reference Table
| Task | Command |
| -------------------- | ----------------------------------------------------------------------- |
| Deploy to production | Gitea Actions workflow (manual trigger) |
| Deploy to test | Automatic on push to `main` |
| Check PM2 status | `pm2 list` |
| View logs | `pm2 logs flyer-crawler-api --lines 100` |
| Restart all | `pm2 restart all` |
| Check NGINX | `sudo nginx -t && sudo systemctl status nginx` |
| Check health | `curl -s https://flyer-crawler.projectium.com/api/health/ready \| jq .` |
| Task | Command |
| -------------------- | ----------------------------------------------------------------------------------------------- |
| Deploy to production | Gitea Actions workflow (manual trigger) |
| Deploy to test | Automatic on push to `main` |
| Check PM2 status | `pm2 list` |
| View logs | `pm2 logs flyer-crawler-api --lines 100` |
| Restart all | `pm2 restart flyer-crawler-api flyer-crawler-worker flyer-crawler-analytics-worker && pm2 save` |
| Check NGINX | `sudo nginx -t && sudo systemctl status nginx` |
| Check health | `curl -s https://flyer-crawler.projectium.com/api/health/ready \| jq .` |
### Deployment URLs
@@ -274,7 +274,40 @@ sudo systemctl reload nginx
---
## PM2 Log Management
## PM2 Process Management
### Critical: Always Save After State Changes
**CRITICAL**: Every `pm2 start`, `pm2 restart`, `pm2 stop`, or `pm2 delete` command MUST be immediately followed by `pm2 save`.
Without `pm2 save`, processes become ephemeral and will disappear on:
- PM2 daemon restarts
- Server reboots
- Internal PM2 reconciliation events
**Correct Pattern:**
```bash
# ✅ CORRECT - Always save after state changes
pm2 restart flyer-crawler-api && pm2 save
pm2 stop flyer-crawler-worker && pm2 save
pm2 delete flyer-crawler-analytics-worker && pm2 save
pm2 startOrReload ecosystem.config.cjs --env production --update-env && pm2 save
# ❌ WRONG - Missing save (processes become ephemeral)
pm2 restart flyer-crawler-api
pm2 stop flyer-crawler-worker
# ✅ Read-only operations don't need save
pm2 list
pm2 logs flyer-crawler-api
pm2 monit
```
**Why This Matters**: PM2 maintains an in-memory process list. The `pm2 save` command writes this list to `~/.pm2/dump.pm2`, which PM2 uses to resurrect processes after daemon restarts. Without it, your carefully managed process state is lost.
### PM2 Log Management
Install and configure pm2-logrotate to manage log files:

View File

@@ -42,25 +42,35 @@
### Critical Commands
**IMPORTANT**: Every `pm2 start`, `pm2 restart`, `pm2 stop`, or `pm2 delete` command MUST be immediately followed by `pm2 save` to persist changes.
```bash
# Check PM2 status
# Check PM2 status (read-only, no save needed)
pm2 list
# Check specific process
# Check specific process (read-only, no save needed)
pm2 show flyer-crawler-api
# View recent logs
# View recent logs (read-only, no save needed)
pm2 logs --lines 50
# Restart specific processes (SAFE)
pm2 restart flyer-crawler-api flyer-crawler-worker flyer-crawler-analytics-worker
# Restart specific processes (SAFE - includes save)
pm2 restart flyer-crawler-api flyer-crawler-worker flyer-crawler-analytics-worker && pm2 save
# DO NOT USE (affects ALL apps)
# ✅ Start processes from config (SAFE - includes save)
pm2 startOrReload /var/www/flyer-crawler.projectium.com/ecosystem.config.cjs --update-env && pm2 save
# ❌ DO NOT USE (affects ALL apps)
# pm2 restart all <-- DANGEROUS
# pm2 stop all <-- DANGEROUS
# pm2 delete all <-- DANGEROUS
# ❌ DO NOT FORGET pm2 save after state changes
# pm2 restart flyer-crawler-api <-- WRONG: Missing save, process becomes ephemeral
```
**Why `pm2 save` Matters**: Without it, processes become ephemeral and disappear on daemon restarts, server reboots, or internal PM2 reconciliation events.
### Severity Classification
| Severity | Criteria | Response Time | Example |

View File

@@ -77,6 +77,7 @@ module.exports = {
min_uptime: '10s',
env: {
NODE_ENV: 'production',
PORT: '3001',
WORKER_LOCK_DURATION: '120000',
...sharedEnv,
},

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "flyer-crawler",
"version": "0.18.0",
"version": "0.19.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "flyer-crawler",
"version": "0.18.0",
"version": "0.19.1",
"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.18.0",
"version": "0.19.1",
"type": "module",
"engines": {
"node": ">=18.0.0"

View File

@@ -149,6 +149,9 @@ pm2 delete all 2>/dev/null || true
# Start PM2 with the dev ecosystem config
pm2 start /app/ecosystem.dev.config.cjs
# Save PM2 process list to persist across daemon restarts
pm2 save
# Show PM2 status
echo ""
echo "--- PM2 Process Status ---"

View File

@@ -108,7 +108,14 @@ describe('useAppInitialization Hook', () => {
});
it('should open "What\'s New" modal if version is new', () => {
vi.spyOn(window.localStorage, 'getItem').mockReturnValue('1.0.0');
// The hook only shows "What's New" if:
// 1. The app version differs from lastSeenVersion
// 2. The onboarding tour has been completed (flyer_crawler_onboarding_completed === 'true')
vi.spyOn(window.localStorage, 'getItem').mockImplementation((key) => {
if (key === 'lastSeenVersion') return '1.0.0';
if (key === 'flyer_crawler_onboarding_completed') return 'true';
return null;
});
renderHook(() => useAppInitialization(), { wrapper });
expect(mockOpenModal).toHaveBeenCalledWith('whatsNew');
expect(window.localStorage.setItem).toHaveBeenCalledWith('lastSeenVersion', '1.0.1');

View File

@@ -68,12 +68,18 @@ vi.mock('bullmq', () => ({
UnrecoverableError: class UnrecoverableError extends Error {},
}));
// Mock redis.server to prevent real Redis connection attempts
// Mock redis.server to prevent real Redis connection attempts.
// The workers.server.ts imports `bullmqConnection` from redis.server,
// so we must export both `connection` and `bullmqConnection` from this mock.
vi.mock('./redis.server', () => ({
connection: {
on: vi.fn(),
quit: vi.fn().mockResolvedValue(undefined),
},
bullmqConnection: {
on: vi.fn(),
quit: vi.fn().mockResolvedValue(undefined),
},
}));
// Mock queues.server to provide mock queue instances

View File

@@ -19,8 +19,10 @@ vi.mock('bullmq', () => ({
}));
// Mock our internal redis connection module to export our mock connection object.
// The queues.server.ts imports `bullmqConnection` (aliased as `connection`),
// so we must export `bullmqConnection` from this mock.
vi.mock('./redis.server', () => ({
connection: mocks.mockConnection,
bullmqConnection: mocks.mockConnection,
}));
describe('Queue Definitions', () => {