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:
cproudlock
2025-10-24 09:46:43 -04:00
parent 81cb74cdef
commit ca46c9a404
2 changed files with 115 additions and 70 deletions

View File

@@ -18,24 +18,25 @@
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #00003d; /* GE Aerospace Deep Navy */ background: #00003d; /* GE Aerospace Deep Navy */
color: #fff; color: #fff;
overflow-x: hidden; overflow: hidden;
height: 100vh;
} }
.container { .container {
max-width: 100%; max-width: 100%;
margin: 0 auto; margin: 0 auto;
padding: 50px 60px; padding: 20px 40px;
padding-bottom: 150px; padding-bottom: 80px;
} }
.header { .header {
display: grid; display: grid;
grid-template-columns: auto 1fr auto; grid-template-columns: auto 1fr auto;
align-items: center; align-items: center;
gap: 60px; gap: 30px;
margin-bottom: 60px; margin-bottom: 25px;
border-bottom: 4px solid #4181ff; /* GE Sky Blue */ border-bottom: 3px solid #4181ff; /* GE Sky Blue */
padding-bottom: 40px; padding-bottom: 15px;
} }
.logo-container { .logo-container {
@@ -44,7 +45,7 @@
} }
.logo-container img { .logo-container img {
height: 160px; height: 90px;
width: auto; width: auto;
} }
@@ -52,27 +53,27 @@
text-align: center; text-align: center;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 15px; gap: 5px;
} }
.location-title { .location-title {
font-size: 32px; font-size: 20px;
font-weight: 600; font-weight: 600;
color: #eaeaea; /* GE Tungsten */ color: #eaeaea; /* GE Tungsten */
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 3px; letter-spacing: 2px;
} }
.header-center h1 { .header-center h1 {
font-size: 72px; font-size: 42px;
font-weight: 700; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 3px; letter-spacing: 2px;
color: #fff; color: #fff;
} }
.clock { .clock {
font-size: 48px; font-size: 28px;
font-weight: 600; font-weight: 600;
letter-spacing: 1px; letter-spacing: 1px;
color: #4181ff; /* GE Sky Blue */ color: #4181ff; /* GE Sky Blue */
@@ -82,16 +83,16 @@
.connection-status { .connection-status {
position: fixed; position: fixed;
top: 30px; top: 0;
right: 30px; right: 0;
padding: 20px 35px; padding: 12px 20px;
border-radius: 10px; border-radius: 0 0 0 8px;
font-size: 28px; font-size: 18px;
font-weight: 700; font-weight: 700;
z-index: 1000; z-index: 1000;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 15px; gap: 10px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 1px;
} }
@@ -112,22 +113,22 @@
} }
.status-dot { .status-dot {
width: 18px; width: 12px;
height: 18px; height: 12px;
border-radius: 50%; border-radius: 50%;
background: #00003d; background: #00003d;
} }
.events-section { .events-section {
margin-bottom: 60px; margin-bottom: 20px;
} }
.section-title { .section-title {
font-size: 64px; font-size: 36px;
font-weight: 800; font-weight: 800;
margin-bottom: 40px; margin-bottom: 15px;
padding: 25px 40px; padding: 12px 25px;
border-radius: 12px; border-radius: 8px;
display: inline-block; display: inline-block;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 2px; letter-spacing: 2px;
@@ -148,11 +149,11 @@
.event-card { .event-card {
background: #fff; background: #fff;
color: #000; color: #000;
padding: 50px 60px; padding: 20px 30px;
margin-bottom: 35px; margin-bottom: 15px;
border-radius: 12px; border-radius: 8px;
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
border-left: 15px solid; border-left: 40px solid;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
@@ -178,12 +179,12 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 30px; margin-bottom: 12px;
gap: 30px; gap: 20px;
} }
.event-title { .event-title {
font-size: 56px; font-size: 32px;
font-weight: 700; font-weight: 700;
flex: 1; flex: 1;
color: #00003d; /* GE Deep Navy for text */ color: #00003d; /* GE Deep Navy for text */
@@ -191,22 +192,22 @@
} }
.event-ticket { .event-ticket {
font-size: 48px; font-size: 24px;
font-weight: 700; font-weight: 700;
background: #00003d; /* GE Deep Navy */ background: #00003d; /* GE Deep Navy */
color: #fff; color: #fff;
padding: 15px 35px; padding: 8px 20px;
border-radius: 10px; border-radius: 6px;
white-space: nowrap; white-space: nowrap;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 1px;
} }
.event-time { .event-time {
font-size: 38px; font-size: 22px;
color: #666; color: #666;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.4;
} }
.event-time strong { .event-time strong {
@@ -216,12 +217,12 @@
.no-events { .no-events {
text-align: center; text-align: center;
font-size: 52px; font-size: 32px;
font-weight: 600; font-weight: 600;
padding: 120px 60px; padding: 40px 30px;
background: rgba(234, 234, 234, 0.1); background: rgba(234, 234, 234, 0.1);
border-radius: 12px; border-radius: 8px;
margin-top: 50px; margin-top: 20px;
color: #eaeaea; /* GE Tungsten */ color: #eaeaea; /* GE Tungsten */
} }
@@ -232,31 +233,43 @@
right: 0; right: 0;
background: rgba(0, 0, 0, 0.8); background: rgba(0, 0, 0, 0.8);
text-align: center; text-align: center;
padding: 25px; padding: 12px;
font-size: 32px; font-size: 18px;
z-index: 999; z-index: 999;
font-weight: 500; font-weight: 500;
} }
.loading { .loading {
text-align: center; text-align: center;
font-size: 52px; font-size: 32px;
font-weight: 600; font-weight: 600;
padding: 120px 60px; padding: 40px 30px;
background: rgba(234, 234, 234, 0.1); background: rgba(234, 234, 234, 0.1);
border-radius: 12px; border-radius: 8px;
margin-top: 50px; margin-top: 20px;
} }
.error-message { .error-message {
background: #dc3545; background: #dc3545;
color: #fff; color: #fff;
padding: 60px; padding: 30px;
border-radius: 12px; border-radius: 8px;
text-align: center; text-align: center;
font-size: 48px; font-size: 28px;
font-weight: 700; 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> </style>
</head> </head>
@@ -338,14 +351,20 @@
const container = document.getElementById('eventsContainer'); const container = document.getElementById('eventsContainer');
let html = ''; 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) { if (data.current && data.current.length > 0) {
html += '<div class="events-section">'; html += '<div class="events-section">';
html += '<div class="section-title current">🔴 CURRENT EVENTS</div>'; 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 += ` html += `
<div class="event-card current"> <div class="event-card current" style="border-left-color: ${borderColor};">
<div class="event-header"> <div class="event-header">
<div class="event-title">${escapeHtml(event.notification)}</div> <div class="event-title">${escapeHtml(event.notification)}</div>
${event.ticketnumber ? `<div class="event-ticket">Ticket: ${escapeHtml(event.ticketnumber)}</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>'; html += '</div>';
} }
// Upcoming Events // Upcoming Events (limit to MAX_UPCOMING)
if (data.upcoming && data.upcoming.length > 0) { if (data.upcoming && data.upcoming.length > 0) {
html += '<div class="events-section">'; 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 += ` html += `
<div class="event-card upcoming"> <div class="event-card upcoming" style="border-left-color: ${borderColor};">
<div class="event-header"> <div class="event-header">
<div class="event-title">${escapeHtml(event.notification)}</div> <div class="event-title">${escapeHtml(event.notification)}</div>
${event.ticketnumber ? `<div class="event-ticket">Ticket: ${escapeHtml(event.ticketnumber)}</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>'; html += '</div>';
} }
// No events message // No events message
if ((!data.current || data.current.length === 0) && (!data.upcoming || data.upcoming.length === 0)) { 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; container.innerHTML = html;
@@ -399,6 +430,17 @@
return div.innerHTML; 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 // Fetch notifications from API
async function fetchNotifications() { async function fetchNotifications() {
try { try {
@@ -428,7 +470,7 @@
container.innerHTML = ` container.innerHTML = `
<div class="error-message"> <div class="error-message">
⚠️ Unable to load events<br> ⚠️ 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> </div>
`; `;
} }

View File

@@ -27,17 +27,20 @@ app.use(express.static('public'));
app.get('/api/notifications', async (req, res) => { app.get('/api/notifications', async (req, res) => {
try { try {
const now = new Date(); 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( const [rows] = await promisePool.query(
`SELECT notificationid, notification, starttime, endtime, ticketnumber, link, isactive `SELECT n.notificationid, n.notification, n.starttime, n.endtime, n.ticketnumber, n.link,
FROM notifications n.isactive, n.isshopfloor, nt.typename, nt.typecolor
WHERE isactive = 1 FROM notifications n
LEFT JOIN notificationtypes nt ON n.notificationtypeid = nt.notificationtypeid
WHERE n.isactive = 1
AND n.isshopfloor = 1
AND ( AND (
(starttime <= ? AND (endtime IS NULL OR endtime >= ?)) (n.starttime <= ? AND (n.endtime IS NULL OR n.endtime >= ?))
OR (starttime BETWEEN ? AND ?) OR (n.starttime BETWEEN ? AND ?)
) )
ORDER BY starttime ASC`, ORDER BY n.starttime ASC`,
[future, now, now, future] [future, now, now, future]
); );