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;
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>
`;
}

View File

@@ -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]
);