Feature: Add type-based color coding and optimize for TV display
- Extended time window from 48 hours to 72 hours - Added isshopfloor filter - only show notifications marked for shopfloor - Added JOIN with notificationtypes table to get type colors - Implemented type-based color coding with 40px thick left border: * Green (#0ad64f) for Awareness and TBD types * Yellow (#ffc107) for Change type * Red (#dc3545) for Incident type - Optimized layout for single-screen TV display (no scrolling): * Reduced all font sizes and spacing significantly * Set overflow: hidden and height: 100vh on body * Reduced header, section titles, event cards, and footer sizes - Limited display to 3 current + 3 upcoming events max - Shows "+ X more event(s)" indicator when needed - Positioned LIVE badge in absolute top-right corner - Updated all text references from 48 hours to 72 hours 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -18,24 +18,25 @@
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #00003d; /* GE Aerospace Deep Navy */
|
||||
color: #fff;
|
||||
overflow-x: hidden;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 50px 60px;
|
||||
padding-bottom: 150px;
|
||||
padding: 20px 40px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 60px;
|
||||
margin-bottom: 60px;
|
||||
border-bottom: 4px solid #4181ff; /* GE Sky Blue */
|
||||
padding-bottom: 40px;
|
||||
gap: 30px;
|
||||
margin-bottom: 25px;
|
||||
border-bottom: 3px solid #4181ff; /* GE Sky Blue */
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
@@ -44,7 +45,7 @@
|
||||
}
|
||||
|
||||
.logo-container img {
|
||||
height: 160px;
|
||||
height: 90px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@@ -52,27 +53,27 @@
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.location-title {
|
||||
font-size: 32px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #eaeaea; /* GE Tungsten */
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.header-center h1 {
|
||||
font-size: 72px;
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
letter-spacing: 2px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.clock {
|
||||
font-size: 48px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
color: #4181ff; /* GE Sky Blue */
|
||||
@@ -82,16 +83,16 @@
|
||||
|
||||
.connection-status {
|
||||
position: fixed;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
padding: 20px 35px;
|
||||
border-radius: 10px;
|
||||
font-size: 28px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 12px 20px;
|
||||
border-radius: 0 0 0 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
gap: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
@@ -112,22 +113,22 @@
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #00003d;
|
||||
}
|
||||
|
||||
.events-section {
|
||||
margin-bottom: 60px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 64px;
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 40px;
|
||||
padding: 25px 40px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 15px;
|
||||
padding: 12px 25px;
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
@@ -148,11 +149,11 @@
|
||||
.event-card {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
padding: 50px 60px;
|
||||
margin-bottom: 35px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.15);
|
||||
border-left: 15px solid;
|
||||
padding: 20px 30px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
border-left: 40px solid;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@@ -178,12 +179,12 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
gap: 30px;
|
||||
margin-bottom: 12px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.event-title {
|
||||
font-size: 56px;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
flex: 1;
|
||||
color: #00003d; /* GE Deep Navy for text */
|
||||
@@ -191,22 +192,22 @@
|
||||
}
|
||||
|
||||
.event-ticket {
|
||||
font-size: 48px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
background: #00003d; /* GE Deep Navy */
|
||||
color: #fff;
|
||||
padding: 15px 35px;
|
||||
border-radius: 10px;
|
||||
padding: 8px 20px;
|
||||
border-radius: 6px;
|
||||
white-space: nowrap;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.event-time {
|
||||
font-size: 38px;
|
||||
font-size: 22px;
|
||||
color: #666;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.event-time strong {
|
||||
@@ -216,12 +217,12 @@
|
||||
|
||||
.no-events {
|
||||
text-align: center;
|
||||
font-size: 52px;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
padding: 120px 60px;
|
||||
padding: 40px 30px;
|
||||
background: rgba(234, 234, 234, 0.1);
|
||||
border-radius: 12px;
|
||||
margin-top: 50px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
color: #eaeaea; /* GE Tungsten */
|
||||
}
|
||||
|
||||
@@ -232,31 +233,43 @@
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
text-align: center;
|
||||
padding: 25px;
|
||||
font-size: 32px;
|
||||
padding: 12px;
|
||||
font-size: 18px;
|
||||
z-index: 999;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
font-size: 52px;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
padding: 120px 60px;
|
||||
padding: 40px 30px;
|
||||
background: rgba(234, 234, 234, 0.1);
|
||||
border-radius: 12px;
|
||||
margin-top: 50px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
padding: 60px;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 48px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin: 50px 0;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.more-events {
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
padding: 15px;
|
||||
margin-top: 10px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
color: #eaeaea;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -338,14 +351,20 @@
|
||||
const container = document.getElementById('eventsContainer');
|
||||
let html = '';
|
||||
|
||||
// Current Events
|
||||
// Limit display to fit on TV screen without scrolling
|
||||
const MAX_CURRENT = 3;
|
||||
const MAX_UPCOMING = 3;
|
||||
|
||||
// Current Events (limit to MAX_CURRENT)
|
||||
if (data.current && data.current.length > 0) {
|
||||
html += '<div class="events-section">';
|
||||
html += '<div class="section-title current">🔴 CURRENT EVENTS</div>';
|
||||
|
||||
data.current.forEach(event => {
|
||||
const currentToShow = data.current.slice(0, MAX_CURRENT);
|
||||
currentToShow.forEach(event => {
|
||||
const borderColor = getColorFromType(event.typecolor);
|
||||
html += `
|
||||
<div class="event-card current">
|
||||
<div class="event-card current" style="border-left-color: ${borderColor};">
|
||||
<div class="event-header">
|
||||
<div class="event-title">${escapeHtml(event.notification)}</div>
|
||||
${event.ticketnumber ? `<div class="event-ticket">Ticket: ${escapeHtml(event.ticketnumber)}</div>` : ''}
|
||||
@@ -358,17 +377,24 @@
|
||||
`;
|
||||
});
|
||||
|
||||
// Show indicator if there are more events
|
||||
if (data.current.length > MAX_CURRENT) {
|
||||
html += `<div class="more-events">+ ${data.current.length - MAX_CURRENT} more current event(s)</div>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Upcoming Events
|
||||
// Upcoming Events (limit to MAX_UPCOMING)
|
||||
if (data.upcoming && data.upcoming.length > 0) {
|
||||
html += '<div class="events-section">';
|
||||
html += '<div class="section-title upcoming">⚠️ UPCOMING (Next 48 Hours)</div>';
|
||||
html += '<div class="section-title upcoming">⚠️ UPCOMING (Next 72 Hours)</div>';
|
||||
|
||||
data.upcoming.forEach(event => {
|
||||
const upcomingToShow = data.upcoming.slice(0, MAX_UPCOMING);
|
||||
upcomingToShow.forEach(event => {
|
||||
const borderColor = getColorFromType(event.typecolor);
|
||||
html += `
|
||||
<div class="event-card upcoming">
|
||||
<div class="event-card upcoming" style="border-left-color: ${borderColor};">
|
||||
<div class="event-header">
|
||||
<div class="event-title">${escapeHtml(event.notification)}</div>
|
||||
${event.ticketnumber ? `<div class="event-ticket">Ticket: ${escapeHtml(event.ticketnumber)}</div>` : ''}
|
||||
@@ -381,12 +407,17 @@
|
||||
`;
|
||||
});
|
||||
|
||||
// Show indicator if there are more events
|
||||
if (data.upcoming.length > MAX_UPCOMING) {
|
||||
html += `<div class="more-events">+ ${data.upcoming.length - MAX_UPCOMING} more upcoming event(s)</div>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// No events message
|
||||
if ((!data.current || data.current.length === 0) && (!data.upcoming || data.upcoming.length === 0)) {
|
||||
html += '<div class="no-events">✅ No scheduled events for the next 48 hours</div>';
|
||||
html += '<div class="no-events">✅ No scheduled events for the next 72 hours</div>';
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
@@ -399,6 +430,17 @@
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Map Bootstrap color classes to hex colors
|
||||
function getColorFromType(typecolor) {
|
||||
const colorMap = {
|
||||
'success': '#0ad64f', // Green (GE Avionics Green) - Awareness, TBD
|
||||
'warning': '#ffc107', // Yellow - Change
|
||||
'danger': '#dc3545', // Red - Incident
|
||||
'secondary': '#6c757d' // Gray - fallback
|
||||
};
|
||||
return colorMap[typecolor] || colorMap['secondary'];
|
||||
}
|
||||
|
||||
// Fetch notifications from API
|
||||
async function fetchNotifications() {
|
||||
try {
|
||||
@@ -428,7 +470,7 @@
|
||||
container.innerHTML = `
|
||||
<div class="error-message">
|
||||
⚠️ Unable to load events<br>
|
||||
<span style="font-size: 36px; margin-top: 20px; display: block;">Retrying...</span>
|
||||
<span style="font-size: 20px; margin-top: 10px; display: block;">Retrying...</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
17
server.js
17
server.js
@@ -27,17 +27,20 @@ app.use(express.static('public'));
|
||||
app.get('/api/notifications', async (req, res) => {
|
||||
try {
|
||||
const now = new Date();
|
||||
const future = new Date(now.getTime() + (48 * 60 * 60 * 1000)); // 48 hours from now
|
||||
const future = new Date(now.getTime() + (72 * 60 * 60 * 1000)); // 72 hours from now
|
||||
|
||||
const [rows] = await promisePool.query(
|
||||
`SELECT notificationid, notification, starttime, endtime, ticketnumber, link, isactive
|
||||
FROM notifications
|
||||
WHERE isactive = 1
|
||||
`SELECT n.notificationid, n.notification, n.starttime, n.endtime, n.ticketnumber, n.link,
|
||||
n.isactive, n.isshopfloor, nt.typename, nt.typecolor
|
||||
FROM notifications n
|
||||
LEFT JOIN notificationtypes nt ON n.notificationtypeid = nt.notificationtypeid
|
||||
WHERE n.isactive = 1
|
||||
AND n.isshopfloor = 1
|
||||
AND (
|
||||
(starttime <= ? AND (endtime IS NULL OR endtime >= ?))
|
||||
OR (starttime BETWEEN ? AND ?)
|
||||
(n.starttime <= ? AND (n.endtime IS NULL OR n.endtime >= ?))
|
||||
OR (n.starttime BETWEEN ? AND ?)
|
||||
)
|
||||
ORDER BY starttime ASC`,
|
||||
ORDER BY n.starttime ASC`,
|
||||
[future, now, now, future]
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user