Compare commits

...

10 Commits

Author SHA1 Message Date
Gitea Actions
dd31141d4e ci: Bump version to 0.9.109 [skip ci] 2026-01-13 23:09:47 +05:00
8073094760 testing/staging fixin
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m15s
2026-01-13 10:08:28 -08:00
Gitea Actions
33a1e146ab ci: Bump version to 0.9.108 [skip ci] 2026-01-13 22:34:20 +05:00
4f8216db77 testing/staging fixin
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 15m55s
2026-01-13 09:33:38 -08:00
Gitea Actions
42d605d19f ci: Bump version to 0.9.107 [skip ci] 2026-01-13 22:06:39 +05:00
749350df7f testing/staging fixin
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 15m56s
2026-01-13 09:03:42 -08:00
Gitea Actions
ac085100fe ci: Bump version to 0.9.106 [skip ci] 2026-01-13 21:43:43 +05:00
ce4ecd1268 use port 3002 in test
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m13s
2026-01-13 08:42:34 -08:00
Gitea Actions
a57cfc396b ci: Bump version to 0.9.105 [skip ci] 2026-01-13 21:00:45 +05:00
987badbf8d use port 3002 in test
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 15m41s
2026-01-13 07:59:49 -08:00
12 changed files with 122 additions and 20 deletions

View File

@@ -94,7 +94,8 @@
"mcp__filesystem__edit_file",
"Bash(timeout 300 tail:*)",
"mcp__filesystem__list_allowed_directories",
"mcp__memory__add_observations"
"mcp__memory__add_observations",
"Bash(ssh:*)"
]
}
}

1
.gitignore vendored
View File

@@ -37,3 +37,4 @@ test-output.txt
Thumbs.db
.claude
nul
tmpclaude*

View File

