IO-3166-Global-Notifications-Part-2 - Checkpoint
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Badge, Layout, Menu, Spin } from "antd";
|
import { Badge, Layout, Menu, Spin } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@@ -95,7 +95,8 @@ function Header({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isConnected, scenarioNotificationsOn } = useSocket();
|
const { isConnected, scenarioNotificationsOn } = useSocket();
|
||||||
const [notificationVisible, setNotificationVisible] = useState(false);
|
const [notificationVisible, setNotificationVisible] = useState(false);
|
||||||
|
const baseTitleRef = useRef(document.title || "");
|
||||||
|
const lastSetTitleRef = useRef("");
|
||||||
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -123,6 +124,40 @@ function Header({
|
|||||||
}
|
}
|
||||||
}, [isConnected, unreadLoading, refetchUnread, userAssociationId]);
|
}, [isConnected, unreadLoading, refetchUnread, userAssociationId]);
|
||||||
|
|
||||||
|
// Keep The unread count in the title.
|
||||||
|
useEffect(() => {
|
||||||
|
const updateTitle = () => {
|
||||||
|
const currentTitle = document.title;
|
||||||
|
// Check if the current title differs from what we last set
|
||||||
|
if (currentTitle !== lastSetTitleRef.current) {
|
||||||
|
// Extract base title by removing any unread count prefix
|
||||||
|
const baseTitleMatch = currentTitle.match(/^\(\d+\)\s*(.*)$/);
|
||||||
|
baseTitleRef.current = baseTitleMatch ? baseTitleMatch[1] : currentTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply unread count to the base title
|
||||||
|
const newTitle = unreadCount > 0 ? `(${unreadCount}) ${baseTitleRef.current}` : baseTitleRef.current;
|
||||||
|
|
||||||
|
// Only update if the title has changed to avoid unnecessary DOM writes
|
||||||
|
if (document.title !== newTitle) {
|
||||||
|
document.title = newTitle;
|
||||||
|
lastSetTitleRef.current = newTitle; // Store what we set
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial update
|
||||||
|
updateTitle();
|
||||||
|
|
||||||
|
// Poll every 100ms to catch child component changes
|
||||||
|
const interval = setInterval(updateTitle, 100);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
document.title = baseTitleRef.current; // Reset to base title on unmount
|
||||||
|
};
|
||||||
|
}, [unreadCount]); // Re-run when unreadCount changes
|
||||||
|
|
||||||
const handleNotificationClick = (e) => {
|
const handleNotificationClick = (e) => {
|
||||||
setNotificationVisible(!notificationVisible);
|
setNotificationVisible(!notificationVisible);
|
||||||
if (handleMenuClick) handleMenuClick(e);
|
if (handleMenuClick) handleMenuClick(e);
|
||||||
@@ -656,7 +691,7 @@ function Header({
|
|||||||
icon: unreadLoading ? (
|
icon: unreadLoading ? (
|
||||||
<Spin size="small" />
|
<Spin size="small" />
|
||||||
) : (
|
) : (
|
||||||
<Badge size="small" count={unreadCount}>
|
<Badge offset={[8, 0]} size="small" count={unreadCount}>
|
||||||
<BellFilled />
|
<BellFilled />
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { Alert, Badge, Button, Space, Spin, Switch, Tooltip, Typography } from "antd";
|
import { Badge, Button, Space, Spin, Switch, Tooltip, Typography } from "antd";
|
||||||
import { CheckCircleFilled, CheckCircleOutlined, EyeFilled, EyeOutlined } from "@ant-design/icons";
|
import { CheckCircleFilled, CheckCircleOutlined, EyeFilled, EyeOutlined } from "@ant-design/icons";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import "./notification-center.styles.scss";
|
import "./notification-center.styles.scss";
|
||||||
import day from "../../utils/day.js";
|
import day from "../../utils/day.js";
|
||||||
import { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
|
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
|
|
||||||
@@ -52,11 +53,7 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
<span className="ro-number">
|
<span className="ro-number">
|
||||||
{t("notifications.labels.ro-number", { ro_number: notification.roNumber })}
|
{t("notifications.labels.ro-number", { ro_number: notification.roNumber })}
|
||||||
</span>
|
</span>
|
||||||
<Text
|
<Text type="secondary" className="relative-time" title={DateTimeFormat(notification.created_at)}>
|
||||||
type="secondary"
|
|
||||||
className="relative-time"
|
|
||||||
title={day(notification.created_at).format("YYYY-MM-DD hh:mm A")}
|
|
||||||
>
|
|
||||||
{day(notification.created_at).fromNow()}
|
{day(notification.created_at).fromNow()}
|
||||||
</Text>
|
</Text>
|
||||||
</Title>
|
</Title>
|
||||||
@@ -83,7 +80,6 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
<div className="notification-controls">
|
<div className="notification-controls">
|
||||||
<Tooltip title={t("notifications.labels.show-unread-only")}>
|
<Tooltip title={t("notifications.labels.show-unread-only")}>
|
||||||
<Space size={4} align="center" className="notification-toggle">
|
<Space size={4} align="center" className="notification-toggle">
|
||||||
{" "}
|
|
||||||
{showUnreadOnly ? (
|
{showUnreadOnly ? (
|
||||||
<EyeFilled className="notification-toggle-icon" />
|
<EyeFilled className="notification-toggle-icon" />
|
||||||
) : (
|
) : (
|
||||||
@@ -94,7 +90,7 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t("notifications.labels.mark-all-read")}>
|
<Tooltip title={t("notifications.labels.mark-all-read")}>
|
||||||
<Button
|
<Button
|
||||||
type={!unreadCount ? "default" : "primary"}
|
type="link"
|
||||||
icon={!unreadCount ? <CheckCircleFilled /> : <CheckCircleOutlined />}
|
icon={!unreadCount ? <CheckCircleFilled /> : <CheckCircleOutlined />}
|
||||||
onClick={markAllRead}
|
onClick={markAllRead}
|
||||||
disabled={!unreadCount}
|
disabled={!unreadCount}
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import {
|
|||||||
GET_NOTIFICATIONS,
|
GET_NOTIFICATIONS,
|
||||||
GET_UNREAD_COUNT,
|
GET_UNREAD_COUNT,
|
||||||
MARK_ALL_NOTIFICATIONS_READ,
|
MARK_ALL_NOTIFICATIONS_READ,
|
||||||
MARK_NOTIFICATION_READ
|
MARK_NOTIFICATION_READ,
|
||||||
|
UPDATE_NOTIFICATIONS_READ_FRAGMENT
|
||||||
} from "../../graphql/notifications.queries.js";
|
} from "../../graphql/notifications.queries.js";
|
||||||
import { gql, useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const SocketContext = createContext(null);
|
const SocketContext = createContext(null);
|
||||||
@@ -290,7 +291,17 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot
|
|||||||
});
|
});
|
||||||
|
|
||||||
notification.info({
|
notification.info({
|
||||||
message: t("notifications.labels.notification-popup-title", { ro_number: jobRoNumber }),
|
message: (
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
markNotificationRead({ variables: { id: notificationId } })
|
||||||
|
.then(() => navigate(`/manage/jobs/${jobId}`))
|
||||||
|
.catch((e) => console.error(`Error marking notification read: ${e?.message || ""}`));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("notifications.labels.notification-popup-title", { ro_number: jobRoNumber })}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
description: (
|
description: (
|
||||||
<ul
|
<ul
|
||||||
className="notification-alert-unordered-list"
|
className="notification-alert-unordered-list"
|
||||||
@@ -327,11 +338,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot
|
|||||||
});
|
});
|
||||||
client.cache.writeFragment({
|
client.cache.writeFragment({
|
||||||
id: notificationRef,
|
id: notificationRef,
|
||||||
fragment: gql`
|
fragment: UPDATE_NOTIFICATIONS_READ_FRAGMENT,
|
||||||
fragment UpdateNotificationRead on notifications {
|
|
||||||
read
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
data: { read: timestamp }
|
data: { read: timestamp }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -383,11 +390,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot
|
|||||||
const notifRef = client.cache.identify({ __typename: "notifications", id: notif.id });
|
const notifRef = client.cache.identify({ __typename: "notifications", id: notif.id });
|
||||||
client.cache.writeFragment({
|
client.cache.writeFragment({
|
||||||
id: notifRef,
|
id: notifRef,
|
||||||
fragment: gql`
|
fragment: UPDATE_NOTIFICATIONS_READ_FRAGMENT,
|
||||||
fragment UpdateNotificationRead on notifications {
|
|
||||||
read
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
data: { read: timestamp }
|
data: { read: timestamp }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,3 +50,9 @@ export const MARK_NOTIFICATION_READ = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const UPDATE_NOTIFICATIONS_READ_FRAGMENT = gql`
|
||||||
|
fragment UpdateNotificationRead on notifications {
|
||||||
|
read
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
223
docker-compose-cluster.yml
Normal file
223
docker-compose-cluster.yml
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Load Balancer (NGINX) with WebSocket support and session persistence
|
||||||
|
load-balancer:
|
||||||
|
image: nginx:latest
|
||||||
|
container_name: load-balancer
|
||||||
|
ports:
|
||||||
|
- "4000:80" # External port 4000 maps to NGINX's port 80
|
||||||
|
volumes:
|
||||||
|
- ./nginx-websocket.conf:/etc/nginx/nginx.conf:ro # Mount NGINX configuration
|
||||||
|
networks:
|
||||||
|
- redis-cluster-net
|
||||||
|
depends_on:
|
||||||
|
- node-app-1
|
||||||
|
- node-app-2
|
||||||
|
- node-app-3
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "curl", "-f", "http://localhost/health" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Node App Instance 1
|
||||||
|
node-app-1:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
container_name: node-app-1
|
||||||
|
hostname: node-app-1
|
||||||
|
networks:
|
||||||
|
- redis-cluster-net
|
||||||
|
env_file:
|
||||||
|
- .env.development
|
||||||
|
depends_on:
|
||||||
|
redis-node-1:
|
||||||
|
condition: service_healthy
|
||||||
|
redis-node-2:
|
||||||
|
condition: service_healthy
|
||||||
|
redis-node-3:
|
||||||
|
condition: service_healthy
|
||||||
|
localstack:
|
||||||
|
condition: service_healthy
|
||||||
|
aws-cli:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
ports:
|
||||||
|
- "4001:4000" # Different external port for local access
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- node-app-npm-cache:/app/node_modules
|
||||||
|
|
||||||
|
# Node App Instance 2
|
||||||
|
node-app-2:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
container_name: node-app-2
|
||||||
|
hostname: node-app-2
|
||||||
|
networks:
|
||||||
|
- redis-cluster-net
|
||||||
|
env_file:
|
||||||
|
- .env.development
|
||||||
|
depends_on:
|
||||||
|
redis-node-1:
|
||||||
|
condition: service_healthy
|
||||||
|
redis-node-2:
|
||||||
|
condition: service_healthy
|
||||||
|
redis-node-3:
|
||||||
|
condition: service_healthy
|
||||||
|
localstack:
|
||||||
|
condition: service_healthy
|
||||||
|
aws-cli:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
ports:
|
||||||
|
- "4002:4000" # Different external port for local access
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- node-app-npm-cache:/app/node_modules
|
||||||
|
|
||||||
|
# Node App Instance 3
|
||||||
|
node-app-3:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
container_name: node-app-3
|
||||||
|
hostname: node-app-3
|
||||||
|
networks:
|
||||||
|
- redis-cluster-net
|
||||||
|
env_file:
|
||||||
|
- .env.development
|
||||||
|
depends_on:
|
||||||
|
redis-node-1:
|
||||||
|
condition: service_healthy
|
||||||
|
redis-node-2:
|
||||||
|
condition: service_healthy
|
||||||
|
redis-node-3:
|
||||||
|
condition: service_healthy
|
||||||
|
localstack:
|
||||||
|
condition: service_healthy
|
||||||
|
aws-cli:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
ports:
|
||||||
|
- "4003:4000" # Different external port for local access
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- node-app-npm-cache:/app/node_modules
|
||||||
|
|
||||||
|
# Redis Node 1
|
||||||
|
redis-node-1:
|
||||||
|
build:
|
||||||
|
context: ./redis
|
||||||
|
container_name: redis-node-1
|
||||||
|
hostname: redis-node-1
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- redis-cluster-net
|
||||||
|
volumes:
|
||||||
|
- redis-node-1-data:/data
|
||||||
|
- redis-lock:/redis-lock
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "redis-cli", "ping" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
# Redis Node 2
|
||||||
|
redis-node-2:
|
||||||
|
build:
|
||||||
|
context: ./redis
|
||||||
|
container_name: redis-node-2
|
||||||
|
hostname: redis-node-2
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- redis-cluster-net
|
||||||
|
volumes:
|
||||||
|
- redis-node-2-data:/data
|
||||||
|
- redis-lock:/redis-lock
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "redis-cli", "ping" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
# Redis Node 3
|
||||||
|
redis-node-3:
|
||||||
|
build:
|
||||||
|
context: ./redis
|
||||||
|
container_name: redis-node-3
|
||||||
|
hostname: redis-node-3
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- redis-cluster-net
|
||||||
|
volumes:
|
||||||
|
- redis-node-3-data:/data
|
||||||
|
- redis-lock:/redis-lock
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "redis-cli", "ping" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
# LocalStack
|
||||||
|
localstack:
|
||||||
|
image: localstack/localstack
|
||||||
|
container_name: localstack
|
||||||
|
hostname: localstack
|
||||||
|
networks:
|
||||||
|
- redis-cluster-net
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
- SERVICES=s3,ses,secretsmanager,cloudwatch,logs
|
||||||
|
- DEBUG=0
|
||||||
|
- AWS_ACCESS_KEY_ID=test
|
||||||
|
- AWS_SECRET_ACCESS_KEY=test
|
||||||
|
- AWS_DEFAULT_REGION=ca-central-1
|
||||||
|
- EXTRA_CORS_ALLOWED_HEADERS=Authorization,Content-Type
|
||||||
|
- EXTRA_CORS_ALLOWED_ORIGINS=*
|
||||||
|
- EXTRA_CORS_EXPOSE_HEADERS=Authorization,Content-Type
|
||||||
|
ports:
|
||||||
|
- "4566:4566"
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "curl", "-f", "http://localhost:4566/_localstack/health" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 20s
|
||||||
|
|
||||||
|
# AWS-CLI
|
||||||
|
aws-cli:
|
||||||
|
image: amazon/aws-cli
|
||||||
|
container_name: aws-cli
|
||||||
|
hostname: aws-cli
|
||||||
|
networks:
|
||||||
|
- redis-cluster-net
|
||||||
|
depends_on:
|
||||||
|
localstack:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- './localstack:/tmp/localstack'
|
||||||
|
- './certs:/tmp/certs'
|
||||||
|
environment:
|
||||||
|
- AWS_ACCESS_KEY_ID=test
|
||||||
|
- AWS_SECRET_ACCESS_KEY=test
|
||||||
|
- AWS_DEFAULT_REGION=ca-central-1
|
||||||
|
entrypoint: /bin/sh -c
|
||||||
|
command: >
|
||||||
|
"
|
||||||
|
aws --endpoint-url=http://localstack:4566 ses verify-domain-identity --domain imex.online --region ca-central-1
|
||||||
|
aws --endpoint-url=http://localstack:4566 ses verify-email-identity --email-address noreply@imex.online --region ca-central-1
|
||||||
|
aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/io-ftp-test.key
|
||||||
|
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
|
||||||
|
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1
|
||||||
|
"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
redis-cluster-net:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
node-app-npm-cache:
|
||||||
|
redis-node-1-data:
|
||||||
|
redis-node-2-data:
|
||||||
|
redis-node-3-data:
|
||||||
|
redis-lock:
|
||||||
@@ -198,6 +198,14 @@
|
|||||||
- name: user
|
- name: user
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on: useremail
|
foreign_key_constraint_on: useremail
|
||||||
|
array_relationships:
|
||||||
|
- name: notifications
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: associationid
|
||||||
|
table:
|
||||||
|
name: notifications
|
||||||
|
schema: public
|
||||||
select_permissions:
|
select_permissions:
|
||||||
- role: user
|
- role: user
|
||||||
permission:
|
permission:
|
||||||
@@ -3484,6 +3492,13 @@
|
|||||||
table:
|
table:
|
||||||
name: notes
|
name: notes
|
||||||
schema: public
|
schema: public
|
||||||
|
- name: notifications
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: jobid
|
||||||
|
table:
|
||||||
|
name: notifications
|
||||||
|
schema: public
|
||||||
- name: parts_dispatches
|
- name: parts_dispatches
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on:
|
foreign_key_constraint_on:
|
||||||
@@ -6732,6 +6747,13 @@
|
|||||||
table:
|
table:
|
||||||
name: ioevents
|
name: ioevents
|
||||||
schema: public
|
schema: public
|
||||||
|
- name: job_watchers
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: user_email
|
||||||
|
table:
|
||||||
|
name: job_watchers
|
||||||
|
schema: public
|
||||||
- name: messages
|
- name: messages
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on:
|
foreign_key_constraint_on:
|
||||||
|
|||||||
45
nginx-websocket.conf
Normal file
45
nginx-websocket.conf
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
upstream node_app {
|
||||||
|
ip_hash; # Enables session persistence based on client IP
|
||||||
|
server node-app-1:4000;
|
||||||
|
server node-app-2:4000;
|
||||||
|
server node-app-3:4000;
|
||||||
|
}
|
||||||
|
|
||||||
|
# WebSocket upgrade configuration
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://node_app;
|
||||||
|
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;
|
||||||
|
|
||||||
|
# WebSocket headers
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_read_timeout 86400; # Keep WebSocket connections alive (24 hours)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
location /health {
|
||||||
|
proxy_pass http://node_app;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2758,6 +2758,7 @@ exports.INSERT_NOTIFICATIONS_MUTATION = ` mutation INSERT_NOTIFICATIONS($object
|
|||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
// REMEMBER: Update the cache_bodyshop event in hasura to include any added fields
|
||||||
exports.GET_BODYSHOP_BY_ID = `
|
exports.GET_BODYSHOP_BY_ID = `
|
||||||
query GET_BODYSHOP_BY_ID($id: uuid!) {
|
query GET_BODYSHOP_BY_ID($id: uuid!) {
|
||||||
bodyshops_by_pk(id: $id) {
|
bodyshops_by_pk(id: $id) {
|
||||||
|
|||||||
@@ -142,4 +142,17 @@ router.post("/alertcheck", eventAuthorizationMiddleware, alertCheck);
|
|||||||
// Redis Cache Routes
|
// Redis Cache Routes
|
||||||
router.post("/bodyshop-cache", eventAuthorizationMiddleware, updateBodyshopCache);
|
router.post("/bodyshop-cache", eventAuthorizationMiddleware, updateBodyshopCache);
|
||||||
|
|
||||||
|
// Health Check for docker-compose-cluster load balancer, only available in development
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
router.get("/health", (req, res) => {
|
||||||
|
const healthStatus = {
|
||||||
|
status: "healthy",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
environment: process.env.NODE_ENV || "unknown",
|
||||||
|
uptime: process.uptime()
|
||||||
|
};
|
||||||
|
res.status(200).json(healthStatus);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
Reference in New Issue
Block a user