@@ -71,7 +71,8 @@ module.exports = {
exp_backoff_restart_delay: 100,
min_uptime: '10s',
env: {
NODE_ENV: 'test',
NODE_ENV: 'staging',
PORT: 3002,
WORKER_LOCK_DURATION: '120000',
...sharedEnv,
},
@@ -89,7 +90,7 @@ module.exports = {
exp_backoff_restart_delay: 100,
min_uptime: '10s',
env: {
NODE_ENV: 'test',
NODE_ENV: 'staging',
...sharedEnv,
},
},
@@ -106,7 +107,7 @@ module.exports = {
exp_backoff_restart_delay: 100,
min_uptime: '10s',
env: {
NODE_ENV: 'test',
NODE_ENV: 'staging',
...sharedEnv,
},
},

View File

@@ -0,0 +1,69 @@
# HTTPS Server Block (main)
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name flyer-crawler-test.projectium.com;
# SSL Configuration (managed by Certbot)
ssl_certificate /etc/letsencrypt/live/flyer-crawler-test.projectium.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/flyer-crawler-test.projectium.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Allow large file uploads (e.g., for flyers)
client_max_body_size 100M;
# Root directory for built application files
root /var/www/flyer-crawler-test.projectium.com;
index index.html;
# Deny access to all dotfiles
location ~ /\. {
deny all;
return 404;
}
# Coverage report (must come before generic location /)
location /coverage/ {
try_files $uri $uri/ =404;
}
# SPA fallback for React Router
location / {
try_files $uri $uri/ /index.html;
}
# Reverse proxy for backend API
location /api/ {
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_pass http://localhost:3002;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
# Correct MIME type for .mjs files
location ~ \.mjs$ {
include /etc/nginx/mime.types;
default_type application/javascript;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
}
# HTTP to HTTPS Redirect
server {
listen 80;
listen [::]:80;
server_name flyer-crawler-test.projectium.com;
return 301 https://$host$request_uri;
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "flyer-crawler",
"version": "0.9.104",
"version": "0.9.109",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "flyer-crawler",
"version": "0.9.104",
"version": "0.9.109",
"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.9.104",
"version": "0.9.109",
"type": "module",
"scripts": {
"dev": "concurrently \"npm:start:dev\" \"vite\"",

View File

@@ -128,7 +128,7 @@ const workerSchema = z.object({
* Server configuration schema.
*/
const serverSchema = z.object({
nodeEnv: z.enum(['development', 'production', 'test']).default('development'),
nodeEnv: z.enum(['development', 'production', 'test', 'staging']).default('development'),
port: intWithDefault(3001),
frontendUrl: z.string().url().optional(),
baseUrl: z.string().optional(),
@@ -262,8 +262,9 @@ function parseConfig(): EnvConfig {
'',
].join('\n');
// In test environment, throw instead of exiting to allow test frameworks to catch
if (process.env.NODE_ENV === 'test') {
// In test/staging environment, throw instead of exiting to allow test frameworks to catch
// and to provide better visibility into config errors during staging deployments
if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'staging') {
throw new Error(errorMessage);
}
@@ -318,6 +319,24 @@ export const isTest = config.server.nodeEnv === 'test';
*/
export const isDevelopment = config.server.nodeEnv === 'development';
/**
* Returns true if running in staging environment.
*/
export const isStaging = config.server.nodeEnv === 'staging';
/**
* Returns true if running in a test-like environment (test or staging).
* Use this for behaviors that should be shared between unit/integration tests
* and the staging deployment server, such as:
* - Using mock AI services (no GEMINI_API_KEY required)
* - Verbose error logging
* - Fallback URL handling
*
* Do NOT use this for security bypasses (auth, rate limiting) - those should
* only be active in NODE_ENV=test, not staging.
*/
export const isTestLikeEnvironment = isTest || isStaging;
/**
* Returns true if SMTP is configured (all required fields present).
*/

View File

@@ -161,9 +161,12 @@ export const errorHandler = (err: Error, req: Request, res: Response, next: Next
`Unhandled API Error (ID: ${errorId})`,
);
// Also log to console in test environment for visibility in test runners
if (process.env.NODE_ENV === 'test') {
console.error(`--- [TEST] UNHANDLED ERROR (ID: ${errorId}) ---`, err);
// Also log to console in test/staging environments for visibility in test runners
if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'staging') {
console.error(
`--- [${process.env.NODE_ENV?.toUpperCase()}] UNHANDLED ERROR (ID: ${errorId}) ---`,
err,
);
}
// In production, send a generic message to avoid leaking implementation details.

View File

@@ -239,10 +239,13 @@ router.post(
'Handling /upload-and-process',
);
// Fix: Explicitly clear userProfile if no auth header is present in test env
// Fix: Explicitly clear userProfile if no auth header is present in test/staging env
// This prevents mockAuth from injecting a non-existent user ID for anonymous requests.
let userProfile = req.user as UserProfile | undefined;
if (process.env.NODE_ENV === 'test' && !req.headers['authorization']) {
if (
(process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'staging') &&
!req.headers['authorization']
) {
userProfile = undefined;
}

View File

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

View File

@@ -19,7 +19,8 @@ import path from 'path';
const isProduction = process.env.NODE_ENV === 'production';
const isTest = process.env.NODE_ENV === 'test';
const isDevelopment = !isProduction && !isTest;
const isStaging = process.env.NODE_ENV === 'staging';
const isDevelopment = !isProduction && !isTest && !isStaging;
// Determine log directory based on environment
// In production/test, use the application directory's logs folder

View File

@@ -15,9 +15,9 @@ export function getBaseUrl(logger: Logger): string {
let baseUrl = (process.env.FRONTEND_URL || process.env.BASE_URL || '').trim();
if (!baseUrl || !baseUrl.startsWith('http')) {
const port = process.env.PORT || 3000;
// In test/development, use http://localhost. In production, this should never be reached.
// In test/staging/development, use http://localhost. In production, this should never be reached.
const fallbackUrl =
process.env.NODE_ENV === 'test'
process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'staging'
? `http://localhost:${port}`
: `http://example.com:${port}`;
if (baseUrl) {
@@ -39,4 +39,4 @@ export function getBaseUrl(logger: Logger): string {
}
return finalUrl;
}
}